peaks-loop 4.0.50 → 4.0.52

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 (127) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README-en.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/cli/commands/baseline-commands.js +11 -1
  5. package/dist/cli/commands/codegraph-command-runtime.d.ts +28 -0
  6. package/dist/cli/commands/codegraph-command-runtime.js +72 -0
  7. package/dist/cli/commands/codegraph-commands.d.ts +2 -11
  8. package/dist/cli/commands/codegraph-commands.js +173 -228
  9. package/dist/cli/commands/codegraph-status-command.d.ts +22 -0
  10. package/dist/cli/commands/codegraph-status-command.js +299 -0
  11. package/dist/cli/commands/core/memory-command.js +6 -2
  12. package/dist/cli/commands/job-commands.js +121 -30
  13. package/dist/cli/commands/project-commands.js +13 -3
  14. package/dist/cli/commands/request-commands.js +19 -8
  15. package/dist/cli/commands/share-commands.js +85 -18
  16. package/dist/cli/commands/slice-commands.js +2 -2
  17. package/dist/services/artifacts/artifact-prerequisites.js +23 -1
  18. package/dist/services/codegraph/codegraph-autorefresh.d.ts +16 -0
  19. package/dist/services/codegraph/codegraph-autorefresh.js +51 -5
  20. package/dist/services/codegraph/codegraph-config-repair-writer.d.ts +88 -0
  21. package/dist/services/codegraph/codegraph-config-repair-writer.js +322 -0
  22. package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +20 -2
  23. package/dist/services/codegraph/codegraph-exclude-integrity.js +24 -3
  24. package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +23 -2
  25. package/dist/services/codegraph/codegraph-exclude-reconciler.js +123 -12
  26. package/dist/services/codegraph/codegraph-exclude-repair.d.ts +109 -55
  27. package/dist/services/codegraph/codegraph-exclude-repair.js +249 -195
  28. package/dist/services/codegraph/codegraph-include-reconciler.d.ts +10 -0
  29. package/dist/services/codegraph/codegraph-include-reconciler.js +160 -0
  30. package/dist/services/codegraph/codegraph-index-integrity.d.ts +268 -0
  31. package/dist/services/codegraph/codegraph-index-integrity.js +471 -0
  32. package/dist/services/codegraph/codegraph-service.d.ts +54 -0
  33. package/dist/services/codegraph/codegraph-service.js +84 -1
  34. package/dist/services/dispatch/sub-agent-dispatcher.d.ts +11 -30
  35. package/dist/services/dispatch/sub-agent-dispatcher.js +5 -48
  36. package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +19 -4
  37. package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.d.ts +54 -0
  38. package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.js +151 -0
  39. package/dist/services/doctor/doctor-service/checks/l3-orphan-sessions.js +10 -10
  40. package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
  41. package/dist/services/doctor/doctor-service/types.d.ts +25 -0
  42. package/dist/services/ide/adapters/claude-code-adapter.js +0 -1
  43. package/dist/services/ide/adapters/codex-adapter.js +1 -2
  44. package/dist/services/ide/adapters/cursor-adapter.js +1 -2
  45. package/dist/services/ide/adapters/hermes-adapter.js +1 -2
  46. package/dist/services/ide/adapters/openclaw-adapter.js +1 -2
  47. package/dist/services/ide/adapters/qoder-adapter.js +1 -2
  48. package/dist/services/ide/adapters/tongyi-lingma-adapter.js +1 -2
  49. package/dist/services/ide/adapters/trae-adapter.js +1 -2
  50. package/dist/services/ide/adapters/zcode-adapter.js +0 -1
  51. package/dist/services/ide/ide-types.d.ts +0 -2
  52. package/dist/services/memory/project-memory-service/index/kind-dispatch.js +48 -13
  53. package/dist/services/memory/project-memory-service/index.d.ts +5 -3
  54. package/dist/services/memory/project-memory-service/index.js +2 -2
  55. package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +15 -1
  56. package/dist/services/memory/project-memory-service/parsers/frontmatter.js +34 -6
  57. package/dist/services/memory/project-memory-service/parsers/markdown-pure.d.ts +27 -1
  58. package/dist/services/memory/project-memory-service/parsers/markdown-pure.js +92 -7
  59. package/dist/services/memory/project-memory-service/types.d.ts +86 -0
  60. package/dist/services/slice/slice-check-types.d.ts +1 -1
  61. package/dist/services/workspace/runtime-layout.d.ts +91 -0
  62. package/dist/services/workspace/runtime-layout.js +148 -0
  63. package/dist/services/workspace/workspace-claude-settings-materializer.js +14 -0
  64. package/package.json +6 -6
  65. package/scripts/clean-dist.mjs +15 -3
  66. package/scripts/sync-version.mjs +26 -4
  67. package/skills/bee/peaks-perf-audit/SKILL.md +2 -2
  68. package/skills/bee/peaks-perf-audit/references/audit-protocol.md +1 -1
  69. package/skills/bee/peaks-prd/SKILL.md +4 -4
  70. package/skills/bee/peaks-prd/references/prd-for-multi-pass.md +1 -1
  71. package/skills/bee/peaks-prd/references/workflow.md +1 -1
  72. package/skills/bee/peaks-qa/SKILL.md +6 -6
  73. package/skills/bee/peaks-qa/references/external-capability-guidance.md +1 -1
  74. package/skills/bee/peaks-qa/references/qa-fanout-contract.md +1 -1
  75. package/skills/bee/peaks-qa/references/qa-skill-presence.md +1 -1
  76. package/skills/bee/peaks-qa/references/reading-handoff-frontmatter.md +2 -2
  77. package/skills/bee/peaks-rd/SKILL.md +2 -2
  78. package/skills/bee/peaks-rd/references/code-reviewer-4dim-hint.md +1 -1
  79. package/skills/bee/peaks-rd/references/external-references.md +1 -1
  80. package/skills/bee/peaks-rd/references/mandatory-perf-baseline.md +1 -1
  81. package/skills/bee/peaks-rd/references/ocr-multilang-1.8.md +2 -2
  82. package/skills/bee/peaks-rd/references/parallel-review-fanout.md +2 -2
  83. package/skills/bee/peaks-rd/references/rd-fanout-contracts.md +11 -8
  84. package/skills/bee/peaks-rd/references/rd-runbook.md +1 -1
  85. package/skills/bee/peaks-rd/references/rd-sub-agent-dispatch.md +7 -7
  86. package/skills/bee/peaks-rd/references/rd-transition-gates.md +1 -1
  87. package/skills/bee/peaks-rd/references/reading-v2-slice-results.md +1 -1
  88. package/skills/bee/peaks-rd/references/skill-presence-and-title.md +1 -1
  89. package/skills/bee/peaks-rd/references/v2-12-fanout-collapse.md +7 -5
  90. package/skills/bee/peaks-rd/references/writing-handoff-frontmatter.md +3 -3
  91. package/skills/bee/peaks-reviewer/SKILL.md +1 -1
  92. package/skills/bee/peaks-sc/SKILL.md +1 -1
  93. package/skills/bee/peaks-security-audit/SKILL.md +3 -3
  94. package/skills/bee/peaks-security-audit/references/audit-protocol.md +1 -1
  95. package/skills/bee/peaks-txt/SKILL.md +3 -3
  96. package/skills/bee/peaks-txt/references/context-capsule.md +1 -1
  97. package/skills/bee/peaks-ui/SKILL.md +1 -1
  98. package/skills/peaks-audit/SKILL.md +1 -1
  99. package/skills/peaks-code/SKILL.md +9 -9
  100. package/skills/peaks-code/references/context-governance.md +1 -1
  101. package/skills/peaks-code/references/dag-orchestrator.md +3 -4
  102. package/skills/peaks-code/references/external-references.md +1 -1
  103. package/skills/peaks-code/references/external-skill-invocation.md +2 -2
  104. package/skills/peaks-code/references/fanout-mandatory.md +3 -3
  105. package/skills/peaks-code/references/frontend-only-mode.md +2 -2
  106. package/skills/peaks-code/references/gstack-integration.md +1 -1
  107. package/skills/peaks-code/references/micro-cycle.md +1 -1
  108. package/skills/peaks-code/references/periodic-checkpoint.md +2 -2
  109. package/skills/peaks-code/references/project-memory-loading.md +19 -1
  110. package/skills/peaks-code/references/project-scan-checklist.md +1 -1
  111. package/skills/peaks-code/references/resume-detection.md +1 -1
  112. package/skills/peaks-code/references/runbook.md +3 -3
  113. package/skills/peaks-code/references/session-overload-signal-index.md +2 -2
  114. package/skills/peaks-code/references/startup-sequence.md +16 -16
  115. package/skills/peaks-code/references/step-11-memory-sediment.md +3 -3
  116. package/skills/peaks-code/references/sub-agent-dispatch.md +7 -6
  117. package/skills/peaks-code/references/swarm-dispatch-contract.md +1 -1
  118. package/skills/peaks-code/references/workflow-gates-and-types.md +3 -3
  119. package/skills/peaks-code/references/worktree-governance.md +1 -1
  120. package/skills/peaks-final-review/SKILL.md +3 -3
  121. package/skills/peaks-ide/references/audit-log-helper.md +5 -4
  122. package/skills/peaks-resume/SKILL.md +1 -1
  123. package/skills/peaks-slice-decompose/SKILL.md +4 -4
  124. package/skills/peaks-slice-decompose/references/cross-pass-edge-interpretation.md +1 -1
  125. package/skills/peaks-slice-decompose/references/granularity-decision.md +1 -1
  126. package/skills/peaks-slice-decompose/references/v2-schema.md +2 -2
  127. package/skills/peaks-solo/SKILL.md +1 -2
@@ -0,0 +1,299 @@
1
+ // src/cli/commands/codegraph-status-command.ts
2
+ //
3
+ // `peaks codegraph status` — the two-axis integrity gate (exclude rules +
4
+ // index coverage), its `--peaks-json` machine envelope, and the attribution
5
+ // of upstream's `[OK] Index is up to date` line.
6
+ //
7
+ // Extracted verbatim from `codegraph-commands.ts` (rid
8
+ // 2026-09-17-oversize-and-scale, D1 — the 800-line file-size cap). Every moved
9
+ // line is byte-identical and no behaviour changed; `attributeUpstreamUpToDate
10
+ // Line` stays importable from `codegraph-commands.ts`, which re-exports it.
11
+ import { resolve } from 'node:path';
12
+ import { createCodegraphInvocation, executeCodegraphInvocation, isCodegraphInitialized } from '../../services/codegraph/codegraph-service.js';
13
+ import { CODEGRAPH_INTEGRITY_EXIT_CODE, inspectCodegraphExcludeIntegrity, isCodegraphExcludeConfigPresent, renderCodegraphExcludeIntegrityLines } from '../../services/codegraph/codegraph-exclude-integrity.js';
14
+ import { CODEGRAPH_INDEX_INTEGRITY_EXIT_CODE, CODEGRAPH_INDEX_STRICT_ENV_VAR, CODEGRAPH_REPAIR_INDEX_COMMAND, codegraphIndexIntegrityExitCode, inspectCodegraphIndexIntegrity, isCodegraphIndexStrictMode, renderCodegraphIndexIntegrityLines, resolveCodegraphIndexIntegrityVerdict } from '../../services/codegraph/codegraph-index-integrity.js';
15
+ import { readCodegraphProjectInputs } from '../../services/codegraph/codegraph-exclude-reconciler.js';
16
+ import { fail, ok } from 'peaks-loop-shared/result';
17
+ import { getErrorMessage, printResult, redactSensitiveErrorMessage } from '../cli-helpers.js';
18
+ import { printCodegraphFailure, rewriteBareCodegraphHints, runCodegraphCommand } from './codegraph-command-runtime.js';
19
+ const ANSI_SGR_PATTERN = /\x1b\[[0-9;]*m/g;
20
+ /**
21
+ * Upstream `status` answers a different question than peaks-loop's
22
+ * integrity gate: upstream says "the on-disk graph matches the last scan"
23
+ * (true), peaks says "that graph covers the repository" (false when rules
24
+ * exclude tracked files). Both verdicts are correct, but an unqualified
25
+ * `[OK] Index is up to date` printed above our `[FAIL] ...` reads as
26
+ * "nothing to see here" — and the OK is the line the eye lands on first.
27
+ * The exit code and the JSON envelope are already right; only this line
28
+ * lies by juxtaposition.
29
+ *
30
+ * So: keep upstream's wording — the line stays recognizable, and the
31
+ * Files/Nodes counts around it are untouched — but drop the bare OK
32
+ * marker and name the only question it answers. Clean runs never reach
33
+ * this, so their output stays byte-identical.
34
+ *
35
+ * The match is anchored to THAT line. An earlier version keyed on
36
+ * `includes('up to date')`, which is content-blind: upstream prints other
37
+ * `[OK] ... are up to date` lines (a language-server or watcher line is the
38
+ * observed one), and each of those was rewritten into a claim about the
39
+ * INDEX — a misattribution introduced by a change whose entire purpose was
40
+ * to stop misleading output. Anything that is not the index line is passed
41
+ * through byte-for-byte, tail note and all.
42
+ */
43
+ const INDEX_UP_TO_DATE_RE = /^\[OK\]\s+Index is up to date\b/i;
44
+ export function attributeUpstreamUpToDateLine(stdout) {
45
+ return stdout
46
+ .split('\n')
47
+ .map((line) => {
48
+ const visible = line.replace(ANSI_SGR_PATTERN, '').trim();
49
+ if (!INDEX_UP_TO_DATE_RE.test(visible)) {
50
+ return line;
51
+ }
52
+ // Only the OK marker is downgraded and the attribution appended: the
53
+ // rest of the line — including whatever upstream wrote after it — is
54
+ // preserved, so nothing upstream actually said is replaced.
55
+ const withoutOk = visible.replace(/^\[OK\]\s*/, '');
56
+ return `[i] ${withoutOk} (upstream: matches the last scan only; repository coverage is answered below)`;
57
+ })
58
+ .join('\n');
59
+ }
60
+ /**
61
+ * `--peaks-json` machine report for `status`. Carries the upstream
62
+ * result AND the peaks-loop integrity verdict as one JSON document so a
63
+ * CI job can gate on `data.integrity.gap` / `data.integrity.rulesToRemove`
64
+ * without scraping human text.
65
+ *
66
+ * EXIT-CODE PRECEDENCE (decided here, mirrored on the human path, pinned
67
+ * by tests on BOTH axes):
68
+ *
69
+ * 1. upstream failed → upstream's exit code. Ranked FIRST because
70
+ * every gate's remedy assumes upstream can run, and because the
71
+ * gate's own branches would otherwise MASK a real command failure:
72
+ * under the advisory default a gap exits 0, so a "gate first" order
73
+ * would turn "upstream failed + gap" into exit 0 — the gate hiding a
74
+ * failure. The gate verdicts stay in `data` verbatim, so ranking
75
+ * them lower hides nothing.
76
+ * 2. exclude gap → 74 (`CODEGRAPH_INDEX_INCOMPLETE`).
77
+ * 3. index not evaluated → 76 (`CODEGRAPH_INDEX_NOT_EVALUATED`): the
78
+ * axis was attempted and failed, which is NOT "verified clean".
79
+ * 4. index gap → 75 (`CODEGRAPH_INDEX_GAP`) in strict mode,
80
+ * 0 in the advisory default.
81
+ *
82
+ * The envelope still reports the finding in case 4 (`ok:false`, the same
83
+ * `code`, plus `indexIntegritySeverity: 'warning'`) — the policy governs
84
+ * the EXIT CODE, not whether the finding is visible. This mirrors the
85
+ * doctor's established convention: `ok:false` + `severity:'warning'`
86
+ * surfaces the finding without escalating the exit code.
87
+ */
88
+ async function runCodegraphStatusJson(io, options, integrity, integrityWarning, indexIntegrity, indexIntegrityWarning, indexVerdict, strict) {
89
+ let result;
90
+ try {
91
+ result = await executeCodegraphInvocation(createCodegraphInvocation({ subcommand: 'status', project: options.project }));
92
+ }
93
+ catch (error) {
94
+ printCodegraphFailure(io, 'codegraph.status', error, true);
95
+ return true;
96
+ }
97
+ const upstream = {
98
+ exitCode: result.exitCode,
99
+ stdout: rewriteBareCodegraphHints(result.stdout).trimEnd(),
100
+ stderr: redactSensitiveErrorMessage(rewriteBareCodegraphHints(result.stderr)).trimEnd()
101
+ };
102
+ const upstreamFailed = result.exitCode !== null && result.exitCode !== 0;
103
+ const data = {
104
+ upstream,
105
+ integrity,
106
+ integrityWarning,
107
+ indexIntegrity,
108
+ indexIntegrityWarning,
109
+ indexIntegrityVerdict: indexVerdict,
110
+ // Only meaningful where there IS a finding: a `clean` or `not-applicable`
111
+ // axis has no severity, and reporting `'warning'` there would read as
112
+ // "something was wrong but advisory".
113
+ indexIntegritySeverity: indexVerdict === 'gap' ? (strict ? 'error' : 'warning') : null
114
+ };
115
+ if (upstreamFailed) {
116
+ printResult(io, fail('codegraph.status', 'CODEGRAPH_COMMAND_FAILED', redactSensitiveErrorMessage(upstream.stderr || upstream.stdout || `codegraph exited with code ${String(result.exitCode)}`), data, ['Check the codegraph project path before retrying']), true);
117
+ }
118
+ else if (integrity?.gap === true) {
119
+ printResult(io, fail('codegraph.status', 'CODEGRAPH_INDEX_INCOMPLETE', `codegraph index is incomplete: ${integrity.excludedTrackedCount} of ${integrity.trackedSourceCount} tracked source files are excluded by ${integrity.rulesToRemove.length} rule(s).`, data, ['Run `peaks codegraph repair-exclude --project <root>` to drop the offending rules and rebuild the index.']), true);
120
+ }
121
+ else if (indexVerdict === 'not-evaluated') {
122
+ // Distinct from the `ok` branch below on purpose: this axis produced
123
+ // NO verdict, and the envelope must not read as "index verified".
124
+ printResult(io, fail('codegraph.status', 'CODEGRAPH_INDEX_NOT_EVALUATED', `codegraph index integrity could not be evaluated: ${indexIntegrityWarning ?? 'unknown cause'}`, data, [
125
+ 'The index was NOT measured — this is not a statement that the index is correct.',
126
+ 'Re-run once the cause above is resolved.'
127
+ ]), true);
128
+ }
129
+ else if (indexVerdict === 'gap') {
130
+ printResult(io, fail('codegraph.status', 'CODEGRAPH_INDEX_GAP', `codegraph index does not cover the repository: ${indexIntegrity?.includeGap.length ?? 0} extractor-supported tracked file(s) are not admitted by the config's include globs, and ${indexIntegrity?.deadRows.length ?? 0} index row(s) point at files that no longer exist.`, data, strict
131
+ ? [
132
+ 'The index must admit every extractor-supported tracked file and hold no row for a path that is gone.',
133
+ `Run \`${CODEGRAPH_REPAIR_INDEX_COMMAND}\` to add the missing include pattern(s) and rebuild the index without the stale rows.`
134
+ ]
135
+ : [
136
+ 'Advisory: this gap is reported as a warning and the command exits 0.',
137
+ `Set ${CODEGRAPH_INDEX_STRICT_ENV_VAR}=1 to make it blocking (exit ${CODEGRAPH_INDEX_INTEGRITY_EXIT_CODE}).`,
138
+ `Run \`${CODEGRAPH_REPAIR_INDEX_COMMAND}\` to add the missing include pattern(s) and rebuild the index without the stale rows.`
139
+ ]), true);
140
+ }
141
+ else {
142
+ printResult(io, ok('codegraph.status', data), true);
143
+ }
144
+ if (upstreamFailed) {
145
+ process.exitCode = result.exitCode ?? 1;
146
+ }
147
+ return upstreamFailed;
148
+ }
149
+ /**
150
+ * `peaks codegraph status` with an integrity gate.
151
+ *
152
+ * The upstream status is still proxied verbatim (that is what the
153
+ * command has always done), but a clean upstream "index is up to date"
154
+ * is no longer sufficient: when git-tracked source files are being
155
+ * excluded by the config, or the index itself does not cover the
156
+ * repository, the command says so and names the rules and files.
157
+ *
158
+ * SEVERITY (user decision, option C): the exclude gate keeps its shipped
159
+ * blocking behaviour (exit 74); the index gate is ADVISORY by default and
160
+ * blocking only when `PEAKS_CODEGRAPH_INDEX_STRICT` is set. A detected
161
+ * gap is the finding either way — only the tag and the exit code move.
162
+ *
163
+ * Read-only by construction — it imports the integrity inspectors, never
164
+ * the repair writer. Fixing the config is `peaks codegraph init`
165
+ * (fresh) or `peaks codegraph repair-exclude` (explicit).
166
+ */
167
+ export async function runCodegraphStatusCommand(io, options, asJson) {
168
+ let integrity = null;
169
+ let integrityWarning = null;
170
+ let indexIntegrity = null;
171
+ let indexIntegrityWarning = null;
172
+ const projectRoot = resolve(options.project);
173
+ const strict = isCodegraphIndexStrictMode();
174
+ // R12-1: the two axes have DIFFERENT applicability predicates, and folding
175
+ // them into the config's alone is how a db-present / config-absent project
176
+ // came to exit 0 in SILENCE — under `PEAKS_CODEGRAPH_INDEX_STRICT=1` too.
177
+ // "Could not evaluate" is a verdict; it may not be spelled the same way as
178
+ // "verified clean" merely because the read happened to be gated out.
179
+ //
180
+ // - exclude axis: applicable iff `.codegraph/config.json` exists. The
181
+ // config IS the subject of that axis, so with no config there is no
182
+ // exclusion list in play — `not-applicable`, silent, as before.
183
+ // - index axis: applicable iff `.codegraph/codegraph.db` exists — the
184
+ // SAME predicate the doctor's `defaultProbe` uses
185
+ // (`isCodegraphInitialized` in
186
+ // `doctor-service/checks/codegraph-index-integrity.ts`), so the two
187
+ // consumers now agree about when the axis is evaluable at all. The
188
+ // index is the subject here and the config is only an INPUT it reads,
189
+ // so a missing config is "could not evaluate" (exit 76, `[FAIL]`),
190
+ // not "nothing to evaluate".
191
+ //
192
+ // The user's constraint holds either way: an absent config is LEGITIMATE
193
+ // for a project that never ran `peaks codegraph init` — with no db BOTH
194
+ // predicates are false and the command stays silent exactly as before.
195
+ const configPresent = isCodegraphExcludeConfigPresent(projectRoot);
196
+ const indexPresent = isCodegraphInitialized(projectRoot);
197
+ // The two axes are evaluated INDEPENDENTLY: an unreadable index must not
198
+ // blind the exclude verdict, and a malformed exclude list must not hide a
199
+ // stale index. Each failure degrades to its own warning.
200
+ if (configPresent || indexPresent) {
201
+ // Perf audit F1: both axes need the SAME tracked-file list and the SAME
202
+ // config, and reading them per-axis spawned `git ls-files` twice per
203
+ // command and re-ran the identical 32-glob `include` filter. Read once
204
+ // and hand the values to both inspectors through their deps seam.
205
+ let sharedInputs = null;
206
+ try {
207
+ sharedInputs = readCodegraphProjectInputs(projectRoot);
208
+ }
209
+ catch (error) {
210
+ // One read, two blind axes — they consume exactly these two inputs,
211
+ // so a failure here blinds both. Report it on both, rather than
212
+ // letting the second axis re-run the same failing read to find out.
213
+ // Each axis claims the failure only where it was applicable at all:
214
+ // an unreadable CONFIG is not an exclude-axis finding on a project
215
+ // that has no config by design.
216
+ if (configPresent) {
217
+ integrityWarning = getErrorMessage(error);
218
+ }
219
+ if (indexPresent) {
220
+ indexIntegrityWarning = getErrorMessage(error);
221
+ }
222
+ }
223
+ if (sharedInputs !== null) {
224
+ if (configPresent) {
225
+ try {
226
+ integrity = inspectCodegraphExcludeIntegrity(projectRoot, sharedInputs);
227
+ }
228
+ catch (error) {
229
+ integrityWarning = getErrorMessage(error);
230
+ }
231
+ }
232
+ // The index axis needs an index. A config without `codegraph.db` is a
233
+ // pre-init / dangling state, not a defect: there are no rows to be
234
+ // stale and nothing `include` withheld from a graph that does not exist.
235
+ if (indexPresent) {
236
+ try {
237
+ indexIntegrity = inspectCodegraphIndexIntegrity(projectRoot, sharedInputs);
238
+ }
239
+ catch (error) {
240
+ indexIntegrityWarning = getErrorMessage(error);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ const indexVerdict = resolveCodegraphIndexIntegrityVerdict(indexIntegrity, indexIntegrityWarning);
246
+ const indexGap = indexVerdict === 'gap';
247
+ if (asJson === true) {
248
+ const upstreamFailed = await runCodegraphStatusJson(io, options, integrity, integrityWarning, indexIntegrity, indexIntegrityWarning, indexVerdict, strict);
249
+ // Upstream failure outranks both gates — and it already set the exit
250
+ // code to upstream's (see `runCodegraphStatusJson`).
251
+ if (upstreamFailed) {
252
+ return;
253
+ }
254
+ }
255
+ else {
256
+ // Only when a gate found a gap: upstream's `[OK] Index is up
257
+ // to date` answers "consistent with the last scan", and printing it
258
+ // unqualified right above our verdict tells the reader two opposite
259
+ // things at once. Clean runs get no transform and stay byte-identical.
260
+ const upstreamFailed = await runCodegraphCommand(io, 'codegraph.status', { subcommand: 'status', project: options.project }, false, integrity?.gap === true || indexGap ? attributeUpstreamUpToDateLine : undefined);
261
+ if (integrityWarning !== null) {
262
+ io.stdout(`[WARN] codegraph exclude integrity not evaluated: ${integrityWarning}`);
263
+ }
264
+ else if (integrity !== null) {
265
+ for (const line of renderCodegraphExcludeIntegrityLines(integrity)) {
266
+ io.stdout(line);
267
+ }
268
+ }
269
+ if (indexVerdict === 'not-evaluated') {
270
+ // `[FAIL]`, in every mode, because the command exits non-zero in
271
+ // every mode: the axis was attempted and failed, so this is not a
272
+ // statement about the index. Distinct wording from the gap line so
273
+ // the two can never be confused.
274
+ io.stdout(`[FAIL] codegraph index integrity could not be evaluated (the index was NOT measured): ${indexIntegrityWarning ?? 'unknown cause'}`);
275
+ }
276
+ else if (indexIntegrity !== null) {
277
+ for (const line of renderCodegraphIndexIntegrityLines(indexIntegrity, strict)) {
278
+ io.stdout(line);
279
+ }
280
+ }
281
+ // Upstream failure outranks both gates — and it already set the exit
282
+ // code to upstream's.
283
+ if (upstreamFailed) {
284
+ return;
285
+ }
286
+ }
287
+ // Gate precedence: exclude wins when both fire (it is the upstream cause,
288
+ // and repairing it also rebuilds the index the staleness axis is about).
289
+ // Then "not evaluated" outranks "gap", because an unmeasured axis must
290
+ // never be reported with the same status as a measured one. See
291
+ // `codegraphIndexIntegrityExitCode`.
292
+ const indexExitCode = codegraphIndexIntegrityExitCode(indexVerdict, strict);
293
+ if (integrity?.gap === true) {
294
+ process.exitCode = CODEGRAPH_INTEGRITY_EXIT_CODE;
295
+ }
296
+ else if (indexExitCode !== null) {
297
+ process.exitCode = indexExitCode;
298
+ }
299
+ }
@@ -1,4 +1,4 @@
1
- import { executeProjectMemoryBackup, executeProjectMemoryExtract, summarizeProjectMemoryBackupResult, summarizeProjectMemoryExtractResult, VALID_PROJECT_MEMORY_KINDS } from '../../../services/memory/project-memory-service.js';
1
+ import { describeMemoryBlockDrops, executeProjectMemoryBackup, executeProjectMemoryExtract, summarizeProjectMemoryBackupResult, summarizeProjectMemoryExtractResult, VALID_PROJECT_MEMORY_KINDS } from '../../../services/memory/project-memory-service.js';
2
2
  import { fail, ok } from 'peaks-loop-shared/result';
3
3
  import { addJsonOption, getErrorMessage, printResult } from '../../cli-helpers.js';
4
4
  /** Derived from the canonical kind vocabulary — never hand-maintain a list here. */
@@ -19,7 +19,11 @@ export function registerMemoryCommand(program, io) {
19
19
  }
20
20
  try {
21
21
  const result = executeProjectMemoryExtract({ projectRoot: options.project, artifactPaths: options.artifact, apply: options.apply === true });
22
- printResult(io, ok('memory.extract', summarizeProjectMemoryExtractResult(result)), options.json);
22
+ // A block that was found but not extracted must not vanish silently: the
23
+ // parser's rejection reasons ride the envelope's existing `warnings`
24
+ // channel (JSON: `warnings[]`; human: `warning: …` on stderr). `data` is
25
+ // unchanged — this adds no field to the summary.
26
+ printResult(io, ok('memory.extract', summarizeProjectMemoryExtractResult(result), describeMemoryBlockDrops(result.droppedBlocks)), options.json);
23
27
  }
24
28
  catch (error) {
25
29
  printResult(io, fail('memory.extract', 'MEMORY_EXTRACT_FAILED', getErrorMessage(error), {}, ['Check artifact paths and remove secrets before extracting memory']), options.json);
@@ -15,7 +15,51 @@ import { JobInitInputSchema, JobCheckpointInputSchema, JobBlockInputSchema, } fr
15
15
  import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
16
16
  import { buildCostCheckEnvelope, runKarpathyCostCheck, } from '../../services/karpathy-cost/karpathy-cost-check-service.js';
17
17
  import { read24hState } from '../../services/24h-mode/store.js';
18
- import { refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
18
+ import { codegraphRefreshNotice, refreshCodegraphAfterSlice, } from '../../services/codegraph/codegraph-autorefresh.js';
19
+ // `printResult`'s third parameter is `asJson: boolean`. Each `job` subcommand
20
+ // registers `--json` (see the `addJsonOption` calls below), so the flag to pass
21
+ // it is `opts.json` — NEVER the whole options object. Commander types an
22
+ // action's `opts` as `any`, so `printResult(io, envelope, opts)` type-checks
23
+ // and is ALWAYS truthy: it forces the envelope branch and makes the
24
+ // `warning: ` / `next: ` human rendering in `cli-helpers.ts` unreachable. That
25
+ // is how the A2 codegraph-refresh warning ended up inside the JSON stdout
26
+ // envelope instead of on stderr as `warning: `. `job progress` always passed
27
+ // `opts.json`; the rest of this file did not, and E1 (2026-09-17) brought them
28
+ // in line. Do not reintroduce the object form.
29
+ function asJson(opts) {
30
+ return opts.json === true;
31
+ }
32
+ /**
33
+ * F1 (rid 2026-09-17-exit-code-truth): report a FAILED envelope **and** make the
34
+ * process exit non-zero.
35
+ *
36
+ * WHY THIS EXISTS. `printResult` (`src/cli/cli-helpers.ts`) renders a failed
37
+ * envelope — `CODE: message` + `nextActions` on stderr — but it does NOT set
38
+ * `process.exitCode`. Measured with the real CLI before this fix:
39
+ * `peaks job block --job-id j1 --slice-id nope --reason why` printed
40
+ * `SLICE_NOT_FOUND: …` on stderr and exited **0**, so CI and every script
41
+ * wrapping `peaks job` read the failure as success. Seven of this file's nine
42
+ * failure-reporting sites had that gap; only `job progress` set the code, and
43
+ * that was the file's one accidental precedent rather than a rule.
44
+ *
45
+ * Every `fail(...)` envelope in this file now goes through here, so "reported a
46
+ * failure" and "exited non-zero" cannot drift apart again. The helper takes the
47
+ * Commander `opts` object and calls `asJson(opts)` itself — it must never be
48
+ * handed a bare `opts.json`-less boolean, and it must never pass `opts` to
49
+ * `printResult` (see the `asJson` comment above for that bug's history).
50
+ *
51
+ * DELIBERATELY NOT APPLIED TO THE ADVISORY PATHS. Three sites in this file
52
+ * report a non-fatal outcome through an `ok(...)` envelope and MUST keep
53
+ * exiting 0: the two `emitJobEvent` best-effort catches (`job init`,
54
+ * `job status`) and the post-slice codegraph refresh in `job checkpoint`, whose
55
+ * stated design is "a refresh failure must never fail the checkpoint". Those
56
+ * are annotated in place; `tests/unit/cli/job-exit-code.test.ts` pins both
57
+ * directions so a future blanket "every warning exits 1" edit turns red.
58
+ */
59
+ function failResult(io, result, opts) {
60
+ printResult(io, result, asJson(opts));
61
+ process.exitCode = 1;
62
+ }
19
63
  function projectRoot(opts) {
20
64
  // Reuse the workspace root resolver from peaks CLI; for now, CWD as a safe placeholder.
21
65
  return opts.project ?? process.cwd();
@@ -125,7 +169,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
125
169
  // a random UUID would scatter state across dirs and break resume/auto-compact.
126
170
  let sessionId = opts.sessionId ?? process.env.PEAKS_SESSION_ID ?? getCurrentSessionId(project);
127
171
  if (!sessionId) {
128
- return printResult(io, fail('init', 'NO_ACTIVE_SESSION', 'peaks job init requires --session-id (or an active peaks-code session via peaks workspace init)', { project }, [
172
+ return failResult(io, fail('init', 'NO_ACTIVE_SESSION', 'peaks job init requires --session-id (or an active peaks-code session via peaks workspace init)', { project }, [
129
173
  'Re-run with --session-id <sid>',
130
174
  'Or run `peaks workspace init` to create a session first'
131
175
  ]), opts);
@@ -142,7 +186,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
142
186
  json: opts.json,
143
187
  });
144
188
  if (!parsed.success)
145
- return printResult(io, fail('init', 'INVALID_INIT', parsed.error.message, {}), opts);
189
+ return failResult(io, fail('init', 'INVALID_INIT', parsed.error.message, {}), opts);
146
190
  const jobRoot = resolveJobStateRoot(opts);
147
191
  const store = new JobStateStore(jobRoot.rootDir);
148
192
  const orch = new JobOrchestrator(store);
@@ -159,10 +203,14 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
159
203
  emitJobEvent({ kind: 'job-started', jobId: state.jobId, total: state.slices.length, strategy: state.mainLoopStrategy });
160
204
  }
161
205
  catch (e) {
162
- // best-effort: event emission failures must not abort job init
206
+ // ADVISORY (F1, 2026-09-17) — deliberately exits 0. Event emission is a
207
+ // telemetry side effect; a failed emit does not mean `job init` failed
208
+ // (the state file was already written above). This catch must NOT be
209
+ // routed through `failResult`. Pinned by the "advisory stays 0" control
210
+ // in tests/unit/cli/job-exit-code.test.ts.
163
211
  void e;
164
212
  }
165
- printResult(io, ok('init', { jobId: state.jobId, sliceCount: state.slices.length, statePath: `${jobRoot.rootDir}/${state.jobId}/state.json` }), opts);
213
+ printResult(io, ok('init', { jobId: state.jobId, sliceCount: state.slices.length, statePath: `${jobRoot.rootDir}/${state.jobId}/state.json` }), asJson(opts));
166
214
  });
167
215
  addJsonOption(job.commands.find(c => c.name() === 'init'));
168
216
  job
@@ -191,10 +239,12 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
191
239
  emitJobEvent({ kind: 'job-progress', jobId: opts.jobId, done: s.done, total: s.total, ...(s.currentSlice ? { currentSlice: s.currentSlice } : {}) });
192
240
  }
193
241
  catch (e) {
194
- // best-effort: event emission failures must not abort job status
242
+ // ADVISORY (F1, 2026-09-17) — deliberately exits 0. The status itself
243
+ // was already read successfully; the emit is telemetry. Same rule as
244
+ // the `job init` catch above: do NOT route through `failResult`.
195
245
  void e;
196
246
  }
197
- printResult(io, ok('status', s), opts);
247
+ printResult(io, ok('status', s), asJson(opts));
198
248
  });
199
249
  addJsonOption(job.commands.find(c => c.name() === 'status'));
200
250
  // M4.2: wire rotate-now to JobRotation (session-rotate callbacks are stubs pending M6.5 batch-fix).
@@ -206,7 +256,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
206
256
  const store = new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir);
207
257
  const rotation = new JobRotation(store, async (_jid) => { /* delegate to peaks session rotate — implementation wired in M6.5 batch-fix */ return { rotated: true }; }, async (jid) => ({ jobId: jid, cycle: 0 }));
208
258
  const r = await rotation.rotateNow(opts.jobId);
209
- printResult(io, ok('rotate-now', r), opts);
259
+ printResult(io, ok('rotate-now', r), asJson(opts));
210
260
  });
211
261
  addJsonOption(job.commands.find(c => c.name() === 'rotate-now'));
212
262
  job.command('subagent-cleanup')
@@ -218,7 +268,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
218
268
  .action(async (opts) => {
219
269
  const wrapper = new SubAgentJobWrapper(new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir), async () => ({ batchId: opts.batchId }));
220
270
  const r = await wrapper.cleanup({ jobId: opts.jobId, batchId: opts.batchId, force: !!opts.force });
221
- printResult(io, ok('subagent-cleanup', r), opts);
271
+ printResult(io, ok('subagent-cleanup', r), asJson(opts));
222
272
  });
223
273
  addJsonOption(job.commands.find(c => c.name() === 'subagent-cleanup'));
224
274
  // M3.2: wire the remaining 5 subcommand slots — block, checkpoint, continue, handoff, resume.
@@ -238,7 +288,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
238
288
  project: projectRoot(opts), json: opts.json,
239
289
  });
240
290
  if (!parsed.success)
241
- return printResult(io, fail('checkpoint', 'INVALID_CHECKPOINT', parsed.error.message, {}), opts);
291
+ return failResult(io, fail('checkpoint', 'INVALID_CHECKPOINT', parsed.error.message, {}), opts);
242
292
  const jobRoot = resolveJobStateRoot(opts, opts.jobId);
243
293
  const store = new JobStateStore(jobRoot.rootDir);
244
294
  // D7: `--slice-id` accepts the canonical `slice-NNN` or the slice's label
@@ -246,7 +296,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
246
296
  // so progress.json is never touched by a checkpoint that matched nothing.
247
297
  const slice = resolveSliceId(store, parsed.data.jobId, parsed.data.sliceId);
248
298
  if ('message' in slice) {
249
- return printResult(io, fail('checkpoint', 'SLICE_NOT_FOUND', slice.message, {
299
+ return failResult(io, fail('checkpoint', 'SLICE_NOT_FOUND', slice.message, {
250
300
  jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, validSliceIds: slice.validSliceIds,
251
301
  }, ['Re-run with one of the valid slice ids']), opts);
252
302
  }
@@ -256,6 +306,9 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
256
306
  // envelope carries a non-blocking `codegraph` result; null for
257
307
  // failed/skipped (no slice-complete boundary).
258
308
  let codegraph = null;
309
+ // A2 (2026-09-17): the human-visible half of the refresh outcome. null
310
+ // when the refresh succeeded or when no codegraph store was in use.
311
+ let codegraphWarning = null;
259
312
  if (parsed.data.state === 'done') {
260
313
  await orch.checkpointDone({ jobId: parsed.data.jobId, sliceId, ...(parsed.data.commitSha ? { commitSha: parsed.data.commitSha } : {}) });
261
314
  // v3.1.2: after each --state done, mirror slice progress to
@@ -272,14 +325,31 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
272
325
  lastCommitSha: parsed.data.commitSha ?? null,
273
326
  updatedAt: new Date().toISOString()
274
327
  });
275
- // Auto codegraph refresh at the slice-complete boundary. Best-effort
276
- // and fail-silent: a refresh failure must never fail the checkpoint.
328
+ // Auto codegraph refresh at the slice-complete boundary. Best-effort:
329
+ // a refresh failure must never fail the checkpoint.
330
+ //
331
+ // A2 (2026-09-17): "must never fail the checkpoint" is not "must
332
+ // never be seen". A refresh that did not happen while a codegraph
333
+ // store IS in use is now a warning line (stderr, `warning: ` prefix,
334
+ // via printResult) naming the reason and the remedy. See
335
+ // `codegraphRefreshNotice` for why `no-codegraph-dir` stays silent.
336
+ //
337
+ // ADVISORY (F1, 2026-09-17) — deliberately exits 0, and the reach of
338
+ // that word is exactly here. The checkpoint itself SUCCEEDED (the slice
339
+ // flipped to done and progress.json was mirrored above); the refresh is
340
+ // a derived index, rebuildable on demand. So the outcome is reported
341
+ // through `ok(...)` + a `warning:` line, never through `failResult`,
342
+ // and `process.exitCode` is left alone even when the refresh throws.
343
+ // Raising it would turn an advisory rebuild into a build breaker for
344
+ // every CI that runs `peaks job checkpoint`. Pinned by the "advisory
345
+ // stays 0" control in tests/unit/cli/job-exit-code.test.ts.
277
346
  try {
278
347
  codegraph = await refreshCodegraphAfterSlice(project);
279
348
  }
280
349
  catch (e) {
281
350
  codegraph = { refreshed: false, reason: 'unavailable', note: `auto codegraph refresh failed: ${e instanceof Error ? e.message : String(e)}` };
282
351
  }
352
+ codegraphWarning = codegraphRefreshNotice(codegraph);
283
353
  }
284
354
  else if (parsed.data.state === 'skipped') {
285
355
  await orch.checkpointSkipped({ jobId: parsed.data.jobId, sliceId, reason: parsed.data.reason });
@@ -287,7 +357,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
287
357
  else {
288
358
  await orch.checkpointFailed({ jobId: parsed.data.jobId, sliceId, reason: parsed.data.reason });
289
359
  }
290
- printResult(io, ok('checkpoint', { sliceId, status: parsed.data.state, codegraph }), opts);
360
+ printResult(io, ok('checkpoint', { sliceId, status: parsed.data.state, codegraph }, codegraphWarning === null ? [] : [codegraphWarning]), asJson(opts));
291
361
  });
292
362
  addJsonOption(job.commands.find(c => c.name() === 'checkpoint'));
293
363
  job
@@ -303,19 +373,19 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
303
373
  project: projectRoot(opts), json: opts.json,
304
374
  });
305
375
  if (!parsed.success)
306
- return printResult(io, fail('block', 'INVALID_BLOCK', parsed.error.message, {}), opts);
376
+ return failResult(io, fail('block', 'INVALID_BLOCK', parsed.error.message, {}), opts);
307
377
  const store = new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir);
308
378
  // D7 (same silent no-op as checkpoint): resolve label → sliceId, reject a
309
379
  // miss before any write.
310
380
  const slice = resolveSliceId(store, parsed.data.jobId, parsed.data.sliceId);
311
381
  if ('message' in slice) {
312
- return printResult(io, fail('block', 'SLICE_NOT_FOUND', slice.message, {
382
+ return failResult(io, fail('block', 'SLICE_NOT_FOUND', slice.message, {
313
383
  jobId: parsed.data.jobId, sliceId: parsed.data.sliceId, validSliceIds: slice.validSliceIds,
314
384
  }, ['Re-run with one of the valid slice ids']), opts);
315
385
  }
316
386
  const orch = new JobOrchestrator(store);
317
387
  await orch.blockSlice({ ...parsed.data, sliceId: slice.sliceId });
318
- printResult(io, ok('block', { blocked: slice.sliceId, reason: parsed.data.reason }), opts);
388
+ printResult(io, ok('block', { blocked: slice.sliceId, reason: parsed.data.reason }), asJson(opts));
319
389
  });
320
390
  addJsonOption(job.commands.find(c => c.name() === 'block'));
321
391
  job
@@ -327,7 +397,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
327
397
  const store = new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir);
328
398
  const orch = new JobOrchestrator(store);
329
399
  const r = orch.continueNow(opts.jobId);
330
- printResult(io, ok('continue', r), opts);
400
+ printResult(io, ok('continue', r), asJson(opts));
331
401
  });
332
402
  addJsonOption(job.commands.find(c => c.name() === 'continue'));
333
403
  job
@@ -339,7 +409,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
339
409
  const store = new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir);
340
410
  const orch = new JobOrchestrator(store);
341
411
  const s = orch.status(opts.jobId);
342
- printResult(io, ok('resume', { resumed: opts.jobId, ...s }), opts);
412
+ printResult(io, ok('resume', { resumed: opts.jobId, ...s }), asJson(opts));
343
413
  });
344
414
  addJsonOption(job.commands.find(c => c.name() === 'resume'));
345
415
  // v3.1.2: read the on-disk slice progress mirror written by `peaks
@@ -353,7 +423,19 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
353
423
  .requiredOption('--job-id <jid>')
354
424
  .option('--session-id <sid>', SESSION_ID_HELP)
355
425
  .option('--project <repo>')
356
- .option('--allow-missing', 'return done=0/total=0 envelope instead of failing when progress.json is absent')
426
+ // H3 (rid 2026-09-17-exit-code-root-cause): this string used to promise
427
+ // "return done=0/total=0 envelope instead of failing when progress.json is
428
+ // absent". No code path has ever returned such an envelope — the option was
429
+ // born in `d9a1a098` with `process.exitCode = 1` on the same branch — so it
430
+ // described a contract that never existed, and `--allow-missing` appeared to
431
+ // be broken. It is not: its job is to pick WHICH structured code reports the
432
+ // absence. The help text was the side that was wrong; it now says what the
433
+ // flag does. See `rd/requests/013-…-exit-code-root-cause.md` for why the
434
+ // alternative reading was rejected (the literal promise is also
435
+ // unimplementable without a new absent-vs-corrupt distinction:
436
+ // `tryReadJobProgress` returns `null` for a corrupt file too, so "done=0"
437
+ // there would report unreadable state as empty state).
438
+ .option('--allow-missing', 'report an absent progress.json as NO_PROGRESS (expected absence) rather than PROGRESS_READ_FAILED (read error); the command still exits non-zero')
357
439
  .action(async (opts) => {
358
440
  try {
359
441
  const jobRoot = resolveJobStateRoot(opts, opts.jobId);
@@ -363,20 +445,29 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
363
445
  ? tryReadJobProgress(project, sessId, opts.jobId)
364
446
  : readJobProgress(project, sessId, opts.jobId);
365
447
  if (progress === null) {
366
- printResult(io, fail('progress', 'NO_PROGRESS', `No progress.json for job ${opts.jobId} at .peaks/_runtime/${sessId}/job/${opts.jobId}/progress.json`, { jobId: opts.jobId, sessionId: sessId }, [
448
+ // F1: this site already set `process.exitCode = 1` by hand; it is
449
+ // routed through `failResult` so all nine failure sites in this file
450
+ // share one rule instead of this one being the lone precedent.
451
+ failResult(io, fail('progress', 'NO_PROGRESS', `No progress.json for job ${opts.jobId} at .peaks/_runtime/${sessId}/job/${opts.jobId}/progress.json`, { jobId: opts.jobId, sessionId: sessId }, [
367
452
  'Run `peaks job checkpoint --state done ...` at least once to seed progress.json.',
368
- 'Or pass --allow-missing to return a zero-progress envelope.'
369
- ]), opts.json);
370
- process.exitCode = 1;
453
+ // H3: the second action used to read "Or pass --allow-missing to
454
+ // return a zero-progress envelope." That line could only ever be
455
+ // printed when `--allow-missing` had ALREADY been passed — this
456
+ // `progress === null` branch is unreachable otherwise, because the
457
+ // no-flag path throws into the catch below and reports
458
+ // PROGRESS_READ_FAILED. So the CLI was advising the caller to pass
459
+ // the flag they had just passed, to obtain an envelope that does
460
+ // not exist. Replaced with the fact the caller actually needs.
461
+ '--allow-missing selects this NO_PROGRESS envelope over a PROGRESS_READ_FAILED read error; it does not change the exit code.'
462
+ ]), opts);
371
463
  return;
372
464
  }
373
465
  printResult(io, ok('progress', progress, [], [
374
466
  `Next: slice #${progress.done + 1} of ${progress.total} (${progress.currentSlice})`
375
- ]), opts.json);
467
+ ]), asJson(opts));
376
468
  }
377
469
  catch (err) {
378
- printResult(io, fail('progress', 'PROGRESS_READ_FAILED', err instanceof Error ? err.message : String(err), { jobId: opts.jobId }, ['Verify the job id and try again']), opts.json);
379
- process.exitCode = 1;
470
+ failResult(io, fail('progress', 'PROGRESS_READ_FAILED', err instanceof Error ? err.message : String(err), { jobId: opts.jobId }, ['Verify the job id and try again']), opts);
380
471
  }
381
472
  });
382
473
  addJsonOption(job.commands.find(c => c.name() === 'progress'));
@@ -389,7 +480,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
389
480
  const store = new JobStateStore(resolveJobStateRoot(opts, opts.jobId).rootDir);
390
481
  const orch = new JobOrchestrator(store);
391
482
  const s = orch.status(opts.jobId);
392
- printResult(io, ok('handoff', { handoffFor: opts.jobId, ...s }), opts);
483
+ printResult(io, ok('handoff', { handoffFor: opts.jobId, ...s }), asJson(opts));
393
484
  });
394
485
  addJsonOption(job.commands.find(c => c.name() === 'handoff'));
395
486
  job
@@ -402,7 +493,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
402
493
  const project = projectRoot(opts);
403
494
  const sessionId = opts.sessionId ?? process.env.PEAKS_SESSION_ID ?? getCurrentSessionId(project);
404
495
  if (!sessionId) {
405
- return printResult(io, fail('karpathy-cost-check', 'NO_ACTIVE_SESSION', 'karpathy-cost-check requires --session-id (or an active peaks-code session)', { project }, [
496
+ return failResult(io, fail('karpathy-cost-check', 'NO_ACTIVE_SESSION', 'karpathy-cost-check requires --session-id (or an active peaks-code session)', { project }, [
406
497
  'Re-run with --session-id <sid>',
407
498
  'Or run `peaks workspace init` to create a session first',
408
499
  ]), opts);
@@ -420,7 +511,7 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
420
511
  reviewFilePath: opts.reviewFile,
421
512
  is24hModeActive,
422
513
  });
423
- printResult(io, buildCostCheckEnvelope(out), opts);
514
+ printResult(io, buildCostCheckEnvelope(out), asJson(opts));
424
515
  });
425
516
  addJsonOption(job.commands.find(c => c.name() === 'karpathy-cost-check'));
426
517
  program.addCommand(job);