devflow-kit 3.0.1 → 3.2.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 (166) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +1 -1
  3. package/dist/agents/git.md +2 -2
  4. package/dist/cli/agents-view/index.js +1 -1
  5. package/dist/cli/agents-view/render.js +71 -17
  6. package/dist/cli/agents-view/state.js +42 -16
  7. package/dist/cli/agents-view/terminal.js +5 -5
  8. package/dist/cli/commands/agents.js +142 -51
  9. package/dist/cli/commands/ambient.js +1 -1
  10. package/dist/cli/commands/attribution-prompts.js +8 -8
  11. package/dist/cli/commands/capture.js +1 -1
  12. package/dist/cli/commands/compliance-prompts.js +8 -8
  13. package/dist/cli/commands/compliance.js +8 -7
  14. package/dist/cli/commands/flags.js +33 -31
  15. package/dist/cli/commands/hud.js +1 -1
  16. package/dist/cli/commands/init-seed.js +9 -9
  17. package/dist/cli/commands/init.js +162 -85
  18. package/dist/cli/commands/install-report.js +10 -10
  19. package/dist/cli/commands/learning.js +302 -136
  20. package/dist/cli/commands/memory.js +36 -15
  21. package/dist/cli/commands/proxy.js +23 -23
  22. package/dist/cli/commands/rules.js +6 -5
  23. package/dist/cli/commands/tracker-prompts.js +6 -6
  24. package/dist/cli/commands/tracker.js +9 -9
  25. package/dist/cli/commands/uninstall.js +183 -59
  26. package/dist/cli/flags-view/render.js +5 -5
  27. package/dist/cli/flags-view/state.js +9 -9
  28. package/dist/cli/flags-view/terminal.js +4 -4
  29. package/dist/cli/tui/cells.js +1 -1
  30. package/dist/cli/tui/terminal.js +6 -6
  31. package/dist/commands/code-review.md +0 -2
  32. package/dist/commands/debug.md +14 -11
  33. package/dist/commands/dynamic-build.md +51 -47
  34. package/dist/commands/dynamic-plan.md +27 -7
  35. package/dist/commands/dynamic-profile.md +17 -3
  36. package/dist/commands/dynamic-tickets.md +18 -4
  37. package/dist/commands/explore.md +9 -3
  38. package/dist/commands/implement.md +20 -16
  39. package/dist/commands/plan.md +13 -9
  40. package/dist/commands/release.md +23 -3
  41. package/dist/commands/research.md +9 -3
  42. package/dist/commands/resolve.md +9 -12
  43. package/dist/commands/self-review.md +0 -2
  44. package/dist/core/agent-frontmatter.js +28 -3
  45. package/dist/core/agent-models.js +204 -42
  46. package/dist/core/agent-state.js +28 -6
  47. package/dist/core/ansi.js +2 -2
  48. package/dist/core/assets.js +1 -1
  49. package/dist/core/cache.js +7 -8
  50. package/dist/core/codex-auth-inspect.js +4 -4
  51. package/dist/core/compliance-compose.js +3 -3
  52. package/dist/core/compliance.js +3 -4
  53. package/dist/core/evidence-policy.js +14 -13
  54. package/dist/core/external-models.js +1 -1
  55. package/dist/core/feature-config.js +71 -13
  56. package/dist/core/feature-switch.js +3 -3
  57. package/dist/core/flags.js +49 -25
  58. package/dist/core/fs-atomic.js +6 -7
  59. package/dist/core/learning-queue-cleanup.js +16 -81
  60. package/dist/core/learning-store.js +61 -0
  61. package/dist/core/linked-path.js +46 -0
  62. package/dist/core/manifest.js +5 -5
  63. package/dist/core/mds-variants.js +13 -13
  64. package/dist/core/model-discovery.js +8 -8
  65. package/dist/core/observations.js +17 -101
  66. package/dist/core/orphan-sweep.js +4 -4
  67. package/dist/core/plugins.js +13 -8
  68. package/dist/core/project-paths.js +9 -13
  69. package/dist/core/proxy-log.js +8 -8
  70. package/dist/core/proxy-state.js +3 -3
  71. package/dist/core/queue-drain.js +31 -0
  72. package/dist/core/reference-sweep.js +6 -6
  73. package/dist/core/teammate-mode-cleanup.js +1 -1
  74. package/dist/core/tracker.js +14 -14
  75. package/dist/hud/colors.js +2 -2
  76. package/dist/hud/components/learning-counts.js +54 -22
  77. package/dist/hud/components/version-badge.js +1 -1
  78. package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
  79. package/dist/skills/git/references/tracker/github/create-release.md +2 -2
  80. package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
  81. package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
  82. package/dist/targets/claude-code/compliance-install.js +17 -15
  83. package/dist/targets/claude-code/hooks.js +2 -2
  84. package/dist/targets/claude-code/installer.js +59 -32
  85. package/dist/targets/claude-code/legacy.js +1 -1
  86. package/dist/targets/claude-code/post-install.js +135 -45
  87. package/dist/targets/claude-code/tracker-install.js +2 -2
  88. package/package.json +1 -1
  89. package/src/assets/agents/code.md +15 -21
  90. package/src/assets/agents/design.md +4 -2
  91. package/src/assets/agents/diagnose.md +3 -1
  92. package/src/assets/agents/evaluate.md +4 -0
  93. package/src/assets/agents/git.mds +2 -2
  94. package/src/assets/agents/knowledge.md +5 -3
  95. package/src/assets/agents/learning.md +281 -196
  96. package/src/assets/agents/research.md +3 -1
  97. package/src/assets/agents/review.md +5 -3
  98. package/src/assets/agents/scrutinize.md +5 -1
  99. package/src/assets/agents/simplify.md +4 -0
  100. package/src/assets/agents/skim.md +4 -2
  101. package/src/assets/agents/synthesize.md +6 -0
  102. package/src/assets/agents/test.md +18 -10
  103. package/src/assets/agents/triage.md +11 -9
  104. package/src/assets/agents/validate.md +14 -10
  105. package/src/assets/commands/_partials/_decisions.mds +8 -3
  106. package/src/assets/commands/_partials/_docs_root.mds +3 -3
  107. package/src/assets/commands/_partials/_engine.mds +16 -32
  108. package/src/assets/commands/_partials/_knowledge.mds +0 -2
  109. package/src/assets/commands/_partials/_preamble.mds +6 -2
  110. package/src/assets/commands/_partials/_settings.mds +2 -2
  111. package/src/assets/commands/_partials/_tracker.mds +1 -1
  112. package/src/assets/commands/code-review.mds +0 -2
  113. package/src/assets/commands/debug.mds +13 -8
  114. package/src/assets/commands/dynamic-build.mds +18 -12
  115. package/src/assets/commands/dynamic-plan.mds +10 -4
  116. package/src/assets/commands/dynamic-profile.mds +1 -1
  117. package/src/assets/commands/dynamic-tickets.mds +2 -2
  118. package/src/assets/commands/explore.mds +9 -1
  119. package/src/assets/commands/implement.mds +19 -13
  120. package/src/assets/commands/plan.mds +12 -8
  121. package/src/assets/commands/release.md +23 -3
  122. package/src/assets/commands/research.mds +9 -3
  123. package/src/assets/commands/resolve.mds +9 -10
  124. package/src/assets/mds/git/_pr.mds +3 -3
  125. package/src/assets/mds/tracker/_common.mds +1 -1
  126. package/src/assets/mds/tracker/_github.mds +3 -3
  127. package/src/assets/mds/tracker/_jira.mds +3 -3
  128. package/src/assets/mds/tracker/_linear.mds +3 -3
  129. package/src/assets/mds/tracker/_mcp.mds +6 -5
  130. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -2
  131. package/src/assets/scripts/hooks/background-memory-update +97 -33
  132. package/src/assets/scripts/hooks/capture-prompt +4 -3
  133. package/src/assets/scripts/hooks/capture-question +4 -3
  134. package/src/assets/scripts/hooks/capture-turn +5 -20
  135. package/src/assets/scripts/hooks/ensure-devflow-init +14 -2
  136. package/src/assets/scripts/hooks/ensure-proxy +5 -6
  137. package/src/assets/scripts/hooks/ensure-root-gitignore +123 -11
  138. package/src/assets/scripts/hooks/git-marker +71 -0
  139. package/src/assets/scripts/hooks/is-hex-sha +1 -1
  140. package/src/assets/scripts/hooks/json-helper.cjs +345 -944
  141. package/src/assets/scripts/hooks/json-parse +25 -129
  142. package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
  143. package/src/assets/scripts/hooks/lib/learning-store.cjs +3207 -0
  144. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
  145. package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
  146. package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
  147. package/src/assets/scripts/hooks/memory-worker +10 -0
  148. package/src/assets/scripts/hooks/pre-compact-memory +66 -14
  149. package/src/assets/scripts/hooks/preamble +9 -1
  150. package/src/assets/scripts/hooks/queue-append +55 -23
  151. package/src/assets/scripts/hooks/resolve-project-root +3 -4
  152. package/src/assets/scripts/hooks/session-start-context +146 -45
  153. package/src/assets/scripts/hooks/session-start-memory +33 -11
  154. package/src/assets/scripts/lib/project-config.cjs +2 -2
  155. package/src/assets/scripts/pr-evidence.cjs +3 -3
  156. package/src/assets/scripts/redact-secrets.cjs +20 -20
  157. package/src/assets/scripts/release-trace.cjs +1 -1
  158. package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
  159. package/src/assets/scripts/resolve-settings.cjs +3 -3
  160. package/src/assets/scripts/verify-evidence.cjs +2 -2
  161. package/src/assets/skills/apply-decisions/SKILL.md +37 -17
  162. package/src/assets/skills/docs-framework/SKILL.md +2 -2
  163. package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
  164. package/src/assets/skills/test-driven-development/SKILL.md +6 -4
  165. package/dist/core/observation-io.js +0 -50
  166. package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @file queue-drain.ts
3
+ *
4
+ * `drainQueueFiles`, the delete the queue drains share — `drainLearningQueue`
5
+ * (learning-queue-cleanup.ts) and `drainMemoryQueue` (cli/commands/memory.ts) —
6
+ * and `formatRefusedDrain`, the line the CLI prints for a drain it refused.
7
+ */
8
+ import { promises as fs } from 'fs';
9
+ import { firstSymbolicLink } from './linked-path.js';
10
+ /**
11
+ * Delete each of `files` unless one of `folders` — `.devflow` and the folder that
12
+ * holds the files, outermost first — is a symbolic link (D-CLI-NO-SYMLINK): through a
13
+ * linked folder, each delete would remove a same-named file wherever the link points.
14
+ * A file that is already gone is not an error; any other failure propagates, as the
15
+ * check's own does, so the command reports it.
16
+ */
17
+ export async function drainQueueFiles(folders, files) {
18
+ const linkedFolder = await firstSymbolicLink(folders);
19
+ if (linkedFolder !== null)
20
+ return { drained: false, linkedFolder };
21
+ await Promise.all(files.map(file => fs.unlink(file).catch((error) => {
22
+ if (error.code !== 'ENOENT')
23
+ throw error;
24
+ })));
25
+ return { drained: true };
26
+ }
27
+ /** The warning for a drain refused at `linkedFolder`: which queue kept its files, and why. */
28
+ export function formatRefusedDrain(queue, linkedFolder) {
29
+ return `The ${queue} queue was not drained: ${linkedFolder} is a symbolic link, and devflow deletes nothing through one`;
30
+ }
31
+ //# sourceMappingURL=queue-drain.js.map
@@ -7,14 +7,14 @@ import * as path from 'path';
7
7
  *
8
8
  * Sibling of {@link sweepOrphanedAssets} in orphan-sweep.ts and deliberately the same
9
9
  * {@link SweepResult} shape — `scanned` is the non-vacuity counter, removals and
10
- * per-item failures are reported rather than thrown (avoids PF-009).
10
+ * per-item failures are reported rather than thrown.
11
11
  *
12
12
  * What is genuinely new is the KEY. `sweepOrphanedAssets` keys a flat directory by
13
13
  * registry name through `mdEntryName`, which cannot express `tracker/{provider}/{op}.md`:
14
14
  * two providers may legitimately both carry a `comment.md`, so the registry name has to
15
15
  * be the relative PATH, and the walk has to descend.
16
16
  *
17
- * Never writes, only removes (avoids PF-011).
17
+ * Never writes, only removes — no delete-then-write window.
18
18
  */
19
19
  /**
20
20
  * Descent bound for every walk over the generated reference tree — this sweep and the
@@ -32,7 +32,7 @@ import * as path from 'path';
32
32
  * The two walkers answer a breach differently by design, and both answer out loud: the
33
33
  * build throws (a generated tree that deep is a build bug, and dist/ is still the
34
34
  * build's own to fail), while this sweep records the unvisited subtree in `failed`
35
- * (avoids PF-009 — an install is not abandoned over one subtree). Neither returns
35
+ * (an install is not abandoned over one subtree). Neither returns
36
36
  * quietly: a subtree the walk never entered must not be summarised as converged.
37
37
  */
38
38
  export const MAX_REFERENCE_SWEEP_DEPTH = 8;
@@ -76,7 +76,7 @@ async function sweepDirectory(dir, prefix, depth, known, knownDirPrefixes, acc)
76
76
  entries = await fs.readdir(dir, { withFileTypes: true });
77
77
  }
78
78
  catch {
79
- return; /* absent or unreadable — not an error (avoids PF-009) */
79
+ return; /* absent or unreadable — not an error */
80
80
  }
81
81
  for (const entry of entries) {
82
82
  const relPath = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
@@ -96,7 +96,7 @@ async function sweepDirectory(dir, prefix, depth, known, knownDirPrefixes, acc)
96
96
  acc.removed.push(relPath);
97
97
  }
98
98
  catch (err) {
99
- acc.failed.push({ name: relPath, error: err }); /* per-item isolation (avoids PF-009) */
99
+ acc.failed.push({ name: relPath, error: err }); /* per-item isolation */
100
100
  }
101
101
  continue;
102
102
  }
@@ -108,7 +108,7 @@ async function sweepDirectory(dir, prefix, depth, known, knownDirPrefixes, acc)
108
108
  acc.removed.push(relPath);
109
109
  }
110
110
  catch (err) {
111
- acc.failed.push({ name: relPath, error: err }); /* per-item isolation (avoids PF-009) */
111
+ acc.failed.push({ name: relPath, error: err }); /* per-item isolation */
112
112
  }
113
113
  }
114
114
  }
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Tolerant: malformed JSON is returned unchanged (no-op). Non-object roots
11
11
  * (null, arrays, primitives) are treated as a no-op — only object roots can
12
- * carry the key (avoids PF-004: a TypeError here would escape the function
12
+ * carry the key (a TypeError here would escape the function
13
13
  * and surface as an unhandled error during uninstall).
14
14
  */
15
15
  export function stripDevflowTeammateModeFromJson(settingsJson) {
@@ -9,9 +9,9 @@
9
9
  * migrateLegacyTrackerConventions) owns the ~/.devflow tracker files.
10
10
  * It sits here rather than in a target adapter because ~/.devflow is
11
11
  * devflow-global, not Claude-Code-specific — the same reason manifest.ts's
12
- * read/write live in src/core/ (applies ADR-013).
12
+ * read/write live in src/core/.
13
13
  *
14
- * avoids PF-014: nothing here calls process.exit() and nothing throws; every
14
+ * Nothing here calls process.exit() and nothing throws; every
15
15
  * fallible path returns a Result, so callers own their own error rendering
16
16
  * and a try/finally in a caller is never skipped.
17
17
  *
@@ -31,7 +31,7 @@ import * as path from 'path';
31
31
  /**
32
32
  * Canonical issue-tracker provider registry.
33
33
  *
34
- * D-TRACKER-ONE-DOMAIN [PF-049]: this table is the SINGLE authority on the closed
34
+ * D-TRACKER-ONE-DOMAIN: this table is the SINGLE authority on the closed
35
35
  * provider set, and `TrackerProvider` below is a projection of it. A provider
36
36
  * therefore exists for the type system exactly when it has a row here: there is no
37
37
  * hand-listed union that can admit an id `parseTrackerId` rejects and the wizard
@@ -63,7 +63,7 @@ export const DEFAULT_TRACKER_PROVIDER = 'github';
63
63
  // Artifact basenames — one spelling for every TypeScript reader
64
64
  //
65
65
  // NOT the only spelling in the repository, and a rename that assumes it is will
66
- // miss the places these names are hardcoded (PF-013): the SessionStart hook's
66
+ // miss the places these names are hardcoded: the SessionStart hook's
67
67
  // Section 3 (shell) and the Tracker agent's prompt (prose), neither of which can
68
68
  // import from here. Each is cross-pinned against these constants by tests —
69
69
  // shell-hooks-tracker, tracker-agent, uninstall-logic and core/tracker — so the
@@ -117,7 +117,7 @@ export const TRACKER_CLAIM_FILE = '.tracker.processing';
117
117
  * no user-authored content (it is a scrubbed, unplaced copy of what the agent
118
118
  * was about to write), so it is an install artifact, never user content.
119
119
  *
120
- * Spelled twice for the reason the basenames above are (PF-013): the agent's
120
+ * Spelled twice for the reason the basenames above are: the agent's
121
121
  * prompt cannot import from here, so the mktemp template is also a literal in
122
122
  * src/assets/agents/tracker.md, and tests/core/tracker.test.ts pins the two
123
123
  * spellings together.
@@ -142,7 +142,7 @@ export const TRACKER_ATTEMPTS_NAMES = TRACKER_PROVIDER_IDS.map(id => trackerAtte
142
142
  * How many background inference attempts a machine gets before the SessionStart
143
143
  * hook stops emitting the setup directive.
144
144
  *
145
- * Spelled twice for the reason the basenames above are (PF-013): the hook is the
145
+ * Spelled twice for the reason the basenames above are: the hook is the
146
146
  * enforcer and cannot import from here, so `TRACKER_ATTEMPTS_MAX=5` is also a
147
147
  * literal in src/assets/scripts/hooks/session-start-context. This constant is the
148
148
  * number `devflow tracker --status` quotes back when it re-arms the counter, and
@@ -187,7 +187,7 @@ export function isTrackerProvider(value) {
187
187
  * `parseTrackerId`'s error text and by `devflow tracker --status`.
188
188
  *
189
189
  * Takes `unknown`, and a non-string renders as its TYPE: the module's
190
- * never-throws contract (PF-014) has to hold for what reaches this sink, not
190
+ * never-throws contract has to hold for what reaches this sink, not
191
191
  * only for what the signature says does — `devflow tracker --status` reads
192
192
  * a conventions file's hand-editable frontmatter, and a caller-side guard is one edit
193
193
  * from being gone. Naming the type also keeps the render total, where `String()`
@@ -242,12 +242,12 @@ export function parseTrackerId(input) {
242
242
  * Tolerant sink normaliser for a raw `manifest.features.tracker` value.
243
243
  *
244
244
  * Absent, null, malformed, a bare string, or an unknown provider → the default
245
- * `{provider:'github'}` (applies ADR-014 self-heal). Drop-not-error: a manifest
245
+ * `{provider:'github'}` (self-heal). Drop-not-error: a manifest
246
246
  * written by a newer devflow, or hand-edited, degrades to what this build
247
247
  * understands instead of failing the read.
248
248
  *
249
249
  * D-TRACKER-SELF-HEAL [DR-26]: self-healing here is SILENT and emits no
250
- * DEGRADED — that is the correct ADR-014 behaviour, and it is a different
250
+ * DEGRADED — that is the correct self-heal behaviour, and it is a different
251
251
  * condition from a per-repo config value outside the domain (which does emit
252
252
  * `unknown tracker provider`). The two must not be conflated.
253
253
  */
@@ -317,8 +317,8 @@ export function parseTrackerFrontmatter(head) {
317
317
  /**
318
318
  * How many leading BYTES of a conventions file any reader takes — the bound the
319
319
  * Tracker agent writes to and the Git agent loads. A line cap alone bounds the
320
- * SCAN, not the read: the file's size is not devflow's to assume (avoids PF-023:
321
- * a bound is only real at the sink).
320
+ * SCAN, not the read: the file's size is not devflow's to assume
321
+ * (a bound is only real at the sink).
322
322
  */
323
323
  export const TRACKER_CONVENTIONS_READ_BYTES = 8000;
324
324
  /**
@@ -327,7 +327,7 @@ export const TRACKER_CONVENTIONS_READ_BYTES = 8000;
327
327
  * `undefined` for an absent, unreadable or non-regular path — a FIFO or a device
328
328
  * is refused before it is opened, so a hostile entry can never block a read.
329
329
  * Follows a symlink to read what it names (a reader cares what the conventions
330
- * SAY); it never moves or writes through one. Never throws (PF-014).
330
+ * SAY); it never moves or writes through one. Never throws.
331
331
  */
332
332
  export async function readBoundedHead(filePath, limit) {
333
333
  let handle;
@@ -360,7 +360,7 @@ export async function readBoundedHead(filePath, limit) {
360
360
  * select a provider the machine never did, and its counter is the one a user in
361
361
  * that repository is capped on.
362
362
  *
363
- * Idempotent when a counter is absent; never throws (PF-014). `fs.rm` with
363
+ * Idempotent when a counter is absent; never throws. `fs.rm` with
364
364
  * `force` treats an absent file — and an absent parent directory — as success.
365
365
  */
366
366
  export async function rearmTrackerInference(devflowDir) {
@@ -385,7 +385,7 @@ export async function rearmTrackerInference(devflowDir) {
385
385
  * bare presence marker, so the hook never has to open the manifest to learn which
386
386
  * provider it is gating.
387
387
  *
388
- * Converges unconditionally in both directions (avoids PF-015): a provider
388
+ * Converges unconditionally in both directions: a provider
389
389
  * flipped back to github removes the sentinel in the same call shape that wrote
390
390
  * it, so there is no "enable wrote it, disable forgot it" asymmetry.
391
391
  */
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * ANSI color helpers — re-exported from src/core/ansi.ts.
3
3
  *
4
- * The canonical implementation lives in src/core/ansi.ts (agent-neutral home,
5
- * applies ADR-013). This file is a re-export barrel so all existing HUD
4
+ * The canonical implementation lives in src/core/ansi.ts (agent-neutral home).
5
+ * This file is a re-export barrel so all existing HUD
6
6
  * component call sites continue to resolve `../colors.js` without change.
7
7
  */
8
8
  export { bold, dim, red, green, yellow, blue, magenta, cyan, gray, white, orange, brightRed, boldRed, bgGreen, bgYellow, bgRed, inverse, truncate, stripAnsi, } from '../core/ansi.js';
@@ -2,18 +2,58 @@ import * as fs from 'node:fs';
2
2
  import { dim } from '../colors.js';
3
3
  import { getDecisionsLedgerPath } from '../../core/project-paths.js';
4
4
  import { getLedgerRoot } from '../../core/ledger-root.js';
5
+ import { isActiveDecisionsStatus } from '../../core/observations.js';
5
6
  /** The HUD's per-git-command budget (src/hud/git.ts GIT_TIMEOUT). */
6
7
  const LEDGER_ROOT_TIMEOUT_MS = 1000;
7
8
  /**
8
- * @devflow-design-decision D309
9
- * Counts come from decisions-ledger.jsonl (the render source of truth), NOT
10
- * the rendered decisions.md/pitfalls.md, so the HUD never couples to markdown
11
- * format. Active-row semantics mirror render-decisions.cjs exactly: a row
12
- * counts when anchor_id is set and decisions_status is absent or outside
13
- * INACTIVE_STATUSES — so the numbers always equal the entries visible in the
14
- * rendered files.
9
+ * The largest ledger the statusline reads: 8 MiB, far above the roughly 0.5 MB
10
+ * real ledgers reach.
11
+ *
12
+ * D-HUD-LEDGER-BOUNDED: the statusline counts the ledger only when it is a regular
13
+ * file of at most LEDGER_MAX_BYTES, and never reads it if it is a symbolic link; a
14
+ * link, any other kind of file or a larger one shows no counts, as an absent
15
+ * ledger does. Reason: the statusline reads the ledger on every prompt, and a
16
+ * repository can commit it as a link to an endless source such as /dev/zero, or
17
+ * as a huge file, either of which would hang the statusline.
15
18
  */
16
- const INACTIVE_STATUSES = new Set(['Deprecated', 'Superseded', 'Retired']);
19
+ export const LEDGER_MAX_BYTES = 8 * 1024 * 1024;
20
+ /**
21
+ * The ledger's text, or null when D-HUD-LEDGER-BOUNDED refuses it or it cannot be
22
+ * read. lstat refuses a link without following it, the open refuses one that took
23
+ * the ledger's place since (O_NOFOLLOW) and never blocks on a FIFO (O_NONBLOCK),
24
+ * and the read takes at most the size fstat checked.
25
+ */
26
+ function readBoundedLedger(ledgerPath) {
27
+ let fd;
28
+ try {
29
+ if (!fs.lstatSync(ledgerPath).isFile())
30
+ return null;
31
+ fd = fs.openSync(ledgerPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ try {
37
+ const stat = fs.fstatSync(fd);
38
+ if (!stat.isFile() || stat.size > LEDGER_MAX_BYTES)
39
+ return null;
40
+ const buf = Buffer.alloc(stat.size);
41
+ let total = 0;
42
+ while (total < buf.length) {
43
+ const read = fs.readSync(fd, buf, total, buf.length - total, total);
44
+ if (read === 0)
45
+ break;
46
+ total += read;
47
+ }
48
+ return buf.toString('utf-8', 0, total);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ finally {
54
+ fs.closeSync(fd);
55
+ }
56
+ }
17
57
  function isLedgerCountRow(val) {
18
58
  if (typeof val !== 'object' || val === null)
19
59
  return false;
@@ -24,24 +64,16 @@ function isLedgerCountRow(val) {
24
64
  return false;
25
65
  return o.decisions_status === undefined || typeof o.decisions_status === 'string';
26
66
  }
27
- function isActive(row) {
28
- if (!row.decisions_status)
29
- return true;
30
- return !INACTIVE_STATUSES.has(row.decisions_status);
31
- }
32
67
  /**
33
68
  * Read .devflow/learning/decisions-ledger.jsonl and count active anchored
34
- * rows by type. Returns null if the ledger is missing or holds no valid rows
35
- * (graceful fallback). Exported for use by the main HUD entry point.
69
+ * rows by type. Returns null if the ledger is missing, is refused by
70
+ * D-HUD-LEDGER-BOUNDED, or holds no valid rows (graceful fallback). Exported for
71
+ * use by the main HUD entry point.
36
72
  */
37
73
  export function gatherLearningCounts(cwd) {
38
- let content;
39
- try {
40
- content = fs.readFileSync(getDecisionsLedgerPath(cwd), 'utf-8');
41
- }
42
- catch {
74
+ const content = readBoundedLedger(getDecisionsLedgerPath(cwd));
75
+ if (content === null)
43
76
  return null;
44
- }
45
77
  const counts = { decisions: 0, pitfalls: 0 };
46
78
  let parsedAny = false;
47
79
  for (const rawLine of content.split('\n')) {
@@ -59,7 +91,7 @@ export function gatherLearningCounts(cwd) {
59
91
  if (!isLedgerCountRow(parsed))
60
92
  continue;
61
93
  parsedAny = true;
62
- if (!isActive(parsed))
94
+ if (!isActiveDecisionsStatus(parsed.decisions_status))
63
95
  continue;
64
96
  if (parsed.type === 'decision')
65
97
  counts.decisions++;
@@ -70,7 +70,7 @@ export default async function versionBadge(ctx) {
70
70
  const current = getCurrentVersion(ctx.devflowDir);
71
71
  if (!current)
72
72
  return null;
73
- const cacheDir = hudCacheDir(ctx.devflowDir); // authoritative path from cache.ts (avoids PF-013)
73
+ const cacheDir = hudCacheDir(ctx.devflowDir); // authoritative path from cache.ts
74
74
  // Cache only the npm registry result (expensive); current is always live
75
75
  let info = readCache(cacheDir, VERSION_CACHE_KEY, validateVersionInfo);
76
76
  if (!info) {
@@ -9,7 +9,7 @@ Load for `resolve-review-threads` under every tracker provider.
9
9
  Each `THREAD_MAP` entry carries one verdict:
10
10
  - `FIXED` — issue addressed
11
11
  - `FALSE_POSITIVE` — not a real issue; requires grep/file:line citation as evidence
12
- - `BY_DESIGN` — intentional; requires ADR or code citation as evidence
12
+ - `BY_DESIGN` — intentional; requires a recorded decision, stated in words, or code citation as evidence
13
13
  - `ESCALATED` — requires human review
14
14
 
15
15
  (The resolution gate these verdicts feed — D9 — is stated in the agent's own section.)
@@ -28,7 +28,7 @@ unexplained unresolved threads.
28
28
  - **FALSE_POSITIVE**: `After investigation, this appears to be a false positive: {evidence}. No code change needed.`
29
29
  - **BY_DESIGN**: `This is intentional: {evidence}. No code change needed.`
30
30
  - **ESCALATED**: `This thread has been escalated for human review and recorded in the resolution summary.`
31
- - Reply bodies MUST NOT contain verbatim content from the external thread body — cite only internal evidence (commit SHAs, file:line from this codebase, ADR IDs)
31
+ - Reply bodies MUST NOT contain verbatim content from the external thread body — cite only internal evidence (commit SHAs, file:line from this codebase)
32
32
  2. Write reply to `$DEVFLOW_BODY_RAW`; apply the Comment-sink scrub (D11) — non-zero exit → DEGRADED for that thread, continue per D4. Post reply via `addPullRequestReviewThreadReply` GraphQL mutation with `-F body=@"$DEVFLOW_BODY"` (file-ref form).
33
33
 
34
34
  (Step 3, the D9 gate, is stated in the agent's own section.)
@@ -2,10 +2,10 @@
2
2
 
3
3
  Load when the resolved tracker provider is `github` and the operation is `create-release`.
4
4
 
5
- **Mechanics held here:** the closed-issues step only. Tag creation, release creation and notes composition stay with the operation.
5
+ **Mechanics held here:** the shipped-issues step only. Tag creation, release creation and notes composition stay with the operation.
6
6
 
7
7
  ### Process
8
8
 
9
9
  Inside step 5 (compose release notes):
10
10
 
11
- - If `SHIPPED_ISSUES` provided: append a `## Closed Issues` section with issue references — **first ≤50 issues** (the same bound `backlink-shipped-issues` applies); if truncated, add a final `…and {n} more issues` line (D4 degrade if enrichment fails)
11
+ - If `SHIPPED_ISSUES` provided: append a `## Shipped Issues` section with issue references — **first ≤50 issues** (the same bound `backlink-shipped-issues` applies); if truncated, add a final `…and {n} more issues` line (D4 degrade if enrichment fails)
@@ -2,13 +2,13 @@
2
2
 
3
3
  Load when the resolved tracker provider is `jira` and the operation is `create-release`.
4
4
 
5
- **Mechanics held here:** the closed-issues step only. Tag creation, release creation and notes composition stay with the operation and are unchanged — they are release-host mechanics, not tracker mechanics.
5
+ **Mechanics held here:** the shipped-issues step only. Tag creation, release creation and notes composition stay with the operation and are unchanged — they are release-host mechanics, not tracker mechanics.
6
6
 
7
7
  ### Process
8
8
 
9
9
  Inside step 5 (compose release notes):
10
10
 
11
- - If `SHIPPED_ISSUES` is provided: append a `## Closed Issues` section rendering each entry through `## Reference Rendering` — **first ≤50 issues** (the same bound `backlink-shipped-issues` applies); if truncated, add a final `…and {n} more issues` line (D4 degrade if enrichment fails).
11
+ - If `SHIPPED_ISSUES` is provided: append a `## Shipped Issues` section rendering each entry through `## Reference Rendering` — **first ≤50 issues** (the same bound `backlink-shipped-issues` applies); if truncated, add a final `…and {n} more issues` line (D4 degrade if enrichment fails).
12
12
  - Pre-flight the list against `^[A-Z][A-Z0-9_]{1,9}-[1-9][0-9]{0,8}$`, anchored at both ends, and **drop** every entry that fails, reporting each as `TRACEABILITY: DEGRADED (issue reference "{ref}" does not match jira reference grammar)` — a bare number among them goes with them, in silence: `ambiguous issue reference` answers a reference a caller named, not one line of a list nobody chose. Every entry dropped ⇒ `TRACEABILITY: DEGRADED (no parseable refs for provider {p})` and the section is omitted rather than rendered empty.
13
13
  - `## Reference Rendering` absent, or its token discarded by the gate below ⇒ render the key itself on its own line, and record the discard under `### Substitutions`.
14
14
 
@@ -2,13 +2,13 @@
2
2
 
3
3
  Load when the resolved tracker provider is `linear` and the operation is `create-release`.
4
4
 
5
- **Mechanics held here:** the closed-issues step only. Tag creation, release creation and notes composition stay with the operation and are unchanged — they are release-host mechanics, not tracker mechanics.
5
+ **Mechanics held here:** the shipped-issues step only. Tag creation, release creation and notes composition stay with the operation and are unchanged — they are release-host mechanics, not tracker mechanics.
6
6
 
7
7
  ### Process
8
8
 
9
9
  Inside step 5 (compose release notes):
10
10
 
11
- - If `SHIPPED_ISSUES` is provided: append a `## Closed Issues` section rendering each entry through `## Reference Rendering` — **first ≤50 issues** (the same bound `backlink-shipped-issues` applies); if truncated, add a final `…and {n} more issues` line (D4 degrade if enrichment fails).
11
+ - If `SHIPPED_ISSUES` is provided: append a `## Shipped Issues` section rendering each entry through `## Reference Rendering` — **first ≤50 issues** (the same bound `backlink-shipped-issues` applies); if truncated, add a final `…and {n} more issues` line (D4 degrade if enrichment fails).
12
12
  - **ASCII-upper-normalise every entry first.** Pre-flight the list against **either** anchored form — the team-key form `^[A-Z][A-Z0-9]{0,9}-[1-9][0-9]{0,8}$` or the internal-id form `^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$`, anchored at both ends, never joined into one alternation, and **drop** every entry that fails, reporting each as `TRACEABILITY: DEGRADED (issue reference "{ref}" does not match linear reference grammar)` — a bare number among them goes with them, in silence: `ambiguous issue reference` answers a reference a caller named, not one line of a list nobody chose. Every entry dropped ⇒ `TRACEABILITY: DEGRADED (no parseable refs for provider {p})` and the section is omitted rather than rendered empty.
13
13
  - `## Reference Rendering` absent, or its token discarded by the gate below ⇒ render the reference itself on its own line, and record the discard under `### Substitutions`.
14
14
 
@@ -14,10 +14,10 @@
14
14
  * decided by the ids its caller passes (D-COMPLIANCE-REPO-LENS), never by which
15
15
  * files are present.
16
16
  *
17
- * Applies ADR-013: I/O orchestration in src/targets/; pure helpers in src/core/.
18
- * Applies PF-009: warn-not-throw for per-item failures.
19
- * Applies PF-011: temp-sibling+rename for skill dir rewrites.
20
- * Applies PF-015: both artifacts converge unconditionally (no || short-circuits).
17
+ * I/O orchestration in src/targets/; pure helpers in src/core/.
18
+ * Warn-not-throw for per-item failures.
19
+ * Temp-sibling+rename for skill dir rewrites.
20
+ * Both artifacts converge unconditionally (no || short-circuits).
21
21
  */
22
22
  import { promises as fs } from 'fs';
23
23
  import * as path from 'path';
@@ -53,7 +53,7 @@ async function pathExists(p) {
53
53
  * Fragments always come from the canonical source (not user-overridable) — they carry
54
54
  * registry-owned content (mapping cells, reference blurbs, checklist items, rule bullets).
55
55
  *
56
- * PF-009: parse errors and unreadable files are reported via warn; the framework is
56
+ * Parse errors and unreadable files are reported via warn; the framework is
57
57
  * silently omitted from the result map (C5 in composeComplianceSkill handles the gap).
58
58
  */
59
59
  async function loadComplianceFragments(canonicalSrc, frameworks, warn) {
@@ -92,8 +92,8 @@ async function loadComplianceFragments(canonicalSrc, frameworks, warn) {
92
92
  * `fragments` is loaded once by convergeComplianceArtifacts and shared with the rule
93
93
  * installer — the SKILL.md and the rule compose from the same parsed set.
94
94
  *
95
- * Applies PF-011: build under a .tmp sibling, remove old target, rename.
96
- * Applies PF-009: unexpected I/O failures are reported via warn; never thrown.
95
+ * Build under a .tmp sibling, remove old target, rename.
96
+ * Unexpected I/O failures are reported via warn; never thrown.
97
97
  */
98
98
  async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, warn) {
99
99
  const canonicalSrc = path.join(skillsDir(), 'compliance');
@@ -108,7 +108,7 @@ async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments
108
108
  try {
109
109
  // Clean up any orphaned tmp from a prior crashed run (best-effort).
110
110
  await fs.rm(tmpTarget, { recursive: true, force: true });
111
- // Build the new directory tree under the tmp sibling (PF-011).
111
+ // Build the new directory tree under the tmp sibling.
112
112
  const refDst = path.join(tmpTarget, 'references');
113
113
  await fs.mkdir(refDst, { recursive: true });
114
114
  // SKILL.md: compose from template (shadow or canonical) + fragments.
@@ -121,7 +121,7 @@ async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments
121
121
  // Always-present reference files (detection.md, sources.md) from the canonical
122
122
  // references/ directory — these are not framework-specific.
123
123
  //
124
- // PF-009: each copy is isolated. A skill dir missing one reference still works;
124
+ // Each copy is isolated. A skill dir missing one reference still works;
125
125
  // aborting the whole install because one file is unreadable would take out
126
126
  // SKILL.md too. Failures warn and the remaining refs still install.
127
127
  const alwaysPresentSrc = path.join(canonicalSrc, 'references');
@@ -148,7 +148,7 @@ async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments
148
148
  warn(`compliance: reference "${fw}.md" not installed — ${String(err)}`);
149
149
  }
150
150
  }
151
- // Atomically swap: remove old target, rename tmp into place.
151
+ // Swap: remove old target, rename tmp into place (two calls, not atomic).
152
152
  await fs.rm(target, { recursive: true, force: true });
153
153
  await fs.rename(tmpTarget, target);
154
154
  }
@@ -174,7 +174,7 @@ async function installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments
174
174
  * source (never the shadow — fragments are registry-owned) and shared with the skill
175
175
  * installer.
176
176
  *
177
- * Applies PF-009: I/O failures are reported via warn; never thrown.
177
+ * I/O failures are reported via warn; never thrown.
178
178
  */
179
179
  async function installRuleFile(claudeDir, devflowDir, frameworks, fragments, warn) {
180
180
  const ruleShadowFile = path.join(devflowDir, 'rules', 'compliance.md');
@@ -203,8 +203,10 @@ async function installRuleFile(claudeDir, devflowDir, frameworks, fragments, war
203
203
  * enabled + !rulesEnabled → skill dir (every ref, machine stamp); remove stale rule
204
204
  * !enabled → skill dir (every ref, neutral stamp); remove rule
205
205
  *
206
- * PF-015: both artifact operations execute unconditionally — no || short-circuits.
207
- * PF-011: skill dir write uses temp-sibling+rename to avoid ENOENT windows.
206
+ * Both artifact operations execute unconditionally — no || short-circuits.
207
+ * Skill dir write builds under a temp sibling, then removes the target and renames
208
+ * the sibling into place, which narrows the ENOENT window to the gap between those
209
+ * two calls.
208
210
  *
209
211
  * D: the `warn` callback is injected (not console.warn) so callers control
210
212
  * surfacing (init log lines, test spies, etc.) — per the dependency-injection
@@ -227,7 +229,7 @@ export async function convergeComplianceArtifacts(opts) {
227
229
  const safeFrameworks = normalizeFrameworks(frameworks);
228
230
  // I13: Track whether every artifact operation in this run completed without error.
229
231
  // `converged` starts true and is set false by the tracking wrapper whenever any warn
230
- // path is taken — including inside installSkillDir / installRuleFile (PF-009 paths).
232
+ // path is taken — including inside installSkillDir / installRuleFile (their per-item warn paths).
231
233
  let converged = true;
232
234
  const trackingWarn = (msg) => {
233
235
  converged = false;
@@ -239,7 +241,7 @@ export async function convergeComplianceArtifacts(opts) {
239
241
  // frameworks need one — a compliance-off machine stamps none.
240
242
  const stampFrameworks = enabled ? safeFrameworks : [];
241
243
  const fragments = await loadComplianceFragments(path.join(skillsDir(), 'compliance'), stampFrameworks, trackingWarn);
242
- // PF-015: the skill and the rule are independent operations. An error in
244
+ // The skill and the rule are independent operations. An error in
243
245
  // installSkillDir is caught internally and reported via trackingWarn, so
244
246
  // execution always continues to the rule step.
245
247
  await installSkillDir(claudeDir, devflowDir, stampFrameworks, fragments, trackingWarn);
@@ -40,8 +40,8 @@ export function endsWithAny(suffixes) {
40
40
  * `/scripts/hooks/session-start-memory.sh`), under any directory — so installs made
41
41
  * under a custom or retired devflow directory are still recognised. It is never
42
42
  * devflow's because it merely CONTAINS a marker word: a user's `~/bin/memory-worker`,
43
- * `echo capture-turn` or `/opt/tools/run-hook preamble` is theirs (applies ADR-024 —
44
- * remove only what devflow can prove it wrote). Removal goes through `removeHooks`,
43
+ * `echo capture-turn` or `/opt/tools/run-hook preamble` is theirs
44
+ * (remove only what devflow can prove it wrote). Removal goes through `removeHooks`,
45
45
  * one hook at a time. Every hook module builds its predicates here;
46
46
  * D-AMBIENT-EXACT-HOOK is the ambient instance of this rule.
47
47
  */