@planu/cli 5.3.50 → 5.3.51
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 +9 -0
- package/dist/engine/evidence-gates/artifact-reader.d.ts +30 -0
- package/dist/engine/evidence-gates/artifact-reader.js +39 -70
- package/dist/engine/evidence-gates/lifecycle-gate.js +7 -2
- package/dist/engine/handoff-artifacts/schema-skeleton.d.ts +5 -0
- package/dist/engine/handoff-artifacts/schema-skeleton.js +108 -0
- package/dist/tools/update-status/dod-gates.d.ts +3 -0
- package/dist/tools/update-status/dod-gates.js +40 -1
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import type { Spec } from '../../types/spec/core.js';
|
|
2
3
|
import type { EvidenceArtifacts, TraceabilityMatrixEvidence } from '../../types/evidence-gates.js';
|
|
4
|
+
export declare const DiscoverySchema: z.ZodObject<{
|
|
5
|
+
version: z.ZodLiteral<1>;
|
|
6
|
+
provenance: z.ZodOptional<z.ZodObject<{
|
|
7
|
+
receiptId: z.ZodString;
|
|
8
|
+
artifactDigest: z.ZodString;
|
|
9
|
+
specId: z.ZodString;
|
|
10
|
+
issuerId: z.ZodString;
|
|
11
|
+
}, z.core.$strict>>;
|
|
12
|
+
rules: z.ZodArray<z.ZodString>;
|
|
13
|
+
examples: z.ZodArray<z.ZodObject<{
|
|
14
|
+
rule: z.ZodString;
|
|
15
|
+
example: z.ZodString;
|
|
16
|
+
}, z.core.$strict>>;
|
|
17
|
+
openQuestions: z.ZodArray<z.ZodObject<{
|
|
18
|
+
question: z.ZodString;
|
|
19
|
+
status: z.ZodEnum<{
|
|
20
|
+
open: "open";
|
|
21
|
+
out_of_scope: "out_of_scope";
|
|
22
|
+
resolved: "resolved";
|
|
23
|
+
}>;
|
|
24
|
+
resolution: z.ZodOptional<z.ZodString>;
|
|
25
|
+
}, z.core.$strict>>;
|
|
26
|
+
outOfScope: z.ZodArray<z.ZodString>;
|
|
27
|
+
glossary: z.ZodArray<z.ZodObject<{
|
|
28
|
+
term: z.ZodString;
|
|
29
|
+
meaning: z.ZodString;
|
|
30
|
+
}, z.core.$strict>>;
|
|
31
|
+
}, z.core.$strict>;
|
|
32
|
+
export declare const DISCOVERY_TEMPLATE_OVERRIDES: Record<string, unknown>;
|
|
3
33
|
/** Digest of canonical artifact content; the provenance envelope is excluded. */
|
|
4
34
|
export declare function evidenceArtifactDigest(artifact: unknown): string;
|
|
5
35
|
export declare function evidenceArtifactCollectionDigest(artifacts: readonly {
|
|
@@ -7,6 +7,7 @@ import { z } from 'zod';
|
|
|
7
7
|
import { projectDataDir } from '../../storage/base-store.js';
|
|
8
8
|
import { computeValidationEvidenceIdentity, computeValidationEvidenceKey, normalizeValidationCommand, readMatchingValidationEvidence, } from '../validation-evidence-ledger.js';
|
|
9
9
|
import { VALIDATION_COMMANDS } from '../validation-impact-planner.js';
|
|
10
|
+
import { renderSchemaSkeleton } from '../handoff-artifacts/schema-skeleton.js';
|
|
10
11
|
const DIGEST = /^sha256:[a-f0-9]{64}$/u;
|
|
11
12
|
const SPEC_ID = /^SPEC-[1-9]\d*$/u;
|
|
12
13
|
const ISSUER_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
@@ -33,7 +34,7 @@ const ProvenanceSchema = z
|
|
|
33
34
|
issuerId: z.string().regex(ISSUER_ID),
|
|
34
35
|
})
|
|
35
36
|
.strict();
|
|
36
|
-
const DiscoverySchema = z
|
|
37
|
+
export const DiscoverySchema = z
|
|
37
38
|
.object({
|
|
38
39
|
version: z.literal(1),
|
|
39
40
|
provenance: ProvenanceSchema.optional(),
|
|
@@ -56,6 +57,9 @@ const DiscoverySchema = z
|
|
|
56
57
|
.max(200),
|
|
57
58
|
})
|
|
58
59
|
.strict();
|
|
60
|
+
export const DISCOVERY_TEMPLATE_OVERRIDES = {
|
|
61
|
+
'openQuestions[].status': 'resolved',
|
|
62
|
+
};
|
|
59
63
|
const TaskPlanSchema = z
|
|
60
64
|
.object({
|
|
61
65
|
version: z.literal(1),
|
|
@@ -244,56 +248,6 @@ const ARTIFACT_HINTS = {
|
|
|
244
248
|
function artifactHint(label) {
|
|
245
249
|
return ARTIFACT_HINTS[label] ?? '';
|
|
246
250
|
}
|
|
247
|
-
/**
|
|
248
|
-
* SPEC-1356 / AC5: compact JSON literal per artifact kind, appended verbatim to
|
|
249
|
-
* schema-validation errors so a retrying caller sees the exact expected shape
|
|
250
|
-
* instead of re-deriving it from prose.
|
|
251
|
-
*/
|
|
252
|
-
const ARTIFACT_EXAMPLES = {
|
|
253
|
-
'Discovery evidence': JSON.stringify({
|
|
254
|
-
version: 1,
|
|
255
|
-
rules: ['Rule text'],
|
|
256
|
-
examples: [{ rule: 'Rule text', example: 'Example text' }],
|
|
257
|
-
openQuestions: [{ question: 'Open question?', status: 'open' }],
|
|
258
|
-
outOfScope: ['Not in scope item'],
|
|
259
|
-
glossary: [{ term: 'Term', meaning: 'Meaning' }],
|
|
260
|
-
}),
|
|
261
|
-
'Task plan evidence': JSON.stringify({
|
|
262
|
-
version: 1,
|
|
263
|
-
tasks: [{ id: 'T1', title: 'Task title', acceptanceCriteria: ['AC1'], status: 'pending' }],
|
|
264
|
-
}),
|
|
265
|
-
'Traceability matrix evidence': JSON.stringify({
|
|
266
|
-
version: 1,
|
|
267
|
-
rows: [
|
|
268
|
-
{
|
|
269
|
-
acceptanceCriterion: 'AC text',
|
|
270
|
-
testEvidence: ['tests/example.test.ts'],
|
|
271
|
-
changedFiles: ['src/example.ts'],
|
|
272
|
-
validationEvidence: 'validate passed with score 100',
|
|
273
|
-
reviewerEvidence: 'planu-implementation-reviewer approved',
|
|
274
|
-
},
|
|
275
|
-
],
|
|
276
|
-
}),
|
|
277
|
-
// Contract kinds are derived from the artifact filenames so no
|
|
278
|
-
// configurable technology literal is hardcoded here (audit:hardcodes).
|
|
279
|
-
...Object.fromEntries([
|
|
280
|
-
'contract-validation-api.json',
|
|
281
|
-
'contract-validation-graphql.json',
|
|
282
|
-
'contract-validation-event.json',
|
|
283
|
-
'contract-validation-ui.json',
|
|
284
|
-
'contract-validation-mcp.json',
|
|
285
|
-
].map((label) => [
|
|
286
|
-
label,
|
|
287
|
-
JSON.stringify({
|
|
288
|
-
version: 1,
|
|
289
|
-
kind: label.slice('contract-validation-'.length, -'.json'.length),
|
|
290
|
-
passed: true,
|
|
291
|
-
}),
|
|
292
|
-
])),
|
|
293
|
-
};
|
|
294
|
-
function artifactExample(label) {
|
|
295
|
-
return ARTIFACT_EXAMPLES[label] ?? '';
|
|
296
|
-
}
|
|
297
251
|
/** SPEC-1356: exported so evidence-autofill (and other producers) target the same path convention. */
|
|
298
252
|
export function handoffEvidencePath(projectId, specId, filename) {
|
|
299
253
|
return join(projectDataDir(projectId), 'handoffs', specId, filename);
|
|
@@ -329,7 +283,9 @@ async function readOptional(args) {
|
|
|
329
283
|
const allIssues = parsed.error.issues
|
|
330
284
|
.map((issue) => `${issue.path.length > 0 ? issue.path.join('.') : '(root)'}: ${issue.message}`)
|
|
331
285
|
.join('; ');
|
|
332
|
-
const example =
|
|
286
|
+
const example = args.template === false
|
|
287
|
+
? ''
|
|
288
|
+
: renderSchemaSkeleton(args.schema, { overrides: args.templateOverrides });
|
|
333
289
|
args.invalidArtifacts.push(`${args.label} is invalid at ${found.path}: ${allIssues} ${artifactHint(args.label)}${example ? ` Expected shape example: ${example}` : ''}`.trim());
|
|
334
290
|
return undefined;
|
|
335
291
|
}
|
|
@@ -340,6 +296,29 @@ async function readOptional(args) {
|
|
|
340
296
|
return undefined;
|
|
341
297
|
}
|
|
342
298
|
}
|
|
299
|
+
const CONTRACT_VALIDATION_KIND_BY_FILENAME = {
|
|
300
|
+
'contract-validation-api.json': 'api',
|
|
301
|
+
'contract-validation-graphql.json': technologyValue('technology-graphql-aed31c'),
|
|
302
|
+
'contract-validation-event.json': 'event',
|
|
303
|
+
'contract-validation-ui.json': 'ui',
|
|
304
|
+
'contract-validation-mcp.json': 'mcp',
|
|
305
|
+
};
|
|
306
|
+
async function readContractValidations(args, invalidArtifacts, contractProvenance) {
|
|
307
|
+
const contractValidations = [];
|
|
308
|
+
for (const filename of Object.keys(CONTRACT_VALIDATION_KIND_BY_FILENAME)) {
|
|
309
|
+
const evidence = await readOptional({
|
|
310
|
+
label: filename,
|
|
311
|
+
invalidArtifacts,
|
|
312
|
+
schema: ContractValidationSchema,
|
|
313
|
+
paths: [handoffEvidencePath(args.projectId, args.specId, filename)],
|
|
314
|
+
templateOverrides: { kind: CONTRACT_VALIDATION_KIND_BY_FILENAME[filename] },
|
|
315
|
+
});
|
|
316
|
+
if (evidence) {
|
|
317
|
+
contractValidations.push(contractProvenance ? { ...evidence, provenance: contractProvenance } : evidence);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return contractValidations;
|
|
321
|
+
}
|
|
343
322
|
export async function readEvidenceArtifacts(args) {
|
|
344
323
|
const invalidArtifacts = [];
|
|
345
324
|
const discovery = await readOptional({
|
|
@@ -347,6 +326,7 @@ export async function readEvidenceArtifacts(args) {
|
|
|
347
326
|
invalidArtifacts,
|
|
348
327
|
schema: DiscoverySchema,
|
|
349
328
|
paths: [handoffEvidencePath(args.projectId, args.specId, 'discovery.json')],
|
|
329
|
+
templateOverrides: DISCOVERY_TEMPLATE_OVERRIDES,
|
|
350
330
|
});
|
|
351
331
|
const taskPlan = await readOptional({
|
|
352
332
|
label: 'Task plan evidence',
|
|
@@ -359,6 +339,11 @@ export async function readEvidenceArtifacts(args) {
|
|
|
359
339
|
invalidArtifacts,
|
|
360
340
|
schema: TraceabilityMatrixSchema,
|
|
361
341
|
paths: [handoffEvidencePath(args.projectId, args.specId, 'traceability-matrix.json')],
|
|
342
|
+
templateOverrides: {
|
|
343
|
+
'rows[].testEvidence': ['example'],
|
|
344
|
+
'rows[].validationEvidence': 'example',
|
|
345
|
+
'rows[].reviewerEvidence': 'example',
|
|
346
|
+
},
|
|
362
347
|
});
|
|
363
348
|
const [discoveryProvenance, taskPlanProvenance, traceabilityProvenance, contractProvenance] = await Promise.all([
|
|
364
349
|
'discovery.provenance.json',
|
|
@@ -370,25 +355,9 @@ export async function readEvidenceArtifacts(args) {
|
|
|
370
355
|
invalidArtifacts,
|
|
371
356
|
schema: ProvenanceSchema,
|
|
372
357
|
paths: [handoffEvidencePath(args.projectId, args.specId, filename)],
|
|
358
|
+
template: false,
|
|
373
359
|
})));
|
|
374
|
-
const contractValidations =
|
|
375
|
-
for (const filename of [
|
|
376
|
-
'contract-validation-api.json',
|
|
377
|
-
'contract-validation-graphql.json',
|
|
378
|
-
'contract-validation-event.json',
|
|
379
|
-
'contract-validation-ui.json',
|
|
380
|
-
'contract-validation-mcp.json',
|
|
381
|
-
]) {
|
|
382
|
-
const evidence = await readOptional({
|
|
383
|
-
label: filename,
|
|
384
|
-
invalidArtifacts,
|
|
385
|
-
schema: ContractValidationSchema,
|
|
386
|
-
paths: [handoffEvidencePath(args.projectId, args.specId, filename)],
|
|
387
|
-
});
|
|
388
|
-
if (evidence) {
|
|
389
|
-
contractValidations.push(contractProvenance ? { ...evidence, provenance: contractProvenance } : evidence);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
360
|
+
const contractValidations = await readContractValidations(args, invalidArtifacts, contractProvenance);
|
|
392
361
|
return {
|
|
393
362
|
discovery: discovery && discoveryProvenance
|
|
394
363
|
? { ...discovery, provenance: discoveryProvenance }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAbsolute, join } from 'node:path';
|
|
2
|
-
import { evidenceArtifactCollectionDigest, evidenceArtifactDigest } from './artifact-reader.js';
|
|
2
|
+
import { DISCOVERY_TEMPLATE_OVERRIDES, DiscoverySchema, evidenceArtifactCollectionDigest, evidenceArtifactDigest, } from './artifact-reader.js';
|
|
3
|
+
import { renderSchemaSkeleton } from '../handoff-artifacts/schema-skeleton.js';
|
|
3
4
|
import { hasAnyAffirmedMatch, stripMetaAnalysisText, stripNonContractText, } from '../text-signal-boundaries.js';
|
|
4
5
|
import { createCriterionIdentity } from '../criterion-identity.js';
|
|
5
6
|
import { computeDurableValidationBindings, doneReceiptBindingsDifferOnlyInEvidence, toValidationReceiptBindings, writeValidationArtifactProvenance, } from '../validation/durable-validation.js';
|
|
@@ -140,6 +141,9 @@ export function inferRequiredContractEvidence(spec, criteria) {
|
|
|
140
141
|
}
|
|
141
142
|
return required;
|
|
142
143
|
}
|
|
144
|
+
const DISCOVERY_TEMPLATE = renderSchemaSkeleton(DiscoverySchema, {
|
|
145
|
+
overrides: DISCOVERY_TEMPLATE_OVERRIDES,
|
|
146
|
+
});
|
|
143
147
|
export function checkDiscoveryGate(spec, artifacts) {
|
|
144
148
|
if (!isNonTrivial(spec)) {
|
|
145
149
|
return [];
|
|
@@ -148,7 +152,8 @@ export function checkDiscoveryGate(spec, artifacts) {
|
|
|
148
152
|
if (!artifacts.discovery) {
|
|
149
153
|
issues.push({
|
|
150
154
|
code: 'discovery_missing',
|
|
151
|
-
message: 'Discovery evidence is required before approval. Add discovery.json with rules, examples, open questions, out-of-scope boundaries, and glossary.'
|
|
155
|
+
message: 'Discovery evidence is required before approval. Add discovery.json with rules, examples, open questions, out-of-scope boundaries, and glossary.' +
|
|
156
|
+
(DISCOVERY_TEMPLATE ? ` Expected shape example: ${DISCOVERY_TEMPLATE}` : ''),
|
|
152
157
|
});
|
|
153
158
|
return issues;
|
|
154
159
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const MAX_TEMPLATE_LENGTH = 600;
|
|
2
|
+
const OMIT_FIELD = Symbol('omit-field');
|
|
3
|
+
class UnsupportedSchemaNodeError extends Error {
|
|
4
|
+
}
|
|
5
|
+
function runtimeDef(schema) {
|
|
6
|
+
const zodInternals = schema?._zod;
|
|
7
|
+
const def = zodInternals?.def;
|
|
8
|
+
if (!def || typeof def.type !== 'string') {
|
|
9
|
+
throw new UnsupportedSchemaNodeError();
|
|
10
|
+
}
|
|
11
|
+
return def;
|
|
12
|
+
}
|
|
13
|
+
function buildObjectValue(def) {
|
|
14
|
+
const shape = (def.shape ?? {});
|
|
15
|
+
const result = {};
|
|
16
|
+
for (const [key, propSchema] of Object.entries(shape)) {
|
|
17
|
+
const value = buildNodeValue(propSchema);
|
|
18
|
+
if (value !== OMIT_FIELD) {
|
|
19
|
+
result[key] = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
function buildNodeValue(schema) {
|
|
25
|
+
const def = runtimeDef(schema);
|
|
26
|
+
switch (def.type) {
|
|
27
|
+
case 'object':
|
|
28
|
+
return buildObjectValue(def);
|
|
29
|
+
case 'array': {
|
|
30
|
+
const element = buildNodeValue(def.element);
|
|
31
|
+
if (element === OMIT_FIELD) {
|
|
32
|
+
throw new UnsupportedSchemaNodeError();
|
|
33
|
+
}
|
|
34
|
+
return [element];
|
|
35
|
+
}
|
|
36
|
+
case 'enum': {
|
|
37
|
+
const entries = def.entries;
|
|
38
|
+
const firstOption = entries ? Object.values(entries)[0] : undefined;
|
|
39
|
+
if (firstOption === undefined) {
|
|
40
|
+
throw new UnsupportedSchemaNodeError();
|
|
41
|
+
}
|
|
42
|
+
return firstOption;
|
|
43
|
+
}
|
|
44
|
+
case 'literal': {
|
|
45
|
+
const values = def.values;
|
|
46
|
+
const firstValue = values?.[0];
|
|
47
|
+
if (firstValue === undefined) {
|
|
48
|
+
throw new UnsupportedSchemaNodeError();
|
|
49
|
+
}
|
|
50
|
+
return firstValue;
|
|
51
|
+
}
|
|
52
|
+
case 'string':
|
|
53
|
+
return 'example';
|
|
54
|
+
case 'number':
|
|
55
|
+
return 1;
|
|
56
|
+
case 'boolean':
|
|
57
|
+
return true;
|
|
58
|
+
case 'optional':
|
|
59
|
+
return OMIT_FIELD;
|
|
60
|
+
default:
|
|
61
|
+
throw new UnsupportedSchemaNodeError();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function isPlainObject(value) {
|
|
65
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
function applyOverrideAtPath(node, segments, value) {
|
|
68
|
+
const [head, ...rest] = segments;
|
|
69
|
+
if (head === undefined) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
const arrayMatch = /^(.+)\[\]$/u.exec(head);
|
|
73
|
+
if (arrayMatch) {
|
|
74
|
+
const [, key] = arrayMatch;
|
|
75
|
+
if (key === undefined || !isPlainObject(node) || !Array.isArray(node[key])) {
|
|
76
|
+
return node;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...node,
|
|
80
|
+
[key]: node[key].map((element) => applyOverrideAtPath(element, rest, value)),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (!isPlainObject(node)) {
|
|
84
|
+
return node;
|
|
85
|
+
}
|
|
86
|
+
return { ...node, [head]: applyOverrideAtPath(node[head], rest, value) };
|
|
87
|
+
}
|
|
88
|
+
function applyOverrides(skeleton, overrides) {
|
|
89
|
+
return Object.entries(overrides).reduce((node, [path, value]) => applyOverrideAtPath(node, path.split('.'), value), skeleton);
|
|
90
|
+
}
|
|
91
|
+
export function renderSchemaSkeleton(schema, options = {}) {
|
|
92
|
+
try {
|
|
93
|
+
const skeleton = buildNodeValue(schema);
|
|
94
|
+
if (skeleton === OMIT_FIELD) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
const withOverrides = applyOverrides(skeleton, options.overrides ?? {});
|
|
98
|
+
const json = JSON.stringify(withOverrides);
|
|
99
|
+
return json.length <= MAX_TEMPLATE_LENGTH ? json : '';
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (err instanceof UnsupportedSchemaNodeError) {
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=schema-skeleton.js.map
|
|
@@ -4,6 +4,9 @@ import type { EscalateResult } from '../../types/escalator.js';
|
|
|
4
4
|
import type { ValidationFreshnessLease } from '../../engine/validation/validation-freshness.js';
|
|
5
5
|
import { type DoneReceiptVerifier } from './done-receipt-verifier.js';
|
|
6
6
|
export { completedJobPublishesReceipt } from './done-receipt-verifier.js';
|
|
7
|
+
export declare const reviewFeedbackTemplate: () => string;
|
|
8
|
+
export declare const implementationReviewTemplate: () => string;
|
|
9
|
+
export declare const validationReportTemplate: () => string;
|
|
7
10
|
/**
|
|
8
11
|
* The reason why the validate gate blocked a `done` transition.
|
|
9
12
|
* Each reason maps to a specific fix-hint shown to the LLM.
|
|
@@ -18,8 +18,40 @@ import { reportClassifiedDegradation } from '../../errors/classified-degradation
|
|
|
18
18
|
import { verifyCurrentDoneReceipt } from './done-receipt-verifier.js';
|
|
19
19
|
import { projectDataDir } from '../../storage/base-store.js';
|
|
20
20
|
import { readTransitionLog } from '../../storage/transition-log.js';
|
|
21
|
-
import { ValidationReportV1Schema } from '../../engine/handoff-artifacts/schemas.js';
|
|
21
|
+
import { ImplementationReviewV1Schema, ReviewFeedbackV1Schema, ValidationReportV1Schema, } from '../../engine/handoff-artifacts/schemas.js';
|
|
22
|
+
import { renderSchemaSkeleton } from '../../engine/handoff-artifacts/schema-skeleton.js';
|
|
22
23
|
export { completedJobPublishesReceipt } from './done-receipt-verifier.js';
|
|
24
|
+
const HANDOFF_ARTIFACT_TEMPLATE_TIMESTAMP = '2026-01-01T00:00:00.000Z';
|
|
25
|
+
const HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION = '1.0.0';
|
|
26
|
+
function lazyTemplate(render) {
|
|
27
|
+
let cached;
|
|
28
|
+
return () => {
|
|
29
|
+
cached ??= render();
|
|
30
|
+
return cached;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export const reviewFeedbackTemplate = lazyTemplate(() => renderSchemaSkeleton(ReviewFeedbackV1Schema, {
|
|
34
|
+
overrides: {
|
|
35
|
+
schema_version: HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION,
|
|
36
|
+
reviewedAt: HANDOFF_ARTIFACT_TEMPLATE_TIMESTAMP,
|
|
37
|
+
'reviewer.agent': 'planu-spec-reviewer',
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
export const implementationReviewTemplate = lazyTemplate(() => renderSchemaSkeleton(ImplementationReviewV1Schema, {
|
|
41
|
+
overrides: {
|
|
42
|
+
schema_version: HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION,
|
|
43
|
+
reviewedAt: HANDOFF_ARTIFACT_TEMPLATE_TIMESTAMP,
|
|
44
|
+
'reviewer.agent': 'planu-implementation-reviewer',
|
|
45
|
+
},
|
|
46
|
+
}));
|
|
47
|
+
export const validationReportTemplate = lazyTemplate(() => renderSchemaSkeleton(ValidationReportV1Schema, {
|
|
48
|
+
overrides: {
|
|
49
|
+
schema_version: HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION,
|
|
50
|
+
completedAt: HANDOFF_ARTIFACT_TEMPLATE_TIMESTAMP,
|
|
51
|
+
'reviewer.agent': AUTOMATED_VALIDATOR_AGENT,
|
|
52
|
+
'reviewer.kind': 'automation',
|
|
53
|
+
},
|
|
54
|
+
}));
|
|
23
55
|
/**
|
|
24
56
|
* SPEC-721 / SPEC-222 Trigger 1: Run validate engine before marking done.
|
|
25
57
|
*
|
|
@@ -482,6 +514,7 @@ export async function readApprovedValidationReportGate(specId, projectId, force)
|
|
|
482
514
|
message: 'No validation-report artifact exists for this spec. Run validate to generate implementation-review evidence before marking done.',
|
|
483
515
|
gates: [],
|
|
484
516
|
fixHint: 'Run validate for this spec, fix any failing gates, then retry update_status(done). Use force:true only with an audited reason.',
|
|
517
|
+
template: validationReportTemplate(),
|
|
485
518
|
}),
|
|
486
519
|
};
|
|
487
520
|
}
|
|
@@ -493,6 +526,7 @@ export async function readApprovedValidationReportGate(specId, projectId, force)
|
|
|
493
526
|
message: 'The validation-report artifact is malformed or uses an obsolete schema. Re-run validate so Planu can generate reviewer evidence.',
|
|
494
527
|
gates: [],
|
|
495
528
|
fixHint: 'Re-run validate for this spec. The report must include reviewer evidence and passing gates.',
|
|
529
|
+
template: validationReportTemplate(),
|
|
496
530
|
}),
|
|
497
531
|
};
|
|
498
532
|
}
|
|
@@ -572,6 +606,7 @@ export async function checkSpecReviewGate(specId, projectId, _forceApprove) {
|
|
|
572
606
|
fixHint: firstErr?.code === 'ARTIFACT_NOT_FOUND'
|
|
573
607
|
? 'Have planu-spec-reviewer write review_feedback.md (ReviewFeedbackV1) into the handoff store, then retry update_status(approved).'
|
|
574
608
|
: 'Have planu-spec-reviewer rewrite review_feedback.md as valid ReviewFeedbackV1 evidence, then retry approval.',
|
|
609
|
+
template: reviewFeedbackTemplate(),
|
|
575
610
|
});
|
|
576
611
|
}
|
|
577
612
|
const review = result.payload;
|
|
@@ -635,6 +670,7 @@ export async function checkImplementationReviewGate(specId, projectId, _force) {
|
|
|
635
670
|
fixHint: firstErr?.code === 'ARTIFACT_NOT_FOUND'
|
|
636
671
|
? 'Have planu-implementation-reviewer write implementation_review.json (ImplementationReviewV1) into the handoff store, then retry update_status(done).'
|
|
637
672
|
: 'Have planu-implementation-reviewer rewrite implementation_review.json as valid ImplementationReviewV1 evidence, then retry done.',
|
|
673
|
+
template: implementationReviewTemplate(),
|
|
638
674
|
});
|
|
639
675
|
}
|
|
640
676
|
const review = result.payload;
|
|
@@ -724,6 +760,7 @@ function implementationReviewGateError(args) {
|
|
|
724
760
|
code: 422,
|
|
725
761
|
context: { specId: args.specId, artifactPath, blockers: args.blockers },
|
|
726
762
|
fixHint: args.fixHint,
|
|
763
|
+
...(args.template ? { template: args.template } : {}),
|
|
727
764
|
},
|
|
728
765
|
};
|
|
729
766
|
}
|
|
@@ -749,6 +786,7 @@ function validationReportGateError(args) {
|
|
|
749
786
|
code: 422,
|
|
750
787
|
context: { specId: args.specId, artifactPath, gates: args.gates },
|
|
751
788
|
fixHint: args.fixHint,
|
|
789
|
+
...(args.template ? { template: args.template } : {}),
|
|
752
790
|
},
|
|
753
791
|
};
|
|
754
792
|
}
|
|
@@ -774,6 +812,7 @@ function specReviewGateError(args) {
|
|
|
774
812
|
code: 422,
|
|
775
813
|
context: { specId: args.specId, artifactPath, blockers: args.blockers },
|
|
776
814
|
fixHint: args.fixHint,
|
|
815
|
+
...(args.template ? { template: args.template } : {}),
|
|
777
816
|
},
|
|
778
817
|
};
|
|
779
818
|
}
|
package/package.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "5.3.
|
|
5
|
+
"version": "5.3.51",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|