@planu/cli 5.7.5 → 5.7.7

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 (210) hide show
  1. package/CHANGELOG.md +88 -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-review-panel-trigger.sh +2 -3
  8. package/dist/config/hook-templates/planu-session-safeguard.sh +22 -15
  9. package/dist/config/official-sdd-tools.d.ts +1 -1
  10. package/dist/config/official-sdd-tools.js +11 -24
  11. package/dist/config/registries/hosts/codex.json +14 -30
  12. package/dist/config/registries/hosts/opencode.json +1 -6
  13. package/dist/config/server-instructions.js +0 -21
  14. package/dist/config/skill-templates/planu-context-assets.md +3 -6
  15. package/dist/config/skill-templates/planu-implement.md +2 -2
  16. package/dist/config/skill-templates/planu-multi-teammate-review.md +8 -12
  17. package/dist/config/skill-templates/planu-release.md +2 -7
  18. package/dist/config/skill-templates/planu-validate.md +3 -3
  19. package/dist/config/spec-templates/crud-rest-api/template.json +5 -1
  20. package/dist/config/spec-templates/file-upload-s3/template.json +5 -1
  21. package/dist/config/spec-templates/stripe-payments/template.json +5 -1
  22. package/dist/config/spec-templates/webhook-system/template.json +5 -1
  23. package/dist/config/subagent-templates/planu-challenger.md +4 -9
  24. package/dist/config/subagent-templates/planu-spec-implementer.md +2 -4
  25. package/dist/config/subagent-templates/planu-validator.md +5 -8
  26. package/dist/config/tool-groups.json +6 -144
  27. package/dist/engine/ambiguity-scorer.js +40 -1
  28. package/dist/engine/autopilot/bootstrap.js +1 -1
  29. package/dist/engine/code-impact-analyzer.js +2 -6
  30. package/dist/engine/context-orchestrator/index.js +2 -2
  31. package/dist/engine/convention-scanner/codebase-scanner.js +2 -2
  32. package/dist/engine/dogfooding/runtime-gap-detector.js +2 -2
  33. package/dist/engine/evidence-gates/artifact-reader.d.ts +1 -0
  34. package/dist/engine/evidence-gates/artifact-reader.js +1 -1
  35. package/dist/engine/evidence-gates/evidence-autofill.d.ts +13 -5
  36. package/dist/engine/evidence-gates/evidence-autofill.js +106 -43
  37. package/dist/engine/evidence-gates/evidence-skeletons.js +3 -1
  38. package/dist/engine/evidence-gates/lifecycle-gate.js +7 -4
  39. package/dist/engine/evidence-index/index-builder.js +21 -4
  40. package/dist/engine/execution/job-runtime.js +1 -1
  41. package/dist/engine/execution/package-handoff-job-executor.d.ts +22 -0
  42. package/dist/engine/execution/package-handoff-job-executor.js +490 -0
  43. package/dist/engine/execution/validate-job-executor.js +20 -8
  44. package/dist/engine/git/exec-git.d.ts +4 -0
  45. package/dist/engine/git/exec-git.js +14 -0
  46. package/dist/engine/handoff-artifacts/implementation-review-reader.js +15 -2
  47. package/dist/engine/handoff-artifacts/schemas.d.ts +2 -0
  48. package/dist/engine/handoff-artifacts/schemas.js +1 -0
  49. package/dist/engine/handoff-format.d.ts +36 -0
  50. package/dist/engine/handoff-format.js +314 -0
  51. package/dist/engine/handoff-packager.d.ts +5 -3
  52. package/dist/engine/handoff-packager.js +89 -32
  53. package/dist/engine/host-tool-filter.js +20 -49
  54. package/dist/engine/human-summary.js +14 -3
  55. package/dist/engine/implementation-contract/common.d.ts +8 -3
  56. package/dist/engine/implementation-contract/common.js +9 -6
  57. package/dist/engine/implementation-contract/evaluator.js +4 -6
  58. package/dist/engine/implementation-contract/renderer.js +18 -23
  59. package/dist/engine/project-health-checker.js +2 -2
  60. package/dist/engine/readiness-checker.js +43 -5
  61. package/dist/engine/reconcile/apply-changes.js +4 -0
  62. package/dist/engine/reconcile/propagate-mirrors.d.ts +3 -0
  63. package/dist/engine/reconcile/propagate-mirrors.js +311 -0
  64. package/dist/engine/self-healing/healer.js +12 -5
  65. package/dist/engine/self-healing/strategies/lint-fix.d.ts +1 -1
  66. package/dist/engine/self-healing/strategies/lint-fix.js +4 -1
  67. package/dist/engine/self-healing/strategies/test-fix.d.ts +4 -3
  68. package/dist/engine/self-healing/strategies/test-fix.js +32 -14
  69. package/dist/engine/self-healing/strategies/typescript-fix.d.ts +1 -1
  70. package/dist/engine/self-healing/strategies/typescript-fix.js +28 -10
  71. package/dist/engine/session/checkpoint-writer.js +8 -4
  72. package/dist/engine/session/session-tracker.d.ts +5 -16
  73. package/dist/engine/session/session-tracker.js +29 -26
  74. package/dist/engine/session-state/writer.js +3 -11
  75. package/dist/engine/skill-generator/workflow-skill-generator.d.ts +4 -2
  76. package/dist/engine/skill-generator/workflow-skill-generator.js +113 -38
  77. package/dist/engine/spec-format/bdd-parser.d.ts +1 -11
  78. package/dist/engine/spec-format/bdd-parser.js +40 -11
  79. package/dist/engine/spec-format/lean-spec-generator.js +32 -12
  80. package/dist/engine/spec-format/retired-scaffold.d.ts +3 -0
  81. package/dist/engine/spec-format/retired-scaffold.js +5 -0
  82. package/dist/engine/spec-format/technical-md-populator.d.ts +1 -0
  83. package/dist/engine/spec-format/technical-md-populator.js +27 -0
  84. package/dist/engine/spec-format/unified-spec-builder.d.ts +1 -0
  85. package/dist/engine/spec-format/unified-spec-builder.js +30 -20
  86. package/dist/engine/spec-grounding/contract.d.ts +3 -3
  87. package/dist/engine/spec-grounding/contract.js +65 -4
  88. package/dist/engine/spec-grounding/grounding-entry-parser.d.ts +5 -0
  89. package/dist/engine/spec-grounding/grounding-entry-parser.js +185 -0
  90. package/dist/engine/spec-quality/generic-output-gate.js +37 -3
  91. package/dist/engine/technical-enricher/index.js +4 -1
  92. package/dist/engine/type-safety-gate.js +3 -11
  93. package/dist/engine/universal-rules/catalog.js +0 -2
  94. package/dist/engine/universal-rules/rules/planu-workflow.js +1 -1
  95. package/dist/engine/validation/durable-validation.d.ts +3 -2
  96. package/dist/engine/validation/durable-validation.js +106 -40
  97. package/dist/engine/validation/validation-freshness.d.ts +3 -2
  98. package/dist/engine/validation/validation-freshness.js +22 -16
  99. package/dist/engine/validation/validation-worktree.js +34 -0
  100. package/dist/hosts/claude-code/ux/mcp-prompts.js +8 -8
  101. package/dist/index.js +3 -17
  102. package/dist/resources/process.js +38 -65
  103. package/dist/storage/session-state-store.js +2 -0
  104. package/dist/storage/spec-store.js +8 -1
  105. package/dist/tools/bump-spec-version.js +5 -5
  106. package/dist/tools/challenge-spec/scenarios-utils.js +2 -2
  107. package/dist/tools/configure-checkpoint-policy.js +3 -3
  108. package/dist/tools/create-spec-helpers.js +2 -5
  109. package/dist/tools/create-spec.js +21 -4
  110. package/dist/tools/facilitate.js +3 -6
  111. package/dist/tools/git/branch-ops.js +16 -4
  112. package/dist/tools/github-release-handler.js +1 -20
  113. package/dist/tools/init-project/agents-md-writer.js +6 -6
  114. package/dist/tools/init-project/claude-md-generator.d.ts +0 -1
  115. package/dist/tools/init-project/claude-md-generator.js +0 -35
  116. package/dist/tools/init-project/handler.js +7 -5
  117. package/dist/tools/init-project/per-client-files-writer.js +5 -5
  118. package/dist/tools/init-project/planu-workflow-generator.js +2 -13
  119. package/dist/tools/init-project/portable-index-reconciler.d.ts +2 -1
  120. package/dist/tools/init-project/portable-index-reconciler.js +7 -2
  121. package/dist/tools/init-project/rules-generator.d.ts +0 -11
  122. package/dist/tools/init-project/rules-generator.js +4 -85
  123. package/dist/tools/init-project/scaffold-writer.d.ts +0 -1
  124. package/dist/tools/init-project/scaffold-writer.js +1 -12
  125. package/dist/tools/init-project/skills-multi-teammate-review-writer.js +1 -1
  126. package/dist/tools/jobs/handlers.d.ts +0 -6
  127. package/dist/tools/jobs/handlers.js +13 -40
  128. package/dist/tools/list-specs.js +6 -21
  129. package/dist/tools/multi-teammate-review.js +1 -14
  130. package/dist/tools/package-handoff.d.ts +3 -2
  131. package/dist/tools/package-handoff.js +153 -427
  132. package/dist/tools/reconcile-session-safeguard-hook.js +22 -15
  133. package/dist/tools/register-platform-tools/design-stack-tools.js +3 -3
  134. package/dist/tools/register-sdd-tools.js +0 -23
  135. package/dist/tools/register-spec-tools/analysis-tools.js +22 -9
  136. package/dist/tools/register-spec-tools/core-spec-tools.js +115 -70
  137. package/dist/tools/schemas/index.d.ts +1 -1
  138. package/dist/tools/schemas/index.js +1 -1
  139. package/dist/tools/schemas/output-schemas.d.ts +1 -18
  140. package/dist/tools/schemas/output-schemas.js +1 -11
  141. package/dist/tools/schemas/package-handoff-output-schema.d.ts +40 -0
  142. package/dist/tools/schemas/package-handoff-output-schema.js +68 -0
  143. package/dist/tools/schemas/validate-output-schema.d.ts +1 -2
  144. package/dist/tools/schemas/validate-output-schema.js +3 -2
  145. package/dist/tools/session-checkpoint.js +2 -2
  146. package/dist/tools/skill-registry/index.d.ts +0 -1
  147. package/dist/tools/skill-registry/index.js +0 -1
  148. package/dist/tools/sync-spec-state-handler.d.ts +2 -2
  149. package/dist/tools/sync-spec-state-handler.js +23 -3
  150. package/dist/tools/tool-registry/core-tools.js +72 -164
  151. package/dist/tools/tool-registry/group-infra.js +2 -39
  152. package/dist/tools/tool-registry/group-quality-compliance.js +20 -15
  153. package/dist/tools/tool-registry-helpers.js +6 -2
  154. package/dist/tools/update-status/batch.js +17 -0
  155. package/dist/tools/update-status/dod-gates.js +14 -1
  156. package/dist/tools/update-status/done-receipt-verifier.d.ts +22 -0
  157. package/dist/tools/update-status/done-receipt-verifier.js +56 -26
  158. package/dist/tools/update-status/evidence-gate.js +10 -21
  159. package/dist/tools/update-status/file-sync.d.ts +1 -0
  160. package/dist/tools/update-status/file-sync.js +6 -0
  161. package/dist/tools/update-status/index.d.ts +6 -0
  162. package/dist/tools/update-status/index.js +39 -44
  163. package/dist/tools/update-status-actions.js +2 -7
  164. package/dist/tools/validate.js +34 -28
  165. package/dist/transports/oauth-validator.js +12 -1
  166. package/dist/transports/transport-factory.d.ts +2 -1
  167. package/dist/transports/transport-factory.js +19 -0
  168. package/dist/types/common/primitives.d.ts +2 -0
  169. package/dist/types/durable-job.d.ts +45 -0
  170. package/dist/types/durable-validation.d.ts +1 -1
  171. package/dist/types/evidence-autofill.d.ts +13 -1
  172. package/dist/types/handoff-artifacts.d.ts +2 -1
  173. package/dist/types/readiness.d.ts +8 -1
  174. package/dist/types/reconcile.d.ts +6 -0
  175. package/dist/types/skill-registry.d.ts +26 -1
  176. package/dist/types/spec/core.d.ts +5 -0
  177. package/dist/types/spec/inputs.d.ts +2 -2
  178. package/dist/types/spec-format.d.ts +11 -1
  179. package/dist/types/spec-grounding.d.ts +26 -0
  180. package/dist/types/transport.d.ts +1 -0
  181. package/dist/types/validation-receipt.d.ts +27 -0
  182. package/package.json +3 -2
  183. package/planu-plugin.json +13 -26
  184. package/src/i18n/messages/en.json +1 -1
  185. package/src/i18n/messages/es.json +1 -1
  186. package/src/i18n/messages/pt.json +1 -1
  187. package/dist/engine/skill-generator/conventions-hasher.d.ts +0 -7
  188. package/dist/engine/skill-generator/conventions-hasher.js +0 -24
  189. package/dist/engine/universal-rules/rules/agent-teams.d.ts +0 -3
  190. package/dist/engine/universal-rules/rules/agent-teams.js +0 -63
  191. package/dist/tools/clarify-requirements/multiple-choice.d.ts +0 -30
  192. package/dist/tools/clarify-requirements/multiple-choice.js +0 -306
  193. package/dist/tools/clarify-requirements/questions-context.d.ts +0 -9
  194. package/dist/tools/clarify-requirements/questions-context.js +0 -74
  195. package/dist/tools/clarify-requirements/questions.d.ts +0 -16
  196. package/dist/tools/clarify-requirements/questions.js +0 -96
  197. package/dist/tools/clarify-requirements.d.ts +0 -4
  198. package/dist/tools/clarify-requirements.js +0 -314
  199. package/dist/tools/code-graph-handler.d.ts +0 -6
  200. package/dist/tools/code-graph-handler.js +0 -72
  201. package/dist/tools/create-rule.d.ts +0 -4
  202. package/dist/tools/create-rule.js +0 -107
  203. package/dist/tools/feedback-handler.d.ts +0 -7
  204. package/dist/tools/feedback-handler.js +0 -150
  205. package/dist/tools/semantic-search-handler.d.ts +0 -7
  206. package/dist/tools/semantic-search-handler.js +0 -71
  207. package/dist/tools/skill-registry/search.d.ts +0 -11
  208. package/dist/tools/skill-registry/search.js +0 -159
  209. package/dist/tools/tool-registry/deprecated-stubs.d.ts +0 -19
  210. package/dist/tools/tool-registry/deprecated-stubs.js +0 -65
@@ -3,8 +3,6 @@ import { z } from 'zod';
3
3
  import { safeGoverned, safeTracked } from '../safe-handler.js';
4
4
  import { projectIdSchema, withProject } from '../tool-registry-helpers.js';
5
5
  import { t } from '../../i18n/index.js';
6
- import { makeDeprecationStub } from './deprecated-stubs.js';
7
- import { registerFromEntries } from '../tool-entry.js';
8
6
  // ── Compliance (register-compliance-tools.ts) ─────────────────────────────────
9
7
  import { handleCheckComplianceUnified, handleCheckComplianceLegacyShim, handleComplianceScoreReportShim, handleComplianceGapAnalysisShim, handleVerifySpecComplianceShim, } from '../compliance-unified-handler.js';
10
8
  import { handleConfigureCompliance } from '../compliance-handler.js';
@@ -42,7 +40,7 @@ import { handleDefineLintRule, handleListLintRules, handleRunSpecLint, handleDel
42
40
  // ── Readiness (register-readiness-tools.ts) ───────────────────────────────────
43
41
  import { handleCheckReadiness } from '../check-readiness.js';
44
42
  import { handlePackageHandoff } from '../package-handoff.js';
45
- import { CheckReadinessOutputSchema, SpecIdSchema } from '../schemas/index.js';
43
+ import { CheckReadinessOutputSchema, SpecIdSchema, PackageHandoffOutputSchema, } from '../schemas/index.js';
46
44
  // ── Approval (register-approval-tools.ts) ────────────────────────────────────
47
45
  import { handleConfigureApprovalPolicy, handleApproveSpec, handleRequestChanges, handleApprovalStatus, handleIssueReviewerToken, } from '../approval-handler.js';
48
46
  // ── SPEC-961: Architecture lint ─────────────────────────────────────────────
@@ -831,7 +829,7 @@ export function registerQualityComplianceGroupTools(s) {
831
829
  description: t('tools.check_readiness.description'),
832
830
  inputSchema: {
833
831
  ...projectIdSchema,
834
- specId: SpecIdSchema.describe('Spec ID to evaluate'),
832
+ specId: SpecIdSchema.describe('Spec ID to evaluate. Must match pattern ^SPEC-\\d+$, max 50 characters.'),
835
833
  mode: z
836
834
  .enum(['strict', 'lenient'])
837
835
  .optional()
@@ -952,7 +950,15 @@ export function registerQualityComplianceGroupTools(s) {
952
950
  description: t('tools.package_handoff.description'),
953
951
  inputSchema: {
954
952
  ...projectIdSchema,
955
- specId: SpecIdSchema.describe('Spec ID to generate handoff package for'),
953
+ specId: SpecIdSchema.describe('Spec ID to generate handoff package for. Must match pattern ^SPEC-\\d+$, max 50 characters.'),
954
+ },
955
+ outputSchema: PackageHandoffOutputSchema,
956
+ annotations: {
957
+ title: 'Submit Durable Handoff Packaging',
958
+ readOnlyHint: false,
959
+ destructiveHint: false,
960
+ idempotentHint: true,
961
+ openWorldHint: false,
956
962
  },
957
963
  }, safeGoverned('package_handoff', withProject((args) => handlePackageHandoff(args))));
958
964
  // ── Approval ─────────────────────────────────────────────────────────────────
@@ -1041,31 +1047,35 @@ export function registerQualityComplianceGroupTools(s) {
1041
1047
  description: 'Request changes on a spec. The spec is moved back to draft and prior approvals are cleared. ' +
1042
1048
  'A comment explaining the required changes is mandatory.',
1043
1049
  inputSchema: {
1044
- projectPath: z.string().min(1).max(4096).describe('Absolute path to the project root.'),
1050
+ projectPath: z
1051
+ .string()
1052
+ .min(1)
1053
+ .max(4096)
1054
+ .describe('Absolute path to the project root. Max 4096 characters.'),
1045
1055
  specId: z
1046
1056
  .string()
1047
1057
  .min(1)
1048
1058
  .max(500)
1049
- .describe('Spec ID to request changes on (e.g. SPEC-042).'),
1059
+ .describe('Spec ID to request changes on (e.g. SPEC-042). Max 500 characters.'),
1050
1060
  reviewer: z
1051
1061
  .string()
1052
1062
  .trim()
1053
1063
  .min(1)
1054
1064
  .max(500)
1055
- .describe('Name, email, or username of the reviewer requesting changes.'),
1065
+ .describe('Name, email, or username of the reviewer requesting changes. Max 500 characters.'),
1056
1066
  comment: z
1057
1067
  .string()
1058
1068
  .trim()
1059
1069
  .min(1)
1060
1070
  .max(10_000)
1061
- .describe('Required explanation of what needs to change before re-approval.'),
1071
+ .describe('Required explanation of what needs to change before re-approval. Max 10000 characters.'),
1062
1072
  role: z
1063
1073
  .string()
1064
1074
  .trim()
1065
1075
  .min(1)
1066
1076
  .max(500)
1067
1077
  .optional()
1068
- .describe('Role of the reviewer (e.g. "tech-lead", "qa").'),
1078
+ .describe('Role of the reviewer (e.g. "tech-lead", "qa"). Max 500 characters.'),
1069
1079
  },
1070
1080
  annotations: { title: 'Request Changes', destructiveHint: true },
1071
1081
  }, safeGoverned('request_changes', (args) => handleRequestChanges(args)));
@@ -1188,10 +1198,5 @@ export function registerQualityComplianceGroupTools(s) {
1188
1198
  .describe('Absolute path to the project root. Used to detect language and test framework.'),
1189
1199
  },
1190
1200
  }, safeTracked('run_mutation_hints', async (args) => handleRunMutationHints({ projectPath: args.projectPath })));
1191
- // ── SPEC-331: Multi-Agent Orchestration Plan (migrated to skill, SPEC-658) ────
1192
- // Tool stays registered for backward compat; callers receive a deprecation notice.
1193
- registerFromEntries(s, [
1194
- makeDeprecationStub('generate_orchestration_plan', '.claude/skills/parallel-safe.md', 'parallel-safe'),
1195
- ]);
1196
1201
  }
1197
1202
  //# sourceMappingURL=group-quality-compliance.js.map
@@ -5,12 +5,16 @@ import { resolveProjectId, missingProjectIdError } from './resolve-project-id.js
5
5
  // ── Shared schema fragments ──────────────────────────────────────────────────
6
6
  /** Standard dual projectId/projectPath optional Zod fields. Spread into inputSchema. */
7
7
  export const projectIdSchema = {
8
- projectId: z.string().max(500).optional().describe('Project ID hash. Prefer projectPath.'),
8
+ projectId: z
9
+ .string()
10
+ .max(500)
11
+ .optional()
12
+ .describe('Project ID hash. Prefer projectPath. Max 500 characters.'),
9
13
  projectPath: z
10
14
  .string()
11
15
  .max(4096)
12
16
  .optional()
13
- .describe('Absolute path to project root. Derives projectId automatically.'),
17
+ .describe('Absolute path to project root. Derives projectId automatically. Max 4096 characters.'),
14
18
  };
15
19
  /**
16
20
  * Wraps a handler that requires a resolved projectId.
@@ -90,6 +90,22 @@ export async function handleUpdateStatusBatch(input) {
90
90
  },
91
91
  };
92
92
  }
93
+ if (input.status === 'approved' || input.status === 'implementing') {
94
+ const missingIds = uniqueSpecIds.filter((specId) => input.evidence?.[specId] === undefined);
95
+ if (missingIds.length > 0) {
96
+ const fixHint = `Supply an evidence entry for each of: ${missingIds.join(', ')}.`;
97
+ return {
98
+ content: [
99
+ {
100
+ type: 'text',
101
+ text: `BATCH_EVIDENCE_REQUIRED: Missing model-routing evidence for: ${missingIds.join(', ')}.\nFix: ${fixHint}`,
102
+ },
103
+ ],
104
+ isError: true,
105
+ structuredContent: { error: 'BATCH_EVIDENCE_REQUIRED', code: 422, missingIds, fixHint },
106
+ };
107
+ }
108
+ }
93
109
  const updated = [];
94
110
  const skipped = [];
95
111
  const failed = [];
@@ -98,6 +114,7 @@ export async function handleUpdateStatusBatch(input) {
98
114
  try {
99
115
  for (const specId of uniqueSpecIds) {
100
116
  const result = await handleUpdateStatus({
117
+ ...input.evidence?.[specId],
101
118
  specId,
102
119
  status: input.status,
103
120
  projectId: resolved.projectId,
@@ -48,6 +48,7 @@ export const implementationReviewTemplate = lazyTemplate(() => renderSchemaSkele
48
48
  schema_version: HANDOFF_ARTIFACT_TEMPLATE_SCHEMA_VERSION,
49
49
  reviewedAt: HANDOFF_ARTIFACT_TEMPLATE_TIMESTAMP,
50
50
  'reviewer.agent': 'planu-implementation-reviewer',
51
+ 'reviewer.sessionId': '<your-session-id>',
51
52
  },
52
53
  }));
53
54
  export const validationReportTemplate = lazyTemplate(() => renderSchemaSkeleton(ValidationReportV1Schema, {
@@ -738,9 +739,21 @@ export async function checkImplementationReviewGate(specId, projectId, _force) {
738
739
  specId,
739
740
  error: 'implementation_review_self_review',
740
741
  message: 'The implementation reviewer must be a different identity from the implementer. Self-review is blocked.',
741
- blockers: [`Implementer and reviewer share the identity "${verified.implementingActor}".`],
742
+ blockers: [
743
+ `Implementer and reviewer share the session identity "${verified.implementingActor}" (verified.implementingActor).`,
744
+ ],
742
745
  fixHint: 'Have a different, independent agent write the implementation review evidence, then retry done.',
743
746
  });
747
+ case 'identity_unverifiable':
748
+ return implementationReviewGateError({
749
+ specId,
750
+ error: 'implementation_review_identity_unverifiable',
751
+ message: 'Reviewer independence is unverifiable. reviewer.sessionId is missing, blank, or a placeholder, or the implementing actor could not be resolved from the transition log.',
752
+ blockers: [
753
+ `reviewer.sessionId was ${JSON.stringify(verified.review?.reviewer.sessionId ?? null)}. implementingActor was ${JSON.stringify(verified.implementingActor ?? null)}.`,
754
+ ],
755
+ fixHint: 'Have planu-implementation-reviewer record its real session identity in reviewer.sessionId, and have the orchestrator pass a real actor on update_status(implementing), then retry done.',
756
+ });
744
757
  case 'unreadable':
745
758
  return implementationReviewGateError({
746
759
  specId,
@@ -1,6 +1,28 @@
1
+ import { RuntimeDatabase } from '../../storage/runtime-db.js';
1
2
  import type { DurableJobRecord } from '../../types/durable-job.js';
3
+ import type { ValidationReceipt, ValidationReceiptBindings, ValidationReceiptVerification } from '../../types/validation-receipt.js';
4
+ import { ValidationReceiptStore } from '../../engine/validation/validation-receipt.js';
2
5
  import { type ValidationFreshnessLease } from '../../engine/validation/validation-freshness.js';
3
6
  export { doneReceiptBindingsDifferOnlyInEvidence } from '../../engine/validation/durable-validation.js';
7
+ export type CurrentBindingsResolution = {
8
+ ok: true;
9
+ currentBindings: ValidationReceiptBindings;
10
+ verification: ValidationReceiptVerification;
11
+ } | {
12
+ ok: false;
13
+ reason: string;
14
+ };
15
+ export declare function resolveVerifiedCurrentBindings(args: {
16
+ database: RuntimeDatabase;
17
+ store: ValidationReceiptStore;
18
+ identity: {
19
+ projectId: string;
20
+ specId: string;
21
+ projectPath?: string;
22
+ specPath?: string;
23
+ };
24
+ receipt: ValidationReceipt;
25
+ }): Promise<CurrentBindingsResolution>;
4
26
  export type DoneReceiptVerifier = (identity: {
5
27
  projectId: string;
6
28
  specId: string;
@@ -59,6 +59,58 @@ import { ValidationReceiptStore, LOCAL_VALIDATION_RECEIPT_ISSUER, createValidati
59
59
  import { beginValidationFreshnessBarrier, bindValidationFreshnessLease, captureValidationFreshnessLease, closeValidationFreshnessLease, } from '../../engine/validation/validation-freshness.js';
60
60
  import { computeDurableValidationBindings, doneReceiptBindingsDifferOnlyInEvidence, toValidationReceiptBindings, } from '../../engine/validation/durable-validation.js';
61
61
  export { doneReceiptBindingsDifferOnlyInEvidence } from '../../engine/validation/durable-validation.js';
62
+ export async function resolveVerifiedCurrentBindings(args) {
63
+ const { database, store, identity, receipt } = args;
64
+ let currentBindings = await store.readCurrentBindings(identity.projectId, identity.specId);
65
+ if (!currentBindings) {
66
+ if (!identity.projectPath || !identity.specPath) {
67
+ return { ok: false, reason: 'stale-bindings' };
68
+ }
69
+ const projectPath = identity.projectPath;
70
+ const specPath = isAbsolute(identity.specPath)
71
+ ? identity.specPath
72
+ : join(projectPath, identity.specPath);
73
+ const observed = await computeDurableValidationBindings({
74
+ canonicalProjectId: identity.projectId,
75
+ specId: identity.specId,
76
+ projectPath,
77
+ specPath,
78
+ });
79
+ const observedBindings = toValidationReceiptBindings(observed.bindings, observed.artifactDigests);
80
+ const isExactMatch = Boolean(receipt.bindings) &&
81
+ JSON.stringify(observedBindings) === JSON.stringify(receipt.bindings);
82
+ if (!isExactMatch &&
83
+ !doneReceiptBindingsDifferOnlyInEvidence(observedBindings, receipt.bindings)) {
84
+ return { ok: false, reason: 'stale-bindings' };
85
+ }
86
+ await store.projectCurrentBindings(observedBindings);
87
+ currentBindings = observedBindings;
88
+ }
89
+ const { authority } = createValidationReceiptAuthority({
90
+ database,
91
+ workspaceId: 'global',
92
+ projectId: identity.projectId,
93
+ issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
94
+ });
95
+ let verification = await authority.verifyCurrent({
96
+ ...currentBindings,
97
+ projectId: identity.projectId,
98
+ specId: identity.specId,
99
+ });
100
+ const evidenceOnlyDrift = verification.reason === 'stale-bindings' &&
101
+ doneReceiptBindingsDifferOnlyInEvidence(currentBindings, receipt.bindings);
102
+ if (evidenceOnlyDrift) {
103
+ verification = await authority.verifyCurrent({
104
+ ...receipt.bindings,
105
+ projectId: identity.projectId,
106
+ specId: identity.specId,
107
+ });
108
+ }
109
+ if (!verification.valid) {
110
+ return { ok: false, reason: verification.reason ?? 'invalid-receipt' };
111
+ }
112
+ return { ok: true, currentBindings, verification };
113
+ }
62
114
  export function completedJobPublishesReceipt(job, receiptId) {
63
115
  if (job?.state !== 'completed' || !job.result || typeof job.result !== 'object') {
64
116
  return false;
@@ -83,33 +135,11 @@ export async function verifyCurrentDoneReceipt(identity) {
83
135
  if (receipt.issuer !== LOCAL_VALIDATION_RECEIPT_ISSUER) {
84
136
  return { valid: false, reason: 'unknown-issuer' };
85
137
  }
86
- const currentBindings = await store.readCurrentBindings(identity.projectId, identity.specId);
87
- if (!currentBindings) {
88
- return { valid: false, reason: 'stale-bindings' };
89
- }
90
- const { authority } = createValidationReceiptAuthority({
91
- database,
92
- workspaceId: 'global',
93
- projectId: identity.projectId,
94
- issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
95
- });
96
- let verification = await authority.verifyCurrent({
97
- ...currentBindings,
98
- projectId: identity.projectId,
99
- specId: identity.specId,
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
- }
110
- if (!verification.valid) {
111
- return verification;
138
+ const resolution = await resolveVerifiedCurrentBindings({ database, store, identity, receipt });
139
+ if (!resolution.ok) {
140
+ return { valid: false, reason: resolution.reason };
112
141
  }
142
+ const { currentBindings, verification } = resolution;
113
143
  const receiptJobExists = database
114
144
  .listJobs(identity.projectId, 'global')
115
145
  .some((job) => completedJobPublishesReceipt(job, receipt.receiptId));
@@ -63,12 +63,12 @@ import { extractAcceptanceCriteriaTexts } from '../../engine/spec-format/accepta
63
63
  import { formatKeyValue } from '../output-formatter.js';
64
64
  import { RuntimeDatabase } from '../../storage/runtime-db.js';
65
65
  import { resolveStorageLayout } from '../../storage/storage-layout.js';
66
- import { ValidationReceiptStore, LOCAL_VALIDATION_RECEIPT_ISSUER, createValidationReceiptAuthority, readCurrentValidationReceipt, } from '../../engine/validation/validation-receipt.js';
66
+ import { ValidationReceiptStore, LOCAL_VALIDATION_RECEIPT_ISSUER, readCurrentValidationReceipt, } from '../../engine/validation/validation-receipt.js';
67
67
  import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
68
68
  import { captureValidationFreshnessLease } from '../../engine/validation/validation-freshness.js';
69
69
  import { computeDurableValidationBindings, toValidationReceiptBindings, } from '../../engine/validation/durable-validation.js';
70
70
  import { checkReconciliationFreshness } from '../../engine/evidence-gates/reconciliation-freshness.js';
71
- import { doneReceiptBindingsDifferOnlyInEvidence } from './done-receipt-verifier.js';
71
+ import { doneReceiptBindingsDifferOnlyInEvidence, resolveVerifiedCurrentBindings, } from './done-receipt-verifier.js';
72
72
  import { extractCanonicalFileOwnership } from '../../engine/handoff-packager.js';
73
73
  import { generateTaskPlanSkeleton, writeEvidenceSkeleton, } from '../../engine/evidence-gates/evidence-skeletons.js';
74
74
  import { extractSection } from '../../engine/spec-format/markdown-sections.js';
@@ -127,10 +127,13 @@ export async function checkLifecycleEvidenceTransitionGate(args) {
127
127
  })
128
128
  : null;
129
129
  if (receiptResult && !receiptResult.ok) {
130
+ const message = receiptResult.reason === 'stale-bindings'
131
+ ? 'stale-bindings: validation inputs changed after the receipt was issued. Re-run validate, then retry done.'
132
+ : receiptResult.reason;
130
133
  return evidenceGateError({
131
134
  specId: args.specId,
132
135
  transition: args.transition,
133
- issues: [{ code: 'evidence_provenance_invalid', message: receiptResult.reason }],
136
+ issues: [{ code: 'evidence_provenance_invalid', message }],
134
137
  requiredContractKinds: [],
135
138
  });
136
139
  }
@@ -244,25 +247,11 @@ async function readVerifiedCurrentReceipt(identity) {
244
247
  if (receipt.issuer !== LOCAL_VALIDATION_RECEIPT_ISSUER) {
245
248
  return { ok: false, reason: 'unknown-issuer' };
246
249
  }
247
- const currentBindings = await store.readCurrentBindings(identity.projectId, identity.specId);
248
- if (!currentBindings) {
249
- return { ok: false, reason: 'stale-bindings' };
250
- }
251
- const { authority } = createValidationReceiptAuthority({
252
- database,
253
- workspaceId: 'global',
254
- projectId: identity.projectId,
255
- issuerId: LOCAL_VALIDATION_RECEIPT_ISSUER,
256
- });
257
- const verification = await authority.verifyCurrent({
258
- ...currentBindings,
259
- projectId: identity.projectId,
260
- specId: identity.specId,
261
- });
262
- if (!verification.valid) {
263
- return { ok: false, reason: verification.reason ?? 'invalid-receipt' };
250
+ const resolution = await resolveVerifiedCurrentBindings({ database, store, identity, receipt });
251
+ if (!resolution.ok) {
252
+ return { ok: false, reason: resolution.reason };
264
253
  }
265
- if (!(await currentBindingsAreFresh(identity, currentBindings))) {
254
+ if (!(await currentBindingsAreFresh(identity, resolution.currentBindings))) {
266
255
  return { ok: false, reason: 'stale-bindings' };
267
256
  }
268
257
  return { ok: true, receipt };
@@ -13,6 +13,7 @@ export interface FrontmatterSyncWarning {
13
13
  export declare function syncSpecFiles(updatedSpec: Spec, _currentStatus: SpecStatus, newStatus: SpecStatus, projectPath?: string, qualityWarnings?: readonly string[]): Promise<{
14
14
  warning?: FrontmatterSyncWarning;
15
15
  title?: string;
16
+ driftReconciled?: boolean;
16
17
  }>;
17
18
  export declare function tryReconcile(newStatus: string, specId: string, projectId: string): Promise<string | null>;
18
19
  /**
@@ -131,6 +131,7 @@ function assertSpecBodyIntegrity(args) {
131
131
  throw new Error(`SPEC_INTEGRITY_WRITE_FAILED: refusing to write ${args.specId} because update_status(${args.newStatus}) would shrink spec.md body by ${String(lost)} characters without an explicit migration marker.`);
132
132
  }
133
133
  }
134
+ const FROZEN_TERMINAL_STATUSES = ['done', 'discarded'];
134
135
  export async function syncSpecFiles(updatedSpec, _currentStatus, newStatus, projectPath, qualityWarnings = []) {
135
136
  let warning;
136
137
  let refreshedTitle;
@@ -148,6 +149,11 @@ export async function syncSpecFiles(updatedSpec, _currentStatus, newStatus, proj
148
149
  parsedMetadata.title !== updatedSpec.title) {
149
150
  refreshedTitle = parsedMetadata.title;
150
151
  }
152
+ if (FROZEN_TERMINAL_STATUSES.includes(newStatus) && parsedMetadata.status === newStatus) {
153
+ return refreshedTitle
154
+ ? { title: refreshedTitle, driftReconciled: true }
155
+ : { driftReconciled: true };
156
+ }
151
157
  const { updateFrontmatterField } = await import('../../engine/frontmatter-parser.js');
152
158
  let updatedContent = updateFrontmatterField(specContent, 'status', newStatus);
153
159
  if (qualityWarnings.length > 0) {
@@ -1,5 +1,6 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import type { UpdateStatusInput, ToolResult, Actuals, Spec, ReconciliationInvocationContext } from '../../types/index.js';
3
+ import { type FrontmatterSyncWarning } from './file-sync.js';
3
4
  import { type GapScore } from '../../engine/code-scanner/index.js';
4
5
  type LifecycleAuthorityRoute = {
5
6
  route: 'ordinary';
@@ -27,6 +28,11 @@ export declare function synthesizeActuals(actuals: Partial<Actuals> | undefined,
27
28
  export declare function computeTransitionRootBinding(projectPath: string): string;
28
29
  /** Recover durable post-commit work for every registered project after server startup. */
29
30
  export declare function recoverPendingPostCommitTasksAtStartup(): Promise<void>;
31
+ export declare function resolveTransitionEventAudit(driftReconciled: boolean, baseReason: string | undefined, baseMeta: Record<string, unknown>): {
32
+ reason: string | undefined;
33
+ meta: Record<string, unknown>;
34
+ };
35
+ export declare function buildDriftAppendFailureWarning(specId: string, specPath: string | null, error: unknown): FrontmatterSyncWarning;
30
36
  export declare function handleUpdateStatus(params: UpdateStatusInput, server?: McpServer): Promise<ToolResult>;
31
37
  export {};
32
38
  //# sourceMappingURL=index.d.ts.map
@@ -140,17 +140,6 @@ function buildAutoAdvancePlan(currentStatus, targetStatus) {
140
140
  stepsExecuted,
141
141
  };
142
142
  }
143
- /**
144
- * SPEC-1356 (T-A3): auto-generate traceability-matrix.json when it is missing,
145
- * BEFORE the evidence gate reads it for a 'done' transition. Never overwrites an
146
- * existing artifact. Tolerates every internal failure (falls through to the
147
- * ordinary evidence gate, which will report the real missing-evidence error).
148
- *
149
- * When autofill actually writes a fresh file, the newly-created evidence has not
150
- * been bound by validate's durable receipt yet, so this blocks once with a clear
151
- * "re-run validate" message instead of letting a stale-relative-to-receipt
152
- * traceability matrix slide through the done gates silently.
153
- */
154
143
  async function runTraceabilityAutofillGate(args) {
155
144
  try {
156
145
  const specPath = isAbsolute(args.spec.specPath) || !args.projectPath
@@ -158,7 +147,7 @@ async function runTraceabilityAutofillGate(args) {
158
147
  : join(args.projectPath, args.spec.specPath);
159
148
  const specBody = await readSpecContent(specPath);
160
149
  if (specBody === null) {
161
- return null;
150
+ return { warning: null };
162
151
  }
163
152
  const result = await autofillTraceabilityMatrix({
164
153
  projectId: args.projectId,
@@ -168,29 +157,16 @@ async function runTraceabilityAutofillGate(args) {
168
157
  ownSpecMdPath: toRepoRelativePath(args.spec.specPath, args.projectPath),
169
158
  });
170
159
  if (!result.written) {
171
- return null;
160
+ return { warning: null };
172
161
  }
173
162
  return {
174
- content: [
175
- {
176
- type: 'text',
177
- text: `traceability-matrix.json was auto-generated at ${result.path} (${String(result.rowCount)} rows). ` +
178
- 'Run validate to bind this evidence, then retry update_status(done).',
179
- },
180
- ],
181
- isError: true,
182
- structuredContent: {
183
- error: 'traceability_matrix_autofilled',
184
- code: 422,
185
- context: { specId: args.specId, path: result.path, rowCount: result.rowCount },
186
- fixHint: 'Run validate so the new traceability-matrix.json is bound, then retry done.',
187
- },
163
+ warning: `traceability-matrix.json auto-generated (${String(result.rowCount)} rows) and bound via evidence-only rebind`,
188
164
  };
189
165
  }
190
166
  catch (error) {
191
167
  // reliability-optional: EVIDENCE_AUTOFILL_GATE — falls through to the ordinary evidence gate
192
168
  reportClassifiedDegradation('EVIDENCE_AUTOFILL_GATE', error);
193
- return null;
169
+ return { warning: null };
194
170
  }
195
171
  }
196
172
  /**
@@ -603,6 +579,18 @@ function buildPersistenceFailureResponse(args) {
603
579
  },
604
580
  };
605
581
  }
582
+ export function resolveTransitionEventAudit(driftReconciled, baseReason, baseMeta) {
583
+ return driftReconciled
584
+ ? { reason: 'drift-reconciliation', meta: { ...baseMeta, driftReconciled: true } }
585
+ : { reason: baseReason, meta: baseMeta };
586
+ }
587
+ export function buildDriftAppendFailureWarning(specId, specPath, error) {
588
+ return {
589
+ specId,
590
+ specPath,
591
+ reason: `drift-reconciliation audit marker not recorded: ${error instanceof Error ? error.message : String(error)}`,
592
+ };
593
+ }
606
594
  async function commitTransitionWithNarrowLock(args) {
607
595
  return withApprovalSpecLock(args.projectPath, args.specId, async () => {
608
596
  const lock = await acquireLock(args.projectPath, args.specId, {
@@ -671,7 +659,12 @@ async function commitTransitionWithNarrowLock(args) {
671
659
  updatedSpec.title = syncResult.title;
672
660
  await specStore.updateSpec(args.projectId, args.specId, { title: syncResult.title });
673
661
  }
674
- return { ok: true, transitionRecord, updatedSpec };
662
+ return {
663
+ ok: true,
664
+ transitionRecord,
665
+ updatedSpec,
666
+ driftReconciled: syncResult.driftReconciled === true,
667
+ };
675
668
  }
676
669
  try {
677
670
  await rollbackTransitionSpec(transitionRecord);
@@ -1011,14 +1004,7 @@ export async function handleUpdateStatus(params, server) {
1011
1004
  sddRoutingGate.gateResults.sddModelRouting = 'forced';
1012
1005
  }
1013
1006
  }
1014
- // SPEC-1356 (T-A3): auto-generate traceability-matrix.json before the
1015
- // evidence gate below reads it, so a spec never fails 'done' purely
1016
- // because nobody hand-wrote the traceability artifact. Never overwrites
1017
- // an existing file. Freshness caveat: when autofill actually writes a
1018
- // new file, its evidence has not yet been bound by validate's durable
1019
- // receipt, so we block once and ask the caller to re-run validate
1020
- // rather than risk certifying stale bindings. See evidence-autofill.ts
1021
- // for the full rationale.
1007
+ let traceabilityAutofillWarning = null;
1022
1008
  if (plannedStatuses.includes('done') &&
1023
1009
  !shouldSkipGateForExplicitLegacyHarness('evidence')) {
1024
1010
  const autofillGate = await runTraceabilityAutofillGate({
@@ -1027,9 +1013,7 @@ export async function handleUpdateStatus(params, server) {
1027
1013
  projectId,
1028
1014
  projectPath: effectiveGatePath,
1029
1015
  });
1030
- if (autofillGate) {
1031
- return autofillGate;
1032
- }
1016
+ traceabilityAutofillWarning = autofillGate.warning;
1033
1017
  }
1034
1018
  // SPEC-1054: BDD/SDD evidence gates. Non-trivial specs must carry
1035
1019
  // Discovery before approval, task-plan before implementation, and
@@ -1327,7 +1311,9 @@ export async function handleUpdateStatus(params, server) {
1327
1311
  : undefined;
1328
1312
  const qualityWarningsToWrite = newStatus === 'approved'
1329
1313
  ? [...readinessGate.qualityWarnings, ...formatGate.qualityWarnings]
1330
- : [];
1314
+ : newStatus === 'done' && traceabilityAutofillWarning
1315
+ ? [...new Set([...(spec.qualityWarnings ?? []), traceabilityAutofillWarning])]
1316
+ : [];
1331
1317
  const persistence = await commitTransitionWithNarrowLock({
1332
1318
  projectPath: effectiveGatePath ?? projectId,
1333
1319
  projectId,
@@ -1351,7 +1337,7 @@ export async function handleUpdateStatus(params, server) {
1351
1337
  if (!persistence.ok) {
1352
1338
  return persistence.error;
1353
1339
  }
1354
- const { transitionRecord, updatedSpec } = persistence;
1340
+ const { transitionRecord, updatedSpec, driftReconciled } = persistence;
1355
1341
  if (updatedSpec.title && updatedSpec.title !== spec.title) {
1356
1342
  spec.title = updatedSpec.title;
1357
1343
  }
@@ -1361,6 +1347,11 @@ export async function handleUpdateStatus(params, server) {
1361
1347
  if (newStatus === 'done') {
1362
1348
  await runTransitionMaintenance(transitionProjectPath);
1363
1349
  }
1350
+ const transitionEventAudit = resolveTransitionEventAudit(driftReconciled, params.reason, {
1351
+ ...transitionEvidenceMeta,
1352
+ transitionId,
1353
+ committedAt,
1354
+ });
1364
1355
  await appendTransitionEvent({
1365
1356
  projectId,
1366
1357
  specId,
@@ -1368,14 +1359,17 @@ export async function handleUpdateStatus(params, server) {
1368
1359
  from: originalStatus,
1369
1360
  to: newStatus,
1370
1361
  actor,
1371
- reason: params.reason,
1362
+ reason: transitionEventAudit.reason,
1372
1363
  sessionId: params.sessionId,
1373
1364
  modelId: params.modelId,
1374
1365
  gateResults: sddRoutingGate.gateResults,
1375
- meta: { ...transitionEvidenceMeta, transitionId, committedAt },
1366
+ meta: transitionEventAudit.meta,
1376
1367
  }).catch((error) => {
1377
1368
  /* reliability-optional: SDD_ROUTING_REPORT — never block committed acknowledgement */
1378
1369
  reportClassifiedDegradation('SDD_ROUTING_REPORT', error);
1370
+ if (driftReconciled) {
1371
+ frontmatterSyncWarnings.push(buildDriftAppendFailureWarning(specId, updatedSpec.specPath, error));
1372
+ }
1379
1373
  });
1380
1374
  if (reverseTransition && params.reason) {
1381
1375
  await appendTransitionEvent({
@@ -1536,6 +1530,7 @@ export async function handleUpdateStatus(params, server) {
1536
1530
  depGuard: depGuardResult,
1537
1531
  // SPEC-698: surface frontmatter sync failures in the response
1538
1532
  frontmatterSyncWarnings: frontmatterSyncWarnings.length > 0 ? frontmatterSyncWarnings : null,
1533
+ qualityWarnings: qualityWarningsToWrite.length > 0 ? qualityWarningsToWrite : null,
1539
1534
  updatedAt: updatedSpec.updatedAt,
1540
1535
  message: ti('tools.update_status.success', { specId, status: newStatus }),
1541
1536
  ...(collector.hasEntries() ? { autopilotSummary: collector.getMessages() } : {}),
@@ -71,13 +71,8 @@ export async function runImplementingSideEffects(projectId, specId, transitionPr
71
71
  });
72
72
  }
73
73
  if (!branchResult.isError) {
74
- try {
75
- const branchData = JSON.parse(branchResult.content[0]?.text ?? '{}');
76
- autoBranch = branchData.branchName;
77
- }
78
- catch {
79
- // ignore parse error
80
- }
74
+ const branchName = branchResult.structuredContent?.branchName;
75
+ autoBranch = typeof branchName === 'string' ? branchName : undefined;
81
76
  }
82
77
  }
83
78
  // -------------------------------------------------------------------------