@planu/cli 4.2.1 → 4.2.3
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 +18 -0
- package/dist/engine/handoff-artifacts/schemas.d.ts +50 -0
- package/dist/engine/handoff-artifacts/schemas.js +16 -0
- package/dist/engine/validator/validation-report-writer.d.ts +21 -0
- package/dist/engine/validator/validation-report-writer.js +106 -0
- package/dist/tools/update-status/dod-gates.d.ts +6 -4
- package/dist/tools/update-status/dod-gates.js +84 -39
- package/dist/tools/update-status/index.js +9 -3
- package/dist/tools/update-status/response-builder.js +1 -1
- package/dist/tools/validate.js +12 -0
- package/dist/types/handoff-artifacts.d.ts +14 -0
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [4.2.3] - 2026-05-22
|
|
2
|
+
|
|
3
|
+
**Tarball SHA-256:** `6d23ef6f7bd12e2a94eb9984e272beb37cd0d9b54ad12170529f95ca870e46a4`
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- feat(planu): require implementation reviewer gate
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## [4.2.2] - 2026-05-21
|
|
10
|
+
|
|
11
|
+
**Tarball SHA-256:** `83193f54dc2fc5f461ef38b8bf2a9931d5d587ece50c77836942351e39b3c415`
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
- fix(release): isolate npm publish cache
|
|
15
|
+
- fix(release): sync native lockfile without moving dev dependencies
|
|
16
|
+
- fix(ci): harden dependency freshness parsing
|
|
17
|
+
|
|
18
|
+
|
|
1
19
|
## [4.2.1] - 2026-05-21
|
|
2
20
|
|
|
3
21
|
**Tarball SHA-256:** `9ff8a396e25eba3e4941bbef80ee19cc41bae46eaafa703cd32092c7ffbfdf2e`
|
|
@@ -57,6 +57,31 @@ export declare const ValidationReportV1Schema: z.ZodObject<{
|
|
|
57
57
|
passed: z.ZodBoolean;
|
|
58
58
|
reason: z.ZodOptional<z.ZodString>;
|
|
59
59
|
}, z.core.$strip>>;
|
|
60
|
+
reviewer: z.ZodObject<{
|
|
61
|
+
kind: z.ZodEnum<{
|
|
62
|
+
human: "human";
|
|
63
|
+
"implementation-review-agent": "implementation-review-agent";
|
|
64
|
+
automation: "automation";
|
|
65
|
+
}>;
|
|
66
|
+
agent: z.ZodString;
|
|
67
|
+
verdict: z.ZodEnum<{
|
|
68
|
+
approved: "approved";
|
|
69
|
+
"changes-requested": "changes-requested";
|
|
70
|
+
}>;
|
|
71
|
+
}, z.core.$strip>;
|
|
72
|
+
specCompliance: z.ZodOptional<z.ZodObject<{
|
|
73
|
+
score: z.ZodNumber;
|
|
74
|
+
command: z.ZodString;
|
|
75
|
+
scenarios: z.ZodArray<z.ZodObject<{
|
|
76
|
+
title: z.ZodString;
|
|
77
|
+
verdict: z.ZodEnum<{
|
|
78
|
+
fail: "fail";
|
|
79
|
+
missing: "missing";
|
|
80
|
+
pass: "pass";
|
|
81
|
+
}>;
|
|
82
|
+
evidence: z.ZodArray<z.ZodString>;
|
|
83
|
+
}, z.core.$strip>>;
|
|
84
|
+
}, z.core.$strip>>;
|
|
60
85
|
score: z.ZodOptional<z.ZodNumber>;
|
|
61
86
|
completedAt: z.ZodISODateTime;
|
|
62
87
|
}, z.core.$strip>;
|
|
@@ -116,6 +141,31 @@ export declare const ARTIFACT_SCHEMAS: {
|
|
|
116
141
|
passed: z.ZodBoolean;
|
|
117
142
|
reason: z.ZodOptional<z.ZodString>;
|
|
118
143
|
}, z.core.$strip>>;
|
|
144
|
+
reviewer: z.ZodObject<{
|
|
145
|
+
kind: z.ZodEnum<{
|
|
146
|
+
human: "human";
|
|
147
|
+
"implementation-review-agent": "implementation-review-agent";
|
|
148
|
+
automation: "automation";
|
|
149
|
+
}>;
|
|
150
|
+
agent: z.ZodString;
|
|
151
|
+
verdict: z.ZodEnum<{
|
|
152
|
+
approved: "approved";
|
|
153
|
+
"changes-requested": "changes-requested";
|
|
154
|
+
}>;
|
|
155
|
+
}, z.core.$strip>;
|
|
156
|
+
specCompliance: z.ZodOptional<z.ZodObject<{
|
|
157
|
+
score: z.ZodNumber;
|
|
158
|
+
command: z.ZodString;
|
|
159
|
+
scenarios: z.ZodArray<z.ZodObject<{
|
|
160
|
+
title: z.ZodString;
|
|
161
|
+
verdict: z.ZodEnum<{
|
|
162
|
+
fail: "fail";
|
|
163
|
+
missing: "missing";
|
|
164
|
+
pass: "pass";
|
|
165
|
+
}>;
|
|
166
|
+
evidence: z.ZodArray<z.ZodString>;
|
|
167
|
+
}, z.core.$strip>>;
|
|
168
|
+
}, z.core.$strip>>;
|
|
119
169
|
score: z.ZodOptional<z.ZodNumber>;
|
|
120
170
|
completedAt: z.ZodISODateTime;
|
|
121
171
|
}, z.core.$strip>;
|
|
@@ -50,6 +50,22 @@ export const ValidationReportV1Schema = z.object({
|
|
|
50
50
|
passed: z.boolean(),
|
|
51
51
|
reason: z.string().optional(),
|
|
52
52
|
})),
|
|
53
|
+
reviewer: z.object({
|
|
54
|
+
kind: z.enum(['implementation-review-agent', 'human', 'automation']),
|
|
55
|
+
agent: z.string().min(1),
|
|
56
|
+
verdict: z.enum(['approved', 'changes-requested']),
|
|
57
|
+
}),
|
|
58
|
+
specCompliance: z
|
|
59
|
+
.object({
|
|
60
|
+
score: z.number().min(0).max(100),
|
|
61
|
+
command: z.string(),
|
|
62
|
+
scenarios: z.array(z.object({
|
|
63
|
+
title: z.string(),
|
|
64
|
+
verdict: z.enum(['pass', 'fail', 'missing']),
|
|
65
|
+
evidence: z.array(z.string()),
|
|
66
|
+
})),
|
|
67
|
+
})
|
|
68
|
+
.optional(),
|
|
53
69
|
score: z.number().optional(),
|
|
54
70
|
completedAt: z.iso.datetime(),
|
|
55
71
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Spec } from '../../types/index.js';
|
|
2
|
+
import type { ValidationReportV1 } from '../../types/handoff-artifacts.js';
|
|
3
|
+
export declare function writeImplementationReviewReport(input: {
|
|
4
|
+
projectId: string;
|
|
5
|
+
specId: string;
|
|
6
|
+
spec: Spec;
|
|
7
|
+
projectPath: string;
|
|
8
|
+
score: number | null;
|
|
9
|
+
lintPassed?: boolean;
|
|
10
|
+
conventionRegression?: boolean;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
written: boolean;
|
|
13
|
+
path?: string;
|
|
14
|
+
sha?: string;
|
|
15
|
+
error?: string;
|
|
16
|
+
reviewer: ValidationReportV1['reviewer'];
|
|
17
|
+
passed: boolean;
|
|
18
|
+
gates: ValidationReportV1['gates'];
|
|
19
|
+
specCompliance: NonNullable<ValidationReportV1['specCompliance']>;
|
|
20
|
+
}>;
|
|
21
|
+
//# sourceMappingURL=validation-report-writer.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { appendArtifact } from '../handoff-artifacts/io.js';
|
|
2
|
+
import { runSpecCompliance, } from './spec-compliance-runner.js';
|
|
3
|
+
export async function writeImplementationReviewReport(input) {
|
|
4
|
+
const specCompliance = await runSpecCompliance(input.spec, input.projectPath);
|
|
5
|
+
const gates = buildGates({
|
|
6
|
+
score: input.score,
|
|
7
|
+
lintPassed: input.lintPassed ?? true,
|
|
8
|
+
conventionRegression: input.conventionRegression ?? false,
|
|
9
|
+
specCompliance,
|
|
10
|
+
});
|
|
11
|
+
const passed = gates.every((gate) => gate.passed);
|
|
12
|
+
const reviewer = {
|
|
13
|
+
kind: 'implementation-review-agent',
|
|
14
|
+
agent: 'planu-implementation-reviewer',
|
|
15
|
+
verdict: passed ? 'approved' : 'changes-requested',
|
|
16
|
+
};
|
|
17
|
+
const reportCompliance = toValidationReportCompliance(specCompliance);
|
|
18
|
+
const payload = {
|
|
19
|
+
schema_version: '1.0.0',
|
|
20
|
+
specId: input.specId,
|
|
21
|
+
passed,
|
|
22
|
+
gates,
|
|
23
|
+
reviewer,
|
|
24
|
+
specCompliance: reportCompliance,
|
|
25
|
+
score: input.score ?? undefined,
|
|
26
|
+
completedAt: new Date().toISOString(),
|
|
27
|
+
};
|
|
28
|
+
try {
|
|
29
|
+
const artifact = await appendArtifact({
|
|
30
|
+
projectId: input.projectId,
|
|
31
|
+
specId: input.specId,
|
|
32
|
+
kind: 'validation-report',
|
|
33
|
+
payload,
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
written: true,
|
|
37
|
+
path: artifact.path,
|
|
38
|
+
sha: artifact.sha,
|
|
39
|
+
reviewer,
|
|
40
|
+
passed,
|
|
41
|
+
gates,
|
|
42
|
+
specCompliance: reportCompliance,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
return {
|
|
47
|
+
written: false,
|
|
48
|
+
error: err instanceof Error ? err.message : String(err),
|
|
49
|
+
reviewer,
|
|
50
|
+
passed,
|
|
51
|
+
gates,
|
|
52
|
+
specCompliance: reportCompliance,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function buildGates(args) {
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
name: 'validate-score',
|
|
60
|
+
passed: args.score === 100,
|
|
61
|
+
reason: args.score === 100
|
|
62
|
+
? undefined
|
|
63
|
+
: `Expected validation score 100 before done, got ${String(args.score ?? 'unscored')}.`,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'lint',
|
|
67
|
+
passed: args.lintPassed,
|
|
68
|
+
reason: args.lintPassed ? undefined : 'Configured lint command failed.',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'convention-regression',
|
|
72
|
+
passed: !args.conventionRegression,
|
|
73
|
+
reason: args.conventionRegression ? 'Convention baseline regression detected.' : undefined,
|
|
74
|
+
},
|
|
75
|
+
buildSpecComplianceGate(args.specCompliance),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function buildSpecComplianceGate(result) {
|
|
79
|
+
if (result.perScenario.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
name: 'spec-compliance',
|
|
82
|
+
passed: true,
|
|
83
|
+
reason: 'No executable scenarios declared; spec-compliance runner skipped.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const passed = result.dimensionScore === 100;
|
|
87
|
+
return {
|
|
88
|
+
name: 'spec-compliance',
|
|
89
|
+
passed,
|
|
90
|
+
reason: passed
|
|
91
|
+
? undefined
|
|
92
|
+
: `Expected all executable scenarios to pass, got ${String(result.dimensionScore)}.`,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function toValidationReportCompliance(result) {
|
|
96
|
+
return {
|
|
97
|
+
score: result.dimensionScore,
|
|
98
|
+
command: result.command,
|
|
99
|
+
scenarios: result.perScenario.map((scenario) => ({
|
|
100
|
+
title: scenario.title,
|
|
101
|
+
verdict: scenario.verdict,
|
|
102
|
+
evidence: scenario.evidence,
|
|
103
|
+
})),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=validation-report-writer.js.map
|
|
@@ -45,7 +45,10 @@ export type ValidateGateResult = {
|
|
|
45
45
|
* @param forceStatusReason Why — required and ≥100 chars when forceStatus=true.
|
|
46
46
|
* @param budgetMs Max milliseconds to wait for validate. Default: 9000.
|
|
47
47
|
*/
|
|
48
|
-
export declare function runValidateGate(spec: Spec, projectPath: string, forceStatus: boolean, forceStatusReason: string | undefined, budgetMs?: number
|
|
48
|
+
export declare function runValidateGate(spec: Spec, projectPath: string, forceStatus: boolean, forceStatusReason: string | undefined, budgetMs?: number, reviewContext?: {
|
|
49
|
+
projectId: string;
|
|
50
|
+
specId: string;
|
|
51
|
+
}): Promise<ValidateGateResult>;
|
|
49
52
|
/**
|
|
50
53
|
* Checks the DoD gate before transitioning to 'done'.
|
|
51
54
|
* Returns an error ToolResult if blocked (unless force=true), null if passed.
|
|
@@ -71,9 +74,8 @@ export interface DoneGateResult {
|
|
|
71
74
|
}
|
|
72
75
|
/**
|
|
73
76
|
* SPEC-725: Check the validation-report artifact gate before marking done.
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
* Never throws — always best-effort.
|
|
77
|
+
* SPEC-1050: Fail closed. A done transition requires a fresh validation-report
|
|
78
|
+
* with reviewer evidence and all validation gates passing.
|
|
77
79
|
*/
|
|
78
80
|
export declare function checkValidationReportGate(specId: string, projectId: string, force: boolean | undefined): Promise<ToolResult | null>;
|
|
79
81
|
/**
|
|
@@ -8,6 +8,7 @@ import { getComplianceGateConfig } from '../../storage/compliance-gate-config-st
|
|
|
8
8
|
import { readJson, writeJson, projectDataDir } from '../../storage/base-store.js';
|
|
9
9
|
import { randomUUID } from 'node:crypto';
|
|
10
10
|
import { withEscalation } from '../../engine/escalator/with-escalation.js';
|
|
11
|
+
import { writeImplementationReviewReport } from '../../engine/validator/validation-report-writer.js';
|
|
11
12
|
/**
|
|
12
13
|
* SPEC-721 / SPEC-222 Trigger 1: Run validate engine before marking done.
|
|
13
14
|
*
|
|
@@ -24,7 +25,7 @@ import { withEscalation } from '../../engine/escalator/with-escalation.js';
|
|
|
24
25
|
* @param forceStatusReason Why — required and ≥100 chars when forceStatus=true.
|
|
25
26
|
* @param budgetMs Max milliseconds to wait for validate. Default: 9000.
|
|
26
27
|
*/
|
|
27
|
-
export async function runValidateGate(spec, projectPath, forceStatus, forceStatusReason, budgetMs = 9_000) {
|
|
28
|
+
export async function runValidateGate(spec, projectPath, forceStatus, forceStatusReason, budgetMs = 9_000, reviewContext) {
|
|
28
29
|
// ---- Path A: explicit force bypass ----------------------------------------
|
|
29
30
|
if (forceStatus) {
|
|
30
31
|
if (!forceStatusReason || forceStatusReason.trim().length < 100) {
|
|
@@ -96,13 +97,29 @@ export async function runValidateGate(spec, projectPath, forceStatus, forceStatu
|
|
|
96
97
|
};
|
|
97
98
|
}
|
|
98
99
|
// ---- Normal score check — scoreValue is guaranteed number here (null/undefined filtered above) ---
|
|
99
|
-
if (scoreValue <
|
|
100
|
+
if (scoreValue < 100) {
|
|
101
|
+
if (reviewContext) {
|
|
102
|
+
await writeImplementationReviewReport({
|
|
103
|
+
...reviewContext,
|
|
104
|
+
spec,
|
|
105
|
+
projectPath,
|
|
106
|
+
score: scoreValue,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
100
109
|
return {
|
|
101
110
|
blocked: true,
|
|
102
111
|
score: scoreValue,
|
|
103
112
|
reason: 'validate_score_below_threshold',
|
|
104
113
|
};
|
|
105
114
|
}
|
|
115
|
+
if (reviewContext) {
|
|
116
|
+
await writeImplementationReviewReport({
|
|
117
|
+
...reviewContext,
|
|
118
|
+
spec,
|
|
119
|
+
projectPath,
|
|
120
|
+
score: scoreValue,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
106
123
|
return { blocked: false, score: scoreValue, forced: false };
|
|
107
124
|
}
|
|
108
125
|
/**
|
|
@@ -243,9 +260,8 @@ async function recordForceDoneBypass(specId, projectId, failingItems, warningIte
|
|
|
243
260
|
}
|
|
244
261
|
/**
|
|
245
262
|
* SPEC-725: Check the validation-report artifact gate before marking done.
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* Never throws — always best-effort.
|
|
263
|
+
* SPEC-1050: Fail closed. A done transition requires a fresh validation-report
|
|
264
|
+
* with reviewer evidence and all validation gates passing.
|
|
249
265
|
*/
|
|
250
266
|
export async function checkValidationReportGate(specId, projectId, force) {
|
|
251
267
|
if (force) {
|
|
@@ -254,46 +270,80 @@ export async function checkValidationReportGate(specId, projectId, force) {
|
|
|
254
270
|
try {
|
|
255
271
|
const { readArtifact } = await import('../../engine/handoff-artifacts/io.js');
|
|
256
272
|
const result = await readArtifact({ projectId, specId, kind: 'validation-report' });
|
|
257
|
-
// If not found (ARTIFACT_NOT_FOUND), allow — backward compat
|
|
258
273
|
if (!result.ok) {
|
|
259
274
|
const firstErr = result.errors[0];
|
|
260
275
|
if (firstErr?.code === 'ARTIFACT_NOT_FOUND') {
|
|
261
|
-
return
|
|
276
|
+
return validationReportGateError({
|
|
277
|
+
specId,
|
|
278
|
+
error: 'validation_report_missing',
|
|
279
|
+
message: 'No validation-report artifact exists for this spec. Run validate to generate implementation-review evidence before marking done.',
|
|
280
|
+
gates: [],
|
|
281
|
+
fixHint: 'Run validate for this spec, fix any failing gates, then retry update_status(done). Use force:true only with an audited reason.',
|
|
282
|
+
});
|
|
262
283
|
}
|
|
263
|
-
|
|
264
|
-
|
|
284
|
+
return validationReportGateError({
|
|
285
|
+
specId,
|
|
286
|
+
error: 'validation_report_invalid',
|
|
287
|
+
message: 'The validation-report artifact is malformed or uses an obsolete schema. Re-run validate so Planu can generate reviewer evidence.',
|
|
288
|
+
gates: [],
|
|
289
|
+
fixHint: 'Re-run validate for this spec. The report must include reviewer evidence and passing gates.',
|
|
290
|
+
});
|
|
265
291
|
}
|
|
266
292
|
if (!result.payload.passed) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
code: 422,
|
|
285
|
-
context: { specId, artifactPath, gates: result.payload.gates },
|
|
286
|
-
fixHint: 'Fix failing gates and re-run validation. Use force:true to bypass.',
|
|
287
|
-
},
|
|
288
|
-
};
|
|
293
|
+
return validationReportGateError({
|
|
294
|
+
specId,
|
|
295
|
+
error: 'validation_report_failed',
|
|
296
|
+
message: 'Validation report has passed:false — fix all failing gates before marking done.',
|
|
297
|
+
gates: result.payload.gates,
|
|
298
|
+
fixHint: 'Fix failing gates and re-run validate before marking done.',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (result.payload.reviewer.verdict !== 'approved' ||
|
|
302
|
+
result.payload.reviewer.agent.trim().length === 0) {
|
|
303
|
+
return validationReportGateError({
|
|
304
|
+
specId,
|
|
305
|
+
error: 'validation_report_reviewer_not_approved',
|
|
306
|
+
message: 'The implementation reviewer did not approve this spec. Fix the requested changes and re-run validate.',
|
|
307
|
+
gates: result.payload.gates,
|
|
308
|
+
fixHint: 'Fix reviewer findings and re-run validate before marking done.',
|
|
309
|
+
});
|
|
289
310
|
}
|
|
290
311
|
return null;
|
|
291
312
|
}
|
|
292
|
-
catch {
|
|
293
|
-
|
|
294
|
-
|
|
313
|
+
catch (err) {
|
|
314
|
+
return validationReportGateError({
|
|
315
|
+
specId,
|
|
316
|
+
error: 'validation_report_unreadable',
|
|
317
|
+
message: `Could not read validation-report artifact: ${err instanceof Error ? err.message : String(err)}`,
|
|
318
|
+
gates: [],
|
|
319
|
+
fixHint: 'Re-run validate for this spec, then retry update_status(done).',
|
|
320
|
+
});
|
|
295
321
|
}
|
|
296
322
|
}
|
|
323
|
+
function validationReportGateError(args) {
|
|
324
|
+
const artifactPath = `external Planu project data: handoffs/${args.specId}/validation-report.json`;
|
|
325
|
+
return {
|
|
326
|
+
content: [
|
|
327
|
+
{
|
|
328
|
+
type: 'text',
|
|
329
|
+
text: JSON.stringify({
|
|
330
|
+
error: args.error,
|
|
331
|
+
message: args.message,
|
|
332
|
+
artifactPath,
|
|
333
|
+
gates: args.gates,
|
|
334
|
+
fixHint: args.fixHint,
|
|
335
|
+
}, null, 2),
|
|
336
|
+
},
|
|
337
|
+
],
|
|
338
|
+
isError: true,
|
|
339
|
+
structuredContent: {
|
|
340
|
+
error: args.error,
|
|
341
|
+
code: 422,
|
|
342
|
+
context: { specId: args.specId, artifactPath, gates: args.gates },
|
|
343
|
+
fixHint: args.fixHint,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
297
347
|
/**
|
|
298
348
|
* SPEC-335: Combined done-gates runner — DoD + security.
|
|
299
349
|
* When force=true, gates are not blocking but failing items are recorded in
|
|
@@ -309,11 +359,6 @@ export async function checkDoneGates(spec, specId, projectId, projectPath, force
|
|
|
309
359
|
if (secError) {
|
|
310
360
|
return { blocked: secError, forcedBypassWarning: null };
|
|
311
361
|
}
|
|
312
|
-
// SPEC-725: Check validation-report artifact gate
|
|
313
|
-
const validationReportError = await checkValidationReportGate(specId, projectId, false);
|
|
314
|
-
if (validationReportError) {
|
|
315
|
-
return { blocked: validationReportError, forcedBypassWarning: null };
|
|
316
|
-
}
|
|
317
362
|
return { blocked: null, forcedBypassWarning: null };
|
|
318
363
|
}
|
|
319
364
|
// force=true: run DoD in audit mode — collect results but do not block
|
|
@@ -13,7 +13,7 @@ import { checkApprovedDepGate } from '../../engine/dep-guard/index.js';
|
|
|
13
13
|
import { checkApprovalGate } from '../../engine/approval-workflow.js';
|
|
14
14
|
import * as approvalStore from '../../storage/approval-store.js';
|
|
15
15
|
import { isLocked, getLock } from '../../storage/spec-lock-store.js';
|
|
16
|
-
import { runValidateGate, checkDoneGates, checkComplianceGate, checkQaGate, checkApprovedFormatGate, } from './dod-gates.js';
|
|
16
|
+
import { runValidateGate, checkDoneGates, checkComplianceGate, checkQaGate, checkApprovedFormatGate, checkValidationReportGate, } from './dod-gates.js';
|
|
17
17
|
import { buildStatusResponse, buildValidateBlockedResponse, buildDryRunResponse, } from './response-builder.js';
|
|
18
18
|
import { getSuggestedMode } from './mode-hints.js';
|
|
19
19
|
import { autoFillActuals, createDefaultActuals } from '../../engine/actuals-estimator.js';
|
|
@@ -477,7 +477,7 @@ export async function handleUpdateStatus(params, server) {
|
|
|
477
477
|
// SPEC-721: timeout lives inside runValidateGate (Promise.race) — do NOT wrap with
|
|
478
478
|
// withToolTimeout here, which would silently convert timeout into blocked:false (fail-open).
|
|
479
479
|
newStatus === 'done'
|
|
480
|
-
? runValidateGate(spec, effectiveGatePath ?? '', params.forceStatus ?? false, params.forceStatusReason, 9_000)
|
|
480
|
+
? runValidateGate(spec, effectiveGatePath ?? '', params.forceStatus ?? false, params.forceStatusReason, 9_000, { projectId, specId })
|
|
481
481
|
: Promise.resolve(null),
|
|
482
482
|
// Crash shield: only on 'done', skipped if rate-limited (SPEC-628)
|
|
483
483
|
newStatus === 'done' && effectiveGatePath && !crashShieldSkipReason
|
|
@@ -507,6 +507,12 @@ export async function handleUpdateStatus(params, server) {
|
|
|
507
507
|
};
|
|
508
508
|
}
|
|
509
509
|
}
|
|
510
|
+
if (newStatus === 'done' && !(params.force ?? params.forceStatus ?? false)) {
|
|
511
|
+
const validationReportError = await checkValidationReportGate(specId, projectId, false);
|
|
512
|
+
if (validationReportError) {
|
|
513
|
+
return validationReportError;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
510
516
|
// Process crash shield result — record run timestamp on success (SPEC-628)
|
|
511
517
|
/* c8 ignore next */
|
|
512
518
|
if (crashRisksReport !== null) {
|
|
@@ -1033,7 +1039,7 @@ export async function handleUpdateStatus(params, server) {
|
|
|
1033
1039
|
result.cascadeFastResults = cascadeRunResult.fastHookResults;
|
|
1034
1040
|
}
|
|
1035
1041
|
// SPEC-772 Scenario 4: surface validate failure from cascade as explicit warning
|
|
1036
|
-
const autopilotValidateWarning = validateScore !== null && validateScore <
|
|
1042
|
+
const autopilotValidateWarning = validateScore !== null && validateScore < 100
|
|
1037
1043
|
? `Validate score ${String(validateScore)}/100 — below threshold. Check workspace_alerts for cascade details.`
|
|
1038
1044
|
: null;
|
|
1039
1045
|
if (autopilotValidateWarning) {
|
|
@@ -6,7 +6,7 @@ import { buildUpdateStatusSummary } from '../../engine/human-summary.js';
|
|
|
6
6
|
// SPEC-721: Validate gate blocked response builder
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
const REASON_TO_FIX_HINT = {
|
|
9
|
-
validate_score_below_threshold: '
|
|
9
|
+
validate_score_below_threshold: 'Reach 100/100 validation coverage for the spec, then retry. Or use forceStatus:true + forceStatusReason (≥100 chars).',
|
|
10
10
|
validate_gate_no_criteria: 'Add `scenarios:` to the spec frontmatter (BDD format), or use check_readiness to inject ' +
|
|
11
11
|
'criteria, then retry. Or use forceStatus:true + forceStatusReason (≥100 chars).',
|
|
12
12
|
validate_gate_crash: 'The validate engine crashed. Inspect logs, fix the spec or engine, and retry. ' +
|
package/dist/tools/validate.js
CHANGED
|
@@ -14,6 +14,7 @@ import { validateContractCompliance } from '../engine/test-scaffold-generator.js
|
|
|
14
14
|
import { parseConventions, scanConventions } from '../engine/convention-scanner/index.js';
|
|
15
15
|
import { validateScopeCompliance } from '../engine/scope-boundaries/index.js';
|
|
16
16
|
import { compareWithBaseline } from '../storage/convention-baseline.js';
|
|
17
|
+
import { writeImplementationReviewReport } from '../engine/validator/validation-report-writer.js';
|
|
17
18
|
// Re-export for external use (SPEC-018)
|
|
18
19
|
export { validateContractCompliance };
|
|
19
20
|
/** Build the fallback interactiveQuestion for validation failures. */
|
|
@@ -130,6 +131,16 @@ export async function handleValidate(args, server) {
|
|
|
130
131
|
}
|
|
131
132
|
// 7. Lint check (best-effort — non-blocking)
|
|
132
133
|
const lintCheck = runLintCheck(projectPath, knowledge.lintCommand ?? null);
|
|
134
|
+
// SPEC-1050: Generate a mandatory implementation-review artifact.
|
|
135
|
+
const validationReport = await writeImplementationReviewReport({
|
|
136
|
+
projectId,
|
|
137
|
+
specId,
|
|
138
|
+
spec,
|
|
139
|
+
projectPath,
|
|
140
|
+
score: result.score,
|
|
141
|
+
lintPassed: lintCheck.passed,
|
|
142
|
+
conventionRegression: regressionDetected,
|
|
143
|
+
});
|
|
133
144
|
// 8. Build output
|
|
134
145
|
const output = {
|
|
135
146
|
specId,
|
|
@@ -204,6 +215,7 @@ export async function handleValidate(args, server) {
|
|
|
204
215
|
})),
|
|
205
216
|
},
|
|
206
217
|
lintCheck,
|
|
218
|
+
validationReport,
|
|
207
219
|
};
|
|
208
220
|
// SPEC-612: Scope boundary validation — warn if impl files match outOfScope items
|
|
209
221
|
if (spec.outOfScope !== undefined && spec.outOfScope.length > 0) {
|
|
@@ -51,6 +51,20 @@ export interface ValidationReportV1 {
|
|
|
51
51
|
passed: boolean;
|
|
52
52
|
reason?: string;
|
|
53
53
|
}[];
|
|
54
|
+
reviewer: {
|
|
55
|
+
kind: 'implementation-review-agent' | 'human' | 'automation';
|
|
56
|
+
agent: string;
|
|
57
|
+
verdict: 'approved' | 'changes-requested';
|
|
58
|
+
};
|
|
59
|
+
specCompliance?: {
|
|
60
|
+
score: number;
|
|
61
|
+
command: string;
|
|
62
|
+
scenarios: {
|
|
63
|
+
title: string;
|
|
64
|
+
verdict: 'pass' | 'fail' | 'missing';
|
|
65
|
+
evidence: string[];
|
|
66
|
+
}[];
|
|
67
|
+
};
|
|
54
68
|
score?: number;
|
|
55
69
|
completedAt: string;
|
|
56
70
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.3",
|
|
4
4
|
"description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"packageName": "@planu/core"
|
|
33
33
|
},
|
|
34
34
|
"optionalDependencies": {
|
|
35
|
-
"@planu/core-darwin-arm64": "4.2.
|
|
36
|
-
"@planu/core-darwin-x64": "4.2.
|
|
37
|
-
"@planu/core-linux-arm64-gnu": "4.2.
|
|
38
|
-
"@planu/core-linux-arm64-musl": "4.2.
|
|
39
|
-
"@planu/core-linux-x64-gnu": "4.2.
|
|
40
|
-
"@planu/core-linux-x64-musl": "4.2.
|
|
41
|
-
"@planu/core-win32-arm64-msvc": "4.2.
|
|
42
|
-
"@planu/core-win32-x64-msvc": "4.2.
|
|
35
|
+
"@planu/core-darwin-arm64": "4.2.3",
|
|
36
|
+
"@planu/core-darwin-x64": "4.2.3",
|
|
37
|
+
"@planu/core-linux-arm64-gnu": "4.2.3",
|
|
38
|
+
"@planu/core-linux-arm64-musl": "4.2.3",
|
|
39
|
+
"@planu/core-linux-x64-gnu": "4.2.3",
|
|
40
|
+
"@planu/core-linux-x64-musl": "4.2.3",
|
|
41
|
+
"@planu/core-win32-arm64-msvc": "4.2.3",
|
|
42
|
+
"@planu/core-win32-x64-msvc": "4.2.3"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=24.0.0"
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"happy-dom": "^20.9.0",
|
|
190
190
|
"husky": "^9.1.7",
|
|
191
191
|
"javascript-obfuscator": "^5.4.3",
|
|
192
|
-
"knip": "^6.14.
|
|
192
|
+
"knip": "^6.14.2",
|
|
193
193
|
"lint-staged": "^17.0.5",
|
|
194
194
|
"madge": "^8.0.0",
|
|
195
195
|
"prettier": "^3.8.3",
|