devflow-kit 3.8.0 → 3.10.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 (211) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/README.md +6 -6
  3. package/dist/agents/code.md +10 -10
  4. package/dist/agents/fix.md +7 -7
  5. package/dist/agents/git.md +2 -2
  6. package/dist/agents/harden.md +7 -6
  7. package/dist/agents/refactor.md +6 -6
  8. package/dist/agents/skim.md +1 -1
  9. package/dist/agents/triage.md +7 -0
  10. package/dist/cli/commands/ambient.js +10 -2
  11. package/dist/cli/commands/compliance-prompts.js +9 -4
  12. package/dist/cli/commands/compliance.js +171 -24
  13. package/dist/cli/commands/curation.js +79 -23
  14. package/dist/cli/commands/gate-level-prompts.js +39 -23
  15. package/dist/cli/commands/gates.js +6 -7
  16. package/dist/cli/commands/init.js +150 -145
  17. package/dist/cli/commands/install-report.js +34 -0
  18. package/dist/cli/commands/legacy-sweep.js +64 -0
  19. package/dist/cli/commands/memory.js +59 -12
  20. package/dist/cli/commands/proxy.js +6 -1
  21. package/dist/cli/commands/rules.js +29 -12
  22. package/dist/cli/commands/skills.js +42 -9
  23. package/dist/cli/commands/tracker.js +9 -3
  24. package/dist/cli/commands/uninstall.js +354 -60
  25. package/dist/commands/bug-analysis.md +71 -122
  26. package/dist/commands/code-review.md +11 -11
  27. package/dist/commands/debug.md +2 -2
  28. package/dist/commands/dynamic-build.md +25 -25
  29. package/dist/commands/dynamic-plan.md +2 -2
  30. package/dist/commands/dynamic-profile.md +2 -2
  31. package/dist/commands/dynamic-tickets.md +2 -2
  32. package/dist/commands/explore.md +2 -2
  33. package/dist/commands/implement.md +48 -34
  34. package/dist/commands/plan.md +2 -2
  35. package/dist/commands/release.md +3 -3
  36. package/dist/commands/research.md +2 -2
  37. package/dist/commands/resolve.md +50 -46
  38. package/dist/commands/self-review.md +10 -9
  39. package/dist/compliance-off/agents/git.md +120 -0
  40. package/dist/compliance-off/agents/triage.md +180 -0
  41. package/dist/compliance-off/commands/bug-analysis.md +398 -0
  42. package/dist/compliance-off/commands/code-review.md +520 -0
  43. package/dist/compliance-off/commands/dynamic-build.md +1251 -0
  44. package/dist/compliance-off/commands/dynamic-tickets.md +676 -0
  45. package/dist/compliance-off/commands/implement.md +736 -0
  46. package/dist/compliance-off/commands/plan.md +671 -0
  47. package/dist/compliance-off/commands/release.md +309 -0
  48. package/dist/compliance-off/commands/resolve.md +742 -0
  49. package/dist/compliance-off/skills/git/references/decision-markers.md +17 -0
  50. package/dist/compliance-off/skills/git/references/learn-conventions.md +80 -0
  51. package/dist/compliance-off/skills/git/references/pr/check-ci-status.md +33 -0
  52. package/dist/compliance-off/skills/git/references/pr/check-merge-readiness.md +49 -0
  53. package/dist/compliance-off/skills/git/references/pr/fetch-review-threads.md +52 -0
  54. package/dist/compliance-off/skills/git/references/pr/validate-branch.md +47 -0
  55. package/dist/compliance-off/skills/git/references/tracker/_contract.md +33 -0
  56. package/dist/compliance-off/skills/git/references/tracker/_mcp.md +153 -0
  57. package/dist/compliance-off/skills/git/references/tracker/github/create-release.md +35 -0
  58. package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +100 -0
  59. package/dist/compliance-off/skills/git/references/tracker/github/ensure-traceable-issue.md +74 -0
  60. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issue.md +69 -0
  61. package/dist/compliance-off/skills/git/references/tracker/github/fetch-issues-batch.md +65 -0
  62. package/dist/compliance-off/skills/git/references/tracker/github/gather-release-evidence.md +48 -0
  63. package/dist/compliance-off/skills/git/references/tracker/github/setup-task.md +77 -0
  64. package/dist/compliance-off/skills/git/references/tracker/jira/create-release.md +35 -0
  65. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +100 -0
  66. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-traceable-issue.md +61 -0
  67. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issue.md +51 -0
  68. package/dist/compliance-off/skills/git/references/tracker/jira/fetch-issues-batch.md +63 -0
  69. package/dist/compliance-off/skills/git/references/tracker/jira/gather-release-evidence.md +47 -0
  70. package/dist/compliance-off/skills/git/references/tracker/jira/setup-task.md +82 -0
  71. package/dist/compliance-off/skills/git/references/tracker/linear/create-release.md +35 -0
  72. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +100 -0
  73. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-traceable-issue.md +61 -0
  74. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issue.md +51 -0
  75. package/dist/compliance-off/skills/git/references/tracker/linear/fetch-issues-batch.md +63 -0
  76. package/dist/compliance-off/skills/git/references/tracker/linear/gather-release-evidence.md +47 -0
  77. package/dist/compliance-off/skills/git/references/tracker/linear/setup-task.md +83 -0
  78. package/dist/compliance-off/skills/git/references/trust-rule.md +7 -0
  79. package/dist/core/agent-models.js +19 -4
  80. package/dist/core/area-pause-notice.js +44 -0
  81. package/dist/core/assets.js +126 -17
  82. package/dist/core/compliance-off-files.js +27 -0
  83. package/dist/core/curation-move-skip.js +4 -5
  84. package/dist/core/curation-queue-cleanup.js +6 -5
  85. package/dist/core/curation-store.js +1 -0
  86. package/dist/core/curation-variants.js +460 -112
  87. package/dist/core/evidence-policy.js +10 -8
  88. package/dist/core/flags.js +2 -2
  89. package/dist/core/installed-variants.js +63 -0
  90. package/dist/core/manifest.js +48 -5
  91. package/dist/core/mds-variants.js +700 -3
  92. package/dist/core/orphan-sweep.js +11 -2
  93. package/dist/core/plugins.js +69 -12
  94. package/dist/core/tracked-areas.js +138 -14
  95. package/dist/curation-compliance-off/agents/triage.md +176 -0
  96. package/dist/curation-compliance-off/commands/bug-analysis.md +349 -0
  97. package/dist/curation-compliance-off/commands/code-review.md +481 -0
  98. package/dist/curation-compliance-off/commands/dynamic-build.md +1211 -0
  99. package/dist/curation-compliance-off/commands/dynamic-tickets.md +634 -0
  100. package/dist/curation-compliance-off/commands/implement.md +689 -0
  101. package/dist/curation-compliance-off/commands/plan.md +631 -0
  102. package/dist/curation-compliance-off/commands/release.md +279 -0
  103. package/dist/curation-compliance-off/commands/resolve.md +692 -0
  104. package/dist/curation-off/agents/code.md +10 -10
  105. package/dist/curation-off/agents/fix.md +7 -7
  106. package/dist/curation-off/agents/harden.md +7 -6
  107. package/dist/curation-off/agents/refactor.md +6 -6
  108. package/dist/curation-off/agents/triage.md +7 -0
  109. package/dist/curation-off/commands/bug-analysis.md +69 -119
  110. package/dist/curation-off/commands/code-review.md +11 -11
  111. package/dist/curation-off/commands/debug.md +2 -2
  112. package/dist/curation-off/commands/dynamic-build.md +25 -25
  113. package/dist/curation-off/commands/explore.md +2 -2
  114. package/dist/curation-off/commands/implement.md +48 -34
  115. package/dist/curation-off/commands/plan.md +2 -2
  116. package/dist/curation-off/commands/release.md +1 -1
  117. package/dist/curation-off/commands/resolve.md +50 -46
  118. package/dist/curation-off/commands/self-review.md +10 -9
  119. package/dist/hud/components/curation-counts.js +27 -0
  120. package/dist/skills/git/references/pr/post-resolution-summary.md +12 -11
  121. package/dist/skills/git/references/pr/post-review-summary.md +12 -11
  122. package/dist/skills/git/references/pr/resolve-review-threads.md +1 -1
  123. package/dist/skills/git/references/pr/update-pr-evidence.md +2 -2
  124. package/dist/skills/git/references/publication-gate.md +6 -7
  125. package/dist/skills/git/references/tracker/github/manage-debt.md +6 -2
  126. package/dist/skills/git/references/tracker/jira/manage-debt.md +6 -2
  127. package/dist/skills/git/references/tracker/linear/manage-debt.md +6 -2
  128. package/dist/targets/claude-code/behavior-install.js +23 -9
  129. package/dist/targets/claude-code/charter-render.js +374 -0
  130. package/dist/targets/claude-code/compliance-install.js +32 -12
  131. package/dist/targets/claude-code/compliance-off-files.js +245 -0
  132. package/dist/targets/claude-code/curation-install.js +570 -100
  133. package/dist/targets/claude-code/installer.js +467 -62
  134. package/dist/targets/claude-code/language-stamp.js +6 -0
  135. package/dist/targets/claude-code/linked-folder.js +182 -0
  136. package/dist/targets/claude-code/reference-converge.js +118 -0
  137. package/dist/targets/claude-code/tracker-install.js +21 -8
  138. package/dist/targets/claude-code/tracker-servers.js +20 -6
  139. package/dist/targets/claude-code/workflow-folder-install.js +231 -0
  140. package/dist/variant-axes.json +109 -0
  141. package/package.json +1 -1
  142. package/src/assets/agents/_partials/_code.mds +6 -6
  143. package/src/assets/agents/code.mds +4 -4
  144. package/src/assets/agents/evaluate.md +7 -13
  145. package/src/assets/agents/fix.mds +1 -1
  146. package/src/assets/agents/git.mds +34 -2
  147. package/src/assets/agents/harden.mds +7 -6
  148. package/src/assets/agents/skim.mds +1 -1
  149. package/src/assets/agents/test.md +21 -12
  150. package/src/assets/agents/triage.mds +9 -0
  151. package/src/assets/agents/validate.md +27 -34
  152. package/src/assets/commands/_partials/_compliance.mds +5 -3
  153. package/src/assets/commands/_partials/_decisions.mds +10 -8
  154. package/src/assets/commands/_partials/_engine.mds +9 -8
  155. package/src/assets/commands/_partials/_publication.mds +10 -1
  156. package/src/assets/commands/_partials/_settings.mds +2 -2
  157. package/src/assets/commands/bug-analysis.mds +80 -126
  158. package/src/assets/commands/code-review.mds +52 -9
  159. package/src/assets/commands/dynamic-build.mds +109 -15
  160. package/src/assets/commands/dynamic-tickets.mds +13 -0
  161. package/src/assets/commands/implement.mds +145 -33
  162. package/src/assets/commands/plan.mds +19 -0
  163. package/src/assets/commands/release.mds +50 -1
  164. package/src/assets/commands/resolve.mds +229 -44
  165. package/src/assets/commands/self-review.mds +8 -7
  166. package/src/assets/mds/git/_ops.mds +29 -1
  167. package/src/assets/mds/git/_ops_pr.mds +20 -10
  168. package/src/assets/mds/git/_ops_steps.mds +27 -0
  169. package/src/assets/mds/git/_pr.mds +15 -15
  170. package/src/assets/mds/git/_references.mds +12 -7
  171. package/src/assets/mds/tracker/_common.mds +4 -0
  172. package/src/assets/mds/tracker/_github.mds +43 -4
  173. package/src/assets/mds/tracker/_jira.mds +44 -4
  174. package/src/assets/mds/tracker/_linear.mds +44 -4
  175. package/src/assets/rules/quality.md +2 -0
  176. package/src/assets/scripts/ci-wait.cjs +493 -118
  177. package/src/assets/scripts/hooks/assets/implement-installed +1 -0
  178. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +13 -11
  179. package/src/assets/scripts/hooks/background-memory-update +79 -1
  180. package/src/assets/scripts/hooks/json-helper.cjs +44 -2
  181. package/src/assets/scripts/hooks/lib/curation-store.cjs +82 -20
  182. package/src/assets/scripts/hooks/lib/render-decisions.cjs +9 -1
  183. package/src/assets/scripts/hooks/lib/tracked-areas.cjs +226 -33
  184. package/src/assets/scripts/hooks/memory-permission +10 -2
  185. package/src/assets/scripts/hooks/memory-worker +11 -0
  186. package/src/assets/scripts/hooks/pre-compact-memory +11 -0
  187. package/src/assets/scripts/hooks/preamble +18 -8
  188. package/src/assets/scripts/hooks/queue-append +218 -1
  189. package/src/assets/scripts/hooks/session-start-context +89 -37
  190. package/src/assets/scripts/hooks/session-start-memory +39 -0
  191. package/src/assets/scripts/hooks/session-start-orchestrator +4 -2
  192. package/src/assets/scripts/pr-evidence.cjs +46 -4
  193. package/src/assets/scripts/resolve-settings.cjs +244 -30
  194. package/src/assets/scripts/verify-evidence.cjs +708 -124
  195. package/src/assets/skills/agent-references/SKILL.md +7 -1
  196. package/src/assets/skills/agent-references/references/commands/implement-levels.md +6 -4
  197. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +3 -3
  198. package/src/assets/skills/agent-references/references/commands/plan-levels.md +1 -1
  199. package/src/assets/skills/agent-references/references/commands/resolve-levels.md +3 -2
  200. package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +6 -2
  201. package/src/assets/skills/agent-references/references/synthesize/review.md +4 -4
  202. package/src/assets/skills/agent-references/references/triage/severity-tiers.md +43 -0
  203. package/src/assets/skills/agent-references/references/validate/passed-alone.md +14 -0
  204. package/src/assets/skills/docs-framework/SKILL.md +5 -2
  205. package/src/assets/skills/git/SKILL.md +3 -3
  206. package/src/assets/skills/git/references/github-api.md +2 -139
  207. package/src/assets/skills/git/references/github-posting.md +158 -0
  208. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  209. package/src/assets/skills/review-methodology/references/patterns.md +1 -1
  210. package/src/assets/skills/review-methodology/references/report-template.md +1 -2
  211. package/src/assets/skills/testing/SKILL.md +2 -4
@@ -29,6 +29,10 @@
29
29
  # finds the old folder gone (the one-time move ran meanwhile) looks again and goes
30
30
  # on in the new folder, so a capture never drops a row and never waits. Any other
31
31
  # queue's folder is created if absent.
32
+ # A queue in an area that git tracks, or that git did not answer for, is paused: before
33
+ # the row is built, queue_tracked_area asks for the area the queue lies in, and a queue
34
+ # that is not untracked gets no row, no folder and no file. The skip is logged once. The
35
+ # check runs on every call, whether or not the queue's folder exists.
32
36
  # After appending, truncates from 200 to the newest 100 lines under a lock
33
37
  # (curation_lock_acquire on "<queue_file>.lock", 2s timeout), keeping mode 0600.
34
38
  # The append itself is intentionally lock-free (accepted-class race shared with
@@ -40,7 +44,29 @@
40
44
  # *_enabled flag is "true", with the root that queue lies below. Each queue is gated
41
45
  # independently -- callers compute memory_enabled/curation_enabled themselves (see
42
46
  # queue_read_gates below for the at-most-one-fork read of both). With neither queue
43
- # enabled it builds nothing.
47
+ # enabled it builds nothing. A queue whose area is paused (see queue_tracked_area) reads
48
+ # as not enabled for the call, and nothing is built for it.
49
+ #
50
+ # queue_tracked_area <root> <curation|memory|both> [git|node]
51
+ # Does git track files in the data folders of the area at <root>? Sets
52
+ # DF_TRACKED_AREA_CURATION or DF_TRACKED_AREA_MEMORY (one variable per area) to
53
+ # `tracked`, `untracked` or `unknown`, and always returns 0. The area `both` is one
54
+ # call over the union of the two: it sets both variables, and a listing in it is
55
+ # followed by one call per area, so each variable names its own area. A root is asked
56
+ # once per area in a process. The shell twin of lib/tracked-areas.cjs, held to its
57
+ # answers by tests/decisions/tracked-area-parity.test.ts:
58
+ # - no `.git` entry (a file, a folder or a link) at the root: `untracked`, no call;
59
+ # - a root that is not an absolute path, whose physical parent holds a colon, or that
60
+ # can not be entered: `unknown`;
61
+ # - git mode (the default): one `git ls-files -z` over the area's pathspecs, with the
62
+ # leaf's environment scrub, a ceiling at the root's parent and the fsmonitor command
63
+ # off. Only the first byte of the listing is read. A byte read means `tracked`,
64
+ # whatever git's status (git ends on a closed pipe). With no byte, git status 0
65
+ # means `untracked`, and any other status `unknown`;
66
+ # - node mode: the json-helper `tracked-area` op, which bounds the call in time. Without
67
+ # node it runs as git mode; an answer that is not one of the three words is `unknown`.
68
+ # The capture hooks ask in git mode, with no process but git. The session start asks in
69
+ # node mode, so a hung git is cut.
44
70
  #
45
71
  # queue_read_gates <manifest_path> [<root>]
46
72
  # Reads BOTH switches, `memory` and `curation` (curation is read under both of
@@ -245,7 +271,194 @@ _qar_trim_overflow() {
245
271
  return 0
246
272
  }
247
273
 
274
+ # --- The tracked-area gate: no capture into a folder that git tracks ---
275
+ #
276
+ # A folder that git tracks holds the repository's content, not devflow's data, so a
277
+ # capture writes nothing into it, and also when git did not answer (queue_tracked_area,
278
+ # documented above). The check runs before the row is built and before any folder is
279
+ # made, on every call, whether or not the queue's folder exists.
280
+
281
+ # _qta_specs <curation|memory|both> -- sets the array _QTA_SPECS to the pathspecs of the
282
+ # area: the shell twin of AREA_PATHSPECS in lib/tracked-areas.cjs, in the leaf's order and
283
+ # spelling (tests/decisions/tracked-area-parity.test.ts holds the two equal, element by
284
+ # element). `.devflow` is spelled twice, the second time with the fl ligature (U+FB02,
285
+ # bytes ef ac 82), which a case-folding file system opens as the same name. The `.devflow`
286
+ # entry itself (a link, a gitlink or a plain file) stands over every folder: the character
287
+ # class keeps `glob` from matching it as a leading directory. Fails for any other word.
288
+ _qta_specs() {
289
+ local _qs_fl=$'\xef\xac\x82' _qs_dir="" _qs_curation=() _qs_memory=() _qs_entry=()
290
+ for _qs_dir in ".devflow" ".dev${_qs_fl}ow"; do
291
+ _qs_curation+=(
292
+ ":(icase,literal)${_qs_dir}/learning"
293
+ ":(icase,literal)${_qs_dir}/curation"
294
+ ":(icase,glob)${_qs_dir}/learning.backup-*"
295
+ ":(icase,glob)${_qs_dir}/learning.backup-*/**"
296
+ )
297
+ _qs_memory+=(":(icase,literal)${_qs_dir}/memory")
298
+ _qs_entry+=(":(icase,glob)[.]${_qs_dir#.}")
299
+ done
300
+ case "${1:-}" in
301
+ curation) _QTA_SPECS=("${_qs_curation[@]}" "${_qs_entry[@]}") ;;
302
+ memory) _QTA_SPECS=("${_qs_memory[@]}" "${_qs_entry[@]}") ;;
303
+ both) _QTA_SPECS=("${_qs_curation[@]}" "${_qs_entry[@]}" "${_qs_memory[@]}") ;;
304
+ *) _QTA_SPECS=(); return 1 ;;
305
+ esac
306
+ return 0
307
+ }
308
+
309
+ # _qta_probe <root> <curation|memory|both> <git|node> -- sets _QTA_ANSWER to `tracked`,
310
+ # `untracked` or `unknown`, as queue_tracked_area documents. Always returns 0.
311
+ #
312
+ # The git mode reads one byte of the listing and ends. Under bash 3.2 with errexit set the
313
+ # pipeline sits in an `if` condition (an empty listing makes `read` fail, and a failing
314
+ # condition does not end the shell), and PIPESTATUS is copied in both arms before any other
315
+ # command resets it: the second element is the reader's status, the first git's. Nothing
316
+ # follows the pipeline in an `||` list. The `cd -P` runs inside the subshell, so the
317
+ # caller's directory never changes, and git runs there with the variables that redirect it
318
+ # removed (the list is GIT_REDIRECT_VARIABLES of the leaf), a fixed locale, no optional
319
+ # lock, and the root's physical parent as its ceiling.
320
+ _qta_probe() {
321
+ local _qp_root="${1:-}" _qp_area="${2:-}" _qp_mode="${3:-git}" _qp_out="" _qp_ceiling="" _qp_c="" _qp_st=()
322
+ _QTA_ANSWER="unknown"
323
+ case "$_qp_root" in /*) ;; *) return 0 ;; esac
324
+ if [ ! -e "$_qp_root/.git" ] && [ ! -L "$_qp_root/.git" ]; then
325
+ _QTA_ANSWER="untracked"
326
+ return 0
327
+ fi
328
+ if [ "$_qp_mode" = "node" ] && [ "${_HAS_NODE:-}" = "true" ] && [ -n "${_JSON_HELPER:-}" ]; then
329
+ case "$_qp_area" in curation|memory|both) ;; *) return 0 ;; esac
330
+ _qp_out=$(node "$_JSON_HELPER" tracked-area "$_qp_area" "$_qp_root" 2>/dev/null) || _qp_out=""
331
+ _qp_out="${_qp_out%%$'\n'*}"
332
+ case "$_qp_out" in tracked|untracked|unknown) _QTA_ANSWER="$_qp_out" ;; esac
333
+ return 0
334
+ fi
335
+ _qta_specs "$_qp_area" || return 0
336
+ if (
337
+ cd -P "$_qp_root" >/dev/null 2>&1 || exit 1
338
+ _qp_ceiling="${PWD%/*}"
339
+ [ -n "$_qp_ceiling" ] || _qp_ceiling="/"
340
+ case "$_qp_ceiling" in *:*) exit 1 ;; esac
341
+ unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_COMMON_DIR GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_NAMESPACE GIT_LITERAL_PATHSPECS GIT_GLOB_PATHSPECS GIT_NOGLOB_PATHSPECS GIT_ICASE_PATHSPECS
342
+ export LC_ALL=C GIT_OPTIONAL_LOCKS=0 GIT_CEILING_DIRECTORIES="$_qp_ceiling"
343
+ exec git --no-literal-pathspecs -c core.fsmonitor=false ls-files -z -- "${_QTA_SPECS[@]}"
344
+ ) </dev/null 2>/dev/null | { IFS= read -r -n 1 _qp_c; }; then
345
+ _qp_st=("${PIPESTATUS[@]}")
346
+ else
347
+ _qp_st=("${PIPESTATUS[@]}")
348
+ fi
349
+ if [ "${#_qp_st[@]}" -eq 2 ]; then
350
+ if [ "${_qp_st[1]}" -eq 0 ]; then
351
+ _QTA_ANSWER="tracked"
352
+ elif [ "${_qp_st[0]}" -eq 0 ]; then
353
+ _QTA_ANSWER="untracked"
354
+ fi
355
+ fi
356
+ return 0
357
+ }
358
+
359
+ # _qta_ask <root> <curation|memory> <git|node> -- sets the area's variable, unless this
360
+ # process already asked for that root and area. Always returns 0.
361
+ _qta_ask() {
362
+ case "$2" in
363
+ curation)
364
+ if [ "${_QTA_ASKED_CURATION:-}" != "r$1" ]; then
365
+ _qta_probe "$1" curation "$3"
366
+ DF_TRACKED_AREA_CURATION="$_QTA_ANSWER"
367
+ _QTA_ASKED_CURATION="r$1"
368
+ fi
369
+ ;;
370
+ memory)
371
+ if [ "${_QTA_ASKED_MEMORY:-}" != "r$1" ]; then
372
+ _qta_probe "$1" memory "$3"
373
+ DF_TRACKED_AREA_MEMORY="$_QTA_ANSWER"
374
+ _QTA_ASKED_MEMORY="r$1"
375
+ fi
376
+ ;;
377
+ esac
378
+ return 0
379
+ }
380
+
381
+ queue_tracked_area() {
382
+ local _qa_root="${1:-}" _qa_area="${2:-}" _qa_mode="${3:-git}"
383
+ case "$_qa_area" in
384
+ curation|memory)
385
+ _qta_ask "$_qa_root" "$_qa_area" "$_qa_mode"
386
+ ;;
387
+ both)
388
+ if [ "${_QTA_ASKED_CURATION:-}" != "r$_qa_root" ] || [ "${_QTA_ASKED_MEMORY:-}" != "r$_qa_root" ]; then
389
+ _qta_probe "$_qa_root" both "$_qa_mode"
390
+ if [ "$_QTA_ANSWER" = "tracked" ]; then
391
+ # The union listed something: one call per area says which.
392
+ _QTA_ASKED_CURATION=""
393
+ _QTA_ASKED_MEMORY=""
394
+ _qta_ask "$_qa_root" curation "$_qa_mode"
395
+ _qta_ask "$_qa_root" memory "$_qa_mode"
396
+ else
397
+ DF_TRACKED_AREA_CURATION="$_QTA_ANSWER"
398
+ DF_TRACKED_AREA_MEMORY="$_QTA_ANSWER"
399
+ _QTA_ASKED_CURATION="r$_qa_root"
400
+ _QTA_ASKED_MEMORY="r$_qa_root"
401
+ fi
402
+ fi
403
+ ;;
404
+ esac
405
+ return 0
406
+ }
407
+
408
+ # _qar_gate <memory_root> <memory_on> <curation_root> <curation_on> -- the one gate both
409
+ # append functions use. Sets _QAR_MEMORY_ON and _QAR_CURATION_ON to the flags as given,
410
+ # except that a queue whose area is not `untracked` reads "false" (paused), with one fixed
411
+ # log line. Only an enabled queue is asked about. When both are enabled at one root, one
412
+ # call over the union answers for both (queue_tracked_area `both`). Always returns 0.
413
+ _qar_gate() {
414
+ local _qg_mroot="$1" _qg_mon="$2" _qg_croot="$3" _qg_con="$4"
415
+ if [ "$_qg_mon" = "true" ] && [ "$_qg_con" = "true" ] && [ "$_qg_mroot" = "$_qg_croot" ]; then
416
+ queue_tracked_area "$_qg_mroot" both git
417
+ else
418
+ if [ "$_qg_mon" = "true" ]; then queue_tracked_area "$_qg_mroot" memory git; fi
419
+ if [ "$_qg_con" = "true" ]; then queue_tracked_area "$_qg_croot" curation git; fi
420
+ fi
421
+ if [ "$_qg_mon" = "true" ] && [ "${DF_TRACKED_AREA_MEMORY:-unknown}" != "untracked" ]; then
422
+ _qg_mon="false"
423
+ log "Queue skipped: the memory area is paused (git tracks files in its data folders, or did not answer); nothing written"
424
+ fi
425
+ if [ "$_qg_con" = "true" ] && [ "${DF_TRACKED_AREA_CURATION:-unknown}" != "untracked" ]; then
426
+ _qg_con="false"
427
+ log "Queue skipped: the curation area is paused (git tracks files in its data folders, or did not answer); nothing written"
428
+ fi
429
+ _QAR_MEMORY_ON="$_qg_mon"
430
+ _QAR_CURATION_ON="$_qg_con"
431
+ return 0
432
+ }
433
+
434
+ # _qar_area_of <root> <queue_file> -- sets _QAR_AREA to the area the queue lies in:
435
+ # `memory` below <root>/.devflow/memory, `curation` below the ledger folder (either name),
436
+ # and `both` for a queue in neither, which is paused when either area is.
437
+ _qar_area_of() {
438
+ case "$2" in
439
+ "$1"/.devflow/memory/*) _QAR_AREA="memory" ;;
440
+ "$1"/.devflow/learning/*|"$1"/.devflow/curation/*) _QAR_AREA="curation" ;;
441
+ *) _QAR_AREA="both" ;;
442
+ esac
443
+ return 0
444
+ }
445
+
248
446
  queue_append_row() {
447
+ _qar_area_of "$1" "$2"
448
+ case "$_QAR_AREA" in
449
+ memory)
450
+ _qar_gate "$1" true "" false
451
+ if [ "$_QAR_MEMORY_ON" != "true" ]; then return 0; fi
452
+ ;;
453
+ curation)
454
+ _qar_gate "" false "$1" true
455
+ if [ "$_QAR_CURATION_ON" != "true" ]; then return 0; fi
456
+ ;;
457
+ *)
458
+ _qar_gate "$1" true "$1" true
459
+ if [ "$_QAR_MEMORY_ON" != "true" ] || [ "$_QAR_CURATION_ON" != "true" ]; then return 0; fi
460
+ ;;
461
+ esac
249
462
  _qar_build_row "$3" "$4" "$5" || return 0
250
463
  if _qar_append "$1" "$2" "$_QAR_ROW"; then _qar_trim_overflow "$_QAR_FILE"; fi
251
464
  return 0
@@ -257,6 +470,10 @@ queue_append_both() {
257
470
  local _qab_memory_enabled="$5" _qab_curation_enabled="$6"
258
471
  local _qab_role="$7" _qab_content="$8" _qab_ts="$9" _qab_row=""
259
472
 
473
+ # A paused queue reads as not enabled: the gate comes before the row is built.
474
+ _qar_gate "$_qab_memory_root" "$_qab_memory_enabled" "$_qab_curation_root" "$_qab_curation_enabled"
475
+ _qab_memory_enabled="$_QAR_MEMORY_ON"
476
+ _qab_curation_enabled="$_QAR_CURATION_ON"
260
477
  if [ "$_qab_memory_enabled" != "true" ] && [ "$_qab_curation_enabled" != "true" ]; then return 0; fi
261
478
  # One row for both queues: it is built once, and each enabled queue takes the same line.
262
479
  _qar_build_row "$_qab_role" "$_qab_content" "$_qab_ts" || return 0
@@ -6,7 +6,10 @@
6
6
  # which this checkout's project.json / config.json can only narrow
7
7
  # (D-FEATURES-NARROW-ONLY) — this hook itself is never disabled. They and the .gitignore carve-out run only in a git project that is
8
8
  # not the home directory (D-HOOKS-GIT-ONLY), and they read the curation ledger of
9
- # the main worktree when this is a linked one (D-LEDGER-MAIN-WORKTREE).
9
+ # the main worktree when this is a linked one (D-LEDGER-MAIN-WORKTREE). They are also
10
+ # paused while git tracks files in a curation data folder of that ledger root, or does
11
+ # not answer: the hook then reads no curation file and makes no curation folder, and one
12
+ # fixed notice goes to the user as a systemMessage (Section 4b).
10
13
  #
11
14
  # Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines),
12
15
  # then the path of the decisions index when it lists entries.
@@ -23,11 +26,12 @@
23
26
  # Section 4: Legacy install notice — one line when this repository still carries
24
27
  # a retired project-local devflow install (its .claude/settings.json registers
25
28
  # devflow hooks), pointing at `devflow uninstall --scope local`.
26
- # Section 4b: Curation move skipped notice — one fixed line when the one-time move of
27
- # the old ledger folder left the project alone because git tracks files in a curation
28
- # data folder (`tracked`) or did not answer (`unknown`); the `curation-move` shim sets
29
- # DF_CURATION_MOVE_SKIPPED, and on a session that starts, resumes or is cleared the line
30
- # tells the main model to tell the user once.
29
+ # Section 4b: Curation pause notice — one fixed systemMessage when curation is on and
30
+ # git tracks files in a curation data folder (`tracked`) or did not answer (`unknown`).
31
+ # The answer is the skipped move's (the `curation-move` shim sets DF_CURATION_MOVE_SKIPPED)
32
+ # or, when the move was not skipped, the curation area check's own fresh answer; on a
33
+ # session that starts, resumes or is cleared the message tells the user why curation is
34
+ # paused.
31
35
  # Section 5: Compaction resume directive — on a `compact` SessionStart, one fixed
32
36
  # line telling the model to re-read the devflow command it was running from the
33
37
  # phase in progress (D-COMPACT-RESUME-DIRECTIVE).
@@ -53,6 +57,12 @@
53
57
  # - Section 6, changed start: ONE node process (claude-md-audit.cjs), and a
54
58
  # sibling temp file renamed over the stamp. When none of the roots exists there
55
59
  # is nothing to audit: the stamp is written with builtins, and node is not started.
60
+ #
61
+ # Cost of the curation area check (after the move, with curation on in a git project):
62
+ # - the move already answered `tracked` or `unknown`: its answer is reused, zero forks;
63
+ # - no `.git` entry at the ledger root: a builtin test, zero forks;
64
+ # - otherwise ONE node process (json-helper tracked-area), which makes one bounded
65
+ # `git ls-files` call; without node the shell runs that one call itself, unbounded.
56
66
 
57
67
  # Safe no-op fallback: must exist before hook-bootstrap is sourced.
58
68
  dbg() { :; }
@@ -267,12 +277,42 @@ CURATION_ENABLED="$_QG_CURATION"
267
277
  # root is looked at once per session. A held, refused or skipped move leaves the old
268
278
  # folder in place. A skipped move (git tracks files in a data folder or a backup folder,
269
279
  # or git did not answer) sets DF_CURATION_MOVE_SKIPPED to `tracked` or `unknown`, and
270
- # Section 4b tells the user.
280
+ # the curation area check below pauses curation on it.
271
281
  DF_CURATION_MOVE_SKIPPED=""
272
282
  if [ -n "$PROJECT_OK" ]; then
273
283
  df_curation_move "$LEDGER_ROOT" retry 2>/dev/null || true
274
284
  fi
275
285
 
286
+ # --- The curation area check: a pause while git tracks the ledger folders ---
287
+ # A folder that git tracks holds the repository's content, not devflow's data, so while git
288
+ # tracks files in a curation data folder of the LEDGER root (the old ledger folder, the
289
+ # curation folder or a backup folder), or does not answer, curation is off for the rest of
290
+ # this hook: no heal, no Section 1, no Section 2, and no curation folder is made. The
291
+ # move's own answer is reused when it skipped (`tracked` or `unknown`); otherwise
292
+ # queue_tracked_area asks once in node mode, so a git that hangs is cut. The check runs
293
+ # whether or not the folder exists, and on every source, a compaction included. AREA_PAUSE_REASON
294
+ # names why for Section 4b; it stays empty when curation is on.
295
+ AREA_PAUSE_REASON=""
296
+ if [ "$CURATION_ENABLED" = "true" ]; then
297
+ case "${DF_CURATION_MOVE_SKIPPED:-}" in
298
+ tracked|unknown)
299
+ AREA_PAUSE_REASON="$DF_CURATION_MOVE_SKIPPED"
300
+ ;;
301
+ *)
302
+ queue_tracked_area "$LEDGER_ROOT" curation node
303
+ case "$DF_TRACKED_AREA_CURATION" in
304
+ untracked) ;;
305
+ tracked) AREA_PAUSE_REASON="tracked" ;;
306
+ *) AREA_PAUSE_REASON="unknown" ;;
307
+ esac
308
+ ;;
309
+ esac
310
+ if [ -n "$AREA_PAUSE_REASON" ]; then
311
+ CURATION_ENABLED="false"
312
+ log "Curation paused: git tracks files in a curation data folder, or did not answer (${AREA_PAUSE_REASON}); nothing read or written"
313
+ fi
314
+ fi
315
+
276
316
  # --- Section 1: Project Decisions TL;DR ---
277
317
  if [ "$CURATION_ENABLED" = "true" ]; then
278
318
  # Heal older installs that have .devflow/ but not .devflow/curation/, never
@@ -305,10 +345,13 @@ if [ "$CURATION_ENABLED" = "true" ]; then
305
345
  # as DECISIONS_CONTEXT. It interpolates $LEDGER_ROOT, so it needs that root's
306
346
  # shape gate, and it is left out when the index lists no entry: an empty
307
347
  # corpus renders `(none)`. It is left out too when a link leads to the index,
308
- # since the model would read the file the link names and pass that on.
348
+ # since the model would read the file the link names and pass that on. The pass rule
349
+ # tells the model to read the index only while the settings line does not say
350
+ # CURATION=off; resolve-settings.cjs turns that switch off while git tracks the
351
+ # curation folder, so a paused project passes nothing on.
309
352
  # Builtins only: no fork, save the log line of a refused index.
310
353
  _SC_INDEX="$CURATION_DIR/index.md"; DECISIONS_INDEX_LINE=""
311
- DECISIONS_PASS_RULE="Decisions (direct delegations only — workflow skills load their own): read this index once, then pass each agent that takes DECISIONS_CONTEXT the index lines that touch its work, verbatim, behind a (slice: N of M entries — full index: this index's path) line and with the footer."
354
+ DECISIONS_PASS_RULE="Decisions (direct delegations only — workflow skills load their own): when the settings line does not say CURATION=off, read this index once, then pass each agent that takes DECISIONS_CONTEXT the index lines that touch its work, verbatim, behind a (slice: N of M entries — full index: this index's path) line and with the footer."
312
355
  if [ -n "$DIRECTIVE_LEDGER_SAFE" ] && df_file_below "$LEDGER_ROOT" "$_SC_INDEX" && [ -s "$_SC_INDEX" ]; then
313
356
  _SC_IDX1=""; IFS= read -r _SC_IDX1 < "$_SC_INDEX" || true
314
357
  [ "$_SC_IDX1" != "(none)" ] && DECISIONS_INDEX_LINE="Index: $_SC_INDEX"
@@ -873,39 +916,37 @@ ${LEGACY_SECTION}"
873
916
  fi
874
917
  fi
875
918
 
876
- # --- Section 4b: Curation move skipped notice ---
877
- # The move of the old curation folder leaves a project alone when git tracks files in
878
- # a curation data folder, or when git does not answer (curation-move sets
879
- # DF_CURATION_MOVE_SKIPPED at the call above, and the line before that call empties it).
880
- # The user cannot see the hook log, so one fixed line goes into the context on a session
881
- # that starts, resumes or is cleared, and the model tells the user once per such session.
882
- # Each line is a constant: a single-quoted string that interpolates nothing, so no
883
- # repository content reaches the model through it. Its words are MOVE_SKIP_NOTICE in
884
- # src/core/curation-move-skip.ts (tests/shell-hooks.test.ts holds the two equal). A
885
- # compaction repeats nothing.
886
- MOVE_SKIP_SECTION=""
887
- case "${DF_CURATION_MOVE_SKIPPED:-}" in
919
+ # --- Section 4b: Curation pause notice ---
920
+ # While curation is on, curation pauses when git tracks files in a curation data folder,
921
+ # or when git does not answer (the curation area check above sets AREA_PAUSE_REASON). The
922
+ # answer is the skipped move's (curation-move sets DF_CURATION_MOVE_SKIPPED at the call
923
+ # above, and the line before that call empties it) or, when the move was not skipped, the
924
+ # area check's own fresh answer. The user cannot see the hook log, so the pause notice says
925
+ # why curation reads and writes nothing, and what to do. It is not model context. It goes
926
+ # to the user as a systemMessage (AREA_PAUSE_MESSAGE, joined to the envelope at the end),
927
+ # on a session that starts, resumes or is cleared, and a compaction repeats nothing. It is
928
+ # a constant: a single-quoted string that interpolates nothing, so no repository content
929
+ # reaches the user through it. Its words are AREA_PAUSE_NOTICE.curation in src/core/area-pause-notice.ts
930
+ # (tests/shell-hooks.test.ts holds the two equal). With curation switched off, devflow
931
+ # shows no curation text at session start: the move still runs, and its skip is told to
932
+ # the user by `devflow init`, not here.
933
+ AREA_PAUSE_MESSAGE=""
934
+ case "$AREA_PAUSE_REASON" in
888
935
  tracked)
889
- MOVE_SKIP_SECTION='Devflow notice: tell the user once, in one short message alongside your answer: devflow did not move the curation data of this project from .devflow/learning/ to .devflow/curation/, because git tracks files in a curation data folder. To find them, run `git ls-files .devflow`. Untrack them with `git rm --cached <file>`, and delete them if you did not make them. Run these commands in the checkout that holds these folders.'
936
+ AREA_PAUSE_MESSAGE='Devflow curation is paused for this project, because git tracks files in a curation data folder. Devflow reads and writes nothing in these folders while git tracks files there. To find the files, run `git ls-files .devflow`. Untrack them with `git rm --cached <file>`, then delete them if you did not make them. Run these commands in the main checkout.'
890
937
  ;;
891
938
  unknown)
892
- MOVE_SKIP_SECTION='Devflow notice: tell the user once, in one short message alongside your answer: devflow did not move the curation data of this project from .devflow/learning/ to .devflow/curation/, because git did not answer. The move runs again at the next session start or the next init. If this notice stays, check that git runs in this project.'
939
+ AREA_PAUSE_MESSAGE='Devflow curation is paused for this project, because git did not answer whether it tracks files in a curation data folder. Devflow reads and writes nothing in these folders until git answers. The check runs again at the next session start. If this notice stays, check that git runs in this project.'
940
+ ;;
941
+ esac
942
+ case "$SESSION_SOURCE" in
943
+ startup|resume|clear)
944
+ if [ -n "$AREA_PAUSE_MESSAGE" ]; then dbg "curation pause notice emitted"; fi
945
+ ;;
946
+ *)
947
+ AREA_PAUSE_MESSAGE=""
893
948
  ;;
894
949
  esac
895
- if [ -n "$MOVE_SKIP_SECTION" ]; then
896
- case "$SESSION_SOURCE" in
897
- startup|resume|clear)
898
- dbg "curation move skipped notice emitted"
899
- if [ -n "$CONTEXT" ]; then
900
- CONTEXT="${CONTEXT}
901
-
902
- ${MOVE_SKIP_SECTION}"
903
- else
904
- CONTEXT="$MOVE_SKIP_SECTION"
905
- fi
906
- ;;
907
- esac
908
- fi
909
950
 
910
951
  # --- Section 5: Compaction resume directive ---
911
952
  # D-COMPACT-RESUME-DIRECTIVE: after a compaction nothing tells the model to re-read
@@ -1165,6 +1206,17 @@ fi
1165
1206
 
1166
1207
  # --- Output ---
1167
1208
 
1209
+ # The pause notice (Section 4b) leads the systemMessage, ahead of the audit text: both are
1210
+ # for the user, and neither enters the context.
1211
+ if [ -n "$AREA_PAUSE_MESSAGE" ]; then
1212
+ if [ -n "$SYSTEM_MESSAGE" ]; then
1213
+ SYSTEM_MESSAGE="${AREA_PAUSE_MESSAGE}
1214
+ ${SYSTEM_MESSAGE}"
1215
+ else
1216
+ SYSTEM_MESSAGE="$AREA_PAUSE_MESSAGE"
1217
+ fi
1218
+ fi
1219
+
1168
1220
  # Only output if we have something to inject or to tell the user. The stamp is
1169
1221
  # recorded here too: an audit that found nothing to show still ran, and recording it
1170
1222
  # is what makes the next unchanged start free.
@@ -64,6 +64,45 @@ if [ "$_QG_MEMORY" != "true" ]; then
64
64
  exit 0
65
65
  fi
66
66
 
67
+ # --- Memory area check ---
68
+ # A folder that git tracks holds the repository's content, not devflow's data. While git
69
+ # tracks files in the memory folder of this checkout, or a `.git` entry stands and git does
70
+ # not answer, the memory area is paused: this hook moves no batch, reads no working memory,
71
+ # no refresh warning, no queue and no worker notice, and the worker notice stays where it is.
72
+ # The check asks once, in node mode, so a git that hangs is cut (queue_tracked_area), and
73
+ # asks at the project root this hook resolved, not at the ledger root.
74
+ # One fixed notice goes to the user as a systemMessage, on a session that starts, resumes
75
+ # or is cleared; a compaction sends nothing. It is not model context. Each arm is a constant:
76
+ # a single-quoted string that interpolates nothing, so no repository content reaches the
77
+ # user through it. Its words are AREA_PAUSE_NOTICE.memory in src/core/area-pause-notice.ts
78
+ # (tests/shell-hooks.test.ts holds the two equal).
79
+ queue_tracked_area "$PROJECT_ROOT" memory node
80
+ if [ "${DF_TRACKED_AREA_MEMORY:-unknown}" != "untracked" ]; then
81
+ _SSM_PAUSE_REASON="unknown"
82
+ if [ "${DF_TRACKED_AREA_MEMORY:-}" = "tracked" ]; then _SSM_PAUSE_REASON="tracked"; fi
83
+ log "Memory paused: git tracks files in the memory folder, or did not answer (${_SSM_PAUSE_REASON}); nothing read or written"
84
+ AREA_PAUSE_MESSAGE=""
85
+ case "$_SSM_PAUSE_REASON" in
86
+ tracked)
87
+ AREA_PAUSE_MESSAGE='Devflow memory is paused for this checkout, because git tracks files in the memory folder. Devflow reads and writes nothing there while git tracks files there. To find the files, run `git ls-files .devflow`. Untrack them with `git rm --cached <file>`, then delete them if you did not make them. Run these commands in this checkout.'
88
+ ;;
89
+ unknown)
90
+ AREA_PAUSE_MESSAGE='Devflow memory is paused for this checkout, because git did not answer whether it tracks files in the memory folder. Devflow reads and writes nothing there until git answers. The check runs again at the next session start. If this notice stays, check that git runs in this checkout.'
91
+ ;;
92
+ esac
93
+ CONTEXT=""
94
+ SYSTEM_MESSAGE=""
95
+ case "$(printf '%s' "$INPUT" | json_field "source" "")" in
96
+ startup|resume|clear)
97
+ SYSTEM_MESSAGE="$AREA_PAUSE_MESSAGE"
98
+ dbg "memory pause notice emitted"
99
+ json_session_output "$CONTEXT" "$SYSTEM_MESSAGE"
100
+ ;;
101
+ esac
102
+ dbg "EXIT: memory area paused"
103
+ exit 0
104
+ fi
105
+
67
106
  # --- D56c cold-path recovery: orphaned .pending-turns.processing ---
68
107
  # Self-contained (not sourced from a shared helper): applies a 300s stale-age
69
108
  # gate to .pending-turns.processing. background-memory-update is the PRIMARY
@@ -3,8 +3,10 @@
3
3
  # SessionStart Hook: Orchestrator Charter Injection
4
4
  # Presence-gated: registered/removed by `devflow ambient --enable/--disable`.
5
5
  # Git repos only: silent outside a git repo (df_has_git_marker bounded walk).
6
- # Charter includes the plan-handoff fallback bullet — SessionStart provably fires
7
- # (SessionStart:clear) in plan-handoff sessions even if UserPromptSubmit does not.
6
+ # The installed charter carries the plan-handoff fallback bullet only while
7
+ # devflow:implement is installed (init renders the charter for the installed plugin set) —
8
+ # SessionStart provably fires (SessionStart:clear) in plan-handoff sessions even if
9
+ # UserPromptSubmit does not.
8
10
  # NOTE: the literal prefix `Implement the following plan:` is duplicated in
9
11
  # src/assets/scripts/hooks/preamble — update both if Claude Code changes the handoff format.
10
12
 
@@ -91,12 +91,15 @@ const LIMITS = Object.freeze({
91
91
  * D-MARKERS: the ONLY home of the marker literals. Every other site builds a
92
92
  * marker from these fields, and a source guard holds that no literal appears
93
93
  * outside this object. The block markers bracket the PR body's test-plan block;
94
- * the evidence marker is the first line of an evidence comment.
94
+ * the evidence marker is the first line of an evidence comment, and its head prefix
95
+ * is what that line starts with whatever its head and key. The head prefix is the
96
+ * evidence open literal plus ' head:', so it adds no literal of its own.
95
97
  */
96
98
  const MARKERS = Object.freeze({
97
99
  BLOCK_START: '<!-- devflow:test-plan -->',
98
100
  BLOCK_END: '<!-- /devflow:test-plan -->',
99
101
  EVIDENCE_OPEN: '<!-- devflow:evidence',
102
+ get EVIDENCE_HEAD_PREFIX() { return this.EVIDENCE_OPEN + ' head:'; },
100
103
  EVIDENCE_RE: Object.freeze(/^<!-- devflow:evidence head:(?<head>[0-9a-f]{40}) key:(?<key>[0-9a-f]{12}) -->$/),
101
104
  });
102
105
 
@@ -247,7 +250,11 @@ const WAVE_VERDICTS = Object.freeze(/** @type {WaveVerdict[]} */ (['PASS', 'UNVE
247
250
  /** The verdicts of a ticket that merged — the only rows a `Closes` line may name. */
248
251
  const WAVE_MERGED_VERDICTS = Object.freeze(/** @type {WaveVerdict[]} */ (['PASS', 'UNVERIFIED']));
249
252
 
250
- /** An Evaluate or Test cell: a Gate 2 verdict, or `—` where the gate never ran. */
253
+ /**
254
+ * An Evaluate or Test cell: a Gate 2 verdict, or `—` where the gate never ran or a Test run gave no result.
255
+ * `BLOCKED` is never a cell here, and it has two meanings in the table: a row Verdict `BLOCKED` is a ticket that
256
+ * never ran, and a Test result `BLOCKED` is a run that gave no result, shown as `—`.
257
+ */
251
258
  const WAVE_GATE_VALUES = Object.freeze(['PASS', 'FAIL', 'FAIL-FIXED', 'SKIPPED', EM_DASH]);
252
259
 
253
260
  /** A Coverage cell: whether the review pass covered every focus. */
@@ -1250,7 +1257,7 @@ function cell(text) {
1250
1257
  */
1251
1258
  function stubLines(ev, t) {
1252
1259
  return [
1253
- MARKERS.EVIDENCE_OPEN + ' head:' + ev.head + ' key:' + ev.key + ' -->',
1260
+ MARKERS.EVIDENCE_HEAD_PREFIX + ev.head + ' key:' + ev.key + ' -->',
1254
1261
  EVIDENCE_HEADING + ' ' + EM_DASH + ' ' + ev.head.slice(0, 7),
1255
1262
  tallyLine(t),
1256
1263
  '',
@@ -1813,6 +1820,26 @@ function parseWaveRow(line) {
1813
1820
  };
1814
1821
  }
1815
1822
 
1823
+ /**
1824
+ * D-WAVE-LINKS-ONLY: the links-only block, for related lines that ran to the end of the
1825
+ * text. Only `Refs #<n>` and `Refs <KEY-N>` lines make a block of their own (RELATED_LINE_RE
1826
+ * has already shaped each ref), and a ref may not repeat. Null when the lines are not all
1827
+ * `Refs` lines: the caller refuses the block as before.
1828
+ *
1829
+ * @param {readonly WaveRelated[]} related
1830
+ * @returns {Result<WaveBlock> | null}
1831
+ */
1832
+ function parseLinksOnlyBlock(related) {
1833
+ if (related.length === 0 || !related.every(rel => rel.keyword === 'Refs')) return null;
1834
+ /** @type {Set<string>} */
1835
+ const seen = new Set();
1836
+ for (const rel of related) {
1837
+ if (seen.has(rel.ref)) return fail('duplicate', rel.line);
1838
+ seen.add(rel.ref);
1839
+ }
1840
+ return ok(Object.freeze({ tracking: null, related: Object.freeze(related), rows: Object.freeze([]) }));
1841
+ }
1842
+
1816
1843
  /**
1817
1844
  * D-WAVE: parse a wave block (LF or CRLF, at most WAVE_BLOCK_CHARS). It holds
1818
1845
  * exactly, in order: `## Related Issues`; 0–WAVE_ROWS + 1 related lines; one
@@ -1835,13 +1862,23 @@ function parseWaveRow(line) {
1835
1862
  * like every related line, so it adds no free text to the body. A leading `Refs`
1836
1863
  * that names a row is that row's line, never a tracking line.
1837
1864
  *
1865
+ * D-WAVE-LINKS-ONLY: with `options.linksOnly === true` (the opt-in, spelled
1866
+ * `check wave --links-only` on the command line), a block may also be the
1867
+ * `## Related Issues` heading and 1–WAVE_ROWS + 1 `Refs #<n>` or `Refs <KEY-N>` lines and
1868
+ * nothing after them (blank lines aside): no evidence section, no rows. It parses to no
1869
+ * tracking line and no rows. A ref may not repeat (`duplicate`). `Closes` is never
1870
+ * admitted there: with no row, nothing has merged, so nothing may close an issue.
1871
+ * Without the opt-in, and for any other block that stops after its related lines, a lone
1872
+ * `Closes #<n>` included, the block is `malformed`, as the grammar stood before the opt-in.
1873
+ *
1838
1874
  * Structure is checked before the cross rules, so the first failure found is the
1839
1875
  * one reported. An error carries a code and a line number, never input bytes.
1840
1876
  *
1841
1877
  * @param {unknown} text
1878
+ * @param {{ linksOnly?: boolean }} [options]
1842
1879
  * @returns {Result<WaveBlock>}
1843
1880
  */
1844
- function parseWaveBlock(text) {
1881
+ function parseWaveBlock(text, options) {
1845
1882
  if (typeof text !== 'string') return fail('invalid');
1846
1883
  if (text.length > LIMITS.WAVE_BLOCK_CHARS) return fail('oversize');
1847
1884
  const rows = textLines(text);
@@ -1864,6 +1901,11 @@ function parseWaveBlock(text) {
1864
1901
  line: i + 1,
1865
1902
  }));
1866
1903
  }
1904
+ // D-WAVE-LINKS-ONLY: the opt-in is set and the related lines ran to the end of the text.
1905
+ if (isObject(options) && options.linksOnly === true && i >= end) {
1906
+ const linksOnly = parseLinksOnlyBlock(related);
1907
+ if (linksOnly !== null) return linksOnly;
1908
+ }
1867
1909
  // rows[i] is the one blank line; indices past `end` hold only blanks, so a
1868
1910
  // missing section fails the first comparison that expects a non-blank line.
1869
1911
  if (rows[i + 1] !== WAVE_HEADINGS[1]) return fail('malformed', i + 2);