@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
@@ -15,8 +15,9 @@ import { checkApprovalGate } from '../../engine/approval-workflow.js';
15
15
  import * as approvalStore from '../../storage/approval-store.js';
16
16
  import { withApprovalSpecLock } from '../../storage/approval-operation-lock.js';
17
17
  import { isLocked, getLock } from '../../storage/spec-lock-store.js';
18
- import { checkDoneGates, checkComplianceGate, checkApprovedFormatGate, checkSpecReviewGate, writeSpecReviewArtifact, } from './dod-gates.js';
18
+ import { checkDoneGates, checkComplianceGate, checkApprovedFormatGate, checkSpecReviewGate, writeSpecReviewArtifact, deriveDoneReviewDigest, } from './dod-gates.js';
19
19
  import { checkLifecycleEvidenceTransitionGate } from './evidence-gate.js';
20
+ import { autofillTraceabilityMatrix } from '../../engine/evidence-gates/evidence-autofill.js';
20
21
  import { buildStatusResponse, buildDryRunResponse } from './response-builder.js';
21
22
  import { recordDoneMetrics, syncSpecFiles, tryReconcile, recordTerminalTransitionEvent, } from './file-sync.js';
22
23
  import { appendEntry, getLastHash } from '../../storage/audit-trail-store.js';
@@ -27,7 +28,8 @@ import { recordForceUsage } from '../../storage/force-analytics-store.js';
27
28
  import { maybeSafePushOnDone, queuePostCommitTasks, runCascadeForResponse, } from './side-effects.js';
28
29
  import { formatKeyValue } from '../output-formatter.js';
29
30
  import { checkCodeReality } from '../../engine/code-scanner/index.js';
30
- import { join, resolve } from 'node:path';
31
+ import { isAbsolute, join, resolve } from 'node:path';
32
+ import { readSpecContent } from '../../engine/spec-reader.js';
31
33
  import { trackCost } from '../../engine/cost-tracking/operation-tracker.js';
32
34
  import { resolveProjectIdOrAutoDetect } from '../resolve-project-id.js';
33
35
  import { withToolTimeout } from '../safe-handler.js';
@@ -40,6 +42,77 @@ import { assertExecutionCanCommit } from '../../engine/execution/context.js';
40
42
  import { shouldSkipGateForExplicitLegacyHarness } from './gate-harness.js';
41
43
  import { assertValidationFreshnessBarrier, closeValidationFreshnessLease, } from '../../engine/validation/validation-freshness.js';
42
44
  import { currentReconciliationInvocationContext, reconcileImplementingSpec, } from '../../engine/lifecycle-reconciliation.js';
45
+ const IMPLEMENTATION_REVIEW_DIGEST = /^sha256:[a-f0-9]{64}$/u;
46
+ function lifecycleAuthorityError(code, message, fixHint) {
47
+ return {
48
+ content: [{ type: 'text', text: `${code}: ${message}\nFix: ${fixHint}` }],
49
+ isError: true,
50
+ structuredContent: { error: code, code: 422, fixHint },
51
+ };
52
+ }
53
+ /** Select the lifecycle path before any transition, artifact, receipt, or log mutation. */
54
+ export function classifyLifecycleAuthority(input, context, options = {}) {
55
+ const hasRequestId = input.reconciliationRequestId !== undefined;
56
+ const hasTransitionId = input.expectedImplementingTransitionId !== undefined;
57
+ const hasDigest = input.implementationReviewDigest !== undefined;
58
+ const hasAnyAuthority = hasRequestId || hasTransitionId || hasDigest;
59
+ if (input.status === 'done') {
60
+ if (hasRequestId || hasTransitionId) {
61
+ return {
62
+ route: 'error',
63
+ result: lifecycleAuthorityError('RECONCILIATION_TARGET_INVALID', 'Reconciliation authority is valid only for the review target.', 'Remove reconciliationRequestId and expectedImplementingTransitionId from the done request.'),
64
+ };
65
+ }
66
+ // SPEC-1356: implementationReviewDigest is optional. When the caller omits it,
67
+ // the done gate (checkDoneGates via dod-gates.deriveDoneReviewDigest) derives
68
+ // it automatically from the persisted validation-report.json — the caller no
69
+ // longer has to compute the sha256 by hand. `options.requireDoneDigest` is
70
+ // retained only so legacy/explicit-authority callers can still demand it.
71
+ if (options.requireDoneDigest === true && !hasDigest) {
72
+ return {
73
+ route: 'error',
74
+ result: lifecycleAuthorityError('DONE_REVIEW_DIGEST_REQUIRED', 'A done transition requires the implementation review digest.', 'Supply the sha256 digest of the exact persisted validation-report.json bytes.'),
75
+ };
76
+ }
77
+ if (hasDigest &&
78
+ (typeof input.implementationReviewDigest !== 'string' ||
79
+ !IMPLEMENTATION_REVIEW_DIGEST.test(input.implementationReviewDigest))) {
80
+ return {
81
+ route: 'error',
82
+ result: lifecycleAuthorityError('DONE_REVIEW_DIGEST_INVALID', 'The implementation review digest must be sha256 followed by 64 lowercase hex characters.', 'Recompute the digest from the exact persisted validation-report.json bytes.'),
83
+ };
84
+ }
85
+ return { route: 'ordinary' };
86
+ }
87
+ if (input.status === 'review' && hasAnyAuthority) {
88
+ if (!hasRequestId || !hasTransitionId || !hasDigest) {
89
+ return {
90
+ route: 'error',
91
+ result: lifecycleAuthorityError('RECONCILIATION_TUPLE_INCOMPLETE', 'Review reconciliation requires the complete authority tuple.', 'Supply reconciliationRequestId, expectedImplementingTransitionId, implementationReviewDigest, and a reason of at least 100 characters.'),
92
+ };
93
+ }
94
+ if (!input.reason || input.reason.trim().length < 100) {
95
+ return {
96
+ route: 'error',
97
+ result: lifecycleAuthorityError('RECONCILIATION_REASON_INVALID', 'Review reconciliation requires a non-blank reason of at least 100 characters.', 'Explain the reviewed implementation drift and required follow-up in at least 100 characters.'),
98
+ };
99
+ }
100
+ if (context.surface !== 'local-mcp') {
101
+ return {
102
+ route: 'error',
103
+ result: lifecycleAuthorityError('RECONCILIATION_UNTRUSTED_SURFACE', 'Reconciliation is available only through trusted local MCP.', 'Retry the complete reconciliation request through the registered local MCP tool.'),
104
+ };
105
+ }
106
+ return { route: 'reconcile' };
107
+ }
108
+ if (hasAnyAuthority) {
109
+ return {
110
+ route: 'error',
111
+ result: lifecycleAuthorityError('RECONCILIATION_TARGET_INVALID', 'Lifecycle authority fields are invalid for this target.', 'Remove the authority fields or use implementationReviewDigest only with done.'),
112
+ };
113
+ }
114
+ return { route: 'ordinary' };
115
+ }
43
116
  /**
44
117
  * SPEC-280: Silently traverse intermediate states so callers can jump forward
45
118
  * (e.g. draft → approved) without receiving an error.
@@ -52,6 +125,58 @@ function buildAutoAdvancePlan(currentStatus, targetStatus) {
52
125
  stepsExecuted,
53
126
  };
54
127
  }
128
+ /**
129
+ * SPEC-1356 (T-A3): auto-generate traceability-matrix.json when it is missing,
130
+ * BEFORE the evidence gate reads it for a 'done' transition. Never overwrites an
131
+ * existing artifact. Tolerates every internal failure (falls through to the
132
+ * ordinary evidence gate, which will report the real missing-evidence error).
133
+ *
134
+ * When autofill actually writes a fresh file, the newly-created evidence has not
135
+ * been bound by validate's durable receipt yet, so this blocks once with a clear
136
+ * "re-run validate" message instead of letting a stale-relative-to-receipt
137
+ * traceability matrix slide through the done gates silently.
138
+ */
139
+ async function runTraceabilityAutofillGate(args) {
140
+ try {
141
+ const specPath = isAbsolute(args.spec.specPath) || !args.projectPath
142
+ ? args.spec.specPath
143
+ : join(args.projectPath, args.spec.specPath);
144
+ const specBody = await readSpecContent(specPath);
145
+ if (specBody === null) {
146
+ return null;
147
+ }
148
+ const result = await autofillTraceabilityMatrix({
149
+ projectId: args.projectId,
150
+ specId: args.specId,
151
+ projectPath: args.projectPath,
152
+ specBody,
153
+ });
154
+ if (!result.written) {
155
+ return null;
156
+ }
157
+ return {
158
+ content: [
159
+ {
160
+ type: 'text',
161
+ text: `traceability-matrix.json was auto-generated at ${result.path} (${String(result.rowCount)} rows). ` +
162
+ 'Run validate to bind this evidence, then retry update_status(done).',
163
+ },
164
+ ],
165
+ isError: true,
166
+ structuredContent: {
167
+ error: 'traceability_matrix_autofilled',
168
+ code: 422,
169
+ context: { specId: args.specId, path: result.path, rowCount: result.rowCount },
170
+ fixHint: 'Run validate so the new traceability-matrix.json is bound, then retry done.',
171
+ },
172
+ };
173
+ }
174
+ catch (error) {
175
+ // reliability-optional: EVIDENCE_AUTOFILL_GATE — falls through to the ordinary evidence gate
176
+ reportClassifiedDegradation('EVIDENCE_AUTOFILL_GATE', error);
177
+ return null;
178
+ }
179
+ }
55
180
  /**
56
181
  * SPEC-301: Check if spec is locked by another agent.
57
182
  * Returns an error ToolResult if locked, or null to continue.
@@ -101,6 +226,27 @@ async function checkApprovalPolicyGate(projectId, specId, newStatus) {
101
226
  }
102
227
  return null;
103
228
  }
229
+ /**
230
+ * SPEC-1356 (T-A2): a `done` transition no longer requires the caller to supply
231
+ * actuals. When the whole object is omitted, synthesize a minimal record instead
232
+ * of blocking the transition with `actuals_required`. Every caller-provided
233
+ * field — including a caller-provided partial object once the MCP input schema
234
+ * accepts one — is preserved verbatim; only genuinely unmeasured numeric fields
235
+ * default to 0, and `completedAt`/`notes` only default when absent.
236
+ */
237
+ export function synthesizeActuals(actuals, transitionedAt) {
238
+ return {
239
+ devHours: actuals?.devHours ?? 0,
240
+ reviewHours: actuals?.reviewHours ?? 0,
241
+ tokensOpus: actuals?.tokensOpus ?? 0,
242
+ tokensSonnet: actuals?.tokensSonnet ?? 0,
243
+ apiCostUsd: actuals?.apiCostUsd ?? 0,
244
+ humanCostUsd: actuals?.humanCostUsd ?? 0,
245
+ totalCostUsd: actuals?.totalCostUsd ?? 0,
246
+ completedAt: actuals?.completedAt ?? transitionedAt,
247
+ notes: actuals?.notes ?? 'actuals unavailable — auto-filled by done gate',
248
+ };
249
+ }
104
250
  /** Compute done metrics when transitioning to 'done' with actuals provided. */
105
251
  function computeDoneMetrics(newStatus, spec, actuals) {
106
252
  if (newStatus !== 'done' || actuals === undefined || spec === null) {
@@ -605,6 +751,13 @@ async function resolveOrchestrationPlan(newStatus, specScope, specId, projectPat
605
751
  export async function handleUpdateStatus(params, server) {
606
752
  return trackCost(params.projectPath?.trim() ?? '', 'update_status', async () => {
607
753
  const { specId, status: newStatus, actuals, reviewNotes } = params;
754
+ // SPEC-1356: implementationReviewDigest is derived automatically by the done
755
+ // gate when omitted (see dod-gates.deriveDoneReviewDigest), so it is never
756
+ // required here.
757
+ const authorityRoute = classifyLifecycleAuthority(params, currentReconciliationInvocationContext());
758
+ if (authorityRoute.route === 'error') {
759
+ return authorityRoute.result;
760
+ }
608
761
  // SPEC-509: auto-detect project from git root when projectPath/projectId are omitted
609
762
  const resolved = await resolveProjectIdOrAutoDetect({
610
763
  projectId: params.projectId,
@@ -619,9 +772,7 @@ export async function handleUpdateStatus(params, server) {
619
772
  const transitionProjectPath = (params.projectPath?.trim()
620
773
  ? effectiveProjectPath
621
774
  : (knowledge?.projectPath ?? effectiveProjectPath)) || undefined;
622
- if (params.reconciliationRequestId ||
623
- params.expectedImplementingTransitionId ||
624
- params.implementationReviewDigest) {
775
+ if (authorityRoute.route === 'reconcile') {
625
776
  return reconcileImplementingSpec({
626
777
  input: params,
627
778
  projectId,
@@ -693,22 +844,9 @@ export async function handleUpdateStatus(params, server) {
693
844
  return idempotentResult;
694
845
  }
695
846
  }
696
- if (newStatus === 'done' && actuals === undefined) {
697
- return {
698
- content: [
699
- {
700
- type: 'text',
701
- text: 'actuals_required: provide measured values for done. Use zero only when a metric is explicitly unavailable; Planu does not estimate provider tokens or costs.',
702
- },
703
- ],
704
- isError: true,
705
- structuredContent: {
706
- error: 'actuals_required',
707
- code: 422,
708
- fixHint: 'Provide actuals with measured values or zero for unavailable metrics. Values are preserved without estimation.',
709
- },
710
- };
711
- }
847
+ // SPEC-1356 (T-A2): actuals are no longer required to reach done. When
848
+ // omitted, synthesizeActuals() below fills in a defaulted record instead
849
+ // of blocking the transition — see synthesizeActuals doc comment.
712
850
  // SPEC-280/SPEC-1122: plan every intermediate state in memory. No status is
713
851
  // persisted until all gates for the complete path have passed.
714
852
  const { plannedStatuses, stepsExecuted } = buildAutoAdvancePlan(spec.status, newStatus);
@@ -849,6 +987,26 @@ export async function handleUpdateStatus(params, server) {
849
987
  sddRoutingGate.gateResults.sddModelRouting = 'forced';
850
988
  }
851
989
  }
990
+ // SPEC-1356 (T-A3): auto-generate traceability-matrix.json before the
991
+ // evidence gate below reads it, so a spec never fails 'done' purely
992
+ // because nobody hand-wrote the traceability artifact. Never overwrites
993
+ // an existing file. Freshness caveat: when autofill actually writes a
994
+ // new file, its evidence has not yet been bound by validate's durable
995
+ // receipt, so we block once and ask the caller to re-run validate
996
+ // rather than risk certifying stale bindings. See evidence-autofill.ts
997
+ // for the full rationale.
998
+ if (plannedStatuses.includes('done') &&
999
+ !shouldSkipGateForExplicitLegacyHarness('evidence')) {
1000
+ const autofillGate = await runTraceabilityAutofillGate({
1001
+ spec,
1002
+ specId,
1003
+ projectId,
1004
+ projectPath: effectiveGatePath,
1005
+ });
1006
+ if (autofillGate) {
1007
+ return autofillGate;
1008
+ }
1009
+ }
852
1010
  // SPEC-1054: BDD/SDD evidence gates. Non-trivial specs must carry
853
1011
  // Discovery before approval, task-plan before implementation, and
854
1012
  // traceability/contract evidence before done.
@@ -867,6 +1025,21 @@ export async function handleUpdateStatus(params, server) {
867
1025
  }
868
1026
  }
869
1027
  }
1028
+ // SPEC-1356 (T-A1): implementationReviewDigest is optional. When the
1029
+ // caller omits it, derive it automatically from the persisted
1030
+ // validation-report.json instead of requiring the caller to hand-compute
1031
+ // a sha256. Failure to derive it fails closed with a clear "run validate
1032
+ // first" message — identical strength to the explicit-digest path.
1033
+ let resolvedImplementationReviewDigest = params.implementationReviewDigest;
1034
+ if (newStatus === 'done' &&
1035
+ resolvedImplementationReviewDigest === undefined &&
1036
+ !shouldSkipGateForExplicitLegacyHarness('evidence')) {
1037
+ const derived = await deriveDoneReviewDigest(specId, projectId);
1038
+ if ('error' in derived) {
1039
+ return derived.error;
1040
+ }
1041
+ resolvedImplementationReviewDigest = derived.digest;
1042
+ }
870
1043
  // ---------------------------------------------------------------------------
871
1044
  // BATCH A (parallel): code-reality + done-gates — independent of each other
872
1045
  // SPEC-441: Code reality check before transitioning to 'implementing'
@@ -887,7 +1060,7 @@ export async function handleUpdateStatus(params, server) {
887
1060
  : Promise.resolve(null),
888
1061
  // Done gates: only relevant for 'done'
889
1062
  newStatus === 'done' && !shouldSkipGateForExplicitLegacyHarness('evidence')
890
- ? checkDoneGates(spec, specId, projectId, effectiveGatePath, params.force, params.forceStatusReason ?? params.reason ?? 'No force reason provided')
1063
+ ? checkDoneGates(spec, specId, projectId, effectiveGatePath, params.force, params.forceStatusReason ?? params.reason ?? 'No force reason provided', undefined, resolvedImplementationReviewDigest)
891
1064
  : Promise.resolve(null),
892
1065
  ]);
893
1066
  // Process code reality result
@@ -969,8 +1142,12 @@ export async function handleUpdateStatus(params, server) {
969
1142
  return specReviewWriteError;
970
1143
  }
971
1144
  }
972
- // Explicit actuals are preserved verbatim. Provider usage and cost are never inferred.
973
- const resolvedActuals = actuals;
1145
+ // Explicit actuals are preserved verbatim. Provider usage and cost are
1146
+ // never inferred — synthesizeActuals only fills gaps the caller left
1147
+ // unmeasured (SPEC-1356 T-A2).
1148
+ const resolvedActuals = newStatus === 'done' || actuals !== undefined
1149
+ ? synthesizeActuals(actuals, new Date().toISOString())
1150
+ : undefined;
974
1151
  // Run only fast, read-only transition checks before persistence.
975
1152
  const implActions = newStatus === 'implementing'
976
1153
  ? await runImplementingActions(projectId, specId, {
@@ -255,7 +255,19 @@ export async function checkReadinessGate(spec, newStatus, forceApprove) {
255
255
  catch (error) {
256
256
  /* reliability-optional: READINESS_SPEC_UNREADABLE — transition fails closed below */
257
257
  reportClassifiedDegradation('READINESS_SPEC_UNREADABLE', error);
258
- return readinessUnavailable('READINESS_SPEC_UNREADABLE', error instanceof Error ? error.message : 'spec.md could not be read');
258
+ // SPEC-1351 (AC4): a missing spec.md (ENOENT) is a distinct, recoverable
259
+ // situation — surface an actionable message instead of the raw fs error
260
+ // ("ENOENT: no such file or directory, open '<path>'") that update_status
261
+ // was previously appending "Transition blocked..." to verbatim.
262
+ const isMissingFile = typeof error === 'object' &&
263
+ error !== null &&
264
+ 'code' in error &&
265
+ error.code === 'ENOENT';
266
+ return readinessUnavailable('READINESS_SPEC_UNREADABLE', isMissingFile
267
+ ? `spec.md is missing at ${spec.specPath}. Recreate the spec via create_spec or restore spec.md from git/backup before retrying this transition.`
268
+ : error instanceof Error
269
+ ? error.message
270
+ : 'spec.md could not be read');
259
271
  }
260
272
  // Guard: mock environments return undefined; unreadable files return non-string
261
273
  if (typeof body !== 'string') {
@@ -34,22 +34,20 @@ export async function handleSpecHealthCheck(input) {
34
34
  content: [{ type: 'text', text: lines.join('\n') }],
35
35
  };
36
36
  }
37
- import { isNativeActive, fastScanSpecs } from '../engine/core-bridge.js';
37
+ import { fastScanSpecsAsync } from '../engine/core-bridge.js';
38
38
  export async function handleHealthCheckAll(input) {
39
- if (isNativeActive()) {
40
- const briefs = fastScanSpecs(input.projectPath);
41
- if (briefs) {
42
- // Sort worst to best
43
- briefs.sort((a, b) => a.healthScore - b.healthScore);
44
- const lines = [
45
- `Health check for all ${briefs.length} specs (sorted worst to best) [NATIVE]:`,
46
- ``,
47
- ...briefs.map((b) => ` ${b.id}: ${b.healthScore}/100`),
48
- ];
49
- return {
50
- content: [{ type: 'text', text: lines.join('\n') }],
51
- };
52
- }
39
+ const { value: briefs, degradationNotice } = await fastScanSpecsAsync(input.projectPath);
40
+ if (briefs) {
41
+ // Sort worst to best
42
+ briefs.sort((a, b) => a.healthScore - b.healthScore);
43
+ const lines = [
44
+ `Health check for all ${briefs.length} specs (sorted worst to best) [NATIVE]:`,
45
+ ``,
46
+ ...briefs.map((b) => ` ${b.id}: ${b.healthScore}/100`),
47
+ ];
48
+ return {
49
+ content: [{ type: 'text', text: lines.join('\n') }],
50
+ };
53
51
  }
54
52
  const projectId = hashProjectPath(input.projectPath);
55
53
  const specs = await specStore.listSpecs(projectId);
@@ -66,6 +64,9 @@ export async function handleHealthCheckAll(input) {
66
64
  ``,
67
65
  ...scores.map((s) => ` ${s.specId}: ${s.total}/100`),
68
66
  ];
67
+ if (degradationNotice !== undefined) {
68
+ lines.push('', degradationNotice);
69
+ }
69
70
  return {
70
71
  content: [{ type: 'text', text: lines.join('\n') }],
71
72
  };
@@ -11,6 +11,22 @@ import { verifyStateFiles } from '../engine/cascade-hooks/state-drift-detector.j
11
11
  import { readFile } from 'node:fs/promises';
12
12
  import { join } from 'node:path';
13
13
  import { getAsyncAnalysisPath } from './create-spec/post-creation.js';
14
+ import { readTransitionLog } from '../storage/transition-log.js';
15
+ /** SPEC-1351 (AC3): collect ghost-spec quarantine events for a project. */
16
+ async function collectGhostAlerts(projectId) {
17
+ try {
18
+ const entries = await readTransitionLog(projectId);
19
+ return entries
20
+ .filter((entry) => entry.eventType === 'ghost_spec_quarantined')
21
+ .map((entry) => ({
22
+ specId: entry.specId,
23
+ reason: entry.reason ?? `Spec ${entry.specId}: spec.md missing on disk.`,
24
+ }));
25
+ }
26
+ catch {
27
+ return [];
28
+ }
29
+ }
14
30
  export async function handleWorkspaceOverview(args) {
15
31
  const result = await buildWorkspaceOverview(args.healthThreshold);
16
32
  if (result.projectCount === 0) {
@@ -188,6 +204,8 @@ export async function handleWorkspaceAlerts(args) {
188
204
  const autopilotFailures = [];
189
205
  // SPEC-781: collect pending analysis alerts across all projects
190
206
  const pendingAnalysisAlerts = [];
207
+ // SPEC-1351 (AC3): collect quarantined ghost-spec alerts across all projects
208
+ const ghostAlerts = [];
191
209
  await Promise.all(projects.map(async (project) => {
192
210
  try {
193
211
  const projectId = hashProjectPath(project.path);
@@ -217,9 +235,21 @@ export async function handleWorkspaceAlerts(args) {
217
235
  catch {
218
236
  // best-effort
219
237
  }
238
+ // SPEC-1351: check for quarantined ghost specs in this project
239
+ try {
240
+ const projectId = hashProjectPath(project.path);
241
+ const ghosts = await collectGhostAlerts(projectId);
242
+ for (const g of ghosts) {
243
+ ghostAlerts.push({ projectPath: project.path, ...g });
244
+ }
245
+ }
246
+ catch {
247
+ // best-effort
248
+ }
220
249
  }));
221
250
  if (alerts.length === 0 &&
222
251
  autopilotFailures.length === 0 &&
252
+ ghostAlerts.length === 0 &&
223
253
  driftAlerts.length === 0 &&
224
254
  pendingAnalysisAlerts.length === 0) {
225
255
  return {
@@ -248,6 +278,14 @@ export async function handleWorkspaceAlerts(args) {
248
278
  lines.push(`| ${shortPath} | ${f.specId} | ${f.hookName} | ${f.error} | ${shortTime} |`);
249
279
  }
250
280
  }
281
+ // SPEC-1351: quarantined ghost-spec alerts
282
+ if (ghostAlerts.length > 0) {
283
+ lines.push(``, `## Quarantined Ghost Specs (spec.md missing on disk)`, ``, `| Project | Spec ID | Recovery Action |`, `|---|---|---|`);
284
+ for (const g of ghostAlerts) {
285
+ const shortPath = g.projectPath.split('/').slice(-2).join('/');
286
+ lines.push(`| ${shortPath} | ${g.specId} | ${g.reason} |`);
287
+ }
288
+ }
251
289
  // SPEC-776: state drift alerts
252
290
  if (driftAlerts.length > 0) {
253
291
  lines.push(``, `## State File Drift`, ``, `| Project | Alert | Fix |`, `|---|---|---|`);
@@ -0,0 +1,34 @@
1
+ import type { Actuals } from './estimation.js';
2
+ /**
3
+ * Caller-supplied actuals on a `done` transition. Every field is optional:
4
+ * missing numeric fields are treated as "not measured" and defaulted to 0 by
5
+ * the done gate; `completedAt`/`notes` default when the whole object is
6
+ * absent. Any field the caller does supply is always stored verbatim.
7
+ */
8
+ export type ActualsInput = Partial<Actuals>;
9
+ /** Injectable process runner so tests never shell out to real git. */
10
+ export type EvidenceAutofillExec = (command: string, args: string[], options: {
11
+ cwd: string;
12
+ }) => Promise<string>;
13
+ export interface AutofillTraceabilityMatrixArgs {
14
+ projectId: string;
15
+ specId: string;
16
+ /** Absolute path to the project working copy; used for `git diff` and file existence checks. */
17
+ projectPath?: string;
18
+ /** Raw spec.md content (frontmatter + body) used to derive criteria, scenarios, and `## Files`. */
19
+ specBody: string;
20
+ /** Overrides the default `execFile`-based git runner. */
21
+ exec?: EvidenceAutofillExec;
22
+ /** Branch to diff against for changed-files detection. Defaults to `main`. */
23
+ baseBranch?: string;
24
+ }
25
+ export type AutofillTraceabilityMatrixResult = {
26
+ written: true;
27
+ path: string;
28
+ rowCount: number;
29
+ } | {
30
+ written: false;
31
+ reason: 'already-exists' | 'no-criteria' | 'write-failed';
32
+ error?: string;
33
+ };
34
+ //# sourceMappingURL=evidence-autofill.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=evidence-autofill.js.map
@@ -30,6 +30,7 @@ export * from './privacy.js';
30
30
  export * from './events.js';
31
31
  export * from './evidence-gates.js';
32
32
  export * from './evidence-index.js';
33
+ export * from './evidence-autofill.js';
33
34
  export * from './analytics.js';
34
35
  export * from './sdd-flow.js';
35
36
  export * from './sdd-model-routing.js';
@@ -31,6 +31,7 @@ export * from './privacy.js';
31
31
  export * from './events.js';
32
32
  export * from './evidence-gates.js';
33
33
  export * from './evidence-index.js';
34
+ export * from './evidence-autofill.js';
34
35
  export * from './analytics.js';
35
36
  export * from './sdd-flow.js';
36
37
  export * from './sdd-model-routing.js';
@@ -415,5 +415,11 @@ export interface SyncReport {
415
415
  specId: string;
416
416
  reason: string;
417
417
  }[];
418
+ /** SPEC-1351: Store entries whose spec.md is missing on disk, quarantined during this sync. */
419
+ ghosts?: {
420
+ specId: string;
421
+ specPath: string;
422
+ reason: string;
423
+ }[];
418
424
  }
419
425
  //# sourceMappingURL=core.d.ts.map
@@ -70,7 +70,8 @@ export interface UpdateStatusInput {
70
70
  arbitratedBy?: string;
71
71
  /** True when implementation drift requires reconcile_spec before done. */
72
72
  reconcileRequired?: boolean;
73
- actuals?: Actuals;
73
+ /** SPEC-1356: all fields optional — missing values are auto-filled by the done gate. */
74
+ actuals?: Partial<Actuals>;
74
75
  autoCreateBranch?: boolean;
75
76
  reviewNotes?: string;
76
77
  force?: boolean;
@@ -96,7 +97,11 @@ export interface UpdateStatusInput {
96
97
  reconciliationRequestId?: string;
97
98
  /** Latest canonical implementing transition receipt expected by reconciliation. */
98
99
  expectedImplementingTransitionId?: string;
99
- /** SHA-256 digest of the independent changes-requested validation report. */
100
+ /**
101
+ * SHA-256 of the exact validation-report.json bytes. Required as approved
102
+ * implementation evidence for done; for review it belongs only to the complete
103
+ * trusted local-MCP reconciliation tuple.
104
+ */
100
105
  implementationReviewDigest?: string;
101
106
  /**
102
107
  * SPEC-769: Force-approve a spec that scored below 70 on the readiness gate.
@@ -90,7 +90,7 @@ export interface StrictPlanuValidationOptions {
90
90
  }
91
91
  export interface UpdateStatusBatchInput {
92
92
  specIds: string[];
93
- status: SpecStatus;
93
+ status: Exclude<SpecStatus, 'done'>;
94
94
  projectId?: string;
95
95
  projectPath?: string;
96
96
  dryRun?: boolean;
@@ -0,0 +1,18 @@
1
+ import type { InteractiveQuestion } from './interactive-question.js';
2
+ /** Outcome of resolving the English-only gate for create_spec's title/description. */
3
+ export type EnglishOnlyGateAction = 'ok' | 'ask' | 'translated' | 'reject';
4
+ /** Result of `resolveEnglishOnlySpecGate`. Shape depends on `action`. */
5
+ export interface EnglishOnlyGateResolution {
6
+ action: EnglishOnlyGateAction;
7
+ /** Set for 'ask' | 'translated' | 'reject' — the language detected in the original input. */
8
+ originalLanguage?: 'es' | 'pt';
9
+ /** Set for 'ask' — the translation clarification questions to relay via AskUserQuestion. */
10
+ questions?: InteractiveQuestion[];
11
+ /** Set for 'reject' — human-readable reason from the underlying language detector. */
12
+ reason?: string;
13
+ /** Set for 'translated' — the English title to use instead of the original. */
14
+ englishTitle?: string;
15
+ /** Set for 'translated' — the English description to use instead of the original. */
16
+ englishDescription?: string;
17
+ }
18
+ //# sourceMappingURL=spec-language-translation.d.ts.map
@@ -0,0 +1,5 @@
1
+ // types/spec-language-translation.ts — SPEC-1342: translation-request shapes for the
2
+ // English-only spec gate. Lets create_spec offer an in-band translation clarification
3
+ // round instead of hard-rejecting non-English title/description input.
4
+ export {};
5
+ //# sourceMappingURL=spec-language-translation.js.map
@@ -87,8 +87,6 @@ export interface RegistryCredentials {
87
87
  user: string;
88
88
  /** Email associated with the account. */
89
89
  email: string;
90
- /** License plan level. */
91
- plan: 'free' | 'pro';
92
90
  /** ISO 8601 token expiration timestamp. */
93
91
  expiresAt: string;
94
92
  }
@@ -10,10 +10,12 @@ export interface GitState {
10
10
  export interface SessionState {
11
11
  lastCiStatus?: string;
12
12
  }
13
+ export type StatusActiveLifecycleStatus = 'implementing' | 'review';
13
14
  export interface StatusSpecSnapshot {
14
15
  active: {
15
16
  id: string;
16
17
  title: string;
18
+ status: StatusActiveLifecycleStatus;
17
19
  } | null;
18
20
  next: {
19
21
  id: string;
@@ -1,5 +1,5 @@
1
1
  /** Event types recorded in the transition log. */
2
- export type TransitionEventType = 'transitioned_to_terminal' | 'frontmatter_resealed' | 'reopen' | 'terminal_drift_detected' | 'transition' | 'handoff.intake' | 'handoff.spec.lock' | 'handoff.review_feedback' | 'handoff.implementation-report' | 'handoff.validation-report' | 'retro_audit' | 'tdd_red_locked' | 'tdd_green_achieved' | 'status_reconciled' | 'housekeeping';
2
+ export type TransitionEventType = 'transitioned_to_terminal' | 'frontmatter_resealed' | 'reopen' | 'terminal_drift_detected' | 'transition' | 'handoff.intake' | 'handoff.spec.lock' | 'handoff.review_feedback' | 'handoff.implementation-report' | 'handoff.validation-report' | 'retro_audit' | 'tdd_red_locked' | 'tdd_green_achieved' | 'status_reconciled' | 'housekeeping' | 'ghost_spec_quarantined';
3
3
  /** SPEC-734: Outcome of a gate execution in a transition. */
4
4
  export type GateOutcome = 'pass' | 'fail' | 'skip' | 'forced';
5
5
  /** SPEC-734: Result of verifying the hash chain integrity of a project's transition log. */
@@ -1,3 +1,5 @@
1
+ import type { ReadinessReport } from './readiness.js';
2
+ import type { SpecQualityReport } from './spec-quality.js';
1
3
  /**
2
4
  * Origin validator that produced the issue.
3
5
  */
@@ -31,11 +33,15 @@ export interface SpecValidationResult {
31
33
  /** Optional summary metrics for callers that want them without re-running scorers. */
32
34
  metrics?: {
33
35
  readinessScore: number;
34
- qualityScore: number;
35
- qualityGrade: 'A' | 'B' | 'C' | 'D' | 'F';
36
+ qualityScore?: number;
37
+ qualityGrade?: 'A' | 'B' | 'C' | 'D' | 'F';
36
38
  bddScenarioCount: number;
37
39
  criteriaCount: number;
38
40
  };
41
+ /** Readiness evidence generated during this validation invocation. */
42
+ readinessReport?: ReadinessReport;
43
+ /** Quality evidence generated during this validation invocation, when available. */
44
+ qualityReport?: SpecQualityReport;
39
45
  }
40
46
  /**
41
47
  * Options for validateSpecFormat().