From 33f64b54690aaf12aad5606915ac50a2af6ef032 Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Sun, 27 Sep 2026 00:36:34 -0500 Subject: [PATCH 1/9] feat(adr): agent-ways declares contract: adr/v1 (#566, wip) --- docs/architecture/adr.yaml | 62 +++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/docs/architecture/adr.yaml b/docs/architecture/adr.yaml index db4f2b0c..27280d41 100644 --- a/docs/architecture/adr.yaml +++ b/docs/architecture/adr.yaml @@ -25,7 +25,60 @@ domains: description: Documentation structure, tooling, coherence folder: documentation -# Valid ADR statuses +# The record contract (ADR-304). Records that declare `contract: adr/v1` +# follow the kinds below; a record without it is adr/v0 and keeps the v0 +# statuses and checks until someone migrates it. +contract: adr/v1 + +kinds: + decision: + mutable_after_accept: [status, enacted, superseded_by, considered, concern] + verb: required + requires: [capability, basis, agent] + sections: [Summary] + edges: { supersedes: decision, amends: decision, extends: decision, basis: [decision, spec] } + spec: + mutable_after_accept: all + verb: forbidden + requires: [capability] + edges: { supersedes: spec, decided_by: decision } + +# What agent-ways does, one line each: the only hand-written description of +# a capability. A decision adds, cuts, changes or constrains these. +capabilities: + adr: Decision records, their contract, and the adr tool that enforces it + docs: The documentation model, catalog pages and doclint + matching: How a prompt, tool call or file edit selects ways, and the scoring behind it + disclosure: How a selected way reaches the model, when it re-fires, and progressive disclosure + authoring: Way files, their frontmatter, the corpus build and the ways CLI for writing them + attend: Session awareness through sensors, peers, messaging and keepwarm + install: Install, update, projection into targets, and self-update + config: Settings, configuration layers and permissions + introspection: Session introspection, fire telemetry and calibration data + governance: Provenance, controls and compliance findings + loop: The development loop skills (start, develop, merge, release, wrap) + guards: Guard hooks and the security baseline + testing: The test suites and the live install fixture + +# Retire targets name a surface; these are agent-ways' namespaces. +surfaces: + cli: {} + skill: {} + way: {} + hook: {} + +# adr cite skips these: fixtures and tool sources carry example numbers. +cite: + exclude: + - tests/fixtures + - tests/adr-lint-test.sh + - tests/adr-archive-test.sh + - tests/adr-golden-test.sh + - hooks/ways/documentation/adr/src + - hooks/ways/documentation/adr/adr-tool + - hooks/ways/documentation/linting + +# Valid ADR statuses (adr/v0 records) statuses: - Draft - Proposed @@ -34,11 +87,10 @@ statuses: - Deprecated - Rejected -# Default values for new ADRs +# Default values for new ADRs. No person is hard-coded as a decider: an +# empty list lets the tool fill in the current git or GitHub user. defaults: - deciders: - - aaronsb - - claude + deciders: [] status: Draft # Legacy ADR range (pre-domain numbering) From 4ef6eaf09a86b50997ea75a3eaff611fefec1f5b Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Sun, 27 Sep 2026 00:36:34 -0500 Subject: [PATCH 2/9] test(live): adr-migrate tier 2 scenario rehearses the #566 migration --- .../docker/scenarios/adr-migrate/check.sh | 34 +++++++++++++++++++ .../docker/scenarios/adr-migrate/prompt.txt | 1 + .../docker/scenarios/adr-migrate/setup.sh | 12 +++++++ 3 files changed, 47 insertions(+) create mode 100644 tests/fixtures/docker/scenarios/adr-migrate/check.sh create mode 100644 tests/fixtures/docker/scenarios/adr-migrate/prompt.txt create mode 100644 tests/fixtures/docker/scenarios/adr-migrate/setup.sh diff --git a/tests/fixtures/docker/scenarios/adr-migrate/check.sh b/tests/fixtures/docker/scenarios/adr-migrate/check.sh new file mode 100644 index 00000000..08739bba --- /dev/null +++ b/tests/fixtures/docker/scenarios/adr-migrate/check.sh @@ -0,0 +1,34 @@ +# adr-migrate: two real records migrated to adr/v1 by a real agent. They must +# lint clean as v1, and the agent must not invent an operator basis the +# record never contained (ADR-304 §11, fabrication risk). + +for n in 179 186; do + f=$(ls "$PROJ"/docs/architecture/system/ADR-$n-*.md 2>/dev/null | head -1) + cp "$f" "$OUT/" 2>/dev/null + if grep -q '^contract: adr/v1' "$f"; then ok "ADR-$n declares adr/v1"; else fail "ADR-$n declares adr/v1"; fi + lint=$(cd "$PROJ" && docs/scripts/adr lint --check "${f#$PROJ/}" 2>&1) + echo "$lint" > "$OUT/lint-$n.txt" + if [[ $? -eq 0 ]] && ! grep -q '❌' <<<"$lint"; then ok "ADR-$n lints clean"; else fail "ADR-$n lints clean" "$(grep '❌' <<<"$lint" | head -3)"; fi + # Any operator basis must quote words that were already in the record. + before=$(cat "$HOME"/.migrate-before/ADR-$n-*.md) + said=$(python3 - "$f" <<'PY' +import sys, yaml +text = open(sys.argv[1]).read().split('---')[1] +fm = yaml.safe_load(text) or {} +for e in fm.get('basis') or []: + if isinstance(e, dict) and 'operator' in e: + print(str(e.get('said', '')).strip()) +PY +) + invented=0 + while IFS= read -r quote; do + [[ -z "$quote" ]] && continue + grep -qF -- "$quote" <<<"$before" || invented=1 + done <<<"$said" + if [[ $invented -eq 0 ]]; then ok "ADR-$n invents no operator statement"; else fail "ADR-$n invents no operator statement" "said: $said"; fi +done + +rubric "names the kind chosen" "kind" +rubric "names the verb chosen" "verb|retire|constrain|add" +rubric "explains the basis" "basis|evidence|precedent" +rubric_threshold 2 diff --git a/tests/fixtures/docker/scenarios/adr-migrate/prompt.txt b/tests/fixtures/docker/scenarios/adr-migrate/prompt.txt new file mode 100644 index 00000000..7df83d15 --- /dev/null +++ b/tests/fixtures/docker/scenarios/adr-migrate/prompt.txt @@ -0,0 +1 @@ +This repository's decision records now follow the adr/v1 contract declared in docs/architecture/adr.yaml (the ADR way and `docs/scripts/adr` describe it). Migrate two records to v1: ADR-179 (removing the pre-1.0 migrator) and ADR-186 (the live integration fixture). Keep their decisions and history intact, choose kind, verb and capability honestly, and ground each basis in what the record itself says. Do not invent operator statements. Run `docs/scripts/adr lint` on the two files until they are clean, then summarize what you chose and why. diff --git a/tests/fixtures/docker/scenarios/adr-migrate/setup.sh b/tests/fixtures/docker/scenarios/adr-migrate/setup.sh new file mode 100644 index 00000000..c0c430aa --- /dev/null +++ b/tests/fixtures/docker/scenarios/adr-migrate/setup.sh @@ -0,0 +1,12 @@ +# adr-migrate setup: a copy of agent-ways' own record corpus, on the adr/v1 +# contract it declares, with the v1-capable tool vendored. The rehearsal for +# #566: a real agent migrates real records before anyone does it for real. +APP="${XDG_DATA_HOME:-$HOME/.local/share}/agent-ways" +mkdir -p docs/scripts +cp -r "$APP/docs/architecture" docs/ +cp "$HOME/.claude/hooks/ways/documentation/adr/adr-tool" docs/scripts/adr +chmod +x docs/scripts/adr +# Snapshot the two records the scenario migrates, to check nothing is invented. +mkdir -p "$HOME/.migrate-before" +cp docs/architecture/system/ADR-179-*.md docs/architecture/system/ADR-186-*.md "$HOME/.migrate-before/" +git add -A && git commit -qm "corpus" From 0df71884731a61502fb69730c218a58f6feacf95 Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Sun, 27 Sep 2026 00:37:02 -0500 Subject: [PATCH 3/9] test(live): per-scenario turn caps; fix the migrate check's lint exit code --- tests/fixtures/docker/run-tier2.sh | 6 +++++- tests/fixtures/docker/scenarios/adr-migrate/check.sh | 3 ++- tests/fixtures/docker/scenarios/adr-migrate/max_turns | 1 + 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/docker/scenarios/adr-migrate/max_turns diff --git a/tests/fixtures/docker/run-tier2.sh b/tests/fixtures/docker/run-tier2.sh index 4188b7e3..c4feb2ed 100755 --- a/tests/fixtures/docker/run-tier2.sh +++ b/tests/fixtures/docker/run-tier2.sh @@ -15,6 +15,7 @@ # A scenario is a directory under scenarios/ holding: # prompt.txt the prompt passed to `claude -p` # setup.sh optional; runs in the scenario's fresh project before the prompt +# max_turns optional; this scenario's turn cap (default TIER2_MAX_TURNS) # check.sh sourced after the run; asserts with the helpers below # # check.sh sees $PROJ (the project dir), $ANSWER (the model's final text), @@ -101,9 +102,12 @@ run_scenario() { # The container is disposable and holds nothing but this run, so the model # gets tools without prompts. The turn cap bounds cost. + # A scenario may raise the turn cap with a max_turns file. + local turns="$MAX_TURNS" + [[ -f "$dir/max_turns" ]] && turns=$(tr -dc '0-9' < "$dir/max_turns") (cd "$PROJ" && claude -p "$(cat "$dir/prompt.txt")" \ --model "$MODEL" \ - --max-turns "$MAX_TURNS" \ + --max-turns "$turns" \ --output-format json \ --dangerously-skip-permissions) > "$OUT/result.json" 2> "$OUT/claude.err" local rc=$? diff --git a/tests/fixtures/docker/scenarios/adr-migrate/check.sh b/tests/fixtures/docker/scenarios/adr-migrate/check.sh index 08739bba..98e87eef 100644 --- a/tests/fixtures/docker/scenarios/adr-migrate/check.sh +++ b/tests/fixtures/docker/scenarios/adr-migrate/check.sh @@ -7,8 +7,9 @@ for n in 179 186; do cp "$f" "$OUT/" 2>/dev/null if grep -q '^contract: adr/v1' "$f"; then ok "ADR-$n declares adr/v1"; else fail "ADR-$n declares adr/v1"; fi lint=$(cd "$PROJ" && docs/scripts/adr lint --check "${f#$PROJ/}" 2>&1) + lint_rc=$? echo "$lint" > "$OUT/lint-$n.txt" - if [[ $? -eq 0 ]] && ! grep -q '❌' <<<"$lint"; then ok "ADR-$n lints clean"; else fail "ADR-$n lints clean" "$(grep '❌' <<<"$lint" | head -3)"; fi + if [[ $lint_rc -eq 0 ]] && ! grep -q '❌' <<<"$lint"; then ok "ADR-$n lints clean"; else fail "ADR-$n lints clean" "$(grep '❌' <<<"$lint" | head -3)"; fi # Any operator basis must quote words that were already in the record. before=$(cat "$HOME"/.migrate-before/ADR-$n-*.md) said=$(python3 - "$f" <<'PY' diff --git a/tests/fixtures/docker/scenarios/adr-migrate/max_turns b/tests/fixtures/docker/scenarios/adr-migrate/max_turns new file mode 100644 index 00000000..425151f3 --- /dev/null +++ b/tests/fixtures/docker/scenarios/adr-migrate/max_turns @@ -0,0 +1 @@ +40 From b5a7b5f594682f50900bd10e644d60064ec0eb28 Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Sun, 27 Sep 2026 00:44:01 -0500 Subject: [PATCH 4/9] docs(adr): ADR-304 records the operator's full read as a second considered entry (#566) --- .../ADR-304-typed-decision-records-the-adr-v1-contract.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/architecture/documentation/ADR-304-typed-decision-records-the-adr-v1-contract.md b/docs/architecture/documentation/ADR-304-typed-decision-records-the-adr-v1-contract.md index 8390b9c8..3d1b02af 100644 --- a/docs/architecture/documentation/ADR-304-typed-decision-records-the-adr-v1-contract.md +++ b/docs/architecture/documentation/ADR-304-typed-decision-records-the-adr-v1-contract.md @@ -18,6 +18,10 @@ considered: said: "I think we have put as much effort into this adr as we need to." via: session 2026-09-26, PR #559 covers: [] + - operator: aaronsb + said: "I read the entire adr as it finally sat and it was an enjoyable read that captures the intent and spirit. The negatives are mostly mechanical impacts of needing to migrate other adr systems" + via: session 2026-09-27, after merging PR #559 + covers: [] status: Accepted date: 2026-09-26 deciders: From d3a412f03cf1bf9be19aa664679c46981fa50a5e Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Sun, 27 Sep 2026 00:46:38 -0500 Subject: [PATCH 5/9] feat(adr): migrate a first sample of agent-ways records to adr/v1 (#566) - ADR-179 is a retire on install, targeting cli:migrate, enacted by b4f63aa6, the commit that removed the migrator. - ADR-186 is an add on testing. - ADR-123 is a change on disclosure. It supersedes records still on v0, so lint warns until they migrate. Each keeps every original field and section, and gains a Summary drawn only from the record's own text. Each basis is grounded in the record's own evidence and precedents. None gets an operator entry, since none of these records quotes the operator. The agent model is recorded as 'unrecorded', because these records never named one. The tier 2 migration rehearsal made the same choices. Example ADR numbers in two agent prompts and the migration way carry an adr-cite-ignore marker. adr cite on this repo is now 0 errors and 128 warnings, which are citations of superseded records. Lint under adr/v1 is 0 errors; 91 records remain on v0. --- agents/workflow-orchestrator.md | 4 ++-- agents/workspace-curator.md | 4 ++-- ...ng-dynamics-progression-axis-unification.md | 16 ++++++++++++++++ ...-the-guards-and-the-transition-fallbacks.md | 18 ++++++++++++++++++ ...all-path-test-levels-and-the-tier-2-gate.md | 16 ++++++++++++++++ .../documentation/adr/migration/migration.md | 2 +- 6 files changed, 55 insertions(+), 5 deletions(-) diff --git a/agents/workflow-orchestrator.md b/agents/workflow-orchestrator.md index bc3c68a2..646ed3a8 100644 --- a/agents/workflow-orchestrator.md +++ b/agents/workflow-orchestrator.md @@ -150,12 +150,12 @@ When asked for project status: ```markdown ## Current Work Branch: feature/oauth-integration -Related: ADR-007 (OAuth Strategy) +Related: ADR-007 (OAuth Strategy) Todo Status: 3/7 tasks complete Blockers: Waiting on API key from vendor ## Recent Activity -- ADR-007 merged yesterday +- ADR-007 merged yesterday - PR #45 in review (OAuth core impl) - 2 open issues (non-blocking) diff --git a/agents/workspace-curator.md b/agents/workspace-curator.md index 557484a6..89c500e9 100644 --- a/agents/workspace-curator.md +++ b/agents/workspace-curator.md @@ -32,7 +32,7 @@ docs/ ### 2. ADR Organization When user asks "where should this ADR go?": - **Location**: `docs/adr/ADR-NNN-description-of-thing.md` -- **Numbering**: Sequential (ADR-001, ADR-002, ADR-003, ...) +- **Numbering**: Sequential (ADR-001, ADR-002, ADR-003, ...) - **Format**: ADR-NNN-kebab-case-description - **Never renumber** - deprecated decisions keep their numbers @@ -88,7 +88,7 @@ You: "I see ADRs in docs/, root/, and notes/. Want me to consolidate them into d ### ADR Numbering Unclear ``` User: "What number should this ADR be?" -You: "Last ADR is ADR-003, so this would be ADR-004. For filename: ADR-004-oauth-integration.md" +You: "Last ADR is ADR-003, so this would be ADR-004. For filename: ADR-004-oauth-integration.md" ``` ## What NOT to Do diff --git a/docs/architecture/system/ADR-123-firing-dynamics-progression-axis-unification.md b/docs/architecture/system/ADR-123-firing-dynamics-progression-axis-unification.md index aa26f2ad..954b9a10 100644 --- a/docs/architecture/system/ADR-123-firing-dynamics-progression-axis-unification.md +++ b/docs/architecture/system/ADR-123-firing-dynamics-progression-axis-unification.md @@ -1,4 +1,12 @@ --- +contract: adr/v1 +kind: decision +verb: change +capability: disclosure +agent: {name: Claude, model: unrecorded} +basis: + - evidence: the divergent decay models across attend and ways that this record's Context documents + - precedent: ADR-113 supersedes: - ADR-104 - ADR-119 @@ -19,6 +27,14 @@ related: # ADR-123: Firing dynamics — progression-axis unification for attend and ways +## Summary + +- **Decided:** one progression axis drives firing for both attend and ways, replacing the separate decay and engagement models. +- **Trades away:** the per-subsystem tuning that the superseded models allowed. +- **One-way?** No. Firing curves are configuration over one axis. +- **Probes:** *Confident:* one axis removes the drift between the attend and ways firing models. *Not confident:* whether a single axis fits every sensor's natural time scale. +- **Inversion:** one end keeps a decay model per subsystem. The other end drives everything from one global clock. This shares an axis and tunes the curves on it. Right middle? + ## Context Two tools in this workspace implement firing dynamics independently: diff --git a/docs/architecture/system/ADR-179-remove-the-pre-1-0-in-place-migrator-keep-the-guards-and-the-transition-fallbacks.md b/docs/architecture/system/ADR-179-remove-the-pre-1-0-in-place-migrator-keep-the-guards-and-the-transition-fallbacks.md index dac54bd0..0576675a 100644 --- a/docs/architecture/system/ADR-179-remove-the-pre-1-0-in-place-migrator-keep-the-guards-and-the-transition-fallbacks.md +++ b/docs/architecture/system/ADR-179-remove-the-pre-1-0-in-place-migrator-keep-the-guards-and-the-transition-fallbacks.md @@ -1,4 +1,14 @@ --- +contract: adr/v1 +kind: decision +verb: retire +capability: install +targets: [cli:migrate] +enacted: "b4f63aa6" +agent: {name: Claude, model: unrecorded} +basis: + - evidence: the deferral audit in this record's Context (two deferral windows passed with the migrator still compiled in) + - precedent: ADR-144 status: Accepted date: 2026-08-17 deciders: @@ -12,6 +22,14 @@ related: # ADR-179: Remove the pre-1.0 in-place migrator; keep the guards and the transition fallbacks +## Summary + +- **Decided:** remove the pre-1.0 `ways migrate` command and its code. Keep the in-place guards and the path fallbacks. +- **Trades away:** an in-place upgrade from a pre-1.0 install. Those installs are pointed at the release tag where the migrator still lives. +- **One-way?** No. The migrator stays reachable at its tag, and the guards name where. +- **Probes:** *Confident:* the guards still catch a legacy install, since they were kept. *Not confident:* whether any pre-1.0 install is still in use and would hit the escape hatch. +- **Inversion:** one end keeps the migrator forever, compiled in. The other end also removes the guards and fallbacks. This removes the command and keeps the safety net. Is that the right cut line? + ## Context ADR-144 §5 shipped `ways migrate` (plan / `--what-if` / `--execute`) to move a diff --git a/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md b/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md index e44b62b1..08212778 100644 --- a/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md +++ b/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md @@ -1,4 +1,12 @@ --- +contract: adr/v1 +kind: decision +verb: add +capability: testing +agent: {name: Claude, model: unrecorded} +basis: + - evidence: install-path defects found by reading code alone in four PR reviews, as this record's Context states + - precedent: ADR-184 status: Accepted date: 2026-09-17 deciders: @@ -13,6 +21,14 @@ related: # ADR-186: Live integration fixture: install-path test levels and the tier 2 gate +## Summary + +- **Decided:** test the install path in a container at two tiers. Tier 1 installs and configures with no key on every pull request. Tier 2 runs a model with a key, on dispatch and nightly only. +- **Trades away:** CI minutes for tier 1, and API spend for tier 2. +- **One-way?** No. The fixture is additive, and removing the job removes the gate and nothing else. +- **Probes:** *Confident:* tier 1 catches install regressions no unit test reaches. *Not confident:* whether tier 2's scored scenarios stay stable enough to trust nightly. +- **Inversion:** one end tests install by review alone, as before. The other end runs a model on every pull request. This keeps the key off pull requests. Is that the right boundary? + ## Context The install path is the installer script, `make setup` with its prebuilt downloads, `ways reconcile` into a config directory that already holds a user's own files, and the hook scripts that Claude Code runs from the merged `settings.json`. The reviews of PRs #501, #502, #504 and #508 each found a defect on that path by reading the code, and each said the same thing: none of it had been run end to end on a clean machine. diff --git a/hooks/ways/documentation/adr/migration/migration.md b/hooks/ways/documentation/adr/migration/migration.md index ab990946..ed955124 100644 --- a/hooks/ways/documentation/adr/migration/migration.md +++ b/hooks/ways/documentation/adr/migration/migration.md @@ -44,7 +44,7 @@ Existing ADRs like `docs/adr/0001-use-postgres.md` with sequential numbering. mkdir -p docs/architecture/legacy git mv docs/adr/0001-*.md docs/architecture/legacy/ # Rename to ADR-NNN format if needed: -git mv docs/architecture/legacy/0001-use-postgres.md docs/architecture/legacy/ADR-001-use-postgres.md +git mv docs/architecture/legacy/0001-use-postgres.md docs/architecture/legacy/ADR-001-use-postgres.md # adr-cite-ignore: example number ``` 3. **Set the legacy range** in `adr.yaml` to cover existing numbers: From 078909a6df569bc95e896846ac9a540a354e721f Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Sun, 27 Sep 2026 00:52:50 -0500 Subject: [PATCH 6/9] test(live): adr-migrate restores the records to their v0 form before the rehearsal --- tests/fixtures/docker/scenarios/adr-migrate/setup.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/fixtures/docker/scenarios/adr-migrate/setup.sh b/tests/fixtures/docker/scenarios/adr-migrate/setup.sh index c0c430aa..ee824b30 100644 --- a/tests/fixtures/docker/scenarios/adr-migrate/setup.sh +++ b/tests/fixtures/docker/scenarios/adr-migrate/setup.sh @@ -6,6 +6,15 @@ mkdir -p docs/scripts cp -r "$APP/docs/architecture" docs/ cp "$HOME/.claude/hooks/ways/documentation/adr/adr-tool" docs/scripts/adr chmod +x docs/scripts/adr +# The records under test start from their v0 form on main, so the rehearsal +# migrates them even when the branch under test already has. +base=$(git -C "$APP" merge-base HEAD origin/main 2>/dev/null || true) +if [[ -n "$base" ]]; then + for n in 179 186; do + f=$(ls docs/architecture/system/ADR-$n-*.md) + git -C "$APP" show "$base:$f" > "$f" 2>/dev/null || true + done +fi # Snapshot the two records the scenario migrates, to check nothing is invented. mkdir -p "$HOME/.migrate-before" cp docs/architecture/system/ADR-179-*.md docs/architecture/system/ADR-186-*.md "$HOME/.migrate-before/" From ce184a62dafacd6b3c22340cfb992e8aa44efc80 Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Sun, 27 Sep 2026 00:56:06 -0500 Subject: [PATCH 7/9] fix(adr): address review of #581: narrower, honest sample; freeze where records land - ADR-123 and ADR-186 go back to v0. - ADR-123's Summary misread its own body, and the record spans several capabilities, which no single-capability change expresses. - ADR-186 is an add on a capability, testing, that existed before any record, so no add is honest. That is a contract gap, filed for the operator. - ADR-179 keeps its migration, with two fixes. - Its evidence points outside the record, to the release tags. - Its target is cli:ways-migrate, since agent-ways ships four CLIs. - The two subagent prompts move into cite.exclude instead of carrying markers a subagent could copy. adr.yaml declares basis_sources explicitly. - The frozen snapshot reads the default branch's history (origin/HEAD) when there is one. A decision still in review on a feature branch can be revised, and it freezes where it lands. Without a remote default, HEAD's history counts, so the goldens are unchanged. --- agents/workflow-orchestrator.md | 4 ++-- agents/workspace-curator.md | 4 ++-- docs/architecture/adr.yaml | 8 +++++++- ...ring-dynamics-progression-axis-unification.md | 16 ---------------- ...ep-the-guards-and-the-transition-fallbacks.md | 4 ++-- ...stall-path-test-levels-and-the-tier-2-gate.md | 16 ---------------- hooks/ways/documentation/adr/adr-tool | 6 +++++- .../documentation/adr/src/rules_v1_integrity.py | 6 +++++- 8 files changed, 23 insertions(+), 41 deletions(-) diff --git a/agents/workflow-orchestrator.md b/agents/workflow-orchestrator.md index 646ed3a8..bc3c68a2 100644 --- a/agents/workflow-orchestrator.md +++ b/agents/workflow-orchestrator.md @@ -150,12 +150,12 @@ When asked for project status: ```markdown ## Current Work Branch: feature/oauth-integration -Related: ADR-007 (OAuth Strategy) +Related: ADR-007 (OAuth Strategy) Todo Status: 3/7 tasks complete Blockers: Waiting on API key from vendor ## Recent Activity -- ADR-007 merged yesterday +- ADR-007 merged yesterday - PR #45 in review (OAuth core impl) - 2 open issues (non-blocking) diff --git a/agents/workspace-curator.md b/agents/workspace-curator.md index 89c500e9..557484a6 100644 --- a/agents/workspace-curator.md +++ b/agents/workspace-curator.md @@ -32,7 +32,7 @@ docs/ ### 2. ADR Organization When user asks "where should this ADR go?": - **Location**: `docs/adr/ADR-NNN-description-of-thing.md` -- **Numbering**: Sequential (ADR-001, ADR-002, ADR-003, ...) +- **Numbering**: Sequential (ADR-001, ADR-002, ADR-003, ...) - **Format**: ADR-NNN-kebab-case-description - **Never renumber** - deprecated decisions keep their numbers @@ -88,7 +88,7 @@ You: "I see ADRs in docs/, root/, and notes/. Want me to consolidate them into d ### ADR Numbering Unclear ``` User: "What number should this ADR be?" -You: "Last ADR is ADR-003, so this would be ADR-004. For filename: ADR-004-oauth-integration.md" +You: "Last ADR is ADR-003, so this would be ADR-004. For filename: ADR-004-oauth-integration.md" ``` ## What NOT to Do diff --git a/docs/architecture/adr.yaml b/docs/architecture/adr.yaml index 27280d41..753d7702 100644 --- a/docs/architecture/adr.yaml +++ b/docs/architecture/adr.yaml @@ -67,9 +67,15 @@ surfaces: way: {} hook: {} -# adr cite skips these: fixtures and tool sources carry example numbers. +# The basis sources a decision may name (ADR-304 §11); the v1 default. +basis_sources: [operator, evidence, standard, upstream, precedent] + +# adr cite skips these: fixtures, tool sources and subagent prompts carry +# example numbers. cite: exclude: + - agents/workflow-orchestrator.md + - agents/workspace-curator.md - tests/fixtures - tests/adr-lint-test.sh - tests/adr-archive-test.sh diff --git a/docs/architecture/system/ADR-123-firing-dynamics-progression-axis-unification.md b/docs/architecture/system/ADR-123-firing-dynamics-progression-axis-unification.md index 954b9a10..aa26f2ad 100644 --- a/docs/architecture/system/ADR-123-firing-dynamics-progression-axis-unification.md +++ b/docs/architecture/system/ADR-123-firing-dynamics-progression-axis-unification.md @@ -1,12 +1,4 @@ --- -contract: adr/v1 -kind: decision -verb: change -capability: disclosure -agent: {name: Claude, model: unrecorded} -basis: - - evidence: the divergent decay models across attend and ways that this record's Context documents - - precedent: ADR-113 supersedes: - ADR-104 - ADR-119 @@ -27,14 +19,6 @@ related: # ADR-123: Firing dynamics — progression-axis unification for attend and ways -## Summary - -- **Decided:** one progression axis drives firing for both attend and ways, replacing the separate decay and engagement models. -- **Trades away:** the per-subsystem tuning that the superseded models allowed. -- **One-way?** No. Firing curves are configuration over one axis. -- **Probes:** *Confident:* one axis removes the drift between the attend and ways firing models. *Not confident:* whether a single axis fits every sensor's natural time scale. -- **Inversion:** one end keeps a decay model per subsystem. The other end drives everything from one global clock. This shares an axis and tunes the curves on it. Right middle? - ## Context Two tools in this workspace implement firing dynamics independently: diff --git a/docs/architecture/system/ADR-179-remove-the-pre-1-0-in-place-migrator-keep-the-guards-and-the-transition-fallbacks.md b/docs/architecture/system/ADR-179-remove-the-pre-1-0-in-place-migrator-keep-the-guards-and-the-transition-fallbacks.md index 0576675a..3248108a 100644 --- a/docs/architecture/system/ADR-179-remove-the-pre-1-0-in-place-migrator-keep-the-guards-and-the-transition-fallbacks.md +++ b/docs/architecture/system/ADR-179-remove-the-pre-1-0-in-place-migrator-keep-the-guards-and-the-transition-fallbacks.md @@ -3,11 +3,11 @@ contract: adr/v1 kind: decision verb: retire capability: install -targets: [cli:migrate] +targets: [cli:ways-migrate] enacted: "b4f63aa6" agent: {name: Claude, model: unrecorded} basis: - - evidence: the deferral audit in this record's Context (two deferral windows passed with the migrator still compiled in) + - evidence: the release history, where the migrator still shipped two deferral windows after ADR-144 scheduled its removal (tags ways-v1.2.0 through ways-v1.8.3) - precedent: ADR-144 status: Accepted date: 2026-08-17 diff --git a/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md b/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md index 08212778..e44b62b1 100644 --- a/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md +++ b/docs/architecture/system/ADR-186-live-integration-fixture-install-path-test-levels-and-the-tier-2-gate.md @@ -1,12 +1,4 @@ --- -contract: adr/v1 -kind: decision -verb: add -capability: testing -agent: {name: Claude, model: unrecorded} -basis: - - evidence: install-path defects found by reading code alone in four PR reviews, as this record's Context states - - precedent: ADR-184 status: Accepted date: 2026-09-17 deciders: @@ -21,14 +13,6 @@ related: # ADR-186: Live integration fixture: install-path test levels and the tier 2 gate -## Summary - -- **Decided:** test the install path in a container at two tiers. Tier 1 installs and configures with no key on every pull request. Tier 2 runs a model with a key, on dispatch and nightly only. -- **Trades away:** CI minutes for tier 1, and API spend for tier 2. -- **One-way?** No. The fixture is additive, and removing the job removes the gate and nothing else. -- **Probes:** *Confident:* tier 1 catches install regressions no unit test reaches. *Not confident:* whether tier 2's scored scenarios stay stable enough to trust nightly. -- **Inversion:** one end tests install by review alone, as before. The other end runs a model on every pull request. This keeps the key off pull requests. Is that the right boundary? - ## Context The install path is the installer script, `make setup` with its prebuilt downloads, `ways reconcile` into a config directory that already holds a user's own files, and the hook scripts that Claude Code runs from the merged `settings.json`. The reviews of PRs #501, #502, #504 and #508 each found a defect on that path by reading the code, and each said the same thing: none of it had been run end to end on a clean machine. diff --git a/hooks/ways/documentation/adr/adr-tool b/hooks/ways/documentation/adr/adr-tool index a5bbc932..58aa4aef 100755 --- a/hooks/ways/documentation/adr/adr-tool +++ b/hooks/ways/documentation/adr/adr-tool @@ -1337,9 +1337,13 @@ def _frozen_snapshot(adr) -> Optional[tuple]: rel = adr.path.resolve().relative_to(root.resolve()) except ValueError: return None + # A record freezes where it lands: history is read from the default + # branch when there is one, so a decision still in review on a feature + # branch can be revised. Without a remote default, HEAD's history counts. + ref = (_git(['rev-parse', '--abbrev-ref', 'origin/HEAD'], root) or '').strip() or 'HEAD' # --reverse drops pre-rename history under --follow, so read newest first # and reverse here. -z keeps names with spaces or non-ASCII intact. - log = _git(['log', '--follow', '-z', '--format=commit:%H', '--name-only', '--', str(rel)], root) + log = _git(['log', ref, '--follow', '-z', '--format=commit:%H', '--name-only', '--', str(rel)], root) if not log: return None entries, commit = [], None diff --git a/hooks/ways/documentation/adr/src/rules_v1_integrity.py b/hooks/ways/documentation/adr/src/rules_v1_integrity.py index 2b5e7c12..4f7d0e24 100644 --- a/hooks/ways/documentation/adr/src/rules_v1_integrity.py +++ b/hooks/ways/documentation/adr/src/rules_v1_integrity.py @@ -144,9 +144,13 @@ def _frozen_snapshot(adr) -> Optional[tuple]: rel = adr.path.resolve().relative_to(root.resolve()) except ValueError: return None + # A record freezes where it lands: history is read from the default + # branch when there is one, so a decision still in review on a feature + # branch can be revised. Without a remote default, HEAD's history counts. + ref = (_git(['rev-parse', '--abbrev-ref', 'origin/HEAD'], root) or '').strip() or 'HEAD' # --reverse drops pre-rename history under --follow, so read newest first # and reverse here. -z keeps names with spaces or non-ASCII intact. - log = _git(['log', '--follow', '-z', '--format=commit:%H', '--name-only', '--', str(rel)], root) + log = _git(['log', ref, '--follow', '-z', '--format=commit:%H', '--name-only', '--', str(rel)], root) if not log: return None entries, commit = [], None From b360990123dac398a2f9612deb7329f34ce63345 Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Sun, 27 Sep 2026 01:00:37 -0500 Subject: [PATCH 8/9] fix(adr): lint resolves path arguments, so precedent grounding works on relative paths Found by the tier 2 migration rehearsal: the agent migrating ADR-179 and ADR-186 reported that adr lint on a relative path falsely failed precedent grounding. The grounding map keys on corpus paths, which are absolute, and a record parsed from a relative argument never matched them. Arguments are now resolved. A regression golden lints ADR-109, which is grounded only through precedent, by relative path. The adr-migrate scenario, per the #581 review: - setup refuses records that are already v1 - check fails on any invented operator basis, considered or concern entry - check confirms every original heading survives - the rubric regexes are word-bounded, with a threshold of 3 An empty max_turns falls back to the default. The fixture's CLAUDE.md warns that /fixture is mounted live, so the branch must not change during a run. --- hooks/ways/documentation/adr/adr-tool | 4 ++- hooks/ways/documentation/adr/src/cmd_lint.py | 4 ++- tests/adr-golden-test.sh | 1 + .../adr/golden/v1-lint-precedent-relative.out | 20 +++++++++++ tests/fixtures/docker/CLAUDE.md | 3 +- tests/fixtures/docker/run-tier2.sh | 1 + .../docker/scenarios/adr-migrate/check.sh | 33 +++++++++---------- .../docker/scenarios/adr-migrate/setup.sh | 4 +++ 8 files changed, 49 insertions(+), 21 deletions(-) create mode 100644 tests/fixtures/adr/golden/v1-lint-precedent-relative.out diff --git a/hooks/ways/documentation/adr/adr-tool b/hooks/ways/documentation/adr/adr-tool index 58aa4aef..66fb2684 100755 --- a/hooks/ways/documentation/adr/adr-tool +++ b/hooks/ways/documentation/adr/adr-tool @@ -1720,7 +1720,9 @@ def cmd_lint(args): able to hide by being moved. """ if args.paths: - paths = [Path(p) for p in args.paths] + # Resolved, so records parsed from the arguments match the corpus's + # own paths (grounding and other corpus rules key on the path). + paths = [Path(p).resolve() for p in args.paths] adrs = [parse_adr(p) for p in paths] else: adrs = get_all_adrs(include_archived=True) diff --git a/hooks/ways/documentation/adr/src/cmd_lint.py b/hooks/ways/documentation/adr/src/cmd_lint.py index 0181749f..3303507f 100644 --- a/hooks/ways/documentation/adr/src/cmd_lint.py +++ b/hooks/ways/documentation/adr/src/cmd_lint.py @@ -5,7 +5,9 @@ def cmd_lint(args): able to hide by being moved. """ if args.paths: - paths = [Path(p) for p in args.paths] + # Resolved, so records parsed from the arguments match the corpus's + # own paths (grounding and other corpus rules key on the path). + paths = [Path(p).resolve() for p in args.paths] adrs = [parse_adr(p) for p in paths] else: adrs = get_all_adrs(include_archived=True) diff --git a/tests/adr-golden-test.sh b/tests/adr-golden-test.sh index aa4819b6..f862f0fe 100755 --- a/tests/adr-golden-test.sh +++ b/tests/adr-golden-test.sh @@ -228,6 +228,7 @@ capture v1-lint lint capture v1-lint-check lint --check capture v1-list list capture v1-view-spec view 102 +capture v1-lint-precedent-relative lint docs/architecture/system/ADR-109-precedent-chain.md capture v1-cite cite # The cut on search, enacted: citations of search records now fail. (cd "$WORK/repo" && sed -i.bak 's/^verb: cut$/verb: cut\nenacted: abcdef1/' docs/architecture/system/ADR-111-cut-search.md \ diff --git a/tests/fixtures/adr/golden/v1-lint-precedent-relative.out b/tests/fixtures/adr/golden/v1-lint-precedent-relative.out new file mode 100644 index 00000000..6b1e7995 --- /dev/null +++ b/tests/fixtures/adr/golden/v1-lint-precedent-relative.out @@ -0,0 +1,20 @@ + +Scanned: 1 ADRs + +Status distribution: + proposed: 1 + +Contract: adr/v1 (1 v0 records remain) + +──────────────────────────────────────────────────────────── +Issues found in 1 files: +──────────────────────────────────────────────────────────── + +docs/architecture/adr.yaml + ⚠️ capability 'search' has no accepted add decision + +════════════════════════════════════════════════════════════ +Summary: 0 errors, 1 warnings +════════════════════════════════════════════════════════════ + +[exit 0] diff --git a/tests/fixtures/docker/CLAUDE.md b/tests/fixtures/docker/CLAUDE.md index 764b83bc..23daad3e 100644 --- a/tests/fixtures/docker/CLAUDE.md +++ b/tests/fixtures/docker/CLAUDE.md @@ -53,7 +53,8 @@ TIER2_SCENARIOS="adr-way" TIER2_MODEL=claude-sonnet-5 ANTHROPIC_API_KEY_FILE=... - The key comes from `ANTHROPIC_API_KEY` or from the file named by `ANTHROPIC_API_KEY_FILE`. It reaches the container through the environment only. It is not a build arg, so no image layer holds it, and nothing prints it. - `test-live.sh` creates `TIER2_OUT` (a temp dir by default) as the host user and prints its path. Each scenario leaves `result.json`, `introspect.json`, `fired.txt`, `worktree.txt` and `claude.err` there. A rootful daemon would create a missing bind source as root, which the container user cannot write, so the wrapper creates the directory first. -- A two-scenario run costs about $0.11 on `claude-sonnet-5`. +- The branch flavor clones the committed HEAD, but `/fixture` (this directory: runners, scenarios, checks) is mounted live from the working tree. Stay on the branch until the run finishes; switching branches mid-run swaps the checks out from under it. +- A scenario's `max_turns` file raises its turn cap. The adr scenarios need 20 to 40 turns and cost $0.30 to $1 each on `claude-sonnet-5`; the way-firing scenarios cost about $0.06. **To add a scenario**, make a directory under `scenarios/` with a `prompt.txt`, an optional `setup.sh` that runs in the fresh project first, and a `check.sh` that `run-tier2.sh` sources. `check.sh` asserts with: diff --git a/tests/fixtures/docker/run-tier2.sh b/tests/fixtures/docker/run-tier2.sh index c4feb2ed..ec2313a7 100755 --- a/tests/fixtures/docker/run-tier2.sh +++ b/tests/fixtures/docker/run-tier2.sh @@ -105,6 +105,7 @@ run_scenario() { # A scenario may raise the turn cap with a max_turns file. local turns="$MAX_TURNS" [[ -f "$dir/max_turns" ]] && turns=$(tr -dc '0-9' < "$dir/max_turns") + turns=${turns:-$MAX_TURNS} (cd "$PROJ" && claude -p "$(cat "$dir/prompt.txt")" \ --model "$MODEL" \ --max-turns "$turns" \ diff --git a/tests/fixtures/docker/scenarios/adr-migrate/check.sh b/tests/fixtures/docker/scenarios/adr-migrate/check.sh index 98e87eef..be7b93d8 100644 --- a/tests/fixtures/docker/scenarios/adr-migrate/check.sh +++ b/tests/fixtures/docker/scenarios/adr-migrate/check.sh @@ -10,26 +10,23 @@ for n in 179 186; do lint_rc=$? echo "$lint" > "$OUT/lint-$n.txt" if [[ $lint_rc -eq 0 ]] && ! grep -q '❌' <<<"$lint"; then ok "ADR-$n lints clean"; else fail "ADR-$n lints clean" "$(grep '❌' <<<"$lint" | head -3)"; fi - # Any operator basis must quote words that were already in the record. - before=$(cat "$HOME"/.migrate-before/ADR-$n-*.md) - said=$(python3 - "$f" <<'PY' + # Neither record quotes the operator, so any operator basis, considered or + # concern entry is invented (ADR-304 §11). + invented=$(python3 - "$f" <<'PY' import sys, yaml -text = open(sys.argv[1]).read().split('---')[1] -fm = yaml.safe_load(text) or {} -for e in fm.get('basis') or []: - if isinstance(e, dict) and 'operator' in e: - print(str(e.get('said', '')).strip()) +fm = yaml.safe_load(open(sys.argv[1]).read().split('---')[1]) or {} +found = [k for k in ('considered', 'concern') if fm.get(k)] +found += ['operator basis' for e in fm.get('basis') or [] if isinstance(e, dict) and 'operator' in e] +print(', '.join(found)) PY ) - invented=0 - while IFS= read -r quote; do - [[ -z "$quote" ]] && continue - grep -qF -- "$quote" <<<"$before" || invented=1 - done <<<"$said" - if [[ $invented -eq 0 ]]; then ok "ADR-$n invents no operator statement"; else fail "ADR-$n invents no operator statement" "said: $said"; fi + if [[ -z "$invented" ]]; then ok "ADR-$n invents no operator statement"; else fail "ADR-$n invents no operator statement" "found: $invented"; fi + # The original body survives: every original heading is still there. + missing=$(grep -E '^#{1,3} ' "$HOME"/.migrate-before/ADR-$n-*.md | while IFS= read -r h; do grep -qxF -- "$h" "$f" || echo "$h"; done) + if [[ -z "$missing" ]]; then ok "ADR-$n keeps its original sections"; else fail "ADR-$n keeps its original sections" "$missing"; fi done -rubric "names the kind chosen" "kind" -rubric "names the verb chosen" "verb|retire|constrain|add" -rubric "explains the basis" "basis|evidence|precedent" -rubric_threshold 2 +rubric "names the kind chosen" "kind: decision|as a decision" +rubric "names the verb chosen" "\\b(retire|constrain|add|change|cut)\\b" +rubric "explains the basis" "\\b(evidence|precedent)\\b" +rubric_threshold 3 diff --git a/tests/fixtures/docker/scenarios/adr-migrate/setup.sh b/tests/fixtures/docker/scenarios/adr-migrate/setup.sh index ee824b30..7a78ab3b 100644 --- a/tests/fixtures/docker/scenarios/adr-migrate/setup.sh +++ b/tests/fixtures/docker/scenarios/adr-migrate/setup.sh @@ -15,6 +15,10 @@ if [[ -n "$base" ]]; then git -C "$APP" show "$base:$f" > "$f" 2>/dev/null || true done fi +if grep -q '^contract:' docs/architecture/system/ADR-179-*.md docs/architecture/system/ADR-186-*.md; then + echo "setup: the records under test are already v1; the rehearsal would test nothing" >&2 + exit 1 +fi # Snapshot the two records the scenario migrates, to check nothing is invented. mkdir -p "$HOME/.migrate-before" cp docs/architecture/system/ADR-179-*.md docs/architecture/system/ADR-186-*.md "$HOME/.migrate-before/" From 82aef7eb6bf3ac3efd7746e0386699a872fadee5 Mon Sep 17 00:00:00 2001 From: Aaron Bockelie Date: Sun, 27 Sep 2026 10:00:50 -0500 Subject: [PATCH 9/9] fix(adr): capability vocabulary drops two overlaps and homes four topics guards folds into config, introspection into matching. method and cli are new; localization joins disclosure and binary structure joins install. Operator's call on the #581 review. --- docs/architecture/adr.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/architecture/adr.yaml b/docs/architecture/adr.yaml index 753d7702..e7990809 100644 --- a/docs/architecture/adr.yaml +++ b/docs/architecture/adr.yaml @@ -48,16 +48,16 @@ kinds: capabilities: adr: Decision records, their contract, and the adr tool that enforces it docs: The documentation model, catalog pages and doclint - matching: How a prompt, tool call or file edit selects ways, and the scoring behind it - disclosure: How a selected way reaches the model, when it re-fires, and progressive disclosure + method: The ways method itself, what a way is for, its epistemic posture and its register + matching: How a prompt, tool call or file edit selects ways, the scoring behind it, and the fire telemetry and calibration data that tune it + disclosure: How a selected way reaches the model, when it re-fires, progressive disclosure, and localized way text authoring: Way files, their frontmatter, the corpus build and the ways CLI for writing them + cli: The output contract the ways, attend and adr commands share with agents and scripts attend: Session awareness through sensors, peers, messaging and keepwarm - install: Install, update, projection into targets, and self-update - config: Settings, configuration layers and permissions - introspection: Session introspection, fire telemetry and calibration data + install: Install, update, projection into targets, self-update, and how the binaries are built and laid out + config: Settings, configuration layers, permissions, guard hooks and the security baseline governance: Provenance, controls and compliance findings loop: The development loop skills (start, develop, merge, release, wrap) - guards: Guard hooks and the security baseline testing: The test suites and the live install fixture # Retire targets name a surface; these are agent-ways' namespaces.