@windyroad/architect 0.20.3 → 0.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/package.json +6 -2
  4. package/agents/test/architect-codex-eval-contract.bats +0 -33
  5. package/agents/test/architect-needs-direction-verdict.bats +0 -64
  6. package/agents/test/architect-output-formatting.bats +0 -26
  7. package/agents/test/architect-performance-review.bats +0 -76
  8. package/agents/test/architect-pre-edit-review-mode.bats +0 -50
  9. package/agents/test/architect-unratified-dependency-verdict.bats +0 -55
  10. package/hooks/test/architect-compendium-update-entry.bats +0 -299
  11. package/hooks/test/architect-detect-once-per-session.bats +0 -106
  12. package/hooks/test/architect-detect-scope.bats +0 -56
  13. package/hooks/test/architect-enforce-scope.bats +0 -124
  14. package/hooks/test/architect-gate.bats +0 -102
  15. package/hooks/test/architect-mark-reviewed-verdict-grep.bats +0 -113
  16. package/hooks/test/architect-mark-reviewed.bats +0 -26
  17. package/hooks/test/architect-marker-only-exempt.bats +0 -183
  18. package/hooks/test/architect-no-stop-hook.bats +0 -17
  19. package/hooks/test/architect-oversight-marker-discipline.bats +0 -245
  20. package/hooks/test/architect-oversight-nudge.bats +0 -75
  21. package/hooks/test/architect-project-root.bats +0 -81
  22. package/hooks/test/architect-readme-pairing-check.bats +0 -126
  23. package/hooks/test/architect-slide-marker.bats +0 -92
  24. package/hooks/test/codex-agent-completion.bats +0 -64
  25. package/hooks/test/slide-marker-on-subprocess-return.bats +0 -118
  26. package/hooks/test/substance-aware-drift.bats +0 -243
  27. package/scripts/test/check-amends-backreference.bats +0 -68
  28. package/scripts/test/codex-pack-and-agent.bats +0 -86
  29. package/scripts/test/detect-unoversighted.bats +0 -139
  30. package/scripts/test/generate-decisions-compendium.bats +0 -210
  31. package/scripts/test/is-decision-unconfirmed.bats +0 -143
  32. package/skills/capture-adr/test/capture-adr.bats +0 -362
  33. package/skills/create-adr/test/create-adr-adr-044-contract.bats +0 -185
  34. package/skills/create-adr/test/create-adr-codex-eval-contract.bats +0 -34
  35. package/skills/create-adr/test/create-adr-decision-boundary.bats +0 -49
  36. package/skills/create-adr/test/create-adr-id-collision-guard.bats +0 -35
  37. package/skills/create-adr/test/create-adr-next-id-origin-lookup.bats +0 -45
  38. package/skills/create-adr/test/create-adr-rename-restage-reminder.bats +0 -38
  39. package/skills/create-adr/test/create-adr-substance-confirm-pattern.bats +0 -169
  40. package/skills/review-design/test/review-design-codex-contract.bats +0 -22
@@ -123,5 +123,5 @@
123
123
  }
124
124
  },
125
125
  "name": "wr-architect",
126
- "version": "0.20.3"
126
+ "version": "0.20.4"
127
127
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wr-architect",
3
- "version": "0.20.3",
3
+ "version": "0.20.4",
4
4
  "description": "Architecture decision enforcement for AI coding agents",
5
5
  "author": {
6
6
  "name": "Windy Road Technology",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windyroad/architect",
3
- "version": "0.20.3",
3
+ "version": "0.20.4",
4
4
  "description": "Architecture decision enforcement for AI coding agents",
5
5
  "bin": {
6
6
  "windyroad-architect": "./bin/install.mjs"
@@ -33,6 +33,10 @@
33
33
  ".codex-plugin/",
34
34
  "lib/",
35
35
  "!skills/*/eval/",
36
- "!agents/eval/"
36
+ "!agents/eval/",
37
+ "!agents/test/",
38
+ "!hooks/test/",
39
+ "!scripts/test/",
40
+ "!skills/*/test/"
37
41
  ]
38
42
  }
@@ -1,33 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- setup() {
4
- REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../.." && pwd)"
5
- EVAL_DIR="$REPO_ROOT/packages/architect/agents/eval"
6
- }
7
-
8
- @test "architect Codex agent eval covers required verdict classes" {
9
- [ -f "$EVAL_DIR/promptfooconfig.codex.yaml" ]
10
- run grep -n "PASS" "$EVAL_DIR/promptfooconfig.codex.yaml"
11
- [ "$status" -eq 0 ]
12
- run grep -n "ISSUES FOUND" "$EVAL_DIR/promptfooconfig.codex.yaml"
13
- [ "$status" -eq 0 ]
14
- run grep -n "NEEDS DIRECTION" "$EVAL_DIR/promptfooconfig.codex.yaml"
15
- [ "$status" -eq 0 ]
16
- run grep -n "Unratified Dependency" "$EVAL_DIR/promptfooconfig.codex.yaml"
17
- [ "$status" -eq 0 ]
18
- }
19
-
20
- @test "architect Codex agent eval runner uses packed install and exact custom agent" {
21
- run grep -n 'npm pack' "$EVAL_DIR/run-codex-agent-eval.sh"
22
- [ "$status" -eq 0 ]
23
- run grep -n 'windyroad-architect --runtime codex --scope user' "$EVAL_DIR/run-codex-agent-eval.sh"
24
- [ "$status" -eq 0 ]
25
- run grep -n "codex exec" "$EVAL_DIR/run-codex-agent-eval.sh"
26
- [ "$status" -eq 0 ]
27
- run grep -n 'agents/wr-architect-agent.toml' "$EVAL_DIR/run-codex-agent-eval.sh"
28
- [ "$status" -eq 0 ]
29
- run grep -n 'wr-architect:agent' "$EVAL_DIR/run-codex-agent-eval.sh"
30
- [ "$status" -eq 0 ]
31
- run grep -n 'git -C "$TMP_REPO" init' "$EVAL_DIR/run-codex-agent-eval.sh"
32
- [ "$status" -eq 0 ]
33
- }
@@ -1,64 +0,0 @@
1
- #!/usr/bin/env bats
2
- # Doc-lint guard: architect agent.md must carry the NEEDS DIRECTION verdict
3
- # type (ADR-064) so the architect names the question + viable options for an
4
- # unpinned 2+-option decision instead of auto-picking or prose-asking, and the
5
- # main agent translates it into an AskUserQuestion.
6
- #
7
- # tdd-review: structural-permitted (justification: P176 — agent behaviour is
8
- # prompt-driven with no skill-invocation harness to exercise the verdict
9
- # behaviourally; ADR-052 Surface 2 structural-justified case, NOT an ADR-005
10
- # Permitted Exception — ADR-052 narrows ADR-005 to exclude prose-doc greps).
11
- # When P176 lands, upgrade to a behavioural test per ADR-064 Confirmation item 2.
12
- #
13
- # Cross-reference:
14
- # P283 (architect should AskUserQuestion when recording a new decision)
15
- # ADR-064 (Architect Needs-Direction verdict; main agent owns the AskUserQuestion)
16
- # ADR-052 Surface 2 (structural-justified verdict) + P176 (harness gap)
17
- # @jtbd JTBD-001 (enforce governance without slowing down)
18
-
19
- setup() {
20
- AGENT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
21
- AGENT_FILE="${AGENT_DIR}/agent.md"
22
- }
23
-
24
- @test "agent.md carries a NEEDS DIRECTION report verdict (ADR-064)" {
25
- run grep -n "Architecture Review: NEEDS DIRECTION" "$AGENT_FILE"
26
- [ "$status" -eq 0 ]
27
- }
28
-
29
- @test "agent.md lists [Needs Direction] as an issue/verdict type" {
30
- run grep -n "\[Needs Direction\]" "$AGENT_FILE"
31
- [ "$status" -eq 0 ]
32
- }
33
-
34
- @test "agent.md has a 'When to emit Needs Direction' section citing ADR-064" {
35
- run grep -n "When to emit Needs Direction" "$AGENT_FILE"
36
- [ "$status" -eq 0 ]
37
- run grep -n "ADR-064" "$AGENT_FILE"
38
- [ "$status" -eq 0 ]
39
- }
40
-
41
- @test "agent.md requires the main agent to translate the verdict into AskUserQuestion (not prose)" {
42
- run grep -n "AskUserQuestion" "$AGENT_FILE"
43
- [ "$status" -eq 0 ]
44
- }
45
-
46
- @test "agent.md guards the negative bound: do NOT emit Needs Direction on obvious/single-option choices" {
47
- # inverse-P078 over-ask guard — the verdict must not fire when only one viable option exists
48
- run grep -niE "Do NOT emit Needs Direction.*(obvious|one-viable|only-one)" "$AGENT_FILE"
49
- [ "$status" -eq 0 ]
50
- }
51
-
52
- @test "agent.md requires Needs-Direction to name the SUBSTANCE not the grain (ADR-074)" {
53
- # ADR-074 surface 1: the verdict must name the substantive choice, not a
54
- # meta/grain framing question (e.g. "one ADR or two?"). Closes the P315 vector.
55
- run grep -niE "Name the SUBSTANCE, not the grain" "$AGENT_FILE"
56
- [ "$status" -eq 0 ]
57
- run grep -n "ADR-074" "$AGENT_FILE"
58
- [ "$status" -eq 0 ]
59
- }
60
-
61
- @test "agent.md performance-review section cites ADR-026 as parent (ADR-026 Confirmation item 1)" {
62
- run grep -nE "Runtime-Path Performance Review \(per ADR-026" "$AGENT_FILE"
63
- [ "$status" -eq 0 ]
64
- }
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env bats
2
- # Doc-lint guard: architect agent.md must include the output formatting rule
3
- # requiring human-readable titles alongside bare IDs (P032).
4
- #
5
- # Structural assertion — Permitted Exception to the source-grep ban (ADR-005 / P011).
6
- #
7
- # Cross-reference:
8
- # P032 (agent output uses opaque IDs without titles)
9
- # @jtbd JTBD-001 (enforce governance without slowing down)
10
-
11
- setup() {
12
- AGENT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
13
- AGENT_FILE="${AGENT_DIR}/agent.md"
14
- }
15
-
16
- @test "agent.md contains output formatting section" {
17
- run grep -n "## Output Formatting" "$AGENT_FILE"
18
- [ "$status" -eq 0 ]
19
- }
20
-
21
- @test "agent.md output formatting rule requires titles with IDs (P032)" {
22
- run grep -n "title" "$AGENT_FILE"
23
- [ "$status" -eq 0 ]
24
- run grep -n "Output Formatting" "$AGENT_FILE"
25
- [ "$status" -eq 0 ]
26
- }
@@ -1,76 +0,0 @@
1
- #!/usr/bin/env bats
2
- # Doc-lint guard: architect agent.md must include the runtime-path
3
- # performance-review step per ADR-023 (wr-architect agent performance
4
- # review scope). Closes P046 (wr-architect agent misses performance
5
- # implications on high-traffic endpoints).
6
- #
7
- # Structural assertion — Permitted Exception to the source-grep ban
8
- # (ADR-005 / P011).
9
- #
10
- # Cross-reference:
11
- # P046 (wr-architect agent misses per-request performance implications)
12
- # ADR-023 (wr-architect agent performance review scope)
13
- # @jtbd JTBD-002 (ship with confidence)
14
- # @jtbd JTBD-101 (extend the suite with clear patterns)
15
-
16
- setup() {
17
- AGENT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
18
- AGENT_FILE="${AGENT_DIR}/agent.md"
19
- }
20
-
21
- @test "agent.md contains a runtime-path performance review section (ADR-023)" {
22
- run grep -nE "[Pp]erformance [Rr]eview|[Rr]untime-[Pp]ath" "$AGENT_FILE"
23
- [ "$status" -eq 0 ]
24
- }
25
-
26
- @test "agent.md lists the runtime-path trigger categories (cache / throttle / rate-limit / per-request handler)" {
27
- # Cache directives
28
- run grep -niE "cache-control|etag|last-modified" "$AGENT_FILE"
29
- [ "$status" -eq 0 ]
30
- # Rate limiting / throttle
31
- run grep -niE "rate[- ]limit|throttl|quota" "$AGENT_FILE"
32
- [ "$status" -eq 0 ]
33
- # Per-request handler
34
- run grep -niE "per-request|per request handler" "$AGENT_FILE"
35
- [ "$status" -eq 0 ]
36
- }
37
-
38
- @test "agent.md requires quantification: per-request cost delta (ADR-023 Confirmation criterion 2a)" {
39
- run grep -niE "per-request cost delta|cost delta per request" "$AGENT_FILE"
40
- [ "$status" -eq 0 ]
41
- }
42
-
43
- @test "agent.md requires request-frequency estimate with cited source (ADR-023)" {
44
- run grep -niE "[Rr]equest[- ][Ff]requency" "$AGENT_FILE"
45
- [ "$status" -eq 0 ]
46
- # Must require citing a source (ADR/JTBD/telemetry/worst-case)
47
- run grep -niE "cite|source" "$AGENT_FILE"
48
- [ "$status" -eq 0 ]
49
- }
50
-
51
- @test "agent.md requires product (cost x frequency) computation" {
52
- run grep -niE "(cost.*frequency|frequency.*cost|aggregate load|product)" "$AGENT_FILE"
53
- [ "$status" -eq 0 ]
54
- }
55
-
56
- @test "agent.md bans qualitative-only performance claims (ADR-023 Confirmation criterion 2b)" {
57
- # The prompt must forbid phrases like "load is negligible", "microseconds only"
58
- run grep -niE "(must not|MUST NOT).*(qualitative|negligible|microsecond)" "$AGENT_FILE"
59
- [ "$status" -eq 0 ]
60
- }
61
-
62
- @test "agent.md references performance-budget ADR convention by name (ADR-023 Confirmation criterion 2c)" {
63
- run grep -niE "performance[- ]budget" "$AGENT_FILE"
64
- [ "$status" -eq 0 ]
65
- }
66
-
67
- @test "agent.md instructs a verdict against any in-scope performance-budget ADR" {
68
- # Must describe what to do when a budget is present vs missing
69
- run grep -niE "verdict|recommend creating|no performance budget" "$AGENT_FILE"
70
- [ "$status" -eq 0 ]
71
- }
72
-
73
- @test "agent.md cross-references ADR-023 so readers can trace the rule's origin" {
74
- run grep -n "ADR-023" "$AGENT_FILE"
75
- [ "$status" -eq 0 ]
76
- }
@@ -1,50 +0,0 @@
1
- #!/usr/bin/env bats
2
- # Doc-lint guard: architect agent.md must carry an explicit pre-edit /
3
- # proposed-change review-mode carve-out so the agent classifies alignment
4
- # of the PROPOSAL when the calling prompt describes a not-yet-applied
5
- # change, instead of mis-classifying not-yet-applied state as ISSUES FOUND
6
- # (P313 catch-22: gate blocks edits; reviewer wants edits done first).
7
- #
8
- # tdd-review: structural-permitted (justification: P176 — agent behaviour is
9
- # prompt-driven with no skill-invocation harness to exercise the verdict
10
- # behaviourally; ADR-052 Surface 2 structural-justified case, NOT an ADR-005
11
- # Permitted Exception — ADR-052 narrows ADR-005 to exclude prose-doc greps).
12
- # When P176 lands, upgrade to a behavioural test that feeds the agent a
13
- # pre-edit proposal and asserts the PASS verdict.
14
- #
15
- # Cross-reference:
16
- # P313 (Pre-edit governance-gate catch-22 — pass withheld pending edits)
17
- # ADR-052 Surface 2 (structural-justified verdict) + P176 (harness gap)
18
- # @jtbd JTBD-001 (Enforce Governance Without Slowing Down — preserves the
19
- # under-60-second review outcome by removing the redundant
20
- # re-delegation the catch-22 currently forces)
21
-
22
- setup() {
23
- AGENT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
24
- AGENT_FILE="${AGENT_DIR}/agent.md"
25
- }
26
-
27
- @test "agent.md carries a Review Mode section distinguishing pre-edit from post-edit (P313)" {
28
- run grep -nE "^## Review Mode: Pre-edit / proposed-change vs\. Post-edit / applied" "$AGENT_FILE"
29
- [ "$status" -eq 0 ]
30
- }
31
-
32
- @test "agent.md classifies alignment of the PROPOSAL in pre-edit mode (P313 verbatim core sentence)" {
33
- run grep -nE "classify alignment of the PROPOSAL itself" "$AGENT_FILE"
34
- [ "$status" -eq 0 ]
35
- }
36
-
37
- @test "agent.md treats not-yet-applied state as the EXPECTED baseline of a pre-edit gate (P313)" {
38
- run grep -nE "EXPECTED baseline of a pre-edit gate" "$AGENT_FILE"
39
- [ "$status" -eq 0 ]
40
- }
41
-
42
- @test "agent.md explicitly rejects 'edits aren't applied yet' as a valid ISSUES FOUND substance (P313)" {
43
- run grep -nE "Do NOT treat .edits aren't applied yet" "$AGENT_FILE"
44
- [ "$status" -eq 0 ]
45
- }
46
-
47
- @test "agent.md cites P313 as the catch-22 closure (audit-trail)" {
48
- run grep -nE "P313" "$AGENT_FILE"
49
- [ "$status" -eq 0 ]
50
- }
@@ -1,55 +0,0 @@
1
- #!/usr/bin/env bats
2
- # Doc-lint guard: architect agent.md must carry the [Unratified Dependency]
3
- # verdict (ADR-074 enforcement surface 3 / RFC-010 / P318) — flag a change or
4
- # plan that explicitly cites/implements an ADR lacking `human-oversight:
5
- # confirmed` (unratified, non-superseded), keyed on the oversight marker NOT
6
- # `status:`. Closes the residual P315 foreground gap at the architect-review
7
- # surface (the ITIL propose-fix surface is covered by RFC-008).
8
- #
9
- # tdd-review: structural-permitted (justification: P176 — agent behaviour is
10
- # prompt-driven with no skill-invocation harness to exercise the verdict
11
- # behaviourally; ADR-052 Surface 2 structural-justified case, NOT an ADR-005
12
- # Permitted Exception). When P176 lands, upgrade to a behavioural test that
13
- # feeds the agent a change citing an unratified ADR and asserts the verdict.
14
- #
15
- # Cross-reference:
16
- # ADR-074 (Confirm a decision's substance before building dependent work — surface 3)
17
- # ADR-066 (oversight marker; orthogonal status/oversight axes; the "unconfirmed" definition)
18
- # RFC-010 / P318 (this enforcement surface)
19
- # ADR-052 Surface 2 (structural-justified verdict) + P176 (harness gap)
20
- # @jtbd JTBD-002 (ship with confidence) / JTBD-001 (enforce governance without slowing down)
21
-
22
- setup() {
23
- AGENT_DIR="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)"
24
- AGENT_FILE="${AGENT_DIR}/agent.md"
25
- }
26
-
27
- @test "agent.md lists [Unratified Dependency] as an issue/verdict type (ADR-074 surface 3)" {
28
- run grep -n '\[Unratified Dependency\]' "$AGENT_FILE"
29
- [ "$status" -eq 0 ]
30
- }
31
-
32
- @test "agent.md has a 'When to flag [Unratified Dependency]' section citing ADR-074" {
33
- run grep -niE "When to flag \[Unratified Dependency\]" "$AGENT_FILE"
34
- [ "$status" -eq 0 ]
35
- run grep -n "ADR-074" "$AGENT_FILE"
36
- [ "$status" -eq 0 ]
37
- }
38
-
39
- @test "agent.md keys the flag on the oversight marker, NOT on status (orthogonal axes)" {
40
- # ADR-066 / user correction 2026-05-27: building on a ratified-but-proposed ADR is fine.
41
- run grep -niE "NEVER on .?status|not .?\`?status|orthogonal" "$AGENT_FILE"
42
- [ "$status" -eq 0 ]
43
- }
44
-
45
- @test "agent.md scopes the marker check to frontmatter + skips superseded (mirrors is-decision-unconfirmed.sh)" {
46
- run grep -niE "frontmatter" "$AGENT_FILE"
47
- [ "$status" -eq 0 ]
48
- run grep -niE "superseded" "$AGENT_FILE"
49
- [ "$status" -eq 0 ]
50
- }
51
-
52
- @test "agent.md guards against over-firing on transitive/ambient dependence (inverse-P078)" {
53
- run grep -niE "explicit(ly)? cite|over-?(fire|scan)|transitive" "$AGENT_FILE"
54
- [ "$status" -eq 0 ]
55
- }
@@ -1,299 +0,0 @@
1
- #!/usr/bin/env bats
2
-
3
- # Behavioural tests for architect-compendium-update-entry.sh (RFC-014 Story A,
4
- # ADR-078 Phase 1 / Option 9). Exercises the hook against fixture compendium
5
- # trees; asserts on its side-effects (README mutation, staging, exit code) and
6
- # stderr signals — never on hook source content (feedback_behavioural_tests).
7
- #
8
- # The `claude -p` subprocess is stubbed with a PATH-priority fake-claude shim
9
- # (RFC-014 SQ-014-1) that emits a fixed-shape `{"result": "<entry>"}` envelope
10
- # for whichever ADR-ID appears in the prompt. Real-subprocess integration is
11
- # out of scope for Phase 1.
12
-
13
- setup() {
14
- HOOK="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/architect-compendium-update-entry.sh"
15
- PROJ="$(mktemp -d)"
16
- mkdir -p "$PROJ/docs/decisions"
17
- ( cd "$PROJ" && git init -q && git config user.email t@e.x && git config user.name t )
18
-
19
- # PATH-priority fake-claude shim: emits a {"result": "<entry>"} envelope with
20
- # an entry block for the ADR-ID found in the prompt. Marker text STUBBED-<id>
21
- # makes the emitted Decides line assertable.
22
- SHIMDIR="$(mktemp -d)"
23
- cat > "$SHIMDIR/claude" <<'SHIM'
24
- #!/usr/bin/env bash
25
- prompt=$(cat)
26
- id=$(printf '%s' "$prompt" | grep -oE 'ADR-[0-9]+' | head -1 | sed 's/ADR-//')
27
- entry="### ADR-${id} — Stub Title
28
- **Status:** proposed | **Oversight:** confirmed
29
- **Decides:** STUBBED-${id} decision body.
30
- **Confirmation:** stub crit a; stub crit b
31
- **Related:** ADR-001"
32
- jq -cn --arg r "$entry" '{result:$r}'
33
- SHIM
34
- chmod +x "$SHIMDIR/claude"
35
-
36
- # Failing shim variant: exits non-zero, emits nothing (degraded-mode tests).
37
- FAILSHIMDIR="$(mktemp -d)"
38
- cat > "$FAILSHIMDIR/claude" <<'SHIM'
39
- #!/usr/bin/env bash
40
- cat >/dev/null
41
- exit 7
42
- SHIM
43
- chmod +x "$FAILSHIMDIR/claude"
44
-
45
- ORIG_PATH="$PATH"
46
- export PATH="$SHIMDIR:$PATH"
47
- }
48
-
49
- teardown() {
50
- export PATH="$ORIG_PATH"
51
- rm -rf "$PROJ" "$SHIMDIR" "$FAILSHIMDIR"
52
- }
53
-
54
- # Writes a minimal compendium README with one in-force section (ADRs 003, 049,
55
- # 051) and one historical section (ADR 010). Stable fixture for placement tests.
56
- mk_readme() {
57
- cat > "$PROJ/docs/decisions/README.md" <<'EOF'
58
- # Decisions Compendium
59
-
60
- Intro prose.
61
-
62
- ---
63
-
64
- ## In-force decisions
65
-
66
- _3 ADRs._
67
-
68
- ### ADR-003 — Three
69
- **Status:** proposed | **Oversight:** confirmed
70
- **Decides:** Decides three.
71
-
72
- ### ADR-049 — FortyNine
73
- **Status:** accepted | **Oversight:** confirmed
74
- **Decides:** Decides forty-nine.
75
-
76
- ### ADR-051 — FiftyOne
77
- **Status:** proposed | **Oversight:** confirmed
78
- **Decides:** Decides fifty-one.
79
-
80
- ---
81
-
82
- ## Historical decisions
83
-
84
- _1 ADR._
85
-
86
- ### ADR-010 — Ten
87
- **Status:** superseded | **Oversight:** confirmed
88
- **Decides:** Decides ten.
89
- EOF
90
- }
91
-
92
- # mk_adr <nnn> <status> <title>
93
- mk_adr() {
94
- local nnn="$1" status="$2" title="$3"
95
- cat > "$PROJ/docs/decisions/${nnn}-slug.${status}.md" <<EOF
96
- ---
97
- status: "$status"
98
- date: 2026-06-09
99
- human-oversight: confirmed
100
- ---
101
-
102
- # $title
103
-
104
- ## Decision Outcome
105
-
106
- Chosen option: **"$title impl"**, because reasons.
107
-
108
- ## Confirmation
109
-
110
- - (a) first
111
- - (b) second
112
-
113
- ## Related
114
-
115
- - Relates to [ADR-001](001-foo.proposed.md)
116
- EOF
117
- echo "$PROJ/docs/decisions/${nnn}-slug.${status}.md"
118
- }
119
-
120
- run_hook() {
121
- local fp="$1"
122
- echo "{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$fp\"},\"session_id\":\"s1\"}" \
123
- | CLAUDE_PROJECT_DIR="$PROJ" bash "$HOOK"
124
- }
125
-
126
- @test "fires on Edit to a numbered ADR body and refreshes its entry in-place (criterion 1+4)" {
127
- mk_readme
128
- fp=$(mk_adr "049" "accepted" "FortyNine")
129
- run run_hook "$fp"
130
- [ "$status" -eq 0 ]
131
- # In-place replacement: the entry now carries the stubbed Decides marker...
132
- grep -q 'STUBBED-049 decision body' "$PROJ/docs/decisions/README.md"
133
- # ...and there is exactly ONE ADR-049 header (no duplicate).
134
- [ "$(grep -c '^### ADR-049 ' "$PROJ/docs/decisions/README.md")" -eq 1 ]
135
- }
136
-
137
- @test "produces an observable stderr signal on every invocation (criterion 2)" {
138
- mk_readme
139
- fp=$(mk_adr "049" "accepted" "FortyNine")
140
- run run_hook "$fp"
141
- [[ "$output" == *"architect-compendium-update-entry"* ]]
142
- }
143
-
144
- @test "emits the expected entry shape with a Decides line (criterion 3)" {
145
- mk_readme
146
- fp=$(mk_adr "049" "accepted" "FortyNine")
147
- run_hook "$fp"
148
- grep -qE '^\*\*Decides:\*\* STUBBED-049' "$PROJ/docs/decisions/README.md"
149
- }
150
-
151
- @test "inserts a new ADR entry in numeric-sort order within in-force (criterion 5)" {
152
- mk_readme
153
- fp=$(mk_adr "050" "proposed" "Fifty")
154
- run run_hook "$fp"
155
- [ "$status" -eq 0 ]
156
- # ADR-050 must appear between ADR-049 and ADR-051 in the in-force section.
157
- line049=$(grep -n '^### ADR-049 ' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
158
- line050=$(grep -n '^### ADR-050 ' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
159
- line051=$(grep -n '^### ADR-051 ' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
160
- [ -n "$line050" ]
161
- [ "$line049" -lt "$line050" ]
162
- [ "$line050" -lt "$line051" ]
163
- }
164
-
165
- @test "routes a superseded ADR's entry into the historical section (criterion 6)" {
166
- mk_readme
167
- fp=$(mk_adr "012" "superseded" "Twelve")
168
- run run_hook "$fp"
169
- [ "$status" -eq 0 ]
170
- # ADR-012 must land AFTER the Historical-decisions header, not in In-force.
171
- hist=$(grep -n '^## Historical decisions' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
172
- line012=$(grep -n '^### ADR-012 ' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
173
- [ -n "$line012" ]
174
- [ "$line012" -gt "$hist" ]
175
- }
176
-
177
- @test "migrates an entry from in-force to historical when status flips (criterion 6)" {
178
- mk_readme
179
- # ADR-049 currently renders in the in-force fixture section; re-author it as
180
- # superseded — the hook must remove the in-force block and place it historical.
181
- fp=$(mk_adr "049" "superseded" "FortyNine")
182
- run run_hook "$fp"
183
- [ "$status" -eq 0 ]
184
- # Exactly one ADR-049 entry, and it is now below the Historical header.
185
- [ "$(grep -c '^### ADR-049 ' "$PROJ/docs/decisions/README.md")" -eq 1 ]
186
- hist=$(grep -n '^## Historical decisions' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
187
- line049=$(grep -n '^### ADR-049 ' "$PROJ/docs/decisions/README.md" | cut -d: -f1)
188
- [ "$line049" -gt "$hist" ]
189
- }
190
-
191
- @test "stages docs/decisions/README.md after refresh (criterion: same-commit pairing)" {
192
- mk_readme
193
- ( cd "$PROJ" && git add -A && git commit -q -m init )
194
- fp=$(mk_adr "049" "accepted" "FortyNine")
195
- run_hook "$fp"
196
- # README must be in the staged set.
197
- ( cd "$PROJ" && git diff --cached --name-only ) | grep -q 'docs/decisions/README.md'
198
- }
199
-
200
- @test "subprocess failure leaves README unchanged + does NOT block the edit (criterion 7)" {
201
- mk_readme
202
- before=$(cat "$PROJ/docs/decisions/README.md")
203
- fp=$(mk_adr "049" "accepted" "FortyNine")
204
- export PATH="$FAILSHIMDIR:$ORIG_PATH"
205
- run run_hook "$fp"
206
- [ "$status" -eq 0 ] # does not block the body edit
207
- after=$(cat "$PROJ/docs/decisions/README.md")
208
- [ "$before" = "$after" ] # README unchanged (degraded mode)
209
- [[ "$output" == *"degraded mode"* ]]
210
- }
211
-
212
- @test "opt-out ARCHITECT_AUTO_UPDATE_COMPENDIUM=0 self-suppresses (criterion 8)" {
213
- mk_readme
214
- before=$(cat "$PROJ/docs/decisions/README.md")
215
- fp=$(mk_adr "049" "accepted" "FortyNine")
216
- run bash -c "echo '{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"$fp\"}}' | ARCHITECT_AUTO_UPDATE_COMPENDIUM=0 CLAUDE_PROJECT_DIR='$PROJ' bash '$HOOK'"
217
- [ "$status" -eq 0 ]
218
- [ "$before" = "$(cat "$PROJ/docs/decisions/README.md")" ]
219
- [[ "$output" == *"ARCHITECT_AUTO_UPDATE_COMPENDIUM=0"* ]]
220
- }
221
-
222
- @test "ignores README.md edits (no self-recursion)" {
223
- mk_readme
224
- before=$(cat "$PROJ/docs/decisions/README.md")
225
- run run_hook "$PROJ/docs/decisions/README.md"
226
- [ "$status" -eq 0 ]
227
- [ "$before" = "$(cat "$PROJ/docs/decisions/README.md")" ]
228
- }
229
-
230
- @test "ignores non-decisions file edits" {
231
- mk_readme
232
- echo "x" > "$PROJ/other.ts"
233
- run run_hook "$PROJ/other.ts"
234
- [ "$status" -eq 0 ]
235
- }
236
-
237
- @test "fail-closed guard: rejects a subprocess entry that injects spurious ADR ids/sections — restores README, degraded, unstaged (P367)" {
238
- mk_readme
239
- ( cd "$PROJ" && git add -A && git commit -q -m init )
240
- before=$(cat "$PROJ/docs/decisions/README.md")
241
- fp=$(mk_adr "049" "accepted" "FortyNine")
242
- # Malformed shim: valid header for the edited id, but ALSO injects an
243
- # unrelated ADR-999 header and a spurious '## ' section — the additive
244
- # corruption shape empirically reproduced for P367.
245
- BADDIR="$(mktemp -d)"
246
- cat > "$BADDIR/claude" <<'SHIM'
247
- #!/usr/bin/env bash
248
- cat >/dev/null
249
- entry="### ADR-049 — Hijacked
250
- **Status:** accepted | **Oversight:** confirmed
251
- **Decides:** body.
252
-
253
- ## Injected section
254
-
255
- ### ADR-999 — Sneaky"
256
- jq -cn --arg r "$entry" '{result:$r}'
257
- SHIM
258
- chmod +x "$BADDIR/claude"
259
- export PATH="$BADDIR:$ORIG_PATH"
260
- run run_hook "$fp"
261
- [ "$status" -eq 0 ] # never blocks the body edit
262
- [ "$before" = "$(cat "$PROJ/docs/decisions/README.md")" ] # restored unchanged
263
- ! grep -q 'ADR-999' "$PROJ/docs/decisions/README.md" # no injected id survives
264
- [[ "$output" == *"guard"* ]] # observable degraded signal
265
- # README left in its committed state — no corrupted blob staged.
266
- ( cd "$PROJ" && git diff --cached --quiet -- docs/decisions/README.md )
267
- rm -rf "$BADDIR"
268
- }
269
-
270
- @test "fail-closed guard: rejects an emit for the wrong ADR id (edited id absent) — restores, degraded (P367)" {
271
- mk_readme
272
- before=$(cat "$PROJ/docs/decisions/README.md")
273
- fp=$(mk_adr "050" "proposed" "Fifty") # NEW adr — not yet in the compendium
274
- # Shim emits an entry for the WRONG id (049, which already exists) instead of
275
- # the edited 050: the edited id never lands and 049 is duplicated.
276
- WRONGDIR="$(mktemp -d)"
277
- cat > "$WRONGDIR/claude" <<'SHIM'
278
- #!/usr/bin/env bash
279
- cat >/dev/null
280
- entry="### ADR-049 — WrongId
281
- **Status:** proposed | **Oversight:** confirmed
282
- **Decides:** body."
283
- jq -cn --arg r "$entry" '{result:$r}'
284
- SHIM
285
- chmod +x "$WRONGDIR/claude"
286
- export PATH="$WRONGDIR:$ORIG_PATH"
287
- run run_hook "$fp"
288
- [ "$status" -eq 0 ]
289
- [ "$before" = "$(cat "$PROJ/docs/decisions/README.md")" ] # restored unchanged
290
- ! grep -q '^### ADR-050' "$PROJ/docs/decisions/README.md" # edited id never landed
291
- [[ "$output" == *"guard"* ]]
292
- rm -rf "$WRONGDIR"
293
- }
294
-
295
- @test "registered in hooks.json on PostToolUse Edit|Write (criterion 9)" {
296
- HOOKS_JSON="$(cd "$(dirname "$BATS_TEST_FILENAME")/.." && pwd)/hooks.json"
297
- run jq -e '.hooks.PostToolUse[] | select(.matcher | test("Edit")) | .hooks[] | select(.command | test("architect-compendium-update-entry"))' "$HOOKS_JSON"
298
- [ "$status" -eq 0 ]
299
- }