@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.
- package/CHANGELOG.md +49 -1
- package/dist/cli/commands/spec.js +10 -1
- package/dist/core/spec-validator.js +32 -18
- package/dist/engine/evidence-gates/artifact-reader.d.ts +2 -0
- package/dist/engine/evidence-gates/artifact-reader.js +59 -2
- package/dist/engine/evidence-gates/evidence-autofill.d.ts +10 -0
- package/dist/engine/evidence-gates/evidence-autofill.js +148 -0
- package/dist/engine/evidence-gates/evidence-skeletons.d.ts +19 -0
- package/dist/engine/evidence-gates/evidence-skeletons.js +69 -0
- package/dist/engine/execution/operation-journal.js +10 -4
- package/dist/engine/minimality/policy-loader.js +247 -6
- package/dist/engine/planu-core.darwin-arm64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-arm64.node.sbom.json +14 -14
- package/dist/engine/planu-core.darwin-x64.node.manifest.json +7 -7
- package/dist/engine/planu-core.darwin-x64.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-arm64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-gnu.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-arm64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-arm64-musl.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-x64-gnu.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-gnu.node.sbom.json +14 -14
- package/dist/engine/planu-core.linux-x64-musl.node.manifest.json +7 -7
- package/dist/engine/planu-core.linux-x64-musl.node.sbom.json +14 -14
- package/dist/engine/planu-core.win32-arm64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-arm64-msvc.node.sbom.json +14 -14
- package/dist/engine/planu-core.win32-x64-msvc.node.manifest.json +7 -7
- package/dist/engine/planu-core.win32-x64-msvc.node.sbom.json +14 -14
- package/dist/engine/reverse-engineer/api-detector.js +2 -13
- package/dist/engine/reverse-engineer/complexity-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/config-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/dependency-graph.js +2 -13
- package/dist/engine/reverse-engineer/test-analyzer.js +2 -13
- package/dist/engine/reverse-engineer/walk-ignore.d.ts +3 -0
- package/dist/engine/reverse-engineer/walk-ignore.js +26 -0
- package/dist/engine/spec-format/acceptance-criteria.js +13 -12
- package/dist/engine/spec-format/text-fences.js +20 -2
- package/dist/engine/spec-state-syncer.js +1 -1
- package/dist/engine/timing/budget.js +5 -1
- package/dist/server/routes/specs.js +7 -5
- package/dist/tools/challenge-spec-helpers.d.ts +10 -1
- package/dist/tools/challenge-spec-helpers.js +63 -22
- package/dist/tools/challenge-spec.js +18 -3
- package/dist/tools/check-readiness.js +37 -13
- package/dist/tools/create-spec/spec-builder.d.ts +7 -0
- package/dist/tools/create-spec/spec-builder.js +19 -4
- package/dist/tools/create-spec.js +156 -86
- package/dist/tools/register-spec-tools/core-spec-tools.js +13 -12
- package/dist/tools/sync-spec-state-handler.js +49 -1
- package/dist/tools/update-status/batch.d.ts +6 -2
- package/dist/tools/update-status/batch.js +58 -1
- package/dist/tools/update-status/dod-gates.d.ts +16 -1
- package/dist/tools/update-status/dod-gates.js +191 -1
- package/dist/tools/update-status/done-receipt-verifier.d.ts +8 -0
- package/dist/tools/update-status/done-receipt-verifier.js +37 -2
- package/dist/tools/update-status/evidence-gate.d.ts +4 -0
- package/dist/tools/update-status/evidence-gate.js +67 -2
- package/dist/tools/update-status/file-sync.d.ts +2 -2
- package/dist/tools/update-status/index.d.ts +23 -1
- package/dist/tools/update-status/index.js +201 -24
- package/dist/tools/update-status/transition-guard.js +13 -1
- package/dist/tools/workspace-dashboard-handler.js +38 -0
- package/dist/types/evidence-autofill.d.ts +34 -0
- package/dist/types/evidence-autofill.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/spec/core.d.ts +6 -0
- package/dist/types/spec/inputs.d.ts +7 -2
- package/dist/types/spec-format.d.ts +1 -1
- package/dist/types/transition-log.d.ts +1 -1
- package/dist/types/validation.d.ts +8 -2
- package/package.json +11 -10
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
|
@@ -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 (
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
|
973
|
-
|
|
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
|
-
|
|
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') {
|
|
@@ -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
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/types/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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.
|
|
@@ -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
|
|
35
|
-
qualityGrade
|
|
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().
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.1.1",
|
|
4
4
|
"description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -35,14 +35,14 @@
|
|
|
35
35
|
"packageName": "@planu/core"
|
|
36
36
|
},
|
|
37
37
|
"optionalDependencies": {
|
|
38
|
-
"@planu/core-darwin-arm64": "5.
|
|
39
|
-
"@planu/core-darwin-x64": "5.
|
|
40
|
-
"@planu/core-linux-arm64-gnu": "5.
|
|
41
|
-
"@planu/core-linux-arm64-musl": "5.
|
|
42
|
-
"@planu/core-linux-x64-gnu": "5.
|
|
43
|
-
"@planu/core-linux-x64-musl": "5.
|
|
44
|
-
"@planu/core-win32-arm64-msvc": "5.
|
|
45
|
-
"@planu/core-win32-x64-msvc": "5.
|
|
38
|
+
"@planu/core-darwin-arm64": "5.1.1",
|
|
39
|
+
"@planu/core-darwin-x64": "5.1.1",
|
|
40
|
+
"@planu/core-linux-arm64-gnu": "5.1.1",
|
|
41
|
+
"@planu/core-linux-arm64-musl": "5.1.1",
|
|
42
|
+
"@planu/core-linux-x64-gnu": "5.1.1",
|
|
43
|
+
"@planu/core-linux-x64-musl": "5.1.1",
|
|
44
|
+
"@planu/core-win32-arm64-msvc": "5.1.1",
|
|
45
|
+
"@planu/core-win32-x64-msvc": "5.1.1"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=24.0.0"
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"test": "vitest run",
|
|
78
78
|
"test:watch": "vitest",
|
|
79
79
|
"test:coverage": "vitest run --coverage --testTimeout=60000 --maxWorkers=4",
|
|
80
|
+
"test:release-gate": "vitest run --exclude 'tests/integration/**'",
|
|
80
81
|
"test:integration": "vitest run tests/integration",
|
|
81
82
|
"test:website:unit": "vitest run tests/website tests/scripts/generate-website-proof.test.ts tests/scripts/website-tool-counts.test.ts",
|
|
82
83
|
"test:website:browser": "playwright test --config playwright.website.config.ts",
|
|
@@ -117,7 +118,7 @@
|
|
|
117
118
|
"prepublishOnly": "bash scripts/prepublish-guard.sh",
|
|
118
119
|
"prepack": "pnpm build:ts && pnpm package:size",
|
|
119
120
|
"test:debug": "vitest run --inspect-brk --single-thread",
|
|
120
|
-
"validate": "pnpm build && pnpm typecheck && pnpm verify:typescript-migration && pnpm lint && pnpm format:check && pnpm test",
|
|
121
|
+
"validate": "pnpm build && pnpm typecheck && pnpm verify:typescript-migration && pnpm lint && pnpm format:check && pnpm test:release-gate",
|
|
121
122
|
"docker:build": "docker build -t planu .",
|
|
122
123
|
"docker:run": "docker compose up",
|
|
123
124
|
"publish:blog": "node scripts/publish-blog.mjs",
|
package/planu-native.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "5.
|
|
5
|
+
"version": "5.1.1",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|