peaks-loop 4.0.42 → 4.0.44
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 +59 -0
- package/README-en.md +1 -1
- package/README.md +1 -1
- package/dist/cli/commands/_register.js +4 -0
- package/dist/cli/commands/api-diff-commands.d.ts +16 -0
- package/dist/cli/commands/api-diff-commands.js +55 -0
- package/dist/cli/commands/audit-commands.d.ts +16 -3
- package/dist/cli/commands/audit-commands.js +84 -31
- package/dist/cli/commands/codegraph-commands.js +191 -6
- package/dist/cli/commands/final-review-commands.d.ts +34 -10
- package/dist/cli/commands/final-review-commands.js +130 -34
- package/dist/cli/commands/job-commands.js +4 -2
- package/dist/cli/commands/scan-commands.js +1 -1
- package/dist/cli/commands/share-commands.d.ts +49 -0
- package/dist/cli/commands/share-commands.js +114 -14
- package/dist/cli/commands/test-commands.d.ts +60 -3
- package/dist/cli/commands/test-commands.js +125 -7
- package/dist/services/audit/audit-goal-service.js +38 -3
- package/dist/services/codegraph/codegraph-autorefresh.js +12 -0
- package/dist/services/codegraph/codegraph-exclude-integrity.d.ts +61 -0
- package/dist/services/codegraph/codegraph-exclude-integrity.js +98 -0
- package/dist/services/codegraph/codegraph-exclude-reconciler.d.ts +26 -0
- package/dist/services/codegraph/codegraph-exclude-reconciler.js +217 -0
- package/dist/services/codegraph/codegraph-exclude-repair.d.ts +102 -0
- package/dist/services/codegraph/codegraph-exclude-repair.js +266 -0
- package/dist/services/codegraph/codegraph-preflight-service.js +12 -0
- package/dist/services/codegraph/codegraph-service.d.ts +0 -1
- package/dist/services/codegraph/codegraph-service.js +5 -4
- package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.d.ts +29 -0
- package/dist/services/doctor/doctor-service/checks/codegraph-exclude-integrity.js +88 -0
- package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.d.ts +65 -0
- package/dist/services/doctor/doctor-service/checks/ecc-hooks-schema-drift.js +186 -0
- package/dist/services/doctor/doctor-service/plugin-registry.js +4 -0
- package/dist/services/doctor/doctor-service/types.d.ts +47 -0
- package/dist/services/final-review/final-review-service.d.ts +154 -0
- package/dist/services/final-review/final-review-service.js +621 -7
- package/dist/services/final-review/index.d.ts +1 -1
- package/dist/services/final-review/index.js +1 -1
- package/dist/services/llm/anthropic-runner.d.ts +87 -0
- package/dist/services/llm/anthropic-runner.js +171 -0
- package/dist/services/llm/stub-runner.d.ts +11 -0
- package/dist/services/llm/stub-runner.js +33 -0
- package/dist/services/prd/handoff-auto-regen.js +0 -1
- package/dist/services/prd/handoff-service.d.ts +9 -1
- package/dist/services/prd/handoff-service.js +48 -6
- package/dist/services/prd/project-scan-bootstrap-service.js +7 -7
- package/dist/services/scan/api-diff-openapi.d.ts +32 -0
- package/dist/services/scan/api-diff-openapi.js +359 -0
- package/dist/services/scan/api-diff-recorded.d.ts +96 -0
- package/dist/services/scan/api-diff-recorded.js +577 -0
- package/dist/services/scan/api-diff-service.d.ts +34 -0
- package/dist/services/scan/api-diff-service.js +407 -0
- package/dist/services/scan/api-diff-types.d.ts +116 -0
- package/dist/services/scan/api-diff-types.js +46 -0
- package/dist/services/scan/archetype-service.js +27 -1
- package/dist/services/scan/existing-system-service.js +17 -4
- package/dist/services/scan/hook-convention-service.d.ts +26 -0
- package/dist/services/scan/hook-convention-service.js +562 -0
- package/dist/services/scan/scan-types.d.ts +47 -0
- package/dist/services/session/caller-binding-service.d.ts +28 -0
- package/dist/services/session/caller-binding-service.js +10 -2
- package/dist/services/session/caller-id-types.d.ts +12 -2
- package/dist/services/session/index.d.ts +2 -2
- package/dist/services/session/index.js +2 -2
- package/dist/services/session/session-binding-bridge.js +11 -6
- package/dist/services/session/session-manager.d.ts +33 -1
- package/dist/services/session/session-manager.js +84 -25
- package/dist/services/skills/skill-presence-service.d.ts +17 -3
- package/dist/services/skills/skill-presence-service.js +23 -3
- package/package.json +7 -5
- package/skills/bee/peaks-rd/SKILL.md +11 -3
- package/skills/peaks-code/references/existing-system-extraction.md +5 -1
- package/skills/peaks-code/references/frontend-only-mode.md +48 -6
- package/skills/peaks-code/references/project-scan-checklist.md +20 -1
- package/skills/peaks-doctor/references/doctor-check-catalog.md +1 -0
- package/skills/peaks-final-review/SKILL.md +43 -32
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* `peaks prepare-final-review <rid>` CLI wrapper (W5 Fix M2; real provider
|
|
3
|
+
* binding added by the S3 defect-remediation slice).
|
|
3
4
|
*
|
|
4
|
-
* Exposes the `prepareFinalReview()` service (
|
|
5
|
+
* Exposes the `prepareFinalReview()` service (W2 T9 on
|
|
5
6
|
* `feature/slice-topology-multipass`) via the CLI surface. The service
|
|
6
|
-
* depends on an injected `LlmRunner`; this
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* depends on an injected `LlmRunner`; this file owns the binding:
|
|
8
|
+
* - `--llm-provider stub` (default) returns a structured "scaffold ready"
|
|
9
|
+
* envelope WITHOUT calling the service, so CI can verify the route
|
|
10
|
+
* offline. It performs no review, and says so in every hint it emits.
|
|
11
|
+
* - `--llm-provider anthropic` binds the real Messages-API runner
|
|
12
|
+
* (`resolveAnthropicConfig()` + `createAnthropicRunner()`) and runs the
|
|
13
|
+
* service for real, carrying the 4-dim result back in the envelope.
|
|
14
|
+
* An absent credential or model raises `LlmBindingError`, reported under
|
|
15
|
+
* its own error code — never degraded into a scaffold a caller could
|
|
16
|
+
* mistake for a review.
|
|
17
|
+
* Unknown provider names still fail loudly with
|
|
18
|
+
* `LLM_PROVIDER_NOT_IMPLEMENTED` rather than silently falling back to stub.
|
|
12
19
|
*
|
|
13
20
|
* Per the dev-preference "Default-no on new CLI commands" rule and the
|
|
14
21
|
* W4 T14 spec, this is a NEW top-level command (`prepare-final-review`),
|
|
@@ -19,13 +26,30 @@
|
|
|
19
26
|
*/
|
|
20
27
|
import { Command } from 'commander';
|
|
21
28
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
22
|
-
|
|
29
|
+
import type { FinalReviewOutput } from '../../services/final-review/final-review-types.js';
|
|
30
|
+
export type FinalReviewStatus = 'scaffold-only' | 'review-complete' | 'not-applicable';
|
|
31
|
+
/**
|
|
32
|
+
* Which LLM produced this envelope. `unknown` is reserved for failure
|
|
33
|
+
* envelopes, where no binding was ever established.
|
|
34
|
+
*/
|
|
35
|
+
export type FinalReviewProviderBinding = 'stub' | 'anthropic-messages-api' | 'unknown';
|
|
23
36
|
export interface FinalReviewData {
|
|
24
37
|
readonly status: FinalReviewStatus;
|
|
25
38
|
readonly rid: string;
|
|
26
39
|
readonly sessionId: string;
|
|
27
40
|
readonly auditGoalPath: string;
|
|
28
41
|
readonly serviceWired: boolean;
|
|
29
|
-
readonly providerBinding:
|
|
42
|
+
readonly providerBinding: FinalReviewProviderBinding;
|
|
43
|
+
/** Model id the bound provider answered with (real-provider runs only). */
|
|
44
|
+
readonly model?: string;
|
|
45
|
+
/** The 4-dim review the service produced (real-provider runs only). */
|
|
46
|
+
readonly review?: FinalReviewOutput;
|
|
47
|
+
/**
|
|
48
|
+
* Environment variables that were absent when binding failed. Surfaced on
|
|
49
|
+
* `data` because `fail()` redacts `message` through
|
|
50
|
+
* `redactSensitiveErrorMessage`, whose catch-all pattern matches the words
|
|
51
|
+
* `token` / `api_key` and would blank out the very names an operator needs.
|
|
52
|
+
*/
|
|
53
|
+
readonly missingEnv?: readonly string[];
|
|
30
54
|
}
|
|
31
55
|
export declare function registerFinalReviewCommands(program: Command, io: ProgramIO): void;
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* `peaks prepare-final-review <rid>` CLI wrapper (W5 Fix M2; real provider
|
|
3
|
+
* binding added by the S3 defect-remediation slice).
|
|
3
4
|
*
|
|
4
|
-
* Exposes the `prepareFinalReview()` service (
|
|
5
|
+
* Exposes the `prepareFinalReview()` service (W2 T9 on
|
|
5
6
|
* `feature/slice-topology-multipass`) via the CLI surface. The service
|
|
6
|
-
* depends on an injected `LlmRunner`; this
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* depends on an injected `LlmRunner`; this file owns the binding:
|
|
8
|
+
* - `--llm-provider stub` (default) returns a structured "scaffold ready"
|
|
9
|
+
* envelope WITHOUT calling the service, so CI can verify the route
|
|
10
|
+
* offline. It performs no review, and says so in every hint it emits.
|
|
11
|
+
* - `--llm-provider anthropic` binds the real Messages-API runner
|
|
12
|
+
* (`resolveAnthropicConfig()` + `createAnthropicRunner()`) and runs the
|
|
13
|
+
* service for real, carrying the 4-dim result back in the envelope.
|
|
14
|
+
* An absent credential or model raises `LlmBindingError`, reported under
|
|
15
|
+
* its own error code — never degraded into a scaffold a caller could
|
|
16
|
+
* mistake for a review.
|
|
17
|
+
* Unknown provider names still fail loudly with
|
|
18
|
+
* `LLM_PROVIDER_NOT_IMPLEMENTED` rather than silently falling back to stub.
|
|
12
19
|
*
|
|
13
20
|
* Per the dev-preference "Default-no on new CLI commands" rule and the
|
|
14
21
|
* W4 T14 spec, this is a NEW top-level command (`prepare-final-review`),
|
|
@@ -21,8 +28,18 @@ import { existsSync, statSync } from 'node:fs';
|
|
|
21
28
|
import { join, resolve } from 'node:path';
|
|
22
29
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
23
30
|
import { fail, ok } from 'peaks-loop-shared/result';
|
|
31
|
+
import { createAnthropicRunner, LlmBindingError, LlmRequestError, resolveAnthropicConfig, } from '../../services/llm/anthropic-runner.js';
|
|
32
|
+
import { IncompleteFinalReviewError, prepareFinalReview, } from '../../services/final-review/final-review-service.js';
|
|
24
33
|
/** Whitelist of supported `--llm-provider` values for `peaks prepare-final-review`. */
|
|
25
|
-
const SUPPORTED_LLM_PROVIDERS = ['stub'];
|
|
34
|
+
const SUPPORTED_LLM_PROVIDERS = ['anthropic', 'stub'];
|
|
35
|
+
/**
|
|
36
|
+
* Default stays `stub` (unlike `peaks audit goal`, whose default is the real
|
|
37
|
+
* provider): the scaffold route is what CI and the registered e2e contract
|
|
38
|
+
* exercise without credentials, and the stub envelope is labelled
|
|
39
|
+
* `status: 'scaffold-only'` / `providerBinding: 'stub'` so it can never be
|
|
40
|
+
* read as a review.
|
|
41
|
+
*/
|
|
42
|
+
const DEFAULT_LLM_PROVIDER = 'stub';
|
|
26
43
|
function isSupportedLlmProvider(value) {
|
|
27
44
|
return SUPPORTED_LLM_PROVIDERS.includes(value);
|
|
28
45
|
}
|
|
@@ -32,7 +49,7 @@ function isSupportedLlmProvider(value) {
|
|
|
32
49
|
* "this is a placeholder on a failure" apart from "this is a real
|
|
33
50
|
* scaffold-only success".
|
|
34
51
|
*/
|
|
35
|
-
function emptyFinalReviewData(rid, sessionId, auditGoalPath) {
|
|
52
|
+
function emptyFinalReviewData(rid, sessionId, auditGoalPath, missingEnv) {
|
|
36
53
|
return {
|
|
37
54
|
status: 'not-applicable',
|
|
38
55
|
rid,
|
|
@@ -40,6 +57,7 @@ function emptyFinalReviewData(rid, sessionId, auditGoalPath) {
|
|
|
40
57
|
auditGoalPath,
|
|
41
58
|
serviceWired: false,
|
|
42
59
|
providerBinding: 'unknown',
|
|
60
|
+
...(missingEnv === undefined ? {} : { missingEnv }),
|
|
43
61
|
};
|
|
44
62
|
}
|
|
45
63
|
function validateProjectRoot(projectArg) {
|
|
@@ -95,7 +113,7 @@ export function registerFinalReviewCommands(program, io) {
|
|
|
95
113
|
.description('Prepare the 4-dimension business review (final-review primitive) for human acceptance (W2 T9 service; CLI surface in W5 M2)')
|
|
96
114
|
.requiredOption('--project <path>', 'target project root')
|
|
97
115
|
.requiredOption('--session-id <sid>', 'session id whose .peaks/_runtime/<sid>/audit-goal/<rid>.json is the approved goal source')
|
|
98
|
-
.option('--llm-provider <name>',
|
|
116
|
+
.option('--llm-provider <name>', `LLM provider name: ${SUPPORTED_LLM_PROVIDERS.join(' | ')} (default: ${DEFAULT_LLM_PROVIDER} — performs no review)`, DEFAULT_LLM_PROVIDER)).action(async (rid, options) => {
|
|
99
117
|
// 1. Project root must exist and be a directory.
|
|
100
118
|
const projectValidation = validateProjectRoot(options.project);
|
|
101
119
|
if (!projectValidation.ok) {
|
|
@@ -128,34 +146,112 @@ export function registerFinalReviewCommands(program, io) {
|
|
|
128
146
|
process.exitCode = 1;
|
|
129
147
|
return;
|
|
130
148
|
}
|
|
131
|
-
// 5. Provider check:
|
|
132
|
-
|
|
149
|
+
// 5. Provider check: unknown names fail loudly — a silent fallback to
|
|
150
|
+
// `stub` would hand the caller a scaffold envelope that reads as a
|
|
151
|
+
// review route, which is the defect class this gate exists to stop.
|
|
152
|
+
const provider = options.llmProvider ?? DEFAULT_LLM_PROVIDER;
|
|
133
153
|
if (!isSupportedLlmProvider(provider)) {
|
|
134
|
-
printResult(io, fail('final-review.prepare', 'LLM_PROVIDER_NOT_IMPLEMENTED', `
|
|
135
|
-
|
|
136
|
-
'Real provider binding is tracked as a follow-up slice.',
|
|
154
|
+
printResult(io, fail('final-review.prepare', 'LLM_PROVIDER_NOT_IMPLEMENTED', `LLM provider "${provider}" is not implemented. Supported providers: ${SUPPORTED_LLM_PROVIDERS.join(', ')}.`, emptyFinalReviewData(rid, sessionValidation.sessionId, auditGoalPath), [
|
|
155
|
+
`Re-run with \`--llm-provider anthropic\` for a real 4-dim review, or \`--llm-provider ${DEFAULT_LLM_PROVIDER}\` for an offline scaffold.`,
|
|
137
156
|
]), options.json);
|
|
138
157
|
process.exitCode = 1;
|
|
139
158
|
return;
|
|
140
159
|
}
|
|
141
160
|
// 6. Stub path: surface a structured "scaffold ready" envelope.
|
|
142
|
-
// We DO NOT call the service
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
161
|
+
// We DO NOT call the service here — the stub runner answers the
|
|
162
|
+
// audit-goal shape, not the 4-dim review shape, so running it through
|
|
163
|
+
// `prepareFinalReview()` would only manufacture a malformed review.
|
|
164
|
+
// The envelope confirms the route is wired end-to-end and reports the
|
|
165
|
+
// audit-goal path the service WOULD read.
|
|
166
|
+
if (provider === 'stub') {
|
|
167
|
+
const data = {
|
|
168
|
+
status: 'scaffold-only',
|
|
169
|
+
rid,
|
|
170
|
+
sessionId: sessionValidation.sessionId,
|
|
171
|
+
auditGoalPath,
|
|
172
|
+
serviceWired: true,
|
|
173
|
+
providerBinding: 'stub',
|
|
174
|
+
};
|
|
175
|
+
const envelope = ok('final-review.prepare', data, [], [
|
|
176
|
+
'Stub provider: no 4-dim review was performed. This envelope only proves the route is wired and reachable.',
|
|
177
|
+
`Audit-goal file is present at: ${auditGoalPath}`,
|
|
178
|
+
'Re-run with `--llm-provider anthropic` to produce a real review.',
|
|
179
|
+
]);
|
|
180
|
+
printResult(io, envelope, options.json);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
// 7. Real provider path: bind a real `LlmRunner` and run the service.
|
|
184
|
+
// Binding happens INSIDE the try so an absent credential surfaces as
|
|
185
|
+
// `LlmBindingError`'s own code instead of escaping as a crash. No
|
|
186
|
+
// envelope is emitted on that path — a "successful" empty review would
|
|
187
|
+
// be worse than an error.
|
|
188
|
+
try {
|
|
189
|
+
const config = resolveAnthropicConfig();
|
|
190
|
+
const llmRunner = createAnthropicRunner(config);
|
|
191
|
+
const review = await prepareFinalReview(rid, {
|
|
192
|
+
projectRoot: projectValidation.projectRoot,
|
|
193
|
+
sessionId: sessionValidation.sessionId,
|
|
194
|
+
llmRunner,
|
|
195
|
+
});
|
|
196
|
+
const data = {
|
|
197
|
+
status: 'review-complete',
|
|
198
|
+
rid,
|
|
199
|
+
sessionId: sessionValidation.sessionId,
|
|
200
|
+
auditGoalPath,
|
|
201
|
+
serviceWired: true,
|
|
202
|
+
providerBinding: 'anthropic-messages-api',
|
|
203
|
+
model: config.model,
|
|
204
|
+
review,
|
|
205
|
+
};
|
|
206
|
+
const envelope = ok('final-review.prepare', data, review.allPass ? [] : [
|
|
207
|
+
`Dimensions needing human attention: ${review.needsAttention.join(', ') || 'none flagged'}.`,
|
|
208
|
+
], [
|
|
209
|
+
`4-dim review produced by anthropic-messages-api (model: ${config.model}).`,
|
|
210
|
+
`allPass: ${String(review.allPass)}.`,
|
|
211
|
+
]);
|
|
212
|
+
printResult(io, envelope, options.json);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
const code = finalReviewErrorCode(error);
|
|
216
|
+
printResult(io, fail('final-review.prepare', code, getErrorMessage(error), emptyFinalReviewData(rid, sessionValidation.sessionId, auditGoalPath, error instanceof LlmBindingError ? error.missingEnv : undefined), finalReviewNextActions(code)), options.json);
|
|
217
|
+
process.exitCode = 1;
|
|
218
|
+
}
|
|
160
219
|
});
|
|
161
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Map a thrown error to the CLI's error code. The LLM-layer errors already
|
|
223
|
+
* carry codes precise enough to act on (`LLM_CREDENTIAL_MISSING`,
|
|
224
|
+
* `LLM_REQUEST_FAILED`, …), so they are passed through rather than flattened
|
|
225
|
+
* into one opaque failure.
|
|
226
|
+
*/
|
|
227
|
+
function finalReviewErrorCode(error) {
|
|
228
|
+
if (error instanceof LlmBindingError ||
|
|
229
|
+
error instanceof LlmRequestError ||
|
|
230
|
+
error instanceof IncompleteFinalReviewError) {
|
|
231
|
+
return error.code;
|
|
232
|
+
}
|
|
233
|
+
return 'FINAL_REVIEW_FAILED';
|
|
234
|
+
}
|
|
235
|
+
function finalReviewNextActions(code) {
|
|
236
|
+
switch (code) {
|
|
237
|
+
case 'LLM_CREDENTIAL_MISSING':
|
|
238
|
+
return [
|
|
239
|
+
'Export ANTHROPIC_AUTH_TOKEN (or ANTHROPIC_API_KEY) in the environment that launches peaks, then re-run.',
|
|
240
|
+
'For an offline scaffold instead of a review, re-run with `--llm-provider stub` — it performs NO review.',
|
|
241
|
+
];
|
|
242
|
+
case 'LLM_MODEL_MISSING':
|
|
243
|
+
return [
|
|
244
|
+
'Export ANTHROPIC_MODEL (or CLAUDE_CODE_SUBAGENT_MODEL) in the environment that launches peaks, then re-run.',
|
|
245
|
+
];
|
|
246
|
+
case 'LLM_REQUEST_FAILED':
|
|
247
|
+
return [
|
|
248
|
+
'Check ANTHROPIC_BASE_URL and network reachability, then re-run — a transport failure produces no review.',
|
|
249
|
+
];
|
|
250
|
+
case 'INCOMPLETE_FINAL_REVIEW':
|
|
251
|
+
return [
|
|
252
|
+
'The LLM reply was not valid JSON or omitted a required dimension; re-run so the gate is never read as complete.',
|
|
253
|
+
];
|
|
254
|
+
default:
|
|
255
|
+
return ['Re-run with `--llm-provider stub` to validate the CLI route without a real LLM.'];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -56,7 +56,8 @@ function findSessionHoldingJob(project, jobId) {
|
|
|
56
56
|
* `.peaks/_runtime/session.json` binding points at another session):
|
|
57
57
|
* 1. `--session-id` flag (explicit override)
|
|
58
58
|
* 2. `PEAKS_SESSION_ID` env var
|
|
59
|
-
* 3. `getCurrentSessionId(project)` —
|
|
59
|
+
* 3. `getCurrentSessionId(project)` — this caller's session binding, falling back
|
|
60
|
+
* to `.peaks/_runtime/session.json` when no caller binding is resolvable
|
|
60
61
|
* 4. Error (NO_ACTIVE_SESSION) — must never silently fall back to a random uuid
|
|
61
62
|
*
|
|
62
63
|
* When `jobId` is passed and it is absent from the resolved session, the thrown
|
|
@@ -110,7 +111,8 @@ export function registerJobCommands(program, io = { stdout: (t) => process.stdou
|
|
|
110
111
|
.option('--project <repo>')
|
|
111
112
|
.action(async (opts) => {
|
|
112
113
|
const project = projectRoot(opts);
|
|
113
|
-
// Resolve sessionId: explicit flag > PEAKS_SESSION_ID >
|
|
114
|
+
// Resolve sessionId: explicit flag > PEAKS_SESSION_ID > caller-first session binding
|
|
115
|
+
// (this caller's binding, else the project-global session.json) > FAIL.
|
|
114
116
|
// Per spec §3.3, Job state lives at .peaks/_runtime/<sessionId>/job/<jobId>/state.json —
|
|
115
117
|
// a random UUID would scatter state across dirs and break resume/auto-compact.
|
|
116
118
|
let sessionId = opts.sessionId ?? process.env.PEAKS_SESSION_ID ?? getCurrentSessionId(project);
|
|
@@ -30,7 +30,7 @@ export function registerScanCommands(program, io) {
|
|
|
30
30
|
const scan = program.command('scan').description('Deterministic project scans (archetype, existing system) for Peaks workflows');
|
|
31
31
|
addJsonOption(scan
|
|
32
32
|
.command('archetype')
|
|
33
|
-
.description('Detect project archetype, frontend-only mode, and supporting signals from the filesystem (read-only)')
|
|
33
|
+
.description('Detect project archetype, integration mode (three scenarios), frontend-only mode, and supporting signals from the filesystem (read-only)')
|
|
34
34
|
.requiredOption('--project <path>', 'target project root')).action(async (options) => {
|
|
35
35
|
try {
|
|
36
36
|
const report = await scanArchetype({ projectRoot: options.project });
|
|
@@ -12,6 +12,55 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { Command } from 'commander';
|
|
14
14
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
15
|
+
/**
|
|
16
|
+
* A `dispatch-*.json` candidate for `finalize --request-id`, as read off disk.
|
|
17
|
+
* The record's own `createdAt` is carried as the recency key — NOT the file's
|
|
18
|
+
* mtime, which any heartbeat or finalize rewrites, so a `done` record can look
|
|
19
|
+
* newer than the `queued` one that superseded it. Keeping the key on the
|
|
20
|
+
* candidate makes the selection rule a pure function of the records and
|
|
21
|
+
* testable without a filesystem.
|
|
22
|
+
*/
|
|
23
|
+
export interface FinalizeCandidate {
|
|
24
|
+
readonly recordPath: string;
|
|
25
|
+
readonly requestId: string;
|
|
26
|
+
readonly status: string;
|
|
27
|
+
readonly createdAt: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* What `--request-id` actually did, reported in the envelope. N3: the branch
|
|
31
|
+
* used to resolve silently and could not say WHY a record was or was not the
|
|
32
|
+
* one finalized.
|
|
33
|
+
*/
|
|
34
|
+
export interface FinalizeSelection {
|
|
35
|
+
readonly requestId: string;
|
|
36
|
+
readonly rule: string;
|
|
37
|
+
readonly matched: number;
|
|
38
|
+
readonly chosen: string | null;
|
|
39
|
+
readonly rejected: readonly {
|
|
40
|
+
readonly recordPath: string;
|
|
41
|
+
readonly status: string;
|
|
42
|
+
readonly reason: string;
|
|
43
|
+
}[];
|
|
44
|
+
}
|
|
45
|
+
/** The one selection rule `--request-id` and `--batch` now share. */
|
|
46
|
+
export declare const FINALIZE_SELECTION_RULE: string;
|
|
47
|
+
/**
|
|
48
|
+
* N3 — pick the record `finalize --request-id` should act on.
|
|
49
|
+
*
|
|
50
|
+
* The branch this replaces `break`ed on the FIRST file whose record carried
|
|
51
|
+
* the requestId and never looked at `status`. With a re-dispatched request
|
|
52
|
+
* (this session holds six records for `2026-09-12-defect-remediation`) it
|
|
53
|
+
* always resolved to the OLDEST one — the already-`done` RD record — so
|
|
54
|
+
* finalizing reported success while the newer `queued` QA record stayed
|
|
55
|
+
* queued forever. `--batch` never had that bug: it filters on `queued`.
|
|
56
|
+
*
|
|
57
|
+
* Both branches are now the same rule. Return `null` when nothing is queued:
|
|
58
|
+
* a record that already left `queued` is precisely the one that must NOT be
|
|
59
|
+
* re-finalized, so the caller reports the survivors instead of touching one.
|
|
60
|
+
*/
|
|
61
|
+
export declare function selectFinalizeTarget(candidates: readonly FinalizeCandidate[]): FinalizeCandidate | null;
|
|
62
|
+
/** Why a candidate was not the chosen one — reported, never guessed at. */
|
|
63
|
+
export declare function describeFinalizeRejection(candidate: FinalizeCandidate, chosen: FinalizeCandidate | null): string;
|
|
15
64
|
export declare function registerShareCommand(parent: Command, io: ProgramIO): void;
|
|
16
65
|
export declare function registerSharedReadCommand(parent: Command, io: ProgramIO): void;
|
|
17
66
|
export declare function registerAwaitCommand(parent: Command, io: ProgramIO): void;
|
|
@@ -5,6 +5,36 @@ import { readSharedChannel, writeSharedEntry, SHARED_CHANNEL_SOFT_VALUE_WARN } f
|
|
|
5
5
|
import { writeLogEntry } from '../../services/log/logger.js';
|
|
6
6
|
import { getCurrentSessionId } from '../../services/skills/skill-presence-service.js';
|
|
7
7
|
import { summarizeBatchResults } from './sub-agent-shared.js';
|
|
8
|
+
/** The one selection rule `--request-id` and `--batch` now share. */
|
|
9
|
+
export const FINALIZE_SELECTION_RULE = 'prefer status=queued; among those, newest by record createdAt (then filename); ' +
|
|
10
|
+
'no queued match means nothing is finalized';
|
|
11
|
+
/**
|
|
12
|
+
* N3 — pick the record `finalize --request-id` should act on.
|
|
13
|
+
*
|
|
14
|
+
* The branch this replaces `break`ed on the FIRST file whose record carried
|
|
15
|
+
* the requestId and never looked at `status`. With a re-dispatched request
|
|
16
|
+
* (this session holds six records for `2026-09-12-defect-remediation`) it
|
|
17
|
+
* always resolved to the OLDEST one — the already-`done` RD record — so
|
|
18
|
+
* finalizing reported success while the newer `queued` QA record stayed
|
|
19
|
+
* queued forever. `--batch` never had that bug: it filters on `queued`.
|
|
20
|
+
*
|
|
21
|
+
* Both branches are now the same rule. Return `null` when nothing is queued:
|
|
22
|
+
* a record that already left `queued` is precisely the one that must NOT be
|
|
23
|
+
* re-finalized, so the caller reports the survivors instead of touching one.
|
|
24
|
+
*/
|
|
25
|
+
export function selectFinalizeTarget(candidates) {
|
|
26
|
+
const queued = candidates.filter(candidate => candidate.status === 'queued');
|
|
27
|
+
if (queued.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
return [...queued].sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.recordPath.localeCompare(a.recordPath))[0];
|
|
30
|
+
}
|
|
31
|
+
/** Why a candidate was not the chosen one — reported, never guessed at. */
|
|
32
|
+
export function describeFinalizeRejection(candidate, chosen) {
|
|
33
|
+
if (candidate.status !== 'queued' || chosen === null) {
|
|
34
|
+
return 'status is ' + candidate.status;
|
|
35
|
+
}
|
|
36
|
+
return 'superseded by the newer queued record ' + chosen.recordPath;
|
|
37
|
+
}
|
|
8
38
|
export function registerShareCommand(parent, io) {
|
|
9
39
|
addJsonOption(parent
|
|
10
40
|
.command('share')
|
|
@@ -329,6 +359,31 @@ export function registerFinalizeCommand(parent, io) {
|
|
|
329
359
|
const finalized = [];
|
|
330
360
|
const skipped = [];
|
|
331
361
|
const errors = [];
|
|
362
|
+
let selection = null;
|
|
363
|
+
/**
|
|
364
|
+
* N2 — one unreadable record must not abort the sweep.
|
|
365
|
+
*
|
|
366
|
+
* Skipping `active-dispatches.json` and `batch-*.counter.json` by
|
|
367
|
+
* FILENAME removed the two non-record files that happened to be in the
|
|
368
|
+
* directory, but a single stale or foreign `dispatch-*.json` (an old
|
|
369
|
+
* `version`, hand-edited JSON, a truncated write) still made
|
|
370
|
+
* `readRecord` throw from OUTSIDE any try/catch — and the throw
|
|
371
|
+
* escaped to the action's outer handler, so `--request-id` AND
|
|
372
|
+
* `--batch` both died with `FINALIZE_ERROR` and exit 1 without
|
|
373
|
+
* touching a single healthy record.
|
|
374
|
+
*
|
|
375
|
+
* A record that cannot be read is now reported in `errors[]` and
|
|
376
|
+
* skipped; every other record is processed as before.
|
|
377
|
+
*/
|
|
378
|
+
const tryReadRecord = (recordPath) => {
|
|
379
|
+
try {
|
|
380
|
+
return readRecord(recordPath);
|
|
381
|
+
}
|
|
382
|
+
catch (e) {
|
|
383
|
+
errors.push({ recordPath, error: getErrorMessage(e) });
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
};
|
|
332
387
|
const applyOutcome = (recordPath, rid) => {
|
|
333
388
|
markCompleted({ recordPath, now: () => new Date(), status: mapped.status, outcome: mapped.outcome, projectRoot });
|
|
334
389
|
finalized.push({ recordPath, requestId: rid, status: mapped.status });
|
|
@@ -352,29 +407,59 @@ export function registerFinalizeCommand(parent, io) {
|
|
|
352
407
|
const fs2 = await import('node:fs');
|
|
353
408
|
const path2 = await import('node:path');
|
|
354
409
|
const dir = path2.resolve(projectRoot, '.peaks', '_sub_agents', sessionId);
|
|
355
|
-
|
|
410
|
+
const candidates = [];
|
|
356
411
|
if (fs2.existsSync(dir)) {
|
|
357
412
|
for (const f of fs2.readdirSync(dir)) {
|
|
358
|
-
|
|
413
|
+
// Only dispatch records are readable records. The session
|
|
414
|
+
// directory also holds `active-dispatches.json` (an index)
|
|
415
|
+
// and `batch-<uuid>.counter.json` (batch counters); neither
|
|
416
|
+
// carries a `version` field, so `readRecord` on them throws
|
|
417
|
+
// `Dispatch record version mismatch ... got undefined`. The
|
|
418
|
+
// `--batch` branch below has always used this same filter.
|
|
419
|
+
if (!f.startsWith('dispatch-') || !f.endsWith('.json'))
|
|
359
420
|
continue;
|
|
360
421
|
const p = path2.join(dir, f);
|
|
361
|
-
const r =
|
|
362
|
-
if (r.requestId
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
422
|
+
const r = tryReadRecord(p);
|
|
423
|
+
if (r === null || r.requestId !== options.requestId)
|
|
424
|
+
continue;
|
|
425
|
+
candidates.push({
|
|
426
|
+
recordPath: p,
|
|
427
|
+
requestId: r.requestId,
|
|
428
|
+
status: r.status,
|
|
429
|
+
createdAt: r.createdAt
|
|
430
|
+
});
|
|
366
431
|
}
|
|
367
432
|
}
|
|
368
|
-
if (
|
|
433
|
+
if (candidates.length === 0) {
|
|
369
434
|
printResult(io, fail('sub-agent.finalize', 'RECORD_NOT_FOUND', 'No dispatch record for requestId=' + options.requestId, { ok: false }, ['Check --request-id matches the dispatch envelope.']), asJson);
|
|
370
435
|
process.exitCode = 1;
|
|
371
436
|
return;
|
|
372
437
|
}
|
|
373
|
-
|
|
374
|
-
|
|
438
|
+
// N3: same rule as `--batch` — see `selectFinalizeTarget`.
|
|
439
|
+
const chosen = selectFinalizeTarget(candidates);
|
|
440
|
+
selection = {
|
|
441
|
+
requestId: options.requestId,
|
|
442
|
+
rule: FINALIZE_SELECTION_RULE,
|
|
443
|
+
matched: candidates.length,
|
|
444
|
+
chosen: chosen?.recordPath ?? null,
|
|
445
|
+
rejected: candidates
|
|
446
|
+
.filter(candidate => candidate !== chosen)
|
|
447
|
+
.map(candidate => ({
|
|
448
|
+
recordPath: candidate.recordPath,
|
|
449
|
+
status: candidate.status,
|
|
450
|
+
reason: describeFinalizeRejection(candidate, chosen)
|
|
451
|
+
}))
|
|
452
|
+
};
|
|
453
|
+
for (const rejected of selection.rejected) {
|
|
454
|
+
skipped.push({ recordPath: rejected.recordPath, reason: rejected.reason });
|
|
375
455
|
}
|
|
376
|
-
|
|
377
|
-
|
|
456
|
+
if (chosen !== null) {
|
|
457
|
+
try {
|
|
458
|
+
applyOutcome(chosen.recordPath, chosen.requestId);
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
errors.push({ recordPath: chosen.recordPath, error: getErrorMessage(e) });
|
|
462
|
+
}
|
|
378
463
|
}
|
|
379
464
|
}
|
|
380
465
|
else {
|
|
@@ -386,7 +471,9 @@ export function registerFinalizeCommand(parent, io) {
|
|
|
386
471
|
if (!f.startsWith('dispatch-') || !f.endsWith('.json'))
|
|
387
472
|
continue;
|
|
388
473
|
const p = path2.join(dir, f);
|
|
389
|
-
const r =
|
|
474
|
+
const r = tryReadRecord(p);
|
|
475
|
+
if (r === null)
|
|
476
|
+
continue;
|
|
390
477
|
if (r.batchId !== options.batch)
|
|
391
478
|
continue;
|
|
392
479
|
if (r.status !== 'queued') {
|
|
@@ -402,7 +489,20 @@ export function registerFinalizeCommand(parent, io) {
|
|
|
402
489
|
}
|
|
403
490
|
}
|
|
404
491
|
}
|
|
405
|
-
|
|
492
|
+
const hints = [];
|
|
493
|
+
if (errors.length > 0) {
|
|
494
|
+
hints.push('Re-run after fixing; unreadable records are listed in errors[] and were skipped, not fatal.');
|
|
495
|
+
}
|
|
496
|
+
else if (finalized.length === 0 && skipped.length > 0) {
|
|
497
|
+
hints.push('Nothing was finalized: every matching record had already left `queued`. See skipped[] for each record\'s status.');
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
hints.push('All targeted records transitioned out of queued.');
|
|
501
|
+
}
|
|
502
|
+
if (selection !== null) {
|
|
503
|
+
hints.push(`--request-id selection (${selection.rule}): chose ${selection.chosen ?? '(none)'} of ${selection.matched} matching record(s); ${selection.rejected.length} rejected.`);
|
|
504
|
+
}
|
|
505
|
+
printResult(io, ok('sub-agent.finalize', { finalized, skipped, errors, selection, sessionId, outcome }, errors.length > 0 ? [errors.length + ' failed'] : [], hints), asJson);
|
|
406
506
|
if (errors.length > 0)
|
|
407
507
|
process.exitCode = 1;
|
|
408
508
|
}
|
|
@@ -6,14 +6,18 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 1. Auto-detects the framework from package.json (devDependencies +
|
|
8
8
|
* dependencies) via detectTestFramework().
|
|
9
|
-
* 2.
|
|
9
|
+
* 2. Resolves the project-LOCAL runner binary (node_modules) and spawns
|
|
10
|
+
* that, so the command works where the runner is not on PATH — notably
|
|
11
|
+
* Windows, where node_modules/.bin/vitest.cmd is not spawnable without
|
|
12
|
+
* a shell. PATH is a last resort and is reported, not silent.
|
|
13
|
+
* 3. Spawns the framework's CLI with --cache enabled (overriding any
|
|
10
14
|
* --no-cache in the consumer's `test` script). The user can
|
|
11
15
|
* opt back into no-cache via `peaks test --no-cache` or
|
|
12
16
|
* `peaks test --passthrough`.
|
|
13
|
-
*
|
|
17
|
+
* 4. Skips tests where (fileMtime, fileSha256) is unchanged AND the
|
|
14
18
|
* previous run status was 'passed' (per-test fingerprint cache at
|
|
15
19
|
* `<projectRoot>/.peaks/_runtime/test-cache/<hash>.json`).
|
|
16
|
-
*
|
|
20
|
+
* 5. Exits 0 on all-pass / all-skip; exits 1 on any failure.
|
|
17
21
|
*
|
|
18
22
|
* The CLI is invoked by USER (not just by skill) per slice 2.5.0
|
|
19
23
|
* sub-fix B (G16) — a documented exception to the
|
|
@@ -29,6 +33,7 @@
|
|
|
29
33
|
* peaks test --passthrough — do NOT override the consumer's argv
|
|
30
34
|
* peaks test --framework <name> — force a specific framework
|
|
31
35
|
*/
|
|
36
|
+
import { spawn } from 'node:child_process';
|
|
32
37
|
import type { Command } from 'commander';
|
|
33
38
|
import { type ProgramIO } from '../cli-helpers.js';
|
|
34
39
|
import { type TestFramework } from '../../services/test-cache/test-cache-service.js';
|
|
@@ -44,4 +49,56 @@ export declare function buildRunnerArgv(framework: TestFramework, patterns: stri
|
|
|
44
49
|
cache?: boolean;
|
|
45
50
|
passthrough?: boolean;
|
|
46
51
|
}): string[];
|
|
52
|
+
/** Successful resolution — everything `spawn` needs, plus provenance. */
|
|
53
|
+
export type RunnerFound = {
|
|
54
|
+
ok: true;
|
|
55
|
+
/** Executable to spawn: node itself, a local shim, or a PATH hit. */
|
|
56
|
+
command: string;
|
|
57
|
+
/** argv for `command` (includes the JS entry when spawning node). */
|
|
58
|
+
args: string[];
|
|
59
|
+
/** How the runner was found — named in the PATH-fallback notice. */
|
|
60
|
+
via: string;
|
|
61
|
+
/** True only for the PATH fallback, which the caller surfaces visibly. */
|
|
62
|
+
fromPath: boolean;
|
|
63
|
+
};
|
|
64
|
+
export type RunnerResolution = RunnerFound | {
|
|
65
|
+
ok: false;
|
|
66
|
+
searched: string[];
|
|
67
|
+
};
|
|
68
|
+
/** Injection seams for tests — production passes nothing. */
|
|
69
|
+
export type ResolveRunnerDeps = {
|
|
70
|
+
platform?: NodeJS.Platform;
|
|
71
|
+
existsSync?: (path: string) => boolean;
|
|
72
|
+
readFileSync?: (path: string) => string;
|
|
73
|
+
env?: NodeJS.ProcessEnv;
|
|
74
|
+
/** Node executable used to run the runner's JS entry. */
|
|
75
|
+
nodeExecPath?: string;
|
|
76
|
+
};
|
|
77
|
+
export type RunRunnerDeps = ResolveRunnerDeps & {
|
|
78
|
+
spawnFn?: typeof spawn;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Resolve the consumer project's LOCAL runner, and the form of it that
|
|
82
|
+
* `spawn` can actually launch on this platform.
|
|
83
|
+
*
|
|
84
|
+
* Probed, in order (every probe is reported when nothing is found):
|
|
85
|
+
* 1. `<root>/node_modules/.bin/<runner>` (+ `.cmd`/`.exe` on Windows) —
|
|
86
|
+
* the project-local runner the command documents.
|
|
87
|
+
* 2. `<root>/node_modules/<runner>/package.json` → its `bin` JS entry.
|
|
88
|
+
* 3. PATH — last resort only; the caller prints a visible notice.
|
|
89
|
+
*
|
|
90
|
+
* Between 1 and 2 the **JS entry** wins: `spawn` runs it as
|
|
91
|
+
* `node <entry> …`, which is identical on Windows and POSIX and never
|
|
92
|
+
* routes argv through a shell. The `.cmd` shim is only a fallback because
|
|
93
|
+
* it needs cmd.exe to launch it (see `toSpawnable`).
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveRunner(framework: TestFramework, argv: string[], projectRoot: string, deps?: ResolveRunnerDeps): RunnerResolution;
|
|
96
|
+
/** Actionable message for the no-runner case — never a raw ENOENT. */
|
|
97
|
+
export declare function formatRunnerNotFound(framework: TestFramework, searched: string[]): string;
|
|
98
|
+
export declare function runRunner(framework: TestFramework, argv: string[], projectRoot: string, deps?: RunRunnerDeps): Promise<{
|
|
99
|
+
code: number;
|
|
100
|
+
stdout: string;
|
|
101
|
+
stderr: string;
|
|
102
|
+
notice: string | null;
|
|
103
|
+
}>;
|
|
47
104
|
export declare function registerTestCommands(program: Command, _io: ProgramIO): void;
|