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
@@ -56,7 +56,7 @@ conflict the contract wins.
56
56
  Headings below each section's own anchor are `###` by grammar, not by taste: a
57
57
  column-0 `## ` line outside a fence terminates the section for every guard that
58
58
  reads it through `extractOpSectionFromCorpus`, and everything under it becomes
59
- invisible while the bytes stay on disk (PF-063). The one `## ` heading in this
59
+ invisible while the bytes stay on disk. The one `## ` heading in this
60
60
  file is the module-level section below, which sits above every section marker and
61
61
  is therefore emitted nowhere.
62
62
 
@@ -218,13 +218,13 @@ Over `{{comment_cap()}}` characters the rolling item is closed and a successor i
218
218
 
219
219
  Load when the resolved tracker provider is `linear` and the operation is `create-release`.
220
220
 
221
- **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.
221
+ **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.
222
222
 
223
223
  ### Process
224
224
 
225
225
  Inside step 5 (compose release notes):
226
226
 
227
- - 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).
227
+ - 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).
228
228
  - {{common.ref_preflight_list("linear", "**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}$`", "**ASCII-upper-normalise every entry first.** ", ", never joined into one alternation", " and the section is omitted rather than rendered empty.")}}
229
229
  - `## 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`.
230
230
 
@@ -32,16 +32,17 @@ extraction that turns a universal obligation into per-consumer opt-in is the
32
32
  defect, not the saving.
33
33
 
34
34
  The preamble names it on the SAME physical line that composes the per-operation
35
- mechanics path, which is what keeps PF-023's single convergence point at exactly
36
- one line. That is sound rather than a loophole: what PF-023 counts is where a path
37
- is BUILT, and this one is a fixed literal built from nothing — the validated
35
+ mechanics path, which is what keeps the single convergence point (validation lives
36
+ at the one sink every path passes through, not at each entry) at exactly one line.
37
+ That is sound rather than a loophole: what that rule counts is where a path is
38
+ BUILT, and this one is a fixed literal built from nothing — the validated
38
39
  provider token selects the mechanics directory and never reaches this name. A
39
40
  per-operation file naming it again is forbidden, and asserted as forbidden.
40
41
 
41
42
  Headings below the first are `###` by grammar, not by taste: a column-0 `## `
42
43
  line outside a fence terminates this file's section for every guard that reads it
43
44
  through `extractOpSectionFromCorpus`, and everything under it becomes invisible
44
- while the bytes stay on disk (PF-063).
45
+ while the bytes stay on disk.
45
46
 
46
47
  TWO ROLES, AND THE SECOND ONE IS EMITTED NOWHERE. `@define tool_call_contract()`
47
48
  below is the artifact: it becomes `tracker/_mcp.md` and is read once per spawn.
@@ -81,7 +82,7 @@ to `_common.mds` cost 1.2 s in total. A rule that belongs here by subject and
81
82
  would be the tenth define belongs in `_common.mds` with its audience stated at
82
83
  the define, and this paragraph is the reason.
83
84
 
84
- NOTHING MORE GOES INTO THIS MODULE (PF-073). Anything further that wants
85
+ NOTHING MORE GOES INTO THIS MODULE. Anything further that wants
85
86
  hoisting goes to `_common.mds`, or this module shrinks first; and every importer
86
87
  reaches it by ALIAS (`as mcp`), never by a selective import, whose deep copy per
87
88
  importing define is the same cliff. `tests/build-mds-compile-time.test.ts` holds
@@ -1,7 +1,7 @@
1
1
  --- ORCHESTRATOR CHARTER ---
2
2
  You are the orchestrator of this session: you coordinate, agents produce.
3
3
 
4
- Never do work-product mainline: no file edits, no builds, no multi-file reads, no codebase orientation, no debug loops. Delegate all of it.
4
+ Never do work-product mainline: no file edits, no builds, no multi-file reads, no codebase orientation, no debug loops. Delegate all of it, with one bounded exception: the main thread may run only a single git, gh or script command whose output stays under about 40 lines, and never a diff, log or test run.
5
5
 
6
6
  Routing (Agent tool) — pick the roster agent that fits the work:
7
7
  - Search and listing: Explore. Codebase orientation: Skim.
@@ -14,6 +14,8 @@ Stays mainline (judgment work): conversation, decisions, routing, synthesizing a
14
14
  Operating rules:
15
15
  - Decompose mainline. Subagents cannot spawn subagents — you own task breakdown, then delegate leaf tasks.
16
16
  - Subagents see none of this conversation. Make every delegation self-contained: goal, constraints, relevant session decisions and facts, exact paths. A deliverable that draws on the conversation (issue, PR, report) needs the substance in the prompt — not a pointer to it.
17
+ - Report cap: ask every direct delegation for a final report of at most about 1,500 tokens — findings, paths and verdicts, not file dumps.
17
18
  - Parallelize independent delegations in one message. Git operations stay sequential.
18
19
  - Feature knowledge (direct delegations only — workflow skills handle their own): before delegating non-trivial code work, match the task area against .devflow/features/index.md and pass matching KNOWLEDGE.md content as FEATURE_KNOWLEDGE; after delegated changes to a covered area, spawn Knowledge to refresh that KB.
19
- - Plan handoff: if the user's first message begins with `Implement the following plan:`, say so in one sentence, then immediately invoke devflow:implement via the Skill tool with the full plan. Do not pause to ask.
20
+ - Decisions (direct delegations only — workflow skills load their own): pass the index named under PROJECT DECISIONS as DECISIONS_CONTEXT — its content, read once — to every agent that takes it.
21
+ - Plan handoff: if the user's first message begins with `Implement the following plan:`, say so in one sentence, then immediately invoke devflow:implement via the Skill tool with no arguments — the plan is already in this conversation. Do not pause to ask.
@@ -2,12 +2,12 @@
2
2
 
3
3
  # background-memory-update — Eager working-memory refresh worker
4
4
  # Spawned as a detached background process by memory-worker (Stop hook) after the
5
- # 120s throttle expires. Drains .pending-turns.jsonl → calls `claude -p` (sonnet 4.6) →
5
+ # 120s throttle expires. Drains .pending-turns.jsonl → calls `claude -p` (sonnet 5.5) →
6
6
  # rewrites WORKING-MEMORY.md with a git-reconciliation stamp on line 1.
7
7
  #
8
- # Applies ADR-008 (LLM-vs-plumbing): artifact content (WORKING-MEMORY.md) is
8
+ # Applies the LLM-vs-plumbing principle: artifact content (WORKING-MEMORY.md) is
9
9
  # authored by the LLM; this script only does plumbing (lock, queue drain, spawn).
10
- # Avoids PF-006 (does NOT parse Stop hook JSON), PF-007 (edit source only).
10
+ # Does NOT parse Stop hook JSON. Edit this source file, not the installed copy.
11
11
  #
12
12
  # Usage: background-memory-update <CWD> [<manifest_path>]
13
13
  # <manifest_path> — the devflow-global manifest whose `features.memory` is the
@@ -73,7 +73,7 @@ MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
73
73
  QUEUE_FILE="$MEMORY_DIR/.pending-turns.jsonl"
74
74
  PROCESSING_FILE="$MEMORY_DIR/.pending-turns.processing"
75
75
  MEMORY_FILE="$MEMORY_DIR/WORKING-MEMORY.md"
76
- STAGED_FILE="$MEMORY_FILE.new" # staging path for CAS write (applies ADR-023)
76
+ STAGED_FILE="$MEMORY_FILE.new" # staging path for CAS write (D-MEMORY-STAGED-CAS)
77
77
  LOCK_DIR="$MEMORY_DIR/.working-memory.lock"
78
78
  TRIGGER_FILE="$MEMORY_DIR/.working-memory-last-trigger"
79
79
  OK_FILE="$MEMORY_DIR/.last-refresh-ok"
@@ -99,13 +99,25 @@ fi
99
99
  # --- Assert cksum availability (required for CAS verification — avoids fail-open swap) ---
100
100
  # cksum must be on PATH at startup; a missing binary makes both CAS sentinels collapse
101
101
  # to the same "ABSENT" literal, compare equal, and swap unconditionally — reinstating
102
- # the exact clobber ADR-023 was designed to prevent. Fail loudly here rather than
103
- # silently degrading. applies ADR-023
102
+ # the exact clobber of a human edit the CAS was designed to prevent
103
+ # (D-MEMORY-STAGED-CAS). Fail loudly here rather than silently degrading.
104
104
  if ! command -v cksum >/dev/null 2>&1; then
105
105
  log "SKIP: cksum not on PATH — CAS verification unavailable, refusing to write"
106
106
  exit 0
107
107
  fi
108
108
 
109
+ # --- Never write through a symbolic link (D-HOOKS-NO-SYMLINK, git-marker) ---
110
+ # The lock (`mkdir`), the batch merge (`>>`), the claim heartbeat and the success
111
+ # stamp (`touch`) write these paths in place; the claim and the trim rename onto the
112
+ # batch and the swap onto the working memory, and `mv` moves a file into the folder
113
+ # a link at its target names. The trim's copy is created only where nothing stands.
114
+ # So the run is skipped, before it takes the lock or starts an LLM run, when any of
115
+ # these paths or a folder above them is a link.
116
+ if ! df_no_symlink_below "$PROJECT_ROOT" "$LOCK_DIR" "$QUEUE_FILE" "$PROCESSING_FILE" "$OK_FILE" "$MEMORY_FILE"; then
117
+ log "SKIP: a symbolic link sits on the path to the memory files in $MEMORY_DIR; nothing written"
118
+ exit 0
119
+ fi
120
+
109
121
  # --- Worker-level lock (300s stale-break — much longer than learning-lock's 30s) ---
110
122
  # This prevents a second worker (spawned 121s later) from double-writing WORKING-MEMORY.md
111
123
  # while the first worker's claude -p call (up to 120s) is still in flight.
@@ -158,7 +170,7 @@ fi
158
170
 
159
171
  # Clean up any staged file left by a watchdog-killed prior run.
160
172
  # Without this, a stale staged file with a valid stamp would be mistakenly
161
- # mv-ed to the real path on the NEXT run's CAS check (applies ADR-023).
173
+ # mv-ed to the real path on the NEXT run's CAS check (D-MEMORY-STAGED-CAS).
162
174
  rm -f "$STAGED_FILE" 2>/dev/null || true
163
175
 
164
176
  # --- Orphan-only skip: if queue has no assistant/qa turn, exit and leave the queue ---
@@ -180,7 +192,7 @@ if [ ! -f "$PROCESSING_FILE" ] && [ -f "$QUEUE_FILE" ] && [ -s "$QUEUE_FILE" ] &
180
192
  _HAS_CONTENT=$(jq -r 'select(.role=="assistant" or .role=="qa") | .role' "$QUEUE_FILE" 2>/dev/null | head -1 || echo "")
181
193
  else
182
194
  # SECURITY: pass path via argv, never interpolate into node -e source (avoids shell injection
183
- # for repo paths containing quotes or special characters — applies ADR-008 plumbing principle)
195
+ # for repo paths containing quotes or special characters)
184
196
  _HAS_CONTENT=$(node -e '
185
197
  const f = process.argv[1];
186
198
  const lines = require("fs").readFileSync(f, "utf8").split("\n");
@@ -210,8 +222,20 @@ if [ -f "$PROCESSING_FILE" ]; then
210
222
  _PROC_LINES=$(wc -l < "$PROCESSING_FILE" | tr -d ' ')
211
223
  if [ "$_PROC_LINES" -gt "$PROC_OVERFLOW_CAP" ]; then
212
224
  _PROC_TMP="$PROCESSING_FILE.tmp.$$"
213
- tail -"$PROC_OVERFLOW_TARGET" "$PROCESSING_FILE" > "$_PROC_TMP" && mv "$_PROC_TMP" "$PROCESSING_FILE" || rm -f "$_PROC_TMP" 2>/dev/null || true
214
- log "Processing overflow: truncated from $_PROC_LINES to $PROC_OVERFLOW_TARGET lines"
225
+ # SEC-2: the batch is the claimed queue, 0600 like the queue itself, and the
226
+ # cold path at session start moves it back to the queue. mv gives it the
227
+ # trimmed copy's inode and mode, so the copy is created under umask 077, and
228
+ # only where nothing stands at its name: builtin tests come first, so an entry
229
+ # already there, a link to a FIFO or a device included, is never opened
230
+ # (noclobber alone would open one, as no regular file stands there), and
231
+ # noclobber then makes the create itself exclusive. Such an entry is removed
232
+ # instead, and the batch is left untrimmed.
233
+ if [ ! -e "$_PROC_TMP" ] && [ ! -L "$_PROC_TMP" ] && (umask 077 && set -o noclobber && tail -"$PROC_OVERFLOW_TARGET" "$PROCESSING_FILE" > "$_PROC_TMP") && mv "$_PROC_TMP" "$PROCESSING_FILE"; then
234
+ log "Processing overflow: truncated from $_PROC_LINES to $PROC_OVERFLOW_TARGET lines"
235
+ else
236
+ rm -f "$_PROC_TMP" 2>/dev/null || true
237
+ log "Processing overflow: not truncated, its copy could not be written ($_PROC_LINES lines)"
238
+ fi
215
239
  fi
216
240
  elif [ -f "$QUEUE_FILE" ]; then
217
241
  mv "$QUEUE_FILE" "$PROCESSING_FILE" 2>/dev/null || { log "SKIP: failed to claim queue (race condition — another worker got it)"; exit 0; }
@@ -330,37 +354,44 @@ fi
330
354
 
331
355
  log "Built $TURN_COUNT turns from queue"
332
356
 
333
- # --- Capture pre-run cksum baseline + read existing memory ---
357
+ # --- Capture pre-run cksum baseline (D-MEMORY-STAGED-CAS) + read existing memory ---
334
358
  # Baseline captured BEFORE content read so the cksum reflects the exact bytes we
335
359
  # synthesised from. ABSENT sentinel when file missing — resolves toward false-conflict,
336
360
  # never false-success (a file created externally during the run triggers CONFLICT,
337
- # which is safer than accepting a write we did not produce). applies ADR-023.
361
+ # which is safer than accepting a write we did not produce).
338
362
  #
339
363
  # CKSUM_FAILED: if cksum invocation fails (EACCES, missing binary for this path, etc.)
340
364
  # on either side, the CAS must treat it as CONFLICT rather than a match — separating
341
365
  # the "cksum invocation failure" sentinel from "file absent" keeps the "resolves toward
342
366
  # false-conflict, never false-success" invariant true even when cksum errors.
367
+ #
368
+ # D-HOOKS-NO-SYMLINK (git-marker): the existing memory goes into the prompt, so a
369
+ # working memory a symbolic link leads to is treated as absent. A link at its own
370
+ # path has already skipped the run, since the swap renames onto it (above), so
371
+ # only one that appeared since can reach this read.
372
+ MEMORY_PRESENT=""
373
+ if df_file_below "$PROJECT_ROOT" "$MEMORY_FILE"; then MEMORY_PRESENT="yes"; fi
343
374
  CKSUM_FAILED="false"
344
375
  PRE_RUN_CKSUM="ABSENT"
345
- if [ -f "$MEMORY_FILE" ]; then
376
+ if [ -n "$MEMORY_PRESENT" ]; then
346
377
  PRE_RUN_CKSUM=$(cksum "$MEMORY_FILE" 2>/dev/null) || CKSUM_FAILED="true"
347
378
  fi
348
379
 
349
380
  EXISTING_MEMORY=""
350
381
  MEMORY_READ_LIMIT=65536 # bytes — keep prompt under context limit
351
- if [ -f "$MEMORY_FILE" ]; then
382
+ if [ -n "$MEMORY_PRESENT" ]; then
352
383
  EXISTING_MEMORY=$(head -c "$MEMORY_READ_LIMIT" "$MEMORY_FILE")
353
384
  fi
354
385
 
355
386
  # Sets COMMITS_SINCE_NOTE in caller scope.
356
387
  # Reads EXISTING_MEMORY and HEAD_SHA from caller scope (must be set before call).
357
- # Pure parameter expansion for stamp parsing (PF-008-safe); git commands are guarded
358
- # with || echo / || true per set -e discipline (avoids PF-008 on pipeline exits).
388
+ # Pure parameter expansion for stamp parsing (no pipe whose exit status a later stage
389
+ # could mask); git commands are guarded with || echo / || true per set -e discipline.
359
390
  compute_commits_since_note() {
360
391
  local _stamp_line="${EXISTING_MEMORY%%$'\n'*}" _stamp_sha="" _rest
361
392
  case "$_stamp_line" in
362
393
  "<!-- memory-head: "[0-9a-f]*" branch: "*)
363
- # Extract stamp SHA using pure parameter expansion — no subprocess (PF-008-safe).
394
+ # Extract stamp SHA using pure parameter expansion — no subprocess.
364
395
  _rest="${_stamp_line#<!-- memory-head: }"
365
396
  _stamp_sha="${_rest%% *}"
366
397
  ;;
@@ -418,7 +449,7 @@ fi
418
449
 
419
450
  # --- Build prompt (passed via STDIN, not argv — turn content may hold secrets) ---
420
451
  # SECURITY: argv is visible to ps(1); all user/assistant content goes via stdin.
421
- # SECURITY: avoids PF-023 — each untrusted block is wrapped in named XML tags and preceded
452
+ # SECURITY: each untrusted block is wrapped in named XML tags and preceded
422
453
  # by an explicit containment declaration so injected prose cannot masquerade as operator
423
454
  # instructions regardless of positional ordering.
424
455
  PROMPT=$(cat <<EOF
@@ -469,7 +500,7 @@ Instructions:
469
500
  EOF
470
501
  )
471
502
 
472
- log "Spawning claude -p (model claude-sonnet-4-6, ${TURN_COUNT} turns)"
503
+ log "Spawning claude -p (model claude-sonnet-5-5, ${TURN_COUNT} turns)"
473
504
  # SECURITY: never log PROMPT — it contains turn content which may include secrets
474
505
 
475
506
  # --- Watchdog timeout (overridable for tests: DEVFLOW_BG_WATCHDOG_SECS=2) ---
@@ -481,7 +512,6 @@ WATCHDOG_KILL_GRACE_SECS=5 # grace period between SIGTERM and SIGKILL
481
512
  # recovery can never evict a still-live worker. If DEVFLOW_BG_WATCHDOG_SECS is raised above
482
513
  # 294 (leaving < 1s margin), this will fail loudly rather than silently corrupt the
483
514
  # safety guarantee. Checked before spawning claude so a bad override is caught early.
484
- # avoids PF-011
485
515
  _WATCHDOG_TOTAL=$(( WATCHDOG_SECS + WATCHDOG_KILL_GRACE_SECS ))
486
516
  if [ "$STALE_THRESHOLD" -le "$_WATCHDOG_TOTAL" ]; then
487
517
  echo "[background-memory-update] FATAL: STALE_THRESHOLD ($STALE_THRESHOLD) must exceed watchdog total (${WATCHDOG_SECS}+${WATCHDOG_KILL_GRACE_SECS}=${_WATCHDOG_TOTAL})" >&2
@@ -509,7 +539,7 @@ fi
509
539
  # Job-control notifications go to stderr which is redirected to the log — suppress them.
510
540
  set -m
511
541
  DEVFLOW_BG_UPDATER=1 "$CLAUDE_BIN" -p \
512
- --model claude-sonnet-4-6 \
542
+ --model claude-sonnet-5-5 \
513
543
  --dangerously-skip-permissions \
514
544
  --allowedTools 'Read,Write' \
515
545
  --output-format text \
@@ -547,7 +577,7 @@ if [ "$CLAUDE_EXIT" -gt 128 ]; then
547
577
  # Retry-count ceiling: this worker is rate-bounded to one spawn per 120s by memory-worker's
548
578
  # throttle (.working-memory-last-trigger mtime). Persistent failure is surfaced to the user
549
579
  # via session-start-memory State C (stale .last-refresh-ok) rather than a retry counter here,
550
- # keeping this plumbing script simple (applies ADR-008).
580
+ # keeping this plumbing script simple.
551
581
  exit 0
552
582
  elif [ "$CLAUDE_EXIT" -ne 0 ]; then
553
583
  log "FAIL: claude -p exited with code $CLAUDE_EXIT"
@@ -555,36 +585,58 @@ elif [ "$CLAUDE_EXIT" -ne 0 ]; then
555
585
  exit 0
556
586
  fi
557
587
 
558
- # --- CAS verification (applies ADR-023: staged compare-and-swap) ---
559
- # Only our own claude run can create STAGED_FILE between lock-acquire and here,
560
- # so verifying its content proves OUR write succeeded — as opposed to accepting
561
- # any mtime bump, which could come from a concurrent human edit of the real file.
588
+ # --- CAS verification (staged compare-and-swap) ---
589
+ # D-MEMORY-STAGED-CAS: claude never writes WORKING-MEMORY.md. The prompt names
590
+ # only STAGED_FILE (WORKING-MEMORY.md.new); this function checks that file and
591
+ # moves it into place only when the real file is byte-identical (cksum) to the
592
+ # pre-run read. A changed real file means someone edited it during the run:
593
+ # CONFLICT keeps their version, discards the staged file, leaves .last-refresh-ok
594
+ # untouched and keeps the batch in .processing for the next run. Reason: only our
595
+ # own claude run can create STAGED_FILE between lock-acquire and here, so a valid
596
+ # staged file proves OUR write succeeded. A check on the real file (an mtime bump,
597
+ # a stamped line 1) passes for a human's edit too, and taking that for success
598
+ # would delete an unprocessed batch; a direct write would also overwrite an edit
599
+ # made mid-run.
562
600
  #
563
601
  # Sets OUTCOME to one of: updated | conflict | failed
564
602
  # Reads PRE_RUN_CKSUM, CKSUM_FAILED, STAGED_FILE, MEMORY_FILE from caller scope.
565
603
  # Each state is assigned exactly once at the point it is decided (single OUTCOME
566
- # variable, three states). applies ADR-023
604
+ # variable, three states).
567
605
  verify_and_swap() {
568
- [ -f "$STAGED_FILE" ] && [ -s "$STAGED_FILE" ] || {
606
+ # A symbolic link at the staged path counts as no staged file (D-HOOKS-NO-SYMLINK,
607
+ # git-marker): `head` below would read the file it names, and the swap's `mv`
608
+ # would rename the link itself into the working memory's place. The worker
609
+ # removed that path before the LLM run, so a link there was planted during it.
610
+ # df_file_below checks it before both, and logs the refusal once.
611
+ df_file_below "$PROJECT_ROOT" "$STAGED_FILE" && [ -s "$STAGED_FILE" ] || {
569
612
  log "WARN: staged file missing or empty after claude -p run"; OUTCOME="failed"; return; }
570
613
  case "$(head -1 "$STAGED_FILE" 2>/dev/null || echo "")" in
571
614
  "<!-- memory-head:"*) : ;;
572
615
  *) log "WARN: staged file exists but stamp missing on line 1 — treating as failure"
573
616
  rm -f "$STAGED_FILE" 2>/dev/null || true; OUTCOME="failed"; return ;;
574
617
  esac
618
+ # A symbolic link on the working memory's path refuses the swap
619
+ # (D-HOOKS-NO-SYMLINK, git-marker): the run began without one, so a link there now
620
+ # is a change made during the run, and `mv` would move the staged memory into a
621
+ # folder it names. When the run began with no working memory, the cksum
622
+ # comparison below would take the link for that absent file.
623
+ if ! df_no_symlink_below "$PROJECT_ROOT" "$MEMORY_FILE"; then
624
+ rm -f "$STAGED_FILE" 2>/dev/null || true
625
+ log "CONFLICT: a symbolic link appeared on the path to $MEMORY_FILE during the run — not swapped, leaving .processing for retry"
626
+ OUTCOME="conflict"; return
627
+ fi
575
628
  # Re-check real file cksum before swapping.
576
629
  # Residual TOCTOU: a human edit landing in the O(ms) window between this
577
630
  # cksum re-check and the mv would be clobbered; this is accepted — the
578
631
  # staged content was synthesised from the pre-run file, so the next run
579
632
  # re-synthesises from the then-current content (no net information loss).
580
- # applies ADR-023 (staged compare-and-swap)
581
633
  local _post="ABSENT" _post_cksum_failed="false"
582
634
  if [ -f "$MEMORY_FILE" ]; then
583
635
  _post=$(cksum "$MEMORY_FILE" 2>/dev/null) || _post_cksum_failed="true"
584
636
  fi
585
637
  # If either cksum invocation failed, treat as conflict (fail-closed — resolves toward
586
638
  # false-conflict, never false-success; a cksum error must never silently degrade the
587
- # CAS to "always overwrite", reinstating the clobber ADR-023 exists to prevent).
639
+ # CAS to "always overwrite", reinstating the clobber of a human edit it exists to prevent).
588
640
  if [ "$CKSUM_FAILED" = "true" ] || [ "$_post_cksum_failed" = "true" ]; then
589
641
  rm -f "$STAGED_FILE" 2>/dev/null || true
590
642
  log "CONFLICT: cksum failed during CAS verification — leaving .processing for retry (fail-closed)"
@@ -607,14 +659,26 @@ verify_and_swap() {
607
659
  OUTCOME="failed"
608
660
  verify_and_swap
609
661
 
662
+ # Each stamp below is a `touch`, which follows a symbolic link, and its path was
663
+ # checked before the LLM run, which can take minutes. So, like the swap's target,
664
+ # it is checked again just before the touch: a link there now is a change made
665
+ # during the run, and the stamp is skipped and logged (D-HOOKS-NO-SYMLINK, git-marker).
610
666
  if [ "$OUTCOME" = "updated" ]; then
611
667
  rm -f "$PROCESSING_FILE" 2>/dev/null || true
612
- touch "$OK_FILE" 2>/dev/null || true
613
- log "SUCCESS: queue drained, .last-refresh-ok touched"
668
+ if df_no_symlink_below "$PROJECT_ROOT" "$OK_FILE"; then
669
+ touch "$OK_FILE" 2>/dev/null || true
670
+ log "SUCCESS: queue drained, .last-refresh-ok touched"
671
+ else
672
+ log "SUCCESS: queue drained; .last-refresh-ok not touched, a symbolic link appeared on its path during the run"
673
+ fi
614
674
  elif [ "$OUTCOME" = "conflict" ]; then
615
675
  # Heartbeat: this batch is still owned by the retry cycle, not orphaned.
616
676
  # Keeps session-start-memory's 300s cold path from reclaiming a live retry.
617
- touch "$PROCESSING_FILE" 2>/dev/null || true
677
+ if df_no_symlink_below "$PROJECT_ROOT" "$PROCESSING_FILE"; then
678
+ touch "$PROCESSING_FILE" 2>/dev/null || true
679
+ else
680
+ log "Heartbeat skipped: a symbolic link appeared on the path to $PROCESSING_FILE during the run"
681
+ fi
618
682
  log "CONFLICT: queue retained in .processing, .last-refresh-ok untouched"
619
683
  else
620
684
  # Leave .processing for session-start-memory's D56c cold path; do NOT touch .last-refresh-ok
@@ -84,11 +84,12 @@ source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
84
84
  source "$SCRIPT_DIR/get-mtime" || { echo "capture-prompt: failed to source get-mtime" >&2; exit 1; }
85
85
  source "$SCRIPT_DIR/learning-lock" || { echo "capture-prompt: failed to source learning-lock" >&2; exit 1; }
86
86
 
87
- mkdir -p "$MEMORY_DIR" "$LEARNING_DIR" 2>/dev/null || true
88
-
87
+ # Each queue is checked from the root it lies below, and its folder created there,
88
+ # by queue_append_row (D-HOOKS-NO-SYMLINK, git-marker).
89
89
  TS=$(date +%s)
90
90
  queue_append_both \
91
- "$MEMORY_DIR/.pending-turns.jsonl" "$LEARNING_DIR/.pending-turns.jsonl" \
91
+ "$PROJECT_ROOT" "$MEMORY_DIR/.pending-turns.jsonl" \
92
+ "$LEDGER_ROOT" "$LEARNING_DIR/.pending-turns.jsonl" \
92
93
  "$MEMORY_ENABLED" "$LEARNING_ENABLED" \
93
94
  "user" "$PROMPT" "$TS"
94
95
 
@@ -140,8 +140,8 @@ source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
140
140
  source "$SCRIPT_DIR/get-mtime" || { echo "capture-question: failed to source get-mtime" >&2; exit 1; }
141
141
  source "$SCRIPT_DIR/learning-lock" || { echo "capture-question: failed to source learning-lock" >&2; exit 1; }
142
142
 
143
- mkdir -p "$MEMORY_DIR" "$LEARNING_DIR" 2>/dev/null || true
144
-
143
+ # Each queue is checked from the root it lies below, and its folder created there,
144
+ # by queue_append_row (D-HOOKS-NO-SYMLINK, git-marker).
145
145
  _QUESTION_COUNT=0
146
146
  while IFS=$'\t' read -r Q A; do
147
147
  [ -z "$Q" ] && continue
@@ -150,7 +150,8 @@ while IFS=$'\t' read -r Q A; do
150
150
  A: ${A}"
151
151
  TS=$(date +%s)
152
152
  queue_append_both \
153
- "$MEMORY_DIR/.pending-turns.jsonl" "$LEARNING_DIR/.pending-turns.jsonl" \
153
+ "$PROJECT_ROOT" "$MEMORY_DIR/.pending-turns.jsonl" \
154
+ "$LEDGER_ROOT" "$LEARNING_DIR/.pending-turns.jsonl" \
154
155
  "$MEMORY_ENABLED" "$LEARNING_ENABLED" \
155
156
  "qa" "$CONTENT" "$TS"
156
157
  _QUESTION_COUNT=$(( _QUESTION_COUNT + 1 ))
@@ -2,9 +2,7 @@
2
2
 
3
3
  # Learning System: capture-turn (Stop Hook)
4
4
  # Dual-append: writes the assistant turn to BOTH the memory queue and the
5
- # learning queue, each independently gated by its own feature flag (AC-F4). Runs
6
- # the decisions usage scanner (D29 grep-first) regardless of which queue is
7
- # gated on/off -- memory-disabled projects still run the usage scanner. The
5
+ # learning queue, each independently gated by its own feature flag (AC-F4). The
8
6
  # 120s-throttle/nohup-spawn block lives in memory-worker, a separate Stop
9
7
  # hook that Claude Code runs in parallel with this one; the worker tolerates a
10
8
  # queue this hook has not appended to yet (D-QUEUE-NO-ORPHAN-DELETE).
@@ -77,20 +75,6 @@ LEARNING_ENABLED="$_QG_LEARNING"
77
75
 
78
76
  dbg "MEMORY_ENABLED=$MEMORY_ENABLED LEARNING_ENABLED=$LEARNING_ENABLED"
79
77
 
80
- # --- Decisions usage scanner (independent of the memory/learning queue gates below) ---
81
- # D29: Grep-first reorder -- cheap in-process citation check gates the scanner call.
82
- # The scanner writes the ledger's usage file, and it runs before ensure-devflow-init,
83
- # so it takes the D-HOOKS-GIT-ONLY gate itself (df_is_project_root, git-marker via
84
- # resolve-project-root; zero forks): a `.devflow/` an older devflow left in a
85
- # non-git directory, or at HOME, is not a project's ledger.
86
- SCANNER="$SCRIPT_DIR/decisions-usage-scan.cjs"
87
- if [ -f "$SCANNER" ] && printf '%s' "$ASSISTANT_MSG" | grep -qE 'ADR-[0-9]+|PF-[0-9]+'; then
88
- if [ "$LEARNING_ENABLED" = "true" ] && df_is_project_root "$PROJECT_ROOT" 2>/dev/null; then
89
- dbg "Running decisions usage scanner"
90
- printf '%s' "$ASSISTANT_MSG" | node "$SCANNER" --cwd "$LEDGER_ROOT" 2>/dev/null || true
91
- fi
92
- fi
93
-
94
78
  if [ "$MEMORY_ENABLED" != "true" ] && [ "$LEARNING_ENABLED" != "true" ]; then
95
79
  dbg "EXIT: both features disabled"
96
80
  exit 0
@@ -106,11 +90,12 @@ source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
106
90
  source "$SCRIPT_DIR/get-mtime" || { echo "capture-turn: failed to source get-mtime" >&2; exit 1; }
107
91
  source "$SCRIPT_DIR/learning-lock" || { echo "capture-turn: failed to source learning-lock" >&2; exit 1; }
108
92
 
109
- mkdir -p "$MEMORY_DIR" "$LEARNING_DIR" 2>/dev/null || true
110
-
93
+ # Each queue is checked from the root it lies below, and its folder created there,
94
+ # by queue_append_row (D-HOOKS-NO-SYMLINK, git-marker).
111
95
  TS=$(date +%s)
112
96
  queue_append_both \
113
- "$MEMORY_DIR/.pending-turns.jsonl" "$LEARNING_DIR/.pending-turns.jsonl" \
97
+ "$PROJECT_ROOT" "$MEMORY_DIR/.pending-turns.jsonl" \
98
+ "$LEDGER_ROOT" "$LEARNING_DIR/.pending-turns.jsonl" \
114
99
  "$MEMORY_ENABLED" "$LEARNING_ENABLED" \
115
100
  "assistant" "$ASSISTANT_MSG" "$TS"
116
101
 
@@ -2,7 +2,8 @@
2
2
  # Ensures .devflow/ and all subdirectories exist and the project root .gitignore
3
3
  # ignores .devflow/. Called from capture-prompt, capture-turn, capture-question,
4
4
  # memory-worker, and pre-compact-memory. Idempotent.
5
- # Returns non-zero, having written nothing, outside a git project or at HOME.
5
+ # Returns non-zero, having written nothing, outside a git project, at HOME, or when
6
+ # .devflow is a symbolic link.
6
7
  # Usage: source ensure-devflow-init "$CWD"
7
8
 
8
9
  [ -z "$1" ] && return 1
@@ -25,7 +26,7 @@ _EDI_LEDGER="${DF_LEDGER_ROOT:-$_EDI_ROOT}"
25
26
  # pre-compact-memory) passes through before it writes, and each of them treats a
26
27
  # non-zero return as "stop" (`|| exit 0`), so memory and learning stop together
27
28
  # in a non-git directory. ensure-root-gitignore itself stays ungated: it is a
28
- # pure writer its parity suite runs in plain directories (PF-059), and both of
29
+ # pure writer its parity suite runs in plain directories, and both of
29
30
  # its callers now decide before reaching it. A helper that failed to source is a
30
31
  # command not found — the refusing branch, so the failure mode writes nothing.
31
32
  source "$_EDI_DIR/git-marker" 2>/dev/null || true
@@ -33,6 +34,17 @@ df_is_project_root "$_EDI_ROOT" 2>/dev/null || return 1
33
34
 
34
35
  _DEVFLOW_DIR="$_EDI_ROOT/.devflow"
35
36
 
37
+ # D-HOOKS-NO-SYMLINK (git-marker): a `.devflow` that is itself a symbolic link
38
+ # would take every folder below and the carve-out marker into the folder it names,
39
+ # so nothing is written and the caller stops, as at any root it may not write to.
40
+ # The skip is logged once, through log() where the caller defines it. A folder below
41
+ # `.devflow` that is a link is left to the write under it: `mkdir -p` creates
42
+ # nothing through a link it ends at.
43
+ if ! df_no_symlink_below "$_EDI_ROOT" "$_DEVFLOW_DIR"; then
44
+ declare -F log >/dev/null 2>&1 && log "Skipped: $_DEVFLOW_DIR is a symbolic link; nothing written under it"
45
+ return 1
46
+ fi
47
+
36
48
  # learning/ is scaffolded only where the ledger lives. In a linked worktree whose
37
49
  # main checkout holds the ledger (D-LEDGER-MAIN-WORKTREE, resolve-project-root) a
38
50
  # learning/ here would be an empty directory nothing writes to; the capture hooks
@@ -15,8 +15,8 @@
15
15
  # Branding: "subswitch" is an internal identifier (health check body match, SUBSWITCH_CONFIG env var,
16
16
  # spawn args); it MUST NOT appear in user-visible strings or additionalContext messages.
17
17
  #
18
- # avoids PF-001: port is digit-validated before interpolation into /dev/tcp and context strings.
19
- # avoids PF-014: hook always exits 0; no fd held across exit.
18
+ # Port is digit-validated, falling back to the default, before reaching /dev/tcp or context strings.
19
+ # Hook always exits 0; no fd held across exit.
20
20
 
21
21
  # Safe no-op fallback — must exist before hook-bootstrap is sourced
22
22
  dbg() { :; }
@@ -71,7 +71,7 @@ if [ "$PROXY_ENABLED" != "true" ]; then
71
71
  exit 0
72
72
  fi
73
73
 
74
- # Digit-validate port before use in /dev/tcp and string interpolation (avoids PF-001)
74
+ # Digit-validate port before use in /dev/tcp and string interpolation
75
75
  PROXY_PORT_RAW=$(json_field_file "$PROXY_STATE_FILE" "port" "4141")
76
76
  PROXY_PORT="4141"
77
77
  case "$PROXY_PORT_RAW" in
@@ -189,8 +189,7 @@ dbg "PROXY_BIN=$PROXY_BIN PROXY_CONFIG=$PROXY_CONFIG"
189
189
  # b) `command -v subswitch` — globally installed CLI.
190
190
  # Re-resolution is best-effort: on failure the existing warning is emitted and the
191
191
  # hook exits 0 as before. The healed path is used for this session only; the next
192
- # successful `devflow proxy --enable` persists it to proxy.json. (avoids PF-001,
193
- # avoids PF-009: non-fatal on failure, always exits 0)
192
+ # successful `devflow proxy --enable` persists it to proxy.json.
194
193
  if [ -z "$PROXY_BIN" ] || [ ! -f "$PROXY_BIN" ]; then
195
194
  log "prereq: binPath missing or not a file (${PROXY_BIN:-<empty>}) — attempting re-resolution"
196
195
  _RESOLVED_BIN=""
@@ -295,7 +294,7 @@ log "spawning relay: $NODE_BIN $PROXY_BIN serve"
295
294
  (umask 077 && touch "$LOG_FILE") 2>/dev/null || true
296
295
  chmod 600 "$LOG_FILE" 2>/dev/null || true
297
296
 
298
- # SEC-2 (allowlist): scope the relay's env to exactly what it needs — avoids PF-017.
297
+ # SEC-2 (allowlist): scope the relay's env to exactly what it needs.
299
298
  # NODE_EXTRA_CA_CERTS is included when non-empty so corporate-TLS deployments can
300
299
  # supply a CA bundle; an unset/empty value is not injected (an empty path causes
301
300
  # TLS errors rather than being a no-op). NODE_OPTIONS is deliberately excluded: