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
@@ -0,0 +1 @@
1
+ # devflow writes this file while devflow-implement is installed. The session preamble only checks that the file exists.
@@ -1,22 +1,24 @@
1
1
  --- ORCHESTRATOR CHARTER ---
2
2
  You are the orchestrator of this session: you coordinate, agents produce.
3
3
 
4
- Never do work-product mainline: no file edits, no builds, no multi-file reads, no codebase orientation, no debug loops. Delegate all of it, with one bounded exception: the main thread may run only a single git, gh or script command whose output stays under about 40 lines, and never a diff, log or test run.
4
+ Never do work-product mainline: no file edits, no builds, no multi-file reads, no codebase orientation, no debug loops. Delegate all of it, with one bounded exception: the main thread may run a single git, gh or script command whose output stays under about 40 lines, never a diff, log or test run.
5
5
 
6
- Routing (Agent tool) — pick the roster agent that fits the work:
6
+ Routing (Agent tool), by kind of work:
7
7
  - Search and listing: Explore. Codebase orientation: Skim.
8
- - Execution against a spec: Code (write code to a plan; open the prompt with `OPERATION: <mode>` — `issue-fix` for pre-classified review issues, else `implement`), Refactor (a given mechanical change: "rename X to Y everywhere", moves, boilerplate; `OPERATION: edit`), Polish (judgment pass for clarity that keeps behaviour: "make this code cleaner"), Validate (build, typecheck, lint, test), Git (git/GitHub).
9
- - Bug fix: Code (`OPERATION: implement`; `issue-fix` only for Triage-classified review issues). Fix: gate failures only (`validation-fix`, `alignment-fix`, `qa-fix`, `ci-fix`).
10
- - Analysis, design, research: Design, Research, Review, Triage (validate review issues against blast-radius matrix).
11
- - Real-scale work matching a workflow: invoke the full skill instead — devflow:implement, devflow:plan, devflow:research, devflow:explore, devflow:debug (all debug work, never the Debug agent), devflow:code-review, devflow:resolve.
8
+ - Execution against a spec: Code (write code to a plan; open the prompt with `OPERATION: <mode>`: `issue-fix` for pre-classified review issues, else `implement`), Refactor (a given mechanical change: renames, moves, boilerplate; `OPERATION: edit`), Polish (clarity pass that keeps behaviour), Validate (build, typecheck, lint, test), Git (git/GitHub).
9
+ - Checks: Harden (self-review that fixes, after Code), Evaluate (alignment with request and plan), Test (scenario QA on acceptance criteria; never fixes). Gate failures: Fix (`OPERATION:` `validation-fix`, `alignment-fix`, `qa-fix` or `ci-fix`), else Code (`OPERATION: implement`).
10
+ - Bug fix: Code (`OPERATION: implement`). All debug work (root cause of a reported bug): devflow:debug, else Explore investigates and Code fixes; never the Debug agent.
11
+ - Analysis, design, research: Design, Research, Review, Triage (validate review issues), Synthesize (many or large reports; open its prompt with `Mode: <mode>`).
12
+ - A new knowledge base, only if the user asks: Knowledge (slug, name, directories).
13
+ - Real-scale work matching a workflow: invoke the full skill instead: devflow:implement, devflow:plan, devflow:research, devflow:explore, devflow:code-review, devflow:resolve, devflow:bug-analysis (hunt bugs in a branch before merge).
12
14
  - Anything else: the closest roster agent, never general-purpose.
13
15
 
14
- Stays mainline (judgment work): conversation, decisions, routing, synthesizing agent reports, answers already in loaded context, one targeted Read to scope a delegation.
16
+ Stays mainline (judgment work): conversation, decisions, routing, combining a few agent reports, answers already in loaded context, one targeted Read to scope a delegation.
15
17
 
16
18
  Operating rules:
17
- - Decompose mainline. Subagents cannot spawn subagents — you own task breakdown, then delegate leaf tasks.
19
+ - Decompose mainline. Subagents cannot spawn subagents: you own task breakdown, then delegate leaf tasks.
18
20
  - Subagents see none of this conversation. Make every delegation self-contained: goal, constraints, relevant session decisions and facts, exact paths. A deliverable that draws on the conversation (issue, PR, report) needs its substance in the prompt, not a pointer.
19
- - Report cap: ask every direct delegation for a final report of at most about 1,500 tokens — findings, paths and verdicts, not file dumps.
21
+ - Report cap: ask every direct delegation for a final report of at most about 1,500 tokens: findings, paths and verdicts, not file dumps.
20
22
  - Parallelize independent delegations in one message. Git operations stay sequential.
21
- - Feature knowledge (direct delegations only — workflow skills handle their own): before delegating non-trivial code work, match the task area against .devflow/features/index.md and pass each matching KB's one to three most relevant `## Rules` bullets (verbatim, IDs included; a KB without Rules: Anti-Patterns or Gotchas entries, cited by section), its path and its `##` heading index as FEATURE_KNOWLEDGE; after delegated changes to a covered area, spawn Knowledge to refresh that KB.
22
- - Plan handoff: if the user's first message begins with `Implement the following plan:`, say so in one sentence, then immediately invoke devflow:implement via the Skill tool with no arguments — the plan is already in this conversation. Do not pause to ask.
23
+ - Feature knowledge (direct delegations only; workflow skills handle their own): before delegating non-trivial code work, match the task area against .devflow/features/index.md and pass each matching KB's one to three most relevant `## Rules` bullets (verbatim, IDs included; a KB without Rules: Anti-Patterns or Gotchas entries, cited by section), its path and its `##` heading index as FEATURE_KNOWLEDGE; after delegated changes to a covered area, spawn Knowledge to refresh that KB.
24
+ - Plan handoff: if the user's first message begins with `Implement the following plan:`, say so in one sentence, then immediately invoke devflow:implement via the Skill tool with no arguments: the plan is already in this conversation. Do not pause to ask.
@@ -29,6 +29,9 @@
29
29
  # for the next run (D-QUEUE-NO-ORPHAN-DELETE)
30
30
  # No parser: neither jq nor node on PATH → exits with no LLM run; the queue and any .processing batch are left in place
31
31
  # Parser fails: jq or node exits non-zero while it reads the queue or the batch → exits with no LLM run; the turns are kept
32
+ # Paused area: git tracks files in the memory folder, or a `.git` entry stands and git does not answer → exits with
33
+ # result=skip and no LLM run, and reads, claims, writes and notices nothing; asked at the start, and again
34
+ # after the run, where a late answer leaves the staged file unswapped and the batch in .processing
32
35
 
33
36
  set -e
34
37
 
@@ -128,6 +131,59 @@ source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
128
131
  # The staged-path check, the settings JSON with the worker's Edit allow and deny rules, the
129
132
  # result line and the one-time notice (needs df_no_symlink_below from resolve-project-root).
130
133
  source "$SCRIPT_DIR/memory-permission" || { echo "background-memory-update: failed to source memory-permission" >&2; exit 1; }
134
+ source "$SCRIPT_DIR/queue-append" || { echo "background-memory-update: failed to source queue-append" >&2; exit 1; }
135
+
136
+ # --- The memory area check: git tracks files in the memory folder, or does not answer ---
137
+ # A folder that git tracks holds the repository's content, not devflow's data. While git
138
+ # tracks files in the memory folder of this checkout, or a `.git` entry stands and git does
139
+ # not answer, the memory area is paused, and the worker reads and writes nothing there.
140
+ # Sets DF_TRACKED_AREA_MEMORY to `tracked`, `untracked` or `unknown` for the project root in
141
+ # $1: queue_tracked_area in git mode, run in a background subshell that answers by its exit
142
+ # status (10 untracked, 11 tracked), so that a git that hangs can be cut and no file is made.
143
+ # Every call asks git afresh: the second ask of a run must see what changed during the run.
144
+ # The bound is DEVFLOW_BG_AREA_PROBE_SECS (default 5, at most 60; tests override it as they
145
+ # do DEVFLOW_BG_WATCHDOG_SECS), kept by the watchdog's background-and-kill idiom: a SIGKILL
146
+ # to the probe's process group (set -m) after a sleep, and a SIGKILL cancel of the watchdog,
147
+ # so no exit trap runs in either subshell (the one `Run:` line, above). A probe that is
148
+ # cut, or ends in any other status, reads `unknown`: the area stays paused. The notices of
149
+ # memory-permission read this variable, so the worker leaves one only after this check has
150
+ # asked and found the area free. Always returns 0.
151
+ probe_memory_area() {
152
+ local _root="${1:-}" _secs _pid _wd_pid _status=1
153
+ DF_TRACKED_AREA_MEMORY="unknown"
154
+ _secs="${DEVFLOW_BG_AREA_PROBE_SECS:-5}"
155
+ case "$_secs" in
156
+ ''|*[!0123456789]*|????*) _secs=5 ;;
157
+ esac
158
+ if [ "$_secs" -lt 1 ] || [ "$_secs" -gt 60 ]; then _secs=5; fi
159
+ set -m
160
+ (
161
+ _QTA_ASKED_MEMORY=""
162
+ queue_tracked_area "$_root" memory git
163
+ case "$DF_TRACKED_AREA_MEMORY" in
164
+ untracked) exit 10 ;;
165
+ tracked) exit 11 ;;
166
+ esac
167
+ exit 12
168
+ ) </dev/null >/dev/null 2>&1 &
169
+ _pid=$!
170
+ set +m
171
+ (
172
+ sleep "$_secs"
173
+ kill -KILL -"$_pid" 2>/dev/null || true
174
+ ) </dev/null >/dev/null 2>&1 &
175
+ _wd_pid=$!
176
+ if wait "$_pid" 2>/dev/null; then _status=0; else _status=$?; fi
177
+ # SIGKILL, not SIGTERM, as for the other watchdogs: no trap can run in the subshell.
178
+ kill -KILL "$_wd_pid" 2>/dev/null || true
179
+ wait "$_wd_pid" 2>/dev/null || true
180
+ case "$_status" in
181
+ 10) DF_TRACKED_AREA_MEMORY="untracked" ;;
182
+ 11) DF_TRACKED_AREA_MEMORY="tracked" ;;
183
+ esac
184
+ return 0
185
+ }
186
+
131
187
  PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
132
188
  [ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
133
189
  # One canonical root feeds every path below, the staged path in the prompt and in the
@@ -136,6 +192,7 @@ PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
136
192
  PROJECT_ROOT_CANONICAL=$(cd -P -- "$PROJECT_ROOT" 2>/dev/null && pwd -P) || PROJECT_ROOT_CANONICAL=""
137
193
  if [ -z "$PROJECT_ROOT_CANONICAL" ]; then
138
194
  log "SKIP: project root could not be resolved; no LLM run"
195
+ probe_memory_area "$PROJECT_ROOT"
139
196
  memory_notice_write "$PROJECT_ROOT/.devflow/memory" "$PROJECT_ROOT" path
140
197
  exit 0
141
198
  fi
@@ -154,13 +211,23 @@ OK_FILE="$MEMORY_DIR/.last-refresh-ok"
154
211
  # have been disabled since spawn, or narrowed by a checkout that now carries a
155
212
  # `features.memory: false`). Same helper as every other memory/curation gate
156
213
  # (D-FEATURES-NARROW-ONLY, see queue-append).
157
- source "$SCRIPT_DIR/queue-append" || { echo "background-memory-update: failed to source queue-append" >&2; exit 1; }
158
214
  queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
159
215
  if [ "$_QG_MEMORY" != "true" ]; then
160
216
  log "ABORT: memory disabled (disabled after spawn)"
161
217
  exit 0
162
218
  fi
163
219
 
220
+ # --- Ask whether git tracks files in the memory folder ---
221
+ # The area may have become tracked since the Stop hook spawned this worker. A paused area
222
+ # ends the run here, before the binary is resolved, a link is checked, the lock is taken or
223
+ # a batch is claimed: nothing is read, moved or written, and no notice is left. The run's
224
+ # `Run:` line says skip.
225
+ probe_memory_area "$PROJECT_ROOT"
226
+ if [ "$DF_TRACKED_AREA_MEMORY" != "untracked" ]; then
227
+ log "SKIP: the memory area is paused (git tracks files in the memory folder, or did not answer); nothing read or written; no LLM run"
228
+ exit 0
229
+ fi
230
+
164
231
  # --- Resolve claude binary ---
165
232
  CLAUDE_BIN=$(command -v claude 2>/dev/null || true)
166
233
  if [ -z "$CLAUDE_BIN" ]; then
@@ -1126,6 +1193,17 @@ case "$MEMORY_RESULT_DENIALS" in
1126
1193
  *) RESULT_DENIED="yes" ;;
1127
1194
  esac
1128
1195
 
1196
+ # The run took minutes, so the memory area is asked again before anything lands in the
1197
+ # folder. If git tracks files there now, or does not answer, nothing below runs: the staged
1198
+ # file is not checked, moved or removed, no stamp is touched, no denied run is counted and
1199
+ # no notice is left. The batch stays in .processing.
1200
+ probe_memory_area "$PROJECT_ROOT"
1201
+ if [ "$DF_TRACKED_AREA_MEMORY" != "untracked" ]; then
1202
+ RUN_RESULT="skip"
1203
+ log "SKIP: the memory area is paused (git tracks files in the memory folder, or did not answer); nothing swapped or stamped; the batch stays in .processing"
1204
+ exit 0
1205
+ fi
1206
+
1129
1207
  # A folder or a link at the staged path now was put there during the run: the path was
1130
1208
  # empty at the spawn. It is removed the way it is before a run (memory_staged_remove), and
1131
1209
  # the run ends as failed: the checks below find no staged file, and the batch stays for
@@ -45,9 +45,16 @@
45
45
  // once; prints none | busy | busy claim |
46
46
  // skipped tracked | skipped unknown | moved <mode>
47
47
  // and its backup, cause and notes
48
+ // tracked-area <curation|memory|both> <root>
49
+ // Does git track files in the data folders of the area
50
+ // at the root? Prints tracked | untracked | unknown, and
51
+ // for unknown a second line `cause <word>`
48
52
  //
49
- // Every curation op above except claim-queue, release-claim and move-ledger first
50
- // refreshes the mtime of an existing queue claim — the heartbeat (D-OWNED-CLAIM).
53
+ // Every curation op above except claim-queue, release-claim, move-ledger and tracked-area
54
+ // first refreshes the mtime of an existing queue claim — the heartbeat (D-OWNED-CLAIM).
55
+ // Where git tracks files in the curation data folders, or does not say whether it does,
56
+ // every curation op refuses with kind `tracked`, exit 1 and an empty stdout, and changes
57
+ // nothing; the answer is asked once per process.
51
58
 
52
59
  'use strict';
53
60
 
@@ -88,6 +95,21 @@ function curationMove() {
88
95
  return curationMoveModule;
89
96
  }
90
97
 
98
+ /** The tracked-areas check, once loaded; see trackedAreas(). */
99
+ let trackedAreasModule = null;
100
+
101
+ /**
102
+ * The tracked-areas check, loaded on first use and memoized. Only the tracked-area op
103
+ * loads it from here: a leaf that requires node built-ins and the path helpers, never
104
+ * the store.
105
+ *
106
+ * @returns {object}
107
+ */
108
+ function trackedAreas() {
109
+ if (trackedAreasModule === null) trackedAreasModule = require('./lib/tracked-areas.cjs');
110
+ return trackedAreasModule;
111
+ }
112
+
91
113
  /** The bytes each read of stdin asks for. */
92
114
  const STDIN_READ_BYTES = 64 * 1024;
93
115
 
@@ -624,6 +646,26 @@ try {
624
646
  break;
625
647
  }
626
648
 
649
+ // -------------------------------------------------------------------------
650
+ // tracked-area <curation|memory|both> <root>
651
+ // Does git track files in the data folders of the area at the root
652
+ // (inspectTrackedAreaOnce, tracked-areas.cjs)? Not a curation op: it sends no
653
+ // heartbeat and loads only the check. The root is an argument, so the shell gate
654
+ // names the root it decided on; it stays one argv word, never a shell word. The
655
+ // answer is asked once per root and area for the life of the process.
656
+ // stdout: tracked | untracked | unknown, and for unknown a second line
657
+ // `cause <word>` (one fixed word that says why git did not answer)
658
+ // -------------------------------------------------------------------------
659
+ case 'tracked-area': {
660
+ const { AREA_PATHSPECS, inspectTrackedAreaOnce } = trackedAreas();
661
+ if (args.length !== 2 || !Object.prototype.hasOwnProperty.call(AREA_PATHSPECS, args[0])) {
662
+ exitWithUsage('tracked-area <curation|memory|both> <root> (the root is an absolute path)');
663
+ }
664
+ const { answer, cause } = inspectTrackedAreaOnce(args[1], args[0]);
665
+ process.stdout.write(answer === 'unknown' ? `unknown\ncause ${cause}\n` : `${answer}\n`);
666
+ break;
667
+ }
668
+
627
669
  default:
628
670
  process.stderr.write(`json-helper: unknown operation "${op}"\n`);
629
671
  process.exit(1);
@@ -10,7 +10,8 @@
10
10
  // means a broken invariant or an I/O failure, never an expected outcome; a lock
11
11
  // held by withDecisionsLock is released on every path, the throw included.
12
12
  //
13
- // Loading: node built-ins and three sibling libs only. This module never requires
13
+ // Loading: node built-ins and four sibling libs only (the path helpers, the lock, the
14
+ // path guard and the tracked-areas check). This module never requires
14
15
  // decisions-format.cjs or render-decisions.cjs at load time: both require it, for
15
16
  // the status list and the one-line and inactive-note text they share with `list`.
16
17
  //
@@ -58,6 +59,7 @@ const {
58
59
  } = require('./project-paths.cjs');
59
60
  const { acquireMkdirLock, releaseLock } = require('./mkdir-lock.cjs');
60
61
  const { safePath } = require('./safe-path.cjs');
62
+ const { inspectTrackedAreaOnce } = require('./tracked-areas.cjs');
61
63
 
62
64
  // ---------------------------------------------------------------------------
63
65
  // Constants
@@ -628,6 +630,36 @@ function linkedFolder(opName, dir) {
628
630
  };
629
631
  }
630
632
 
633
+ /**
634
+ * The refusal of an op run where git tracks files in the curation data folders under
635
+ * `root` (the curation folder, the old ledger folder and the backup folders), or does
636
+ * not say whether it does, or null where git tracks none. A folder that git tracks is the
637
+ * repository's content, so the curation feature is paused there: the op reads and writes
638
+ * nothing. The messages state the fact and name no command; the root is shown so the
639
+ * folder can be found. The check asks git about the index, not the disk, so it holds
640
+ * whether or not the folder is on disk, and it makes no git call for a root with no `.git`
641
+ * entry. It comes first in every op, before a ledger file is read, a lock is taken or a git
642
+ * call of the op's own is made. The answer is asked once per root for the life of the
643
+ * process (inspectTrackedAreaOnce), so the heartbeat, the op and its lock share one call.
644
+ *
645
+ * The refusal carries `reason`, `tracked` where git tracks files and `unknown` where it
646
+ * did not answer, so a caller that picks a notice reads the field, never the message.
647
+ *
648
+ * @param {string} opName - operation name, for the message
649
+ * @param {string} root - project root
650
+ * @returns {{ ok: false, error: { kind: 'tracked', reason: 'tracked'|'unknown', message: string } } | null}
651
+ */
652
+ function trackedCurationArea(opName, root) {
653
+ const { answer } = inspectTrackedAreaOnce(root, 'curation');
654
+ if (answer === 'untracked') return null;
655
+ const reason = answer === 'tracked' ? 'tracked' : 'unknown';
656
+ const fact = reason === 'tracked' ? 'git tracks files' : 'git did not answer whether it tracks files';
657
+ return {
658
+ ok: false,
659
+ error: { kind: 'tracked', reason, message: `${opName}: ${fact} in the curation data folders under ${root}; nothing was written` },
660
+ };
661
+ }
662
+
631
663
  /** The Stats of `file` without following a link, or null when nothing is there. */
632
664
  function lstatOrNull(file) {
633
665
  try {
@@ -744,16 +776,23 @@ function isResult(value) {
744
776
  * names. Refused here, before the lock is taken, so every writer refuses in one
745
777
  * place; the claim heartbeat makes the same check, and read-only paths still read.
746
778
  *
779
+ * A writer also refuses with `tracked`, changing nothing, where git tracks files in the
780
+ * curation data folders under its root, or does not say whether it does
781
+ * (trackedCurationArea). That check follows the link check and comes before the journal,
782
+ * the folder check and the lock, so a paused project gets no lock folder.
783
+ *
747
784
  * @param {string} opName - operation name, for messages
748
785
  * @param {string} root - project root
749
786
  * @param {() => { ok: boolean }} fn - the locked body; it must return a Result
750
787
  * @param {{ timeoutMs?: number, staleMs?: number }} [opts]
751
788
  * @returns {{ ok: true, value?: unknown } | { ok: false, error: { kind: string, message: string } }}
752
- * fn's Result, or an error of kind `not-a-directory`, `no-curation-dir` or `busy`.
789
+ * fn's Result, or an error of kind `not-a-directory`, `tracked`, `no-curation-dir` or `busy`.
753
790
  */
754
791
  function withDecisionsLock(opName, root, fn, { timeoutMs = LOCK_ACQUIRE_TIMEOUT_MS, staleMs = LOCK_STALE_MS } = {}) {
755
792
  const linked = linkedCurationFolder(root);
756
793
  if (linked !== null) return linkedFolder(opName, linked);
794
+ const paused = trackedCurationArea(opName, root);
795
+ if (paused !== null) return paused;
757
796
  const journal = moveJournalPathOrNull(root);
758
797
  const oldFolder = path.join(root, '.devflow', LEGACY_NAMES.folder);
759
798
  if (journal !== null && moveJournalHoldsWriters(journal, oldFolder)) return unfinishedMove(opName, journal);
@@ -1344,7 +1383,7 @@ function lastActivityMs(row) {
1344
1383
  * @param {{ now?: number, timeoutMs?: number }} [opts] - now: epoch ms (default Date.now())
1345
1384
  * @returns {{ ok: true, value: { rotated: number, appended: number } } | { ok: false, error: { kind: string, message: string } }}
1346
1385
  * rotated: rows removed from the log; appended: rows added to the archive.
1347
- * Errors are withDecisionsLock's not-a-directory, no-curation-dir and busy.
1386
+ * Errors are withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
1348
1387
  */
1349
1388
  function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
1350
1389
  return withDecisionsLock('rotate-observations', root, () => {
@@ -1407,7 +1446,7 @@ function rotateObservations(root, { now = Date.now(), timeoutMs } = {}) {
1407
1446
  * @param {{ now?: number, timeoutMs?: number }} [opts] - now: epoch ms (default Date.now())
1408
1447
  * @returns {{ ok: true, value: { cleared: number, kept: number } } | { ok: false, error: { kind: string, message: string } }}
1409
1448
  * cleared: rows removed from the log; kept: rows left in it. Error kinds:
1410
- * ledger-malformed, and withDecisionsLock's not-a-directory, no-curation-dir and busy.
1449
+ * ledger-malformed, and withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
1411
1450
  */
1412
1451
  function clearUnreferenced(root, { now = Date.now(), timeoutMs } = {}) {
1413
1452
  return withDecisionsLock('clear', root, () => {
@@ -1475,7 +1514,7 @@ function removeEmptyDir(dir) {
1475
1514
  * @param {{ timeoutMs?: number }} [opts]
1476
1515
  * @returns {{ ok: true, value: { removed: number } } | { ok: false, error: { kind: string, message: string } }}
1477
1516
  * removed: the entries removed from the curation directory. Errors are
1478
- * withDecisionsLock's not-a-directory, no-curation-dir and busy.
1517
+ * withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
1479
1518
  */
1480
1519
  function resetCuration(root, { timeoutMs } = {}) {
1481
1520
  const curationDir = getCurationDir(root);
@@ -1643,7 +1682,8 @@ function moveQueueToClaim(queuePath, claimPath) {
1643
1682
  * clobber a batch, mv kept the queue's old mtime so a fresh claim could look stale
1644
1683
  * at once, and an unconditional final unlink deleted another run's claim.
1645
1684
  *
1646
- * Without .devflow/curation/ it answers none and creates nothing.
1685
+ * Without .devflow/curation/ it answers none and creates nothing; where git tracks files
1686
+ * in the curation data folders it refuses, kind `tracked`, before it looks (trackedCurationArea).
1647
1687
  *
1648
1688
  * @param {string} root - project root
1649
1689
  * @param {{ now?: number, token?: string, timeoutMs?: number }} [opts]
@@ -1654,6 +1694,8 @@ function moveQueueToClaim(queuePath, claimPath) {
1654
1694
  */
1655
1695
  function claimQueue(root, { now = Date.now(), token = newClaimToken(), timeoutMs } = {}) {
1656
1696
  assertClaimToken('claimQueue', token);
1697
+ const paused = trackedCurationArea('claim-queue', root);
1698
+ if (paused !== null) return paused;
1657
1699
  if (!hasCurationDir(root)) return { ok: true, value: { state: 'none' } };
1658
1700
  return withDecisionsLock('claim-queue', root, () => {
1659
1701
  const claimPath = getCurationPendingTurnsProcessingPath(root);
@@ -1693,7 +1735,7 @@ function claimQueue(root, { now = Date.now(), token = newClaimToken(), timeoutMs
1693
1735
  * @param {string} token
1694
1736
  * @param {{ timeoutMs?: number }} [opts]
1695
1737
  * @returns {{ ok: true, value: { state: 'released'|'not-owner'|'gone' } } | { ok: false, error: { kind: string, message: string } }}
1696
- * errors include withDecisionsLock's not-a-directory, no-curation-dir and busy
1738
+ * errors include withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy
1697
1739
  * @throws {TypeError} when `token` is not a claim token
1698
1740
  */
1699
1741
  function releaseClaim(root, token, { timeoutMs } = {}) {
@@ -1722,14 +1764,17 @@ function releaseClaim(root, token, { timeoutMs } = {}) {
1722
1764
  * takes no lock — json-helper sends it before each curation op runs. A claim in a
1723
1765
  * curation tree reached through a symbolic link is left alone (D-NO-LINKED-TREE):
1724
1766
  * `lutimes` follows a linked folder above the claim, and the op that follows
1725
- * refuses that tree anyway.
1767
+ * refuses that tree anyway. A claim in a project whose curation data folders git
1768
+ * tracks, or where git does not answer, is left alone as well (trackedCurationArea).
1726
1769
  *
1727
1770
  * @param {string} root - project root
1728
1771
  * @param {{ now?: number }} [opts] - now: epoch ms (default Date.now())
1729
1772
  * @returns {{ ok: true, value: { touched: boolean } } | { ok: false, error: { kind: 'heartbeat-failed', message: string } }}
1730
1773
  */
1731
1774
  function touchClaim(root, { now = Date.now() } = {}) {
1732
- if (linkedCurationFolder(root) !== null) return { ok: true, value: { touched: false } };
1775
+ if (linkedCurationFolder(root) !== null || trackedCurationArea('heartbeat', root) !== null) {
1776
+ return { ok: true, value: { touched: false } };
1777
+ }
1733
1778
  const claimPath = getCurationPendingTurnsProcessingPath(root);
1734
1779
  const at = new Date(now);
1735
1780
  try {
@@ -2231,13 +2276,15 @@ function restoreFirst(id, carriers) {
2231
2276
  * observations: the count the log row holds afterwards; reprojected: the
2232
2277
  * anchors re-projected, in anchor order. Error kinds: invalid-input (with
2233
2278
  * problems), duplicate-log-id, restore-first, cannot-reproject, and
2234
- * withDecisionsLock's not-a-directory, no-curation-dir and busy.
2279
+ * withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
2235
2280
  * @throws {TypeError} when `mode` is not a put mode
2236
2281
  */
2237
2282
  function putObservation(root, mode, input, { now = Date.now(), timeoutMs, scopeMatches } = {}) {
2238
2283
  if (!VALIDATION_MODES.includes(mode)) {
2239
2284
  throw new TypeError(`putObservation: mode must be one of ${VALIDATION_MODES.join(', ')}, got '${mode}'`);
2240
2285
  }
2286
+ const paused = trackedCurationArea('put-observation', root);
2287
+ if (paused !== null) return paused;
2241
2288
  const matches = scopeMatches || gitScopeMatcher(root);
2242
2289
  return withDecisionsLock('put-observation', root, () => putUnderLock(root, mode, input, { now, scopeMatches: matches }), { timeoutMs });
2243
2290
  }
@@ -2303,14 +2350,17 @@ function putUnderLock(root, mode, input, { now, scopeMatches }) {
2303
2350
  * The data behind `list`, read-only: buildListing over the ledger and the log as
2304
2351
  * they are, every glob scope checked against the files git tracks. Malformed
2305
2352
  * lines are counted, never quarantined (D-QUARANTINE-MALFORMED). Refuses without
2306
- * .devflow/curation/ (D-NO-STRAY-TREE).
2353
+ * .devflow/curation/ (D-NO-STRAY-TREE), and, before it reads a file, where git tracks files
2354
+ * in the curation data folders (trackedCurationArea).
2307
2355
  *
2308
2356
  * @param {string} root - project root
2309
2357
  * @param {{ scopeMatches?: (glob: string) => boolean }} [opts] - default gitScopeMatcher(root)
2310
2358
  * @returns {{ ok: true, value: { active: object[], inactive: object[], observations: object[], integrity: object[], malformed: { ledger: number, log: number } } }
2311
- * | { ok: false, error: { kind: 'no-curation-dir', message: string } }}
2359
+ * | { ok: false, error: { kind: 'tracked'|'no-curation-dir', message: string } }}
2312
2360
  */
2313
2361
  function readListing(root, { scopeMatches } = {}) {
2362
+ const paused = trackedCurationArea('list', root);
2363
+ if (paused !== null) return paused;
2314
2364
  if (!hasCurationDir(root)) return noCurationDir('list', root);
2315
2365
  const { ledgerRows, logRows, rejected } = readCurationState(root);
2316
2366
  return {
@@ -2393,14 +2443,17 @@ function formatListing(listing) {
2393
2443
  * the log and the history as they are. When lines were skipped as malformed
2394
2444
  * (D-QUARANTINE-MALFORMED), a found entry gains `malformed: { ledger, log }` and
2395
2445
  * a not-found message says a skipped line may hold it. Refuses without
2396
- * .devflow/curation/ (D-NO-STRAY-TREE).
2446
+ * .devflow/curation/ (D-NO-STRAY-TREE), and, before it reads a file, where git tracks files
2447
+ * in the curation data folders (trackedCurationArea).
2397
2448
  *
2398
2449
  * @param {string} root - project root
2399
2450
  * @param {string} key - an anchor id or an observation id
2400
2451
  * @returns {{ ok: true, value: { key: string, ledger: object[], log: object|null, history_versions: object[], flags: object[], malformed?: { ledger: number, log: number } } }
2401
- * | { ok: false, error: { kind: 'no-curation-dir'|'invalid-key'|'not-found', message: string } }}
2452
+ * | { ok: false, error: { kind: 'tracked'|'no-curation-dir'|'invalid-key'|'not-found', message: string } }}
2402
2453
  */
2403
2454
  function showByKey(root, key) {
2455
+ const paused = trackedCurationArea('show', root);
2456
+ if (paused !== null) return paused;
2404
2457
  if (!hasCurationDir(root)) return noCurationDir('show', root);
2405
2458
  const { ledgerRows, logRows, rejected } = readCurationState(root);
2406
2459
  const shown = showEntry(key, ledgerRows, logRows, { historyVersions: id => historyVersions(root, id) });
@@ -2448,10 +2501,12 @@ function warmScopeMatcher(ledgerRows, scopeMatches) {
2448
2501
  * now: epoch ms (default Date.now()); scopeMatches: default gitScopeMatcher(root)
2449
2502
  * @returns {{ ok: true, value: { ref: { ref: 'origin/HEAD'|'HEAD', commit: string } | null, due: Array<{ anchor_id: string, reason: string, bytes: number }> } }
2450
2503
  * | { ok: false, error: { kind: string, message: string } }}
2451
- * due is selectDue's answer; errors are withDecisionsLock's not-a-directory,
2504
+ * due is selectDue's answer; errors are withDecisionsLock's tracked, not-a-directory,
2452
2505
  * no-curation-dir and busy
2453
2506
  */
2454
2507
  function claimDue(root, { now = Date.now(), timeoutMs, scopeMatches } = {}) {
2508
+ const paused = trackedCurationArea('claim-due', root);
2509
+ if (paused !== null) return paused;
2455
2510
  if (!hasCurationDir(root)) return noCurationDir('claim-due', root);
2456
2511
  const matches = scopeMatches || gitScopeMatcher(root);
2457
2512
  const ref = resolveVerifyRef(root);
@@ -2725,12 +2780,14 @@ function mintAnchor(ledgerRows, type, citedAnchors) {
2725
2780
  * | { ok: false, error: { kind: string, message: string } }}
2726
2781
  * Error kinds: not-in-log, duplicate-log-id, already-promoted, v1-observation,
2727
2782
  * type-mismatch, cited-numbers-exhausted, and withDecisionsLock's
2728
- * not-a-directory, no-curation-dir and busy.
2783
+ * tracked, not-a-directory, no-curation-dir and busy.
2729
2784
  * @throws {TypeError} for a type other than decision or pitfall, or a malformed obsId
2730
2785
  */
2731
2786
  function assignAnchor(root, type, obsId, { now = Date.now(), timeoutMs, citedAnchors } = {}) {
2732
2787
  if (anchorPrefixFor(type) === null) throw new TypeError(`assignAnchor: type must be 'decision' or 'pitfall', got '${type}'`);
2733
2788
  if (typeof obsId !== 'string' || !OBS_ID_RE.test(obsId)) throw new TypeError('assignAnchor: obsId must be an observation id');
2789
+ const paused = trackedCurationArea('assign-anchor', root);
2790
+ if (paused !== null) return paused;
2734
2791
  if (!hasCurationDir(root)) return noCurationDir('assign-anchor', root);
2735
2792
  const cited = citedAnchors || collectCitedAnchorIds(root);
2736
2793
  return withDecisionsLock('assign-anchor', root, () => assignUnderLock(root, type, obsId, { now, cited }), { timeoutMs });
@@ -2884,7 +2941,7 @@ function recordRefreshHistory(root, plans, ledgerRows, { now }) {
2884
2941
  * @returns {{ ok: true, value: { refreshed: Array<{ anchor_id: string, state: 'verified'|'reprojected'|'unchanged' }> } }
2885
2942
  * | { ok: false, error: { kind: string, message: string, problems?: Array<{ anchor_id: string, message: string }> } }}
2886
2943
  * refreshed: each anchor once, in the order given. Error kinds: refused (with
2887
- * problems), and withDecisionsLock's not-a-directory, no-curation-dir and busy.
2944
+ * problems), and withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
2888
2945
  * @throws {TypeError} when anchorIds is empty or holds anything but anchor ids
2889
2946
  */
2890
2947
  function refreshAnchors(root, anchorIds, { verified = false, now = Date.now(), timeoutMs } = {}) {
@@ -3067,9 +3124,11 @@ function restoreRefusal(kind, message) {
3067
3124
  * @param {{ verifyRef?: { ref: 'origin/HEAD'|'HEAD', commit: string } | null }} [opts]
3068
3125
  * verifyRef: default resolveVerifyRef(root); null when there is no commit to check
3069
3126
  * @returns {{ ok: true, value: { path: string, quote: string, ref: string, commit: string } }
3070
- * | { ok: false, error: { kind: 'no-verify-ref'|'not-at-ref'|'git-failed'|'quote-not-found', message: string } }}
3127
+ * | { ok: false, error: { kind: 'tracked'|'no-verify-ref'|'not-at-ref'|'git-failed'|'quote-not-found', message: string } }}
3071
3128
  */
3072
3129
  function quoteAtRef(root, at, quote, { verifyRef } = {}) {
3130
+ const paused = trackedCurationArea('retire-anchor', root);
3131
+ if (paused !== null) return paused;
3073
3132
  const checkedAt = verifyRef === undefined ? resolveVerifyRef(root) : verifyRef;
3074
3133
  if (checkedAt === null) {
3075
3134
  return retireRefusal('no-verify-ref', 'there is no commit to check the quote at (not a git repository, or no commit yet); nothing was written');
@@ -3121,7 +3180,7 @@ function quoteAtRef(root, at, quote, { verifyRef } = {}) {
3121
3180
  * kinds: invalid-input (with problems), quoteAtRef's, not-found,
3122
3181
  * duplicate-anchor, already-inactive, successor-not-found,
3123
3182
  * successor-duplicate-anchor, successor-inactive, and withDecisionsLock's
3124
- * not-a-directory, no-curation-dir and busy.
3183
+ * tracked, not-a-directory, no-curation-dir and busy.
3125
3184
  * @throws {TypeError} for a malformed anchorId or a status that is not inactive
3126
3185
  */
3127
3186
  function retireAnchor(root, anchorId, status, input, { now = Date.now(), timeoutMs, verifyRef } = {}) {
@@ -3129,6 +3188,8 @@ function retireAnchor(root, anchorId, status, input, { now = Date.now(), timeout
3129
3188
  if (!INACTIVE_STATUSES.includes(status)) {
3130
3189
  throw new TypeError(`retireAnchor: status must be one of ${INACTIVE_STATUSES.join(', ')}, got '${status}'`);
3131
3190
  }
3191
+ const paused = trackedCurationArea('retire-anchor', root);
3192
+ if (paused !== null) return paused;
3132
3193
  if (!hasCurationDir(root)) return noCurationDir('retire-anchor', root);
3133
3194
  const problems = retireInputProblems(anchorId, status, input);
3134
3195
  if (problems.length > 0) return invalidInput('retire-anchor', problems);
@@ -3200,7 +3261,7 @@ function retireUnderLock(root, anchorId, status, note, { now }) {
3200
3261
  * @returns {{ ok: true, value: { anchor_id: string, status: 'Accepted'|'Active' } }
3201
3262
  * | { ok: false, error: { kind: string, message: string } }}
3202
3263
  * Error kinds: not-found, duplicate-anchor, already-active, type-mismatch, and
3203
- * withDecisionsLock's not-a-directory, no-curation-dir and busy.
3264
+ * withDecisionsLock's tracked, not-a-directory, no-curation-dir and busy.
3204
3265
  * @throws {TypeError} for a malformed anchorId
3205
3266
  */
3206
3267
  function restoreAnchor(root, anchorId, { now = Date.now(), timeoutMs } = {}) {
@@ -3268,6 +3329,7 @@ module.exports = {
3268
3329
  writeJsonlAtomic,
3269
3330
  // Locking
3270
3331
  hasCurationDir,
3332
+ trackedCurationArea,
3271
3333
  withDecisionsLock,
3272
3334
  // State and the ledger registry
3273
3335
  readCurationState,
@@ -51,6 +51,7 @@ const {
51
51
  isV2,
52
52
  readJsonl,
53
53
  hasCurationDir,
54
+ trackedCurationArea,
54
55
  withDecisionsLock,
55
56
  writeFileAtomic,
56
57
  } = require('./curation-store.cjs');
@@ -397,12 +398,19 @@ function renderCommand(root) {
397
398
  * `--check`: render in memory and compare with the files on disk, naming each
398
399
  * file that differs or is missing; exit 1 on any drift. It writes nothing and
399
400
  * takes no lock — taking it would create the lock directory — and refuses
400
- * without the curation directory (D-NO-STRAY-TREE).
401
+ * without the curation directory (D-NO-STRAY-TREE). Where git tracks files in the
402
+ * curation data folders, or does not say whether it does, it refuses first, exit 1:
403
+ * it reads no file there (trackedCurationArea).
401
404
  *
402
405
  * @param {string} root
403
406
  * @returns {number} exit code
404
407
  */
405
408
  function checkCommand(root) {
409
+ const paused = trackedCurationArea(CLI_NAME, root);
410
+ if (paused !== null) {
411
+ process.stderr.write(`${paused.error.message}\n`);
412
+ return 1;
413
+ }
406
414
  if (!hasCurationDir(root)) {
407
415
  process.stderr.write(`${CLI_NAME}: no .devflow/curation/ under ${root} — run from the project root\n`);
408
416
  return 1;