peaks-loop 4.0.51 → 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.
- package/CHANGELOG.md +20 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/baseline-commands.js +11 -1
- package/dist/cli/commands/codegraph-command-runtime.d.ts +28 -0
- package/dist/cli/commands/codegraph-command-runtime.js +72 -0
- package/dist/cli/commands/codegraph-commands.d.ts +2 -11
- package/dist/cli/commands/codegraph-commands.js +173 -228
- package/dist/cli/commands/codegraph-status-command.d.ts +22 -0
- package/dist/cli/commands/codegraph-status-command.js +299 -0
- package/dist/cli/commands/core/memory-command.js +6 -2
- package/dist/cli/commands/job-commands.js +121 -30
- package/dist/cli/commands/project-commands.js +13 -3
- package/dist/cli/commands/request-commands.js +19 -8
- package/dist/cli/commands/slice-commands.js +2 -2
- package/dist/services/artifacts/artifact-prerequisites.js +23 -1
- package/dist/services/codegraph/codegraph-autorefresh.d.ts +16 -0
- package/dist/services/codegraph/codegraph-autorefresh.js +51 -5
- package/dist/services/codegraph/codegraph-config-repair-writer.d.ts +88 -0
- package/dist/services/codegraph/codegraph-config-repair-writer.js +322 -0
- package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +20 -2
- package/dist/services/codegraph/codegraph-exclude-integrity.js +24 -3
- package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +23 -2
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +123 -12
- package/dist/services/codegraph/codegraph-exclude-repair.d.ts +109 -55
- package/dist/services/codegraph/codegraph-exclude-repair.js +249 -195
- package/dist/services/codegraph/codegraph-include-reconciler.d.ts +10 -0
- package/dist/services/codegraph/codegraph-include-reconciler.js +160 -0
- package/dist/services/codegraph/codegraph-index-integrity.d.ts +268 -0
- package/dist/services/codegraph/codegraph-index-integrity.js +471 -0
- package/dist/services/codegraph/codegraph-service.d.ts +54 -0
- package/dist/services/codegraph/codegraph-service.js +84 -1
- package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +19 -4
- package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.d.ts +54 -0
- package/dist/services/doctor/doctor-service/checks/codegraph-index-integrity.js +151 -0
- package/dist/services/doctor/doctor-service/checks/l3-orphan-sessions.js +10 -10
- package/dist/services/doctor/doctor-service/plugin-registry.js +2 -0
- package/dist/services/doctor/doctor-service/types.d.ts +25 -0
- package/dist/services/memory/project-memory-service/index/kind-dispatch.js +48 -13
- package/dist/services/memory/project-memory-service/index.d.ts +5 -3
- package/dist/services/memory/project-memory-service/index.js +2 -2
- package/dist/services/memory/project-memory-service/parsers/frontmatter.d.ts +15 -1
- package/dist/services/memory/project-memory-service/parsers/frontmatter.js +34 -6
- package/dist/services/memory/project-memory-service/parsers/markdown-pure.d.ts +27 -1
- package/dist/services/memory/project-memory-service/parsers/markdown-pure.js +92 -7
- package/dist/services/memory/project-memory-service/types.d.ts +86 -0
- package/dist/services/slice/slice-check-types.d.ts +1 -1
- package/dist/services/workspace/runtime-layout.d.ts +91 -0
- package/dist/services/workspace/runtime-layout.js +148 -0
- package/dist/services/workspace/workspace-claude-settings-materializer.js +14 -0
- package/package.json +6 -6
- package/scripts/clean-dist.mjs +15 -3
- package/scripts/sync-version.mjs +26 -4
- package/skills/bee/peaks-prd/SKILL.md +1 -1
- package/skills/bee/peaks-qa/references/qa-skill-presence.md +1 -1
- package/skills/bee/peaks-rd/references/skill-presence-and-title.md +1 -1
- package/skills/bee/peaks-sc/SKILL.md +1 -1
- package/skills/bee/peaks-txt/SKILL.md +3 -3
- package/skills/peaks-code/SKILL.md +1 -1
- package/skills/peaks-code/references/project-memory-loading.md +19 -1
- package/skills/peaks-code/references/step-11-memory-sediment.md +1 -1
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
467
|
+
]), asJson(opts));
|
|
376
468
|
}
|
|
377
469
|
catch (err) {
|
|
378
|
-
|
|
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
|
|
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);
|