crewforth 3.3.0 → 3.5.0-rc.1

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 (154) hide show
  1. package/CHANGELOG.md +271 -0
  2. package/README.md +7 -2
  3. package/VERSION +1 -1
  4. package/adopt.sh +40 -5
  5. package/bin/cli.js +193 -2
  6. package/kit/AGENT_TEMPLATE.md +15 -0
  7. package/kit/CLAUDE.md +49 -17
  8. package/kit/agents/crew-backend-expert.md +4 -3
  9. package/kit/agents/crew-commit-agent.md +2 -0
  10. package/kit/agents/crew-database-expert.md +2 -1
  11. package/kit/agents/crew-devops-expert.md +1 -0
  12. package/kit/agents/crew-docs-agent.md +3 -1
  13. package/kit/agents/crew-frontend-expert.md +2 -0
  14. package/kit/agents/crew-performance-expert.md +7 -2
  15. package/kit/agents/crew-planner.md +15 -3
  16. package/kit/agents/crew-privacy-agent.md +2 -0
  17. package/kit/agents/crew-review-agent.md +8 -1
  18. package/kit/agents/crew-security-expert.md +4 -3
  19. package/kit/agents/crew-test-expert.md +6 -2
  20. package/kit/eval/doctor.sh +201 -1
  21. package/kit/eval/gate-report.sh +28 -0
  22. package/kit/eval/golden-holdout.txt +2 -0
  23. package/kit/eval/golden-routing.txt +17 -0
  24. package/kit/eval/lib/crew-map/contracts.mjs +77 -0
  25. package/kit/eval/lib/crew-map/crew-map.sh +2 -1
  26. package/kit/eval/lib/crew-map/detect.mjs +2 -1
  27. package/kit/eval/lib/crew-map/narrative.mjs +14 -3
  28. package/kit/eval/lib/crew-map/project.mjs +45 -6
  29. package/kit/eval/lib/crew-map/scan.mjs +153 -14
  30. package/kit/eval/lib/findings/findings.schema.json +11 -1
  31. package/kit/eval/lib/findings/findings.sh +24 -1
  32. package/kit/eval/lib/hook-inject.sh +159 -0
  33. package/kit/eval/lib/lockfiles.sh +71 -0
  34. package/kit/eval/lib/payload-check.sh +29 -0
  35. package/kit/eval/lib/preview.sh +3 -3
  36. package/kit/eval/lib/review-bench/bench.mjs +14 -2
  37. package/kit/eval/lib/review-prepass/prepass.mjs +32 -3
  38. package/kit/eval/lib/settings-json.awk +18 -0
  39. package/kit/eval/lib/untrusted-text.awk +38 -0
  40. package/kit/eval/scan-skill.sh +67 -20
  41. package/kit/eval/smoke-test.sh +1792 -52
  42. package/kit/eval/utilization.sh +77 -3
  43. package/kit/hooks/agent-outcome.sh +9 -1
  44. package/kit/hooks/board-sync.sh +49 -3
  45. package/kit/hooks/board.sh +8 -8
  46. package/kit/hooks/branch-fresh.sh +101 -0
  47. package/kit/hooks/floor-blocklist.txt +19 -1
  48. package/kit/hooks/guard-bash.sh +92 -8
  49. package/kit/hooks/guard-question.sh +2 -1
  50. package/kit/hooks/loop-guard.sh +445 -0
  51. package/kit/hooks/map-stale.sh +437 -0
  52. package/kit/hooks/pre-commit +48 -7
  53. package/kit/hooks/precompact-snapshot.sh +102 -6
  54. package/kit/hooks/prompt-approval.sh +45 -9
  55. package/kit/hooks/route-hint.sh +36 -7
  56. package/kit/hooks/secret-blocklist.txt +55 -1
  57. package/kit/hooks/session-guard.sh +85 -28
  58. package/kit/hooks/session-rehydrate.sh +34 -11
  59. package/kit/hooks/session-stats.sh +169 -8
  60. package/kit/hooks/session-update-check.sh +5 -1
  61. package/kit/hooks/skill-trust.sh +23 -14
  62. package/kit/hooks/turn-check.sh +190 -0
  63. package/kit/hooks/worktree-provision.sh +91 -0
  64. package/kit/owned-blobs.tsv +4 -2
  65. package/kit/settings.json +86 -0
  66. package/kit/skills/a11y/SKILL.md +50 -3
  67. package/kit/skills/a11y/references/recipes.md +41 -0
  68. package/kit/skills/adr/SKILL.md +14 -2
  69. package/kit/skills/api-design/SKILL.md +6 -1
  70. package/kit/skills/api-design/references/idempotency.md +39 -0
  71. package/kit/skills/brainstorm/SKILL.md +4 -2
  72. package/kit/skills/brainstorm/references/asking.md +47 -0
  73. package/kit/skills/ci-pipeline/SKILL.md +11 -1
  74. package/kit/skills/ci-pipeline/references/agent-job.md +33 -0
  75. package/kit/skills/commit-message/SKILL.md +9 -3
  76. package/kit/skills/confidence-check/SKILL.md +9 -7
  77. package/kit/skills/confidence-check/references/source-check.md +37 -0
  78. package/kit/skills/crew-approve/SKILL.md +3 -0
  79. package/kit/skills/crew-audit/SKILL.md +63 -0
  80. package/kit/skills/crew-board/SKILL.md +2 -2
  81. package/kit/skills/crew-code-review/SKILL.md +27 -4
  82. package/kit/skills/crew-code-review/references/panel-mode.md +9 -9
  83. package/kit/skills/crew-code-review/references/simplify.md +28 -0
  84. package/kit/skills/crew-doctor/SKILL.md +3 -2
  85. package/kit/skills/crew-gates/SKILL.md +3 -0
  86. package/kit/skills/crew-gates/references/enforcers.md +30 -0
  87. package/kit/skills/crew-interview/SKILL.md +42 -0
  88. package/kit/skills/crew-map/SKILL.md +26 -6
  89. package/kit/skills/crew-plan/SKILL.md +9 -2
  90. package/kit/skills/crew-pr/SKILL.md +57 -0
  91. package/kit/skills/crew-pr/references/checks.md +35 -0
  92. package/kit/skills/crew-pr/references/threads.md +23 -0
  93. package/kit/skills/crew-review/SKILL.md +34 -1
  94. package/kit/skills/crew-ship/SKILL.md +5 -2
  95. package/kit/skills/crew-skill/SKILL.md +26 -1
  96. package/kit/skills/crew-update/SKILL.md +4 -1
  97. package/kit/skills/db-migration/SKILL.md +15 -0
  98. package/kit/skills/db-migration/references/expand-contract.md +52 -0
  99. package/kit/skills/dependency-audit/SKILL.md +34 -1
  100. package/kit/skills/dependency-audit/scripts/record.sh +37 -0
  101. package/kit/skills/dependency-upgrade/SKILL.md +29 -1
  102. package/kit/skills/deploy/SKILL.md +18 -0
  103. package/kit/skills/deploy/references/topology-a.md +7 -0
  104. package/kit/skills/deploy/references/topology-b.md +2 -2
  105. package/kit/skills/docs-writer/SKILL.md +37 -2
  106. package/kit/skills/eval-grader/SKILL.md +9 -0
  107. package/kit/skills/eval-grader/references/method.md +26 -0
  108. package/kit/skills/frontend/SKILL.md +5 -2
  109. package/kit/skills/frontend/references/from-design.md +29 -0
  110. package/kit/skills/frontend/references/page-metadata.md +17 -0
  111. package/kit/skills/frontend/references/verify-contract.md +18 -1
  112. package/kit/skills/frontend/references/worst-data.md +35 -0
  113. package/kit/skills/frontend-design/SKILL.md +20 -4
  114. package/kit/skills/frontend-design/references/design-review.md +21 -0
  115. package/kit/skills/frontend-design/references/detection.md +44 -0
  116. package/kit/skills/frontend-design/references/motion.md +54 -0
  117. package/kit/skills/frontend-design/references/ui-copy.md +35 -0
  118. package/kit/skills/frontend-rn-expo/SKILL.md +3 -0
  119. package/kit/skills/handoff/SKILL.md +28 -3
  120. package/kit/skills/handoff/references/cold-reader.md +13 -6
  121. package/kit/skills/i18n-integrity/SKILL.md +3 -0
  122. package/kit/skills/iterate/SKILL.md +7 -2
  123. package/kit/skills/mcp-builder/SKILL.md +21 -4
  124. package/kit/skills/mcp-builder/references/building.md +74 -13
  125. package/kit/skills/mcp-builder/references/cli-for-agents.md +26 -0
  126. package/kit/skills/observability/SKILL.md +13 -1
  127. package/kit/skills/performance/SKILL.md +25 -1
  128. package/kit/skills/performance/references/bottlenecks.md +11 -0
  129. package/kit/skills/performance/references/web-vitals.md +31 -0
  130. package/kit/skills/red-team/SKILL.md +25 -3
  131. package/kit/skills/reflect/SKILL.md +7 -2
  132. package/kit/skills/release/SKILL.md +13 -0
  133. package/kit/skills/security-scan/SKILL.md +6 -2
  134. package/kit/skills/security-scan/references/ai-agents.md +16 -0
  135. package/kit/skills/security-scan/references/fronts.md +59 -2
  136. package/kit/skills/security-scan/references/reporting.md +13 -0
  137. package/kit/skills/security-scan/references/verify.md +9 -0
  138. package/kit/skills/sonarqube-check/SKILL.md +32 -2
  139. package/kit/skills/sonarqube-check/references/ratchet.md +32 -0
  140. package/kit/skills/spec-planning/SKILL.md +25 -8
  141. package/kit/skills/systematic-debugging/SKILL.md +15 -7
  142. package/kit/skills/systematic-debugging/references/techniques.md +5 -0
  143. package/kit/skills/teamboard/SKILL.md +3 -3
  144. package/kit/skills/testing/SKILL.md +18 -3
  145. package/kit/skills/testing/references/browser-headless.md +21 -0
  146. package/kit/skills/token-budget/SKILL.md +26 -5
  147. package/kit/skills/worktree/SKILL.md +13 -5
  148. package/kit/studio/server/lib/session.js +23 -2
  149. package/kit/studio/server/lib/worktree-changes.js +64 -0
  150. package/kit/studio/web/convo.js +14 -0
  151. package/kit/studio/web/map-plan.js +54 -8
  152. package/kit/studio/web/map.js +15 -3
  153. package/package.json +5 -4
  154. package/start.sh +11 -2
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env bash
2
+ # turn-check.sh — UserPromptSubmit + Stop: the files the main thread changed in a turn get the project's own fast
3
+ # check once, before the turn ends.
4
+ #
5
+ # WHY. A crew agent's work is checked when it stops (agent-outcome.sh runs its card's `verify`). The main thread's
6
+ # own edits, which the discipline allows for small changes, had nothing: the turn ended, the user read "done", and
7
+ # a lint error waited for the next commit or the next person. This closes that gap with the command the project
8
+ # already declared for it, and only that one.
9
+ #
10
+ # WHAT RUNS. The `Fast check (files):` line of ./CLAUDE.md (## Verify). `<file>` in it becomes the turn's changed files,
11
+ # each in single quotes; a command with no `<file>` runs as written. No line, the template's placeholder, or `none`:
12
+ # nothing runs, nothing is written, no process starts. The command is judged exactly as agent-outcome.sh judges a
13
+ # verify command before it runs it (_am_verify_ok in guard-agent-model.sh: permission rules, the shell gate, the
14
+ # mode); one it may not run is said to the user once per session and not run.
15
+ #
16
+ # WHICH FILES ARE THE TURN'S. At UserPromptSubmit a snapshot goes into the git directory (never committed): HEAD, the
17
+ # time, and the content id of every file that was already dirty. At Stop the turn's set is
18
+ # * a dirty file whose content differs from the snapshot (one dirty before the turn and untouched is not the
19
+ # turn's, it is earlier work);
20
+ # * the files of commits the turn made (commit, amend, cherry-pick, revert), as `git diff <snapshot HEAD> HEAD`;
21
+ # an amend therefore counts only what it changed. When the turn moved HEAD any other way (pull, merge, reset,
22
+ # checkout, rebase), the commits are not counted at all: someone else's code is not this turn's to check.
23
+ # Deleted files are left out. An empty set is silence.
24
+ #
25
+ # RED. Exit 2 with the end of the check's output, which Claude Code hands back to the model so the turn goes on to
26
+ # fix it. Once per turn: Claude Code marks the next Stop of the same turn `stop_hook_active`, and that Stop passes.
27
+ #
28
+ # MODES: CREW_TURN_CHECK=off nothing runs. CREW_TURN_CHECK_TIMEOUT seconds the check may take (default 120, at
29
+ # most 170, under the hook's own 180). NOT A GATE ON ITS OWN FAILURE: an error of this script is silence (exit 0);
30
+ # the only exit 2 is a check that ran and failed.
31
+ set -uo pipefail
32
+ case "${CREW_TURN_CHECK:-on}" in off|OFF|0|false) exit 0 ;; esac
33
+ trap 'exit 0' EXIT
34
+ export LC_ALL=C
35
+ IN=""; IFS= read -r -d '' -t "${CREW_STDIN_TIMEOUT:-2}" IN 2>/dev/null; [ "$?" -gt 128 ] && exit 0
36
+
37
+ _TC_HERE="${BASH_SOURCE%/*}"; [ "$_TC_HERE" = "${BASH_SOURCE}" ] && _TC_HERE=.
38
+ P="${CLAUDE_PROJECT_DIR:-$PWD}"; cd "$P" 2>/dev/null || exit 0
39
+
40
+ # The declared command, read with the builtin `read`: a project that declared none pays no process at all.
41
+ FC=""
42
+ if [ -f CLAUDE.md ]; then
43
+ while IFS= read -r _l || [ -n "$_l" ]; do
44
+ case "$_l" in "Fast check (files):"*) FC="${_l#Fast check (files):}"; break ;; esac
45
+ done < CLAUDE.md
46
+ fi
47
+ FC="${FC%$'\r'}"; FC="${FC#"${FC%%[![:space:]]*}"}"; FC="${FC%"${FC##*[![:space:]]}"}"
48
+ case "$FC" in '`'*'`') FC="${FC#\`}"; FC="${FC%\`}" ;; esac
49
+ case "$FC" in ''|'<'*|none|None|NONE) exit 0 ;; esac
50
+
51
+ _field(){ # $1 = key -> _F, the payload's string value (letters, digits, - and _ only), "" when absent
52
+ local r="${IN#*\"$1\"}"; _F=""
53
+ [ "$r" = "$IN" ] && return 0
54
+ r="${r#"${r%%[![:space:]:]*}"}"; r="${r#\"}"; _F="${r%%\"*}"
55
+ case "$_F" in *[!A-Za-z0-9_-]*) _F="" ;; esac
56
+ }
57
+ _field hook_event_name; EV="$_F"
58
+ _field session_id; SID="$_F"; [ -n "$SID" ] || exit 0
59
+
60
+ GD="$(git rev-parse --absolute-git-dir 2>/dev/null)" || exit 0
61
+ SNAPD="$GD/crewforth-turn"; SNAP="$SNAPD/$SID"
62
+ _now(){ if [ "${BASH_VERSINFO[0]}" -ge 5 ] || { [ "${BASH_VERSINFO[0]}" -eq 4 ] && [ "${BASH_VERSINFO[1]}" -ge 2 ]; }; then printf -v NOW '%(%s)T' -1; else NOW="$(date +%s)"; fi; }
63
+
64
+ # Dirty paths, NUL-separated, from `status -z` (a rename's old name is dropped; a deleted file is dropped).
65
+ _dirty(){ # -> DIRTY array
66
+ DIRTY=()
67
+ local rec st path skip=0
68
+ while IFS= read -r -d '' rec; do
69
+ if [ "$skip" = 1 ]; then skip=0; continue; fi
70
+ st="${rec:0:2}"; path="${rec:3}"
71
+ case "$st" in R*|C*) skip=1 ;; esac
72
+ case "$st" in *D*) continue ;; esac
73
+ [ -f "$path" ] && DIRTY+=("$path")
74
+ done < <(git -c core.fsmonitor= status --porcelain=v1 -z --untracked-files=all 2>/dev/null)
75
+ }
76
+ _hashes(){ # paths in "$@" -> HASH array, one content id per path (git hash-object, one process)
77
+ HASH=()
78
+ [ "$#" -gt 0 ] || return 0
79
+ local h
80
+ while IFS= read -r h; do HASH+=("$h"); done < <(printf '%s\n' "$@" | git hash-object --stdin-paths 2>/dev/null)
81
+ }
82
+
83
+ if [ "$EV" = UserPromptSubmit ]; then
84
+ HEAD="$(git rev-parse -q --verify HEAD 2>/dev/null)"
85
+ _dirty; _hashes ${DIRTY[@]+"${DIRTY[@]}"}; _now
86
+ mkdir -p "$SNAPD" 2>/dev/null || exit 0
87
+ {
88
+ printf 'head\t%s\ntime\t%s\n' "${HEAD:--}" "$NOW"
89
+ i=0; while [ "$i" -lt "${#DIRTY[@]}" ]; do printf 'f\t%s\t%s\n' "${HASH[$i]:--}" "${DIRTY[$i]}"; i=$((i+1)); done
90
+ } > "$SNAP.tmp" 2>/dev/null && mv -f "$SNAP.tmp" "$SNAP" 2>/dev/null
91
+ exit 0
92
+ fi
93
+
94
+ [ "$EV" = Stop ] || exit 0
95
+ case "$IN" in *'"stop_hook_active":true'*|*'"stop_hook_active": true'*) exit 0 ;; esac
96
+ [ -f "$SNAP" ] || exit 0
97
+
98
+ # ---- the turn's files -----------------------------------------------------------------------------------------
99
+ # No associative arrays: macOS ships bash 3.2. The snapshot's dirty files are one newline-framed string of
100
+ # "<content id> <path>" lines, and a lookup is one `case`.
101
+ SH="-"; ST=0; BEFORE=$'\n'
102
+ while IFS=$'\t' read -r k a b; do
103
+ case "$k" in head) SH="$a" ;; time) ST="$a" ;; f) BEFORE="$BEFORE$a $b"$'\n' ;; esac
104
+ done < "$SNAP"
105
+ rm -f "$SNAP" 2>/dev/null
106
+ case "$ST" in ''|*[!0-9]*) ST=0 ;; esac
107
+
108
+ SEEN=$'\n'; FILES=()
109
+ _add(){
110
+ [ -n "$1" ] && [ -f "$1" ] || return 0
111
+ case "$SEEN" in *$'\n'"$1"$'\n'*) return 0 ;; esac
112
+ SEEN="$SEEN$1"$'\n'; FILES+=("$1")
113
+ }
114
+
115
+ _dirty; _hashes ${DIRTY[@]+"${DIRTY[@]}"}
116
+ i=0; while [ "$i" -lt "${#DIRTY[@]}" ]; do
117
+ p="${DIRTY[$i]}"
118
+ case "$BEFORE" in *$'\n'"${HASH[$i]:-x} $p"$'\n'*) ;; *) _add "$p" ;; esac
119
+ i=$((i+1))
120
+ done
121
+
122
+ HEAD="$(git rev-parse -q --verify HEAD 2>/dev/null)"
123
+ if [ -n "$HEAD" ] && [ "$SH" != - ] && [ "$HEAD" != "$SH" ]; then
124
+ # Only a turn whose every move of HEAD was a commit of its own counts its commits.
125
+ OURS=1; N=0
126
+ while IFS=$'\t' read -r sel subj; do
127
+ t="${sel#*@\{}"; t="${t%\}}"; case "$t" in *[!0-9]*|'') continue ;; esac
128
+ [ "$t" -ge "$ST" ] || break
129
+ N=$((N+1))
130
+ case "$subj" in "commit:"*|"commit (amend):"*|"commit (initial):"*|"cherry-pick:"*|"revert:"*) ;; *) OURS=0 ;; esac
131
+ done < <(git log -g --date=unix --format='%gd%x09%gs' -n 50 HEAD 2>/dev/null)
132
+ if [ "$OURS" = 1 ] && [ "$N" -gt 0 ]; then
133
+ while IFS= read -r -d '' p; do _add "$p"; done < <(git -c core.fsmonitor= diff --name-only -z --no-renames --diff-filter=d --no-ext-diff "$SH" "$HEAD" -- 2>/dev/null)
134
+ fi
135
+ fi
136
+ [ "${#FILES[@]}" -gt 0 ] || exit 0
137
+
138
+ # ---- the command ---------------------------------------------------------------------------------------------
139
+ CMD="$FC"
140
+ case "$FC" in *'<file>'*)
141
+ Q=""; n=0
142
+ for p in "${FILES[@]}"; do
143
+ case "$p" in *"'"*|*[\;\&\|\<\>\`\$]*|-*) continue ;; esac
144
+ Q="${Q:+$Q }'$p'"; n=$((n+1)); [ "$n" -ge 50 ] && break
145
+ done
146
+ [ -n "$Q" ] || exit 0
147
+ CMD="${FC//<file>/$Q}" ;;
148
+ esac
149
+
150
+ [ -f "$_TC_HERE/guard-agent-model.sh" ] || exit 0
151
+ . "$_TC_HERE/guard-agent-model.sh"
152
+ declare -F _am_verify_ok >/dev/null 2>&1 || exit 0
153
+ _am_state
154
+ _field permission_mode; PM="${_F:-default}"
155
+ if ! _am_verify_ok "$CMD" "$PM"; then
156
+ # Said once per session: a check that never runs must not look like one that passes.
157
+ if mkdir -p "$SNAPD" 2>/dev/null && [ ! -e "$SNAPD/$SID.said" ] && : > "$SNAPD/$SID.said" 2>/dev/null; then
158
+ W="${_AMVW//\\/\\\\}"; W="${W//\"/\\\"}"
159
+ printf '{"systemMessage":"crew: the fast check of this turn was not run: %s. CLAUDE.md declares it under Fast check (files)."}\n' "$W"
160
+ fi
161
+ exit 0
162
+ fi
163
+
164
+ LIM="${CREW_TURN_CHECK_TIMEOUT:-120}"; case "$LIM" in ''|*[!0-9]*) LIM=120 ;; esac; [ "$LIM" -le 170 ] || LIM=170
165
+ OUTF="$SNAPD/$SID.out"; mkdir -p "$SNAPD" 2>/dev/null || exit 0
166
+ if command -v setsid >/dev/null 2>&1; then ( exec setsid bash -c "$CMD" ) > "$OUTF" 2>&1 </dev/null &
167
+ else set -m; ( exec bash -c "$CMD" ) > "$OUTF" 2>&1 </dev/null & set +m; fi
168
+ VPID=$!; W=0; RC=""
169
+ while kill -0 "$VPID" 2>/dev/null; do
170
+ if [ "$W" -ge "$LIM" ]; then
171
+ kill -TERM -- "-$VPID" 2>/dev/null; kill -TERM "$VPID" 2>/dev/null; sleep 1
172
+ kill -KILL -- "-$VPID" 2>/dev/null; kill -KILL "$VPID" 2>/dev/null; RC=timeout; break
173
+ fi
174
+ sleep 1; W=$((W+1))
175
+ done
176
+ if [ "$RC" = timeout ]; then
177
+ wait "$VPID" 2>/dev/null; rm -f "$OUTF" 2>/dev/null
178
+ printf '{"systemMessage":"crew: the fast check of this turn did not finish in %ss and was stopped; nothing is concluded from it."}\n' "$LIM"
179
+ exit 0
180
+ fi
181
+ wait "$VPID" 2>/dev/null; RC=$?
182
+ TAIL="$(tail -n 20 "$OUTF" 2>/dev/null | cut -c1-300)"; rm -f "$OUTF" 2>/dev/null
183
+ [ "$RC" = 0 ] && exit 0
184
+ {
185
+ printf 'The fast check this project declares failed on the files this turn changed (exit %s): %s\n' "$RC" "$CMD"
186
+ printf 'The end of its output:\n%s\n' "$TAIL"
187
+ printf 'Fix what it reports in those files, or say to the user why it stays red. This is said once per turn.\n'
188
+ } >&2
189
+ trap - EXIT
190
+ exit 2
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ # PreToolUse + PostToolUse on EnterWorktree — a worktree the session enters carries Crewforth with it.
3
+ #
4
+ # Why: `.claude/` is gitignored in a fresh install, so a worktree made with `git worktree add ../x` has none. While
5
+ # the session that entered it lives, its hooks keep running from the main checkout. But Claude Code records the
6
+ # session under the worktree once it enters one, and a session resumed there reads its settings from the worktree:
7
+ # with no `.claude/settings.json` there, NO hook is registered — no commit approval, no write gate, no fill line.
8
+ # Measured on Claude Code 2.1.309: a resumed session in such a worktree ran no project hook (0 of 1), the same
9
+ # worktree with `.claude/` copied in ran it (1 of 1). A field session ran four and a half hours that way, printing a
10
+ # fill it had made up. A worktree Claude Code creates under `.claude/worktrees/` is not affected (measured: the
11
+ # resumed session there still ran the main checkout's hooks), so it is left alone.
12
+ #
13
+ # What it does: before the move (PreToolUse, when the call names an existing path) and after it (PostToolUse, the
14
+ # path the tool reports), a linked worktree of THIS repository with no `.claude/settings.json` gets a copy of this
15
+ # checkout's `.claude/`, minus what belongs to one checkout only (`worktrees/`, `state/`, the review record, the
16
+ # gate log). If the copy cannot be made, the PreToolUse call is refused (exit 2) with the reason, and the
17
+ # PostToolUse call tells the user and the model, since the move already happened.
18
+ set -uo pipefail
19
+ # ---- CREW-FAILCLOSED ---------------------------------------------------------------------------------------
20
+ # A gate that stops on an error of its own must refuse the call, not let it through. Claude Code blocks a tool call
21
+ # on exit 2 only; a hook that dies with 1, or runs on past the rule that broke, has allowed it. Both happened in
22
+ # one defect (measured, bash 5): a local array declared and not set is "unbound" under `set -u`, the rule that read
23
+ # it was abandoned in the middle, the script went on with the NEXT top-level command and left with 0, and
24
+ # `rm .claude/hooks/pre-commit` passed. bash does one of two things with such an error, by the kind of error:
25
+ # * it leaves the shell with a status that is neither 0 nor 2 -> the EXIT trap below turns that into a refusal;
26
+ # * it abandons the top-level command it was in and continues -> the whole gate IS one top-level command,
27
+ # _gate_main, which only ever ends by `exit`. If it RETURNS, it was abandoned, and the line after it refuses.
28
+ # Not covered, and said so: an error inside a command substitution (only that subshell ends), and a command
29
+ # that is not found (status 127, the script continues inside the same command).
30
+ # Byte-identical in every PreToolUse gate; the suite pins it. No process is started.
31
+ _crew_stop(){ # $1 = what happened
32
+ declare -F gatelog >/dev/null 2>&1 && gatelog BLOCK 4.5 "the gate stopped on an error of its own"
33
+ echo "GUARD (§4.5): this gate stopped on an error of its own ($1) before it finished judging the call, so the call is refused." >&2
34
+ echo "Nothing about the call itself was found. The error is the line above these two; it is a defect in Crewforth, not in what you ran. Run the doctor (bash .claude/eval/doctor.sh, Bash tool, not PowerShell) and report it." >&2
35
+ exit 2
36
+ }
37
+ trap '_crew_rc=$?; trap - EXIT; case "$_crew_rc" in 0|2) exit "$_crew_rc" ;; esac; _crew_stop "exit status $_crew_rc"' EXIT
38
+ # ---- /CREW-FAILCLOSED
39
+ unq(){ local p="${1#\"}"; p="${p%\"}"; p="${p//\\\\//}"; p="${p//\\//}"; printf '%s' "$p"; } # JSON string -> path; \ -> /
40
+ _gate_main(){
41
+ local IN HERE SRC READER V EV P_IN P_OUT T MINE THEIRS TOP ERR e WHY SAFE
42
+ IFS= read -r -d '' IN || true
43
+ HERE="$(cd "$(dirname "$0")" && pwd)"
44
+ SRC="$(cd "$HERE/.." && pwd)" # this checkout's .claude/
45
+ READER="$SRC/eval/lib/settings-json.awk"
46
+ [ -f "$READER" ] || exit 0 # no reader, nothing to read the payload with: fail open
47
+
48
+ V="$(printf '%s' "$IN" | awk -v op=gets -v path='hook_event_name|tool_input.path|tool_response.worktreePath' -f "$READER" - 2>/dev/null)"
49
+ EV="${V%%$'\t'*}"; V="${V#*$'\t'}"; P_IN="${V%%$'\t'*}"; V="${V#*$'\t'}"; P_OUT="${V%%$'\t'*}"
50
+ EV="$(unq "$EV")"
51
+ case "$EV" in
52
+ PreToolUse) T="$(unq "$P_IN")" ;;
53
+ PostToolUse) T="$(unq "$P_OUT")" ;;
54
+ *) exit 0 ;;
55
+ esac
56
+ [ -n "$T" ] && [ -d "$T" ] || exit 0 # no path (Claude creates one under .claude/worktrees/) or none yet
57
+
58
+ # Only a linked worktree of THIS repository, asked of git rather than compared as spelled (C:/ against /c/).
59
+ MINE="$(git -C "$SRC/.." rev-parse --git-common-dir 2>/dev/null)" || exit 0
60
+ THEIRS="$(git -C "$T" rev-parse --git-common-dir 2>/dev/null)" || exit 0
61
+ case "$MINE" in /*|?:*) ;; *) MINE="$SRC/../$MINE" ;; esac
62
+ case "$THEIRS" in /*|?:*) ;; *) THEIRS="$T/$THEIRS" ;; esac
63
+ [ "$MINE" -ef "$THEIRS" ] || exit 0
64
+ TOP="$(git -C "$T" rev-parse --show-toplevel 2>/dev/null)" || exit 0
65
+ [ "$TOP" -ef "$SRC/.." ] && exit 0 # the main checkout itself
66
+ case "$(cd "$TOP" && pwd -P)/" in "$(cd "$SRC" && pwd -P)/worktrees/"*) exit 0 ;; esac # Claude Code's own: unaffected
67
+ [ -f "$TOP/.claude/settings.json" ] && exit 0 # already carries it (or .claude/ is tracked)
68
+
69
+ ERR=""
70
+ mkdir -p "$TOP/.claude" 2>/dev/null || ERR="could not create $TOP/.claude"
71
+ if [ -z "$ERR" ]; then
72
+ for e in "$SRC"/* "$SRC"/.[!.]*; do
73
+ [ -e "$e" ] || continue
74
+ case "${e##*/}" in worktrees|state|review-pass.json|gate-log.tsv) continue ;; esac
75
+ cp -R "$e" "$TOP/.claude/" 2>/dev/null || { ERR="could not copy ${e##*/}"; break; }
76
+ done
77
+ fi
78
+ [ -z "$ERR" ] && { [ -f "$TOP/.claude/settings.json" ] && [ -d "$TOP/.claude/hooks" ] || ERR="the copy has no settings.json or hooks/"; }
79
+ [ -z "$ERR" ] && exit 0
80
+
81
+ WHY="Crewforth could not put its .claude/ into the worktree $TOP ($ERR). A session resumed there would load no Crewforth hook: no commit approval, no write gate, no fill reading."
82
+ if [ "$EV" = PreToolUse ]; then
83
+ echo "$WHY Fix the cause above, then enter the worktree again." >&2
84
+ exit 2
85
+ fi
86
+ SAFE="${WHY//\\/\\\\}"; SAFE="${SAFE//\"/\\\"}"
87
+ printf '{"systemMessage":"⚠️ %s","hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"%s Tell the user, and do not resume this session in that worktree until it is fixed."}}\n' "$SAFE" "$SAFE"
88
+ exit 0
89
+ }
90
+ _gate_main "$@"
91
+ _crew_stop "a command of the gate was abandoned"
@@ -1,9 +1,10 @@
1
- # Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.2.1 (below VERSION 3.3.0) — do not edit by hand.
1
+ # Generated by packaging/gen-legacy-blobs.sh from the final release tags v1.0.0..v3.4.0 (below VERSION 3.5.0) — do not edit by hand.
2
2
  # The flat files Crewforth owns: a copy whose bytes (CR aside) are one of these ids was never edited.
3
3
  # install_path blob_sha1 first_tag last_tag
4
4
  .claude/AGENT_TEMPLATE.md 075e2c26706c06f56af01652cd24c36918513bee v3.0.0 v3.0.4
5
5
  .claude/AGENT_TEMPLATE.md 2c8223d811245e3627a3011bb64fb9e4b0bf3b94 v3.2.0 v3.2.1
6
6
  .claude/AGENT_TEMPLATE.md 3fdc551d221ee3665b21ef8cc3563de861a9abe6 v1.4.0 v1.6.3
7
+ .claude/AGENT_TEMPLATE.md 59d3af63b0480696dec38136e124bd582066fb16 v3.3.0 v3.4.0
7
8
  .claude/AGENT_TEMPLATE.md 686fed306c873b45c245c77a95a8e6de6ee383d5 v1.7.0 v2.11.0
8
9
  .claude/AGENT_TEMPLATE.md 7ab19417b6ff3ac732e2c894be17ffb3ec091f7d v3.1.0 v3.1.1
9
10
  .claude/AGENT_TEMPLATE.md 981df9ea27b6f1c5ab5cb509d40875a07603858d v2.12.0 v2.13.0
@@ -29,6 +30,7 @@
29
30
  .claude/DISCIPLINE.md b7e1066451a8590e0a2607ad0a6fe80b21599c53 v1.1.3 v1.1.8
30
31
  .claude/DISCIPLINE.md b90a067ee323a08146862b889293f8feb49f8e7e v3.1.0 v3.1.1
31
32
  .claude/DISCIPLINE.md bf062c2b51a9cf3cc4d0caac389c148cf6fdc977 v2.12.0 v2.12.0
33
+ .claude/DISCIPLINE.md c04f53866b84c54aa0e60089cb54cdc7c9aa8df1 v3.3.0 v3.4.0
32
34
  .claude/DISCIPLINE.md c693f1bd5832ce570058c6eedcacb5dff13d89e8 v1.10.0 v1.10.0
33
35
  .claude/DISCIPLINE.md cf881a5229d85850194a98b8d0d0438a8eaaa9d6 v3.2.0 v3.2.1
34
36
  .claude/DISCIPLINE.md d63a01a183c9a80d739c840a511d338dc2765820 v1.4.0 v1.4.4
@@ -45,4 +47,4 @@
45
47
  .claude/README.md 97d4dd1f7f1695052cc1d06cc43f5bcdc603c989 v1.0.0 v1.0.4
46
48
  .claude/README.md a3419891cfda7385564008e4befc1007bc5364cb v1.1.1 v1.7.0
47
49
  .claude/README.md a3b76e0fa31267610d28b742c122a962f6db59c4 v3.0.1 v3.0.4
48
- .claude/README.md b6c9b57a67d25b7e4781e4eb61ff9637e6abe5ef v3.1.0 v3.2.1
50
+ .claude/README.md b6c9b57a67d25b7e4781e4eb61ff9637e6abe5ef v3.1.0 v3.4.0
package/kit/settings.json CHANGED
@@ -119,6 +119,17 @@
119
119
  "timeout": 600
120
120
  }
121
121
  ]
122
+ },
123
+ {
124
+ "matcher": "EnterWorktree",
125
+ "hooks": [
126
+ {
127
+ "type": "command",
128
+ "shell": "bash",
129
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/worktree-provision.sh",
130
+ "timeout": 600
131
+ }
132
+ ]
122
133
  }
123
134
  ],
124
135
  "UserPromptSubmit": [
@@ -141,6 +152,12 @@
141
152
  "shell": "bash",
142
153
  "command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/route-hint.sh",
143
154
  "timeout": 60
155
+ },
156
+ {
157
+ "type": "command",
158
+ "shell": "bash",
159
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/turn-check.sh",
160
+ "timeout": 30
144
161
  }
145
162
  ]
146
163
  }
@@ -159,6 +176,18 @@
159
176
  "shell": "bash",
160
177
  "command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/prompt-approval.sh",
161
178
  "timeout": 60
179
+ },
180
+ {
181
+ "type": "command",
182
+ "shell": "bash",
183
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/map-stale.sh",
184
+ "timeout": 30
185
+ },
186
+ {
187
+ "type": "command",
188
+ "shell": "bash",
189
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/turn-check.sh",
190
+ "timeout": 180
162
191
  }
163
192
  ]
164
193
  }
@@ -220,6 +249,52 @@
220
249
  "timeout": 60
221
250
  }
222
251
  ]
252
+ },
253
+ {
254
+ "matcher": "EnterWorktree",
255
+ "hooks": [
256
+ {
257
+ "type": "command",
258
+ "shell": "bash",
259
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/worktree-provision.sh",
260
+ "timeout": 60
261
+ }
262
+ ]
263
+ },
264
+ {
265
+ "matcher": "Bash|Edit|Write|MultiEdit|NotebookEdit",
266
+ "hooks": [
267
+ {
268
+ "type": "command",
269
+ "shell": "bash",
270
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/loop-guard.sh",
271
+ "timeout": 30
272
+ }
273
+ ]
274
+ },
275
+ {
276
+ "matcher": "Edit|Write|MultiEdit|NotebookEdit",
277
+ "hooks": [
278
+ {
279
+ "type": "command",
280
+ "shell": "bash",
281
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/map-stale.sh",
282
+ "timeout": 30
283
+ }
284
+ ]
285
+ }
286
+ ],
287
+ "PostToolUseFailure": [
288
+ {
289
+ "matcher": "Bash|Edit|Write|MultiEdit|NotebookEdit",
290
+ "hooks": [
291
+ {
292
+ "type": "command",
293
+ "shell": "bash",
294
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/loop-guard.sh",
295
+ "timeout": 30
296
+ }
297
+ ]
223
298
  }
224
299
  ],
225
300
  "PreCompact": [
@@ -269,6 +344,17 @@
269
344
  }
270
345
  ]
271
346
  },
347
+ {
348
+ "matcher": "startup|resume",
349
+ "hooks": [
350
+ {
351
+ "type": "command",
352
+ "shell": "bash",
353
+ "command": "cd \"$CLAUDE_PROJECT_DIR\" 2>/dev/null; bash .claude/hooks/branch-fresh.sh",
354
+ "timeout": 30
355
+ }
356
+ ]
357
+ },
272
358
  {
273
359
  "matcher": "startup",
274
360
  "hooks": [
@@ -8,9 +8,9 @@ description: |
8
8
  # Accessibility (a11y)
9
9
 
10
10
  <!-- routing-eval reads the next line; why it sits in the body: AGENT_TEMPLATE.md -->
11
- Trigger phrases: "a11y", "accessibility", "WCAG", "screen reader", "keyboard navigation", "contrast", "ARIA", "keyboard only", "with a keyboard", "keyboard focus", "without a mouse", "focus ring", "tab order", "alt text", "color blind", "colour blind", "voiceover", "talkback", "reduced motion"
11
+ Trigger phrases: "a11y", "accessibility", "WCAG", "screen reader", "keyboard navigation", "contrast", "ARIA", "keyboard only", "with a keyboard", "keyboard focus", "without a mouse", "focus ring", "tab order", "alt text", "color blind", "colour blind", "voiceover", "talkback", "reduced motion", "target size", "focus not obscured", "focus is hidden", "WCAG 2.2"
12
12
 
13
- Goal: make the interface usable by **everyone**, including keyboard, screen reader, and low vision. Baseline target: **WCAG 2.1 AA**.
13
+ Goal: make the interface usable by **everyone**, including keyboard, screen reader, and low vision. Baseline target: **WCAG 2.2 AA**.
14
14
  Stack-agnostic (web/React/RN); do a web search when needed for framework-specific APIs.
15
15
 
16
16
  ## Checklist
@@ -21,9 +21,56 @@ Stack-agnostic (web/React/RN); do a web search when needed for framework-specifi
21
21
  - [ ] **Alt text**: `alt` on meaningful images; `alt=""` on decorative images
22
22
  - [ ] **Forms**: every input has a `label`; error message programmatically linked (`aria-describedby`)
23
23
  - [ ] **ARIA**: only when needed; wrong ARIA is worse than no ARIA; role/name/state correct
24
- - [ ] **Motion/animation**: respect `prefers-reduced-motion`
24
+ - [ ] **Motion/animation**: respect `prefers-reduced-motion`; anything that moves for more than 5 s can be paused (2.2.2)
25
25
  - [ ] **Language**: `<html lang>` correct (coordinate with i18n)
26
26
 
27
+ WCAG 2.2 added these; a 2.1 checklist misses all of them:
28
+ - [ ] **Focus not obscured** (2.4.11): a focused element is never fully hidden under a sticky header, footer or banner
29
+ - [ ] **Dragging** (2.5.7): every drag has a single-pointer alternative (buttons, a menu)
30
+ - [ ] **Target size** (2.5.8): pointer targets ≥ 24×24 CSS px, or spaced so a 24 px circle on each does not overlap another
31
+ - [ ] **Redundant entry** (3.3.7): information already given in the same flow is filled in or selectable, not asked again
32
+ - [ ] **Accessible authentication** (3.3.8): no memory or puzzle test to sign in without an alternative; paste and
33
+ password managers work
34
+
35
+ Commonly missed, all checkable in the markup:
36
+ - [ ] **Autocomplete** (1.3.5): personal-data inputs carry the right `autocomplete` token
37
+ - [ ] **No paste blocking** on any input, passwords and confirmations included
38
+ - [ ] **Submit stays enabled**: a disabled-until-valid button hides why; validate on submit and say what is wrong
39
+ - [ ] **Zoom and reflow** (1.4.4, 1.4.10): usable at 200% text and at 320 CSS px wide; no `maximum-scale=1` or
40
+ `user-scalable=no` in the viewport meta
41
+ - [ ] **Skip link** (2.4.1): the first focusable element jumps past repeated navigation to `main`
42
+ - [ ] **No flashing** (2.3.1): nothing flashes more than three times a second
43
+ - [ ] **Error summary**: a form that fails lists its errors in one focusable block that links to each field
44
+ - [ ] **No positive `tabindex`**: `0` and `-1` only; order comes from the DOM
45
+
46
+ **Criteria, not conventions.** A failed A or AA criterion, or a task a keyboard or screen-reader user cannot finish,
47
+ is a finding. AAA, an ARIA Authoring Practices pattern, or a 44 px target where 24 px passes is a recommendation:
48
+ say so, and never rank it with a finding.
49
+
50
+ ## Before you finish: grep for these
51
+ | Pattern in the diff | Why it fails |
52
+ |---|---|
53
+ | `outline: none` / `outline: 0` with no `:focus-visible` rule | focus becomes invisible (2.4.7) |
54
+ | `<div onClick` / `<span onClick` with no role and key handler | not reachable by keyboard (2.1.1) |
55
+ | `tabindex="1"` or higher | breaks the reading order (2.4.3) |
56
+ | `disabled={!isValid}` on a submit | hides why the form cannot be sent |
57
+ | a live region rendered together with its text | many readers announce nothing: render it empty first |
58
+ | `maximum-scale=1`, `user-scalable=no` | blocks zoom (1.4.4) |
59
+ | `aria-hidden="true"` on an element that holds something focusable | focus lands on nothing the reader can name |
60
+ | `<img` with no `alt` | no text alternative (1.1.1) |
61
+ | `onDrag`/`draggable` with no button or menu path | no single-pointer alternative (2.5.7) |
62
+
63
+ ## Symptom → cause → fix
64
+ | Symptom | Usual cause | Fix |
65
+ |---|---|---|
66
+ | Reader says "button" and nothing else | icon-only button with no name | `aria-label`, or visually hidden text |
67
+ | Tab skips an element | a `div` handling clicks | a real `button` / `a` |
68
+ | Focus lost after a modal closes | focus not returned | return it to the element that opened the modal |
69
+ | An error appears and nothing is announced | message not tied to the field or a live region | `aria-describedby` on the field; a status region for the summary |
70
+ | Layout breaks at 200% zoom | fixed heights or widths in px | let text boxes grow; `min-width: 0` on flex and grid children |
71
+
72
+ Recipes for focus, forms and announcements (which channel says what, in order): `references/recipes.md`.
73
+
27
74
  ## How
28
75
  1. **Start with semantics** — the right element solves 80%. Use `button` instead of `role="button"`+`div`.
29
76
  2. **Navigate with the keyboard** — drop the mouse, run the whole flow with Tab/Shift-Tab/Enter/Escape; is focus visible and its order sensible.
@@ -0,0 +1,41 @@
1
+ # Accessibility recipes: focus, forms, announcements
2
+
3
+ Loaded from the `a11y` skill when a change moves focus, validates a form or tells the user something happened.
4
+ Stack-neutral; the React Native and Flutter equivalents are named in the skill.
5
+
6
+ ## Focus
7
+ - **Where focus goes after an action.** Opening a dialog: to its first meaningful control, or to the dialog itself
8
+ when it starts with text. Closing it: back to the control that opened it. Deleting the focused row: to the next
9
+ row, or the previous one when it was the last. A route change: to the new page's `h1` (`tabindex="-1"`), or to a
10
+ skip target, so the reader starts at the content and not at the top of the navigation.
11
+ - **Visible focus.** Style `:focus-visible`, never remove `outline` without a replacement of at least 2 px with 3:1
12
+ contrast against what surrounds it. A sticky header must not cover the focused element: set `scroll-padding-top`
13
+ to its height.
14
+ - **Traps.** Only a modal traps focus, and Escape always leaves it. `inert` on the background is the simplest trap
15
+ where it is supported.
16
+
17
+ ## Forms
18
+ - Every field has a visible `label` tied by `for`/`id`; a placeholder is not a label.
19
+ - Validate on submit (and on blur for a field the user has left), not on every keystroke.
20
+ - On a failed submit: put a summary at the top that names how many errors there are and links to each field, move
21
+ focus to it, and tie each field's message to the field with `aria-describedby` and `aria-invalid="true"`.
22
+ - Keep the submit button enabled; a disabled button cannot be focused and does not say what is missing.
23
+ - Required fields say so in text, not only with a colour or an asterisk the reader skips.
24
+ - Known personal data carries its `autocomplete` token (`name`, `email`, `tel`, `street-address`, `one-time-code`,
25
+ `current-password`, `new-password`).
26
+
27
+ ## Announcements: the cheapest channel that works
28
+ Use the first rung that fits; each one further down interrupts more.
29
+ 1. **Move focus** when the user must act next (a dialog opened, an error summary).
30
+ 2. **`aria-describedby`** when the message belongs to one control (a field hint, a field error).
31
+ 3. **`role="status"`** (polite) for a result the user should hear without being interrupted: "Saved", "3 results".
32
+ 4. **`role="alert"`** (assertive) only for something that cannot wait: a session about to expire, a failed payment.
33
+
34
+ Rules that make the live regions work:
35
+ - Render the region **empty** when the page loads and change its text later. A region that arrives together with its
36
+ text is often not announced.
37
+ - One polite region per page is usually enough; reuse it instead of adding one per component.
38
+ - Do not announce what focus already says. Moving focus to an error summary and also putting it in an alert reads it
39
+ twice.
40
+ - Toasts that disappear on their own need a status region and enough time to read (at least 5 s, longer for longer
41
+ text), or a way to bring them back.
@@ -34,7 +34,14 @@ every teammate's next session opening, and it travels even when the board lives
34
34
  file stays the long form; the board entry is the part that has to reach people, so it must be readable on its
35
35
  own — a title nobody can act on ("auth decided") is not a record.
36
36
 
37
- ## Format (docs/adr/NNNN-short-title.md, ~1 page)
37
+ ## Where it goes: the project's scheme first
38
+ Before writing, look for records that already exist: a decisions folder (`docs/adr/`, `doc/adr/`,
39
+ `docs/decisions/`, `Documentation/Decisions/`, `architecture/decisions/`), an `.adr-dir` file (adr-tools), or files
40
+ named like `ADR-004-…` or `0004-…`. Found: continue it, same folder, extension (.md, .rst, .adoc), numbering and
41
+ heading style, next number. Not found: `docs/adr/NNNN-short-title.md` below. Never start a second scheme beside one
42
+ that exists; two numbering lines make every "see ADR 12" ambiguous.
43
+
44
+ ## Format (fallback: docs/adr/NNNN-short-title.md, ~1 page)
38
45
  ```
39
46
  # NNNN. <Decision title>
40
47
  Status: proposed | accepted | rejected | superseded (by NNNN)
@@ -46,11 +53,15 @@ What was decided (clear, one sentence + rationale)?
46
53
  Pros / cons / accepted trade-offs.
47
54
  ## Alternatives considered
48
55
  Why were they not chosen?
56
+ Verified against: <package>@<version> · <docs or source> · <date> — or "none" (no external fact relied on)
49
57
  ```
50
58
 
51
59
  ## Principles
52
60
  - **Invariant:** a new decision marks the old ADR as `superseded`; an ADR is **never deleted** (decision history is preserved).
53
61
  - Numbered and dated; keep it short.
62
+ - **A number a test enforces moves with its ADR.** When a test or gate pins a figure an ADR set (a cap, a count, a
63
+ budget), the change that moves the figure amends that ADR in the same commit; a figure that drifts from its record
64
+ leaves the next reader two answers.
54
65
 
55
66
  ## Deliberately bypassing a rule is a decision too
56
67
  A gate the user knowingly overrode — a deferred DoD item, an accepted known gap, a trimmed scope — belongs here
@@ -67,7 +78,8 @@ Record it in the same file as any other decision, with the fields that make it r
67
78
  ```
68
79
 
69
80
  `revisit` is the load-bearing field. A bypass with no condition attached is permanent by default, and nobody
70
- chose permanence. See [[confidence-check]], which is where most bypasses surface.
81
+ chose permanence. See [[confidence-check]], which is where most bypasses surface. A recorded bypass is the user's decision, not a
82
+ deferral (`CLAUDE.md`, "No deferral").
71
83
 
72
84
  ## DoD
73
85
  - Decision + rationale + rejected alternatives recorded; status current.
@@ -20,7 +20,8 @@ public API is a commitment; a breaking change is expensive. Stack-agnostic (REST
20
20
  - [ ] A clear **versioning** strategy (URL `/v1` or header); a breaking change means a new version
21
21
  - [ ] **Pagination/filtering/sorting** defined and consistent on large collections
22
22
  - [ ] **Backward compatibility**: adding a field is additive; removing a field or changing its meaning is breaking → version
23
- - [ ] **Idempotency** (for POST/payment-like cases) supported via a key when needed
23
+ - [ ] **Idempotency** (for POST/payment-like cases) supported via a key when needed: the key, its atomic claim and
24
+ the answers to a replay are in **`references/idempotency.md`**
24
25
  - [ ] The contract is documented in **OpenAPI**; example request/response present (coordinate with `docs-writer`)
25
26
 
26
27
  ## How
@@ -42,6 +43,10 @@ public API is a commitment; a breaking change is expensive. Stack-agnostic (REST
42
43
  | A new optional parameter | Add a required parameter |
43
44
  | A new enum value (if the consumer is tolerant) | Change a type/meaning, change a status code |
44
45
 
46
+ **Observable behaviour is the contract,** not only the documented fields: someone depends on the error text, the
47
+ order of a list, the default page size and how long a call takes. Changing one of those is breaking for them; say
48
+ it in the changelog like a field change.
49
+
45
50
  ## Invariant rules
46
51
  1. **A public API is a commitment** — a breaking change is not made silently; version + announcement.
47
52
  2. **Consistency > local cleverness** — a single naming/error/pagination pattern across the whole API.
@@ -0,0 +1,39 @@
1
+ # Idempotency keys
2
+
3
+ A retried POST that charges twice, ships twice or creates two rows is the bug this prevents. The key makes a retry
4
+ safe; the store behind it decides whether it really is.
5
+
6
+ ## The key
7
+ - **From the intent, not the request.** The client sends `Idempotency-Key` (a UUID made once per user action, kept
8
+ across its retries), or the server derives one from what the action means (order id + operation). A key made per
9
+ HTTP attempt protects nothing: every retry is new.
10
+ - **Scoped to the caller.** Store it per account or API client, so two callers cannot collide or read each other's
11
+ result.
12
+
13
+ ## Claiming it
14
+ - **Atomically.** Insert the key under a unique constraint (or an equivalent conditional write) before the side
15
+ effect runs. "Look it up, then insert" leaves a window where two retries both find nothing and both charge.
16
+ - **With a fingerprint of the payload.** Store a hash of the normalised body with the key.
17
+
18
+ ## Answers
19
+ | The request | The answer |
20
+ |---|---|
21
+ | New key | Run it; store the outcome with the key |
22
+ | Same key, same payload, finished | The stored response, status code included, without running it again |
23
+ | Same key, different payload | `422` (or `409`): a client bug, never a silent replay of the first result |
24
+ | Same key, first attempt still running | `409` with `Retry-After`, or wait briefly, or `202` with a status URL; never a second run |
25
+
26
+ Three outcomes are stored: **succeeded** (replay it), **failed in a way the client can fix** (replay the 4xx), and
27
+ **failed before or during the side effect** (5xx, timeout): release the key, or mark it retryable, so the next
28
+ retry can run. Storing a 5xx as final turns a passing outage into a permanent failure for that action.
29
+
30
+ ## Retention
31
+ Keep a key longer than the longest retry chain any client runs (its backoff schedule and the queue's redelivery
32
+ window), and say how long in the API docs. A key dropped after one hour is useless to a client that retries for a
33
+ day.
34
+
35
+ ## Review checks
36
+ - The side effect and the key claim are in one transaction, or the key is claimed first and the effect is
37
+ conditional on it.
38
+ - A downstream call (payment provider, email) gets its own idempotency key, derived from ours.
39
+ - A test sends the same key twice in parallel and finds one effect.