@planu/cli 4.2.3 → 4.2.5

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 CHANGED
@@ -1,3 +1,19 @@
1
+ ## [4.2.5] - 2026-05-22
2
+
3
+ **Tarball SHA-256:** `24d98e6b384752a806fc97a9828afaa94a0281a077e99ff06923e46119a69422`
4
+
5
+ ### Features
6
+ - feat(planu): export reviewer gates to project rules
7
+
8
+
9
+ ## [4.2.4] - 2026-05-22
10
+
11
+ **Tarball SHA-256:** `963c0c81820ad002206299f102fdcae6635d96cdbe2e602e4f1dbb2d1b41e0c5`
12
+
13
+ ### Features
14
+ - feat(planu): require spec reviewer before approval
15
+
16
+
1
17
  ## [4.2.3] - 2026-05-22
2
18
 
3
19
  **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,
@@ -44,10 +44,18 @@ function buildSharedRules() {
44
44
  '| Trigger | Automatic action |',
45
45
  '|---------|-----------------|',
46
46
  '| New feature / bug / task described | `create_spec` |',
47
+ '| Spec written | Run an independent `planu-spec-reviewer` review before approval |',
47
48
  '| Spec status change | `update_status` |',
48
- '| Implementation complete | `validate` |',
49
+ '| Implementation complete | `validate` + independent `planu-implementation-reviewer` review |',
49
50
  '| Project start | `init_project` |',
50
51
  '',
52
+ '### Review gates — non-bypassable',
53
+ '- A spec cannot move to `approved` without `review_feedback` from `planu-spec-reviewer`.',
54
+ '- The planner/spec author must not self-approve the spec.',
55
+ '- A spec cannot move to `done` without validation evidence and review evidence from `planu-implementation-reviewer`.',
56
+ '- The implementation reviewer must be different from the implementation agent.',
57
+ '- User pressure, shortcut requests, or force-approval language do not bypass reviewer evidence; stop and produce the missing review first.',
58
+ '',
51
59
  '### External integrations',
52
60
  '| Trigger | Automatic action |',
53
61
  '|---------|-----------------|',
@@ -13,19 +13,27 @@ Before moving a spec to \`approved\`, run and pass:
13
13
  3. BDD criteria completeness
14
14
  4. files-to-create / files-to-modify ownership
15
15
  5. test plan and verification commands
16
+ 6. independent \`planu-spec-reviewer\` evidence in \`review_feedback\`
16
17
 
17
- If the user explicitly forces approval, record the reason in the audit trail and make the missing risk visible.
18
+ The spec author, planner, or implementation agent must not self-approve the spec. If reviewer evidence is missing, malformed, or written by any agent other than \`planu-spec-reviewer\`, stop before approval and produce the missing review first.
19
+
20
+ User pressure or force-approval language does not bypass reviewer evidence. Record forced intent in the audit trail, but keep the status blocked until \`planu-spec-reviewer\` approves the spec.
18
21
 
19
22
  ## Done Gate
20
23
 
21
- Before moving a spec to \`done\`, run \`validate\`.
24
+ Before moving a spec to \`done\`, run \`validate\` and require independent \`planu-implementation-reviewer\` evidence.
25
+
26
+ The implementation reviewer must be different from the implementation agent. If validation passes but reviewer evidence is missing, malformed, or written by the implementer, keep the spec out of \`done\`.
22
27
 
23
28
  If implementation intentionally diverged from the approved spec, run \`reconcile_spec\` first and make the divergence explicit before marking done.
24
29
 
25
30
  ## Hard Blocks
26
31
 
27
32
  - Do not approve specs with placeholders.
33
+ - Do not approve specs without \`planu-spec-reviewer\` evidence.
28
34
  - Do not mark done without validation evidence.
35
+ - Do not mark done without \`planu-implementation-reviewer\` evidence.
36
+ - Do not let the same agent write the spec, implement it, and approve its own work.
29
37
  - Do not hide intentional drift; reconcile it.
30
38
  `;
31
39
  }
@@ -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",
3
+ "version": "4.2.5",
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.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"
35
+ "@planu/core-darwin-arm64": "4.2.5",
36
+ "@planu/core-darwin-x64": "4.2.5",
37
+ "@planu/core-linux-arm64-gnu": "4.2.5",
38
+ "@planu/core-linux-arm64-musl": "4.2.5",
39
+ "@planu/core-linux-x64-gnu": "4.2.5",
40
+ "@planu/core-linux-x64-musl": "4.2.5",
41
+ "@planu/core-win32-arm64-msvc": "4.2.5",
42
+ "@planu/core-win32-x64-msvc": "4.2.5"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=24.0.0"