@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 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): Promise<ValidateGateResult>;
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
- * If a validation-report.json exists and payload.passed === false, block transition.
75
- * If the artifact is absent, allow (backward compat).
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 < 70) {
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
- * If a validation-report.json exists and payload.passed === false, block transition.
247
- * If the artifact is absent, allow (backward compat).
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 null;
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
- // Other errors (malformed file) — best-effort: allow
264
- return null;
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
- const artifactPath = `external Planu project data: handoffs/${specId}/validation-report.json`;
268
- return {
269
- content: [
270
- {
271
- type: 'text',
272
- text: JSON.stringify({
273
- error: 'validation_report_failed',
274
- message: `Validation report at ${artifactPath} has passed: false — fix all failing gates before marking done.`,
275
- artifactPath,
276
- gates: result.payload.gates,
277
- fixHint: 'Fix all failing gates and re-run validation, then retry update_status(done). Use force:true to bypass.',
278
- }, null, 2),
279
- },
280
- ],
281
- isError: true,
282
- structuredContent: {
283
- error: 'validation_report_failed',
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
- /* best-effort — never block transition */
294
- return null;
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 < 70
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: 'Fix failing acceptance criteria, then retry. Or use forceStatus:true + forceStatusReason (≥100 chars).',
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. ' +
@@ -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.1",
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.1",
36
- "@planu/core-darwin-x64": "4.2.1",
37
- "@planu/core-linux-arm64-gnu": "4.2.1",
38
- "@planu/core-linux-arm64-musl": "4.2.1",
39
- "@planu/core-linux-x64-gnu": "4.2.1",
40
- "@planu/core-linux-x64-musl": "4.2.1",
41
- "@planu/core-win32-arm64-msvc": "4.2.1",
42
- "@planu/core-win32-x64-msvc": "4.2.1"
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.1",
192
+ "knip": "^6.14.2",
193
193
  "lint-staged": "^17.0.5",
194
194
  "madge": "^8.0.0",
195
195
  "prettier": "^3.8.3",