devflow-kit 2.4.0 → 3.0.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 (213) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/README.md +111 -18
  3. package/dist/agents/git.md +822 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/ambient.js +160 -145
  6. package/dist/cli/commands/attribution-prompts.js +1 -1
  7. package/dist/cli/commands/capture.js +29 -55
  8. package/dist/cli/commands/compliance-prompts.js +1 -1
  9. package/dist/cli/commands/compliance.js +48 -55
  10. package/dist/cli/commands/context.js +17 -32
  11. package/dist/cli/commands/debug.js +65 -26
  12. package/dist/cli/commands/flags.js +3 -3
  13. package/dist/cli/commands/hud.js +34 -10
  14. package/dist/cli/commands/init-seed.js +61 -27
  15. package/dist/cli/commands/init.js +649 -240
  16. package/dist/cli/commands/install-report.js +200 -0
  17. package/dist/cli/commands/knowledge/index.js +2 -2
  18. package/dist/cli/commands/knowledge/toggle.js +35 -37
  19. package/dist/cli/commands/learning.js +79 -57
  20. package/dist/cli/commands/legacy-hooks.js +11 -14
  21. package/dist/cli/commands/memory.js +134 -135
  22. package/dist/cli/commands/prompt-io.js +4 -4
  23. package/dist/cli/commands/proxy.js +23 -41
  24. package/dist/cli/commands/security.js +81 -29
  25. package/dist/cli/commands/skills.js +71 -7
  26. package/dist/cli/commands/tracker-prompts.js +145 -0
  27. package/dist/cli/commands/tracker.js +277 -0
  28. package/dist/cli/commands/uninstall.js +520 -169
  29. package/dist/cli.js +2 -0
  30. package/dist/commands/bug-analysis.md +58 -14
  31. package/dist/commands/code-review.md +110 -32
  32. package/dist/commands/debug.md +55 -11
  33. package/dist/commands/dynamic-build.md +344 -73
  34. package/dist/commands/dynamic-plan.md +77 -27
  35. package/dist/commands/dynamic-profile.md +25 -11
  36. package/dist/commands/dynamic-tickets.md +76 -15
  37. package/dist/commands/explore.md +37 -7
  38. package/dist/commands/implement.md +314 -62
  39. package/dist/commands/plan.md +146 -32
  40. package/dist/commands/release.md +64 -17
  41. package/dist/commands/research.md +34 -8
  42. package/dist/commands/resolve.md +196 -68
  43. package/dist/commands/self-review.md +45 -9
  44. package/dist/core/agent-models.js +55 -12
  45. package/dist/core/assets.js +58 -2
  46. package/dist/core/compliance-compose.js +27 -27
  47. package/dist/core/evidence-policy.js +363 -0
  48. package/dist/core/feature-config.js +200 -65
  49. package/dist/core/feature-switch.js +112 -0
  50. package/dist/core/flags.js +34 -6
  51. package/dist/core/fs-atomic.js +27 -0
  52. package/dist/core/hook-log-dirs.js +104 -0
  53. package/dist/core/learning-tuning-config.js +5 -3
  54. package/dist/core/ledger-root.js +102 -0
  55. package/dist/core/manifest.js +38 -10
  56. package/dist/core/mds-variants.js +798 -0
  57. package/dist/core/migrations.js +49 -23
  58. package/dist/core/model-discovery.js +12 -1
  59. package/dist/core/plugins.js +361 -12
  60. package/dist/core/project-paths.js +1 -18
  61. package/dist/core/proxy-log.js +8 -6
  62. package/dist/core/proxy-state.js +11 -8
  63. package/dist/core/reference-sweep.js +136 -0
  64. package/dist/core/same-location.js +25 -0
  65. package/dist/core/tracker.js +494 -0
  66. package/dist/hud/components/config-counts.js +15 -4
  67. package/dist/hud/components/learning-counts.js +14 -0
  68. package/dist/hud/config.js +2 -1
  69. package/dist/hud/cost-history.js +2 -4
  70. package/dist/hud/git.js +52 -7
  71. package/dist/hud/index.js +7 -9
  72. package/dist/skills/git/references/decision-markers.md +19 -0
  73. package/dist/skills/git/references/learn-conventions.md +56 -0
  74. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  75. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  76. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  77. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  78. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  79. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  80. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  81. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  82. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  83. package/dist/skills/git/references/publication-gate.md +13 -0
  84. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  85. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  87. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  89. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  90. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  91. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  92. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  93. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  94. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  95. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  96. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  97. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  98. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  99. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  100. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  101. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  102. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  103. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  104. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  105. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  106. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  107. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  108. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  109. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  110. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  111. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  112. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  113. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  114. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  115. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  116. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  117. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  118. package/dist/skills/git/references/trust-rule.md +7 -0
  119. package/dist/targets/claude-code/claude-paths.js +59 -57
  120. package/dist/targets/claude-code/compliance-install.js +49 -65
  121. package/dist/targets/claude-code/hooks.js +108 -3
  122. package/dist/targets/claude-code/installer.js +1187 -32
  123. package/dist/targets/claude-code/legacy.js +5 -0
  124. package/dist/targets/claude-code/post-install.js +366 -151
  125. package/dist/targets/claude-code/tracker-install.js +134 -0
  126. package/package.json +8 -6
  127. package/src/assets/agents/code.md +45 -6
  128. package/src/assets/agents/design.md +2 -1
  129. package/src/assets/agents/git.mds +825 -0
  130. package/src/assets/agents/knowledge.md +3 -3
  131. package/src/assets/agents/learning.md +11 -0
  132. package/src/assets/agents/review.md +3 -1
  133. package/src/assets/agents/synthesize.md +1 -1
  134. package/src/assets/agents/test.md +16 -5
  135. package/src/assets/agents/tracker.md +474 -0
  136. package/src/assets/agents/validate.md +7 -5
  137. package/src/assets/commands/_partials/_compliance.mds +19 -1
  138. package/src/assets/commands/_partials/_decisions.mds +15 -3
  139. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  140. package/src/assets/commands/_partials/_engine.mds +13 -11
  141. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  142. package/src/assets/commands/_partials/_factory.mds +1 -1
  143. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  144. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  145. package/src/assets/commands/_partials/_preamble.mds +2 -2
  146. package/src/assets/commands/_partials/_publication.mds +8 -2
  147. package/src/assets/commands/_partials/_settings.mds +28 -0
  148. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  149. package/src/assets/commands/_partials/_tracker.mds +18 -0
  150. package/src/assets/commands/_partials/_wave.mds +16 -10
  151. package/src/assets/commands/bug-analysis.mds +31 -19
  152. package/src/assets/commands/code-review.mds +67 -41
  153. package/src/assets/commands/debug.mds +13 -7
  154. package/src/assets/commands/dynamic-build.mds +274 -66
  155. package/src/assets/commands/dynamic-plan.mds +50 -23
  156. package/src/assets/commands/dynamic-profile.mds +24 -11
  157. package/src/assets/commands/dynamic-tickets.mds +63 -16
  158. package/src/assets/commands/explore.mds +4 -5
  159. package/src/assets/commands/implement.mds +234 -67
  160. package/src/assets/commands/plan.mds +91 -33
  161. package/src/assets/commands/release.md +64 -17
  162. package/src/assets/commands/research.mds +11 -9
  163. package/src/assets/commands/resolve.mds +150 -78
  164. package/src/assets/commands/self-review.mds +24 -25
  165. package/src/assets/mds/git/_pr.mds +331 -0
  166. package/src/assets/mds/git/_references.mds +135 -0
  167. package/src/assets/mds/tracker/_common.mds +156 -0
  168. package/src/assets/mds/tracker/_github.mds +472 -0
  169. package/src/assets/mds/tracker/_jira.mds +407 -0
  170. package/src/assets/mds/tracker/_linear.mds +449 -0
  171. package/src/assets/mds/tracker/_mcp.mds +305 -0
  172. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  173. package/src/assets/scripts/hooks/background-memory-update +40 -19
  174. package/src/assets/scripts/hooks/capture-prompt +18 -8
  175. package/src/assets/scripts/hooks/capture-question +18 -8
  176. package/src/assets/scripts/hooks/capture-turn +27 -13
  177. package/src/assets/scripts/hooks/debug-trace +11 -6
  178. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  179. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  180. package/src/assets/scripts/hooks/ensure-root-gitignore +236 -60
  181. package/src/assets/scripts/hooks/git-marker +48 -0
  182. package/src/assets/scripts/hooks/hook-log-init +3 -1
  183. package/src/assets/scripts/hooks/json-helper.cjs +228 -5
  184. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
  185. package/src/assets/scripts/hooks/log-paths +80 -0
  186. package/src/assets/scripts/hooks/memory-worker +22 -13
  187. package/src/assets/scripts/hooks/pre-compact-memory +44 -15
  188. package/src/assets/scripts/hooks/preamble +1 -4
  189. package/src/assets/scripts/hooks/queue-append +146 -28
  190. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  191. package/src/assets/scripts/hooks/session-start-context +534 -20
  192. package/src/assets/scripts/hooks/session-start-memory +38 -15
  193. package/src/assets/scripts/lib/project-config.cjs +633 -0
  194. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  195. package/src/assets/scripts/redact-secrets.cjs +490 -62
  196. package/src/assets/scripts/release-trace.cjs +1143 -0
  197. package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
  198. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  199. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  200. package/src/assets/skills/compliance/SKILL.md +4 -2
  201. package/src/assets/skills/docs-framework/SKILL.md +11 -10
  202. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  203. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  204. package/src/assets/skills/git/SKILL.md +8 -78
  205. package/src/assets/skills/git/references/github-api.md +179 -141
  206. package/src/assets/skills/git/references/patterns.md +11 -6
  207. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  208. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  209. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  210. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  211. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  212. package/src/targets/claude-code/templates/managed-settings.json +25 -9
  213. package/src/assets/agents/git.md +0 -938
@@ -24,7 +24,12 @@
24
24
  // session-output <context> Build SessionStart output envelope
25
25
  // prompt-output <context> Build UserPromptSubmit output envelope
26
26
  // backup-construct Build pre-compact backup JSON from --arg pairs
27
- // assign-anchor <type> <obs_id> Claim next ADR/PF number, render both .md files
27
+ // assign-anchor <type> <obs_id> [--allow-collision]
28
+ // Claim next ADR/PF number, render both .md files.
29
+ // Refuses on a pre-mint citation collision (E4) unless
30
+ // --allow-collision is passed.
31
+ // next-anchor <type> Read-only: print the next candidate ADR/PF id and
32
+ // any pre-mint collision hits; mutates nothing (E4)
28
33
  // retire-anchor <anchor_id> <status> Flip ledger row status, re-render both .md files
29
34
  // refresh-anchor <anchor_id> Re-project log obs onto ledger row, re-render
30
35
  // rotate-observations [<log>] [<arch>] Archive observing rows older than 30 days
@@ -33,6 +38,7 @@
33
38
 
34
39
  const fs = require('fs');
35
40
  const path = require('path');
41
+ const { execFileSync } = require('child_process');
36
42
 
37
43
  const op = process.argv[2];
38
44
  const args = process.argv.slice(3);
@@ -156,6 +162,154 @@ function nextAnchorFromLedger(ledgerRows, type) {
156
162
  return { anchorId: `${prefix}-${nextN}`, nextN };
157
163
  }
158
164
 
165
+ // ---------------------------------------------------------------------------
166
+ // Pre-mint collision guard (E4).
167
+ //
168
+ // A design doc can cite a design-local number ("PF-017") in tracked source
169
+ // before the ledger ever mints that same number for an unrelated entry — the
170
+ // two silently collide and nothing catches it until a human notices the text
171
+ // doesn't match. This scans the project tree for a whole-word citation of the
172
+ // candidate id BEFORE assign-anchor writes it, and refuses to mint over a hit.
173
+ // The consumer-side counterpart lives in mdl's scripts/verify-ledger-citations.mjs.
174
+ // ---------------------------------------------------------------------------
175
+
176
+ /** Directory names excluded from collision scanning at any depth (E4). */
177
+ const COLLISION_SCAN_EXCLUDED_SEGMENTS = new Set(['.git', 'node_modules', 'target', 'dist']);
178
+
179
+ /** Files larger than this are skipped during collision scanning — bounds the scan (E4). */
180
+ const COLLISION_SCAN_MAX_FILE_BYTES = 5 * 1024 * 1024;
181
+
182
+ /**
183
+ * True when a project-relative path must be excluded from collision scanning:
184
+ * the ledger's own files (`.devflow/learning/**`, self-citation is expected,
185
+ * not a collision) or any of the excluded directory segments.
186
+ *
187
+ * @param {string} relPath - path relative to the project root, either separator style
188
+ * @returns {boolean}
189
+ */
190
+ function isCollisionScanExcluded(relPath) {
191
+ const norm = relPath.split(path.sep).join('/');
192
+ if (norm === '.devflow/learning' || norm.startsWith('.devflow/learning/')) return true;
193
+ return norm.split('/').some(seg => COLLISION_SCAN_EXCLUDED_SEGMENTS.has(seg));
194
+ }
195
+
196
+ /**
197
+ * List tracked files via `git ls-files` (respects .gitignore; args passed as an
198
+ * array — never shelled through a string-built command). Throws when the
199
+ * project root is not a git working tree or the `git` binary is unavailable;
200
+ * callers fall back to `listFsWalkFiles`.
201
+ *
202
+ * D-NO-FSMONITOR: `ls-files` reads the index, and reading the index runs the
203
+ * command a repository's config names in `core.fsmonitor` — code chosen by the
204
+ * repository this hook runs inside. The call turns it off for itself
205
+ * (`-c core.fsmonitor=false`), so the listing stays a pure read.
206
+ *
207
+ * @param {string} projectRoot
208
+ * @returns {string[]} project-relative paths
209
+ */
210
+ function listGitTrackedFiles(projectRoot) {
211
+ const out = execFileSync('git', ['-c', 'core.fsmonitor=false', 'ls-files', '-z'], {
212
+ cwd: projectRoot,
213
+ stdio: ['ignore', 'pipe', 'ignore'],
214
+ });
215
+ return out.toString('utf8').split('\0').filter(Boolean);
216
+ }
217
+
218
+ /**
219
+ * Bounded, non-recursing-into-excluded-dirs fs walk — fallback for a project
220
+ * root that is not a git working tree. The walk is bounded by construction:
221
+ * it only descends into directories actually present on disk, and never
222
+ * descends into an excluded directory at all (E4).
223
+ *
224
+ * @param {string} projectRoot
225
+ * @returns {string[]} project-relative paths
226
+ */
227
+ function listFsWalkFiles(projectRoot) {
228
+ const results = [];
229
+ const stack = [''];
230
+ while (stack.length > 0) {
231
+ const relDir = stack.pop();
232
+ const absDir = relDir ? path.join(projectRoot, relDir) : projectRoot;
233
+ let entries;
234
+ try {
235
+ entries = fs.readdirSync(absDir, { withFileTypes: true });
236
+ } catch {
237
+ continue; // unreadable dir — best-effort scan, skip
238
+ }
239
+ for (const entry of entries) {
240
+ const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
241
+ if (isCollisionScanExcluded(relPath)) continue;
242
+ if (entry.isDirectory()) {
243
+ stack.push(relPath);
244
+ } else if (entry.isFile()) {
245
+ results.push(relPath);
246
+ }
247
+ }
248
+ }
249
+ return results;
250
+ }
251
+
252
+ /**
253
+ * Scan the project tree for a whole-word citation of `id` (e.g. `ADR-042`),
254
+ * excluding the ledger's own files and common vendored/build directories.
255
+ * Prefers tracked files (`git ls-files`) when the project root is a git
256
+ * working tree; falls back to a bounded fs walk otherwise. Best-effort:
257
+ * unreadable, binary, or oversized files are skipped rather than failing
258
+ * the scan.
259
+ *
260
+ * @param {string} projectRoot
261
+ * @param {string} id - e.g. 'ADR-042' or 'PF-017'
262
+ * @returns {{ file: string, line: number }[]} hits, empty when no collision
263
+ */
264
+ function scanForAnchorCollision(projectRoot, id) {
265
+ let files;
266
+ try {
267
+ files = listGitTrackedFiles(projectRoot);
268
+ } catch {
269
+ files = listFsWalkFiles(projectRoot);
270
+ }
271
+
272
+ const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
273
+ const pattern = new RegExp(`\\b${escaped}\\b`);
274
+ const hits = [];
275
+ for (const relPath of files) {
276
+ if (isCollisionScanExcluded(relPath)) continue;
277
+ const absPath = path.join(projectRoot, relPath);
278
+ let stat;
279
+ try {
280
+ stat = fs.statSync(absPath);
281
+ } catch {
282
+ continue; // race: listed then removed — best-effort scan, skip
283
+ }
284
+ if (!stat.isFile() || stat.size > COLLISION_SCAN_MAX_FILE_BYTES) continue;
285
+ let content;
286
+ try {
287
+ content = fs.readFileSync(absPath, 'utf8');
288
+ } catch {
289
+ continue; // unreadable or invalid utf8 — best-effort scan, skip
290
+ }
291
+ if (content.includes('\u0000')) continue; // binary heuristic
292
+ const lines = content.split('\n');
293
+ for (let i = 0; i < lines.length; i++) {
294
+ if (pattern.test(lines[i])) {
295
+ hits.push({ file: relPath, line: i + 1 });
296
+ }
297
+ }
298
+ }
299
+ return hits;
300
+ }
301
+
302
+ /**
303
+ * Format collision hits for a stderr/stdout report — one `file:line` per line,
304
+ * two-space indented (E4).
305
+ *
306
+ * @param {{ file: string, line: number }[]} hits
307
+ * @returns {string}
308
+ */
309
+ function formatCollisionHits(hits) {
310
+ return hits.map(h => ` ${h.file}:${h.line}`).join('\n');
311
+ }
312
+
159
313
  /**
160
314
  * Read .decisions-usage.json. Returns {version, entries} or empty default.
161
315
  * @param {string} projectRoot - Path to project root (cwd)
@@ -533,20 +687,36 @@ try {
533
687
  }
534
688
 
535
689
  // -------------------------------------------------------------------------
536
- // assign-anchor <type> <obs_id>
690
+ // assign-anchor <type> <obs_id> [--allow-collision]
537
691
  // AC-A2: Assign next anchor ID for the given type (decision|pitfall) to the
538
692
  // observation identified by obs_id in decisions-log.jsonl. Atomic under a
539
693
  // single .decisions.lock acquisition. Registers usage, re-renders both .md.
540
694
  //
695
+ // E4: before writing, refuses if the candidate id is already cited as a
696
+ // whole word somewhere in tracked source (a pre-mint collision — see
697
+ // scanForAnchorCollision above). --allow-collision skips the scan and
698
+ // mints anyway, for the human-ruled case where the citation should be
699
+ // superseded by the ledger's number.
700
+ //
541
701
  // Locking discipline: holds ONLY .decisions.lock (never .observations.lock).
542
702
  // O(anchored) — single pass for max numeric suffix (AC-P2).
543
703
  // -------------------------------------------------------------------------
544
704
  case 'assign-anchor': {
545
- const assignType = args[0]; // 'decision' or 'pitfall'
546
- const assignObsId = args[1];
705
+ const aaKnownFlags = new Set(['--allow-collision']);
706
+ const aaFlags = args.filter(a => a.startsWith('--'));
707
+ const aaUnknownFlags = aaFlags.filter(f => !aaKnownFlags.has(f));
708
+ if (aaUnknownFlags.length > 0) {
709
+ process.stderr.write(`assign-anchor: unknown flag(s): ${aaUnknownFlags.join(', ')}\n`);
710
+ process.exit(1);
711
+ }
712
+ const aaAllowCollision = aaFlags.includes('--allow-collision');
713
+ const aaPositional = args.filter(a => !a.startsWith('--'));
714
+
715
+ const assignType = aaPositional[0]; // 'decision' or 'pitfall'
716
+ const assignObsId = aaPositional[1];
547
717
 
548
718
  if (!assignType || !assignObsId) {
549
- process.stderr.write('assign-anchor: usage: assign-anchor <type> <obs_id>\n');
719
+ process.stderr.write('assign-anchor: usage: assign-anchor <type> <obs_id> [--allow-collision]\n');
550
720
  process.exit(1);
551
721
  }
552
722
  if (assignType !== 'decision' && assignType !== 'pitfall') {
@@ -565,6 +735,22 @@ try {
565
735
  // Compute next anchor — O(anchored), single pass
566
736
  const { anchorId: aaAnchorId } = nextAnchorFromLedger(aaLedgerRows, assignType);
567
737
 
738
+ // E4: pre-mint collision guard — refuse if the candidate id is already
739
+ // cited (as a whole word) somewhere in tracked source with a different
740
+ // meaning, before any ledger write. Never auto-skip to the next free
741
+ // number — the collision is a human call (rename the citation, or
742
+ // rerun with --allow-collision to mint over it deliberately).
743
+ if (!aaAllowCollision) {
744
+ const aaCollisionHits = scanForAnchorCollision(aaProjectRoot, aaAnchorId);
745
+ if (aaCollisionHits.length > 0) {
746
+ throw new Error(
747
+ `assign-anchor: '${aaAnchorId}' is already cited in source with a different ` +
748
+ `meaning; resolve the collision before minting (or pass --allow-collision):\n` +
749
+ formatCollisionHits(aaCollisionHits)
750
+ );
751
+ }
752
+ }
753
+
568
754
  // Read observation from log
569
755
  let aaLogEntries = parseLedger(aaLogPath);
570
756
  const aaObsIdx = aaLogEntries.findIndex(e => e.id === assignObsId);
@@ -648,6 +834,41 @@ try {
648
834
  break;
649
835
  }
650
836
 
837
+ // -------------------------------------------------------------------------
838
+ // next-anchor <type>
839
+ // E4: Read-only preview of what assign-anchor would mint next — no lock
840
+ // acquired, no file written, no usage entry registered. Prints the
841
+ // candidate id and, when a pre-mint collision guard would fire, its
842
+ // file:line hits — so a caller can check before committing to assign-anchor.
843
+ // -------------------------------------------------------------------------
844
+ case 'next-anchor': {
845
+ const naType = args[0];
846
+
847
+ if (!naType) {
848
+ process.stderr.write('next-anchor: usage: next-anchor <type>\n');
849
+ process.exit(1);
850
+ }
851
+ if (naType !== 'decision' && naType !== 'pitfall') {
852
+ process.stderr.write(`next-anchor: type must be 'decision' or 'pitfall', got '${naType}'\n`);
853
+ process.exit(1);
854
+ }
855
+
856
+ const naProjectRoot = process.cwd();
857
+ const naLedgerRows = parseLedger(getDecisionsLedgerPath(naProjectRoot));
858
+ const { anchorId: naAnchorId } = nextAnchorFromLedger(naLedgerRows, naType);
859
+ const naHits = scanForAnchorCollision(naProjectRoot, naAnchorId);
860
+
861
+ process.stdout.write(naAnchorId + '\n');
862
+ if (naHits.length > 0) {
863
+ process.stderr.write(
864
+ `next-anchor: '${naAnchorId}' is already cited in source — collision hits:\n` +
865
+ formatCollisionHits(naHits) + '\n'
866
+ );
867
+ process.exit(1);
868
+ }
869
+ break;
870
+ }
871
+
651
872
  // -------------------------------------------------------------------------
652
873
  // retire-anchor <anchor_id> <status>
653
874
  // AC-A3, AC-F5, AC-F7: Flip decisions_status on the ledger row. Idempotent.
@@ -906,5 +1127,7 @@ if (typeof module !== 'undefined' && module.exports) {
906
1127
  initDecisionsContent,
907
1128
  nextAnchorFromLedger,
908
1129
  rotateObservations,
1130
+ scanForAnchorCollision,
1131
+ isCollisionScanExcluded,
909
1132
  };
910
1133
  }
@@ -43,7 +43,7 @@ function getDocsDir(projectRoot) {
43
43
  // Feature config (neutral .devflow root — not inside learning/)
44
44
  // ---------------------------------------------------------------------------
45
45
 
46
- /** .devflow/config.json — feature toggles {memory, learning, knowledge} */
46
+ /** .devflow/config.json — per-repo facts {reviewPublication, tracker override} */
47
47
  function getFeatureConfigPath(projectRoot) {
48
48
  return path.join(projectRoot, '.devflow', 'config.json');
49
49
  }
@@ -174,23 +174,6 @@ function getHandoffPath(projectRoot, branchSlug) {
174
174
  return path.join(projectRoot, '.devflow', 'docs', `handoff-${branchSlug}.md`);
175
175
  }
176
176
 
177
- // ---------------------------------------------------------------------------
178
- // Gitignore entries
179
- // ---------------------------------------------------------------------------
180
-
181
- /**
182
- * The canonical list of generic gitignore entries Devflow adds to a project's
183
- * root .gitignore for LOCAL-scope installs. Currently just `.claude/`.
184
- *
185
- * `.devflow/` is intentionally NOT here: it is managed by ensureDevflowGitignore
186
- * (TS) / ensure-root-gitignore (hook), which write the feature-knowledge carve-out
187
- * for ALL scopes. Adding a bare `.devflow/` here would append a wholesale-ignore
188
- * line after the carve-out and re-bury it (last match wins in .gitignore).
189
- */
190
- function getGitignoreEntries() {
191
- return ['.claude/'];
192
- }
193
-
194
177
  module.exports = {
195
178
  // Core directories
196
179
  getMemoryDir,
@@ -225,6 +208,4 @@ module.exports = {
225
208
  getDesignDir,
226
209
  getResearchDir,
227
210
  getHandoffPath,
228
- // Gitignore entries
229
- getGitignoreEntries,
230
211
  };
@@ -2,11 +2,88 @@
2
2
  # Shared log path computation for Devflow hooks.
3
3
  # Source this file to get devflow_log_dir function.
4
4
 
5
+ # D-LOG-DIR-CAP (hook side). Every hook logs under ~/.devflow/logs/<cwd-slug>/,
6
+ # one folder per working directory, and `devflow init` prunes them to the
7
+ # MAX_HOOK_LOG_DIRS most recent (src/core/hook-log-dirs.ts). A machine that never
8
+ # re-inits would still grow without bound, so devflow_log_dir also prunes — but
9
+ # ONLY when it creates a new folder, which is rare: the common path (the folder
10
+ # already exists) pays nothing. The pass is bounded (one `ls`, at most
11
+ # _DF_LOG_DIRS_SCANNED_MAX entries read, _DF_LOG_DIRS_PRUNED_PER_CALL removed in
12
+ # one `rm`), bash 3.2 compatible, and never spawns node. Only directories are
13
+ # counted or removed: root files (proxy.log) and symbolic links are left alone.
14
+ #
15
+ # Recency: folders are ranked by their own mtime (`ls -t`), which an append to a
16
+ # log inside does not move, so a candidate is spared when any of its first
17
+ # _DF_LOG_FILES_READ_PER_DIR entries is newer than the oldest folder the cap
18
+ # keeps (`-nt`, a shell builtin). A spared folder can leave the count briefly
19
+ # above the cap; init's exact prune settles it.
20
+ #
21
+ # _DF_MAX_HOOK_LOG_DIRS must equal MAX_HOOK_LOG_DIRS (tests/hook-log-paths.test.ts).
22
+ _DF_MAX_HOOK_LOG_DIRS=200
23
+ _DF_LOG_DIRS_PRUNED_PER_CALL=50
24
+ _DF_LOG_DIRS_SCANNED_MAX=100000
25
+ _DF_LOG_FILES_READ_PER_DIR=64
26
+
5
27
  # Cache the computed log dir path to avoid spawning 4 subprocesses (sed+tr+mkdir+chmod)
6
28
  # on every call within the same hook process.
7
29
  _LOG_DIR_CACHED=""
8
30
  _LOG_DIR_CACHED_CWD=""
9
31
 
32
+ # _df_log_dir_is_recent <dir> <cutoff>
33
+ # True when one of the first _DF_LOG_FILES_READ_PER_DIR entries in <dir> is newer
34
+ # than <cutoff>. Globbing must be on.
35
+ _df_log_dir_is_recent() {
36
+ local _dir="$1" _cutoff="$2" _f _read=0
37
+ for _f in "$_dir"/.[!.]* "$_dir"/..?* "$_dir"/*; do
38
+ [ "$_read" -lt "$_DF_LOG_FILES_READ_PER_DIR" ] || break
39
+ [ -e "$_f" ] || [ -L "$_f" ] || continue
40
+ _read=$((_read + 1))
41
+ if [ "$_f" -nt "$_cutoff" ]; then return 0; fi
42
+ done
43
+ return 1
44
+ }
45
+
46
+ # _df_prune_log_dirs <logs-root> <just-created-dir>
47
+ # Remove the oldest folders beyond _DF_MAX_HOOK_LOG_DIRS, at most
48
+ # _DF_LOG_DIRS_PRUNED_PER_CALL of them. Never fails the caller.
49
+ _df_prune_log_dirs() {
50
+ local _logs="$1" _new="$2"
51
+ local _listing _name _n=0 _i _removed=0 _cutoff _d _glob_off=0
52
+ local -a _dirs _batch
53
+ _listing=$(ls -1At "$_logs" 2>/dev/null) || return 0
54
+
55
+ case $- in *f*) _glob_off=1 ;; esac
56
+ local IFS='
57
+ '
58
+ set -f
59
+ for _name in $_listing; do
60
+ [ "$_n" -lt "$_DF_LOG_DIRS_SCANNED_MAX" ] || break
61
+ case "$_name" in */*) continue ;; esac
62
+ [ -L "$_logs/$_name" ] && continue
63
+ [ -d "$_logs/$_name" ] || continue
64
+ _dirs[_n]="$_logs/$_name"
65
+ _n=$((_n + 1))
66
+ done
67
+ set +f
68
+
69
+ if [ "$_n" -gt "$_DF_MAX_HOOK_LOG_DIRS" ]; then
70
+ _cutoff="${_dirs[_DF_MAX_HOOK_LOG_DIRS - 1]}"
71
+ _i=$((_n - 1))
72
+ while [ "$_i" -ge "$_DF_MAX_HOOK_LOG_DIRS" ] && [ "$_removed" -lt "$_DF_LOG_DIRS_PRUNED_PER_CALL" ]; do
73
+ _d="${_dirs[_i]}"
74
+ if [ "$_d" != "$_new" ] && ! _df_log_dir_is_recent "$_d" "$_cutoff"; then
75
+ _batch[_removed]="$_d"
76
+ _removed=$((_removed + 1))
77
+ fi
78
+ _i=$((_i - 1))
79
+ done
80
+ if [ "$_removed" -gt 0 ]; then rm -rf -- "${_batch[@]}" 2>/dev/null || true; fi
81
+ fi
82
+
83
+ if [ "$_glob_off" = 1 ]; then set -f; fi
84
+ return 0
85
+ }
86
+
10
87
  devflow_log_dir() {
11
88
  local cwd="$1"
12
89
  if [ "$cwd" = "$_LOG_DIR_CACHED_CWD" ] && [ -n "$_LOG_DIR_CACHED" ]; then
@@ -16,8 +93,11 @@ devflow_log_dir() {
16
93
  local slug
17
94
  slug=$(echo "$cwd" | sed 's|^/||' | tr '/' '-')
18
95
  local dir="$HOME/.devflow/logs/$slug"
96
+ local created=0
97
+ [ -d "$dir" ] || created=1
19
98
  mkdir -p "$dir"
20
99
  chmod 700 "$dir"
100
+ if [ "$created" = 1 ]; then _df_prune_log_dirs "$HOME/.devflow/logs" "$dir" || true; fi
21
101
  _LOG_DIR_CACHED="$dir"
22
102
  _LOG_DIR_CACHED_CWD="$cwd"
23
103
  echo "$dir"
@@ -2,9 +2,11 @@
2
2
 
3
3
  # Memory pipeline: memory-worker (Stop Hook)
4
4
  # Owns the 120s-throttle + nohup-spawn logic for background-memory-update.
5
- # Registered AFTER capture-turn in the Stop hook array so append-before-spawn
6
- # ordering is preserved by array position. This hook does NOT append to any
7
- # queue itself -- capture-turn already did that earlier in the same Stop event.
5
+ # Claude Code runs a Stop event's hooks in parallel, so this hook may fire
6
+ # before capture-turn has appended this turn's assistant row. That is safe:
7
+ # background-memory-update leaves a user-only queue in place and skips the LLM
8
+ # run (D-QUEUE-NO-ORPHAN-DELETE), and the next run takes the whole turn. This
9
+ # hook does NOT append to any queue itself.
8
10
 
9
11
  # Safe no-op fallback: must exist before set -e and before hook-bootstrap is sourced.
10
12
  dbg() { :; }
@@ -32,15 +34,19 @@ source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
32
34
  PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
33
35
  [ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
34
36
 
35
- DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
36
- MEMORY_DIR="$DEVFLOW_DIR/memory"
37
+ # The machine-wide manifest (the memory switch, D-FEATURES-NARROW-ONLY in
38
+ # queue-append) lives at the machine root,
39
+ # $HOME/.devflow (D-ONE-HOME).
40
+ DEVFLOW_MANIFEST="$HOME/.devflow/manifest.json"
41
+ PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
42
+ MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
37
43
 
38
- # Read feature config -- memory:false gates this hook entirely (ADR-001).
39
- FEATURE_CONFIG="$DEVFLOW_DIR/config.json"
40
- MEMORY_ENABLED="true"
41
- if [ -f "$FEATURE_CONFIG" ]; then
42
- MEMORY_ENABLED=$(json_field_file "$FEATURE_CONFIG" "memory" "true")
43
- fi
44
+ # Memory gate: the machine switch, narrowed by this checkout's project.json /
45
+ # config.json `features.memory`, gates this hook entirely -- read by the helper
46
+ # every memory/learning gate shares (D-FEATURES-NARROW-ONLY, see queue-append).
47
+ source "$SCRIPT_DIR/queue-append" || { echo "memory-worker: failed to source queue-append" >&2; exit 1; }
48
+ queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
49
+ MEMORY_ENABLED="$_QG_MEMORY"
44
50
 
45
51
  dbg "MEMORY_ENABLED=$MEMORY_ENABLED"
46
52
 
@@ -93,8 +99,11 @@ dbg "Throttle passed: TRIGGER_AGE=${TRIGGER_AGE}s >= 120s — spawning worker"
93
99
  # Touch trigger BEFORE spawning (prevents concurrent Stop hooks from double-spawning)
94
100
  touch "$TRIGGER_FILE" 2>/dev/null || true
95
101
 
96
- # Spawn detached worker — nohup + disown so it survives the Stop hook process exit
97
- nohup "$UPDATER" "$CWD" </dev/null >>/dev/null 2>&1 & disown
102
+ # Spawn detached worker — nohup + disown so it survives the Stop hook process exit.
103
+ # The manifest path is handed over explicitly: the worker re-checks the memory
104
+ # switch after spawn against the same manifest this hook gated on (and the same
105
+ # checkout's repository files, from the project root it resolves itself).
106
+ nohup "$UPDATER" "$CWD" "$DEVFLOW_MANIFEST" </dev/null >>/dev/null 2>&1 & disown
98
107
 
99
108
  log "Spawned background-memory-update worker (CWD=$CWD)"
100
109
  dbg "=== HOOK COMPLETE ==="
@@ -43,20 +43,24 @@ source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
43
43
  PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
44
44
  [ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
45
45
 
46
- DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
47
- MEMORY_DIR="$DEVFLOW_DIR/memory"
46
+ # The machine-wide manifest (the memory switch, D-FEATURES-NARROW-ONLY in
47
+ # queue-append) lives at the machine root,
48
+ # $HOME/.devflow (D-ONE-HOME).
49
+ DEVFLOW_MANIFEST="$HOME/.devflow/manifest.json"
50
+ PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
51
+ MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
48
52
 
49
53
  # Normal logging
50
54
  source "$SCRIPT_DIR/hook-log-init" "pre-compact-memory"
51
55
 
52
- # Check feature config — single source of truth for memory enabled/disabled (ADR-001).
53
- FEATURE_CONFIG="$DEVFLOW_DIR/config.json"
54
- if [ -f "$FEATURE_CONFIG" ]; then
55
- MEMORY_ENABLED=$(json_field_file "$FEATURE_CONFIG" "memory" "true")
56
- if [ "$MEMORY_ENABLED" = "false" ]; then
57
- dbg "EXIT: memory disabled in feature config"
58
- exit 0
59
- fi
56
+ # Memory gate: the machine switch, narrowed by this checkout's project.json /
57
+ # config.json `features.memory`, read by the helper every memory/learning gate
58
+ # shares (D-FEATURES-NARROW-ONLY, see queue-append).
59
+ source "$SCRIPT_DIR/queue-append" || { echo "pre-compact-memory: failed to source queue-append" >&2; exit 1; }
60
+ queue_read_gates "$DEVFLOW_MANIFEST" "$PROJECT_ROOT"
61
+ if [ "$_QG_MEMORY" != "true" ]; then
62
+ dbg "EXIT: memory disabled"
63
+ exit 0
60
64
  fi
61
65
 
62
66
  # Auto-create .devflow/ and ensure .gitignore entries (idempotent after first run)
@@ -75,9 +79,26 @@ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
75
79
  if cd "$CWD" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1; then
76
80
  GIT_HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "")
77
81
  GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
78
- GIT_STATUS=$(git status --porcelain 2>/dev/null | head -30 || echo "")
82
+ # D-DETACHED-HEAD: a checkout of a commit rather than a branch (`git checkout
83
+ # <sha>`, bisect, a rebase stop, `git worktree add --detach`, a CI checkout)
84
+ # prints no branch name. It is labelled `(detached)`, echoing git's own
85
+ # `(HEAD detached at …)` display and holding no space, so the stamp's
86
+ # one-token `branch:` field still parses; the backup names it and the bootstrap
87
+ # below still stamps memory, keyed on the commit it describes. (A branch really
88
+ # named `(detached)` is legal git and would read the same — accepted: nothing
89
+ # decides on the label, it only names.) An unborn branch has no HEAD commit,
90
+ # keeps the empty label, and is still skipped.
91
+ if [ -z "$GIT_BRANCH" ] && is_hex_sha "$GIT_HEAD_SHA" 40 40; then
92
+ GIT_BRANCH="(detached)"
93
+ fi
94
+ # D-NO-FSMONITOR: `status` and `diff` read the index, and reading the index
95
+ # runs the command the repository's config names in `core.fsmonitor` — code
96
+ # chosen by the repository this hook runs inside. Each index read turns it
97
+ # off for itself (`-c core.fsmonitor=false`); rev-parse, branch and log never
98
+ # read the index.
99
+ GIT_STATUS=$(git -c core.fsmonitor=false status --porcelain 2>/dev/null | head -30 || echo "")
79
100
  GIT_LOG=$(git log --oneline -10 2>/dev/null || echo "")
80
- GIT_DIFF_STAT=$(git diff --stat HEAD 2>/dev/null || echo "")
101
+ GIT_DIFF_STAT=$(git -c core.fsmonitor=false diff --stat HEAD 2>/dev/null || echo "")
81
102
  dbg "GIT_BRANCH=$GIT_BRANCH HEAD=$GIT_HEAD_SHA"
82
103
  fi
83
104
 
@@ -101,8 +122,12 @@ json_backup_construct \
101
122
  log "Wrote backup: $BACKUP_FILE"
102
123
  dbg "Wrote backup: $BACKUP_FILE"
103
124
 
104
- # Bootstrap minimal WORKING-MEMORY.md if absent; skip on detached HEAD, unborn branch,
105
- # or malformed SHA. is_hex_sha 40 40: exactly 40 lowercase hex chars required.
125
+ # Bootstrap minimal WORKING-MEMORY.md if absent; skip on an unborn branch or a
126
+ # malformed SHA. is_hex_sha 40 40: exactly 40 lowercase hex chars required. A detached
127
+ # HEAD bootstraps with a `branch: (detached)` stamp (D-DETACHED-HEAD, above): the
128
+ # stamp's memory-head SHA is what drift detection keys on, and the Context line names
129
+ # the commit by its short SHA, so the first compaction there no longer leaves the
130
+ # session with nothing to restore.
106
131
  # avoids REL-5: O_EXCL-style atomic create via noclobber so the existence test and the
107
132
  # create are one operation — if the worker's CAS mv lands in the window, noclobber fails
108
133
  # (file already exists) and we skip the bootstrap rather than truncating fresh memory.
@@ -122,7 +147,11 @@ if [ -n "$GIT_BRANCH" ] && is_hex_sha "$GIT_HEAD_SHA" 40 40; then
122
147
  echo "- (none recorded)"
123
148
  echo ""
124
149
  echo "## Context"
125
- echo "- Branch: $GIT_BRANCH"
150
+ if [ "$GIT_BRANCH" = "(detached)" ]; then
151
+ echo "- Branch: (detached) @ ${GIT_HEAD_SHA:0:7}"
152
+ else
153
+ echo "- Branch: $GIT_BRANCH"
154
+ fi
126
155
  echo "$GIT_LOG" | head -3 | while IFS= read -r line; do
127
156
  [ -n "$line" ] && echo "- $line"
128
157
  done
@@ -66,11 +66,8 @@ elif [[ "$HEAD" == "Implement the following plan:"* ]]; then
66
66
  elif [[ "$HEAD" == "/"* ]]; then
67
67
  dbg "EXIT: slash command — no reminder"
68
68
  else
69
- # Model-tier taxonomy (haiku=mechanical, sonnet=defined execution, opus=analysis/design/research)
70
- # is cross-referenced with the full routing table in orchestrator-charter.md.
71
- # Update both together if routing changes.
72
69
  dbg "ORCHESTRATOR_REMINDER injected"
73
- json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds, multi-file reads, and debug loops via the Agent tool (haiku=mechanical, sonnet=defined execution, opus=analysis/design/research) or the matching devflow workflow skill.
70
+ json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds, multi-file reads, and debug loops to the fitting roster agent (Agent tool) or the matching devflow workflow skill.
74
71
  Keep only judgment work mainline: conversation, decisions, routing, synthesis of agent reports."
75
72
  fi
76
73