Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 63 additions & 5 deletions docs/architecture/adr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,66 @@ 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
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, 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)
testing: The test suites and the live install fixture

# Retire targets name a surface; these are agent-ways' namespaces.
surfaces:
cli: {}
skill: {}
way: {}
hook: {}

# 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
- 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
Expand All @@ -34,11 +93,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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
---
contract: adr/v1
kind: decision
verb: retire
capability: install
targets: [cli:ways-migrate]
enacted: "b4f63aa6"
agent: {name: Claude, model: unrecorded}
basis:
- 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
deciders:
Expand All @@ -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
Expand Down
10 changes: 8 additions & 2 deletions hooks/ways/documentation/adr/adr-tool
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1716,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)
Expand Down
2 changes: 1 addition & 1 deletion hooks/ways/documentation/adr/migration/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 3 additions & 1 deletion hooks/ways/documentation/adr/src/cmd_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion hooks/ways/documentation/adr/src/rules_v1_integrity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/adr-golden-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
20 changes: 20 additions & 0 deletions tests/fixtures/adr/golden/v1-lint-precedent-relative.out
Original file line number Diff line number Diff line change
@@ -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]
3 changes: 2 additions & 1 deletion tests/fixtures/docker/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
7 changes: 6 additions & 1 deletion tests/fixtures/docker/run-tier2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -101,9 +102,13 @@ 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")
turns=${turns:-$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=$?
Expand Down
32 changes: 32 additions & 0 deletions tests/fixtures/docker/scenarios/adr-migrate/check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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)
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
# 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
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
)
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: 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
1 change: 1 addition & 0 deletions tests/fixtures/docker/scenarios/adr-migrate/max_turns
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
40
1 change: 1 addition & 0 deletions tests/fixtures/docker/scenarios/adr-migrate/prompt.txt
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions tests/fixtures/docker/scenarios/adr-migrate/setup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# 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
# 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
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/"
git add -A && git commit -qm "corpus"
Loading