devflow-kit 3.11.0 → 3.12.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 (204) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/agents/architect.md +87 -0
  3. package/dist/agents/code.md +21 -25
  4. package/dist/agents/debug.md +21 -49
  5. package/dist/agents/design.md +9 -43
  6. package/dist/agents/fix.md +15 -29
  7. package/dist/agents/git.md +19 -22
  8. package/dist/agents/harden.md +14 -19
  9. package/dist/agents/knowledge.md +11 -19
  10. package/dist/agents/refactor.md +9 -14
  11. package/dist/agents/research.md +11 -34
  12. package/dist/agents/review.md +22 -52
  13. package/dist/agents/skim.md +7 -17
  14. package/dist/agents/triage.md +7 -14
  15. package/dist/commands/bug-analysis.md +31 -62
  16. package/dist/commands/code-review.md +38 -87
  17. package/dist/commands/debug.md +32 -82
  18. package/dist/commands/dynamic-build.md +155 -211
  19. package/dist/commands/dynamic-plan.md +71 -93
  20. package/dist/commands/dynamic-profile.md +7 -155
  21. package/dist/commands/dynamic-tickets.md +65 -186
  22. package/dist/commands/explore.md +27 -70
  23. package/dist/commands/implement.md +98 -186
  24. package/dist/commands/plan.md +60 -107
  25. package/dist/commands/release.md +25 -91
  26. package/dist/commands/research.md +34 -103
  27. package/dist/commands/resolve.md +64 -128
  28. package/dist/commands/self-review.md +35 -99
  29. package/dist/compliance-off/agents/git.md +15 -18
  30. package/dist/compliance-off/agents/triage.md +7 -14
  31. package/dist/compliance-off/commands/bug-analysis.md +31 -62
  32. package/dist/compliance-off/commands/code-review.md +34 -82
  33. package/dist/compliance-off/commands/dynamic-build.md +149 -205
  34. package/dist/compliance-off/commands/dynamic-tickets.md +65 -186
  35. package/dist/compliance-off/commands/implement.md +94 -180
  36. package/dist/compliance-off/commands/plan.md +60 -107
  37. package/dist/compliance-off/commands/release.md +25 -90
  38. package/dist/compliance-off/commands/resolve.md +54 -128
  39. package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
  40. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
  41. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
  42. package/dist/core/plugins.js +3 -3
  43. package/dist/curation-compliance-off/agents/triage.md +7 -13
  44. package/dist/curation-compliance-off/commands/bug-analysis.md +21 -41
  45. package/dist/curation-compliance-off/commands/code-review.md +25 -62
  46. package/dist/curation-compliance-off/commands/dynamic-build.md +142 -194
  47. package/dist/curation-compliance-off/commands/dynamic-tickets.md +54 -173
  48. package/dist/curation-compliance-off/commands/implement.md +86 -161
  49. package/dist/curation-compliance-off/commands/plan.md +51 -86
  50. package/dist/curation-compliance-off/commands/release.md +17 -78
  51. package/dist/curation-compliance-off/commands/resolve.md +45 -108
  52. package/dist/curation-off/agents/architect.md +81 -0
  53. package/dist/curation-off/agents/code.md +20 -23
  54. package/dist/curation-off/agents/debug.md +19 -47
  55. package/dist/curation-off/agents/design.md +8 -41
  56. package/dist/curation-off/agents/fix.md +14 -27
  57. package/dist/curation-off/agents/harden.md +13 -18
  58. package/dist/curation-off/agents/knowledge.md +9 -17
  59. package/dist/curation-off/agents/refactor.md +8 -12
  60. package/dist/curation-off/agents/research.md +9 -32
  61. package/dist/curation-off/agents/review.md +11 -40
  62. package/dist/curation-off/agents/skim.md +7 -17
  63. package/dist/curation-off/agents/triage.md +7 -13
  64. package/dist/curation-off/commands/bug-analysis.md +21 -41
  65. package/dist/curation-off/commands/code-review.md +29 -67
  66. package/dist/curation-off/commands/debug.md +24 -63
  67. package/dist/curation-off/commands/dynamic-build.md +148 -200
  68. package/dist/curation-off/commands/dynamic-plan.md +60 -80
  69. package/dist/curation-off/commands/dynamic-tickets.md +54 -173
  70. package/dist/curation-off/commands/explore.md +18 -50
  71. package/dist/curation-off/commands/implement.md +90 -167
  72. package/dist/curation-off/commands/plan.md +51 -86
  73. package/dist/curation-off/commands/release.md +17 -79
  74. package/dist/curation-off/commands/research.md +23 -81
  75. package/dist/curation-off/commands/resolve.md +55 -108
  76. package/dist/curation-off/commands/self-review.md +26 -78
  77. package/dist/skills/git/references/pr/update-pr-evidence.md +2 -1
  78. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
  79. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
  80. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
  81. package/dist/targets/claude-code/curation-install.js +2 -2
  82. package/dist/variant-axes.json +5 -1
  83. package/package.json +1 -1
  84. package/src/assets/agents/_partials/_code.mds +3 -5
  85. package/src/assets/agents/_partials/_design.mds +86 -0
  86. package/src/assets/agents/architect.mds +41 -0
  87. package/src/assets/agents/code.mds +18 -20
  88. package/src/assets/agents/curate.md +26 -35
  89. package/src/assets/agents/debug.mds +21 -49
  90. package/src/assets/agents/design.mds +11 -114
  91. package/src/assets/agents/evaluate.md +6 -16
  92. package/src/assets/agents/explore.md +1 -2
  93. package/src/assets/agents/fix.mds +12 -24
  94. package/src/assets/agents/git.mds +20 -23
  95. package/src/assets/agents/harden.mds +14 -19
  96. package/src/assets/agents/knowledge.mds +10 -38
  97. package/src/assets/agents/polish.md +13 -39
  98. package/src/assets/agents/refactor.mds +6 -9
  99. package/src/assets/agents/research.mds +11 -34
  100. package/src/assets/agents/review.mds +12 -84
  101. package/src/assets/agents/skim.mds +7 -17
  102. package/src/assets/agents/synthesize.md +3 -21
  103. package/src/assets/agents/test.md +31 -32
  104. package/src/assets/agents/tracker.md +116 -229
  105. package/src/assets/agents/triage.mds +7 -26
  106. package/src/assets/agents/validate.md +1 -3
  107. package/src/assets/commands/_partials/_compliance.mds +2 -2
  108. package/src/assets/commands/_partials/_decisions.mds +17 -24
  109. package/src/assets/commands/_partials/_docs_root.mds +31 -10
  110. package/src/assets/commands/_partials/_engine.mds +34 -56
  111. package/src/assets/commands/_partials/_evidence_policy.mds +1 -1
  112. package/src/assets/commands/_partials/_factory.mds +10 -104
  113. package/src/assets/commands/_partials/_knowledge.mds +47 -51
  114. package/src/assets/commands/_partials/_plan_contract.mds +2 -2
  115. package/src/assets/commands/_partials/_preamble.mds +21 -21
  116. package/src/assets/commands/_partials/_publication.mds +3 -3
  117. package/src/assets/commands/_partials/_roster.mds +12 -13
  118. package/src/assets/commands/_partials/_settings.mds +6 -3
  119. package/src/assets/commands/_partials/_ticket_template.mds +3 -7
  120. package/src/assets/commands/_partials/_tracker.mds +10 -5
  121. package/src/assets/commands/_partials/_wave.mds +23 -32
  122. package/src/assets/commands/bug-analysis.mds +3 -7
  123. package/src/assets/commands/code-review.mds +12 -33
  124. package/src/assets/commands/debug.mds +5 -47
  125. package/src/assets/commands/dynamic-build.mds +48 -70
  126. package/src/assets/commands/dynamic-plan.mds +23 -42
  127. package/src/assets/commands/dynamic-profile.mds +7 -57
  128. package/src/assets/commands/dynamic-tickets.mds +9 -29
  129. package/src/assets/commands/explore.mds +5 -44
  130. package/src/assets/commands/implement.mds +75 -117
  131. package/src/assets/commands/plan.mds +27 -57
  132. package/src/assets/commands/release.mds +4 -62
  133. package/src/assets/commands/research.mds +7 -53
  134. package/src/assets/commands/resolve.mds +40 -87
  135. package/src/assets/commands/self-review.mds +5 -45
  136. package/src/assets/mds/git/_ops_pr.mds +1 -0
  137. package/src/assets/mds/git/_ops_steps.mds +4 -4
  138. package/src/assets/mds/git/_pr.mds +1 -1
  139. package/src/assets/rules/engineering.md +1 -1
  140. package/src/assets/rules/quality.md +2 -2
  141. package/src/assets/scripts/ci-wait.cjs +150 -18
  142. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +3 -3
  143. package/src/assets/scripts/hooks/preamble +2 -3
  144. package/src/assets/scripts/pr-evidence.cjs +1 -0
  145. package/src/assets/scripts/resolve-evidence-policy.cjs +14 -13
  146. package/src/assets/scripts/resolve-root.cjs +186 -0
  147. package/src/assets/scripts/resolve-settings.cjs +14 -9
  148. package/src/assets/scripts/verify-evidence.cjs +355 -22
  149. package/src/assets/skills/accessibility/SKILL.md +28 -95
  150. package/src/assets/skills/agent-references/SKILL.md +2 -4
  151. package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +1 -1
  152. package/src/assets/skills/agent-references/references/commands/implement-levels.md +4 -7
  153. package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +1 -1
  154. package/src/assets/skills/agent-references/references/commands/plan-levels.md +2 -2
  155. package/src/assets/skills/apply-decisions/SKILL.md +5 -6
  156. package/src/assets/skills/apply-feature-knowledge/SKILL.md +3 -13
  157. package/src/assets/skills/architecture/SKILL.md +29 -105
  158. package/src/assets/skills/boundary-validation/SKILL.md +27 -78
  159. package/src/assets/skills/complexity/SKILL.md +8 -104
  160. package/src/assets/skills/compliance/SKILL.md +9 -67
  161. package/src/assets/skills/consistency/SKILL.md +7 -107
  162. package/src/assets/skills/database/SKILL.md +7 -72
  163. package/src/assets/skills/dependencies/SKILL.md +9 -109
  164. package/src/assets/skills/dependencies/references/detection.md +10 -0
  165. package/src/assets/skills/dependency-research/SKILL.md +9 -63
  166. package/src/assets/skills/design-review/SKILL.md +1 -35
  167. package/src/assets/skills/docs-framework/SKILL.md +4 -47
  168. package/src/assets/skills/documentation/SKILL.md +7 -71
  169. package/src/assets/skills/documentation/references/patterns.md +1 -1
  170. package/src/assets/skills/documentation/references/violations.md +1 -1
  171. package/src/assets/skills/feature-knowledge/SKILL.md +19 -222
  172. package/src/assets/skills/feature-knowledge/references/new-kb.md +128 -0
  173. package/src/assets/skills/gap-analysis/SKILL.md +1 -31
  174. package/src/assets/skills/git/SKILL.md +2 -22
  175. package/src/assets/skills/go/SKILL.md +24 -113
  176. package/src/assets/skills/java/SKILL.md +12 -98
  177. package/src/assets/skills/patterns/SKILL.md +8 -93
  178. package/src/assets/skills/performance/SKILL.md +29 -88
  179. package/src/assets/skills/python/SKILL.md +22 -120
  180. package/src/assets/skills/qa/SKILL.md +16 -54
  181. package/src/assets/skills/qa/references/browser-testing.md +0 -24
  182. package/src/assets/skills/qa/references/sources.md +2 -2
  183. package/src/assets/skills/quality-gates/SKILL.md +13 -100
  184. package/src/assets/skills/react/SKILL.md +33 -95
  185. package/src/assets/skills/regression/SKILL.md +8 -73
  186. package/src/assets/skills/reliability/SKILL.md +11 -120
  187. package/src/assets/skills/research-codebase/SKILL.md +7 -49
  188. package/src/assets/skills/research-competitor/SKILL.md +4 -39
  189. package/src/assets/skills/research-external/SKILL.md +5 -43
  190. package/src/assets/skills/research-market/SKILL.md +5 -43
  191. package/src/assets/skills/research-technology/SKILL.md +8 -68
  192. package/src/assets/skills/review-methodology/SKILL.md +10 -56
  193. package/src/assets/skills/rust/SKILL.md +19 -109
  194. package/src/assets/skills/security/SKILL.md +39 -66
  195. package/src/assets/skills/software-design/SKILL.md +20 -105
  196. package/src/assets/skills/software-design/references/patterns.md +84 -2
  197. package/src/assets/skills/software-design/references/violations.md +1 -1
  198. package/src/assets/skills/test-driven-development/SKILL.md +32 -107
  199. package/src/assets/skills/test-driven-development/references/rationalization-prevention.md +33 -1
  200. package/src/assets/skills/testing/SKILL.md +35 -53
  201. package/src/assets/skills/typescript/SKILL.md +23 -102
  202. package/src/assets/skills/ui-design/SKILL.md +26 -74
  203. package/src/assets/skills/worktree-support/SKILL.md +4 -10
  204. package/dist/curation-off/commands/dynamic-profile.md +0 -214
@@ -9,24 +9,36 @@
9
9
  // only (D-CI-WORKFLOWS): at most one answered probe per run, and its answer is kept for
10
10
  // the run; a failed probe is a failed poll, and the next poll probes again. It waits for
11
11
  // CI to start until CI_WAIT_MAX_SECONDS, classifies the head it was told about, and
12
- // prints exactly one line.
12
+ // prints one CI line. With --waits and --fixes it prints one more line, the next
13
+ // action of the CI gate (D-CI-NEXT).
13
14
  // Installed beside hud.sh and redact-secrets.cjs under ~/.devflow/scripts/.
14
15
  //
15
16
  // Usage:
16
- // node ci-wait.cjs --pr <n> --head <sha>
17
+ // node ci-wait.cjs --pr <n> --head <sha> [--waits <w> --fixes <f>]
17
18
  // <n> the pull request number: digits, no leading zero, at most 9
18
19
  // <sha> the pushed head: 40 lowercase hex
20
+ // <w> the waits run so far, this one included: 1 to 3
21
+ // <f> the Fix agents run so far: 0 to 2
22
+ // Give --waits and --fixes together or not at all.
19
23
  //
20
24
  // Run it from the worktree, so gh finds the repository from the working directory.
21
25
  //
22
- // stdout (D-CI-LINE) is exactly one line, ending in a newline:
26
+ // stdout (D-CI-LINE) is one line, ending in a newline:
23
27
  // CI <STATUS> pr=<n> head=<first 7 hex> failing=<names|-> pending=<names|-> waited=<seconds>
24
28
  // with ` reason=<code>` appended to INDETERMINATE only. STATUS is one of STATUSES,
25
- // the check-ci-status Output enum. An argv that fails its gate prints the short form
29
+ // the check-ci-status Output enum. With --waits and --fixes (D-CI-NEXT) a second line
30
+ // follows the CI line, ending in a newline:
31
+ // NEXT=<wait|fix|proceed> SAY=<message>
32
+ // where the message is one fixed phrase of this file, or `none`. The line is the gate
33
+ // table of /implement and /resolve as a pure function of the status, the reason, the
34
+ // seconds waited and the two counts, so a prompt only acts on the answer. An argv that
35
+ // fails its gate prints the short form
26
36
  // `CI INDETERMINATE pr=- head=- waited=0 reason=usage` and makes no gh call. The
27
- // boundary re-checks the line against LINE_RE and the 400-character bound before it
28
- // writes it. Exit 0 whenever a line was printed; a reader treats a missing or
29
- // unparseable line, an unlisted status or a non-zero exit as INDETERMINATE.
37
+ // boundary re-checks the CI line against LINE_RE and the 400-character bound, and the
38
+ // NEXT line against the fixed set NEXT_LINES, before it writes them. A failed check
39
+ // prints the one failure line and no NEXT line, as it does without the two flags.
40
+ // Exit 0 whenever a line was printed; a reader treats a missing or unparseable line, an
41
+ // unlisted status or a non-zero exit as INDETERMINATE.
30
42
  //
31
43
  // Design constraints (binding). The text of each verdict rule is written once, at the
32
44
  // marker named below; this list points to it. The workflow-probe rule has two readers,
@@ -49,8 +61,11 @@
49
61
  // way. A silent foreground node call of about 570 s under a Bash timeout of
50
62
  // 600000 ms completed in a main-thread `claude -p` probe on 2026-10-09.
51
63
  // - D-CI-NO-EXIT: nothing here calls process.exit(). An explicit exit can
52
- // truncate stdout when it is a pipe, and the one line is the whole result. The
64
+ // truncate stdout when it is a pipe, and the printed lines are the whole result. The
53
65
  // boundary sets process.exitCode and lets the event loop drain.
66
+ // - D-CI-NEXT: see nextAction. The gate table is plumbing, not judgment: it maps a
67
+ // classified status and two counts to one action and one fixed phrase. The phrases
68
+ // carry no gh output, so no check name reaches the NEXT line.
54
69
  // - D-CI-WAIT-BOUNDS: every loop has a fixed bound (MAX_POLLS, the deadline, the
55
70
  // name lists), and every wait is capped at CI_WAIT_MAX_SECONDS.
56
71
  //
@@ -160,6 +175,9 @@ const NAME_TOKEN_RE = /^(?:-|\+[0-9]+)$/;
160
175
 
161
176
  const PR_RE = /^[1-9][0-9]{0,8}$/;
162
177
  const HEAD_RE = /^[0-9a-f]{40}$/;
178
+ /** D-CI-NEXT: the waits run so far, this one included (1 to MAX_WAITS), and the fixes (0 to MAX_FIXES). */
179
+ const WAITS_RE = /^[1-3]$/;
180
+ const FIXES_RE = /^[0-2]$/;
163
181
  const NO_PR_RE = /Could not resolve to a PullRequest/;
164
182
  const NO_CHECKS_RE = /no checks reported/i;
165
183
 
@@ -173,6 +191,35 @@ const LINE_RE = new RegExp(
173
191
 
174
192
  const USAGE_LINE = 'CI INDETERMINATE pr=- head=- waited=0 reason=usage';
175
193
 
194
+ /**
195
+ * D-CI-NEXT: the budget of one CI gate (3 waits and 2 fixes in all), the actions it
196
+ * answers with and the phrases it says. A phrase is fixed text of this file; none holds
197
+ * gh output. `none` says nothing. After a full-deadline wait that ends in one of
198
+ * FINAL_AFTER_FULL_WAIT, another wait cannot change the answer, so the gate stops.
199
+ * WAITS_RE and FIXES_RE spell the same two bounds.
200
+ */
201
+ const MAX_WAITS = 3;
202
+ const MAX_FIXES = 2;
203
+ const NEXTS = Object.freeze(['wait', 'fix', 'proceed']);
204
+ const FINAL_AFTER_FULL_WAIT = Object.freeze(['head-mismatch', 'no-workflow-check']);
205
+ const SAY_NONE = 'none';
206
+ const SAY_NO_CI = 'No PR/CI configured, skipping CI validation.';
207
+ const SAY_RUNNING = 'CI still running — verify manually before merging';
208
+ const SAY_UNKNOWN = 'CI status unknown — verify manually before merging';
209
+ const SAY_FIX = 'CI failed — a fix runs for the failing checks';
210
+ const SAY_STILL_FAILS = 'CI still fails after ' + MAX_FIXES + ' fixes — verify manually before merging';
211
+
212
+ /** The only NEXT lines this script prints: an action and a phrase that go together. */
213
+ const NEXT_LINES = Object.freeze([
214
+ 'NEXT=wait SAY=' + SAY_NONE,
215
+ 'NEXT=fix SAY=' + SAY_FIX,
216
+ 'NEXT=proceed SAY=' + SAY_NONE,
217
+ 'NEXT=proceed SAY=' + SAY_NO_CI,
218
+ 'NEXT=proceed SAY=' + SAY_RUNNING,
219
+ 'NEXT=proceed SAY=' + SAY_UNKNOWN,
220
+ 'NEXT=proceed SAY=' + SAY_STILL_FAILS,
221
+ ]);
222
+
176
223
  /**
177
224
  * D-CI-WORKFLOWS: the probe, a GET of the repository's workflow list. It is a fixed
178
225
  * argv with no field flag, no method flag and no input, and it names neither the PR nor
@@ -204,6 +251,8 @@ const VIEW_FIELDS = 'headRefOid,mergeable,state';
204
251
  * @typedef {{ status: string, pr: string, head: string, failing: string[], pending: string[],
205
252
  * waited: number, reason: string | null }} CiResult
206
253
  * `head` is the first 7 hex characters.
254
+ * @typedef {{ next: string, say: string }} NextAction
255
+ * `next` is one of NEXTS; `say` is one fixed phrase or SAY_NONE (D-CI-NEXT).
207
256
  * @typedef {{ pr: string, head: string, exec: ExecFn, clock: Clock, env?: NodeJS.ProcessEnv }} WaitInput
208
257
  * @typedef {{ state: 'deadline', rowsRead: boolean } | { state: 'failed' } | { state: 'pending', pending: string[] }
209
258
  * | { state: 'open' }
@@ -231,26 +280,34 @@ const DEADLINE_LAST = Object.freeze({ status: 'INDETERMINATE', reason: 'deadline
231
280
  // ---------------------------------------------------------------------------
232
281
 
233
282
  /**
234
- * D-CI-ARGV: exactly `--pr <digits> --head <40 lowercase hex>`, each once, in either
235
- * order, and nothing else. Anything that fails is refused before any gh call, so a
236
- * value never reaches gh's argv unless it is a plain number or a plain SHA.
283
+ * D-CI-ARGV: exactly `--pr <digits> --head <40 lowercase hex>`, each once, in any
284
+ * order, and nothing else. D-CI-NEXT adds an opt-in pair, `--waits <1 to 3>` and
285
+ * `--fixes <0 to 2>`: both or neither, each once, a single digit in its range. Anything
286
+ * that fails is refused before any gh call, so a value never reaches gh's argv unless it
287
+ * is a plain number or a plain SHA. The result holds `waits` and `fixes` only when the
288
+ * pair was given.
237
289
  *
238
290
  * @param {readonly string[]} args the arguments after the script path
239
- * @returns {{ pr: string, head: string } | null}
291
+ * @returns {{ pr: string, head: string, waits?: number, fixes?: number } | null}
240
292
  */
241
293
  function parseArgs(args) {
242
- if (!Array.isArray(args) || args.length !== 4) return null;
294
+ if (!Array.isArray(args) || (args.length !== 4 && args.length !== 8)) return null;
243
295
  let pr = null;
244
296
  let head = null;
245
- for (let i = 0; i < 4; i += 2) {
297
+ let waits = null;
298
+ let fixes = null;
299
+ for (let i = 0; i < args.length; i += 2) {
246
300
  const flag = args[i];
247
301
  const value = args[i + 1];
248
302
  if (typeof value !== 'string') return null;
249
303
  if (flag === '--pr' && pr === null && PR_RE.test(value)) pr = value;
250
304
  else if (flag === '--head' && head === null && HEAD_RE.test(value)) head = value;
305
+ else if (flag === '--waits' && waits === null && WAITS_RE.test(value)) waits = Number(value);
306
+ else if (flag === '--fixes' && fixes === null && FIXES_RE.test(value)) fixes = Number(value);
251
307
  else return null;
252
308
  }
253
- return pr !== null && head !== null ? { pr, head } : null;
309
+ if (pr === null || head === null) return null;
310
+ return waits === null || fixes === null ? { pr, head } : { pr, head, waits, fixes };
254
311
  }
255
312
 
256
313
  // ---------------------------------------------------------------------------
@@ -364,6 +421,63 @@ function checkLine(line) {
364
421
  return m !== null && (m[1] === 'INDETERMINATE') === (m[4] !== undefined);
365
422
  }
366
423
 
424
+ /**
425
+ * D-CI-NEXT: what the CI gate does with one printed result. The table is the one the
426
+ * /implement and /resolve prompts used to carry:
427
+ * PASSING proceed, saying nothing
428
+ * NO_PR, NO_CI proceed, saying that no CI is configured
429
+ * PENDING wait while fewer than MAX_WAITS waits have run, else proceed
430
+ * INDETERMINATE wait while fewer than MAX_WAITS waits have run, unless the
431
+ * wait ran the whole deadline and the reason is one of
432
+ * FINAL_AFTER_FULL_WAIT; else proceed, saying the status is unknown
433
+ * FAILING fix while fewer than MAX_FIXES fixes have run, else proceed
434
+ * A status outside STATUSES is unknown, which fails closed to the same proceed.
435
+ * `waited` and `reason` are the result's own values, never read back from text.
436
+ *
437
+ * @param {{ status: string, reason: string | null, waited: number }} result
438
+ * @param {number} waits the waits run so far, this one included
439
+ * @param {number} fixes the Fix agents run so far
440
+ * @returns {NextAction}
441
+ */
442
+ function nextAction(result, waits, fixes) {
443
+ const proceed = say => ({ next: 'proceed', say });
444
+ switch (result.status) {
445
+ case 'PASSING':
446
+ return proceed(SAY_NONE);
447
+ case 'NO_PR':
448
+ case 'NO_CI':
449
+ return proceed(SAY_NO_CI);
450
+ case 'PENDING':
451
+ return waits < MAX_WAITS ? { next: 'wait', say: SAY_NONE } : proceed(SAY_RUNNING);
452
+ case 'INDETERMINATE': {
453
+ const final = result.waited === CI_WAIT_MAX_SECONDS && FINAL_AFTER_FULL_WAIT.includes(result.reason);
454
+ return waits < MAX_WAITS && !final ? { next: 'wait', say: SAY_NONE } : proceed(SAY_UNKNOWN);
455
+ }
456
+ case 'FAILING':
457
+ return fixes < MAX_FIXES ? { next: 'fix', say: SAY_FIX } : proceed(SAY_STILL_FAILS);
458
+ default:
459
+ return proceed(SAY_UNKNOWN);
460
+ }
461
+ }
462
+
463
+ /**
464
+ * @param {NextAction} action
465
+ * @returns {string} `NEXT=<action> SAY=<message>`
466
+ */
467
+ function renderNext(action) {
468
+ return 'NEXT=' + action.next + ' SAY=' + action.say;
469
+ }
470
+
471
+ /**
472
+ * Whether a line is one this script may print as the NEXT line: one of NEXT_LINES, whole.
473
+ *
474
+ * @param {string} line
475
+ * @returns {boolean}
476
+ */
477
+ function checkNext(line) {
478
+ return typeof line === 'string' && NEXT_LINES.includes(line);
479
+ }
480
+
367
481
  // ---------------------------------------------------------------------------
368
482
  // One bounded gh call
369
483
  // ---------------------------------------------------------------------------
@@ -947,9 +1061,10 @@ async function main(argv, deps) {
947
1061
  /** @param {string} reason */
948
1062
  const indeterminate = reason =>
949
1063
  'CI INDETERMINATE pr=' + parsed.pr + ' head=' + parsed.head.slice(0, 7) + ' waited=0 reason=' + reason;
1064
+ let result = null;
950
1065
  let line;
951
1066
  try {
952
- const result = await waitForCi({
1067
+ result = await waitForCi({
953
1068
  pr: parsed.pr,
954
1069
  head: parsed.head,
955
1070
  exec: injected.exec || defaultExec,
@@ -958,10 +1073,19 @@ async function main(argv, deps) {
958
1073
  });
959
1074
  line = renderLine(result);
960
1075
  } catch (_) {
1076
+ result = null;
961
1077
  line = indeterminate('gh-failed');
962
1078
  }
963
- if (!checkLine(line)) line = indeterminate('unparseable');
964
- return { code: 0, stdout: line + '\n' };
1079
+ if (!checkLine(line)) {
1080
+ result = null;
1081
+ line = indeterminate('unparseable');
1082
+ }
1083
+ if (result === null || parsed.waits === undefined || parsed.fixes === undefined) {
1084
+ return { code: 0, stdout: line + '\n' };
1085
+ }
1086
+ const next = renderNext(nextAction(result, parsed.waits, parsed.fixes));
1087
+ if (!checkNext(next)) return { code: 0, stdout: indeterminate('unparseable') + '\n' };
1088
+ return { code: 0, stdout: line + '\n' + next + '\n' };
965
1089
  }
966
1090
 
967
1091
  // ---------------------------------------------------------------------------
@@ -1011,6 +1135,11 @@ module.exports = Object.freeze({
1011
1135
  NAMES_LISTED,
1012
1136
  LINE_RE,
1013
1137
  USAGE_LINE,
1138
+ MAX_WAITS,
1139
+ MAX_FIXES,
1140
+ NEXTS,
1141
+ FINAL_AFTER_FULL_WAIT,
1142
+ NEXT_LINES,
1014
1143
  WORKFLOWS_ARGS,
1015
1144
  parseArgs,
1016
1145
  sanitizeName,
@@ -1024,6 +1153,9 @@ module.exports = Object.freeze({
1024
1153
  askWorkflows,
1025
1154
  renderLine,
1026
1155
  checkLine,
1156
+ nextAction,
1157
+ renderNext,
1158
+ checkNext,
1027
1159
  waitForCi,
1028
1160
  main,
1029
1161
  });
@@ -3,12 +3,12 @@ You are the orchestrator of this session: you coordinate, agents produce.
3
3
 
4
4
  Never do work-product mainline: no file edits, no builds, no multi-file reads, no codebase orientation, no debug loops. Delegate all of it, with one bounded exception: the main thread may run a single git, gh or script command whose output stays under about 40 lines, never a diff, log or test run.
5
5
 
6
- Routing (Agent tool), by kind of work:
6
+ Routing (Agent tool):
7
7
  - Search and listing: Explore. Codebase orientation: Skim.
8
8
  - Execution against a spec: Code (write code to a plan; open the prompt with `OPERATION: <mode>`: `issue-fix` for pre-classified review issues, else `implement`), Refactor (a given mechanical change: renames, moves, boilerplate; `OPERATION: edit`), Polish (clarity pass that keeps behaviour), Validate (build, typecheck, lint, test), Git (git/GitHub).
9
9
  - Checks: Harden (self-review that fixes, after Code), Evaluate (alignment with request and plan), Test (scenario QA on acceptance criteria; never fixes). Gate failures: Fix (`OPERATION:` `validation-fix`, `alignment-fix`, `qa-fix` or `ci-fix`), else Code (`OPERATION: implement`).
10
10
  - Bug fix: Code (`OPERATION: implement`). All debug work (root cause of a reported bug): devflow:debug, else Explore investigates and Code fixes; never the Debug agent.
11
- - Analysis, design, research: Design, Research, Review, Triage (validate review issues), Synthesize (many or large reports; open its prompt with `Mode: <mode>`).
11
+ - Analysis, design, research: Design, Architect, Research, Review, Triage (validate review issues), Synthesize (many or large reports; open its prompt with `Mode: <mode>`).
12
12
  - A new knowledge base, only if the user asks: Knowledge (slug, name, directories).
13
13
  - Real-scale work matching a workflow: invoke the full skill instead: devflow:implement, devflow:plan, devflow:research, devflow:explore, devflow:code-review, devflow:resolve, devflow:bug-analysis (hunt bugs in a branch before merge).
14
14
  - Anything else: the closest roster agent, never general-purpose.
@@ -20,5 +20,5 @@ Operating rules:
20
20
  - Subagents see none of this conversation. Make every delegation self-contained: goal, constraints, relevant session decisions and facts, exact paths. A deliverable that draws on the conversation (issue, PR, report) needs its substance in the prompt, not a pointer.
21
21
  - Report cap: ask every direct delegation for a final report of at most about 1,500 tokens: findings, paths and verdicts, not file dumps.
22
22
  - Parallelize independent delegations in one message. Git operations stay sequential.
23
- - Feature knowledge (direct delegations only; workflow skills handle their own): before delegating non-trivial code work, match the task area against .devflow/features/index.md and pass each matching KB's one to three most relevant `## Rules` bullets (verbatim, IDs included; a KB without Rules: Anti-Patterns or Gotchas entries, cited by section), its path and its `##` heading index as FEATURE_KNOWLEDGE; after delegated changes to a covered area, spawn Knowledge to refresh that KB.
23
+ - Feature knowledge (direct delegations only): before delegating non-trivial code work, match the task area against .devflow/features/index.md and pass each matching KB's one to three most relevant `## Rules` bullets (verbatim, IDs included; a KB without Rules: Anti-Patterns or Gotchas entries, cited by section), its path and its `##` heading index as FEATURE_KNOWLEDGE; after delegated changes to a covered area, spawn Knowledge to refresh that KB.
24
24
  - Plan handoff: if the user's first message begins with `Implement the following plan:`, say so in one sentence, then immediately invoke devflow:implement via the Skill tool with no arguments: the plan is already in this conversation. Do not pause to ask.
@@ -11,7 +11,7 @@
11
11
  # which the installed render drops when implement is absent) and in the dispatch below —
12
12
  # update both together if Claude Code changes the handoff format.
13
13
  # 2. Slash skip: no reminder for slash commands (/...).
14
- # 3. Orchestrator reminder (2 lines): all other prompts, and a handoff prompt when implement
14
+ # 3. Orchestrator reminder (1 line): all other prompts, and a handoff prompt when implement
15
15
  # is not installed, get a generic delegation reminder.
16
16
 
17
17
  # Safe no-op fallback: must exist before hook-bootstrap is sourced.
@@ -85,8 +85,7 @@ elif [[ "$HEAD" == "/"* ]]; then
85
85
  dbg "EXIT: slash command — no reminder"
86
86
  else
87
87
  dbg "ORCHESTRATOR_REMINDER injected"
88
- json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds and multi-file reads to the fitting roster agent (Agent tool) or the matching devflow workflow skill.
89
- Keep only judgment work mainline: conversation, decisions, routing, combining a few agent reports."
88
+ json_prompt_output "Orchestrator reminder: coordinate, don't produce — delegate edits, builds and multi-file reads to a roster agent or a devflow workflow skill; keep only judgment work mainline (conversation, decisions, routing, combining agent reports)."
90
89
  fi
91
90
 
92
91
  dbg "=== HOOK COMPLETE ==="
@@ -1966,6 +1966,7 @@ module.exports = Object.freeze({
1966
1966
  MARKERS,
1967
1967
  TP_LINE_RE,
1968
1968
  CLAIM_LINE_RE,
1969
+ CLAIMS_HEADING,
1969
1970
  EXCEPTION_LINE_RE,
1970
1971
  EVIDENCE_LINE_RE,
1971
1972
  SHA_RE,
@@ -15,7 +15,7 @@
15
15
  // `.devflow/policy.json` is detected by presence and never parsed
16
16
  // (D-POLICY-JSON-RETIRED).
17
17
  //
18
- // stdout is exactly one line plus "\n", or empty (D-POLICY-LINE):
18
+ // stdout is exactly one line plus "\n" (D-POLICY-LINE):
19
19
  // EVIDENCE_POLICY=<required|standard> SOURCE=<file|worktree|default|invalid|error>
20
20
  // REF=<branch|none>[ WARN=<w>[,<w>…]] ISSUE_REQUIRED=<bool> APPLY_CONVENTIONS=<bool>
21
21
  // REQUIRE_NON_AUTHOR_APPROVAL=<bool>
@@ -25,7 +25,7 @@
25
25
  //
26
26
  // Exit codes (a caller treats EVERY non-zero code as `required`):
27
27
  // 0 resolved — the line above
28
- // 1 usage error — stdout entirely empty, usage on stderr
28
+ // 1 usage error — usage on stderr; prints FAIL_CLOSED_LINE
29
29
  // 2 input unusable — <dir> missing or not a directory; prints FAIL_CLOSED_LINE
30
30
  // 3 never emitted — this script writes no file, so the write-failure code of
31
31
  // redact-secrets.cjs has no arm here and is absent from EXIT_CODES
@@ -139,8 +139,9 @@ const { SAFE_REF_RE } = projectConfig;
139
139
  const OUTPUT_LINE_RE = /^EVIDENCE_POLICY=(?<policy>required|standard) SOURCE=(?<source>file|worktree|default|invalid|error) REF=(?<ref>none|(?![A-Za-z0-9._/-]{0,254}\.\.)[A-Za-z0-9][A-Za-z0-9._/-]{0,254})(?: WARN=(?<warn>(?:remote-unavailable|invalid-file|raised-by-compliance|pr-changes-policy)(?:,(?:remote-unavailable|invalid-file|raised-by-compliance|pr-changes-policy)){0,3}))? ISSUE_REQUIRED=(?<issue>true|false) APPLY_CONVENTIONS=(?<conventions>true|false) REQUIRE_NON_AUTHOR_APPROVAL=(?<approval>true|false)$/;
140
140
 
141
141
  /**
142
- * The line every refusal prints (exits 2, 4 and 5). A constant, so the boundary
142
+ * The line every refusal prints (exits 1, 2, 4 and 5). A constant, so the boundary
143
143
  * can never fail to compose it, and `required` in every field a consumer reads.
144
+ * The literal fallback of the _evidence_policy.mds partial is these bytes.
144
145
  */
145
146
  const FAIL_CLOSED_LINE =
146
147
  'EVIDENCE_POLICY=required SOURCE=error REF=none ISSUE_REQUIRED=true APPLY_CONVENTIONS=true REQUIRE_NON_AUTHOR_APPROVAL=true';
@@ -196,7 +197,7 @@ const LS_REMOTE_SYMREF_RE = /^ref: refs\/heads\/([^\t\n]{1,255})\tHEAD$/;
196
197
  * Injected by tests only, to reach the output gate.
197
198
  *
198
199
  * @typedef {{ code: number, line: string }} MainOutcome
199
- * `line` is '' only for code 1, FAIL_CLOSED_LINE for codes 2, 4 and 5.
200
+ * `line` is FAIL_CLOSED_LINE for codes 1, 2, 4 and 5.
200
201
  */
201
202
 
202
203
  /** @type {ParsedPolicy} */
@@ -1008,8 +1009,8 @@ function passesOutputGate(line, resolution) {
1008
1009
 
1009
1010
  /**
1010
1011
  * Settle whatever main() returned into what the boundary may print. The code
1011
- * must be a known exit code and the line must fit it: '' for usage, a coherent
1012
- * line for 0, FAIL_CLOSED_LINE for the refusals. Anything else is refused.
1012
+ * must be a known exit code and the line must fit it: a coherent line for 0,
1013
+ * FAIL_CLOSED_LINE for the refusals. Anything else is refused.
1013
1014
  *
1014
1015
  * @param {unknown} outcome
1015
1016
  * @returns {MainOutcome}
@@ -1019,11 +1020,10 @@ function settleOutcome(outcome) {
1019
1020
  if (o === null || typeof o !== 'object' || typeof o.line !== 'string') {
1020
1021
  return { code: EXIT_CODES.INTERNAL_ERROR, line: FAIL_CLOSED_LINE };
1021
1022
  }
1022
- if (o.code === EXIT_CODES.USAGE) return { code: EXIT_CODES.USAGE, line: '' };
1023
1023
  if (o.code === EXIT_CODES.RESOLVED) {
1024
1024
  return isCoherentLine(o.line) ? { code: o.code, line: o.line } : { code: EXIT_CODES.OUTPUT_GATE_REFUSED, line: FAIL_CLOSED_LINE };
1025
1025
  }
1026
- if (o.code === EXIT_CODES.INPUT_UNUSABLE || o.code === EXIT_CODES.OUTPUT_GATE_REFUSED) {
1026
+ if (o.code === EXIT_CODES.USAGE || o.code === EXIT_CODES.INPUT_UNUSABLE || o.code === EXIT_CODES.OUTPUT_GATE_REFUSED) {
1027
1027
  return { code: o.code, line: FAIL_CLOSED_LINE };
1028
1028
  }
1029
1029
  return { code: EXIT_CODES.INTERNAL_ERROR, line: FAIL_CLOSED_LINE };
@@ -1069,7 +1069,7 @@ function main(argv, deps) {
1069
1069
  const args = parseArgs(argv);
1070
1070
  if (args.kind === 'usage') {
1071
1071
  process.stderr.write(args.usage + '\n');
1072
- return { code: EXIT_CODES.USAGE, line: '' };
1072
+ return { code: EXIT_CODES.USAGE, line: FAIL_CLOSED_LINE };
1073
1073
  }
1074
1074
 
1075
1075
  if (!isUsableDirectory(args.dir)) {
@@ -1106,9 +1106,9 @@ function main(argv, deps) {
1106
1106
  //
1107
1107
  // This is the ONLY place that writes to stdout and sets process.exitCode.
1108
1108
  // It re-checks what main() returned (settleOutcome) before writing, so even a
1109
- // main() that returned something malformed prints either nothing (usage) or a
1110
- // line that passes the gate. Its catch is the fail-closed arm for anything
1111
- // main() did not anticipate.
1109
+ // main() that returned something malformed prints a line that passes the gate
1110
+ // or FAIL_CLOSED_LINE. Its catch is the fail-closed arm for anything main() did
1111
+ // not anticipate.
1112
1112
  // ---------------------------------------------------------------------------
1113
1113
 
1114
1114
  if (require.main === module) {
@@ -1120,7 +1120,7 @@ if (require.main === module) {
1120
1120
  outcome = { code: EXIT_CODES.INTERNAL_ERROR, line: FAIL_CLOSED_LINE };
1121
1121
  }
1122
1122
  const settled = settleOutcome(outcome);
1123
- if (settled.line !== '') process.stdout.write(settled.line + '\n');
1123
+ process.stdout.write(settled.line + '\n');
1124
1124
  process.exitCode = settled.code;
1125
1125
  }
1126
1126
 
@@ -1141,5 +1141,6 @@ module.exports = Object.freeze({
1141
1141
  complianceDefault,
1142
1142
  resolve,
1143
1143
  formatLine,
1144
+ settleOutcome,
1144
1145
  main,
1145
1146
  });
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ // src/assets/scripts/resolve-root.cjs
3
+ //
4
+ // Prints the two roots a prompt works from, so a command runs ONE call for them:
5
+ // the checkout (where docs artifacts and feature knowledge live) and the ledger
6
+ // (where the curation index lives). Installed as a top-level sibling of
7
+ // resolve-settings.cjs under ~/.devflow/scripts/, and it requires that file: the git
8
+ // call, its default exec and the ledger rule are resolve-settings.cjs's own
9
+ // (gitToplevel, defaultExec, ledgerRootOf). The ledger rule is the one the hooks apply
10
+ // (hooks/resolve-project-root, and its TypeScript twin src/core/ledger-root.ts).
11
+ // The script holds no other judgment.
12
+ //
13
+ // Usage: node resolve-root.cjs [<dir>] (<dir> defaults to cwd)
14
+ //
15
+ // stdout on success is exactly two lines, each ended by "\n":
16
+ // WORKTREE=<absolute path>
17
+ // LEDGER=<absolute path>
18
+ // WORKTREE is git's toplevel for <dir>. LEDGER is the main worktree when <dir> is in
19
+ // a linked worktree and the main worktree holds a `.devflow/` folder that is not the
20
+ // home directory; otherwise it is the toplevel. When git does not know the ledger
21
+ // root, LEDGER is the toplevel. Outside a git repository both lines name the absolute
22
+ // form of <dir>.
23
+ //
24
+ // Fail closed: on any failure stdout stays empty, stderr holds one reason line and the
25
+ // exit code is not 0. The caller then uses its start directory for both roots.
26
+ //
27
+ // Exit codes:
28
+ // 0 resolved — the two lines above
29
+ // 1 usage error — usage on stderr
30
+ // 2 input unusable — <dir> is missing or not a directory, or a path holds a control
31
+ // character (any C0 character, such as CR, LF or NUL)
32
+ // 4 internal error — or git did not answer
33
+ // (the numbers match resolve-settings.cjs; 3 and 5 are never emitted)
34
+ //
35
+ // Design constraints (shared with resolve-settings.cjs):
36
+ // - main() returns {code, line} and never calls process.exit or throws; the single
37
+ // `require.main === module` boundary is the only stdout write and the only
38
+ // exitCode assignment
39
+ // - every subprocess is spawned with an argv array (never a shell), stdin ignored,
40
+ // a timeout and a maxBuffer — resolve-settings.cjs's runGit does this
41
+ // - a local read only: no network, nothing written
42
+
43
+ 'use strict';
44
+
45
+ const fs = require('fs');
46
+ const path = require('path');
47
+ const settings = require('./resolve-settings.cjs');
48
+
49
+ const EXIT_CODES = Object.freeze({
50
+ RESOLVED: 0,
51
+ USAGE: 1,
52
+ INPUT_UNUSABLE: 2,
53
+ INTERNAL_ERROR: 4,
54
+ });
55
+
56
+ const USAGE = 'Usage: node resolve-root.cjs [<dir>]';
57
+
58
+ /** Any C0 control character: a path that holds one cannot be a one-line answer. */
59
+ const CONTROL_RE = /[\x00-\x1f]/;
60
+
61
+ /** The grammar of a successful answer: two absolute paths, one per line, no control character. */
62
+ const OUTPUT_RE = /^WORKTREE=\/[^\x00-\x1f]*\nLEDGER=\/[^\x00-\x1f]*$/;
63
+
64
+ /**
65
+ * @param {readonly string[]} argv process.argv
66
+ * @returns {{ kind: 'resolve', dir: string } | { kind: 'usage', usage: string }}
67
+ */
68
+ function parseArgs(argv) {
69
+ const rest = Array.isArray(argv) ? argv.slice(2) : [];
70
+ /** @type {string[]} */
71
+ const positionals = [];
72
+ for (const arg of rest) {
73
+ if (typeof arg !== 'string' || arg.startsWith('-')) {
74
+ return {
75
+ kind: 'usage',
76
+ usage: 'resolve-root: unrecognised argument ' + JSON.stringify(String(arg)).slice(0, 80) + '\n' + USAGE,
77
+ };
78
+ }
79
+ positionals.push(arg);
80
+ }
81
+ if (positionals.length > 1) return { kind: 'usage', usage: USAGE };
82
+ return { kind: 'resolve', dir: positionals.length === 1 ? positionals[0] : process.cwd() };
83
+ }
84
+
85
+ /**
86
+ * @param {string} dir
87
+ * @returns {boolean}
88
+ */
89
+ function isUsableDirectory(dir) {
90
+ try {
91
+ return fs.statSync(dir).isDirectory();
92
+ } catch (_) {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Print one reason line and return the failure outcome.
99
+ *
100
+ * @param {number} code
101
+ * @param {string} reason
102
+ * @returns {{ code: number, line: string }}
103
+ */
104
+ function fail(code, reason) {
105
+ process.stderr.write('resolve-root: ' + reason + ' — failing closed\n');
106
+ return { code, line: '' };
107
+ }
108
+
109
+ /**
110
+ * @param {readonly string[]} argv process.argv
111
+ * @param {{ exec?: (file: string, args: string[], opts: object) => unknown }} [deps] tests only
112
+ * @returns {{ code: number, line: string }}
113
+ */
114
+ function main(argv, deps) {
115
+ try {
116
+ const exec = deps && typeof deps.exec === 'function' ? deps.exec : settings.defaultExec;
117
+ const args = parseArgs(argv);
118
+ if (args.kind === 'usage') {
119
+ process.stderr.write(args.usage + '\n');
120
+ return { code: EXIT_CODES.USAGE, line: '' };
121
+ }
122
+ const dir = path.resolve(args.dir);
123
+ if (CONTROL_RE.test(dir)) return fail(EXIT_CODES.INPUT_UNUSABLE, 'the directory path holds a control character');
124
+ if (!isUsableDirectory(dir)) {
125
+ return fail(EXIT_CODES.INPUT_UNUSABLE, 'not a usable directory: ' + JSON.stringify(dir).slice(0, 300));
126
+ }
127
+ const toplevel = settings.gitToplevel(exec, dir);
128
+ if (toplevel.kind === 'unknown') return fail(EXIT_CODES.INTERNAL_ERROR, 'git did not answer');
129
+ let worktree = dir;
130
+ let ledger = dir;
131
+ if (toplevel.kind === 'root') {
132
+ worktree = toplevel.root;
133
+ const found = settings.ledgerRootOf(toplevel.root, { exec });
134
+ ledger = found.kind === 'root' ? found.root : toplevel.root;
135
+ }
136
+ if (CONTROL_RE.test(worktree) || CONTROL_RE.test(ledger)) {
137
+ return fail(EXIT_CODES.INPUT_UNUSABLE, 'a root path holds a control character');
138
+ }
139
+ return { code: EXIT_CODES.RESOLVED, line: 'WORKTREE=' + worktree + '\nLEDGER=' + ledger };
140
+ } catch (_) {
141
+ return fail(EXIT_CODES.INTERNAL_ERROR, 'internal error');
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Settle whatever main() returned into what the boundary may print: a success carries
147
+ * a line that passes the grammar, and every other outcome prints nothing.
148
+ *
149
+ * @param {unknown} outcome
150
+ * @returns {{ code: number, line: string }}
151
+ */
152
+ function settleOutcome(outcome) {
153
+ const o = /** @type {any} */ (outcome);
154
+ if (o === null || typeof o !== 'object' || typeof o.line !== 'string' || typeof o.code !== 'number') {
155
+ return { code: EXIT_CODES.INTERNAL_ERROR, line: '' };
156
+ }
157
+ if (o.code === EXIT_CODES.RESOLVED) {
158
+ return OUTPUT_RE.test(o.line) ? { code: o.code, line: o.line } : { code: EXIT_CODES.INTERNAL_ERROR, line: '' };
159
+ }
160
+ return { code: o.code === EXIT_CODES.USAGE || o.code === EXIT_CODES.INPUT_UNUSABLE ? o.code : EXIT_CODES.INTERNAL_ERROR, line: '' };
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Top-level boundary — the ONLY stdout write and the ONLY exitCode assignment
165
+ // ---------------------------------------------------------------------------
166
+
167
+ if (require.main === module) {
168
+ let outcome;
169
+ try {
170
+ outcome = main(process.argv);
171
+ } catch (_) {
172
+ process.stderr.write('resolve-root: internal error — failing closed\n');
173
+ outcome = { code: EXIT_CODES.INTERNAL_ERROR, line: '' };
174
+ }
175
+ const settled = settleOutcome(outcome);
176
+ if (settled.line !== '') process.stdout.write(settled.line + '\n');
177
+ process.exitCode = settled.code;
178
+ }
179
+
180
+ module.exports = Object.freeze({
181
+ EXIT_CODES,
182
+ OUTPUT_RE,
183
+ parseArgs,
184
+ settleOutcome,
185
+ main,
186
+ });