@planu/cli 5.7.5 → 5.7.6

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 (196) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +3 -3
  3. package/dist/.planu-build.json +1 -1
  4. package/dist/cli/commands/package-handoff.js +30 -5
  5. package/dist/config/compliance-profiles.json +36 -3
  6. package/dist/config/environment-schema.json +21 -0
  7. package/dist/config/hook-templates/planu-session-safeguard.sh +22 -15
  8. package/dist/config/official-sdd-tools.d.ts +1 -1
  9. package/dist/config/official-sdd-tools.js +11 -24
  10. package/dist/config/registries/hosts/codex.json +14 -30
  11. package/dist/config/registries/hosts/opencode.json +1 -6
  12. package/dist/config/server-instructions.js +0 -21
  13. package/dist/config/skill-templates/planu-context-assets.md +3 -6
  14. package/dist/config/skill-templates/planu-implement.md +2 -2
  15. package/dist/config/skill-templates/planu-multi-teammate-review.md +1 -1
  16. package/dist/config/skill-templates/planu-release.md +2 -7
  17. package/dist/config/skill-templates/planu-validate.md +3 -3
  18. package/dist/config/spec-templates/crud-rest-api/template.json +5 -1
  19. package/dist/config/spec-templates/file-upload-s3/template.json +5 -1
  20. package/dist/config/spec-templates/stripe-payments/template.json +5 -1
  21. package/dist/config/spec-templates/webhook-system/template.json +5 -1
  22. package/dist/config/subagent-templates/planu-challenger.md +4 -9
  23. package/dist/config/subagent-templates/planu-spec-implementer.md +2 -4
  24. package/dist/config/subagent-templates/planu-validator.md +5 -8
  25. package/dist/config/tool-groups.json +6 -144
  26. package/dist/engine/ambiguity-scorer.js +40 -1
  27. package/dist/engine/autopilot/bootstrap.js +1 -1
  28. package/dist/engine/code-impact-analyzer.js +2 -6
  29. package/dist/engine/context-orchestrator/index.js +2 -2
  30. package/dist/engine/convention-scanner/codebase-scanner.js +2 -2
  31. package/dist/engine/dogfooding/runtime-gap-detector.js +2 -2
  32. package/dist/engine/evidence-gates/artifact-reader.d.ts +1 -0
  33. package/dist/engine/evidence-gates/artifact-reader.js +1 -1
  34. package/dist/engine/evidence-gates/evidence-autofill.d.ts +13 -5
  35. package/dist/engine/evidence-gates/evidence-autofill.js +106 -43
  36. package/dist/engine/evidence-gates/lifecycle-gate.js +7 -4
  37. package/dist/engine/evidence-index/index-builder.js +21 -4
  38. package/dist/engine/execution/job-runtime.js +1 -1
  39. package/dist/engine/execution/package-handoff-job-executor.d.ts +22 -0
  40. package/dist/engine/execution/package-handoff-job-executor.js +490 -0
  41. package/dist/engine/execution/validate-job-executor.js +20 -8
  42. package/dist/engine/git/exec-git.d.ts +4 -0
  43. package/dist/engine/git/exec-git.js +14 -0
  44. package/dist/engine/handoff-artifacts/implementation-review-reader.js +15 -2
  45. package/dist/engine/handoff-artifacts/schemas.d.ts +2 -0
  46. package/dist/engine/handoff-artifacts/schemas.js +1 -0
  47. package/dist/engine/handoff-format.d.ts +36 -0
  48. package/dist/engine/handoff-format.js +314 -0
  49. package/dist/engine/handoff-packager.d.ts +5 -3
  50. package/dist/engine/handoff-packager.js +89 -32
  51. package/dist/engine/host-tool-filter.js +20 -49
  52. package/dist/engine/human-summary.js +14 -3
  53. package/dist/engine/implementation-contract/common.d.ts +8 -3
  54. package/dist/engine/implementation-contract/common.js +9 -6
  55. package/dist/engine/implementation-contract/evaluator.js +4 -6
  56. package/dist/engine/implementation-contract/renderer.js +18 -23
  57. package/dist/engine/project-health-checker.js +2 -2
  58. package/dist/engine/readiness-checker.js +27 -0
  59. package/dist/engine/reconcile/apply-changes.js +4 -0
  60. package/dist/engine/reconcile/propagate-mirrors.d.ts +3 -0
  61. package/dist/engine/reconcile/propagate-mirrors.js +311 -0
  62. package/dist/engine/self-healing/healer.js +12 -5
  63. package/dist/engine/self-healing/strategies/lint-fix.d.ts +1 -1
  64. package/dist/engine/self-healing/strategies/lint-fix.js +4 -1
  65. package/dist/engine/self-healing/strategies/test-fix.d.ts +4 -3
  66. package/dist/engine/self-healing/strategies/test-fix.js +32 -14
  67. package/dist/engine/self-healing/strategies/typescript-fix.d.ts +1 -1
  68. package/dist/engine/self-healing/strategies/typescript-fix.js +28 -10
  69. package/dist/engine/session/checkpoint-writer.js +8 -4
  70. package/dist/engine/session/session-tracker.d.ts +5 -16
  71. package/dist/engine/session/session-tracker.js +29 -26
  72. package/dist/engine/session-state/writer.js +3 -11
  73. package/dist/engine/skill-generator/workflow-skill-generator.d.ts +4 -2
  74. package/dist/engine/skill-generator/workflow-skill-generator.js +113 -38
  75. package/dist/engine/spec-format/bdd-parser.d.ts +1 -11
  76. package/dist/engine/spec-format/bdd-parser.js +40 -11
  77. package/dist/engine/spec-format/lean-spec-generator.js +30 -10
  78. package/dist/engine/spec-format/retired-scaffold.d.ts +3 -0
  79. package/dist/engine/spec-format/retired-scaffold.js +5 -0
  80. package/dist/engine/spec-format/technical-md-populator.d.ts +1 -0
  81. package/dist/engine/spec-format/technical-md-populator.js +27 -0
  82. package/dist/engine/spec-format/unified-spec-builder.d.ts +1 -0
  83. package/dist/engine/spec-format/unified-spec-builder.js +30 -20
  84. package/dist/engine/spec-grounding/contract.js +115 -1
  85. package/dist/engine/spec-quality/generic-output-gate.js +37 -3
  86. package/dist/engine/technical-enricher/index.js +4 -1
  87. package/dist/engine/type-safety-gate.js +3 -11
  88. package/dist/engine/universal-rules/catalog.js +0 -2
  89. package/dist/engine/universal-rules/rules/planu-workflow.js +1 -1
  90. package/dist/engine/validation/durable-validation.d.ts +3 -2
  91. package/dist/engine/validation/durable-validation.js +106 -40
  92. package/dist/engine/validation/validation-freshness.d.ts +3 -2
  93. package/dist/engine/validation/validation-freshness.js +22 -16
  94. package/dist/engine/validation/validation-worktree.js +34 -0
  95. package/dist/hosts/claude-code/ux/mcp-prompts.js +8 -8
  96. package/dist/index.js +3 -17
  97. package/dist/resources/process.js +38 -65
  98. package/dist/storage/session-state-store.js +2 -0
  99. package/dist/tools/challenge-spec/scenarios-utils.js +2 -2
  100. package/dist/tools/configure-checkpoint-policy.js +3 -3
  101. package/dist/tools/create-spec-helpers.js +2 -5
  102. package/dist/tools/create-spec.js +21 -4
  103. package/dist/tools/facilitate.js +3 -6
  104. package/dist/tools/git/branch-ops.js +16 -4
  105. package/dist/tools/github-release-handler.js +1 -20
  106. package/dist/tools/init-project/agents-md-writer.js +6 -6
  107. package/dist/tools/init-project/claude-md-generator.d.ts +0 -1
  108. package/dist/tools/init-project/claude-md-generator.js +0 -35
  109. package/dist/tools/init-project/handler.js +7 -5
  110. package/dist/tools/init-project/per-client-files-writer.js +5 -5
  111. package/dist/tools/init-project/planu-workflow-generator.js +2 -13
  112. package/dist/tools/init-project/portable-index-reconciler.d.ts +2 -1
  113. package/dist/tools/init-project/portable-index-reconciler.js +7 -2
  114. package/dist/tools/init-project/rules-generator.d.ts +0 -11
  115. package/dist/tools/init-project/rules-generator.js +4 -85
  116. package/dist/tools/init-project/scaffold-writer.d.ts +0 -1
  117. package/dist/tools/init-project/scaffold-writer.js +1 -12
  118. package/dist/tools/jobs/handlers.d.ts +0 -6
  119. package/dist/tools/jobs/handlers.js +13 -40
  120. package/dist/tools/list-specs.js +6 -21
  121. package/dist/tools/package-handoff.d.ts +3 -2
  122. package/dist/tools/package-handoff.js +153 -427
  123. package/dist/tools/reconcile-session-safeguard-hook.js +22 -15
  124. package/dist/tools/register-sdd-tools.js +0 -23
  125. package/dist/tools/register-spec-tools/core-spec-tools.js +18 -27
  126. package/dist/tools/schemas/index.d.ts +1 -1
  127. package/dist/tools/schemas/index.js +1 -1
  128. package/dist/tools/schemas/output-schemas.d.ts +1 -18
  129. package/dist/tools/schemas/output-schemas.js +1 -11
  130. package/dist/tools/schemas/package-handoff-output-schema.d.ts +40 -0
  131. package/dist/tools/schemas/package-handoff-output-schema.js +68 -0
  132. package/dist/tools/schemas/validate-output-schema.d.ts +1 -2
  133. package/dist/tools/schemas/validate-output-schema.js +3 -2
  134. package/dist/tools/session-checkpoint.js +2 -2
  135. package/dist/tools/skill-registry/index.d.ts +0 -1
  136. package/dist/tools/skill-registry/index.js +0 -1
  137. package/dist/tools/sync-spec-state-handler.d.ts +2 -2
  138. package/dist/tools/sync-spec-state-handler.js +23 -3
  139. package/dist/tools/tool-registry/core-tools.js +1 -140
  140. package/dist/tools/tool-registry/group-infra.js +2 -39
  141. package/dist/tools/tool-registry/group-quality-compliance.js +9 -1
  142. package/dist/tools/update-status/batch.js +17 -0
  143. package/dist/tools/update-status/dod-gates.js +14 -1
  144. package/dist/tools/update-status/done-receipt-verifier.d.ts +22 -0
  145. package/dist/tools/update-status/done-receipt-verifier.js +56 -26
  146. package/dist/tools/update-status/evidence-gate.js +10 -21
  147. package/dist/tools/update-status/file-sync.d.ts +1 -0
  148. package/dist/tools/update-status/file-sync.js +6 -0
  149. package/dist/tools/update-status/index.d.ts +6 -0
  150. package/dist/tools/update-status/index.js +39 -44
  151. package/dist/tools/update-status-actions.js +2 -7
  152. package/dist/tools/validate.js +34 -28
  153. package/dist/transports/oauth-validator.js +12 -1
  154. package/dist/transports/transport-factory.d.ts +2 -1
  155. package/dist/transports/transport-factory.js +19 -0
  156. package/dist/types/common/primitives.d.ts +2 -0
  157. package/dist/types/durable-job.d.ts +45 -0
  158. package/dist/types/durable-validation.d.ts +1 -1
  159. package/dist/types/evidence-autofill.d.ts +13 -1
  160. package/dist/types/handoff-artifacts.d.ts +2 -1
  161. package/dist/types/readiness.d.ts +8 -1
  162. package/dist/types/reconcile.d.ts +6 -0
  163. package/dist/types/skill-registry.d.ts +26 -1
  164. package/dist/types/spec/core.d.ts +5 -0
  165. package/dist/types/spec/inputs.d.ts +2 -2
  166. package/dist/types/spec-format.d.ts +11 -1
  167. package/dist/types/spec-grounding.d.ts +12 -0
  168. package/dist/types/transport.d.ts +1 -0
  169. package/dist/types/validation-receipt.d.ts +27 -0
  170. package/package.json +3 -2
  171. package/planu-plugin.json +13 -26
  172. package/src/i18n/messages/en.json +1 -1
  173. package/src/i18n/messages/es.json +1 -1
  174. package/src/i18n/messages/pt.json +1 -1
  175. package/dist/engine/skill-generator/conventions-hasher.d.ts +0 -7
  176. package/dist/engine/skill-generator/conventions-hasher.js +0 -24
  177. package/dist/engine/universal-rules/rules/agent-teams.d.ts +0 -3
  178. package/dist/engine/universal-rules/rules/agent-teams.js +0 -63
  179. package/dist/tools/clarify-requirements/multiple-choice.d.ts +0 -30
  180. package/dist/tools/clarify-requirements/multiple-choice.js +0 -306
  181. package/dist/tools/clarify-requirements/questions-context.d.ts +0 -9
  182. package/dist/tools/clarify-requirements/questions-context.js +0 -74
  183. package/dist/tools/clarify-requirements/questions.d.ts +0 -16
  184. package/dist/tools/clarify-requirements/questions.js +0 -96
  185. package/dist/tools/clarify-requirements.d.ts +0 -4
  186. package/dist/tools/clarify-requirements.js +0 -314
  187. package/dist/tools/code-graph-handler.d.ts +0 -6
  188. package/dist/tools/code-graph-handler.js +0 -72
  189. package/dist/tools/create-rule.d.ts +0 -4
  190. package/dist/tools/create-rule.js +0 -107
  191. package/dist/tools/feedback-handler.d.ts +0 -7
  192. package/dist/tools/feedback-handler.js +0 -150
  193. package/dist/tools/semantic-search-handler.d.ts +0 -7
  194. package/dist/tools/semantic-search-handler.js +0 -71
  195. package/dist/tools/skill-registry/search.d.ts +0 -11
  196. package/dist/tools/skill-registry/search.js +0 -159
@@ -635,15 +635,15 @@ async function readStableState(lease, expectedGeneration) {
635
635
  return undefined;
636
636
  }
637
637
  /** Drain invalidation work and verify epoch, generation, health, expiry and bound inputs. */
638
- export async function assertValidationFreshnessBarrier(lease, bindings) {
638
+ export async function evaluateValidationFreshnessBarrier(lease, bindings) {
639
639
  const candidate = active.get(key(lease.projectId, lease.specId));
640
640
  if (!candidate?.healthy ||
641
641
  candidate.epoch !== lease.epoch ||
642
642
  candidate.generation !== lease.generation) {
643
- return false;
643
+ return { fresh: false, reason: 'lease-superseded' };
644
644
  }
645
645
  if (!(await drainAmbiguousEvents(lease, candidate, candidate.bindings))) {
646
- return false;
646
+ return { fresh: false, reason: 'invalidation-drain-failed' };
647
647
  }
648
648
  let fingerprintBefore;
649
649
  try {
@@ -652,23 +652,23 @@ export async function assertValidationFreshnessBarrier(lease, bindings) {
652
652
  catch (error) {
653
653
  reportClassifiedDegradation('VALIDATION_FRESHNESS_CHECK_FAILED', error);
654
654
  invalidateExactEpoch(lease, candidate);
655
- return false;
655
+ return { fresh: false, reason: 'evidence-check-failed', changedInput: candidate.handoffPath };
656
656
  }
657
657
  if (fingerprintBefore !== candidate.evidenceFingerprint) {
658
658
  invalidateExactEpoch(lease, candidate);
659
- return false;
659
+ return { fresh: false, reason: 'evidence-changed', changedInput: candidate.handoffPath };
660
660
  }
661
661
  try {
662
662
  if (candidate.bindings &&
663
663
  (await gitSourceTreeDigest(candidate.projectPath)) !== candidate.bindings.sourceDigest) {
664
664
  invalidateExactEpoch(lease, candidate);
665
- return false;
665
+ return { fresh: false, reason: 'source-changed', changedInput: candidate.projectPath };
666
666
  }
667
667
  }
668
668
  catch (error) {
669
669
  reportClassifiedDegradation('VALIDATION_FRESHNESS_CHECK_FAILED', error);
670
670
  invalidateExactEpoch(lease, candidate);
671
- return false;
671
+ return { fresh: false, reason: 'source-check-failed', changedInput: candidate.projectPath };
672
672
  }
673
673
  // A generation change invalidates the lease synchronously. Do not wait for
674
674
  // the durable receipt cleanup when the in-memory barrier already fails
@@ -684,15 +684,21 @@ export async function assertValidationFreshnessBarrier(lease, bindings) {
684
684
  }
685
685
  if (!state || fingerprintAfter !== fingerprintBefore) {
686
686
  invalidateExactEpoch(lease, candidate);
687
- return false;
687
+ return { fresh: false, reason: 'state-lost', changedInput: candidate.handoffPath };
688
688
  }
689
689
  if (state.generation !== lease.generation) {
690
- return false;
690
+ return { fresh: false, reason: 'lease-superseded' };
691
691
  }
692
692
  if (Date.now() >= state.expiresAt) {
693
- return false;
693
+ return { fresh: false, reason: 'lease-expired' };
694
+ }
695
+ if (bindings !== undefined && JSON.stringify(state.bindings) !== JSON.stringify(bindings)) {
696
+ return { fresh: false, reason: 'bindings-mismatch' };
694
697
  }
695
- return bindings === undefined || JSON.stringify(state.bindings) === JSON.stringify(bindings);
698
+ return { fresh: true };
699
+ }
700
+ export async function assertValidationFreshnessBarrier(lease, bindings) {
701
+ return (await evaluateValidationFreshnessBarrier(lease, bindings)).fresh;
696
702
  }
697
703
  export async function captureValidationFreshnessLease(identity, bindings) {
698
704
  const state = active.get(key(identity.projectId, identity.specId));
@@ -707,7 +713,7 @@ export async function rebaselineValidationFreshnessLeaseAfterRecompute(input) {
707
713
  for (let attempt = 0; attempt < 2; attempt += 1) {
708
714
  const before = await readStableState(input.lease);
709
715
  if (!before || Date.now() >= before.expiresAt) {
710
- return undefined;
716
+ return { rejection: { reason: 'lease-expired' } };
711
717
  }
712
718
  const generationBefore = before.generation;
713
719
  let fingerprintBefore;
@@ -721,14 +727,14 @@ export async function rebaselineValidationFreshnessLeaseAfterRecompute(input) {
721
727
  catch (error) {
722
728
  reportClassifiedDegradation('VALIDATION_FRESHNESS_CHECK_FAILED', error);
723
729
  invalidateExactEpoch(input.lease, before);
724
- return undefined;
730
+ return { rejection: { reason: 'recompute-failed', changedInput: before.handoffPath } };
725
731
  }
726
732
  const after = await readStableState(input.lease);
727
733
  if (after?.generation !== generationBefore || fingerprintBefore !== fingerprintAfter) {
728
734
  continue;
729
735
  }
730
736
  if (JSON.stringify(input.expectedBindings) !== JSON.stringify(observedBindings)) {
731
- return undefined;
737
+ return { rejection: { reason: 'bindings-mismatch' } };
732
738
  }
733
739
  const confirmed = await readStableState(input.lease);
734
740
  if (confirmed?.generation !== after.generation) {
@@ -736,9 +742,9 @@ export async function rebaselineValidationFreshnessLeaseAfterRecompute(input) {
736
742
  }
737
743
  confirmed.bindings = structuredClone(observedBindings);
738
744
  confirmed.evidenceFingerprint = fingerprintAfter;
739
- return { ...input.lease, generation: confirmed.generation };
745
+ return { lease: { ...input.lease, generation: confirmed.generation } };
740
746
  }
741
- return undefined;
747
+ return { rejection: { reason: 'lease-unstable' } };
742
748
  }
743
749
  export async function resetValidationFreshnessForTests() {
744
750
  const leases = [...active.entries()].map(([identity, state]) => {
@@ -9,6 +9,7 @@ const runFile = promisify(execFile);
9
9
  const ROOT_ID = /^[a-f0-9]{64}$/u;
10
10
  const SAFE_SPEC_ID = /^SPEC-[1-9]\d*$/u;
11
11
  const PORTABLE_SPEC = /^planu\/specs\/(SPEC-\d{3,}(?:-[^/]+)?)\/spec\.md$/u;
12
+ const STALE_SPEC_TAIL = /planu[/\\]specs[/\\]([^/\\]+)[/\\]spec\.md$/u;
12
13
  export class ValidationWorktreeError extends Error {
13
14
  code;
14
15
  specId;
@@ -83,6 +84,35 @@ async function resolveGitRoot(projectPath, specId) {
83
84
  }
84
85
  return membership;
85
86
  }
87
+ function derivePortableSuffix(specPath, specId) {
88
+ const match = STALE_SPEC_TAIL.exec(specPath);
89
+ const dir = match?.[1];
90
+ if (!dir || (dir !== specId && !dir.startsWith(`${specId}-`))) {
91
+ return undefined;
92
+ }
93
+ const portableSuffix = `planu/specs/${dir}/spec.md`;
94
+ return PORTABLE_SPEC.test(portableSuffix) ? portableSuffix : undefined;
95
+ }
96
+ async function classifyStaleSpecPath(specPath, specId) {
97
+ if (!isAbsolute(specPath)) {
98
+ return undefined;
99
+ }
100
+ const portableSuffix = derivePortableSuffix(specPath, specId);
101
+ if (!portableSuffix) {
102
+ return undefined;
103
+ }
104
+ try {
105
+ await lstat(dirname(specPath));
106
+ return undefined;
107
+ }
108
+ catch (error) {
109
+ if (isEnoent(error)) {
110
+ return portableSuffix;
111
+ }
112
+ reportClassifiedDegradation('VALIDATION_STALE_PREFIX_PROBE_FAILED', error);
113
+ return undefined;
114
+ }
115
+ }
86
116
  async function resolveSpecSourceRoot(input) {
87
117
  const specPath = isAbsolute(input.canonicalSpecPath)
88
118
  ? input.canonicalSpecPath
@@ -92,6 +122,10 @@ async function resolveSpecSourceRoot(input) {
92
122
  source = await resolveGitMembership(dirname(specPath), input.specId);
93
123
  }
94
124
  catch {
125
+ const portableSuffix = await classifyStaleSpecPath(specPath, input.specId);
126
+ if (portableSuffix) {
127
+ throw new ValidationWorktreeError('VALIDATION_STALE_SPEC_PATH', input.specId, portableSuffix);
128
+ }
95
129
  throw new ValidationWorktreeError('VALIDATION_SPEC_PATH_TRAVERSAL', input.specId);
96
130
  }
97
131
  if (source.gitCommonDir !== input.canonical.gitCommonDir) {
@@ -87,7 +87,7 @@ function buildExecuteSddFlowPrompt(specId, projectPath, mode) {
87
87
  const branchStep = mode !== 'implement-only'
88
88
  ? '4. Create a feature branch: `git checkout -b feat/' + specId + '-<slug>`.'
89
89
  : '4. Implement in the current branch (implement-only mode).';
90
- const prStep = mode !== 'implement-only' ? '12. Open a PR with `create_pr_from_spec`.' : '';
90
+ const prStep = mode !== 'implement-only' ? '12. Open a PR with `gh pr create`.' : '';
91
91
  return [
92
92
  `You are implementing Planu spec \`${specId}\`${pathHint}${modeHint} using the full SDD flow.`,
93
93
  '',
@@ -104,7 +104,7 @@ function buildExecuteSddFlowPrompt(specId, projectPath, mode) {
104
104
  '8. Call `validate({ specId: "' +
105
105
  specId +
106
106
  '" })` and capture the durable acknowledgement fields: `operationId`, `projectId`, `workspaceId`, and `cursor`.',
107
- '9. Call `watch_job` with those identifiers and the latest cursor. Repeat while the job is non-terminal; do not treat an accepted or running job as a validation result.',
107
+ '9. Poll `get_job` with those identifiers and the latest cursor. Repeat while the job is non-terminal; do not treat an accepted or running job as a validation result.',
108
108
  '10. Require terminal state `completed`, then inspect `job.result.structuredContent`. If the job is failed, dead-letter, or cancelled, stop and report it. Fix every failing criterion and submit a new validation job when needed.',
109
109
  '11. Call `update_status({ specId: "' +
110
110
  specId +
@@ -118,18 +118,18 @@ function buildExecuteSddFlowPrompt(specId, projectPath, mode) {
118
118
  }
119
119
  function buildChallengeSpecPrompt(specId, focus) {
120
120
  const focusHint = focus !== undefined && focus !== 'all' ? ` with a focus on **${focus}**` : '';
121
- const securityStep = focus === 'security' || focus === 'all'
122
- ? '5. Call `red_team` for adversarial security scenarios.'
121
+ const securityHint = focus === 'security' || focus === 'all'
122
+ ? ' Pay special attention to adversarial and security-relevant scenarios.'
123
123
  : '';
124
124
  return [
125
125
  `You are stress-testing Planu spec \`${specId}\`${focusHint} to find weaknesses before implementation.`,
126
126
  '',
127
127
  'Steps:',
128
128
  `1. Call \`list_specs\` and read the full spec for \`${specId}\`.`,
129
- `2. Call \`challenge_spec({ specId: "${specId}" })\` and read all findings.`,
130
- '3. Call `ears_lint` to check criterion format quality.',
131
- '4. Call `validate_criteria_quality` for testability gaps.',
132
- securityStep,
129
+ `2. Call \`challenge_spec({ specId: "${specId}" })\` and read all findings.${securityHint}`,
130
+ '3. Call `check_readiness({ specId: "' +
131
+ specId +
132
+ '" })` to verify the spec has enough detail to implement.',
133
133
  '',
134
134
  'For each finding, provide:',
135
135
  '- Severity: 🔴 blocking | 🟡 missing edge case | 🟢 ambiguity',
package/dist/index.js CHANGED
@@ -83,6 +83,9 @@ function scheduleStartupStateRecovery(startupSync, recoverPendingPostCommitTasks
83
83
  void import('./engine/execution/validate-job-executor.js').then(({ recoverValidateJobsAtStartup }) => {
84
84
  recoverValidateJobsAtStartup();
85
85
  });
86
+ void import('./engine/execution/package-handoff-job-executor.js').then(({ recoverPackageHandoffJobsAtStartup }) => {
87
+ recoverPackageHandoffJobsAtStartup();
88
+ });
86
89
  }
87
90
  const MCP_HOST_MARKERS = [
88
91
  ['claude-code', 'claude-code'],
@@ -139,23 +142,6 @@ async function setupPostHandshake(handshakeGate, officialToolNames) {
139
142
  checkForUpdates().catch(() => {
140
143
  /* best-effort */
141
144
  });
142
- // SPEC-566: Auto-configure Claude Code keybindings (global, one-time)
143
- void (async () => {
144
- try {
145
- const { globalStore: gs } = await import('./storage/index.js');
146
- const cfg = await gs.getGlobalConfig();
147
- if (!cfg.keybindingsConfigured) {
148
- const { configureClaudeKeybindings } = await import('./engine/claude-setup/keybindings-configurator.js');
149
- const configured = await configureClaudeKeybindings();
150
- if (configured) {
151
- await gs.updateGlobalConfig({ keybindingsConfigured: true });
152
- }
153
- }
154
- }
155
- catch {
156
- /* best-effort */
157
- }
158
- })();
159
145
  // SPEC-334: Sync planu/ YAML frontmatter status → data/ store (non-blocking)
160
146
  scheduleStartupStateRecovery(startupSync, recoverPendingPostCommitTasksAtStartup);
161
147
  // SPEC-1271: bounded, fair, durable spec.created outbox recovery — fire-and-forget so a
@@ -16,90 +16,63 @@ function getSddProcess() {
16
16
  {
17
17
  id: 'initializing',
18
18
  name: '1. Initialize',
19
- tools: ['init_project', 'init_constitution', 'detect_agent'],
20
- description: 'Detect project language, framework, architecture, and conventions. ' +
21
- 'Generate the project constitution. Detect the AI agent platform in use.',
19
+ tools: ['init_project'],
20
+ description: 'Load or bootstrap the Planu project context for the current repository.',
22
21
  },
23
22
  {
24
- id: 'analyzing',
25
- name: '2. Analyze',
26
- tools: [
27
- 'clarify_requirements',
28
- 'suggest_stack',
29
- 'check_versions',
30
- 'consult_docs',
31
- 'discover_mcps',
32
- 'suggest_mcps',
33
- 'suggest_tooling',
34
- ],
35
- description: 'Clarify requirements, analyze the stack, check dependency versions, ' +
36
- 'discover available MCP servers, and assess dev lifecycle tooling.',
37
- },
38
- {
39
- id: 'creating-spec',
40
- name: '3. Create Spec',
41
- tools: ['create_spec', 'reverse_engineer', 'estimate'],
42
- description: 'Create a full SDD spec.md from requirements or ' +
43
- 'reverse-engineer from existing code. Estimate effort, cost, and tokens.',
23
+ id: 'defining',
24
+ name: '2. Define',
25
+ tools: ['facilitate', 'create_spec'],
26
+ description: 'Facilitate requirements discovery through the universal SDD entry point, ' +
27
+ 'then create the spec.md that becomes the contract for implementation.',
44
28
  },
45
29
  {
46
- id: 'filtering',
47
- name: '4. Filter & Enrich',
30
+ id: 'approving',
31
+ name: '3. Refine & Approve',
48
32
  tools: [
49
- 'generate_adr',
50
- 'design_schema',
51
- 'define_ui_contract',
52
33
  'challenge_spec',
53
- 'generate_execution_plan',
54
- ],
55
- description: 'Enrich the spec with architecture decisions, database schema, UI contracts, ' +
56
- 'resilience analysis, and step-by-step execution plans.',
57
- },
58
- {
59
- id: 'reviewing',
60
- name: '5. Review',
61
- tools: [
62
- 'summarize_spec',
63
- 'generate_checklist',
64
- 'reconcile_spec',
65
- 'list_specs',
34
+ 'check_readiness',
35
+ 'estimate',
36
+ 'request_changes',
66
37
  'update_status',
67
38
  ],
68
- description: 'Review specs via summaries and checklists. Reconcile with changes. ' +
69
- 'Transition spec status through the lifecycle.',
39
+ description: 'Stress-test the spec, verify it has enough detail to implement, estimate ' +
40
+ 'effort and cost, request changes if gaps remain, and move the spec through ' +
41
+ 'draft → review → approved.',
70
42
  },
71
43
  {
72
44
  id: 'implementing',
73
- name: '6. Implement',
74
- tools: [
75
- 'generate_skill',
76
- 'generate_sub_agent',
77
- 'generate_rules',
78
- 'manage_git',
79
- 'manage_context',
80
- 'orchestrate',
81
- ],
82
- description: 'Generate agent skills and rules. Manage git branches and PRs. ' +
83
- 'Orchestrate multi-agent workflows. Persist context across sessions.',
45
+ name: '4. Implement',
46
+ tools: ['package_handoff', 'bump_spec_version'],
47
+ description: 'Generate the implementation handoff package for the coding agent and bump ' +
48
+ "the spec's SemVer as its contract evolves.",
84
49
  },
85
50
  {
86
51
  id: 'validating',
87
- name: '7. Validate',
88
- tools: ['validate', 'detect_drift', 'audit', 'generate_tests'],
89
- description: 'Validate implementation against spec. Detect drift. Audit code quality. ' +
90
- 'Generate and run test plans.',
52
+ name: '5. Validate',
53
+ tools: ['validate', 'get_job', 'restart_job'],
54
+ description: 'Check the implementation against acceptance criteria, poll the durable ' +
55
+ 'validation job to completion, and restart it if it fails or is cancelled.',
56
+ },
57
+ {
58
+ id: 'closing',
59
+ name: '6. Reconcile & Close',
60
+ tools: ['reconcile_spec', 'migrate_legacy_spec', 'update_status_batch'],
61
+ description: 'Reconcile implementation drift back into the spec, migrate legacy spec docs ' +
62
+ 'onto the canonical spec.md, and batch-transition specs to done. Shipping ' +
63
+ 'runs the local-first release contract (`pnpm check:preflight`, then ' +
64
+ '`pnpm release:local`) outside the MCP tool surface.',
91
65
  },
92
66
  {
93
- id: 'documenting',
94
- name: '8. Document & Ship',
95
- tools: ['generate_docs', 'generate_docs_site', 'integrate_pm', 'learn_pattern'],
96
- description: 'Generate documentation, run the local-first release contract ' +
97
- '(`pnpm check:preflight`, `pnpm release:local` when shipping), ' +
98
- 'sync with project management tools, and learn patterns for future estimations.',
67
+ id: 'tracking',
68
+ name: '7. Track',
69
+ tools: ['list_specs', 'planu_status', 'session_checkpoint'],
70
+ description: 'List specs and their status, check overall project health, and persist ' +
71
+ 'session context so work can resume across sessions.',
99
72
  },
100
73
  ],
101
74
  specLifecycle: ['draft', 'review', 'approved', 'implementing', 'done'],
102
- totalTools: 104,
75
+ totalTools: 19,
103
76
  };
104
77
  }
105
78
  /**
@@ -31,6 +31,8 @@ export function getStagedFiles(projectPath) {
31
31
  cwd: projectPath,
32
32
  encoding: 'utf-8',
33
33
  stdio: ['pipe', 'pipe', 'pipe'],
34
+ timeout: 5_000,
35
+ maxBuffer: 10 * 1024 * 1024,
34
36
  });
35
37
  return output
36
38
  .split('\n')
@@ -33,8 +33,8 @@ const CAPABILITY_SIGNALS = {
33
33
  /\bbatch\s+upload\b/i,
34
34
  ],
35
35
  scale: [
36
- /\b(?:traffic|load\b(?![\s‐-―-]+bearing)|throughput|requests?\s+per\s+second|concurrent\s+users?)\b/i,
37
- /\b(?:auto[ -]?scal|load\s+test|capacity\s+plan)/i,
36
+ /\b(?:traffic|throughput|requests?\s+per\s+second|concurrent\s+users?)\b/i,
37
+ /\b(?:auto[ -]?scal|load\s+(?:test|balanc|shedding|envelope|spike)|(?:peak|heavy|normal|sustained|read|write)\s+load|capacity\s+plan)/i,
38
38
  /\b(?:data\s+growth|growing\s+dataset|query\s+performance\s+at\s+scale)\b/i,
39
39
  ],
40
40
  dataWrite: [
@@ -1,16 +1,16 @@
1
1
  // tools/configure-checkpoint-policy.ts — Configure auto checkpoint policy (SPEC-563).
2
- import { sessionTracker } from '../engine/session/session-tracker.js';
2
+ import { setCheckpointPolicy, getCheckpointPolicy } from '../engine/session/session-tracker.js';
3
3
  import { compactResult } from './output-formatter.js';
4
4
  /**
5
5
  * Handle the configure_checkpoint_policy tool.
6
6
  * Updates threshold and/or enabled flag, returns the current policy.
7
7
  */
8
8
  export function handleConfigureCheckpointPolicy(args) {
9
- sessionTracker.setPolicy({
9
+ setCheckpointPolicy({
10
10
  threshold: args.threshold,
11
11
  enabled: args.enabled,
12
12
  });
13
- const policy = sessionTracker.getPolicy();
13
+ const policy = getCheckpointPolicy();
14
14
  return compactResult(`Checkpoint policy updated for ${args.projectPath}.\n` +
15
15
  `Threshold: every ${String(policy.threshold)} calls, ` +
16
16
  `enabled: ${String(policy.enabled)}.`);
@@ -26,11 +26,8 @@ export async function tryAutoSetupGit(projectId, specId, options) {
26
26
  /* v8 ignore start -- requires real git repo with branches */
27
27
  try {
28
28
  const r = await handleAutoSetup(projectId, specId, undefined, options);
29
- if (!r.isError && r.content[0]?.type === 'text') {
30
- const data = JSON.parse(r.content[0].text);
31
- if (typeof data.newBranch !== 'string') {
32
- return undefined;
33
- }
29
+ const data = r.structuredContent;
30
+ if (!r.isError && data && typeof data.newBranch === 'string') {
34
31
  return { data, branch: data.newBranch };
35
32
  }
36
33
  }
@@ -20,6 +20,7 @@ import { generateLeanTechnicalContent, } from '../engine/spec-format/lean-techni
20
20
  import { extractFilesFromSpecBody, extractMarkerDeclaredPaths, } from '../engine/spec-format/technical-md-populator.js';
21
21
  import { buildCanonicalUnifiedSpecContent, containsUnresolvedContractPlaceholder, validateUnifiedSpecCandidate, } from '../engine/spec-format/unified-spec-builder.js';
22
22
  import { buildImplementationContractSection } from '../engine/implementation-contract/index.js';
23
+ import { updateFrontmatterField } from '../engine/frontmatter-parser.js';
23
24
  import { resolveEnglishOnlySpecGate } from '../engine/spec-language/english-only.js';
24
25
  import { FallbackGenerator } from '../engine/spec-generator/index.js';
25
26
  import { analyzeProjectForSpec, getEmptyAutopilotResult, } from './create-spec/autopilot-analyzer.js';
@@ -1257,7 +1258,7 @@ async function prepareCreateSpecCandidate(initialParams, server) {
1257
1258
  outOfScope,
1258
1259
  verificationCommands,
1259
1260
  });
1260
- const unifiedSpec = buildCanonicalUnifiedSpecContent({
1261
+ let unifiedSpec = buildCanonicalUnifiedSpecContent({
1261
1262
  leanSpecBody: leanSpec,
1262
1263
  sourceDescription: params.description,
1263
1264
  technicalBody: leanTechnical,
@@ -1269,12 +1270,18 @@ async function prepareCreateSpecCandidate(initialParams, server) {
1269
1270
  const candidateValidation = validateUnifiedSpecCandidate(unifiedSpec, {
1270
1271
  groundedFilePaths: groundedTechnical.records.map((record) => record.path),
1271
1272
  });
1272
- if (!candidateValidation.valid) {
1273
+ // SPEC-1435: MISSING_EXECUTABLE_CRITERIA no longer blocks persistence — plain-text
1274
+ // criteria are accepted input today (checkbox items via extractAcceptanceCriteriaTexts),
1275
+ // and the constant GIVEN/WHEN/THEN scaffold that used to satisfy this check has been
1276
+ // retired. The readiness-checker approval gate now blocks a zero-BDD spec at
1277
+ // review -> approved instead; here it downgrades to a persisted quality warning.
1278
+ const blockingCandidateIssues = candidateValidation.issues.filter((issue) => issue.code !== 'MISSING_EXECUTABLE_CRITERIA');
1279
+ if (blockingCandidateIssues.length > 0) {
1273
1280
  return earlyPreparation({
1274
1281
  content: [
1275
1282
  {
1276
1283
  type: 'text',
1277
- text: `Unified spec candidate is invalid: ${candidateValidation.issues
1284
+ text: `Unified spec candidate is invalid: ${blockingCandidateIssues
1278
1285
  .slice(0, 3)
1279
1286
  .map((issue) => issue.message)
1280
1287
  .join('; ')}`,
@@ -1285,10 +1292,20 @@ async function prepareCreateSpecCandidate(initialParams, server) {
1285
1292
  error: 'SPEC_FORMAT_INVALID',
1286
1293
  code: 422,
1287
1294
  persisted: false,
1288
- issues: candidateValidation.issues,
1295
+ issues: blockingCandidateIssues,
1289
1296
  },
1290
1297
  });
1291
1298
  }
1299
+ if (candidateValidation.issues.some((issue) => issue.code === 'MISSING_EXECUTABLE_CRITERIA')) {
1300
+ const nonExecutableCriteriaWarning = 'Acceptance criteria are not executable GIVEN/WHEN/THEN — restate them before approval';
1301
+ spec.qualityWarnings = [
1302
+ ...new Set([...(spec.qualityWarnings ?? []), nonExecutableCriteriaWarning]),
1303
+ ];
1304
+ // unifiedSpec's frontmatter was already serialized from `spec` before this warning was
1305
+ // known — patch the persisted YAML field directly rather than re-serializing the whole
1306
+ // candidate.
1307
+ unifiedSpec = updateFrontmatterField(unifiedSpec, 'qualityWarnings', JSON.stringify(spec.qualityWarnings));
1308
+ }
1292
1309
  const genericOutputGate = checkGenericSpecOutput(unifiedSpec);
1293
1310
  if (!genericOutputGate.passed) {
1294
1311
  return earlyPreparation({
@@ -39,8 +39,8 @@ function buildExistingProjectResponse(description) {
39
39
  return (`I'll run a diagnostic on your project.\n\n` +
40
40
  `Please provide the project path and call:\n` +
41
41
  `- \`init_project\` with your project path (if not yet initialized)\n` +
42
- `- \`reverse_engineer\` to analyze the codebase\n` +
43
- `- \`audit\` to check spec health\n\n` +
42
+ `- \`list_specs\` to see existing specs\n` +
43
+ `- \`planu_status\` to check overall project health\n\n` +
44
44
  `Request received: "${description.slice(0, 120)}"`);
45
45
  }
46
46
  // ---------------------------------------------------------------------------
@@ -51,7 +51,6 @@ const PROBLEM_TO_TOOL = [
51
51
  [/validat/i, 'validate', 'validate a spec'],
52
52
  [/spec|user stor/i, 'create_spec', 'create a spec/user story'],
53
53
  [/estimat/i, 'estimate', 'estimate effort and cost'],
54
- [/test/i, 'generate_tests', 'generate tests'],
55
54
  [/document|doc/i, 'generate_docs', 'generate documentation'],
56
55
  [/drift|diverge/i, 'detect_drift', 'detect spec drift'],
57
56
  [/schema|database|db/i, 'design_schema', 'design a database schema'],
@@ -69,10 +68,8 @@ function buildConcreteProblemResponse(description) {
69
68
  }
70
69
  return (`I can help with that! Here are the most relevant Planu tools:\n\n` +
71
70
  `- \`create_spec\` — write a new user story / spec\n` +
72
- `- \`clarify_requirements\` — clarify vague requirements\n` +
73
71
  `- \`estimate\` — estimate effort and cost\n` +
74
- `- \`validate\` — check if a spec is complete\n` +
75
- `- \`generate_tests\` — generate test scaffolds\n\n` +
72
+ `- \`validate\` — check if a spec is complete\n\n` +
76
73
  `What specifically would you like to do?`);
77
74
  }
78
75
  // ---------------------------------------------------------------------------
@@ -54,7 +54,10 @@ export async function handleCreateBranch(projectId, specId, config, resolvedProj
54
54
  ? `Switched to existing branch '${branchName}'`
55
55
  : `Branch '${branchName}' already exists (checkout left untouched)`,
56
56
  };
57
- return compactResult(formatKeyValue(result));
57
+ return {
58
+ ...compactResult(formatKeyValue(result)),
59
+ structuredContent: result,
60
+ };
58
61
  }
59
62
  }
60
63
  catch {
@@ -74,7 +77,10 @@ export async function handleCreateBranch(projectId, specId, config, resolvedProj
74
77
  branchName,
75
78
  message: ti('tools.manage_git.success', { action: 'create-branch' }),
76
79
  };
77
- return compactResult(formatKeyValue(result));
80
+ return {
81
+ ...compactResult(formatKeyValue(result)),
82
+ structuredContent: result,
83
+ };
78
84
  }
79
85
  export async function handleCheckBranch(projectId, _specId, config) {
80
86
  const projectPath = await resolveProjectPath(projectId);
@@ -217,7 +223,10 @@ export async function handleAutoSetup(projectId, specId, config, options) {
217
223
  ? `Switched to existing branch '${branchName}' (${gitFlowType} workflow detected)`
218
224
  : `Branch '${branchName}' already exists (checkout left untouched)`,
219
225
  };
220
- return compactResult(formatKeyValue(result));
226
+ return {
227
+ ...compactResult(formatKeyValue(result)),
228
+ structuredContent: result,
229
+ };
221
230
  }
222
231
  }
223
232
  catch {
@@ -235,7 +244,10 @@ export async function handleAutoSetup(projectId, specId, config, options) {
235
244
  ? `Created branch '${branchName}' from '${baseBranch}' (${gitFlowType} workflow detected)`
236
245
  : `Created branch '${branchName}' from '${baseBranch}' without switching the checkout`,
237
246
  };
238
- return compactResult(formatKeyValue(result));
247
+ return {
248
+ ...compactResult(formatKeyValue(result)),
249
+ structuredContent: result,
250
+ };
239
251
  }
240
252
  // ---------------------------------------------------------------------------
241
253
  // Worktree operations (SPEC-260)
@@ -252,26 +252,7 @@ export async function handleCreateRelease(args, exec = defaultExec) {
252
252
  /* best-effort — release_completed cascade never blocks the release response */
253
253
  }
254
254
  })();
255
- // SPEC-1003: Auto-merge develop to main after release (best-effort)
256
- void (async () => {
257
- try {
258
- const releasePath = process.cwd();
259
- const run = promisify(nodeExecFile);
260
- const { stdout: currentBranch } = await run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
261
- cwd: releasePath,
262
- });
263
- if (currentBranch.trim() === 'develop') {
264
- await run('git', ['checkout', 'main'], { cwd: releasePath });
265
- await run('git', ['merge', 'develop', '--no-edit'], { cwd: releasePath });
266
- await run('git', ['push', '--no-verify', 'origin', 'main'], { cwd: releasePath });
267
- await run('git', ['checkout', 'develop'], { cwd: releasePath });
268
- outputLines.push(' Auto-merge: develop → main');
269
- }
270
- }
271
- catch {
272
- /* best-effort — auto-merge never blocks release */
273
- }
274
- })();
255
+ outputLines.push(' Mirror sync (run manually if develop exists): git checkout develop && git merge --ff-only origin/main && git push origin develop');
275
256
  return {
276
257
  content: [
277
258
  {
@@ -34,13 +34,13 @@ Auto-generated by \`init_project\`. Mirrors CLAUDE.md for IDEs that read AGENTS.
34
34
  \`\`\`
35
35
  facilitate("what to build") → create_spec → challenge_spec → check_readiness
36
36
  → update_status(review) → approve → update_status(approved) → implement
37
- → validate (ack) → watch_job (completed) → inspect job.result → update_status(done)
37
+ → validate (ack) → get_job (poll until completed) → inspect job.result → update_status(done)
38
38
  \`\`\`
39
39
 
40
40
  ## Durable Validation Gate
41
41
 
42
- 1. Call \`validate\` and capture \`operationId\`, \`projectId\`, \`workspaceId\`, and \`cursor\` from the acknowledgement
43
- 2. Call \`watch_job\` with those identifiers and the latest cursor until the job reaches a terminal state
42
+ 1. Call \`validate\` and capture \`operationId\`, \`projectId\`, and \`workspaceId\` from the acknowledgement
43
+ 2. Poll \`get_job\` with those identifiers until the job reaches a terminal state
44
44
  3. Require \`completed\` and inspect \`job.result.structuredContent\`; stop on \`failed\`, \`dead-letter\`, or \`cancelled\`
45
45
  4. Call \`update_status(done)\` only when the completed result reports \`ready: true\`, \`status: "passing"\`, and passing quality gates
46
46
 
@@ -52,7 +52,7 @@ facilitate("what to build") → create_spec → challenge_spec → check_readine
52
52
  ## Session End
53
53
 
54
54
  - Call \`session_checkpoint\` to save context
55
- - Run \`validate\`, await the completed job with \`watch_job\`, inspect \`job.result.structuredContent\`, then call \`update_status(done)\`
55
+ - Run \`validate\`, poll \`get_job\` until completed, inspect \`job.result.structuredContent\`, then call \`update_status(done)\`
56
56
 
57
57
  ## Quick Reference
58
58
 
@@ -79,7 +79,7 @@ Auto-generated by init_project.
79
79
  1. Before writing any code, call facilitate() or create_spec() first.
80
80
  2. Never write code without an approved Planu spec.
81
81
  3. Always call validate() after implementation; it returns a durable job acknowledgement, not pass/fail.
82
- 4. Call watch_job() until completed and inspect job.result.structuredContent before marking done.
82
+ 4. Poll get_job() until completed and inspect job.result.structuredContent before marking done.
83
83
  5. Use update_status() to track progress: draft → review → approved → implementing → done.
84
84
 
85
85
  ## Session
@@ -91,7 +91,7 @@ Auto-generated by init_project.
91
91
 
92
92
  - New feature: facilitate("what to build")
93
93
  - Check status: planu_status
94
- - Mark done: validate, watch_job until completed, inspect job.result.structuredContent, then update_status(done)
94
+ - Mark done: validate, poll get_job until completed, inspect job.result.structuredContent, then update_status(done)
95
95
  `;
96
96
  async function writeIfMissing(path, content) {
97
97
  try {