devflow-kit 3.3.0 → 3.4.0

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 (138) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/agents/code.md +330 -0
  3. package/{src/assets → dist}/agents/design.md +1 -1
  4. package/{src/assets → dist}/agents/diagnose.md +1 -2
  5. package/dist/agents/git.md +29 -56
  6. package/{src/assets → dist}/agents/knowledge.md +4 -3
  7. package/{src/assets → dist}/agents/research.md +2 -2
  8. package/{src/assets → dist}/agents/review.md +8 -7
  9. package/{src/assets → dist}/agents/scrutinize.md +1 -1
  10. package/dist/agents/skim.md +148 -0
  11. package/{src/assets → dist}/agents/triage.md +1 -1
  12. package/dist/cli/commands/init.js +62 -0
  13. package/dist/cli/commands/learning.js +38 -3
  14. package/dist/cli/commands/uninstall.js +42 -1
  15. package/dist/commands/bug-analysis.md +30 -8
  16. package/dist/commands/code-review.md +141 -60
  17. package/dist/commands/debug.md +14 -12
  18. package/dist/commands/dynamic-build.md +37 -38
  19. package/dist/commands/dynamic-plan.md +30 -18
  20. package/dist/commands/dynamic-profile.md +27 -13
  21. package/dist/commands/dynamic-tickets.md +28 -14
  22. package/dist/commands/explore.md +15 -13
  23. package/dist/commands/implement.md +33 -28
  24. package/dist/commands/plan.md +37 -24
  25. package/dist/commands/release.md +69 -4
  26. package/dist/commands/research.md +33 -11
  27. package/dist/commands/resolve.md +35 -32
  28. package/dist/commands/self-review.md +36 -23
  29. package/dist/core/agent-models.js +43 -0
  30. package/dist/core/assets.js +55 -10
  31. package/dist/core/claude-md-audit.js +190 -0
  32. package/dist/core/feature-switch.js +20 -1
  33. package/dist/core/flags.js +28 -0
  34. package/dist/core/fs-atomic.js +8 -3
  35. package/dist/core/learning-variants.js +213 -0
  36. package/dist/core/manifest.js +62 -0
  37. package/dist/core/mds-variants.js +38 -1
  38. package/dist/core/plugins.js +71 -9
  39. package/{src/assets → dist/learning-off}/agents/code.md +6 -10
  40. package/dist/learning-off/agents/design.md +119 -0
  41. package/dist/learning-off/agents/diagnose.md +210 -0
  42. package/dist/learning-off/agents/knowledge.md +90 -0
  43. package/dist/learning-off/agents/research.md +149 -0
  44. package/dist/learning-off/agents/review.md +228 -0
  45. package/dist/learning-off/agents/scrutinize.md +117 -0
  46. package/{src/assets → dist/learning-off}/agents/skim.md +1 -8
  47. package/dist/learning-off/agents/triage.md +163 -0
  48. package/dist/learning-off/commands/bug-analysis.md +420 -0
  49. package/dist/learning-off/commands/code-review.md +525 -0
  50. package/dist/learning-off/commands/debug.md +294 -0
  51. package/dist/learning-off/commands/dynamic-build.md +1255 -0
  52. package/dist/learning-off/commands/dynamic-plan.md +424 -0
  53. package/dist/learning-off/commands/dynamic-profile.md +214 -0
  54. package/dist/learning-off/commands/dynamic-tickets.md +632 -0
  55. package/dist/learning-off/commands/explore.md +210 -0
  56. package/dist/learning-off/commands/implement.md +808 -0
  57. package/dist/learning-off/commands/plan.md +664 -0
  58. package/dist/learning-off/commands/release.md +310 -0
  59. package/dist/learning-off/commands/research.md +222 -0
  60. package/dist/learning-off/commands/resolve.md +837 -0
  61. package/dist/learning-off/commands/self-review.md +266 -0
  62. package/dist/skills/git/references/tracker/_contract.md +33 -0
  63. package/dist/skills/git/references/tracker/github/fetch-issue.md +2 -0
  64. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +2 -0
  65. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +4 -0
  66. package/dist/skills/git/references/tracker/github/post-wave-report.md +2 -0
  67. package/dist/skills/git/references/tracker/github/setup-task.md +12 -0
  68. package/dist/skills/git/references/tracker/jira/associate-release.md +1 -1
  69. package/dist/skills/git/references/tracker/jira/fetch-issue.md +2 -0
  70. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +2 -0
  71. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +4 -0
  72. package/dist/skills/git/references/tracker/jira/post-wave-report.md +2 -0
  73. package/dist/skills/git/references/tracker/jira/setup-task.md +14 -2
  74. package/dist/skills/git/references/tracker/linear/associate-release.md +1 -1
  75. package/dist/skills/git/references/tracker/linear/fetch-issue.md +2 -0
  76. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +2 -0
  77. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +4 -0
  78. package/dist/skills/git/references/tracker/linear/post-wave-report.md +2 -0
  79. package/dist/skills/git/references/tracker/linear/setup-task.md +14 -2
  80. package/dist/targets/claude-code/installer.js +72 -36
  81. package/dist/targets/claude-code/language-stamp.js +185 -0
  82. package/dist/targets/claude-code/learning-install.js +489 -0
  83. package/package.json +1 -1
  84. package/src/assets/agents/code.mds +339 -0
  85. package/src/assets/agents/design.mds +149 -0
  86. package/src/assets/agents/diagnose.mds +225 -0
  87. package/src/assets/agents/evaluate.md +1 -3
  88. package/src/assets/agents/git.mds +29 -56
  89. package/src/assets/agents/knowledge.mds +125 -0
  90. package/src/assets/agents/research.mds +176 -0
  91. package/src/assets/agents/review.mds +286 -0
  92. package/src/assets/agents/scrutinize.mds +132 -0
  93. package/src/assets/agents/skim.mds +161 -0
  94. package/src/assets/agents/triage.mds +194 -0
  95. package/src/assets/agents/validate.md +8 -6
  96. package/src/assets/commands/_partials/_compliance.mds +5 -4
  97. package/src/assets/commands/_partials/_decisions.mds +31 -0
  98. package/src/assets/commands/_partials/_engine.mds +9 -1
  99. package/src/assets/commands/_partials/_knowledge.mds +25 -12
  100. package/src/assets/commands/_partials/_preamble.mds +33 -9
  101. package/src/assets/commands/_partials/_publication.mds +5 -4
  102. package/src/assets/commands/_partials/_settings.mds +13 -5
  103. package/src/assets/commands/_partials/_wave.mds +8 -0
  104. package/src/assets/commands/bug-analysis.mds +24 -2
  105. package/src/assets/commands/code-review.mds +147 -44
  106. package/src/assets/commands/debug.mds +17 -1
  107. package/src/assets/commands/dynamic-build.mds +33 -2
  108. package/src/assets/commands/dynamic-plan.mds +36 -6
  109. package/src/assets/commands/dynamic-profile.mds +9 -1
  110. package/src/assets/commands/dynamic-tickets.mds +16 -2
  111. package/src/assets/commands/explore.mds +27 -1
  112. package/src/assets/commands/implement.mds +41 -8
  113. package/src/assets/commands/plan.mds +47 -8
  114. package/src/assets/commands/{release.md → release.mds} +27 -24
  115. package/src/assets/commands/research.mds +28 -4
  116. package/src/assets/commands/resolve.mds +43 -2
  117. package/src/assets/commands/self-review.mds +30 -5
  118. package/src/assets/mds/tracker/_contract.mds +72 -0
  119. package/src/assets/mds/tracker/_github.mds +13 -2
  120. package/src/assets/mds/tracker/_jira.mds +17 -5
  121. package/src/assets/mds/tracker/_linear.mds +17 -5
  122. package/src/assets/mds/tracker/_mcp.mds +2 -2
  123. package/src/assets/mds/tracker/_steps.mds +97 -0
  124. package/src/assets/rules/context-economy.md +10 -0
  125. package/src/assets/rules/go.md +1 -0
  126. package/src/assets/rules/java.md +1 -0
  127. package/src/assets/rules/python.md +1 -0
  128. package/src/assets/rules/rust.md +1 -0
  129. package/src/assets/rules/typescript.md +1 -0
  130. package/src/assets/scripts/claude-md-audit.cjs +611 -0
  131. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +1 -2
  132. package/src/assets/scripts/hooks/json-helper.cjs +13 -5
  133. package/src/assets/scripts/hooks/json-parse +34 -10
  134. package/src/assets/scripts/hooks/session-start-context +315 -7
  135. package/src/assets/skills/apply-decisions/SKILL.md +1 -1
  136. package/src/assets/skills/apply-feature-knowledge/SKILL.md +5 -5
  137. package/src/assets/skills/feature-knowledge/SKILL.md +43 -12
  138. package/src/assets/skills/quality-gates/SKILL.md +1 -1
@@ -98,18 +98,42 @@ json_extract_cwd_prompt() {
98
98
 
99
99
  # --- Hook output envelopes ---
100
100
 
101
- # Build SessionStart output. Usage: json_session_output "$CONTEXT"
101
+ # Build SessionStart output. Usage: json_session_output "$CONTEXT" ["$SYSTEM_MESSAGE"]
102
+ #
103
+ # D-SYSTEMMESSAGE-ENVELOPE: the optional second argument is a message shown to the
104
+ # user, carried in the top-level `systemMessage` key beside (never inside)
105
+ # `hookSpecificOutput`; it does not enter the model's context. With ONE argument the
106
+ # envelope is exactly what every existing caller has always received, byte for byte
107
+ # (ensure-proxy, session-start-orchestrator, session-start-memory and Sections 1-5 of
108
+ # session-start-context pass one). With two:
109
+ # - a message and a context -> both keys, hookSpecificOutput first;
110
+ # - a message and no context -> `systemMessage` alone, with NO hookSpecificOutput key;
111
+ # - an empty message -> the one-argument envelope for that context.
112
+ # The node fallback (json-helper.cjs session-output) builds the same content, compact
113
+ # where jq pretty-prints.
102
114
  json_session_output() {
103
- local ctx="$1"
104
- if [ "$_HAS_JQ" = "true" ]; then
105
- jq -n --arg ctx "$ctx" '{
106
- "hookSpecificOutput": {
107
- "hookEventName": "SessionStart",
108
- "additionalContext": $ctx
109
- }
110
- }'
115
+ local ctx="$1" msg="${2:-}"
116
+ if [ "$#" -lt 2 ] || [ -z "$msg" ]; then
117
+ if [ "$_HAS_JQ" = "true" ]; then
118
+ jq -n --arg ctx "$ctx" '{
119
+ "hookSpecificOutput": {
120
+ "hookEventName": "SessionStart",
121
+ "additionalContext": $ctx
122
+ }
123
+ }'
124
+ else
125
+ node "$_JSON_HELPER" session-output "$ctx"
126
+ fi
127
+ elif [ "$_HAS_JQ" = "true" ]; then
128
+ jq -n --arg ctx "$ctx" --arg msg "$msg" '
129
+ (if $ctx != "" then {
130
+ "hookSpecificOutput": {
131
+ "hookEventName": "SessionStart",
132
+ "additionalContext": $ctx
133
+ }
134
+ } else {} end) + {"systemMessage": $msg}'
111
135
  else
112
- node "$_JSON_HELPER" session-output "$ctx"
136
+ node "$_JSON_HELPER" session-output "$ctx" "$msg"
113
137
  fi
114
138
  }
115
139
 
@@ -23,6 +23,31 @@
23
23
  # Section 4: Legacy install notice — one line when this repository still carries
24
24
  # a retired project-local devflow install (its .claude/settings.json registers
25
25
  # devflow hooks), pointing at `devflow uninstall --scope local`.
26
+ # Section 5: Compaction resume directive — on a `compact` SessionStart, one fixed
27
+ # line telling the model to re-read the devflow command it was running from the
28
+ # phase in progress (D-COMPACT-RESUME-DIRECTIVE).
29
+ # Section 6: CLAUDE.md import audit — a top-level systemMessage (shown to the user,
30
+ # never part of additionalContext) naming an `@path` import that puts a large file
31
+ # into every thread's always-loaded context, once per finding, with a stamp that
32
+ # makes an unchanged start free (D-CLAUDE-MD-IMPORT-AUDIT, D-AUDIT-STAMP,
33
+ # D-SYSTEMMESSAGE-ENVELOPE).
34
+ #
35
+ # Sections 5 and 6 add NO new exit path: neither calls `exit`, a failure in either
36
+ # leaves the other sections and the directive intact, and the hook still exits as it
37
+ # did before (the existing exits are the bad-CWD and re-entrancy returns, the
38
+ # no-JSON-tool return at the top, and the two sourcing failures). With neither jq nor
39
+ # node the hook returns before Section 5 can run, so there is no directive then; that
40
+ # is accepted and documented.
41
+ #
42
+ # Cost of Sections 5 and 6 (the [DR-10] table below belongs to Section 3):
43
+ # - Section 5: a `case` on a value parsed with the CWD, in the same single
44
+ # subprocess as before; zero extra forks.
45
+ # - Section 6, unchanged start (the stamp's root set equals the current one and no
46
+ # recorded path changed): shell builtins only, one bounded read of the stamp, no
47
+ # fork, no node process.
48
+ # - Section 6, changed start: ONE node process (claude-md-audit.cjs), and a
49
+ # sibling temp file renamed over the stamp. When none of the roots exists there
50
+ # is nothing to audit: the stamp is written with builtins, and node is not started.
26
51
 
27
52
  # Safe no-op fallback: must exist before hook-bootstrap is sourced.
28
53
  dbg() { :; }
@@ -49,7 +74,13 @@ source "$SCRIPT_DIR/hook-bootstrap" "session-start-context"
49
74
  source "$SCRIPT_DIR/json-parse" || { echo "session-start-context: failed to source json-parse" >&2; exit 1; }
50
75
  if [ "$_JSON_AVAILABLE" = "false" ]; then exit 0; fi
51
76
 
52
- CWD=$(printf '%s' "$INPUT" | json_field "cwd" "")
77
+ # One subprocess yields both fields: the working directory and the SessionStart
78
+ # `source` (startup|resume|clear|compact) that Section 5 gates on, so the directive
79
+ # costs no fork of its own. Sections 3 and 4 keep reading `source` themselves, where
80
+ # they gate (startup|clear), so neither depends on this value.
81
+ _SC_FIELDS=$(printf '%s' "$INPUT" | json_extract_cwd_field "source")
82
+ CWD="${_SC_FIELDS%%$'\001'*}"
83
+ SESSION_SOURCE="${_SC_FIELDS#*$'\001'}"
53
84
  if [ -z "$CWD" ] || [ ! -d "$CWD" ]; then
54
85
  dbg "EXIT: bad CWD"
55
86
  exit 0
@@ -239,6 +270,7 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
239
270
  # since the model would read the file the link names and pass that on.
240
271
  # Builtins only: no fork, save the log line of a refused index.
241
272
  _SC_INDEX="$LEARNING_DIR/index.md"; DECISIONS_INDEX_LINE=""
273
+ DECISIONS_PASS_RULE="Decisions (direct delegations only — workflow skills load their own): pass this index as DECISIONS_CONTEXT — its content, read once — to every agent that takes it."
242
274
  if [ -n "$DIRECTIVE_LEDGER_SAFE" ] && df_file_below "$LEDGER_ROOT" "$_SC_INDEX" && [ -s "$_SC_INDEX" ]; then
243
275
  _SC_IDX1=""; IFS= read -r _SC_IDX1 < "$_SC_INDEX" || true
244
276
  [ "$_SC_IDX1" != "(none)" ] && DECISIONS_INDEX_LINE="Index: $_SC_INDEX"
@@ -250,10 +282,19 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
250
282
  DECISIONS_SECTION="${DECISIONS_SECTION}
251
283
  $(printf '%b' "$DECISIONS_TLDR")"
252
284
  fi
253
- # The index line goes last.
285
+ # The index line goes last among the data lines, and the pass rule follows it.
286
+ # D-DECISIONS-CHARTER-HANDOFF: the rule that tells the main model to pass this
287
+ # index on as DECISIONS_CONTEXT lives here, not in the orchestrator charter. It
288
+ # is only useful with an index line to pass, so it is emitted only with one, and
289
+ # this block is already gated on the machine switch narrowed by the repository,
290
+ # so a learning-off machine or repo never sees it (the charter paid for it in
291
+ # every session, learning or not). It also reaches sessions with ambient off,
292
+ # which the charter never did. A static literal: no input is interpolated, so it
293
+ # needs no shape gate. The same text reaches the jq and the node envelope.
254
294
  if [ -n "$DECISIONS_INDEX_LINE" ]; then
255
295
  DECISIONS_SECTION="${DECISIONS_SECTION}
256
- ${DECISIONS_INDEX_LINE}"
296
+ ${DECISIONS_INDEX_LINE}
297
+ ${DECISIONS_PASS_RULE}"
257
298
  fi
258
299
  if [ -n "$CONTEXT" ]; then
259
300
  CONTEXT="${CONTEXT}
@@ -779,11 +820,270 @@ ${LEGACY_SECTION}"
779
820
  fi
780
821
  fi
781
822
 
823
+ # --- Section 5: Compaction resume directive ---
824
+ # D-COMPACT-RESUME-DIRECTIVE: after a compaction nothing tells the model to re-read
825
+ # the devflow command it was running; Claude Code re-injects a long command only in
826
+ # part (the plan records commands over about 5k tokens being truncated on re-injection,
827
+ # which this repository cannot verify). On the SessionStart whose source is `compact`
828
+ # this section adds one fixed line, appended to CONTEXT with the other sections'
829
+ # join, and never sent as a systemMessage.
830
+ #
831
+ # The line is a constant: a single-quoted string that interpolates nothing, so the
832
+ # `$CLAUDE_CONFIG_DIR` in it reaches the model as text and its bytes do not depend on
833
+ # the caller. It names, in at most 250 ASCII characters (tests/session-start-compact
834
+ # holds the cap and the numeric-floors ceiling `compact-directive-max-chars`):
835
+ # (a) an if-clause, so it applies only when a devflow command was running, with an
836
+ # explicit ignore clause;
837
+ # (b) where the command file lives, by rule: commands/devflow/ under the Claude
838
+ # config directory ($CLAUDE_CONFIG_DIR, else ~/.claude);
839
+ # (c) the read mechanics: list the headings, then read from the phase in progress,
840
+ # never the whole file;
841
+ # (d) that the command's input (COMMAND_INPUT in the commands that bind it, an
842
+ # ARGUMENTS line in the others) comes from the conversation summary, not from a
843
+ # placeholder in the file;
844
+ # (e) to resume after the last finished phase.
845
+ # The gate is a POSITIVE `compact` case, emitted on every compact run whatever the
846
+ # project (the directive does no project work), so it never becomes a denylist and a
847
+ # missing, absent or unrecognised source emits nothing. The model decides whether a
848
+ # command was running: detecting one here would be judgment, which stays out of hooks.
849
+ case "$SESSION_SOURCE" in
850
+ compact)
851
+ dbg "compaction resume directive emitted"
852
+ COMPACT_SECTION='If a devflow command was running: re-read {$CLAUDE_CONFIG_DIR or ~/.claude}/commands/devflow/<name>.md, list headings, read from current phase, take input (COMMAND_INPUT/ARGUMENTS) from the summary, resume after the last finished phase. Else ignore.'
853
+ if [ -n "$CONTEXT" ]; then
854
+ CONTEXT="${CONTEXT}
855
+
856
+ ${COMPACT_SECTION}"
857
+ else
858
+ CONTEXT="$COMPACT_SECTION"
859
+ fi
860
+ ;;
861
+ esac
862
+
863
+ # --- Section 6: CLAUDE.md import audit and its stamp ---
864
+ # D-CLAUDE-MD-IMPORT-AUDIT: a CLAUDE.md `@path` import loads its file into every
865
+ # thread at launch, so a large import is paid for in every session. This section
866
+ # reports the ones over the thresholds that claude-md-audit.cjs holds (and documents:
867
+ # grammar, bounds, thresholds, the upstream differences) in a top-level systemMessage,
868
+ # which the user sees and the model does not (D-SYSTEMMESSAGE-ENVELOPE, json-parse).
869
+ # It never enters CONTEXT, and the audit READS only: this section and init write the
870
+ # stamp, nothing else.
871
+ #
872
+ # D-AUDIT-STAMP: one machine-root file, $HOME/.devflow/.claude-md-audit, so a changed
873
+ # start costs one node process and an unchanged one costs none. Its rows are `V 1`,
874
+ # `R <root>` (the gated root set), `P <flag> <path>` (every path the last audit
875
+ # examined: 0 absent, 1 regular file, 2 exists but is not one), `K <key>` (a finding
876
+ # already displayed) and `E <reason>`; the format is specified in claude-md-audit.cjs.
877
+ # Gated roots: CLAUDE.md in the Claude config directory ($CLAUDE_CONFIG_DIR when
878
+ # absolute, else ~/.claude) always; in a git project that is not HOME
879
+ # (PROJECT_OK) also its CLAUDE.md, .claude/CLAUDE.md and CLAUDE.local.md.
880
+ # Fast path (builtins only, no fork): the audit is skipped when the stamp's R rows
881
+ # equal the current root set and every P row still holds: `-e` agrees with the
882
+ # flag, and a regular file is not `-nt` (newer than) the stamp. Mtimes are whole
883
+ # seconds. Two gaps are accepted: a file whose mtime is moved back before the stamp
884
+ # is missed until its next forward change, and a write landing while an audit runs
885
+ # can be missed the same way. The fast path decides only whether the audit runs.
886
+ # Slow path: ONE node process over the roots (none when no root exists: there is
887
+ # nothing to read, so the stamp records the absent roots with builtins). A finding is recorded as displayed
888
+ # only after the output below was emitted, so a lost stamp update can repeat a
889
+ # message and can never hide a new finding.
890
+ # Failure: node failing, the script missing or throwing, or output that is not the
891
+ # framed block shows nothing, and writes a stamp holding the R rows, the keys
892
+ # already displayed and an error marker; an unchanged broken state then starts no
893
+ # node process, and a changed root set retries. A root path holding a control byte
894
+ # is never recorded, so a setup with one re-audits at every start (rare).
895
+ # Skipped silently: no node on PATH, no ~/.devflow (a session never creates the
896
+ # machine root), or a stamp path that is a link or not a regular file.
897
+ # A stamp over 256 KiB, or one with a NUL byte, is read as absent.
898
+ SYSTEM_MESSAGE=""
899
+ AUDIT_STAMP_NEXT=""
900
+ AUDIT_STAMP_FILE="$TRACKER_DEVFLOW_DIR/.claude-md-audit"
901
+ AUDIT_SCRIPT="$SCRIPT_DIR/../claude-md-audit.cjs"
902
+ _SC_AUDIT_OLD_STAMP=""
903
+ _SC_AUDIT_ROOTS=()
904
+
905
+ # _sc_audit_stamp_blocked — succeeds when the stamp path is a link or exists and is
906
+ # not a regular file: the write is refused and the audit skipped (D-AUDIT-STAMP).
907
+ _sc_audit_stamp_blocked() {
908
+ if [ -L "$AUDIT_STAMP_FILE" ]; then return 0; fi
909
+ if [ -e "$AUDIT_STAMP_FILE" ] && [ ! -f "$AUDIT_STAMP_FILE" ]; then return 0; fi
910
+ return 1
911
+ }
912
+
913
+ # _sc_audit_stamp_fresh — succeeds when the audit can be skipped. Builtins only.
914
+ # Sets _SC_AUDIT_OLD_STAMP to the stamp text (empty when absent, over 256 KiB or
915
+ # holding a NUL: `read -d '' -n N` succeeds only when it stops short of the end of
916
+ # the file, which is a stamp the shell cannot vouch for). Ends in an explicit return
917
+ # so a caller running under set -e would not die on the normal path: a function's last
918
+ # status is the status of its call, and under errexit a non-zero one is fatal.
919
+ _sc_audit_stamp_fresh() {
920
+ local line="" have="" want="" root="" path=""
921
+ _SC_AUDIT_OLD_STAMP=""
922
+ [ -f "$AUDIT_STAMP_FILE" ] || return 1
923
+ if IFS= read -r -d '' -n 262145 _SC_AUDIT_OLD_STAMP 2>/dev/null < "$AUDIT_STAMP_FILE"; then
924
+ _SC_AUDIT_OLD_STAMP=""
925
+ return 1
926
+ fi
927
+ case "$_SC_AUDIT_OLD_STAMP" in
928
+ 'V 1'|'V 1'$'\n'*) ;;
929
+ *) _SC_AUDIT_OLD_STAMP=""; return 1 ;;
930
+ esac
931
+ for root in "${_SC_AUDIT_ROOTS[@]}"; do
932
+ want="${want}R ${root}"$'\n'
933
+ done
934
+ while IFS= read -r line; do
935
+ case "$line" in
936
+ 'R '*) have="${have}${line}"$'\n' ;;
937
+ esac
938
+ done <<< "$_SC_AUDIT_OLD_STAMP"
939
+ if [ "$have" != "$want" ]; then return 1; fi
940
+ while IFS= read -r line; do
941
+ case "$line" in
942
+ 'P 0 '*)
943
+ path="${line#P 0 }"
944
+ if [ -e "$path" ]; then return 1; fi
945
+ ;;
946
+ 'P 1 '*)
947
+ path="${line#P 1 }"
948
+ if [ ! -e "$path" ] || [ "$path" -nt "$AUDIT_STAMP_FILE" ]; then return 1; fi
949
+ ;;
950
+ 'P 2 '*)
951
+ path="${line#P 2 }"
952
+ if [ ! -e "$path" ]; then return 1; fi
953
+ ;;
954
+ esac
955
+ done <<< "$_SC_AUDIT_OLD_STAMP"
956
+ return 0
957
+ }
958
+
959
+ # _sc_audit_shell_stamp <absent|failed> — a stamp the shell writes without node, for the
960
+ # two cases that need no audit text. Sets AUDIT_STAMP_NEXT. The rows are the ones
961
+ # claude-md-audit.cjs renders for the same state (tests/session-start-compact.test.ts
962
+ # holds the two equal): a root with a control byte is left out, as the script leaves it
963
+ # out, and the keys of the previous stamp are kept.
964
+ # absent No root exists, so there is nothing to audit and nothing to show: the root
965
+ # rows and a `P 0` row each. A CLAUDE.md appearing at any of them re-audits.
966
+ # failed The audit could not run: the root rows and an error marker, no P rows, so
967
+ # only a changed root set retries.
968
+ _sc_audit_shell_stamp() {
969
+ local kind="$1" line="" root="" text='V 1'$'\n'
970
+ for root in "${_SC_AUDIT_ROOTS[@]}"; do
971
+ case "$root" in
972
+ *[[:cntrl:]]*) ;;
973
+ *) text="${text}R ${root}"$'\n' ;;
974
+ esac
975
+ done
976
+ if [ "$kind" = "absent" ]; then
977
+ for root in "${_SC_AUDIT_ROOTS[@]}"; do
978
+ case "$root" in
979
+ *[[:cntrl:]]*) ;;
980
+ *) text="${text}P 0 ${root}"$'\n' ;;
981
+ esac
982
+ done
983
+ fi
984
+ while IFS= read -r line; do
985
+ case "$line" in
986
+ 'K '*) text="${text}${line}"$'\n' ;;
987
+ esac
988
+ done <<< "$_SC_AUDIT_OLD_STAMP"
989
+ if [ "$kind" = "failed" ]; then text="${text}E audit-failed"$'\n'; fi
990
+ AUDIT_STAMP_NEXT="$text"
991
+ return 0
992
+ }
993
+
994
+ # _sc_audit_no_root — succeeds when none of the gated roots exists. The common case on a
995
+ # machine with no CLAUDE.md anywhere, settled by `-e` tests alone: no node process.
996
+ _sc_audit_no_root() {
997
+ local root=""
998
+ for root in "${_SC_AUDIT_ROOTS[@]}"; do
999
+ if [ -e "$root" ]; then return 1; fi
1000
+ done
1001
+ return 0
1002
+ }
1003
+
1004
+ # _sc_audit_run — the slow path: one node process, its framed block parsed with
1005
+ # builtins. `M ` rows are the systemMessage lines; V/R/P/K/E rows are the new stamp.
1006
+ # Sets SYSTEM_MESSAGE and AUDIT_STAMP_NEXT; a block that is not complete is a failure.
1007
+ _sc_audit_run() {
1008
+ local out="" line="" rc=0 magic="" end="" msg="" stamp=""
1009
+ out=$(node "$AUDIT_SCRIPT" hook "$HOME" "$AUDIT_STAMP_FILE" "${_SC_AUDIT_ROOTS[@]}" 2>/dev/null)
1010
+ rc=$?
1011
+ if [ "$rc" -eq 0 ]; then
1012
+ while IFS= read -r line; do
1013
+ case "$line" in
1014
+ 'devflow-claude-md-audit 1') magic="yes" ;;
1015
+ 'END') end="yes" ;;
1016
+ 'M '*)
1017
+ if [ -n "$msg" ]; then msg="${msg}"$'\n'"${line#M }"; else msg="${line#M }"; fi
1018
+ ;;
1019
+ [VRPKE]' '*) stamp="${stamp}${line}"$'\n' ;;
1020
+ esac
1021
+ done <<< "$out"
1022
+ fi
1023
+ if [ "$rc" -eq 0 ] && [ -n "$magic" ] && [ -n "$end" ] && [ -n "$stamp" ]; then
1024
+ SYSTEM_MESSAGE="$msg"
1025
+ AUDIT_STAMP_NEXT="$stamp"
1026
+ dbg "claude-md audit ran (message=${#msg} chars)"
1027
+ else
1028
+ dbg "claude-md audit failed — recording the failure, showing nothing"
1029
+ _sc_audit_shell_stamp failed
1030
+ fi
1031
+ return 0
1032
+ }
1033
+
1034
+ # _sc_audit_record — write AUDIT_STAMP_NEXT, after the output has been emitted. A
1035
+ # sibling temp file (owner-only: the umask is set before the copy exists, because a rename
1036
+ # installs the copy's mode and not the replaced file's) is
1037
+ # renamed over the stamp; nothing stands at either name that is a link or not a
1038
+ # regular file (noclobber alone would still open a FIFO, and mv onto a linked directory
1039
+ # moves into it), and a failed write leaves the old stamp. Never fails the hook.
1040
+ _sc_audit_record() {
1041
+ local tmp="${AUDIT_STAMP_FILE}.tmp.$$"
1042
+ [ -n "$AUDIT_STAMP_NEXT" ] || return 0
1043
+ [ -d "$TRACKER_DEVFLOW_DIR" ] || return 0
1044
+ if _sc_audit_stamp_blocked; then return 0; fi
1045
+ if [ -e "$tmp" ] || [ -L "$tmp" ]; then return 0; fi
1046
+ if ( umask 077; set -o noclobber; printf '%s' "$AUDIT_STAMP_NEXT" > "$tmp" ) 2>/dev/null; then
1047
+ if _sc_audit_stamp_blocked; then
1048
+ rm -f -- "$tmp" 2>/dev/null
1049
+ return 0
1050
+ fi
1051
+ mv -f -- "$tmp" "$AUDIT_STAMP_FILE" 2>/dev/null || rm -f -- "$tmp" 2>/dev/null
1052
+ else
1053
+ rm -f -- "$tmp" 2>/dev/null
1054
+ fi
1055
+ return 0
1056
+ }
1057
+
1058
+ if [ "$_HAS_NODE" = "true" ] && [ -d "$TRACKER_DEVFLOW_DIR" ]; then
1059
+ if _sc_audit_stamp_blocked; then
1060
+ dbg "claude-md audit skipped: the stamp path is a link or not a regular file"
1061
+ else
1062
+ _SC_AUDIT_CLAUDE_DIR="$HOME/.claude"
1063
+ case "${CLAUDE_CONFIG_DIR:-}" in /*) _SC_AUDIT_CLAUDE_DIR="$CLAUDE_CONFIG_DIR" ;; esac
1064
+ _SC_AUDIT_ROOTS=("$_SC_AUDIT_CLAUDE_DIR/CLAUDE.md")
1065
+ if [ -n "$PROJECT_OK" ]; then
1066
+ _SC_AUDIT_ROOTS+=("$PROJECT_ROOT/CLAUDE.md" "$PROJECT_ROOT/.claude/CLAUDE.md" "$PROJECT_ROOT/CLAUDE.local.md")
1067
+ fi
1068
+ if _sc_audit_stamp_fresh; then
1069
+ dbg "claude-md audit skipped: the stamp is current"
1070
+ elif _sc_audit_no_root; then
1071
+ dbg "claude-md audit skipped: no root exists — recording that"
1072
+ _sc_audit_shell_stamp absent
1073
+ else
1074
+ _sc_audit_run
1075
+ fi
1076
+ fi
1077
+ fi
1078
+
782
1079
  # --- Output ---
783
1080
 
784
- # Only output if we have something to inject
785
- if [ -z "$CONTEXT" ]; then
1081
+ # Only output if we have something to inject or to tell the user. The stamp is
1082
+ # recorded here too: an audit that found nothing to show still ran, and recording it
1083
+ # is what makes the next unchanged start free.
1084
+ if [ -z "$CONTEXT" ] && [ -z "$SYSTEM_MESSAGE" ]; then
786
1085
  dbg "EXIT: no context to inject"
1086
+ _sc_audit_record
787
1087
  exit 0
788
1088
  fi
789
1089
 
@@ -791,5 +1091,13 @@ dbg "OUTPUT_LENGTH=${#CONTEXT}"
791
1091
  log "Injecting context (${#CONTEXT} chars)"
792
1092
  dbg "=== HOOK COMPLETE ==="
793
1093
 
794
- # Output as additionalContext JSON envelope (Claude sees it as system context, not user-visible)
795
- json_session_output "$CONTEXT"
1094
+ # Output as additionalContext JSON envelope (Claude sees it as system context, not
1095
+ # user-visible). The audit text, if any, travels in the envelope's top-level
1096
+ # systemMessage and never in the context (D-SYSTEMMESSAGE-ENVELOPE). With no message
1097
+ # the second argument is empty and the envelope is the one callers always got.
1098
+ json_session_output "$CONTEXT" "$SYSTEM_MESSAGE"
1099
+ _SC_OUT_RC=$?
1100
+
1101
+ # A finding is recorded as displayed only after it was shown (D-AUDIT-STAMP).
1102
+ if [ "$_SC_OUT_RC" -eq 0 ]; then _sc_audit_record; fi
1103
+ exit "$_SC_OUT_RC"
@@ -47,4 +47,4 @@ Cite only IDs in `DECISIONS_CONTEXT`; never guess or rebuild one. When nothing c
47
47
 
48
48
  ## Skip Guard
49
49
 
50
- When `DECISIONS_CONTEXT` is empty, `(none)` or not provided, skip this skill — unless your instructions tell you to read the decisions index yourself; that index is then your `DECISIONS_CONTEXT`. Never load decisions files otherwise.
50
+ When `DECISIONS_CONTEXT` is empty, `(none)` or not provided, skip this skill. Never load decisions files otherwise.
@@ -2,7 +2,7 @@
2
2
  name: apply-feature-knowledge
3
3
  description: Consume FEATURE_KNOWLEDGE, the pre-computed feature context
4
4
  user-invocable: false
5
- allowed-tools: Read
5
+ allowed-tools: Read, Bash
6
6
  ---
7
7
 
8
8
  # Apply Feature Knowledge
@@ -19,9 +19,9 @@ allowed-tools: Read
19
19
 
20
20
  ## 3-Step Algorithm
21
21
 
22
- 1. **Read** each section of `FEATURE_KNOWLEDGE` (headed `--- Feature knowledge: {slug} ---`) for architecture, data flow, patterns, anti-patterns, gotchas and the integration points your task touches.
23
- 2. **Apply** it: follow documented patterns unless you have a specific reason not to; check your work against each anti-pattern and gotcha; respect documented integration boundaries; start exploring from the key files.
24
- 3. **Verify** against current code: it may not reflect recent changes. Where it is silent on your area, explore further. Where an assertion seems outdated or contradicts the code, Read the source and trust it. Note a discrepancy in your output when it matters for the task.
22
+ 1. **Read** each `FEATURE_KNOWLEDGE` block (headed `--- Feature knowledge: {slug} ---`): `Rules:` holds anti-patterns, gotchas and invariants; `KB:` is the full file's path (under `WORKTREE_PATH` when given).
23
+ 2. **Apply** them: check your work against every bullet and cite it as `{slug} KB-AP-n` (a section-labelled bullet as `{slug} {section}`), never a bare ID. For architecture or integration context, Read that section on demand, with `offset` and `limit`: its start line is on the `Headings:` line, or comes from `command grep -n '^## ' "<KB path>"`. Quote KB text only from a Read view.
24
+ 3. **Verify** against current code: where an assertion is outdated or contradicts the code, Read the source and trust it. Note a discrepancy in your output when it matters.
25
25
 
26
26
  ---
27
27
 
@@ -32,4 +32,4 @@ Do not mention feature knowledge or its absence in your output.
32
32
 
33
33
  ## Freshness Model
34
34
 
35
- Feature knowledge is **verify-on-read**: check key assertions against current code, not staleness markers, and when in doubt Read the file.
35
+ Feature knowledge is **verify-on-read**: check key assertions against current code, and when in doubt Read the file.
@@ -102,16 +102,20 @@ updated: {ISO date}
102
102
 
103
103
  # {Feature Area Name}
104
104
 
105
+ ## Rules
106
+ - **KB-AP-1** [one-line anti-pattern or gotcha: what to avoid, and the consequence]
107
+ - **KB-INV-1** [one-line invariant that must hold]
108
+
105
109
  ## Overview
106
110
  [1-2 paragraphs: what this knowledge covers and why it matters for this codebase]
107
111
 
108
112
  ## [Main Sections — vary by category, see Category Templates below]
109
113
 
110
114
  ## Anti-Patterns
111
- [What to avoid and why — with explanation of consequences]
115
+ [The longer explanation of each anti-pattern, headed by its ID — never a restatement of the Rules bullet]
112
116
 
113
117
  ## Gotchas
114
- [Non-obvious behaviors, edge cases, things that break silently]
118
+ [Non-obvious behaviors, edge cases, things that break silently — headed by the ID of the Rules bullet each explains]
115
119
 
116
120
  ## Key Files
117
121
  [Most important files with one-line descriptions]
@@ -120,6 +124,23 @@ updated: {ISO date}
120
124
  [Links to other feature knowledge entries and key source files — never an ADR/PF ID]
121
125
  ```
122
126
 
127
+ ### Rules Section
128
+
129
+ `## Rules` comes first, after the title: the anti-patterns, gotchas and invariants of the feature area as one-line bullets, in about 3–5K characters. A bullet fits one line and never starts with `## `.
130
+
131
+ - **IDs**: anti-pattern and gotcha bullets take `KB-AP-n`; invariant bullets take `KB-INV-n`. Numbers are unique within a KB. A bullet keeps its ID across rewrites; a removed bullet's ID is retired and never reused. A citation names the slug and the ID together, as `{slug} KB-AP-n`; a bare ID is never cited.
132
+ - **Explanations**: `## Anti-Patterns` and `## Gotchas` stay below Rules as the longer explanations, each headed by the IDs it explains.
133
+ - **No volatile numbers**: a volatile number is a count, size, line number, version or threshold that changes when the code changes. Where a value matters, name the test or constant that pins it. Bullet IDs are not volatile numbers.
134
+ - **Legacy KBs**: a KB without `## Rules` is valid until curated. Readers take one to three entries from its Anti-Patterns or Gotchas, cited by section name, with the KB path and heading index.
135
+
136
+ ### Size Budget
137
+
138
+ Per KB: target 30,000 characters, ceiling 40,000. An index line is at most 300 characters and a description at most 220, so slug, areas and description fit one line. **Curate, never truncate**: reword or consolidate, and nothing is cut or dropped to meet the budget. Split into focused sub-knowledge bases (each with its own index entry) only when curation cannot bring a KB under the ceiling.
139
+
140
+ ### Refreshing an Existing KB
141
+
142
+ When `EXISTING_KB` is provided, change only the sections the new work touches, and add or update Rules bullets for what changed. Never renumber, and never rewrite untouched sections to meet the budget. A KB still above the ceiling after a refresh is written as it stands, and the final message says so.
143
+
123
144
  ### Category Templates
124
145
 
125
146
  Use the matching template as your main sections. Anti-Patterns, Gotchas, Key Files, and Related are shared across all categories.
@@ -159,6 +180,7 @@ The `description` field is how this feature knowledge entry gets discovered. It
159
180
  - Start with "Use when"
160
181
  - Name specific scenarios where this knowledge applies
161
182
  - Include keywords a developer would search for
183
+ - Stay within 220 characters
162
184
 
163
185
  Good: `"Use when adding a new vendor integration, implementing API clients, or connecting to external services. Keywords: integration, vendor, API client, webhook."`
164
186
  Bad: `"Integration stuff"`
@@ -205,7 +227,7 @@ Never include bare code snippets without context.
205
227
  | No code examples at all | Insufficient actionable guidance |
206
228
  | Examples without inline comments | Missing required context |
207
229
  | "In the future, we might..." | Speculative — remove it |
208
- | 500+ lines in a single file | Should be split into focused files |
230
+ | Over 40,000 characters, or volatile numbers in Rules | Curate it; name the pinning test or constant instead of a count |
209
231
  | No cross-references in Related | Isolated knowledge island |
210
232
 
211
233
  ---
@@ -228,7 +250,8 @@ Run through this before writing. If any check fails, go back and fix it.
228
250
  **Structure:**
229
251
  - [ ] Category is correct and main sections follow the matching template
230
252
  - [ ] Description field starts with "Use when" and includes keywords
231
- - [ ] File stays under 500 lines (split if necessary)
253
+ - [ ] `## Rules` comes first: one-line bullets with `KB-AP-n` / `KB-INV-n` IDs, about 3–5K characters, no volatile numbers
254
+ - [ ] File within the 40,000-character ceiling (curate, never truncate); description at most 220 characters
232
255
 
233
256
  **Connections:**
234
257
  - [ ] Cross-references to related feature knowledge entries in Related section; decisions and pitfalls stated in words, with no ADR/PF ID anywhere in the file
@@ -249,10 +272,10 @@ After writing KNOWLEDGE.md, update the index cache directly:
249
272
  - **{slug}** — {areas} — {Use-when description}
250
273
  ```
251
274
 
252
- Where:
275
+ The whole line is at most 300 characters; reword a longer one, never cut it. Where:
253
276
  - `{slug}` matches the `feature:` frontmatter field
254
277
  - `{areas}` is a comma-separated summary of the `directories:` frontmatter field
255
- - `{Use-when description}` is the `description:` frontmatter field value (the full "Use when..." sentence)
278
+ - `{Use-when description}` is the `description:` frontmatter field value (the full "Use when..." sentence, at most 220 characters)
256
279
 
257
280
  If `index.md` does not exist, create it with just this line. If the file already has an entry for this slug, replace that line in-place. This is the discoverable cache read by `knowledge_load()` — the KNOWLEDGE.md frontmatter is always authoritative.
258
281
 
@@ -275,9 +298,17 @@ updated: 2026-04-30
275
298
 
276
299
  # Third-Party Integrations
277
300
 
301
+ ## Rules
302
+
303
+ - **KB-AP-1** Fetch logic never lives in a command file: it breaks the lib/command split and cannot be tested alone.
304
+ - **KB-AP-2** URLs never live in lib modules: they go through `config.ts`.
305
+ - **KB-AP-3** A lib module never calls `showError()`: lib modules throw, commands catch and display.
306
+ - **KB-AP-4** `spawn` needs the binary on PATH: detect `ENOENT` and give an install URL.
307
+ - **KB-INV-1** Every external constant lives in `src/lib/config.ts`.
308
+
278
309
  ## Overview
279
310
 
280
- This project integrates with external systems in three ways: spawning CLI processes, fetching files from remote registries, and writing to directories that AI editors watch. Each integration lives in its own module under `src/lib/` and is wired into a command in `src/commands/`. All external constants are centralized in `src/lib/config.ts`.
311
+ This project integrates with external systems by spawning CLI processes, fetching files from remote registries, and writing to directories that AI editors watch. Each integration lives in its own module under `src/lib/` and is wired into a command in `src/commands/`. All external constants are centralized in `src/lib/config.ts`.
281
312
 
282
313
  The key cross-cutting pattern is the separation between lib modules (which integrate) and commands (which orchestrate). Violating this creates coupling that breaks the error handling model.
283
314
 
@@ -289,7 +320,7 @@ The key cross-cutting pattern is the separation between lib modules (which integ
289
320
 
290
321
  ## Standard Structure
291
322
 
292
- Every integration follows the same file organization. This example shows the pattern that all three existing integrations (Claude CLI, GitHub, AI editors) follow:
323
+ Every integration follows the same file organization. This example shows the pattern that every existing integration (Claude CLI, GitHub, AI editors) follows:
293
324
 
294
325
  ```
295
326
  src/lib/
@@ -309,13 +340,13 @@ All constants use `SCREAMING_SNAKE_CASE` with a descriptive prefix. If a value c
309
340
 
310
341
  ## Anti-Patterns
311
342
 
312
- - **Putting fetch logic in a command file** — breaks the lib/command separation and makes the integration untestable in isolation.
313
- - **Hardcoding URLs in lib modules** — always use `config.ts`. Scattered strings become stale and hard to find.
314
- - **Calling `showError()` from a lib module** — lib modules throw; commands catch and display.
343
+ - **KB-AP-1, putting fetch logic in a command file** — breaks the lib/command separation and makes the integration untestable in isolation.
344
+ - **KB-AP-2, hardcoding URLs in lib modules** — always use `config.ts`. Scattered strings become stale and hard to find.
345
+ - **KB-AP-3, calling `showError()` from a lib module** — lib modules throw; commands catch and display.
315
346
 
316
347
  ## Gotchas
317
348
 
318
- - `spawn` requires the binary on PATH. If integrating a tool that may not be globally installed, detect `ENOENT` and provide an install URL.
349
+ - **KB-AP-4**: `spawn` requires the binary on PATH. If integrating a tool that may not be globally installed, detect `ENOENT` and provide an install URL.
319
350
  - Temp files use `Date.now()`. If two processes run simultaneously, add a random suffix to avoid collisions.
320
351
 
321
352
  ## Key Files
@@ -31,7 +31,7 @@ Based on [Google Engineering Practices](https://google.github.io/eng-practices/r
31
31
  ### P0 - Design
32
32
  Does the implementation fit the architecture? Follows existing patterns, respects layer boundaries, dependencies injected.
33
33
 
34
- If `FEATURE_KNOWLEDGE` is provided, verify implementation respects the feature area's documented architecture and anti-patterns. Flag deviations as P0-Design issues when the documented pattern is clearly intentional.
34
+ If `FEATURE_KNOWLEDGE` is provided (Rules bullets and the KB path, no heading index), verify implementation respects each bullet's anti-pattern, gotcha or invariant; Read a KB section from its path for the architecture. Flag deviations as P0-Design issues when the documented pattern is clearly intentional.
35
35
 
36
36
  ### P0 - Functionality
37
37
  Does the code work? Happy path, edge cases (null, empty, boundary), no race conditions.