@planu/cli 5.3.29 → 5.3.31

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 CHANGED
@@ -1,3 +1,23 @@
1
+ ## [5.3.31] - 2026-08-21
2
+
3
+ ### Bug Fixes
4
+ - fix(spec-1584): exclude .claude/worktrees from Stryker sandbox to survive residual worktrees
5
+
6
+ ### Chores
7
+ - chore(planu): file SPEC-1584 (Stryker worktree dangling-symlink) draft
8
+
9
+
10
+ ## [5.3.30] - 2026-08-20
11
+
12
+ ### Bug Fixes
13
+ - fix(spec-1583): drop redundant access() precheck that TCC-fails website-proof
14
+ - fix(spec-1581): re-verify receipt+log integrity on evidence-only drift
15
+ - fix(spec-1581): done gate rebinds evidence-only traceability drift
16
+
17
+ ### Chores
18
+ - chore(planu): SPEC-1581 done + SPEC-1582 draft filed
19
+
20
+
1
21
  ## [5.3.29] - 2026-08-20
2
22
 
3
23
  ### Bug Fixes
@@ -10,5 +10,5 @@ export declare function checkLifecycleEvidenceGate(args: {
10
10
  criteria: string[];
11
11
  artifacts: EvidenceArtifacts;
12
12
  receiptContext?: EvidenceReceiptContext;
13
- }): EvidenceGateResult;
13
+ }): Promise<EvidenceGateResult>;
14
14
  //# sourceMappingURL=lifecycle-gate.d.ts.map
@@ -1,6 +1,8 @@
1
+ import { isAbsolute, join } from 'node:path';
1
2
  import { evidenceArtifactCollectionDigest, evidenceArtifactDigest } from './artifact-reader.js';
2
3
  import { hasAnyAffirmedMatch, stripMetaAnalysisText, stripNonContractText, } from '../text-signal-boundaries.js';
3
4
  import { createCriterionIdentity } from '../criterion-identity.js';
5
+ import { computeDurableValidationBindings, doneReceiptBindingsDifferOnlyInEvidence, toValidationReceiptBindings, writeValidationArtifactProvenance, } from '../validation/durable-validation.js';
4
6
  const CONTRACT_HINTS = {
5
7
  api: [
6
8
  /\b(?:public|external|rest|http)[ -]?api\b/i,
@@ -26,55 +28,107 @@ function isNonTrivial(spec) {
26
28
  function hasText(value) {
27
29
  return value !== undefined && value.trim().length > 0;
28
30
  }
29
- function provenanceIssues(artifacts, context) {
30
- const receipt = context.receipt;
31
+ function collectProvenanceCandidates(artifacts) {
31
32
  const candidates = [];
32
33
  if (artifacts.discovery) {
33
- candidates.push({
34
- label: 'discovery',
35
- artifact: artifacts.discovery,
36
- expectedDigest: receipt.bindings.artifactDigests.discovery,
37
- });
34
+ candidates.push({ label: 'discovery', artifact: artifacts.discovery, digestKey: 'discovery' });
38
35
  }
39
36
  if (artifacts.taskPlan) {
40
- candidates.push({
41
- label: 'task plan',
42
- artifact: artifacts.taskPlan,
43
- expectedDigest: receipt.bindings.artifactDigests.taskPlan,
44
- });
37
+ candidates.push({ label: 'task plan', artifact: artifacts.taskPlan, digestKey: 'taskPlan' });
45
38
  }
46
39
  if (artifacts.traceabilityMatrix) {
47
40
  candidates.push({
48
41
  label: 'traceability matrix',
49
42
  artifact: artifacts.traceabilityMatrix,
50
- expectedDigest: receipt.bindings.artifactDigests.traceability,
43
+ digestKey: 'traceability',
51
44
  });
52
45
  }
53
46
  for (const artifact of artifacts.contractValidations.slice(0, 1)) {
54
47
  candidates.push({
55
48
  label: 'contract validation collection',
56
49
  artifact,
57
- expectedDigest: receipt.bindings.artifactDigests.contractValidation,
50
+ digestKey: 'contractValidation',
58
51
  actualDigest: evidenceArtifactCollectionDigest(artifacts.contractValidations),
59
52
  });
60
53
  }
61
- return candidates.flatMap(({ label, artifact, expectedDigest, actualDigest }) => {
62
- const provenance = artifact.provenance;
63
- const observedDigest = actualDigest ?? evidenceArtifactDigest(artifact);
64
- const valid = provenance?.receiptId === receipt.receiptId &&
65
- provenance.specId === receipt.bindings.specId &&
66
- provenance.issuerId === receipt.issuer &&
67
- provenance.artifactDigest === observedDigest &&
68
- provenance.artifactDigest === expectedDigest;
69
- return valid
70
- ? []
71
- : [
72
- {
73
- code: 'evidence_provenance_invalid',
74
- message: `${label} is not bound to the current Planu validation receipt.`,
75
- },
76
- ];
77
- });
54
+ return candidates;
55
+ }
56
+ function observedDigestOf(candidate) {
57
+ return candidate.actualDigest ?? evidenceArtifactDigest(candidate.artifact);
58
+ }
59
+ function candidateMatchesReceipt(candidate, receipt) {
60
+ const provenance = candidate.artifact.provenance;
61
+ const observedDigest = observedDigestOf(candidate);
62
+ const expectedDigest = receipt.bindings.artifactDigests[candidate.digestKey];
63
+ return (provenance?.receiptId === receipt.receiptId &&
64
+ provenance.specId === receipt.bindings.specId &&
65
+ provenance.issuerId === receipt.issuer &&
66
+ provenance.artifactDigest === observedDigest &&
67
+ provenance.artifactDigest === expectedDigest);
68
+ }
69
+ function candidateIdentityMatchesReceipt(candidate, receipt) {
70
+ const provenance = candidate.artifact.provenance;
71
+ return (provenance?.receiptId === receipt.receiptId &&
72
+ provenance.specId === receipt.bindings.specId &&
73
+ provenance.issuerId === receipt.issuer);
74
+ }
75
+ async function resolveObservedBindings(receipt, context) {
76
+ if (!context.projectPath || !context.specPath) {
77
+ return undefined;
78
+ }
79
+ try {
80
+ const projectPath = context.projectPath;
81
+ const specPath = isAbsolute(context.specPath)
82
+ ? context.specPath
83
+ : join(projectPath, context.specPath);
84
+ const observed = await computeDurableValidationBindings({
85
+ canonicalProjectId: receipt.bindings.projectId,
86
+ specId: receipt.bindings.specId,
87
+ projectPath,
88
+ specPath,
89
+ });
90
+ return toValidationReceiptBindings(observed.bindings, observed.artifactDigests);
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ }
96
+ async function provenanceIssues(artifacts, context) {
97
+ const receipt = context.receipt;
98
+ const candidates = collectProvenanceCandidates(artifacts);
99
+ const failing = candidates.filter((candidate) => !candidateMatchesReceipt(candidate, receipt));
100
+ if (failing.length === 0) {
101
+ return [];
102
+ }
103
+ const observedBindings = await resolveObservedBindings(receipt, context);
104
+ const reconcilable = observedBindings && doneReceiptBindingsDifferOnlyInEvidence(observedBindings, receipt.bindings)
105
+ ? observedBindings
106
+ : undefined;
107
+ const issues = [];
108
+ let reconciled = false;
109
+ for (const candidate of failing) {
110
+ const canReconcile = reconcilable !== undefined &&
111
+ candidateIdentityMatchesReceipt(candidate, receipt) &&
112
+ observedDigestOf(candidate) === reconcilable.artifactDigests[candidate.digestKey];
113
+ if (!canReconcile) {
114
+ issues.push({
115
+ code: 'evidence_provenance_invalid',
116
+ message: `${candidate.label} is not bound to the current Planu validation receipt.`,
117
+ });
118
+ continue;
119
+ }
120
+ if (!reconciled) {
121
+ await writeValidationArtifactProvenance({
122
+ canonicalProjectId: receipt.bindings.projectId,
123
+ specId: receipt.bindings.specId,
124
+ receiptId: receipt.receiptId,
125
+ issuerId: receipt.issuer,
126
+ artifactDigests: reconcilable.artifactDigests,
127
+ });
128
+ reconciled = true;
129
+ }
130
+ }
131
+ return issues;
78
132
  }
79
133
  export function inferRequiredContractEvidence(spec, criteria) {
80
134
  const text = stripMetaAnalysisText(stripNonContractText([spec.title, ...spec.tags, ...criteria].join('\n')));
@@ -205,13 +259,13 @@ export function checkDoneEvidenceGate(spec, criteria, artifacts) {
205
259
  }
206
260
  return issues;
207
261
  }
208
- export function checkLifecycleEvidenceGate(args) {
262
+ export async function checkLifecycleEvidenceGate(args) {
209
263
  const issues = args.artifacts.invalidArtifacts.map((artifact) => ({
210
264
  code: 'evidence_artifact_invalid',
211
265
  message: artifact,
212
266
  }));
213
267
  if (args.receiptContext) {
214
- issues.push(...provenanceIssues(args.artifacts, args.receiptContext));
268
+ issues.push(...(await provenanceIssues(args.artifacts, args.receiptContext)));
215
269
  }
216
270
  if (args.transition === 'approved') {
217
271
  issues.push(...checkDiscoveryGate(args.spec, args.artifacts));
@@ -4,6 +4,13 @@ export type { DurableValidationBindings } from '../../types/durable-validation.j
4
4
  export { gitSourceTreeDigest } from './validation-source-digest.js';
5
5
  export { createPendingValidationSubmission, createValidationSubmission, validationOperationId, validationSubmissionOperationId, } from './validation-submission.js';
6
6
  export declare function toValidationReceiptBindings(bindings: DurableValidationBindings, artifactDigests: ValidationArtifactDigests): ValidationReceiptBindings;
7
+ /**
8
+ * Evidence artifacts (discovery/task-plan/traceability/contract-validation) may legitimately
9
+ * be regenerated after a receipt was issued (e.g. autopilot skeleton fill). If ONLY the evidence
10
+ * digests changed while every code-facing binding stayed byte-identical, the receipt is rebound
11
+ * to the freshly observed evidence digests instead of being rejected as stale.
12
+ */
13
+ export declare function doneReceiptBindingsDifferOnlyInEvidence(observed: ValidationReceiptBindings | undefined, stored: ValidationReceiptBindings | undefined): boolean;
7
14
  /** Compute immutable validation and lifecycle-artifact bindings without trusting spec metadata. */
8
15
  export declare function computeDurableValidationBindings(input: {
9
16
  readonly canonicalProjectId: string;
@@ -46,6 +46,31 @@ export function toValidationReceiptBindings(bindings, artifactDigests) {
46
46
  artifactDigests,
47
47
  };
48
48
  }
49
+ /**
50
+ * Evidence artifacts (discovery/task-plan/traceability/contract-validation) may legitimately
51
+ * be regenerated after a receipt was issued (e.g. autopilot skeleton fill). If ONLY the evidence
52
+ * digests changed while every code-facing binding stayed byte-identical, the receipt is rebound
53
+ * to the freshly observed evidence digests instead of being rejected as stale.
54
+ */
55
+ export function doneReceiptBindingsDifferOnlyInEvidence(observed, stored) {
56
+ if (!observed?.artifactDigests || !stored?.artifactDigests) {
57
+ return false;
58
+ }
59
+ const codeBindingsMatch = observed.projectId === stored.projectId &&
60
+ observed.specId === stored.specId &&
61
+ observed.sourceDigest === stored.sourceDigest &&
62
+ observed.specDigest === stored.specDigest &&
63
+ observed.policyDigest === stored.policyDigest &&
64
+ observed.toolchainDigest === stored.toolchainDigest &&
65
+ observed.artifactDigests.approvedSpec === stored.artifactDigests.approvedSpec;
66
+ if (!codeBindingsMatch) {
67
+ return false;
68
+ }
69
+ return (observed.artifactDigests.discovery !== stored.artifactDigests.discovery ||
70
+ observed.artifactDigests.taskPlan !== stored.artifactDigests.taskPlan ||
71
+ observed.artifactDigests.traceability !== stored.artifactDigests.traceability ||
72
+ observed.artifactDigests.contractValidation !== stored.artifactDigests.contractValidation);
73
+ }
49
74
  async function digestFile(path, missingLabel) {
50
75
  try {
51
76
  return digest(await readFile(path));
@@ -1,6 +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
+ export { doneReceiptBindingsDifferOnlyInEvidence } from '../../engine/validation/durable-validation.js';
4
4
  export type DoneReceiptVerifier = (identity: {
5
5
  projectId: string;
6
6
  specId: string;
@@ -12,12 +12,5 @@ export type DoneReceiptVerifier = (identity: {
12
12
  freshnessLease?: ValidationFreshnessLease;
13
13
  }>;
14
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;
22
15
  export declare function verifyCurrentDoneReceipt(identity: Parameters<DoneReceiptVerifier>[0]): ReturnType<DoneReceiptVerifier>;
23
16
  //# sourceMappingURL=done-receipt-verifier.d.ts.map
@@ -57,7 +57,8 @@ import { projectDataDir } from '../../storage/base-store.js';
57
57
  import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
58
58
  import { ValidationReceiptStore, LOCAL_VALIDATION_RECEIPT_ISSUER, createValidationReceiptAuthority, readCurrentValidationReceipt, } from '../../engine/validation/validation-receipt.js';
59
59
  import { beginValidationFreshnessBarrier, bindValidationFreshnessLease, captureValidationFreshnessLease, closeValidationFreshnessLease, } from '../../engine/validation/validation-freshness.js';
60
- import { computeDurableValidationBindings, toValidationReceiptBindings, } from '../../engine/validation/durable-validation.js';
60
+ import { computeDurableValidationBindings, doneReceiptBindingsDifferOnlyInEvidence, toValidationReceiptBindings, } from '../../engine/validation/durable-validation.js';
61
+ export { doneReceiptBindingsDifferOnlyInEvidence } from '../../engine/validation/durable-validation.js';
61
62
  export function completedJobPublishesReceipt(job, receiptId) {
62
63
  if (job?.state !== 'completed' || !job.result || typeof job.result !== 'object') {
63
64
  return false;
@@ -70,31 +71,6 @@ export function completedJobPublishesReceipt(job, receiptId) {
70
71
  return (published.receiptId === receiptId &&
71
72
  published.receiptKey === `validation-receipt:${receiptId.slice('sha256:'.length)}`);
72
73
  }
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
- }
98
74
  export async function verifyCurrentDoneReceipt(identity) {
99
75
  const env_1 = { stack: [], error: void 0, hasError: false };
100
76
  try {
@@ -117,11 +93,20 @@ export async function verifyCurrentDoneReceipt(identity) {
117
93
  projectId: identity.projectId,
118
94
  issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
119
95
  });
120
- const verification = await authority.verifyCurrent({
96
+ let verification = await authority.verifyCurrent({
121
97
  ...currentBindings,
122
98
  projectId: identity.projectId,
123
99
  specId: identity.specId,
124
100
  });
101
+ const evidenceOnlyDrift = verification.reason === 'stale-bindings' &&
102
+ doneReceiptBindingsDifferOnlyInEvidence(currentBindings, receipt.bindings);
103
+ if (evidenceOnlyDrift) {
104
+ verification = await authority.verifyCurrent({
105
+ ...receipt.bindings,
106
+ projectId: identity.projectId,
107
+ specId: identity.specId,
108
+ });
109
+ }
125
110
  if (!verification.valid) {
126
111
  return verification;
127
112
  }
@@ -134,12 +134,20 @@ export async function checkLifecycleEvidenceTransitionGate(args) {
134
134
  requiredContractKinds: [],
135
135
  });
136
136
  }
137
- const result = checkLifecycleEvidenceGate({
137
+ const result = await checkLifecycleEvidenceGate({
138
138
  transition: args.transition,
139
139
  spec: args.spec,
140
140
  criteria,
141
141
  artifacts,
142
- ...(receiptResult?.ok ? { receiptContext: { receipt: receiptResult.receipt } } : {}),
142
+ ...(receiptResult?.ok
143
+ ? {
144
+ receiptContext: {
145
+ receipt: receiptResult.receipt,
146
+ ...(args.projectPath === undefined ? {} : { projectPath: args.projectPath }),
147
+ specPath: args.spec.specPath,
148
+ },
149
+ }
150
+ : {}),
143
151
  });
144
152
  if (args.transition === 'done') {
145
153
  const index = await buildSpecEvidenceIndex({
@@ -1,4 +1,4 @@
1
- import type { ValidationArtifactProvenance, ValidationReceipt } from './validation-receipt.js';
1
+ import type { ValidationArtifactDigests, ValidationArtifactProvenance, ValidationReceipt } from './validation-receipt.js';
2
2
  import type { ValidationGateCapability } from './validation-evidence.js';
3
3
  export type EvidenceGateTransition = 'approved' | 'implementing' | 'done';
4
4
  export interface DiscoveryEvidence {
@@ -68,6 +68,16 @@ export interface EvidenceArtifacts {
68
68
  }
69
69
  export interface EvidenceReceiptContext {
70
70
  receipt: ValidationReceipt;
71
+ projectPath?: string;
72
+ specPath?: string;
73
+ }
74
+ export interface ProvenanceCandidate {
75
+ label: string;
76
+ artifact: {
77
+ provenance?: ValidationArtifactProvenance;
78
+ };
79
+ digestKey: keyof ValidationArtifactDigests;
80
+ actualDigest?: string;
71
81
  }
72
82
  export interface EvidenceGateIssue {
73
83
  code: 'discovery_missing' | 'discovery_unresolved_questions' | 'task_plan_missing' | 'task_plan_uncovered_criteria' | 'traceability_missing' | 'traceability_uncovered_criteria' | 'traceability_incomplete_rows' | 'contract_validation_missing' | 'contract_validation_failed' | 'evidence_provenance_invalid' | 'done_drift_uncovered_criteria' | 'done_drift_stale_evidence' | 'done_drift_unapproved_scope' | 'evidence_artifact_invalid' | 'SUPERSEDED_RECONCILIATION_EVIDENCE';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "5.3.29",
3
+ "version": "5.3.31",
4
4
  "description": "Planu — MCP Server for Spec Driven Development. Cross-platform (Linux/macOS/Windows, x64/arm64).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/planu-plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dev.planu.cli",
3
3
  "displayName": "Planu — Spec Driven Development",
4
4
  "description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
5
- "version": "5.3.29",
5
+ "version": "5.3.31",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": ["npx", "@planu/cli@latest"],
8
8
  "packageName": "@planu/cli",