@planu/cli 5.0.0 → 5.1.1

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/dist/cli/commands/spec.js +10 -1
  3. package/dist/core/spec-validator.js +32 -18
  4. package/dist/engine/evidence-gates/artifact-reader.d.ts +2 -0
  5. package/dist/engine/evidence-gates/artifact-reader.js +59 -2
  6. package/dist/engine/evidence-gates/evidence-autofill.d.ts +10 -0
  7. package/dist/engine/evidence-gates/evidence-autofill.js +148 -0
  8. package/dist/engine/evidence-gates/evidence-skeletons.d.ts +19 -0
  9. package/dist/engine/evidence-gates/evidence-skeletons.js +69 -0
  10. package/dist/engine/execution/operation-journal.js +10 -4
  11. package/dist/engine/minimality/policy-loader.js +247 -6
  12. package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
  13. package/dist/engine/planu-core.darwin-arm64.node.sbom.json +14 -14
  14. package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
  15. package/dist/engine/planu-core.darwin-x64.node.sbom.json +14 -14
  16. package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
  17. package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +14 -14
  18. package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
  19. package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +14 -14
  20. package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
  21. package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +14 -14
  22. package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
  23. package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +14 -14
  24. package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
  25. package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +14 -14
  26. package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
  27. package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +14 -14
  28. package/dist/engine/reverse-engineer/api-detector.js +2 -13
  29. package/dist/engine/reverse-engineer/complexity-analyzer.js +2 -13
  30. package/dist/engine/reverse-engineer/config-analyzer.js +2 -13
  31. package/dist/engine/reverse-engineer/dependency-graph.js +2 -13
  32. package/dist/engine/reverse-engineer/test-analyzer.js +2 -13
  33. package/dist/engine/reverse-engineer/walk-ignore.d.ts +3 -0
  34. package/dist/engine/reverse-engineer/walk-ignore.js +26 -0
  35. package/dist/engine/spec-format/acceptance-criteria.js +13 -12
  36. package/dist/engine/spec-format/text-fences.js +20 -2
  37. package/dist/engine/spec-state-syncer.js +1 -1
  38. package/dist/engine/timing/budget.js +5 -1
  39. package/dist/server/routes/specs.js +7 -5
  40. package/dist/tools/challenge-spec-helpers.d.ts +10 -1
  41. package/dist/tools/challenge-spec-helpers.js +63 -22
  42. package/dist/tools/challenge-spec.js +18 -3
  43. package/dist/tools/check-readiness.js +37 -13
  44. package/dist/tools/create-spec/spec-builder.d.ts +7 -0
  45. package/dist/tools/create-spec/spec-builder.js +19 -4
  46. package/dist/tools/create-spec.js +156 -86
  47. package/dist/tools/register-spec-tools/core-spec-tools.js +13 -12
  48. package/dist/tools/sync-spec-state-handler.js +49 -1
  49. package/dist/tools/update-status/batch.d.ts +6 -2
  50. package/dist/tools/update-status/batch.js +58 -1
  51. package/dist/tools/update-status/dod-gates.d.ts +16 -1
  52. package/dist/tools/update-status/dod-gates.js +191 -1
  53. package/dist/tools/update-status/done-receipt-verifier.d.ts +8 -0
  54. package/dist/tools/update-status/done-receipt-verifier.js +37 -2
  55. package/dist/tools/update-status/evidence-gate.d.ts +4 -0
  56. package/dist/tools/update-status/evidence-gate.js +67 -2
  57. package/dist/tools/update-status/file-sync.d.ts +2 -2
  58. package/dist/tools/update-status/index.d.ts +23 -1
  59. package/dist/tools/update-status/index.js +201 -24
  60. package/dist/tools/update-status/transition-guard.js +13 -1
  61. package/dist/tools/workspace-dashboard-handler.js +38 -0
  62. package/dist/types/evidence-autofill.d.ts +34 -0
  63. package/dist/types/evidence-autofill.js +2 -0
  64. package/dist/types/index.d.ts +1 -0
  65. package/dist/types/index.js +1 -0
  66. package/dist/types/spec/core.d.ts +6 -0
  67. package/dist/types/spec/inputs.d.ts +7 -2
  68. package/dist/types/spec-format.d.ts +1 -1
  69. package/dist/types/transition-log.d.ts +1 -1
  70. package/dist/types/validation.d.ts +8 -2
  71. package/package.json +11 -10
  72. package/planu-native.json +1 -1
  73. package/planu-plugin.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,51 @@
1
+ ## [5.1.1] - 2026-08-04
2
+
3
+ ### Bug Fixes
4
+ - fix(release-harness): resolve published artifact from staged repack when publish has no tarball argv
5
+ - fix(release): stop CLI-guard stdout pollution, npm view array probe, and tarball-path publish leak
6
+
7
+
8
+ ## [5.1.0] - 2026-08-04
9
+
10
+ ### Features
11
+ - feat(lifecycle): automate done evidence pipeline end to end
12
+
13
+ ### Bug Fixes
14
+ - fix(update-status): make typed failure explicit at dod-gates catch sites
15
+ - fix(deps): patch hono, fast-uri, ip-address, and undici advisories via overrides
16
+ - fix(evidence-gates): derive contract example kinds from filenames
17
+ - fix(reverse-engineer): share walk-ignore list and add fast release-gate test lane
18
+ - fix(challenge): suppress ungrounded concurrency boilerplate in challenge_spec
19
+ - fix(update-status): report per-spec failure reasons in batch results
20
+ - fix(create-spec): guarantee spec.md write before reporting persisted
21
+ - fix(create-spec): release idempotency claim when create_spec fails before commit
22
+ - fix(release): accept npm 12 pack metadata
23
+ - fix(deps): patch transitive security advisories
24
+ - fix(privacy): redact minimality policy locators
25
+ - fix(lifecycle): route review evidence by target
26
+ - fix(readiness): unify canonical validation evidence
27
+
28
+ ### Chores
29
+ - chore(pnpm): disable modules purge confirmation for non-TTY automation
30
+ - chore(planu): session checkpoint before release
31
+ - chore(planu): close SPEC-1350 lifecycle state
32
+ - chore(planu): hand off freshness blocker
33
+ - chore(planu): checkpoint release handoff
34
+ - chore(planu): preserve native engine review state
35
+ - chore(planu): persist delayed challenge evidence
36
+ - chore(planu): recover delayed audit specs
37
+ - chore(planu): capture lifecycle dogfood failures
38
+ - chore(planu): persist release remediation handoffs
39
+ - chore(planu): approve final release blockers
40
+ - chore(planu): start dependency security remediation
41
+ - chore(planu): approve final security remediation
42
+ - chore(planu): checkpoint final release remediations
43
+ - chore(planu): start lifecycle routing implementation
44
+ - chore(planu): approve lifecycle routing remediation
45
+ - chore(planu): checkpoint release remediation specs
46
+ - chore(planu): track v5 release dogfood regressions
47
+
48
+
1
49
  ## [5.0.0] - 2026-07-31
2
50
 
3
51
  ### Breaking Changes
@@ -4508,4 +4556,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
4508
4556
  - Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
4509
4557
  - Multi-language i18n (EN/ES/PT) for generated specs
4510
4558
  - Clean Architecture (hexagonal) — engine, tools, storage, types layers
4511
- - 10,857 tests with ≥95% coverage
4559
+ - 10,857 tests with ≥95% coverage
@@ -30,13 +30,14 @@ function printSpecSubcommandHelp() {
30
30
  ` ${'list'.padEnd(10)} ${dim('List specs [--status draft|approved|done...]')}`,
31
31
  ` ${'show'.padEnd(10)} ${dim('Show spec details (SPEC-NNN)')}`,
32
32
  ` ${'status'.padEnd(10)} ${dim('Update spec status (SPEC-NNN <status>)')}`,
33
+ ` ${''.padEnd(10)} ${dim('Batch targets: draft|review|approved|implementing|discarded')}`,
33
34
  ` ${'validate'.padEnd(10)} ${dim('Validate a spec against its codebase')}`,
34
35
  '',
35
36
  cyan('Examples:'),
36
37
  ` planu spec create "Add login flow"`,
37
38
  ` planu spec list --status approved`,
38
39
  ` planu spec show SPEC-001`,
39
- ` planu spec status SPEC-001 done`,
40
+ ` planu spec status SPEC-001 done --implementation-review-digest sha256:<64-hex>`,
40
41
  ` planu spec validate SPEC-001`,
41
42
  ];
42
43
  process.stdout.write(lines.join('\n') + '\n');
@@ -207,6 +208,7 @@ async function runShow(args, flags) {
207
208
  // ---------------------------------------------------------------------------
208
209
  // Subcommand: spec status
209
210
  // ---------------------------------------------------------------------------
211
+ // eslint-disable-next-line complexity -- batch done requires an explicit pre-resolution fail-closed branch
210
212
  async function runStatus(args, flags) {
211
213
  const { positionals, values } = parseArgs({
212
214
  args,
@@ -215,6 +217,7 @@ async function runStatus(args, flags) {
215
217
  notes: { type: 'string', short: 'n' },
216
218
  batch: { type: 'boolean' },
217
219
  set: { type: 'string', short: 's' },
220
+ 'implementation-review-digest': { type: 'string' },
218
221
  },
219
222
  strict: false,
220
223
  allowPositionals: true,
@@ -232,6 +235,11 @@ async function runStatus(args, flags) {
232
235
  process.exitCode = 1;
233
236
  return;
234
237
  }
238
+ if (batch && status === 'done') {
239
+ process.stderr.write(`${red('BATCH_DONE_UNSUPPORTED:')} Close specs individually with update_status(done) and full implementation review evidence.\n`);
240
+ process.exitCode = 1;
241
+ return;
242
+ }
235
243
  const projectId = values['project-id'] ?? detectProjectId();
236
244
  if (batch) {
237
245
  const result = await handleUpdateStatusBatch({
@@ -253,6 +261,7 @@ async function runStatus(args, flags) {
253
261
  projectId,
254
262
  status: status,
255
263
  reviewNotes: values.notes ?? undefined,
264
+ implementationReviewDigest: values['implementation-review-digest'] ?? undefined,
256
265
  });
257
266
  if (result.isError) {
258
267
  process.stderr.write(`${red(formatToolResult(result, flags))}\n`);
@@ -6,6 +6,8 @@ import { readFile } from 'node:fs/promises';
6
6
  import { checkSpecReadiness } from '../engine/readiness-checker.js';
7
7
  import { scoreSpecQuality } from '../engine/spec-quality-scorer.js';
8
8
  import { parseFrontmatter, stripFrontmatter } from '../engine/frontmatter-parser.js';
9
+ import { extractNormalizedAcceptanceCriteria } from '../engine/spec-format/acceptance-criteria.js';
10
+ import { parseFrontmatterScenarios } from '../engine/validator/spec-compliance-runner.js';
9
11
  // ── Required frontmatter fields ──────────────────────────────────────────────
10
12
  const REQUIRED_FRONTMATTER_FIELDS = ['id', 'title', 'status', 'type', 'target', 'scope'];
11
13
  // ── Required body sections (SPEC-785 aligned with lean-spec-generator output) ─
@@ -133,6 +135,20 @@ function mapQualityToValidation(dimensions) {
133
135
  }
134
136
  return warnings;
135
137
  }
138
+ function countCanonicalBddScenarios(raw) {
139
+ const criteria = extractNormalizedAcceptanceCriteria(raw, { allowLegacyBodyFallback: false });
140
+ const bodyCriteria = criteria.filter((criterion) => criterion.source === 'body');
141
+ const bddCandidates = bodyCriteria.length > 0
142
+ ? bodyCriteria.map((criterion) => criterion.text)
143
+ : parseFrontmatterScenarios(raw).map((scenario) => scenario.steps?.map((step) => `${step.keyword} ${step.text}`).join(' ') ??
144
+ scenario.title);
145
+ return {
146
+ criteriaCount: criteria.length,
147
+ bddScenarioCount: bddCandidates.filter((criterion) => /\b(?:given|dado)\b/i.test(criterion) &&
148
+ /\b(?:when|cuando)\b/i.test(criterion) &&
149
+ /\b(?:then|entonces)\b/i.test(criterion)).length,
150
+ };
151
+ }
136
152
  // ── Public API ───────────────────────────────────────────────────────────────
137
153
  /**
138
154
  * Validate a spec's format against the unified spec.md schema (SPEC-630).
@@ -168,22 +184,16 @@ export async function validateSpecFormat(spec, opts) {
168
184
  allWarnings.push(...unifiedResult.warnings);
169
185
  // ── 4. Readiness check ───────────────────────────────────────────────────
170
186
  let readinessScore = 0;
171
- let bddScenarioCount = 0;
172
- let criteriaCount = 0;
187
+ let readinessReport;
188
+ // The canonical metrics are independent from the readiness checker. Keep
189
+ // them truthful even when that best-effort checker throws.
190
+ const { criteriaCount, bddScenarioCount } = countCanonicalBddScenarios(raw);
173
191
  try {
174
192
  // Cast to Spec since checkSpecReadiness needs the full type but only reads
175
193
  // the fields available on the Pick type we receive.
176
- const report = await checkSpecReadiness(spec, readinessMode);
177
- readinessScore = report.score;
178
- // Count scenarios from frontmatter block
179
- bddScenarioCount = (raw.match(/^\s+-\s+title:/gm) ?? []).length;
180
- // Count criteria lines (checkbox or GIVEN/WHEN/THEN)
181
- const bodyLines = body.split('\n');
182
- criteriaCount = bodyLines.filter((l) => /^- \[[ x]\]/i.test(l.trim()) || /^- .*\bgiven\b.*\bwhen\b.*\bthen\b/i.test(l.trim())).length;
183
- if (criteriaCount === 0) {
184
- criteriaCount = bddScenarioCount;
185
- }
186
- const mapped = mapReadinessToValidation(report.issues.blockers, report.issues.warnings);
194
+ readinessReport = await checkSpecReadiness(spec, readinessMode);
195
+ readinessScore = readinessReport.score;
196
+ const mapped = mapReadinessToValidation(readinessReport.issues.blockers, readinessReport.issues.warnings);
187
197
  allErrors.push(...mapped.errors);
188
198
  allWarnings.push(...mapped.warnings);
189
199
  }
@@ -197,11 +207,12 @@ export async function validateSpecFormat(spec, opts) {
197
207
  return { valid, errors: allErrors, warnings: allWarnings };
198
208
  }
199
209
  // ── 5. Quality scoring ───────────────────────────────────────────────────
200
- let qualityScore = 0;
201
- let qualityGrade = 'F';
210
+ let qualityScore;
211
+ let qualityGrade;
212
+ let qualityReport;
202
213
  if (!skipQuality) {
203
214
  try {
204
- const qualityReport = await scoreSpecQuality(spec);
215
+ qualityReport = await scoreSpecQuality(spec);
205
216
  qualityScore = qualityReport.score.total;
206
217
  qualityGrade = qualityReport.score.grade;
207
218
  const qualityWarnings = mapQualityToValidation(qualityReport.dimensions);
@@ -226,11 +237,14 @@ export async function validateSpecFormat(spec, opts) {
226
237
  warnings: allWarnings,
227
238
  metrics: {
228
239
  readinessScore,
229
- qualityScore,
230
- qualityGrade,
231
240
  bddScenarioCount,
232
241
  criteriaCount,
242
+ ...(qualityScore === undefined || qualityGrade === undefined
243
+ ? {}
244
+ : { qualityScore, qualityGrade }),
233
245
  },
246
+ readinessReport,
247
+ qualityReport,
234
248
  };
235
249
  }
236
250
  //# sourceMappingURL=spec-validator.js.map
@@ -15,6 +15,8 @@ export declare function traceabilityRowHasCurrentCommandEvidence(row: Traceabili
15
15
  projectId: string;
16
16
  projectPath: string;
17
17
  }): boolean;
18
+ /** SPEC-1356: exported so evidence-autofill (and other producers) target the same path convention. */
19
+ export declare function handoffEvidencePath(projectId: string, specId: string, filename: string): string;
18
20
  export declare function readEvidenceArtifacts(args: {
19
21
  spec: Spec;
20
22
  projectId: string;
@@ -244,7 +244,58 @@ const ARTIFACT_HINTS = {
244
244
  function artifactHint(label) {
245
245
  return ARTIFACT_HINTS[label] ?? '';
246
246
  }
247
- function handoffEvidencePath(projectId, specId, filename) {
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
+ /** SPEC-1356: exported so evidence-autofill (and other producers) target the same path convention. */
298
+ export function handoffEvidencePath(projectId, specId, filename) {
248
299
  return join(projectDataDir(projectId), 'handoffs', specId, filename);
249
300
  }
250
301
  async function readUnknown(paths) {
@@ -273,7 +324,13 @@ async function readOptional(args) {
273
324
  }
274
325
  const parsed = args.schema.safeParse(found.value);
275
326
  if (!parsed.success) {
276
- args.invalidArtifacts.push(`${args.label} is invalid at ${found.path}: ${parsed.error.issues.map((issue) => issue.message).join('; ')} ${artifactHint(args.label)}`.trim());
327
+ // SPEC-1356 / AC5: surface every Zod issue (field path + message), not just the
328
+ // first one, so a single retry can fix every constraint violation at once.
329
+ const allIssues = parsed.error.issues
330
+ .map((issue) => `${issue.path.length > 0 ? issue.path.join('.') : '(root)'}: ${issue.message}`)
331
+ .join('; ');
332
+ const example = artifactExample(args.label);
333
+ args.invalidArtifacts.push(`${args.label} is invalid at ${found.path}: ${allIssues} ${artifactHint(args.label)}${example ? ` Expected shape example: ${example}` : ''}`.trim());
277
334
  return undefined;
278
335
  }
279
336
  return parsed.data;
@@ -0,0 +1,10 @@
1
+ import type { AutofillTraceabilityMatrixArgs, AutofillTraceabilityMatrixResult } from '../../types/evidence-autofill.js';
2
+ /**
3
+ * Generate `traceability-matrix.json` in the handoff evidence store when it does
4
+ * not already exist. Never overwrites an existing file — if one is present this
5
+ * is a no-op that reports `already-exists`. Tolerates every failure by returning
6
+ * a typed non-throwing result; callers must never let this crash a lifecycle
7
+ * transition.
8
+ */
9
+ export declare function autofillTraceabilityMatrix(args: AutofillTraceabilityMatrixArgs): Promise<AutofillTraceabilityMatrixResult>;
10
+ //# sourceMappingURL=evidence-autofill.d.ts.map
@@ -0,0 +1,148 @@
1
+ // engine/evidence-gates/evidence-autofill.ts — SPEC-1356: automated lifecycle evidence pipeline
2
+ //
3
+ // Generates traceability-matrix.json in the handoff evidence store when it is
4
+ // missing, so a `done` transition never fails purely because nobody hand-wrote
5
+ // the traceability artifact.
6
+ //
7
+ // ORDERING CAVEAT (documented per SPEC-1356 T-A3): writing traceability-matrix.json
8
+ // *after* `validate` has already computed and bound durable validation receipts
9
+ // would make those bindings stale relative to the newly-written evidence — the
10
+ // receipt would be certifying a repository state that does not include this file.
11
+ // This module does NOT attempt to re-run or re-bind validation. Instead, callers
12
+ // (see `src/tools/update-status/index.ts`) MUST treat a write from this module as
13
+ // "evidence created, not yet bound": when autofill actually writes the file, the
14
+ // caller must block the `done` transition with a message telling the operator to
15
+ // re-run `validate` once so the freshly-created evidence gets bound, then retry
16
+ // `done`. This is the simpler, safe choice given SPEC-1356 could not fully verify
17
+ // that skipping straight through to the done gates after autofill would keep
18
+ // receipt bindings correct in every code path (reconciliation, forced bypass,
19
+ // legacy harnesses, etc.).
20
+ import { execFile } from 'node:child_process';
21
+ import { existsSync } from 'node:fs';
22
+ import { isAbsolute, join, resolve } from 'node:path';
23
+ import { promisify } from 'node:util';
24
+ import { extractAcceptanceCriteriaTexts } from '../spec-format/acceptance-criteria.js';
25
+ import { parseFrontmatterScenarios } from '../validator/spec-compliance-runner.js';
26
+ import { extractCanonicalFileOwnership } from '../handoff-packager.js';
27
+ import { handoffEvidencePath } from './artifact-reader.js';
28
+ import { atomicWriteFile } from '../safety/atomic-write-file.js';
29
+ import { ValidationReportV1Schema } from '../handoff-artifacts/schemas.js';
30
+ import { projectDataDir } from '../../storage/base-store.js';
31
+ import { readFile } from 'node:fs/promises';
32
+ import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
33
+ const execFileAsync = promisify(execFile);
34
+ const defaultExec = async (command, args, options) => {
35
+ const { stdout } = await execFileAsync(command, args, { cwd: options.cwd, encoding: 'utf-8' });
36
+ return stdout;
37
+ };
38
+ /** Best-effort `git diff --name-only <mergeBase>...HEAD`; never throws. */
39
+ async function computeChangedFilesFromGit(projectPath, exec, baseBranch) {
40
+ try {
41
+ const mergeBaseOut = await exec('git', ['merge-base', 'HEAD', baseBranch], {
42
+ cwd: projectPath,
43
+ });
44
+ const mergeBase = mergeBaseOut.trim();
45
+ if (!mergeBase) {
46
+ return [];
47
+ }
48
+ const diffOut = await exec('git', ['diff', '--name-only', `${mergeBase}...HEAD`], {
49
+ cwd: projectPath,
50
+ });
51
+ return diffOut
52
+ .split('\n')
53
+ .map((line) => line.trim())
54
+ .filter((line) => line.length > 0);
55
+ }
56
+ catch (error) {
57
+ /* reliability-optional: EVIDENCE_AUTOFILL_GIT_DIFF — falls back to ## Files ownership */
58
+ reportClassifiedDegradation('EVIDENCE_AUTOFILL_GIT_DIFF', error);
59
+ return [];
60
+ }
61
+ }
62
+ function filterExistingRepoFiles(projectPath, paths) {
63
+ return paths.filter((path) => {
64
+ if (isAbsolute(path)) {
65
+ return false;
66
+ }
67
+ try {
68
+ return existsSync(resolve(projectPath, path));
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ });
74
+ }
75
+ /** Best-effort read + sha256 summary of the persisted validation report, for prose evidence. */
76
+ async function readValidationReportSummary(projectId, specId) {
77
+ const reportPath = join(projectDataDir(projectId), 'handoffs', specId, 'validation-report.json');
78
+ try {
79
+ const raw = await readFile(reportPath, 'utf-8');
80
+ const parsed = ValidationReportV1Schema.safeParse(JSON.parse(raw));
81
+ if (!parsed.success) {
82
+ return {};
83
+ }
84
+ const { score, reviewer } = parsed.data;
85
+ return {
86
+ validationEvidence: `Validated by handoffs/${specId}/validation-report.json (score=${score ?? 'n/a'}).`,
87
+ reviewerEvidence: `Reviewed by ${reviewer.agent} (${reviewer.kind}), verdict=${reviewer.verdict}.`,
88
+ };
89
+ }
90
+ catch {
91
+ return {};
92
+ }
93
+ }
94
+ /**
95
+ * Generate `traceability-matrix.json` in the handoff evidence store when it does
96
+ * not already exist. Never overwrites an existing file — if one is present this
97
+ * is a no-op that reports `already-exists`. Tolerates every failure by returning
98
+ * a typed non-throwing result; callers must never let this crash a lifecycle
99
+ * transition.
100
+ */
101
+ export async function autofillTraceabilityMatrix(args) {
102
+ const targetPath = handoffEvidencePath(args.projectId, args.specId, 'traceability-matrix.json');
103
+ try {
104
+ if (existsSync(targetPath)) {
105
+ return { written: false, reason: 'already-exists' };
106
+ }
107
+ const criteria = extractAcceptanceCriteriaTexts(args.specBody);
108
+ if (criteria.length === 0) {
109
+ return { written: false, reason: 'no-criteria' };
110
+ }
111
+ const scenarios = parseFrontmatterScenarios(args.specBody);
112
+ const exec = args.exec ?? defaultExec;
113
+ const baseBranch = args.baseBranch ?? 'main';
114
+ const gitChangedFiles = args.projectPath
115
+ ? await computeChangedFilesFromGit(args.projectPath, exec, baseBranch)
116
+ : [];
117
+ const ownership = extractCanonicalFileOwnership(args.specBody);
118
+ const ownershipFiles = [...ownership.toCreate, ...ownership.toModify, ...ownership.toTest];
119
+ const rawChangedFiles = gitChangedFiles.length > 0 ? gitChangedFiles : ownershipFiles;
120
+ const changedFiles = args.projectPath
121
+ ? filterExistingRepoFiles(args.projectPath, rawChangedFiles)
122
+ : rawChangedFiles;
123
+ const { validationEvidence, reviewerEvidence } = await readValidationReportSummary(args.projectId, args.specId);
124
+ const rows = criteria.map((acceptanceCriterion, index) => {
125
+ const testPaths = scenarios[index]?.tests?.map((test) => test.path) ?? [];
126
+ return {
127
+ acceptanceCriterion,
128
+ ...(testPaths.length > 0 ? { testEvidence: testPaths } : {}),
129
+ changedFiles,
130
+ ...(validationEvidence ? { validationEvidence } : {}),
131
+ ...(reviewerEvidence ? { reviewerEvidence } : {}),
132
+ };
133
+ });
134
+ const payload = { version: 1, rows };
135
+ await atomicWriteFile(targetPath, JSON.stringify(payload, null, 2));
136
+ return { written: true, path: targetPath, rowCount: rows.length };
137
+ }
138
+ catch (error) {
139
+ /* reliability-optional: EVIDENCE_AUTOFILL_WRITE — caller proceeds without autofill */
140
+ reportClassifiedDegradation('EVIDENCE_AUTOFILL_WRITE', error);
141
+ return {
142
+ written: false,
143
+ reason: 'write-failed',
144
+ error: error instanceof Error ? error.message : String(error),
145
+ };
146
+ }
147
+ }
148
+ //# sourceMappingURL=evidence-autofill.js.map
@@ -0,0 +1,19 @@
1
+ import type { Spec } from '../../types/spec/core.js';
2
+ import type { DiscoveryEvidence, TaskPlanEvidence } from '../../types/evidence-gates.js';
3
+ /** Build a strict-schema discovery.json skeleton grounded in the spec's own grounding metadata. */
4
+ export declare function generateDiscoverySkeleton(args: {
5
+ spec: Spec;
6
+ body: string | null;
7
+ }): DiscoveryEvidence;
8
+ /** Build a strict-schema task-plan.json skeleton with one pending task per canonical criterion. */
9
+ export declare function generateTaskPlanSkeleton(args: {
10
+ criteria: string[];
11
+ }): TaskPlanEvidence;
12
+ /** Persist an auto-generated evidence skeleton to the external handoff store. */
13
+ export declare function writeEvidenceSkeleton(args: {
14
+ projectId: string;
15
+ specId: string;
16
+ filename: string;
17
+ artifact: DiscoveryEvidence | TaskPlanEvidence;
18
+ }): Promise<string>;
19
+ //# sourceMappingURL=evidence-skeletons.d.ts.map
@@ -0,0 +1,69 @@
1
+ // evidence-skeletons.ts — SPEC-1356: auto-generate schema-valid discovery/task-plan skeletons
2
+ // when the underlying handoff evidence file is missing, so lifecycle gates self-heal instead
3
+ // of blocking on a manual authoring step. Skeletons still require review; they only unblock
4
+ // the transition, they do not fabricate confidence.
5
+ import { join } from 'node:path';
6
+ import { extractListItems, extractSection } from '../spec-format/markdown-sections.js';
7
+ import { parseCriterionGroundingRecords } from '../spec-grounding/contract.js';
8
+ import { projectDataDir } from '../../storage/base-store.js';
9
+ import { atomicWriteFile } from '../safety/atomic-write-file.js';
10
+ const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---/;
11
+ const DEFAULT_OUT_OF_SCOPE = 'None declared beyond the spec scope.';
12
+ const DEFAULT_RULE = 'Behavior must follow the approved spec Problem and Technical sections.';
13
+ function specFrontmatter(specBody) {
14
+ return FRONTMATTER_RE.exec(specBody)?.[1] ?? '';
15
+ }
16
+ function outOfScopeFromBody(specBody) {
17
+ if (!specBody) {
18
+ return [DEFAULT_OUT_OF_SCOPE];
19
+ }
20
+ const section = extractSection(specBody, 'out of scope');
21
+ if (!section) {
22
+ return [DEFAULT_OUT_OF_SCOPE];
23
+ }
24
+ const items = extractListItems(section);
25
+ return items.length > 0 ? items : [DEFAULT_OUT_OF_SCOPE];
26
+ }
27
+ /** Build a strict-schema discovery.json skeleton grounded in the spec's own grounding metadata. */
28
+ export function generateDiscoverySkeleton(args) {
29
+ const records = args.body ? parseCriterionGroundingRecords(specFrontmatter(args.body)) : [];
30
+ const rules = records.length > 0 ? [...new Set(records.map((record) => record.text))] : [DEFAULT_RULE];
31
+ const examples = records.length > 0
32
+ ? records.map((record) => ({
33
+ rule: record.text,
34
+ example: record.evidence[0] ??
35
+ `Derived from ${args.spec.id} (${args.spec.title}) grounding evidence.`,
36
+ }))
37
+ : [{ rule: DEFAULT_RULE, example: `Derived from spec ${args.spec.id}: ${args.spec.title}.` }];
38
+ return {
39
+ version: 1,
40
+ rules,
41
+ examples,
42
+ openQuestions: [],
43
+ outOfScope: outOfScopeFromBody(args.body),
44
+ glossary: [{ term: args.spec.title, meaning: args.spec.feature ?? args.spec.title }],
45
+ };
46
+ }
47
+ /** Build a strict-schema task-plan.json skeleton with one pending task per canonical criterion. */
48
+ export function generateTaskPlanSkeleton(args) {
49
+ const criteria = args.criteria.length > 0 ? args.criteria : ['Implementation satisfies the approved spec.'];
50
+ return {
51
+ version: 1,
52
+ tasks: criteria.map((criterion, index) => ({
53
+ id: `AC-${String(index + 1)}`,
54
+ title: criterion.length > 500 ? `${criterion.slice(0, 497)}...` : criterion,
55
+ acceptanceCriteria: [`AC${String(index + 1)}`],
56
+ status: 'pending',
57
+ })),
58
+ };
59
+ }
60
+ function handoffPath(projectId, specId, filename) {
61
+ return join(projectDataDir(projectId), 'handoffs', specId, filename);
62
+ }
63
+ /** Persist an auto-generated evidence skeleton to the external handoff store. */
64
+ export async function writeEvidenceSkeleton(args) {
65
+ const path = handoffPath(args.projectId, args.specId, args.filename);
66
+ await atomicWriteFile(path, `${JSON.stringify(args.artifact, null, 2)}\n`);
67
+ return path;
68
+ }
69
+ //# sourceMappingURL=evidence-skeletons.js.map
@@ -54,9 +54,15 @@ export class OperationJournal {
54
54
  const existing = this.get(operation, idempotencyKey);
55
55
  if (existing) {
56
56
  if (existing.requestDigest !== requestDigest) {
57
- throw new BoundaryFailure('Conflict', 'operation begin', 'Use a new idempotency key for a different request.');
57
+ // SPEC-1348: a rolled-back attempt produced no durable effects, so the
58
+ // key is re-claimable even when the retry payload differs byte-wise.
59
+ if (existing.state !== 'rolled-back') {
60
+ throw new BoundaryFailure('Conflict', 'operation begin', 'Use a new idempotency key for a different request.');
61
+ }
62
+ }
63
+ else {
64
+ return existing;
58
65
  }
59
- return existing;
60
66
  }
61
67
  const now = new Date().toISOString();
62
68
  const entry = {
@@ -66,8 +72,8 @@ export class OperationJournal {
66
72
  requestDigest,
67
73
  state: 'intent',
68
74
  ...(recovery ?? {}),
69
- recoveryAttempts: 0,
70
- createdAt: now,
75
+ recoveryAttempts: existing?.recoveryAttempts ?? 0,
76
+ createdAt: existing?.createdAt ?? now,
71
77
  updatedAt: now,
72
78
  };
73
79
  this.put(entry);