@planu/cli 5.0.0 → 5.2.0

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 (112) hide show
  1. package/CHANGELOG.md +76 -1
  2. package/dist/cli/commands/doctor.d.ts +22 -0
  3. package/dist/cli/commands/doctor.js +176 -2
  4. package/dist/cli/commands/spec.js +10 -1
  5. package/dist/core/spec-validator.js +32 -18
  6. package/dist/engine/autopilot/bootstrap.js +27 -0
  7. package/dist/engine/core-bridge.d.ts +28 -0
  8. package/dist/engine/core-bridge.js +67 -0
  9. package/dist/engine/drift-monitor.js +16 -18
  10. package/dist/engine/evidence-gates/artifact-reader.d.ts +2 -0
  11. package/dist/engine/evidence-gates/artifact-reader.js +59 -2
  12. package/dist/engine/evidence-gates/evidence-autofill.d.ts +10 -0
  13. package/dist/engine/evidence-gates/evidence-autofill.js +148 -0
  14. package/dist/engine/evidence-gates/evidence-skeletons.d.ts +19 -0
  15. package/dist/engine/evidence-gates/evidence-skeletons.js +69 -0
  16. package/dist/engine/execution/operation-journal.js +10 -4
  17. package/dist/engine/living-spec/hash-tracker.js +26 -28
  18. package/dist/engine/minimality/policy-loader.js +247 -6
  19. package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
  20. package/dist/engine/planu-core.darwin-arm64.node.sbom.json +14 -14
  21. package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
  22. package/dist/engine/planu-core.darwin-x64.node.sbom.json +14 -14
  23. package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
  24. package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +14 -14
  25. package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
  26. package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +14 -14
  27. package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
  28. package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +14 -14
  29. package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
  30. package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +14 -14
  31. package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
  32. package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +14 -14
  33. package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
  34. package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +14 -14
  35. package/dist/engine/reverse-engineer/api-detector.js +2 -13
  36. package/dist/engine/reverse-engineer/complexity-analyzer.js +2 -13
  37. package/dist/engine/reverse-engineer/config-analyzer.js +2 -13
  38. package/dist/engine/reverse-engineer/dependency-graph.js +2 -13
  39. package/dist/engine/reverse-engineer/test-analyzer.js +2 -13
  40. package/dist/engine/reverse-engineer/walk-ignore.d.ts +3 -0
  41. package/dist/engine/reverse-engineer/walk-ignore.js +26 -0
  42. package/dist/engine/spec-format/acceptance-criteria.js +13 -12
  43. package/dist/engine/spec-format/text-fences.js +20 -2
  44. package/dist/engine/spec-language/english-only.d.ts +14 -0
  45. package/dist/engine/spec-language/english-only.js +58 -0
  46. package/dist/engine/spec-migrator/criteria-scanner.js +11 -13
  47. package/dist/engine/spec-migrator/drift-detector.js +10 -12
  48. package/dist/engine/spec-state-syncer.js +1 -1
  49. package/dist/engine/timing/budget.js +5 -1
  50. package/dist/engine/vector-store/tfidf.d.ts +13 -9
  51. package/dist/engine/vector-store/tfidf.js +26 -0
  52. package/dist/engine/worker-config-loader.d.ts +1 -1
  53. package/dist/engine/worker-config-loader.js +1 -11
  54. package/dist/engine/workers/schema.d.ts +0 -8
  55. package/dist/engine/workers/schema.js +0 -1
  56. package/dist/i18n/index.d.ts +18 -0
  57. package/dist/i18n/index.js +40 -1
  58. package/dist/server/routes/specs.js +7 -5
  59. package/dist/storage/global-store.d.ts +9 -0
  60. package/dist/storage/global-store.js +23 -0
  61. package/dist/storage/semantic-index-store.d.ts +23 -0
  62. package/dist/storage/semantic-index-store.js +105 -0
  63. package/dist/storage/status-store/self-healing.js +15 -17
  64. package/dist/tools/challenge-spec/scenarios-utils.js +5 -1
  65. package/dist/tools/challenge-spec-helpers.d.ts +10 -1
  66. package/dist/tools/challenge-spec-helpers.js +63 -22
  67. package/dist/tools/challenge-spec.js +18 -3
  68. package/dist/tools/check-readiness.js +37 -13
  69. package/dist/tools/create-spec/spec-builder.d.ts +7 -0
  70. package/dist/tools/create-spec/spec-builder.js +19 -4
  71. package/dist/tools/create-spec.js +216 -101
  72. package/dist/tools/init-project/handler.js +78 -25
  73. package/dist/tools/learn.js +10 -8
  74. package/dist/tools/register-spec-tools/core-spec-tools.js +13 -12
  75. package/dist/tools/registry/auth.js +1 -11
  76. package/dist/tools/semantic-search-handler.js +5 -6
  77. package/dist/tools/status-handler.js +19 -6
  78. package/dist/tools/sync-spec-state-handler.js +49 -1
  79. package/dist/tools/update-status/batch.d.ts +6 -2
  80. package/dist/tools/update-status/batch.js +58 -1
  81. package/dist/tools/update-status/dod-gates.d.ts +16 -1
  82. package/dist/tools/update-status/dod-gates.js +191 -1
  83. package/dist/tools/update-status/done-receipt-verifier.d.ts +8 -0
  84. package/dist/tools/update-status/done-receipt-verifier.js +37 -2
  85. package/dist/tools/update-status/evidence-gate.d.ts +4 -0
  86. package/dist/tools/update-status/evidence-gate.js +67 -2
  87. package/dist/tools/update-status/file-sync.d.ts +2 -2
  88. package/dist/tools/update-status/index.d.ts +23 -1
  89. package/dist/tools/update-status/index.js +201 -24
  90. package/dist/tools/update-status/transition-guard.js +13 -1
  91. package/dist/tools/validation-loop-handler.js +16 -15
  92. package/dist/tools/workspace-dashboard-handler.js +38 -0
  93. package/dist/types/evidence-autofill.d.ts +34 -0
  94. package/dist/types/evidence-autofill.js +2 -0
  95. package/dist/types/index.d.ts +1 -0
  96. package/dist/types/index.js +1 -0
  97. package/dist/types/spec/core.d.ts +6 -0
  98. package/dist/types/spec/inputs.d.ts +7 -2
  99. package/dist/types/spec-format.d.ts +1 -1
  100. package/dist/types/spec-language-translation.d.ts +18 -0
  101. package/dist/types/spec-language-translation.js +5 -0
  102. package/dist/types/spec-registry.d.ts +0 -2
  103. package/dist/types/status.d.ts +2 -0
  104. package/dist/types/transition-log.d.ts +1 -1
  105. package/dist/types/validation.d.ts +8 -2
  106. package/dist/types/vector-store.d.ts +18 -0
  107. package/dist/types/workers.d.ts +0 -3
  108. package/package.json +11 -10
  109. package/planu-native.json +8 -29
  110. package/planu-plugin.json +1 -1
  111. package/dist/engine/security/cve-refresher.d.ts +0 -12
  112. package/dist/engine/security/cve-refresher.js +0 -128
@@ -1,3 +1,11 @@
1
+ /* eslint-disable max-lines -- done evidence gates remain co-located for one auditable fail-closed boundary */
2
+ // tools/update-status/dod-gates.ts — DoD gate checks and forceStatus bypass
3
+ // SPEC-729: Validate gate wrapped with withEscalation for bounded retries.
4
+ // SPEC-780: checkApprovedFormatGate honours forceApprove, mirroring SPEC-769 readiness gate.
5
+ import { createHash } from 'node:crypto';
6
+ import { constants } from 'node:fs';
7
+ import { lstat, open, realpath } from 'node:fs/promises';
8
+ import { isAbsolute, relative, resolve as resolvePath, sep } from 'node:path';
1
9
  import { validateSpec } from '../../engine/validator.js';
2
10
  import { generateDoD } from '../../engine/validator/dor-dod.js';
3
11
  import { addLesson, getLessons } from '../../storage/lessons-store.js';
@@ -9,6 +17,8 @@ import { writeSpecReviewFeedback } from '../../engine/validator/spec-review-writ
9
17
  import { formatKeyValue } from '../output-formatter.js';
10
18
  import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
11
19
  import { verifyCurrentDoneReceipt } from './done-receipt-verifier.js';
20
+ import { projectDataDir } from '../../storage/base-store.js';
21
+ import { ValidationReportV1Schema } from '../../engine/handoff-artifacts/schemas.js';
12
22
  export { completedJobPublishesReceipt } from './done-receipt-verifier.js';
13
23
  /**
14
24
  * SPEC-721 / SPEC-222 Trigger 1: Run validate engine before marking done.
@@ -275,6 +285,176 @@ function validationReceiptGateError(specId, reason) {
275
285
  },
276
286
  };
277
287
  }
288
+ function doneReviewDigestError(specId, code, message) {
289
+ const artifactPath = `external Planu project data: handoffs/${specId}/validation-report.json`;
290
+ const fixHint = 'Run validate, then recompute the sha256 digest from the exact report bytes.';
291
+ return {
292
+ content: [{ type: 'text', text: `${code}: ${message}\nFix: ${fixHint}` }],
293
+ isError: true,
294
+ structuredContent: {
295
+ error: code,
296
+ code: 422,
297
+ context: { specId, artifactPath },
298
+ fixHint,
299
+ },
300
+ };
301
+ }
302
+ const MAX_VALIDATION_REPORT_BYTES = 5 * 1024 * 1024;
303
+ function isContained(root, candidate) {
304
+ const pathFromRoot = relative(root, candidate);
305
+ return (pathFromRoot === '' ||
306
+ (!isAbsolute(pathFromRoot) && pathFromRoot !== '..' && !pathFromRoot.startsWith(`..${sep}`)));
307
+ }
308
+ function sameFileMetadata(left, right) {
309
+ return (left.dev === right.dev &&
310
+ left.ino === right.ino &&
311
+ left.size === right.size &&
312
+ left.mtimeNs === right.mtimeNs &&
313
+ left.ctimeNs === right.ctimeNs);
314
+ }
315
+ async function readStableDoneReviewReport(specId, projectId) {
316
+ const handoffsRoot = resolvePath(projectDataDir(projectId), 'handoffs');
317
+ const specRoot = resolvePath(handoffsRoot, specId);
318
+ const reportPath = resolvePath(specRoot, 'validation-report.json');
319
+ if (!isContained(handoffsRoot, specRoot) || !isContained(specRoot, reportPath)) {
320
+ return doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_UNSAFE', 'The validation report path escapes its contained handoff directory.');
321
+ }
322
+ let canonicalHandoffs;
323
+ let canonicalSpecRoot;
324
+ let canonicalReport;
325
+ let pathStats;
326
+ try {
327
+ [canonicalHandoffs, canonicalSpecRoot, canonicalReport, pathStats] = await Promise.all([
328
+ realpath(handoffsRoot),
329
+ realpath(specRoot),
330
+ realpath(reportPath),
331
+ lstat(reportPath, { bigint: true }),
332
+ ]);
333
+ }
334
+ catch {
335
+ return {
336
+ ...doneReviewDigestError(specId, 'DONE_REVIEW_DIGEST_MISMATCH', 'No persisted validation report was found for this spec. Run validate first, then retry done.'),
337
+ isError: true,
338
+ };
339
+ }
340
+ if (!isContained(canonicalHandoffs, canonicalSpecRoot) ||
341
+ !isContained(canonicalSpecRoot, canonicalReport) ||
342
+ pathStats.isSymbolicLink() ||
343
+ !pathStats.isFile() ||
344
+ pathStats.size > BigInt(MAX_VALIDATION_REPORT_BYTES)) {
345
+ return doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_UNSAFE', 'The validation report is not a contained bounded regular file.');
346
+ }
347
+ if (typeof constants.O_NOFOLLOW !== 'number') {
348
+ return doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_UNSAFE', 'The platform cannot safely open the validation report without following links.');
349
+ }
350
+ let handle;
351
+ try {
352
+ handle = await open(reportPath, constants.O_RDONLY | constants.O_NOFOLLOW);
353
+ }
354
+ catch {
355
+ return {
356
+ ...doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_UNSAFE', 'The validation report could not be opened without following links.'),
357
+ isError: true,
358
+ };
359
+ }
360
+ try {
361
+ const openedStats = await handle.stat({ bigint: true });
362
+ if (!openedStats.isFile() || !sameFileMetadata(pathStats, openedStats)) {
363
+ return doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_CHANGED', 'The validation report identity changed before it was opened.');
364
+ }
365
+ const reportBytes = Buffer.alloc(Number(openedStats.size));
366
+ let offset = 0;
367
+ while (offset < reportBytes.length) {
368
+ const { bytesRead } = await handle.read(reportBytes, offset, reportBytes.length - offset, offset);
369
+ if (bytesRead === 0) {
370
+ return doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_CHANGED', 'The validation report changed while it was being read.');
371
+ }
372
+ offset += bytesRead;
373
+ }
374
+ const finalHandleStats = await handle.stat({ bigint: true });
375
+ if (!sameFileMetadata(openedStats, finalHandleStats)) {
376
+ return doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_CHANGED', 'The opened validation report changed while it was being read.');
377
+ }
378
+ let finalPathStats;
379
+ let finalCanonicalReport;
380
+ try {
381
+ [finalPathStats, finalCanonicalReport] = await Promise.all([
382
+ lstat(reportPath, { bigint: true }),
383
+ realpath(reportPath),
384
+ ]);
385
+ }
386
+ catch {
387
+ return {
388
+ ...doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_CHANGED', 'The validation report path changed while it was being read.'),
389
+ isError: true,
390
+ };
391
+ }
392
+ if (!isContained(canonicalSpecRoot, finalCanonicalReport)) {
393
+ return doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_UNSAFE', 'The validation report path escaped containment while it was being read.');
394
+ }
395
+ if (finalPathStats.isSymbolicLink() ||
396
+ !finalPathStats.isFile() ||
397
+ !sameFileMetadata(openedStats, finalPathStats)) {
398
+ return doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_CHANGED', 'The validation report path identity changed while it was being read.');
399
+ }
400
+ return reportBytes;
401
+ }
402
+ finally {
403
+ await handle.close();
404
+ }
405
+ }
406
+ /**
407
+ * SPEC-1356 (T-A1): auto-derive the sha256 digest of the persisted
408
+ * `validation-report.json` so callers no longer have to compute it by hand.
409
+ * Reads and hashes the exact same bytes `checkDoneReviewDigest` would validate
410
+ * against, using the same hardened path/identity checks. Returns the digest, or
411
+ * the exact blocking `ToolResult` when the report cannot be safely read (e.g.
412
+ * validate has not run yet). Exported for direct unit testing and for the
413
+ * orchestration layer (`update-status/index.ts`) that no longer requires the
414
+ * caller to supply `implementationReviewDigest`.
415
+ */
416
+ export async function deriveDoneReviewDigest(specId, projectId) {
417
+ const reportResult = await readStableDoneReviewReport(specId, projectId);
418
+ if (!Buffer.isBuffer(reportResult)) {
419
+ return { error: reportResult };
420
+ }
421
+ return { digest: `sha256:${createHash('sha256').update(reportResult).digest('hex')}` };
422
+ }
423
+ async function checkDoneReviewDigest(specId, projectId, implementationReviewDigest) {
424
+ if (!/^sha256:[a-f0-9]{64}$/u.test(implementationReviewDigest)) {
425
+ return doneReviewDigestError(specId, 'DONE_REVIEW_DIGEST_INVALID', 'The implementation review digest is malformed.');
426
+ }
427
+ const reportResult = await readStableDoneReviewReport(specId, projectId);
428
+ if (!Buffer.isBuffer(reportResult)) {
429
+ return reportResult;
430
+ }
431
+ const observed = `sha256:${createHash('sha256').update(reportResult).digest('hex')}`;
432
+ if (observed !== implementationReviewDigest) {
433
+ return doneReviewDigestError(specId, 'DONE_REVIEW_DIGEST_MISMATCH', 'The implementation review digest does not match the persisted validation report.');
434
+ }
435
+ let decoded;
436
+ try {
437
+ decoded = JSON.parse(reportResult.toString('utf8'));
438
+ }
439
+ catch {
440
+ return {
441
+ ...doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_UNSAFE', 'The validation report bytes are not valid JSON.'),
442
+ isError: true,
443
+ };
444
+ }
445
+ const parsed = ValidationReportV1Schema.safeParse(decoded);
446
+ if (!parsed.success ||
447
+ parsed.data.specId !== specId ||
448
+ !parsed.data.passed ||
449
+ parsed.data.gates.some((gate) => !gate.passed) ||
450
+ parsed.data.minimalityReport?.blocked === true ||
451
+ parsed.data.reviewer.kind !== 'implementation-review-agent' ||
452
+ parsed.data.reviewer.agent !== 'planu-implementation-reviewer' ||
453
+ parsed.data.reviewer.verdict !== 'approved') {
454
+ return doneReviewDigestError(specId, 'DONE_REVIEW_REPORT_UNSAFE', 'The validation report schema or independent reviewer evidence is invalid.');
455
+ }
456
+ return null;
457
+ }
278
458
  /**
279
459
  * SPEC-725: Check the validation-report artifact gate before marking done.
280
460
  * SPEC-1050: Fail closed. A done transition requires a fresh validation-report
@@ -493,8 +673,18 @@ function specReviewGateError(args) {
493
673
  * When force=true, gates are not blocking but failing items are recorded in
494
674
  * the project's force-bypass-log.json for traceability (guard: done ≠ implemented).
495
675
  */
496
- export async function checkDoneGates(spec, specId, projectId, projectPath, _force, _forceReason = 'No force reason provided', verifyReceipt = verifyCurrentDoneReceipt) {
676
+ export async function checkDoneGates(spec, specId, projectId, projectPath, _force, _forceReason = 'No force reason provided', verifyReceipt = verifyCurrentDoneReceipt, implementationReviewDigest) {
497
677
  try {
678
+ if (implementationReviewDigest !== undefined) {
679
+ const digestError = await checkDoneReviewDigest(specId, projectId, implementationReviewDigest);
680
+ if (digestError) {
681
+ return {
682
+ blocked: digestError,
683
+ forcedBypassWarning: null,
684
+ freshnessLease: null,
685
+ };
686
+ }
687
+ }
498
688
  const result = await verifyReceipt({ projectId, specId, projectPath, specPath: spec.specPath });
499
689
  return result.valid
500
690
  ? {
@@ -1,5 +1,6 @@
1
1
  import { type DurableJobRecord } from '../../storage/runtime-db.js';
2
2
  import { type ValidationFreshnessLease } from '../../engine/validation/validation-freshness.js';
3
+ import type { ValidationReceiptBindings } from '../../types/validation-receipt.js';
3
4
  export type DoneReceiptVerifier = (identity: {
4
5
  projectId: string;
5
6
  specId: string;
@@ -11,5 +12,12 @@ export type DoneReceiptVerifier = (identity: {
11
12
  freshnessLease?: ValidationFreshnessLease;
12
13
  }>;
13
14
  export declare function completedJobPublishesReceipt(job: DurableJobRecord | undefined, receiptId: string): boolean;
15
+ /**
16
+ * Evidence artifacts (discovery/task-plan/traceability/contract-validation) may legitimately
17
+ * be regenerated after a receipt was issued (e.g. autopilot skeleton fill). If ONLY the evidence
18
+ * digests changed while every code-facing binding stayed byte-identical, the receipt is rebound
19
+ * to the freshly observed evidence digests instead of being rejected as stale.
20
+ */
21
+ export declare function doneReceiptBindingsDifferOnlyInEvidence(observed: ValidationReceiptBindings | undefined, stored: ValidationReceiptBindings | undefined): boolean;
14
22
  export declare function verifyCurrentDoneReceipt(identity: Parameters<DoneReceiptVerifier>[0]): ReturnType<DoneReceiptVerifier>;
15
23
  //# sourceMappingURL=done-receipt-verifier.d.ts.map
@@ -70,6 +70,31 @@ export function completedJobPublishesReceipt(job, receiptId) {
70
70
  return (published.receiptId === receiptId &&
71
71
  published.receiptKey === `validation-receipt:${receiptId.slice('sha256:'.length)}`);
72
72
  }
73
+ /**
74
+ * Evidence artifacts (discovery/task-plan/traceability/contract-validation) may legitimately
75
+ * be regenerated after a receipt was issued (e.g. autopilot skeleton fill). If ONLY the evidence
76
+ * digests changed while every code-facing binding stayed byte-identical, the receipt is rebound
77
+ * to the freshly observed evidence digests instead of being rejected as stale.
78
+ */
79
+ export function doneReceiptBindingsDifferOnlyInEvidence(observed, stored) {
80
+ if (!observed?.artifactDigests || !stored?.artifactDigests) {
81
+ return false;
82
+ }
83
+ const codeBindingsMatch = observed.projectId === stored.projectId &&
84
+ observed.specId === stored.specId &&
85
+ observed.sourceDigest === stored.sourceDigest &&
86
+ observed.specDigest === stored.specDigest &&
87
+ observed.policyDigest === stored.policyDigest &&
88
+ observed.toolchainDigest === stored.toolchainDigest &&
89
+ observed.artifactDigests.approvedSpec === stored.artifactDigests.approvedSpec;
90
+ if (!codeBindingsMatch) {
91
+ return false;
92
+ }
93
+ return (observed.artifactDigests.discovery !== stored.artifactDigests.discovery ||
94
+ observed.artifactDigests.taskPlan !== stored.artifactDigests.taskPlan ||
95
+ observed.artifactDigests.traceability !== stored.artifactDigests.traceability ||
96
+ observed.artifactDigests.contractValidation !== stored.artifactDigests.contractValidation);
97
+ }
73
98
  export async function verifyCurrentDoneReceipt(identity) {
74
99
  const env_1 = { stack: [], error: void 0, hasError: false };
75
100
  try {
@@ -127,8 +152,18 @@ export async function verifyCurrentDoneReceipt(identity) {
127
152
  specPath,
128
153
  });
129
154
  const observedBindings = toValidationReceiptBindings(observed.bindings, observed.artifactDigests);
130
- if (JSON.stringify(observedBindings) !== JSON.stringify(currentBindings) ||
131
- !(await bindValidationFreshnessLease(ownedLease, observedBindings))) {
155
+ if (JSON.stringify(observedBindings) !== JSON.stringify(currentBindings)) {
156
+ if (!doneReceiptBindingsDifferOnlyInEvidence(observedBindings, currentBindings)) {
157
+ return { valid: false, reason: 'stale-bindings' };
158
+ }
159
+ await store.projectCurrentBindings(observedBindings);
160
+ console.warn('[planu:evidence-rebind]', {
161
+ projectId: identity.projectId,
162
+ specId: identity.specId,
163
+ rebindAt: new Date().toISOString(),
164
+ });
165
+ }
166
+ if (!(await bindValidationFreshnessLease(ownedLease, observedBindings))) {
132
167
  return { valid: false, reason: 'stale-bindings' };
133
168
  }
134
169
  }
@@ -1,6 +1,10 @@
1
1
  import type { ToolResult } from '../../types/index.js';
2
2
  import type { Spec } from '../../types/spec/core.js';
3
3
  import type { EvidenceGateTransition } from '../../types/evidence-gates.js';
4
+ /** SPEC-1356: repo-relative paths mentioned in the spec's Problem/Technical prose also count
5
+ * as approved canonical scope, even when they are not in the file-ownership checklist. Paths
6
+ * do not need to exist yet (a spec may name files it has not created). */
7
+ export declare function extractCanonicalScopePathsFromSections(body: string): string[];
4
8
  /** SPEC-1054: BDD/SDD lifecycle evidence gate. */
5
9
  export declare function checkLifecycleEvidenceTransitionGate(args: {
6
10
  spec: Spec;
@@ -68,6 +68,18 @@ import { captureValidationFreshnessLease } from '../../engine/validation/validat
68
68
  import { computeDurableValidationBindings, toValidationReceiptBindings, } from '../../engine/validation/durable-validation.js';
69
69
  import { checkReconciliationFreshness } from '../../engine/evidence-gates/reconciliation-freshness.js';
70
70
  import { extractCanonicalFileOwnership } from '../../engine/handoff-packager.js';
71
+ import { generateDiscoverySkeleton, generateTaskPlanSkeleton, writeEvidenceSkeleton, } from '../../engine/evidence-gates/evidence-skeletons.js';
72
+ import { extractSection } from '../../engine/spec-format/markdown-sections.js';
73
+ const CANONICAL_SCOPE_PATH = /(?:src|tests|scripts|website)\/[\w/.@-]+\.\w+/g;
74
+ /** SPEC-1356: repo-relative paths mentioned in the spec's Problem/Technical prose also count
75
+ * as approved canonical scope, even when they are not in the file-ownership checklist. Paths
76
+ * do not need to exist yet (a spec may name files it has not created). */
77
+ export function extractCanonicalScopePathsFromSections(body) {
78
+ const problem = extractSection(body, 'problem') ?? '';
79
+ const technical = extractSection(body, 'technical') ?? '';
80
+ const matches = `${problem}\n${technical}`.match(CANONICAL_SCOPE_PATH) ?? [];
81
+ return [...new Set(matches)];
82
+ }
71
83
  /** SPEC-1054: BDD/SDD lifecycle evidence gate. */
72
84
  export async function checkLifecycleEvidenceTransitionGate(args) {
73
85
  const staleReconciliationEvidence = await checkReconciliationFreshness({
@@ -85,7 +97,7 @@ export async function checkLifecycleEvidenceTransitionGate(args) {
85
97
  requiredContractKinds: [],
86
98
  });
87
99
  }
88
- const [criteriaResult, artifacts] = await Promise.all([
100
+ const [criteriaResult, initialArtifacts] = await Promise.all([
89
101
  extractLifecycleGateCriteriaAndBody(args.spec, args.projectPath),
90
102
  readEvidenceArtifacts({
91
103
  spec: args.spec,
@@ -94,6 +106,16 @@ export async function checkLifecycleEvidenceTransitionGate(args) {
94
106
  projectPath: args.projectPath,
95
107
  }),
96
108
  ]);
109
+ const { criteria, body } = criteriaResult;
110
+ const artifacts = await autofillMissingEvidenceSkeletons({
111
+ spec: args.spec,
112
+ projectId: args.projectId,
113
+ specId: args.specId,
114
+ transition: args.transition,
115
+ criteria,
116
+ body,
117
+ artifacts: initialArtifacts,
118
+ });
97
119
  const receiptResult = args.transition === 'done'
98
120
  ? await readVerifiedCurrentReceipt({
99
121
  projectId: args.projectId,
@@ -110,7 +132,6 @@ export async function checkLifecycleEvidenceTransitionGate(args) {
110
132
  requiredContractKinds: [],
111
133
  });
112
134
  }
113
- const { criteria, body } = criteriaResult;
114
135
  const result = checkLifecycleEvidenceGate({
115
136
  transition: args.transition,
116
137
  spec: args.spec,
@@ -129,12 +150,14 @@ export async function checkLifecycleEvidenceTransitionGate(args) {
129
150
  ? extractCanonicalFileOwnership(body)
130
151
  : { present: false, toCreate: [], toModify: [], toTest: [], blockers: [] };
131
152
  const groundedTechnicalPaths = body ? parseGroundedTechnicalPaths(body) : [];
153
+ const problemTechnicalScopePaths = body ? extractCanonicalScopePathsFromSections(body) : [];
132
154
  const approvedScopePaths = [
133
155
  ...new Set([
134
156
  ...canonicalOwnership.toCreate,
135
157
  ...canonicalOwnership.toModify,
136
158
  ...canonicalOwnership.toTest,
137
159
  ...groundedTechnicalPaths,
160
+ ...problemTechnicalScopePaths,
138
161
  ]),
139
162
  ];
140
163
  if (canonicalOwnership.blockers.length > 0) {
@@ -159,6 +182,48 @@ export async function checkLifecycleEvidenceTransitionGate(args) {
159
182
  requiredContractKinds: result.requiredContractKinds,
160
183
  });
161
184
  }
185
+ /** True when the artifact truly has no file yet, as opposed to failing schema validation. */
186
+ function isArtifactAbsent(artifacts, label) {
187
+ return !artifacts.invalidArtifacts.some((entry) => entry.startsWith(label));
188
+ }
189
+ /**
190
+ * SPEC-1356: self-heal missing (not malformed) discovery/task-plan handoff evidence by
191
+ * generating a schema-valid skeleton and persisting it, instead of blocking the transition
192
+ * on a manual authoring step. Malformed existing artifacts still fail the gate as before.
193
+ */
194
+ async function autofillMissingEvidenceSkeletons(args) {
195
+ if (args.spec.scope === 'trivial') {
196
+ return args.artifacts;
197
+ }
198
+ let artifacts = args.artifacts;
199
+ if (args.transition === 'approved' &&
200
+ !artifacts.discovery &&
201
+ isArtifactAbsent(artifacts, 'Discovery evidence')) {
202
+ const skeleton = generateDiscoverySkeleton({ spec: args.spec, body: args.body });
203
+ await writeEvidenceSkeleton({
204
+ projectId: args.projectId,
205
+ specId: args.specId,
206
+ filename: 'discovery.json',
207
+ artifact: skeleton,
208
+ });
209
+ console.warn('[planu:evidence-skeleton]', { specId: args.specId, filename: 'discovery.json' });
210
+ artifacts = { ...artifacts, discovery: skeleton };
211
+ }
212
+ if (args.transition === 'implementing' &&
213
+ !artifacts.taskPlan &&
214
+ isArtifactAbsent(artifacts, 'Task plan evidence')) {
215
+ const skeleton = generateTaskPlanSkeleton({ criteria: args.criteria });
216
+ await writeEvidenceSkeleton({
217
+ projectId: args.projectId,
218
+ specId: args.specId,
219
+ filename: 'task-plan.json',
220
+ artifact: skeleton,
221
+ });
222
+ console.warn('[planu:evidence-skeleton]', { specId: args.specId, filename: 'task-plan.json' });
223
+ artifacts = { ...artifacts, taskPlan: skeleton };
224
+ }
225
+ return artifacts;
226
+ }
162
227
  async function readVerifiedCurrentReceipt(identity) {
163
228
  try {
164
229
  const env_1 = { stack: [], error: void 0, hasError: false };
@@ -1,6 +1,6 @@
1
- import type { SpecStatus, UpdateStatusInput } from '../../types/index.js';
1
+ import type { SpecStatus, Actuals } from '../../types/index.js';
2
2
  import type { Spec } from '../../types/spec/core.js';
3
- export declare function recordDoneMetrics(projectId: string, specId: string, spec: Spec, actuals: NonNullable<UpdateStatusInput['actuals']>, projectPath?: string): Promise<void>;
3
+ export declare function recordDoneMetrics(projectId: string, specId: string, spec: Spec, actuals: Actuals, projectPath?: string): Promise<void>;
4
4
  /**
5
5
  * SPEC-698: A frontmatter sync warning surfaced when `update_status` could not
6
6
  * keep the on-disk spec.md status in sync with the data store.
@@ -1,8 +1,30 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import type { UpdateStatusInput, ToolResult } from '../../types/index.js';
2
+ import type { UpdateStatusInput, ToolResult, Actuals, ReconciliationInvocationContext } from '../../types/index.js';
3
+ type LifecycleAuthorityRoute = {
4
+ route: 'ordinary';
5
+ } | {
6
+ route: 'reconcile';
7
+ } | {
8
+ route: 'error';
9
+ result: ToolResult;
10
+ };
11
+ /** Select the lifecycle path before any transition, artifact, receipt, or log mutation. */
12
+ export declare function classifyLifecycleAuthority(input: UpdateStatusInput, context: ReconciliationInvocationContext, options?: {
13
+ requireDoneDigest?: boolean;
14
+ }): LifecycleAuthorityRoute;
15
+ /**
16
+ * SPEC-1356 (T-A2): a `done` transition no longer requires the caller to supply
17
+ * actuals. When the whole object is omitted, synthesize a minimal record instead
18
+ * of blocking the transition with `actuals_required`. Every caller-provided
19
+ * field — including a caller-provided partial object once the MCP input schema
20
+ * accepts one — is preserved verbatim; only genuinely unmeasured numeric fields
21
+ * default to 0, and `completedAt`/`notes` only default when absent.
22
+ */
23
+ export declare function synthesizeActuals(actuals: Partial<Actuals> | undefined, transitionedAt: string): Actuals;
3
24
  /** Bind deferred lifecycle work to one normalized absolute root without persisting that path. */
4
25
  export declare function computeTransitionRootBinding(projectPath: string): string;
5
26
  /** Recover durable post-commit work for every registered project after server startup. */
6
27
  export declare function recoverPendingPostCommitTasksAtStartup(): Promise<void>;
7
28
  export declare function handleUpdateStatus(params: UpdateStatusInput, server?: McpServer): Promise<ToolResult>;
29
+ export {};
8
30
  //# sourceMappingURL=index.d.ts.map