@planu/cli 4.2.3 → 4.2.4
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 +8 -0
- package/dist/engine/handoff-artifacts/schemas.d.ts +30 -2
- package/dist/engine/handoff-artifacts/schemas.js +7 -0
- package/dist/engine/validator/spec-review-writer.d.ts +14 -0
- package/dist/engine/validator/spec-review-writer.js +49 -0
- package/dist/tools/update-status/dod-gates.d.ts +4 -0
- package/dist/tools/update-status/dod-gates.js +90 -0
- package/dist/tools/update-status/index.js +13 -1
- package/dist/types/handoff-artifacts.d.ts +7 -0
- package/package.json +9 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [4.2.4] - 2026-05-22
|
|
2
|
+
|
|
3
|
+
**Tarball SHA-256:** `963c0c81820ad002206299f102fdcae6635d96cdbe2e602e4f1dbb2d1b41e0c5`
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
- feat(planu): require spec reviewer before approval
|
|
7
|
+
|
|
8
|
+
|
|
1
9
|
## [4.2.3] - 2026-05-22
|
|
2
10
|
|
|
3
11
|
**Tarball SHA-256:** `6d23ef6f7bd12e2a94eb9984e272beb37cd0d9b54ad12170529f95ca870e46a4`
|
|
@@ -31,13 +31,27 @@ export declare const SpecLockV1Schema: z.ZodObject<{
|
|
|
31
31
|
}, z.core.$strip>;
|
|
32
32
|
export declare const ReviewFeedbackV1Schema: z.ZodObject<{
|
|
33
33
|
schema_version: z.ZodString;
|
|
34
|
+
specId: z.ZodString;
|
|
34
35
|
verdict: z.ZodEnum<{
|
|
35
36
|
approved: "approved";
|
|
36
37
|
"changes-requested": "changes-requested";
|
|
37
38
|
}>;
|
|
39
|
+
reviewer: z.ZodObject<{
|
|
40
|
+
kind: z.ZodEnum<{
|
|
41
|
+
human: "human";
|
|
42
|
+
"spec-review-agent": "spec-review-agent";
|
|
43
|
+
automation: "automation";
|
|
44
|
+
}>;
|
|
45
|
+
agent: z.ZodString;
|
|
46
|
+
verdict: z.ZodEnum<{
|
|
47
|
+
approved: "approved";
|
|
48
|
+
"changes-requested": "changes-requested";
|
|
49
|
+
}>;
|
|
50
|
+
}, z.core.$strip>;
|
|
38
51
|
blockers: z.ZodArray<z.ZodString>;
|
|
39
52
|
suggestions: z.ZodArray<z.ZodString>;
|
|
40
53
|
body: z.ZodString;
|
|
54
|
+
reviewedAt: z.ZodISODateTime;
|
|
41
55
|
}, z.core.$strip>;
|
|
42
56
|
export declare const ImplementationReportV1Schema: z.ZodObject<{
|
|
43
57
|
schema_version: z.ZodString;
|
|
@@ -60,8 +74,8 @@ export declare const ValidationReportV1Schema: z.ZodObject<{
|
|
|
60
74
|
reviewer: z.ZodObject<{
|
|
61
75
|
kind: z.ZodEnum<{
|
|
62
76
|
human: "human";
|
|
63
|
-
"implementation-review-agent": "implementation-review-agent";
|
|
64
77
|
automation: "automation";
|
|
78
|
+
"implementation-review-agent": "implementation-review-agent";
|
|
65
79
|
}>;
|
|
66
80
|
agent: z.ZodString;
|
|
67
81
|
verdict: z.ZodEnum<{
|
|
@@ -115,13 +129,27 @@ export declare const ARTIFACT_SCHEMAS: {
|
|
|
115
129
|
}, z.core.$strip>;
|
|
116
130
|
readonly review_feedback: z.ZodObject<{
|
|
117
131
|
schema_version: z.ZodString;
|
|
132
|
+
specId: z.ZodString;
|
|
118
133
|
verdict: z.ZodEnum<{
|
|
119
134
|
approved: "approved";
|
|
120
135
|
"changes-requested": "changes-requested";
|
|
121
136
|
}>;
|
|
137
|
+
reviewer: z.ZodObject<{
|
|
138
|
+
kind: z.ZodEnum<{
|
|
139
|
+
human: "human";
|
|
140
|
+
"spec-review-agent": "spec-review-agent";
|
|
141
|
+
automation: "automation";
|
|
142
|
+
}>;
|
|
143
|
+
agent: z.ZodString;
|
|
144
|
+
verdict: z.ZodEnum<{
|
|
145
|
+
approved: "approved";
|
|
146
|
+
"changes-requested": "changes-requested";
|
|
147
|
+
}>;
|
|
148
|
+
}, z.core.$strip>;
|
|
122
149
|
blockers: z.ZodArray<z.ZodString>;
|
|
123
150
|
suggestions: z.ZodArray<z.ZodString>;
|
|
124
151
|
body: z.ZodString;
|
|
152
|
+
reviewedAt: z.ZodISODateTime;
|
|
125
153
|
}, z.core.$strip>;
|
|
126
154
|
readonly 'implementation-report': z.ZodObject<{
|
|
127
155
|
schema_version: z.ZodString;
|
|
@@ -144,8 +172,8 @@ export declare const ARTIFACT_SCHEMAS: {
|
|
|
144
172
|
reviewer: z.ZodObject<{
|
|
145
173
|
kind: z.ZodEnum<{
|
|
146
174
|
human: "human";
|
|
147
|
-
"implementation-review-agent": "implementation-review-agent";
|
|
148
175
|
automation: "automation";
|
|
176
|
+
"implementation-review-agent": "implementation-review-agent";
|
|
149
177
|
}>;
|
|
150
178
|
agent: z.ZodString;
|
|
151
179
|
verdict: z.ZodEnum<{
|
|
@@ -27,10 +27,17 @@ export const SpecLockV1Schema = z.object({
|
|
|
27
27
|
});
|
|
28
28
|
export const ReviewFeedbackV1Schema = z.object({
|
|
29
29
|
schema_version: SchemaVersion,
|
|
30
|
+
specId: z.string(),
|
|
30
31
|
verdict: z.enum(['approved', 'changes-requested']),
|
|
32
|
+
reviewer: z.object({
|
|
33
|
+
kind: z.enum(['spec-review-agent', 'human', 'automation']),
|
|
34
|
+
agent: z.string().min(1),
|
|
35
|
+
verdict: z.enum(['approved', 'changes-requested']),
|
|
36
|
+
}),
|
|
31
37
|
blockers: z.array(z.string()),
|
|
32
38
|
suggestions: z.array(z.string()),
|
|
33
39
|
body: z.string(),
|
|
40
|
+
reviewedAt: z.iso.datetime(),
|
|
34
41
|
});
|
|
35
42
|
export const ImplementationReportV1Schema = z.object({
|
|
36
43
|
schema_version: SchemaVersion,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Spec } from '../../types/index.js';
|
|
2
|
+
import type { ReviewFeedbackV1 } from '../../types/handoff-artifacts.js';
|
|
3
|
+
export declare function writeSpecReviewFeedback(input: {
|
|
4
|
+
projectId: string;
|
|
5
|
+
specId: string;
|
|
6
|
+
spec: Spec;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
written: boolean;
|
|
9
|
+
path?: string;
|
|
10
|
+
sha?: string;
|
|
11
|
+
payload: ReviewFeedbackV1;
|
|
12
|
+
error?: string;
|
|
13
|
+
}>;
|
|
14
|
+
//# sourceMappingURL=spec-review-writer.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { appendArtifact } from '../handoff-artifacts/io.js';
|
|
2
|
+
export async function writeSpecReviewFeedback(input) {
|
|
3
|
+
const blockers = buildSpecReviewBlockers(input.spec);
|
|
4
|
+
const verdict = blockers.length === 0 ? 'approved' : 'changes-requested';
|
|
5
|
+
const payload = {
|
|
6
|
+
schema_version: '1.0.0',
|
|
7
|
+
specId: input.specId,
|
|
8
|
+
verdict,
|
|
9
|
+
reviewer: {
|
|
10
|
+
kind: 'spec-review-agent',
|
|
11
|
+
agent: 'planu-spec-reviewer',
|
|
12
|
+
verdict,
|
|
13
|
+
},
|
|
14
|
+
blockers,
|
|
15
|
+
suggestions: blockers.length === 0
|
|
16
|
+
? ['Proceed to approval gates; keep implementation reviewer separate after coding.']
|
|
17
|
+
: [
|
|
18
|
+
'Resolve blockers, rerun challenge/readiness checks, then move the spec back to review.',
|
|
19
|
+
],
|
|
20
|
+
body: blockers.length === 0
|
|
21
|
+
? `Spec ${input.specId} reviewed by planu-spec-reviewer and approved for approval gates.`
|
|
22
|
+
: `Spec ${input.specId} reviewed by planu-spec-reviewer with requested changes.`,
|
|
23
|
+
reviewedAt: new Date().toISOString(),
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
const artifact = await appendArtifact({
|
|
27
|
+
projectId: input.projectId,
|
|
28
|
+
specId: input.specId,
|
|
29
|
+
kind: 'review_feedback',
|
|
30
|
+
payload,
|
|
31
|
+
});
|
|
32
|
+
return { written: true, path: artifact.path, sha: artifact.sha, payload };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return {
|
|
36
|
+
written: false,
|
|
37
|
+
payload,
|
|
38
|
+
error: err instanceof Error ? err.message : String(err),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function buildSpecReviewBlockers(spec) {
|
|
43
|
+
const blockers = [];
|
|
44
|
+
if (!spec.title || spec.title.trim().length === 0) {
|
|
45
|
+
blockers.push('Spec title is missing.');
|
|
46
|
+
}
|
|
47
|
+
return blockers;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=spec-review-writer.js.map
|
|
@@ -78,6 +78,10 @@ export interface DoneGateResult {
|
|
|
78
78
|
* with reviewer evidence and all validation gates passing.
|
|
79
79
|
*/
|
|
80
80
|
export declare function checkValidationReportGate(specId: string, projectId: string, force: boolean | undefined): Promise<ToolResult | null>;
|
|
81
|
+
/** SPEC-1051: Write spec-review evidence when a spec enters review. */
|
|
82
|
+
export declare function writeSpecReviewArtifact(spec: Spec, specId: string, projectId: string): Promise<ToolResult | null>;
|
|
83
|
+
/** SPEC-1051: Approval requires a dedicated spec reviewer artifact. */
|
|
84
|
+
export declare function checkSpecReviewGate(specId: string, projectId: string, _forceApprove: boolean | undefined): Promise<ToolResult | null>;
|
|
81
85
|
/**
|
|
82
86
|
* SPEC-335: Combined done-gates runner — DoD + security.
|
|
83
87
|
* When force=true, gates are not blocking but failing items are recorded in
|
|
@@ -9,6 +9,7 @@ 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
11
|
import { writeImplementationReviewReport } from '../../engine/validator/validation-report-writer.js';
|
|
12
|
+
import { writeSpecReviewFeedback } from '../../engine/validator/spec-review-writer.js';
|
|
12
13
|
/**
|
|
13
14
|
* SPEC-721 / SPEC-222 Trigger 1: Run validate engine before marking done.
|
|
14
15
|
*
|
|
@@ -320,6 +321,71 @@ export async function checkValidationReportGate(specId, projectId, force) {
|
|
|
320
321
|
});
|
|
321
322
|
}
|
|
322
323
|
}
|
|
324
|
+
/** SPEC-1051: Write spec-review evidence when a spec enters review. */
|
|
325
|
+
export async function writeSpecReviewArtifact(spec, specId, projectId) {
|
|
326
|
+
const result = await writeSpecReviewFeedback({ projectId, specId, spec });
|
|
327
|
+
if (result.written) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return specReviewGateError({
|
|
331
|
+
specId,
|
|
332
|
+
error: 'spec_review_write_failed',
|
|
333
|
+
message: `Could not write spec review feedback: ${result.error ?? 'unknown error'}`,
|
|
334
|
+
blockers: result.payload.blockers,
|
|
335
|
+
fixHint: 'Fix the artifact store issue, then retry update_status(review).',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
/** SPEC-1051: Approval requires a dedicated spec reviewer artifact. */
|
|
339
|
+
export async function checkSpecReviewGate(specId, projectId, _forceApprove) {
|
|
340
|
+
try {
|
|
341
|
+
const { readArtifact } = await import('../../engine/handoff-artifacts/io.js');
|
|
342
|
+
const result = await readArtifact({ projectId, specId, kind: 'review_feedback' });
|
|
343
|
+
if (!result.ok) {
|
|
344
|
+
const firstErr = result.errors[0];
|
|
345
|
+
return specReviewGateError({
|
|
346
|
+
specId,
|
|
347
|
+
error: firstErr?.code === 'ARTIFACT_NOT_FOUND' ? 'spec_review_missing' : 'spec_review_invalid',
|
|
348
|
+
message: firstErr?.code === 'ARTIFACT_NOT_FOUND'
|
|
349
|
+
? 'No spec review feedback exists for this spec. Move it to review so planu-spec-reviewer can review it before approval.'
|
|
350
|
+
: 'Spec review feedback is malformed or uses an obsolete schema. Re-run update_status(review).',
|
|
351
|
+
blockers: [],
|
|
352
|
+
fixHint: firstErr?.code === 'ARTIFACT_NOT_FOUND'
|
|
353
|
+
? 'Run update_status(review), resolve any review blockers, then retry update_status(approved).'
|
|
354
|
+
: 'Re-run update_status(review) to regenerate reviewer evidence, then retry approval.',
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const review = result.payload;
|
|
358
|
+
if (review.verdict !== 'approved' || review.reviewer.verdict !== 'approved') {
|
|
359
|
+
return specReviewGateError({
|
|
360
|
+
specId,
|
|
361
|
+
error: 'spec_review_changes_requested',
|
|
362
|
+
message: 'Spec reviewer requested changes. Approval is blocked until the review passes.',
|
|
363
|
+
blockers: review.blockers,
|
|
364
|
+
fixHint: 'Resolve the spec review blockers, rerun update_status(review), then retry approval.',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (review.reviewer.kind !== 'spec-review-agent' ||
|
|
368
|
+
review.reviewer.agent !== 'planu-spec-reviewer') {
|
|
369
|
+
return specReviewGateError({
|
|
370
|
+
specId,
|
|
371
|
+
error: 'spec_review_wrong_reviewer',
|
|
372
|
+
message: 'Approval requires a dedicated spec reviewer. The implementation reviewer cannot approve the spec plan.',
|
|
373
|
+
blockers: [`Reviewer was ${review.reviewer.agent}. Expected planu-spec-reviewer.`],
|
|
374
|
+
fixHint: 'Run update_status(review) so planu-spec-reviewer writes a fresh review artifact.',
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
return specReviewGateError({
|
|
381
|
+
specId,
|
|
382
|
+
error: 'spec_review_unreadable',
|
|
383
|
+
message: `Could not read spec review feedback: ${err instanceof Error ? err.message : String(err)}`,
|
|
384
|
+
blockers: [],
|
|
385
|
+
fixHint: 'Re-run update_status(review), then retry update_status(approved).',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
323
389
|
function validationReportGateError(args) {
|
|
324
390
|
const artifactPath = `external Planu project data: handoffs/${args.specId}/validation-report.json`;
|
|
325
391
|
return {
|
|
@@ -344,6 +410,30 @@ function validationReportGateError(args) {
|
|
|
344
410
|
},
|
|
345
411
|
};
|
|
346
412
|
}
|
|
413
|
+
function specReviewGateError(args) {
|
|
414
|
+
const artifactPath = `external Planu project data: handoffs/${args.specId}/review_feedback.md`;
|
|
415
|
+
return {
|
|
416
|
+
content: [
|
|
417
|
+
{
|
|
418
|
+
type: 'text',
|
|
419
|
+
text: JSON.stringify({
|
|
420
|
+
error: args.error,
|
|
421
|
+
message: args.message,
|
|
422
|
+
artifactPath,
|
|
423
|
+
blockers: args.blockers,
|
|
424
|
+
fixHint: args.fixHint,
|
|
425
|
+
}, null, 2),
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
isError: true,
|
|
429
|
+
structuredContent: {
|
|
430
|
+
error: args.error,
|
|
431
|
+
code: 422,
|
|
432
|
+
context: { specId: args.specId, artifactPath, blockers: args.blockers },
|
|
433
|
+
fixHint: args.fixHint,
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
347
437
|
/**
|
|
348
438
|
* SPEC-335: Combined done-gates runner — DoD + security.
|
|
349
439
|
* When force=true, gates are not blocking but failing items are recorded in
|
|
@@ -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, checkValidationReportGate, } from './dod-gates.js';
|
|
16
|
+
import { runValidateGate, checkDoneGates, checkComplianceGate, checkQaGate, checkApprovedFormatGate, checkValidationReportGate, checkSpecReviewGate, writeSpecReviewArtifact, } 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';
|
|
@@ -398,6 +398,18 @@ export async function handleUpdateStatus(params, server) {
|
|
|
398
398
|
if (challengeGate) {
|
|
399
399
|
return challengeGate;
|
|
400
400
|
}
|
|
401
|
+
if (newStatus === 'review') {
|
|
402
|
+
const specReviewWriteError = await writeSpecReviewArtifact(spec, specId, projectId);
|
|
403
|
+
if (specReviewWriteError) {
|
|
404
|
+
return specReviewWriteError;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (newStatus === 'approved') {
|
|
408
|
+
const specReviewError = await checkSpecReviewGate(specId, projectId, params.forceApprove);
|
|
409
|
+
if (specReviewError) {
|
|
410
|
+
return specReviewError;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
401
413
|
// SPEC-595: Elicit confirmation for destructive status transitions (→done with forceStatus)
|
|
402
414
|
const elicitResult = await runForceStatusElicitation(server, specId, newStatus, params.forceStatus);
|
|
403
415
|
if (elicitResult) {
|
|
@@ -26,10 +26,17 @@ export interface SpecLockV1 {
|
|
|
26
26
|
/** Reviewer → Planner: structured feedback from code review */
|
|
27
27
|
export interface ReviewFeedbackV1 {
|
|
28
28
|
schema_version: string;
|
|
29
|
+
specId: string;
|
|
29
30
|
verdict: 'approved' | 'changes-requested';
|
|
31
|
+
reviewer: {
|
|
32
|
+
kind: 'spec-review-agent' | 'human' | 'automation';
|
|
33
|
+
agent: string;
|
|
34
|
+
verdict: 'approved' | 'changes-requested';
|
|
35
|
+
};
|
|
30
36
|
blockers: string[];
|
|
31
37
|
suggestions: string[];
|
|
32
38
|
body: string;
|
|
39
|
+
reviewedAt: string;
|
|
33
40
|
}
|
|
34
41
|
/** Implementer → Validator: result of implementation run */
|
|
35
42
|
export interface ImplementationReportV1 {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.4",
|
|
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.4",
|
|
36
|
+
"@planu/core-darwin-x64": "4.2.4",
|
|
37
|
+
"@planu/core-linux-arm64-gnu": "4.2.4",
|
|
38
|
+
"@planu/core-linux-arm64-musl": "4.2.4",
|
|
39
|
+
"@planu/core-linux-x64-gnu": "4.2.4",
|
|
40
|
+
"@planu/core-linux-x64-musl": "4.2.4",
|
|
41
|
+
"@planu/core-win32-arm64-msvc": "4.2.4",
|
|
42
|
+
"@planu/core-win32-x64-msvc": "4.2.4"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=24.0.0"
|