@planu/cli 5.3.51 → 5.3.55

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 (40) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/dist/cli/commands/package-handoff.d.ts +3 -0
  3. package/dist/cli/commands/package-handoff.js +43 -0
  4. package/dist/cli/commands/spec.js +3 -8
  5. package/dist/cli/commands/status.d.ts +52 -1
  6. package/dist/cli/commands/status.js +39 -35
  7. package/dist/cli/router.js +5 -2
  8. package/dist/engine/git/canonical-branch.d.ts +2 -0
  9. package/dist/engine/git/canonical-branch.js +31 -0
  10. package/dist/engine/handoff-artifacts/schemas.d.ts +2 -0
  11. package/dist/engine/handoff-artifacts/schemas.js +1 -0
  12. package/dist/engine/human-summary.js +1 -1
  13. package/dist/engine/lifecycle-reconciliation.js +3 -0
  14. package/dist/engine/scope-boundaries/contradiction-checker.js +29 -5
  15. package/dist/engine/sdd-model-routing.js +7 -2
  16. package/dist/engine/spec-format/lean-spec-generator.js +1 -1
  17. package/dist/engine/staleness/stale-implementing.js +19 -58
  18. package/dist/engine/text-signal-boundaries.js +3 -1
  19. package/dist/engine/validator/spec-compliance-runner.d.ts +1 -0
  20. package/dist/engine/validator/spec-compliance-runner.js +13 -6
  21. package/dist/engine/validator/validation-report-writer.js +1 -0
  22. package/dist/engine/workflow-validator/worktree-protocol.js +45 -36
  23. package/dist/storage/transition-log.d.ts +2 -14
  24. package/dist/storage/transition-log.js +84 -45
  25. package/dist/tools/challenge-spec/challenge-report.js +42 -11
  26. package/dist/tools/challenge-spec/scenarios-utils.js +9 -2
  27. package/dist/tools/generate-orchestration-script.js +2 -1
  28. package/dist/tools/register-platform-tools/design-stack-tools.js +1 -1
  29. package/dist/tools/suggest-tooling/orchestration-generator.js +2 -2
  30. package/dist/tools/sync-spec-state-handler.js +20 -7
  31. package/dist/tools/update-status/dod-gates.js +36 -6
  32. package/dist/tools/update-status/file-sync.d.ts +1 -0
  33. package/dist/tools/update-status/file-sync.js +45 -13
  34. package/dist/tools/update-status/index.js +12 -1
  35. package/dist/tools/update-status/transition-guard.d.ts +1 -0
  36. package/dist/tools/update-status/transition-guard.js +50 -4
  37. package/dist/tools/validate.js +5 -2
  38. package/dist/types/handoff-artifacts.d.ts +2 -0
  39. package/package.json +20 -20
  40. package/planu-plugin.json +1 -1
@@ -17,7 +17,7 @@ import { formatKeyValue } from '../output-formatter.js';
17
17
  import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
18
18
  import { verifyCurrentDoneReceipt } from './done-receipt-verifier.js';
19
19
  import { projectDataDir } from '../../storage/base-store.js';
20
- import { readTransitionLog } from '../../storage/transition-log.js';
20
+ import { readTransitionLog, transitionLogPath } from '../../storage/transition-log.js';
21
21
  import { ImplementationReviewV1Schema, ReviewFeedbackV1Schema, ValidationReportV1Schema, } from '../../engine/handoff-artifacts/schemas.js';
22
22
  import { renderSchemaSkeleton } from '../../engine/handoff-artifacts/schema-skeleton.js';
23
23
  export { completedJobPublishesReceipt } from './done-receipt-verifier.js';
@@ -30,6 +30,11 @@ function lazyTemplate(render) {
30
30
  return cached;
31
31
  };
32
32
  }
33
+ function schemaBlockers(errors) {
34
+ return errors
35
+ .filter((issue) => issue.code !== 'ARTIFACT_NOT_FOUND' && issue.code !== 'INVALID_JSON')
36
+ .map((issue) => `${issue.path}: ${issue.message}`);
37
+ }
33
38
  export const reviewFeedbackTemplate = lazyTemplate(() => renderSchemaSkeleton(ReviewFeedbackV1Schema, {
34
39
  overrides: {
35
40
  schema_version: HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION,
@@ -525,6 +530,7 @@ export async function readApprovedValidationReportGate(specId, projectId, force)
525
530
  error: 'validation_report_invalid',
526
531
  message: 'The validation-report artifact is malformed or uses an obsolete schema. Re-run validate so Planu can generate reviewer evidence.',
527
532
  gates: [],
533
+ blockers: schemaBlockers(result.errors),
528
534
  fixHint: 'Re-run validate for this spec. The report must include reviewer evidence and passing gates.',
529
535
  template: validationReportTemplate(),
530
536
  }),
@@ -602,7 +608,7 @@ export async function checkSpecReviewGate(specId, projectId, _forceApprove) {
602
608
  message: firstErr?.code === 'ARTIFACT_NOT_FOUND'
603
609
  ? 'No spec review feedback exists for this spec. An independent planu-spec-reviewer must review the spec and write its evidence before approval.'
604
610
  : 'Spec review feedback is malformed or uses an obsolete schema.',
605
- blockers: [],
611
+ blockers: schemaBlockers(result.errors),
606
612
  fixHint: firstErr?.code === 'ARTIFACT_NOT_FOUND'
607
613
  ? 'Have planu-spec-reviewer write review_feedback.md (ReviewFeedbackV1) into the handoff store, then retry update_status(approved).'
608
614
  : 'Have planu-spec-reviewer rewrite review_feedback.md as valid ReviewFeedbackV1 evidence, then retry approval.',
@@ -666,7 +672,7 @@ export async function checkImplementationReviewGate(specId, projectId, _force) {
666
672
  message: firstErr?.code === 'ARTIFACT_NOT_FOUND'
667
673
  ? 'No implementation review exists for this spec. An independent planu-implementation-reviewer must review the implementation and write its own evidence before done.'
668
674
  : 'Implementation review evidence is malformed or uses an obsolete schema.',
669
- blockers: [],
675
+ blockers: schemaBlockers(result.errors),
670
676
  fixHint: firstErr?.code === 'ARTIFACT_NOT_FOUND'
671
677
  ? 'Have planu-implementation-reviewer write implementation_review.json (ImplementationReviewV1) into the handoff store, then retry update_status(done).'
672
678
  : 'Have planu-implementation-reviewer rewrite implementation_review.json as valid ImplementationReviewV1 evidence, then retry done.',
@@ -704,7 +710,22 @@ export async function checkImplementationReviewGate(specId, projectId, _force) {
704
710
  fixHint: 'Have planu-implementation-reviewer (with the recognized identity) write the review artifact, then retry done.',
705
711
  });
706
712
  }
707
- const implementerActor = await findImplementingActor(specId, projectId);
713
+ let implementerActor;
714
+ try {
715
+ implementerActor = await findImplementingActor(specId, projectId);
716
+ }
717
+ catch (err) {
718
+ /* reliability-optional: TRANSITION_LOG_GATE_READ — typed gate error blocks done */
719
+ reportClassifiedDegradation('TRANSITION_LOG_GATE_READ', err);
720
+ return implementationReviewGateError({
721
+ specId,
722
+ error: 'implementing_actor_unreadable',
723
+ message: `Could not read the transition log to find the implementing actor: ${err instanceof Error ? err.message : String(err)}`,
724
+ blockers: [],
725
+ fixHint: 'Fix the transition log so it can be read, then retry update_status(done).',
726
+ artifactPath: transitionLogPath(projectId),
727
+ });
728
+ }
708
729
  if (implementerActor !== undefined && implementerActor === review.reviewer.agent) {
709
730
  return implementationReviewGateError({
710
731
  specId,
@@ -739,7 +760,8 @@ async function findImplementingActor(specId, projectId) {
739
760
  return undefined;
740
761
  }
741
762
  function implementationReviewGateError(args) {
742
- const artifactPath = `external Planu project data: handoffs/${args.specId}/implementation_review.json`;
763
+ const artifactPath = args.artifactPath ??
764
+ `external Planu project data: handoffs/${args.specId}/implementation_review.json`;
743
765
  return {
744
766
  content: [
745
767
  {
@@ -766,6 +788,7 @@ function implementationReviewGateError(args) {
766
788
  }
767
789
  function validationReportGateError(args) {
768
790
  const artifactPath = `external Planu project data: handoffs/${args.specId}/validation-report.json`;
791
+ const blockerCount = args.blockers && args.blockers.length > 0 ? args.blockers.length : undefined;
769
792
  return {
770
793
  content: [
771
794
  {
@@ -776,6 +799,8 @@ function validationReportGateError(args) {
776
799
  artifactPath,
777
800
  failedGates: args.gates.filter((gate) => !gate.passed).length,
778
801
  totalGates: args.gates.length,
802
+ blockers: blockerCount,
803
+ firstBlocker: args.blockers?.[0],
779
804
  fixHint: args.fixHint,
780
805
  }, 'Validation report gate failed'),
781
806
  },
@@ -784,7 +809,12 @@ function validationReportGateError(args) {
784
809
  structuredContent: {
785
810
  error: args.error,
786
811
  code: 422,
787
- context: { specId: args.specId, artifactPath, gates: args.gates },
812
+ context: {
813
+ specId: args.specId,
814
+ artifactPath,
815
+ gates: args.gates,
816
+ ...(args.blockers ? { blockers: args.blockers } : {}),
817
+ },
788
818
  fixHint: args.fixHint,
789
819
  ...(args.template ? { template: args.template } : {}),
790
820
  },
@@ -12,6 +12,7 @@ export interface FrontmatterSyncWarning {
12
12
  }
13
13
  export declare function syncSpecFiles(updatedSpec: Spec, _currentStatus: SpecStatus, newStatus: SpecStatus, projectPath?: string, qualityWarnings?: readonly string[]): Promise<{
14
14
  warning?: FrontmatterSyncWarning;
15
+ title?: string;
15
16
  }>;
16
17
  export declare function tryReconcile(newStatus: string, specId: string, projectId: string): Promise<string | null>;
17
18
  /**
@@ -75,6 +75,24 @@ async function autoCaptureLessonEstimation(spec, actuals, accuracy, projectId, p
75
75
  }
76
76
  const SPEC_BODY_SHRINK_ABSOLUTE_THRESHOLD = 200;
77
77
  const SPEC_BODY_SHRINK_RATIO_THRESHOLD = 0.05;
78
+ const SPEC_ARTIFACT_ABSENT_CODE = 'SPEC_ARTIFACT_ABSENT';
79
+ function specArtifactAbsentError() {
80
+ const error = new Error('Spec artifact is absent');
81
+ error.code = SPEC_ARTIFACT_ABSENT_CODE;
82
+ return error;
83
+ }
84
+ function isAbsentSpecArtifactFailure(err, resolvedArtifactPath) {
85
+ if (!(err instanceof Error)) {
86
+ return false;
87
+ }
88
+ const code = err.code;
89
+ if (code === SPEC_ARTIFACT_ABSENT_CODE) {
90
+ return true;
91
+ }
92
+ return (code === 'ENOENT' &&
93
+ resolvedArtifactPath !== undefined &&
94
+ err.path === resolvedArtifactPath);
95
+ }
78
96
  async function resolveSpecPathForTransition(spec, projectPath) {
79
97
  if (!projectPath) {
80
98
  return spec.specPath;
@@ -100,11 +118,16 @@ async function resolveSpecPathForTransition(spec, projectPath) {
100
118
  isAbsolute(relativeCandidate)) {
101
119
  throw new Error('SPEC_PATH_RELOCALIZATION_FAILED: specPath escapes the selected root');
102
120
  }
103
- const [rootRealpath, candidateRealpath, candidateStat] = await Promise.all([
104
- realpath(selectedRoot),
121
+ const rootRealpath = await realpath(selectedRoot);
122
+ const [candidateRealpath, candidateStat] = await Promise.all([
105
123
  realpath(candidate),
106
124
  lstat(candidate),
107
- ]);
125
+ ]).catch((err) => {
126
+ if (err instanceof Error && err.code === 'ENOENT') {
127
+ throw specArtifactAbsentError();
128
+ }
129
+ throw err;
130
+ });
108
131
  const realRelative = relative(rootRealpath, candidateRealpath);
109
132
  if (candidateStat.isSymbolicLink() ||
110
133
  !candidateStat.isFile() ||
@@ -138,15 +161,20 @@ function assertSpecBodyIntegrity(args) {
138
161
  }
139
162
  export async function syncSpecFiles(updatedSpec, _currentStatus, newStatus, projectPath, qualityWarnings = []) {
140
163
  let warning;
164
+ let refreshedTitle;
165
+ let transitionSpecPath;
141
166
  if (updatedSpec.specPath) {
142
167
  try {
143
- const transitionSpecPath = await resolveSpecPathForTransition(updatedSpec, projectPath);
168
+ transitionSpecPath = await resolveSpecPathForTransition(updatedSpec, projectPath);
144
169
  const specContent = await readFile(transitionSpecPath, 'utf-8');
145
- if (projectPath) {
146
- const parsedId = parseFrontmatter(specContent).metadata.id;
147
- if (parsedId !== updatedSpec.id) {
148
- throw new Error('SPEC_PATH_RELOCALIZATION_FAILED: spec frontmatter ID does not match');
149
- }
170
+ const parsedMetadata = parseFrontmatter(specContent).metadata;
171
+ if (projectPath && parsedMetadata.id !== updatedSpec.id) {
172
+ throw new Error('SPEC_PATH_RELOCALIZATION_FAILED: spec frontmatter ID does not match');
173
+ }
174
+ if (typeof parsedMetadata.title === 'string' &&
175
+ parsedMetadata.title.trim() !== '' &&
176
+ parsedMetadata.title !== updatedSpec.title) {
177
+ refreshedTitle = parsedMetadata.title;
150
178
  }
151
179
  const { updateFrontmatterField } = await import('../../engine/frontmatter-parser.js');
152
180
  let updatedContent = updateFrontmatterField(specContent, 'status', newStatus);
@@ -191,19 +219,23 @@ export async function syncSpecFiles(updatedSpec, _currentStatus, newStatus, proj
191
219
  }
192
220
  }
193
221
  catch (err) {
194
- /* reliability-optional: SPEC_FRONTMATTER_SYNC warning is returned to the caller */
222
+ if (newStatus === 'discarded' && isAbsentSpecArtifactFailure(err, transitionSpecPath)) {
223
+ return {};
224
+ }
195
225
  const msg = redactFailureMessage(err);
196
- // SPEC-698: log AND surface the warning so the user sees it in the tool response
197
226
  reportClassifiedDegradation('SPEC_FRONTMATTER_SYNC', err);
198
227
  warning = { specId: updatedSpec.id, specPath: updatedSpec.specPath, reason: msg };
199
228
  }
200
229
  }
201
- else {
230
+ else if (newStatus !== 'discarded') {
202
231
  const reason = 'specPath not set on spec — spec.md not updated on disk. Run reconcile_spec to re-link this spec to its file.';
203
232
  console.warn(`[Planu] update_status: ${reason} (${updatedSpec.id})`);
204
233
  warning = { specId: updatedSpec.id, specPath: null, reason };
205
234
  }
206
- return warning ? { warning } : {};
235
+ if (warning) {
236
+ return { warning };
237
+ }
238
+ return refreshedTitle ? { title: refreshedTitle } : {};
207
239
  }
208
240
  /** Flip all ` done: false` lines to ` done: true` in spec content.
209
241
  * Safe: in lean YAML spec, `done: false` only appears inside criteria items. */
@@ -9,7 +9,7 @@ import { cascadeCheck } from '../../engine/spec-versioner.js';
9
9
  import { runComplianceGates } from '../update-status-convention-gate.js';
10
10
  import { runDoneActions, runDoneSideEffects, runImplementingActions, runImplementingSideEffects, } from '../update-status-actions.js';
11
11
  import { compactObj } from '../../engine/compact-obj.js';
12
- import { checkTransition, checkDorGate, checkAmbiguityGate, checkReadinessGate, checkChallengeGate, checkImplementationDependencyGate, resolveAutoAdvanceSteps, isReverseTransition, validateReverseTransition, } from './transition-guard.js';
12
+ import { checkTransition, checkDorGate, checkAmbiguityGate, checkReadinessGate, checkChallengeGate, checkImplementationDependencyGate, checkSpecArtifactCommittedGate, resolveAutoAdvanceSteps, isReverseTransition, validateReverseTransition, } from './transition-guard.js';
13
13
  import { checkApprovedDepGate } from '../../engine/dep-guard/index.js';
14
14
  import { checkApprovalGate } from '../../engine/approval-workflow.js';
15
15
  import * as approvalStore from '../../storage/approval-store.js';
@@ -667,6 +667,10 @@ async function commitTransitionWithNarrowLock(args) {
667
667
  const updatedSpec = transitionRecord.spec;
668
668
  const syncResult = await syncSpecFiles(updatedSpec, args.originalStatus, args.newStatus, args.projectPath, args.qualityWarnings);
669
669
  if (!syncResult.warning) {
670
+ if (syncResult.title) {
671
+ updatedSpec.title = syncResult.title;
672
+ await specStore.updateSpec(args.projectId, args.specId, { title: syncResult.title });
673
+ }
670
674
  return { ok: true, transitionRecord, updatedSpec };
671
675
  }
672
676
  try {
@@ -959,6 +963,10 @@ export async function handleUpdateStatus(params, server) {
959
963
  if (dependencyGate) {
960
964
  return dependencyGate;
961
965
  }
966
+ const specArtifactGate = await checkSpecArtifactCommittedGate(spec, 'implementing', transitionProjectPath);
967
+ if (specArtifactGate) {
968
+ return specArtifactGate;
969
+ }
962
970
  }
963
971
  // SPEC-964: Challenge gate — block 'review' if challenge_spec was never run
964
972
  const challengeGateStatus = plannedStatuses.includes('review') ? 'review' : newStatus;
@@ -1344,6 +1352,9 @@ export async function handleUpdateStatus(params, server) {
1344
1352
  return persistence.error;
1345
1353
  }
1346
1354
  const { transitionRecord, updatedSpec } = persistence;
1355
+ if (updatedSpec.title && updatedSpec.title !== spec.title) {
1356
+ spec.title = updatedSpec.title;
1357
+ }
1347
1358
  const frontmatterSyncWarnings = [];
1348
1359
  const transitionId = transitionRecord.transitionId;
1349
1360
  const committedAt = transitionRecord.timestamp;
@@ -60,6 +60,7 @@ export declare function checkImplementationDependencyGate(spec: Spec, allSpecs:
60
60
  * Fails closed when the spec cannot be scored.
61
61
  */
62
62
  export declare function checkAmbiguityGate(spec: Spec, newStatus: SpecStatus): Promise<ToolResult | null>;
63
+ export declare function checkSpecArtifactCommittedGate(spec: Spec, newStatus: SpecStatus, projectPath: string | undefined): Promise<ToolResult | null>;
63
64
  /**
64
65
  * SPEC-769/SPEC-492: Readiness gate — block 'review'/'approved' when spec
65
66
  * quality is too low or the persisted spec document is not English.
@@ -1,5 +1,6 @@
1
1
  // tools/update-status/transition-guard.ts — Valid state transitions and DoR gate
2
2
  import { readFile } from 'node:fs/promises';
3
+ import { isAbsolute, join } from 'node:path';
3
4
  import { ti } from '../../i18n/index.js';
4
5
  import { validateDoR } from '../../engine/dor-dod.js';
5
6
  import { dispatchFeedbackEvent } from '../learn.js';
@@ -232,6 +233,47 @@ export async function checkAmbiguityGate(spec, newStatus) {
232
233
  },
233
234
  };
234
235
  }
236
+ export async function checkSpecArtifactCommittedGate(spec, newStatus, projectPath) {
237
+ if (newStatus !== 'implementing' || !projectPath || !spec.specPath) {
238
+ return null;
239
+ }
240
+ const absoluteSpecPath = isAbsolute(spec.specPath)
241
+ ? spec.specPath
242
+ : join(projectPath, spec.specPath);
243
+ try {
244
+ const { git } = await import('../git/git-helpers.js');
245
+ const { stdout } = await git(projectPath, ['status', '--porcelain', '--', absoluteSpecPath]);
246
+ if (stdout.trim().length === 0) {
247
+ return null;
248
+ }
249
+ }
250
+ catch (error) {
251
+ reportClassifiedDegradation('SPEC_ARTIFACT_GIT_STATUS_UNAVAILABLE', error);
252
+ return null;
253
+ }
254
+ const fixHint = `Commit ${absoluteSpecPath} (git add "${absoluteSpecPath}" && git commit) then retry update_status(specId: "${spec.id}", status: "implementing").`;
255
+ return {
256
+ content: [
257
+ {
258
+ type: 'text',
259
+ text: formatKeyValue({
260
+ error: 'SPEC_ARTIFACT_UNCOMMITTED',
261
+ message: `spec.md for ${spec.id} has uncommitted changes. The approved contract on disk must match what git holds before an implementer is dispatched.`,
262
+ specId: spec.id,
263
+ specPath: absoluteSpecPath,
264
+ fixHint,
265
+ }),
266
+ },
267
+ ],
268
+ isError: true,
269
+ structuredContent: {
270
+ error: 'SPEC_ARTIFACT_UNCOMMITTED',
271
+ specId: spec.id,
272
+ specPath: absoluteSpecPath,
273
+ fixHint,
274
+ },
275
+ };
276
+ }
235
277
  /**
236
278
  * SPEC-769/SPEC-492: Readiness gate — block 'review'/'approved' when spec
237
279
  * quality is too low or the persisted spec document is not English.
@@ -511,6 +553,7 @@ function gateUnavailableResult(code, message) {
511
553
  function readinessUnavailable(code, message, qualityWarnings = []) {
512
554
  return { blockResult: gateUnavailableResult(code, message), qualityWarnings };
513
555
  }
556
+ const CHALLENGE_RESOLUTION_INSTRUCTIONS = 'Add a "### Challenge Resolution" subsection to the spec\'s spec.md with one "- " bullet per unresolved finding, each bullet containing that finding\'s exact scenario text plus your mitigation, then re-run challenge_spec to re-derive the report.';
514
557
  /**
515
558
  * SPEC-964: Challenge gate — block 'review' if challenge_spec was never run
516
559
  * or does not contain enough explicit resolution evidence.
@@ -584,7 +627,7 @@ export function checkChallengeGate(spec, newStatus) {
584
627
  content: [
585
628
  {
586
629
  type: 'text',
587
- text: `Challenge gate blocked: ${String(unresolvedCriticalCount)} unresolved critical finding(s) remain (addressed ${String(addressedCount)}/${String(requiredCount)} required). Record mitigation or accepted-risk evidence for every critical finding before retrying.`,
630
+ text: `Challenge gate blocked: ${String(unresolvedCriticalCount)} unresolved critical finding(s) remain (addressed ${String(addressedCount)}/${String(requiredCount)} required). Record resolution evidence for every critical finding before retrying.`,
588
631
  },
589
632
  ],
590
633
  isError: true,
@@ -595,7 +638,8 @@ export function checkChallengeGate(spec, newStatus) {
595
638
  requiredCount,
596
639
  unresolvedCount,
597
640
  unresolvedCriticalCount,
598
- fixHint: `Resolve every critical challenge_spec finding with concrete evidence, then retry update_status(specId="${spec.id}", status="review").`,
641
+ unresolvedCriticalIdentities: gateState.unresolvedCriticalIdentities,
642
+ fixHint: `${CHALLENGE_RESOLUTION_INSTRUCTIONS} Then retry update_status(specId="${spec.id}", status="review").`,
599
643
  },
600
644
  };
601
645
  }
@@ -604,7 +648,7 @@ export function checkChallengeGate(spec, newStatus) {
604
648
  content: [
605
649
  {
606
650
  type: 'text',
607
- text: `Challenge gate blocked: only ${String(addressedCount)} scenario(s) have explicit resolution evidence (minimum ${String(requiredCount)} required). Record mitigation or accepted-risk evidence for discovered findings before retrying.`,
651
+ text: `Challenge gate blocked: only ${String(addressedCount)} scenario(s) have explicit resolution evidence (minimum ${String(requiredCount)} required). Record resolution evidence for discovered findings before retrying.`,
608
652
  },
609
653
  ],
610
654
  isError: true,
@@ -613,7 +657,9 @@ export function checkChallengeGate(spec, newStatus) {
613
657
  code: 'INSUFFICIENT_CHALLENGES',
614
658
  addressedCount,
615
659
  requiredCount,
616
- fixHint: `Resolve challenge_spec findings with concrete evidence, then retry update_status(specId="${spec.id}", status="review").`,
660
+ unresolvedCount,
661
+ unresolvedIdentities: gateState.unresolvedIdentities,
662
+ fixHint: `${CHALLENGE_RESOLUTION_INSTRUCTIONS} Then retry update_status(specId="${spec.id}", status="review").`,
617
663
  },
618
664
  };
619
665
  }
@@ -688,8 +688,11 @@ function buildExecutableCoverage(specCompliance) {
688
688
  const matches = specCompliance.scenarios
689
689
  .filter((scenario) => scenario.verdict === 'pass')
690
690
  .map((scenario) => scenario.title);
691
+ const indeterminate = specCompliance.scenarios
692
+ .filter((scenario) => scenario.verdict !== 'pass' && scenario.unverifiable)
693
+ .map((scenario) => scenario.title);
691
694
  const missing = specCompliance.scenarios
692
- .filter((scenario) => scenario.verdict !== 'pass')
695
+ .filter((scenario) => scenario.verdict !== 'pass' && !scenario.unverifiable)
693
696
  .map((scenario) => scenario.title);
694
697
  return {
695
698
  score: specCompliance.score,
@@ -697,7 +700,7 @@ function buildExecutableCoverage(specCompliance) {
697
700
  fieldsTotal: specCompliance.scenarios.length,
698
701
  matches,
699
702
  missing,
700
- indeterminate: [],
703
+ indeterminate,
701
704
  };
702
705
  }
703
706
  function buildEffectiveValidationResult(result, specCompliance) {
@@ -86,6 +86,7 @@ export interface ValidationReportV1 {
86
86
  title: string;
87
87
  verdict: 'pass' | 'fail' | 'missing';
88
88
  evidence: string[];
89
+ unverifiable?: true;
89
90
  }[];
90
91
  };
91
92
  minimalityReport?: MinimalImplementationReport;
@@ -133,6 +134,7 @@ export type ReconciliationSpecSync = (spec: Spec, currentStatus: 'implementing'
133
134
  warning?: {
134
135
  reason: string;
135
136
  };
137
+ title?: string;
136
138
  }>;
137
139
  export interface ArtifactPayloadMap {
138
140
  intake: IntakeV1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "5.3.51",
3
+ "version": "5.3.55",
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",
@@ -128,53 +128,53 @@
128
128
  ],
129
129
  "license": "SEE LICENSE IN LICENSE",
130
130
  "dependencies": {
131
- "@anthropic-ai/sdk": "^0.115.0",
132
- "@hono/node-server": "2.1.0",
131
+ "@anthropic-ai/sdk": "^0.120.0",
132
+ "@hono/node-server": "2.1.1",
133
133
  "@modelcontextprotocol/sdk": "^1.30.0",
134
134
  "glob": "^13.0.6",
135
135
  "yaml": "^2.9.0",
136
136
  "zod": "^4.4.3"
137
137
  },
138
138
  "devDependencies": {
139
- "@axe-core/playwright": "4.12.1",
140
- "@commitlint/cli": "^21.2.1",
141
- "@commitlint/config-conventional": "^21.2.0",
139
+ "@axe-core/playwright": "4.13.0",
140
+ "@commitlint/cli": "^21.2.2",
141
+ "@commitlint/config-conventional": "^21.2.2",
142
142
  "@eslint/js": "^10.0.1",
143
143
  "@lhci/cli": "0.15.1",
144
- "@noble/hashes": "2.2.0",
144
+ "@noble/hashes": "2.3.0",
145
145
  "@playwright/test": "1.62.1",
146
- "@scure/base": "2.2.0",
146
+ "@scure/base": "2.3.0",
147
147
  "@secretlint/secretlint-rule-no-homedir": "^13.0.4",
148
148
  "@secretlint/secretlint-rule-preset-recommend": "^13.0.4",
149
149
  "@stryker-mutator/core": "^9.6.1",
150
150
  "@stryker-mutator/vitest-runner": "^9.6.1",
151
- "@supabase/supabase-js": "^2.112.0",
152
- "@types/node": "^26.1.2",
151
+ "@supabase/supabase-js": "^2.112.4",
152
+ "@types/node": "^26.2.0",
153
153
  "@types/qrcode": "1.5.6",
154
154
  "@typescript/native": "npm:typescript@^7.0.2",
155
155
  "@vitejs/plugin-vue": "^6.0.8",
156
- "@vitest/coverage-v8": "^4.1.10",
156
+ "@vitest/coverage-v8": "^4.1.11",
157
157
  "@vue/test-utils": "^2.4.11",
158
- "eslint": "10.8.0",
158
+ "eslint": "10.9.0",
159
159
  "eslint-config-prettier": "^10.1.8",
160
160
  "eslint-import-resolver-typescript": "^4.4.5",
161
161
  "eslint-plugin-import": "^2.32.0",
162
- "happy-dom": "^20.11.1",
162
+ "happy-dom": "^20.11.6",
163
163
  "husky": "^9.1.7",
164
- "javascript-obfuscator": "^5.5.0",
164
+ "javascript-obfuscator": "^5.6.0",
165
165
  "jiti": "2.7.0",
166
- "knip": "^6.31.0",
166
+ "knip": "^6.32.2",
167
167
  "lint-staged": "^17.3.0",
168
168
  "madge": "^8.0.0",
169
169
  "prettier": "^3.9.6",
170
170
  "qrcode": "1.5.4",
171
171
  "secretlint": "^13.0.4",
172
- "tsc-alias": "^1.9.1",
172
+ "tsc-alias": "^1.9.2",
173
173
  "type-coverage": "^2.30.1",
174
174
  "typescript": "npm:@typescript/typescript6@^6.0.2",
175
- "typescript-eslint": "^8.66.0",
176
- "vite": "^8.2.0",
177
- "vitest": "^4.1.10",
178
- "vue": "^3.5.40"
175
+ "typescript-eslint": "^8.67.0",
176
+ "vite": "^8.2.2",
177
+ "vitest": "^4.1.11",
178
+ "vue": "^3.5.41"
179
179
  }
180
180
  }
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.51",
5
+ "version": "5.3.55",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": ["npx", "@planu/cli@latest"],
8
8
  "packageName": "@planu/cli",