devflow-kit 3.0.1 → 3.1.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 (133) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/agents/git.md +2 -2
  3. package/dist/cli/agents-view/index.js +1 -1
  4. package/dist/cli/agents-view/render.js +2 -2
  5. package/dist/cli/agents-view/state.js +2 -2
  6. package/dist/cli/agents-view/terminal.js +5 -5
  7. package/dist/cli/commands/agents.js +7 -6
  8. package/dist/cli/commands/ambient.js +1 -1
  9. package/dist/cli/commands/attribution-prompts.js +8 -8
  10. package/dist/cli/commands/capture.js +1 -1
  11. package/dist/cli/commands/compliance-prompts.js +8 -8
  12. package/dist/cli/commands/compliance.js +8 -7
  13. package/dist/cli/commands/flags.js +33 -31
  14. package/dist/cli/commands/hud.js +1 -1
  15. package/dist/cli/commands/init-seed.js +9 -9
  16. package/dist/cli/commands/init.js +34 -32
  17. package/dist/cli/commands/install-report.js +10 -10
  18. package/dist/cli/commands/learning.js +267 -129
  19. package/dist/cli/commands/memory.js +1 -1
  20. package/dist/cli/commands/proxy.js +23 -23
  21. package/dist/cli/commands/rules.js +6 -5
  22. package/dist/cli/commands/tracker-prompts.js +6 -6
  23. package/dist/cli/commands/tracker.js +9 -9
  24. package/dist/cli/commands/uninstall.js +20 -20
  25. package/dist/cli/flags-view/render.js +5 -5
  26. package/dist/cli/flags-view/state.js +9 -9
  27. package/dist/cli/flags-view/terminal.js +4 -4
  28. package/dist/cli/tui/cells.js +1 -1
  29. package/dist/cli/tui/terminal.js +6 -6
  30. package/dist/commands/dynamic-build.md +18 -4
  31. package/dist/commands/dynamic-plan.md +19 -5
  32. package/dist/commands/dynamic-profile.md +17 -3
  33. package/dist/commands/dynamic-tickets.md +18 -4
  34. package/dist/commands/release.md +15 -1
  35. package/dist/commands/research.md +1 -1
  36. package/dist/commands/resolve.md +8 -9
  37. package/dist/core/agent-frontmatter.js +3 -3
  38. package/dist/core/agent-models.js +6 -6
  39. package/dist/core/agent-state.js +2 -2
  40. package/dist/core/ansi.js +2 -2
  41. package/dist/core/cache.js +7 -8
  42. package/dist/core/codex-auth-inspect.js +4 -4
  43. package/dist/core/compliance-compose.js +3 -3
  44. package/dist/core/compliance.js +3 -4
  45. package/dist/core/evidence-policy.js +14 -13
  46. package/dist/core/external-models.js +1 -1
  47. package/dist/core/feature-config.js +3 -3
  48. package/dist/core/feature-switch.js +3 -3
  49. package/dist/core/flags.js +25 -25
  50. package/dist/core/fs-atomic.js +6 -7
  51. package/dist/core/learning-queue-cleanup.js +16 -80
  52. package/dist/core/learning-store.js +61 -0
  53. package/dist/core/manifest.js +5 -5
  54. package/dist/core/mds-variants.js +13 -13
  55. package/dist/core/model-discovery.js +8 -8
  56. package/dist/core/observations.js +17 -101
  57. package/dist/core/orphan-sweep.js +4 -4
  58. package/dist/core/plugins.js +4 -5
  59. package/dist/core/project-paths.js +9 -13
  60. package/dist/core/proxy-log.js +8 -8
  61. package/dist/core/proxy-state.js +3 -3
  62. package/dist/core/reference-sweep.js +6 -6
  63. package/dist/core/teammate-mode-cleanup.js +1 -1
  64. package/dist/core/tracker.js +14 -14
  65. package/dist/hud/colors.js +2 -2
  66. package/dist/hud/components/learning-counts.js +2 -16
  67. package/dist/hud/components/version-badge.js +1 -1
  68. package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
  69. package/dist/targets/claude-code/compliance-install.js +17 -15
  70. package/dist/targets/claude-code/hooks.js +2 -2
  71. package/dist/targets/claude-code/installer.js +24 -24
  72. package/dist/targets/claude-code/legacy.js +1 -1
  73. package/dist/targets/claude-code/post-install.js +7 -7
  74. package/dist/targets/claude-code/tracker-install.js +2 -2
  75. package/package.json +1 -1
  76. package/src/assets/agents/code.md +1 -4
  77. package/src/assets/agents/design.md +2 -2
  78. package/src/assets/agents/diagnose.md +1 -1
  79. package/src/assets/agents/git.mds +2 -2
  80. package/src/assets/agents/knowledge.md +3 -3
  81. package/src/assets/agents/learning.md +281 -196
  82. package/src/assets/agents/research.md +1 -1
  83. package/src/assets/agents/review.md +3 -3
  84. package/src/assets/agents/scrutinize.md +1 -1
  85. package/src/assets/agents/skim.md +1 -1
  86. package/src/assets/agents/triage.md +9 -9
  87. package/src/assets/commands/_partials/_decisions.mds +8 -3
  88. package/src/assets/commands/_partials/_docs_root.mds +3 -3
  89. package/src/assets/commands/_partials/_engine.mds +1 -1
  90. package/src/assets/commands/_partials/_preamble.mds +6 -2
  91. package/src/assets/commands/_partials/_settings.mds +2 -2
  92. package/src/assets/commands/dynamic-build.mds +1 -1
  93. package/src/assets/commands/dynamic-plan.mds +3 -3
  94. package/src/assets/commands/dynamic-profile.mds +1 -1
  95. package/src/assets/commands/dynamic-tickets.mds +2 -2
  96. package/src/assets/commands/release.md +15 -1
  97. package/src/assets/commands/research.mds +1 -1
  98. package/src/assets/commands/resolve.mds +8 -9
  99. package/src/assets/mds/git/_pr.mds +3 -3
  100. package/src/assets/mds/tracker/_common.mds +1 -1
  101. package/src/assets/mds/tracker/_github.mds +1 -1
  102. package/src/assets/mds/tracker/_jira.mds +1 -1
  103. package/src/assets/mds/tracker/_linear.mds +1 -1
  104. package/src/assets/mds/tracker/_mcp.mds +6 -5
  105. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +1 -0
  106. package/src/assets/scripts/hooks/background-memory-update +28 -22
  107. package/src/assets/scripts/hooks/capture-turn +1 -17
  108. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  109. package/src/assets/scripts/hooks/ensure-proxy +5 -6
  110. package/src/assets/scripts/hooks/ensure-root-gitignore +1 -1
  111. package/src/assets/scripts/hooks/is-hex-sha +1 -1
  112. package/src/assets/scripts/hooks/json-helper.cjs +348 -814
  113. package/src/assets/scripts/hooks/json-parse +3 -2
  114. package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
  115. package/src/assets/scripts/hooks/lib/learning-store.cjs +3102 -0
  116. package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
  117. package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
  118. package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
  119. package/src/assets/scripts/hooks/queue-append +2 -2
  120. package/src/assets/scripts/hooks/resolve-project-root +3 -4
  121. package/src/assets/scripts/hooks/session-start-context +40 -18
  122. package/src/assets/scripts/lib/project-config.cjs +2 -2
  123. package/src/assets/scripts/pr-evidence.cjs +3 -3
  124. package/src/assets/scripts/redact-secrets.cjs +20 -20
  125. package/src/assets/scripts/release-trace.cjs +1 -1
  126. package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
  127. package/src/assets/scripts/resolve-settings.cjs +3 -3
  128. package/src/assets/scripts/verify-evidence.cjs +2 -2
  129. package/src/assets/skills/apply-decisions/SKILL.md +37 -17
  130. package/src/assets/skills/docs-framework/SKILL.md +2 -2
  131. package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
  132. package/dist/core/observation-io.js +0 -50
  133. package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
@@ -56,10 +56,9 @@ _df_nongit_root() {
56
56
  # DF_LEDGER_ROOT — where the learning ledger and its queue live.
57
57
  #
58
58
  # D-LEDGER-MAIN-WORKTREE: a linked worktree (`git worktree add`, `claude
59
- # --worktree`) has its own toplevel, so a ledger anchored there restarts at
60
- # ADR-001 — main's decisions are invisible in it, and the IDs it mints collide
61
- # with main's (in a repo whose collision guard refuses, the Learning agent stops
62
- # for good). The ledger is one per REPOSITORY, not per checkout: DF_LEDGER_ROOT
59
+ # --worktree`) has its own toplevel, so a ledger anchored there restarts its
60
+ # numbering at 001 — main's decisions are invisible in it, and the IDs it mints collide
61
+ # with main's. The ledger is one per REPOSITORY, not per checkout: DF_LEDGER_ROOT
63
62
  # is the main worktree — the parent of `--git-common-dir` — when that common dir
64
63
  # is a `<dir>/.git` directory AND `<dir>/.devflow` already exists; otherwise it is
65
64
  # DF_ROOT. The existence test keeps a main checkout that never ran devflow from
@@ -8,7 +8,8 @@
8
8
  # not the home directory (D-HOOKS-GIT-ONLY), and they read the learning ledger of
9
9
  # the main worktree when this is a linked one (D-LEDGER-MAIN-WORKTREE).
10
10
  #
11
- # Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines).
11
+ # Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines),
12
+ # then the path of the decisions index when it lists entries.
12
13
  # Section 2: Learning maintenance directive — when captured turns are pending in
13
14
  # the learning queue (or a crashed run left a stale .processing batch), instructs
14
15
  # the main model to spawn the background Learning agent with the resolved model.
@@ -85,7 +86,7 @@ fi
85
86
  # Ensure the project root .gitignore ignores .devflow/ wholesale. This runs on every
86
87
  # git-project session regardless of feature toggles, so memory-off projects
87
88
  # (learning/knowledge only) still get .devflow/ ignored — this is the
88
- # memory-independent path that fixes the gitignore/memory coupling (PF-014). Single
89
+ # memory-independent path that fixes the gitignore/memory coupling. Single
89
90
  # source of truth: ensure-root-gitignore. Soft-fail: a gitignore write must never
90
91
  # block context injection. Marker keeps it O(1).
91
92
  [ -n "$PROJECT_OK" ] && [ -d "$PROJECT_ROOT" ] && [ -f "$SCRIPT_DIR/ensure-root-gitignore" ] && source "$SCRIPT_DIR/ensure-root-gitignore" "$PROJECT_ROOT" || true
@@ -101,7 +102,8 @@ TRACKER_DEVFLOW_DIR="$HOME/.devflow"
101
102
  #
102
103
  # Section 2 embeds $LEDGER_ROOT; Section 3 embeds $PROJECT_ROOT and
103
104
  # $TRACKER_DEVFLOW_DIR — each inside a double-quoted `prompt: "..."` string the
104
- # model reads out of additionalContext. The provider and model TOKENS in those
105
+ # model reads out of additionalContext. Section 1's index line embeds
106
+ # $LEDGER_ROOT too, as the path the model reads the decisions index from. The provider and model TOKENS in those
105
107
  # directives are admitted by positive allowlists, and so are these: the gate is the
106
108
  # POSITIVE shape ^[A-Za-z0-9/._+-]+$, expressed as "rejects if any character falls
107
109
  # outside it".
@@ -134,16 +136,16 @@ TRACKER_DEVFLOW_DIR="$HOME/.devflow"
134
136
  # admitted set is locale-dependent, and no rule here rests on it.
135
137
  #
136
138
  # Checked ONCE, here, where every value is resolved and above every section that
137
- # interpolates them, so no sink can embed a value no gate saw (PF-023 — the
139
+ # interpolates them, so no sink can embed a value no gate saw (the
138
140
  # invariant belongs at the convergence point all callers pass through, not in
139
141
  # whichever section someone remembered). `case` is a shell builtin, so the GitHub
140
142
  # path still forks zero times [DR-10]. The empty arm is explicit: an unset root
141
143
  # must not read as "no forbidden character, therefore safe".
142
144
  #
143
145
  # ONE gate per VALUE, applied to the root each section embeds, not one over their
144
- # concatenation. Section 2 interpolates $LEDGER_ROOT alone — the main worktree's
145
- # root in a linked worktree (D-LEDGER-MAIN-WORKTREE), so it is the value checked
146
- # for that section, not the checkout's own; only Section 3 interpolates
146
+ # concatenation. Sections 1 and 2 interpolate $LEDGER_ROOT alone — the main
147
+ # worktree's root in a linked worktree (D-LEDGER-MAIN-WORKTREE), so it is the value
148
+ # checked for them, not the checkout's own; only Section 3 interpolates
147
149
  # $PROJECT_ROOT and $TRACKER_DEVFLOW_DIR. Gating values jointly made a rejected
148
150
  # ~/.devflow shape suppress the Learning directive as well — a value Section 2
149
151
  # never embeds, silently disabling the whole learning pipeline for any machine
@@ -211,10 +213,27 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
211
213
  fi
212
214
  fi
213
215
  done
214
- if [ -n "$DECISIONS_TLDR" ]; then
215
- dbg "Decisions TL;DR found"
216
- DECISIONS_SECTION="--- PROJECT DECISIONS (TL;DR) ---
216
+ # The index line names the decisions index, so the main model can pass it on
217
+ # as DECISIONS_CONTEXT. It interpolates $LEDGER_ROOT, so it needs that root's
218
+ # shape gate, and it is left out when the index lists no entry: an empty
219
+ # corpus renders `(none)`. Builtins only — no fork on any path.
220
+ _SC_INDEX="$LEARNING_DIR/index.md"; DECISIONS_INDEX_LINE=""
221
+ if [ -n "$DIRECTIVE_LEDGER_SAFE" ] && [ -s "$_SC_INDEX" ]; then
222
+ _SC_IDX1=""; IFS= read -r _SC_IDX1 < "$_SC_INDEX" || true
223
+ [ "$_SC_IDX1" != "(none)" ] && DECISIONS_INDEX_LINE="Index: $_SC_INDEX"
224
+ fi
225
+ if [ -n "$DECISIONS_TLDR" ] || [ -n "$DECISIONS_INDEX_LINE" ]; then
226
+ dbg "Decisions TL;DR or index found"
227
+ DECISIONS_SECTION="--- PROJECT DECISIONS (TL;DR) ---"
228
+ if [ -n "$DECISIONS_TLDR" ]; then
229
+ DECISIONS_SECTION="${DECISIONS_SECTION}
217
230
  $(printf '%b' "$DECISIONS_TLDR")"
231
+ fi
232
+ # The index line goes last.
233
+ if [ -n "$DECISIONS_INDEX_LINE" ]; then
234
+ DECISIONS_SECTION="${DECISIONS_SECTION}
235
+ ${DECISIONS_INDEX_LINE}"
236
+ fi
218
237
  if [ -n "$CONTEXT" ]; then
219
238
  CONTEXT="${CONTEXT}
220
239
 
@@ -227,11 +246,14 @@ ${DECISIONS_SECTION}"
227
246
  fi
228
247
 
229
248
  # --- Section 2: Learning maintenance directive ---
230
- # Emitted when captured turns are waiting: queue non-empty, or a leftover
231
- # .processing batch whose owner crashed (stale mtime, 900s threshold — same
232
- # family the Learning agent itself uses to discriminate live from crashed). A
233
- # FRESH .processing means a live Learning agent already owns the batch, so the
234
- # directive is suppressed even if new turns queued since its claim.
249
+ # Emitted when captured turns are waiting: the queue is non-empty, or a
250
+ # .processing claim has had no heartbeat for PROCESSING_STALE_SECS and its batch
251
+ # needs a new owner. A FRESH .processing means a live Learning agent holds the
252
+ # claim, so the directive is suppressed even if new turns queued since. This
253
+ # check is advisory and decides only whether to spawn: the claim itself is taken
254
+ # by json-helper's claim-queue op under the learning lock, which applies the same
255
+ # threshold (D-OWNED-CLAIM, lib/learning-store.cjs), so a spawn that loses a race
256
+ # exits on `busy`.
235
257
  if [ "$LEARNING_ENABLED" = "true" ]; then
236
258
  QUEUE_FILE="$LEARNING_DIR/.pending-turns.jsonl"
237
259
  PROCESSING_FILE="$LEARNING_DIR/.pending-turns.processing"
@@ -440,8 +462,8 @@ if [ -n "$TRACKER_PROVIDER" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
440
462
  tracker_gates_open() {
441
463
  # Gate 1 — the attempt cap. Read with the `read` builtin: no fork.
442
464
  #
443
- # The counter's shape is ONE decimal integer line and nothing else (PF-062 —
444
- # document the shape of any file that gates an action, and keep absent and
465
+ # The counter's shape is ONE decimal integer line and nothing else
466
+ # (document the shape of any file that gates an action, and keep absent and
445
467
  # malformed distinct from a value). Absent means "no attempt yet" = 0.
446
468
  # Malformed self-heals to 0 and is overwritten with a well-formed count on
447
469
  # emission below, so a stray byte can never recur: refusing forever would
@@ -571,7 +593,7 @@ if [ -n "$TRACKER_PROVIDER" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
571
593
  # allowlisted tokens. Decided once at the top of the file, where both values are
572
594
  # resolved; consulted here because this is one of the two sinks that interpolate
573
595
  # them, and a control stated once for the file is not a control at a sink that
574
- # never consults it (PF-023). The third path, the conventions file, is composed
596
+ # never consults it. The third path, the conventions file, is composed
575
597
  # from the gated devflow directory and the allowlisted provider alone, so this
576
598
  # gate covers it too.
577
599
  if [ -z "$DIRECTIVE_PATHS_SAFE" ]; then
@@ -2,14 +2,14 @@
2
2
  //
3
3
  // The ONE parser of devflow's per-repository config files — the team-committed
4
4
  // `.devflow/project.json` and the personal, uncommitted `.devflow/config.json`
5
- // (applies PF-023: the invariant lives at the sink every reader passes through).
5
+ // (the invariant lives at the sink every reader passes through).
6
6
  // Installed beside its two callers as ~/.devflow/scripts/lib/project-config.cjs:
7
7
  // resolve-evidence-policy.cjs reads `evidence` and `compliance` at every source
8
8
  // resolve-settings.cjs reads every key of both files, locally
9
9
  //
10
10
  // Pure except the two readers (readBoundedRegularFile, readMachineManifest), which
11
11
  // only ever read. Nothing here writes, spawns or prints: devflow never writes
12
- // project.json (applies ADR-024).
12
+ // project.json, which the team owns and commits.
13
13
  //
14
14
  // D-PROJECT-CONFIG: `.devflow/project.json` is a JSON object whose every key is
15
15
  // optional and whose unknown keys are ignored:
@@ -898,7 +898,7 @@ function needsDiff(x) {
898
898
  /**
899
899
  * D-LADDER: the arms, in the order they are tried. PRECEDENCE is derived from
900
900
  * this table. Every arm but the last is a POSITIVE match; the last is the
901
- * conservative default (applies PF-075: a verified state is only ever reached by
901
+ * conservative default (a verified state is only ever reached by
902
902
  * a positive conjunction, never because nothing else matched).
903
903
  *
904
904
  * @type {readonly Arm[]}
@@ -978,7 +978,7 @@ const ARMS = Object.freeze([
978
978
  /**
979
979
  * D-LADDER: the order `classify` tries its arms in — first match wins, and the
980
980
  * terminal arm is the conservative UNVERIFIED, so no state is ever reached by
981
- * exhaustion (applies PF-075). DERIVED from ARMS, so the order the contract states
981
+ * exhaustion. DERIVED from ARMS, so the order the contract states
982
982
  * (parity-pinned to this list) and the order the code runs cannot drift apart.
983
983
  */
984
984
  const PRECEDENCE = Object.freeze(/** @type {State[]} */ (ARMS.map(arm => arm.state)));
@@ -1446,7 +1446,7 @@ function parseEvidenceComment(text) {
1446
1446
  }
1447
1447
 
1448
1448
  // ---------------------------------------------------------------------------
1449
- // splice — only the bytes between the markers are devflow's (applies ADR-024)
1449
+ // splice — only the bytes between the markers are devflow's
1450
1450
  // ---------------------------------------------------------------------------
1451
1451
 
1452
1452
  /**
@@ -40,19 +40,19 @@
40
40
  // posted" from "the script broke": the first is final, the second is retried.
41
41
  //
42
42
  // Design constraints (binding):
43
- // PF-011 the file sink — the one file this script writes — goes via
44
- // temp-sibling + rename (atomic same-fs write; readers see old-or-new,
45
- // never a momentarily absent file). `--emit` prints to stdout and
46
- // touches no file, so it has nothing to protect
47
- // PF-014 never call process.exit() inside any scope with pending cleanup or
48
- // buffered output; main() returns an exit code; the single top-level
49
- // boundary writes stdout SYNCHRONOUSLY then sets process.exitCode so
50
- // nothing is truncated and no finally block is skipped
51
- // PF-018 all regexes are bounded (no unbounded [\s\S]*); skip-list checked
52
- // before any replacement; the unterminated-header pattern uses a
53
- // character class instead of a lazy quantifier for bounded scan
54
- // PF-023 self-contained sink-side control — never assumes upstream masking
55
- // happened; the scrubber is authoritative for its own rule set
43
+ // - the file sink — the one file this script writes — goes via
44
+ // temp-sibling + rename (atomic same-fs write; readers see old-or-new,
45
+ // never a momentarily absent file). `--emit` prints to stdout and
46
+ // touches no file, so it has nothing to protect
47
+ // - never call process.exit() inside any scope with pending cleanup or
48
+ // buffered output; main() returns an exit code; the single top-level
49
+ // boundary writes stdout SYNCHRONOUSLY then sets process.exitCode so
50
+ // nothing is truncated and no finally block is skipped
51
+ // - all regexes are bounded (no unbounded [\s\S]*); skip-list checked
52
+ // before any replacement; the unterminated-header pattern uses a
53
+ // character class instead of a lazy quantifier for bounded scan
54
+ // - self-contained sink-side control — never assumes upstream masking
55
+ // happened; the scrubber is authoritative for its own rule set
56
56
 
57
57
  'use strict';
58
58
 
@@ -98,7 +98,7 @@ const NONCE_HEX_CHARS = 32;
98
98
  * rule then describes a property of every body this script can emit, instead of
99
99
  * an obligation nine documents have to restate correctly.
100
100
  *
101
- * PF-018: bounded — a fixed alternation over two literals, anchored per line by
101
+ * Bounded — a fixed alternation over two literals, anchored per line by
102
102
  * the `m` flag, with no quantifier to backtrack through. The trailing space is
103
103
  * load-bearing: it is what keeps prose such as `D11-FAILURE` out of the refusal.
104
104
  */
@@ -268,7 +268,7 @@ function hasMixedAlphanumerics(s) {
268
268
  // Scrubbing pass
269
269
  //
270
270
  // Rules are applied in the declared order. Each rule uses a bounded regex to
271
- // avoid catastrophic backtracking (PF-018). The final secret-assignment rule
271
+ // avoid catastrophic backtracking. The final secret-assignment rule
272
272
  // operates line-by-line and applies entropy + character-class heuristics to
273
273
  // avoid false positives on config references.
274
274
  // ---------------------------------------------------------------------------
@@ -375,7 +375,7 @@ function scrub(content) {
375
375
  // (`this.apiToken`, `api-key`). Without these the rule only ever fired on a
376
376
  // bare column-0 assignment, which is the rarest form inside a review finding.
377
377
  // The declarator group is bounded {0,3} and each iteration must consume a
378
- // literal keyword, so the added alternation cannot backtrack (PF-018).
378
+ // literal keyword, so the added alternation cannot backtrack.
379
379
  //
380
380
  // Conditions for replacement (all must hold):
381
381
  // (a) The KEY matches SECRET_KEY_RE — not merely the line, so a prose line
@@ -590,7 +590,7 @@ function frameEmit(scrubbed, scrubLine, nonceSource) {
590
590
  // ---------------------------------------------------------------------------
591
591
  // main — returns an exit code (never calls process.exit() internally)
592
592
  //
593
- // PF-014: no process.exit() inside any scope that has pending cleanup or
593
+ // No process.exit() inside any scope that has pending cleanup or
594
594
  // buffered output. main() returns a numeric code for error paths or an
595
595
  // object {scrubLine} for the success path. The single top-level boundary
596
596
  // writes stdout synchronously and sets process.exitCode — nothing is
@@ -656,7 +656,7 @@ function readInput(inputPath) {
656
656
  function runFileMode(args, content) {
657
657
  const { result, counts } = scrub(content);
658
658
 
659
- // ---- atomic write (PF-011: temp-sibling + rename) ----
659
+ // ---- atomic write (temp-sibling + rename) ----
660
660
  const tmpPath = args.outputPath + '.tmp';
661
661
  try {
662
662
  fs.writeFileSync(tmpPath, result, 'utf8');
@@ -686,7 +686,7 @@ function runFileMode(args, content) {
686
686
  * prints them, so there is nothing for a write discipline to protect: a scrubbed
687
687
  * comment body put on disk is a second copy with the input directory's lifetime,
688
688
  * and proving that directory writable would let a filesystem property refuse a
689
- * clean, fully gated body. The file mode's temp-sibling + rename (PF-011) guards
689
+ * clean, fully gated body. The file mode's temp-sibling + rename guards
690
690
  * the one file this script does write.
691
691
  *
692
692
  * @param {string} content
@@ -762,7 +762,7 @@ function main(argv, deps) {
762
762
  //
763
763
  // This is the ONLY place that writes to stdout and sets process.exitCode.
764
764
  // No other code path may call process.exit() or write to stdout.
765
- // (PF-014: single synchronous write, no pending cleanup, no buffered output)
765
+ // (single synchronous write, no pending cleanup, no buffered output)
766
766
  //
767
767
  // AMENDED for --emit, not bypassed. main()'s return widened from
768
768
  // `number | {scrubLine}` to also carry `{emitLine, body, code}`, and the write
@@ -445,7 +445,7 @@ function isChangelogOnly(paths) {
445
445
  /**
446
446
  * D-TRACE-CLASSIFY: one first-parent commit's class. First match wins, in
447
447
  * CLASSES order, and the terminal arm is `untraced` — an input no rule
448
- * recognises surfaces in the confirm rather than passing silently (avoids PF-075).
448
+ * recognises surfaces in the confirm rather than passing silently.
449
449
  *
450
450
  * @param {Commit} commit
451
451
  * @param {ClassifyContext} ctx
@@ -11,7 +11,7 @@
11
11
  // The policy is plumbing, decided once by the caller: this script prints it plus
12
12
  // the mechanism inputs, and operations only ever see the inputs. It WRITES NOTHING
13
13
  // — no file, no git ref, no remote state — so `.devflow/project.json` stays a
14
- // team-owned file that only the team commits (applies ADR-024). A committed
14
+ // team-owned file that only the team commits. A committed
15
15
  // `.devflow/policy.json` is detected by presence and never parsed
16
16
  // (D-POLICY-JSON-RETIRED).
17
17
  //
@@ -432,7 +432,7 @@ function readManifestCompliance() {
432
432
  * starting (ENOENT), timing out, overflowing its buffer or being killed. Only an
433
433
  * answered non-zero exit is a real "no" ("not a repository", "no such path",
434
434
  * "no such ref"); an unanswered call is NOT KNOWING, and a local-git step that
435
- * does not know must not read as the permissive answer (avoids PF-075).
435
+ * does not know must not read as the permissive answer.
436
436
  *
437
437
  * @param {CallResult} r
438
438
  * @returns {boolean}
@@ -621,7 +621,7 @@ function lsRemoteDefaultBranch(ctx, root) {
621
621
  * unreadable git did not answer, or origin/HEAD exists but its answer names
622
622
  * no safe branch (another remote, a hostile or unparseable name).
623
623
  * Either is a failure, never the residual case: gatherFacts reads
624
- * it as an invalid base, which resolves required (avoids PF-075).
624
+ * it as an invalid base, which resolves required.
625
625
  * resolve-settings' defaultBranchCompliance fails the same answer
626
626
  * closed, to `generic`.
627
627
  *
@@ -35,7 +35,7 @@
35
35
  // publication can only lower it (D-PUBLICATION-CEILING) — a branch's
36
36
  // `reviewPublication: "full"` raises nothing, since only a personal value asks
37
37
  // for more than `auto`. The tracker (provider, site, key) is taken as the
38
- // branch states it. It WRITES NOTHING (applies ADR-024).
38
+ // branch states it. It WRITES NOTHING: each layer it folds is changed only by its owner.
39
39
  //
40
40
  // stdout is exactly one line plus "\n", or empty (D-SETTINGS-LINE):
41
41
  // TRACKER=<github|jira|linear> TRACKER_SOURCE=<project|personal|machine|default>
@@ -750,7 +750,7 @@ function readRepoLayers(root, deps) {
750
750
  * (D-LENS-UNION, D-SETTINGS-LOCAL-ONLY): origin/HEAD names the branch D, and
751
751
  * `refs/remotes/origin/<D>:.devflow/project.json` is read through the same
752
752
  * parser. The lens only adds, so every state that cannot be read is a malformed
753
- * declaration (`generic`), never "declares nothing" (avoids PF-075):
753
+ * declaration (`generic`), never "declares nothing":
754
754
  * absent origin/HEAD is not recorded, or D holds no project.json
755
755
  * malformed a git call did not answer, origin/HEAD names no safe branch, the
756
756
  * blob overflows its bound, the file is unreadable, or its
@@ -898,7 +898,7 @@ function settleOutcome(outcome) {
898
898
  }
899
899
 
900
900
  // ---------------------------------------------------------------------------
901
- // The suggestion the CLI prints (never writes — ADR-024)
901
+ // The suggestion the CLI prints (never writes — the team commits it)
902
902
  // ---------------------------------------------------------------------------
903
903
 
904
904
  /**
@@ -459,7 +459,7 @@ function runCall(io, file, args, timeout, maxBuffer) {
459
459
  /**
460
460
  * Whether a call ran to completion and exited on its own. Only an answered exit
461
461
  * is a real "no"; a call that never started, timed out, overflowed or was refused
462
- * is NOT KNOWING, and never reads as the permissive answer (avoids PF-075).
462
+ * is NOT KNOWING, and never reads as the permissive answer.
463
463
  *
464
464
  * @param {CallResult} r
465
465
  * @returns {boolean}
@@ -1530,7 +1530,7 @@ function gateEvidenceLine(deps, fields, total) {
1530
1530
  }
1531
1531
 
1532
1532
  // ---------------------------------------------------------------------------
1533
- // splice — the compare-and-swap (D-VERIFY-CAS, applies ADR-023 and ADR-024)
1533
+ // splice — the compare-and-swap (D-VERIFY-CAS)
1534
1534
  // ---------------------------------------------------------------------------
1535
1535
 
1536
1536
  /**
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  name: apply-decisions
3
- description: Canonical algorithm for consuming DECISIONS_CONTEXT index — scan index, identify relevant entries, Read full bodies on demand, cite verbatim IDs inline.
3
+ description: Canonical algorithm for consuming DECISIONS_CONTEXT index — scan index, identify relevant entries, Read full bodies on demand, cite verbatim IDs in-session and state the rule in words in anything committed or posted.
4
4
  user-invocable: false
5
5
  allowed-tools: Read
6
6
  ---
7
7
 
8
8
  # Apply Decisions
9
9
 
10
- Canonical consumer algorithm for the `DECISIONS_CONTEXT` index passed by orchestrators. The index lists each ADR/PF entry with ID, truncated title, status, and area — not the full body. Use this skill to surface the right decisions and pitfalls for your task without loading the entire corpus.
10
+ Canonical consumer algorithm for the `DECISIONS_CONTEXT` index passed by orchestrators. The index lists each active ADR/PF entry with its ID and title — plus a status tag and an area on a v1 line, or the entry's scope on a v2 line — not the full body. Use this skill to surface the right decisions and pitfalls for your task without loading the entire corpus.
11
11
 
12
12
  ## Iron Law
13
13
 
@@ -23,35 +23,52 @@ Canonical consumer algorithm for the `DECISIONS_CONTEXT` index passed by orchest
23
23
 
24
24
  ### Step 1: Scan the index
25
25
 
26
- Read through all entries in `DECISIONS_CONTEXT`. The index format is:
26
+ Read through all entries in `DECISIONS_CONTEXT`. The index lists active entries only, in two line kinds:
27
27
 
28
28
  ```
29
29
  Decisions (N):
30
- ADR-001 Title truncated to 60 chars [Active]
31
- ADR-002 Another decision [Active]
30
+ ADR-NNN Return Result types from every fallible operation [Accepted]
31
+ ADR-NNN Claim the learning queue with an op, never with mv — src/assets/scripts/hooks/**, area:learning
32
32
 
33
33
  Pitfalls (M):
34
- PF-004 Background hook god scripts [Active] — src/assets/scripts/hooks/foo.cjs
35
- PF-011 DECISIONS_CONTEXT fan-out [Active] — src/assets/commands/...
34
+ PF-NNN Background hook god scripts [Active] — src/assets/scripts/hooks/foo.cjs
35
+ PF-NNN A rename claims a shared file only while nothing re-creates it — area:hooks
36
36
 
37
37
  ADR-NNN entries live in {worktree}/.devflow/learning/decisions.md
38
38
  PF-NNN entries live in {worktree}/.devflow/learning/pitfalls.md
39
39
  Read the relevant file and locate the matching `## ADR-NNN:` or `## PF-NNN:` heading for the full body.
40
40
  ```
41
41
 
42
+ - **A v1 line** ends in a status tag — `[Accepted]` on a decision, `[Active]` on a pitfall — after a title cut to 60 characters, and a pitfall adds its area after `—`.
43
+ - **A v2 line** has no tag. Its title is whole, and after `—` comes its scope: the globs and `area:` tags the rule governs, cut to 80 characters.
44
+
42
45
  ### Step 2: Identify plausibly-relevant entries
43
46
 
44
- From the index, identify entries whose title or area plausibly overlaps with:
45
- - The files you are modifying or reviewing
47
+ From the index, identify entries whose title, area or scope plausibly overlaps with:
48
+ - The files you are modifying or reviewing — a v2 scope glob that matches one of them is a strong signal
46
49
  - The category of issue you are addressing (e.g., error handling, hook scripts, JSON parsing)
47
50
  - The architectural area of your change
48
51
 
49
- Titles are truncated to 60 characters — if a truncated title looks relevant, proceed to Step 3.
52
+ A v1 title may be cut short — if a truncated title looks relevant, proceed to Step 3.
50
53
 
51
54
  ### Step 3: Read the full body
52
55
 
53
56
  For each plausibly-relevant entry, use the Read tool to open the decisions file listed in the `DECISIONS_CONTEXT` footer and locate the matching `## ADR-NNN:` or `## PF-NNN:` heading. Read the full section to confirm relevance and understand the decision or pitfall completely.
54
57
 
58
+ A v2 body reads:
59
+
60
+ ```
61
+ ## ADR-NNN: {title}
62
+
63
+ - **Status**: Accepted · verified {date}
64
+ - **Scope**: `{glob}`, `area:{tag}`
65
+ - **Decision**: {the rule}
66
+ - **Why**: {why it holds}
67
+ - **Source**: {where it was learned}
68
+ ```
69
+
70
+ A pitfall's Status is `Active` and its rule line is labelled `**Rule**`. The verified date is the day the entry was last confirmed true; a body without one has not been confirmed since it was written. A v1 body carries `**Date**`, `**Status**`, `**Context**`, `**Decision**` and `**Consequences**` for a decision, or `**Area**`, `**Issue**`, `**Impact**`, `**Resolution**` and `**Status**` for a pitfall.
71
+
55
72
  **The footer is the single source of truth for file paths.** Never substitute hardcoded paths — the footer resolves to the correct worktree, which may differ from your cwd in multi-worktree flows.
56
73
 
57
74
  ```
@@ -62,9 +79,11 @@ Use the exact paths from the DECISIONS_CONTEXT footer, e.g.:
62
79
 
63
80
  Only cite an entry after you have read its full body and confirmed it applies.
64
81
 
65
- ### Step 4: Cite inline
82
+ ### Step 4: Cite inline — in-session handoffs only
83
+
84
+ When applying a prior decision, cite as `applies ADR-NNN`. When avoiding a known pitfall, cite as `avoids PF-NNN`. Place these citations only where they stay in the session: your reasoning, decision tables, prompts to downstream agents, and your report back to the caller.
66
85
 
67
- When applying a prior decision, cite as `applies ADR-NNN` in your reasoning or output. When avoiding a known pitfall, cite as `avoids PF-NNN`. Place citations in the Reasoning column of decision tables, in inline comments, or in your structured output — wherever your agent's output format captures rationale.
86
+ Anything committed, pushed or posted states the rule in words, never its ID: code, comments, tests, docs, KNOWLEDGE.md files, commit messages, PR and issue text, review and PR comments, resolution summaries — and any report a later step copies into one of those. The ledger is gitignored and numbered per machine, so no other clone can resolve an ID, and numbers get reused.
68
87
 
69
88
  ### Step 5: Use verbatim IDs only
70
89
 
@@ -76,17 +95,17 @@ Cite only IDs that appear verbatim in `DECISIONS_CONTEXT`. Do not guess at IDs t
76
95
 
77
96
  **Scenario**: Reviewing `src/assets/scripts/hooks/background-learning` for issues.
78
97
 
79
- 1. **Scan** — Index shows `PF-004 Background hook god scripts [Active] — src/assets/scripts/hooks/foo.cjs`
98
+ 1. **Scan** — Index shows `PF-NNN Background hook god scripts [Active] — src/assets/scripts/hooks/foo.cjs`
80
99
  2. **Identify** — Area field includes `src/assets/scripts/hooks/` which overlaps with the file under review
81
- 3. **Read** — Open the pitfalls file at the path given in the DECISIONS_CONTEXT footer (e.g., `<worktree>/.devflow/learning/pitfalls.md`), find `## PF-004:` section, read full body
82
- 4. **Cite** — If the file shows signs of the god-script pattern, note `avoids PF-004` in reasoning
83
- 5. **Verbatim** — ID `PF-004` appeared in the index; citation is valid
100
+ 3. **Read** — Open the pitfalls file at the path given in the DECISIONS_CONTEXT footer (e.g., `<worktree>/.devflow/learning/pitfalls.md`), find `## PF-NNN:` section, read full body
101
+ 4. **Cite** — If the file shows signs of the god-script pattern, note `avoids PF-NNN` in reasoning; a comment you commit for the fix says why in words ("hooks stay thin dispatchers"), never the ID
102
+ 5. **Verbatim** — ID `PF-NNN` appeared in the index; citation is valid
84
103
 
85
104
  ---
86
105
 
87
106
  ## Skip Guard
88
107
 
89
- When `DECISIONS_CONTEXT` is empty, `(none)`, or not provided: skip this skill entirely. Do not attempt to load decisions files independently. Do not speculate about what decisions or pitfalls might exist.
108
+ When `DECISIONS_CONTEXT` is empty, `(none)`, or not provided: skip this skill entirely — unless your agent instructions tell you to read the decisions index yourself, in which case the index you read is your `DECISIONS_CONTEXT`. Do not otherwise load decisions files independently. Do not speculate about what decisions or pitfalls might exist.
90
109
 
91
110
  ---
92
111
 
@@ -98,3 +117,4 @@ When `DECISIONS_CONTEXT` is empty, `(none)`, or not provided: skip this skill en
98
117
  | Avoiding a known pitfall | `avoids PF-NNN` |
99
118
  | Entry not in index | (no citation — silence is correct) |
100
119
  | Entry in index but not read yet | (no citation — read first) |
120
+ | Text that is committed, pushed or posted | (no ID — state the rule in words) |
@@ -140,8 +140,8 @@ ensure_docs_dir() { mkdir -p "$(get_docs_root)/.devflow/docs/$1"; }
140
140
  | Resolve cmd | `.devflow/docs/reviews/{branch-slug}/{timestamp}/resolution-summary.md` | Written by /resolve orchestrator (Phase 5) |
141
141
  | Code-review cmd | `.devflow/docs/reviews/{branch-slug}/.last-review-head` | Overwrites with HEAD SHA |
142
142
  | Working Memory | `.devflow/memory/WORKING-MEMORY.md` | Overwrites (auto-maintained by Stop hook) |
143
- | Decisions | `.devflow/learning/decisions.md` | Rendered from `decisions-ledger.jsonl` (active ADR-NNN rows; retired rows dropped) |
144
- | Pitfalls | `.devflow/learning/pitfalls.md` | Rendered from `decisions-ledger.jsonl` (active PF-NNN rows; retired rows dropped) |
143
+ | Decisions | `.devflow/learning/decisions.md` | Rendered from `decisions-ledger.jsonl` (active ADR-NNN entries in full; inactive ones listed in its Inactive table) |
144
+ | Pitfalls | `.devflow/learning/pitfalls.md` | Rendered from `decisions-ledger.jsonl` (active PF-NNN entries in full; inactive ones listed in its Inactive table) |
145
145
  | Design agent (via /plan) | `.devflow/docs/design/{ISSUE_ID}-{topic-slug}.{timestamp}.md` | Creates new design artifact |
146
146
  | Research agent | `.devflow/docs/research/{topic-slug}/{timestamp}/{type}.md` | Creates new in timestamped dir |
147
147
  | Synthesize agent (research) | `.devflow/docs/research/{topic-slug}/{timestamp}/research-summary.md` | Creates new in timestamped dir |
@@ -117,7 +117,7 @@ updated: {ISO date}
117
117
  [Most important files with one-line descriptions]
118
118
 
119
119
  ## Related
120
- [Links to ADR/PF entries, other feature knowledge entries, key source files]
120
+ [Links to other feature knowledge entries and key source files — never an ADR/PF ID]
121
121
  ```
122
122
 
123
123
  ### Category Templates
@@ -167,9 +167,10 @@ Bad: `"Integration stuff"`
167
167
 
168
168
  Knowledge files must not exist in isolation. The **Related** section must link to:
169
169
  - Other feature knowledge entries that cover related topics
170
- - ADR/PF entries from DECISIONS_CONTEXT (if provided)
171
170
  - Key source files referenced in the knowledge
172
171
 
172
+ A decision or pitfall from DECISIONS_CONTEXT that shapes this area is stated in words in the section it governs, never by its ADR/PF ID: KNOWLEDGE.md is git-tracked and shared with every clone, and ledger IDs resolve only on the machine that recorded them.
173
+
173
174
  References should be bidirectional — when creating a new feature knowledge entry that relates to an existing one, note the connection.
174
175
 
175
176
  ---
@@ -230,7 +231,7 @@ Run through this before writing. If any check fails, go back and fix it.
230
231
  - [ ] File stays under 500 lines (split if necessary)
231
232
 
232
233
  **Connections:**
233
- - [ ] Cross-references to related feature knowledge entries and ADR/PF entries in Related section
234
+ - [ ] Cross-references to related feature knowledge entries in Related section; decisions and pitfalls stated in words, with no ADR/PF ID anywhere in the file
234
235
  - [ ] No isolated knowledge islands — file connects to the broader knowledge network
235
236
  - [ ] Key source files listed in Key Files section
236
237
 
@@ -325,8 +326,8 @@ All constants use `SCREAMING_SNAKE_CASE` with a descriptive prefix. If a value c
325
326
 
326
327
  ## Related
327
328
 
328
- - ADR-003: Lib/command separation principle
329
- - PF-001: Config drift when constants are scattered
329
+ - `.devflow/features/cli-commands/KNOWLEDGE.md` — the command side of the lib/command split: catching errors and routing them to `showError()`
330
+ - `src/commands/` — where each integration is wired into a command
330
331
  ````
331
332
 
332
333
  ---
@@ -1,50 +0,0 @@
1
- import { promises as fs } from 'fs';
2
- import * as p from '@clack/prompts';
3
- import { writeFileAtomicExclusive } from './fs-atomic.js';
4
- import { loadAndCountObservations } from './observations.js';
5
- /**
6
- * @file observation-io.ts
7
- *
8
- * File I/O for observations and user-facing warnings.
9
- * Bridges the pure data module (observations.ts) with the filesystem.
10
- *
11
- * The `.md` files are a pure render of the decisions ledger — never edit them
12
- * directly. To change the status of a decision or pitfall, use the
13
- * `retire-anchor` op in `json-helper.cjs`, which flips `decisions_status` on
14
- * the ledger row and re-renders both `.md` files atomically.
15
- */
16
- /**
17
- * Read and parse observations from a log file.
18
- * Returns empty results if the file does not exist.
19
- */
20
- export async function readObservations(logPath) {
21
- try {
22
- const logContent = await fs.readFile(logPath, 'utf-8');
23
- return loadAndCountObservations(logContent);
24
- }
25
- catch {
26
- return { observations: [], invalidCount: 0 };
27
- }
28
- }
29
- /**
30
- * Write observations back to a log file atomically.
31
- * Each observation is serialized as a JSON line. Uses a `.tmp` sibling + rename so
32
- * concurrent readers (e.g. background-learning during a race) never observe a
33
- * half-written file. Delegates to `writeFileAtomicExclusive` in fs-atomic.ts
34
- * (D34/D39: canonical TS atomic-write helper).
35
- */
36
- export async function writeObservations(logPath, observations) {
37
- const lines = observations.map(o => JSON.stringify(o));
38
- const content = lines.join('\n') + (lines.length ? '\n' : '');
39
- await writeFileAtomicExclusive(logPath, content);
40
- }
41
- /**
42
- * Warn the user if invalid entries were found in a log file.
43
- * Invalid entries are cleaned up automatically by the background curation pass.
44
- */
45
- export function warnIfInvalid(invalidCount) {
46
- if (invalidCount > 0) {
47
- p.log.warn(`Note: ${invalidCount} invalid entry(ies) found. They will be cleaned up automatically.`);
48
- }
49
- }
50
- //# sourceMappingURL=observation-io.js.map