@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
@@ -16,6 +16,10 @@ async function findImplementingActor(specId, projectId) {
16
16
  }
17
17
  return undefined;
18
18
  }
19
+ const PLACEHOLDER_REVIEWER_SESSION_IDS = new Set(['system', 'planu-implementation-reviewer']);
20
+ function isVerifiableReviewerSessionId(sessionId) {
21
+ return Boolean(sessionId) && !PLACEHOLDER_REVIEWER_SESSION_IDS.has(sessionId ?? '');
22
+ }
19
23
  export async function readVerifiedImplementationReview(projectId, specId) {
20
24
  try {
21
25
  const result = await readArtifact({ projectId, specId, kind: 'implementation_review' });
@@ -55,10 +59,19 @@ export async function readVerifiedImplementationReview(projectId, specId) {
55
59
  readError: err instanceof Error ? err.message : String(err),
56
60
  };
57
61
  }
58
- if (implementerActor !== undefined && implementerActor === review.reviewer.agent) {
62
+ const reviewerSessionId = review.reviewer.sessionId?.trim();
63
+ if (!isVerifiableReviewerSessionId(reviewerSessionId) || implementerActor === undefined) {
64
+ return {
65
+ ok: false,
66
+ reason: 'identity_unverifiable',
67
+ review,
68
+ implementingActor: implementerActor,
69
+ };
70
+ }
71
+ if (reviewerSessionId === implementerActor) {
59
72
  return { ok: false, reason: 'self_review', review, implementingActor: implementerActor };
60
73
  }
61
- return { ok: true, review, implementingActor: implementerActor ?? null };
74
+ return { ok: true, review, implementingActor: implementerActor };
62
75
  }
63
76
  catch (err) {
64
77
  /* reliability-optional: IMPLEMENTATION_REVIEW_GATE_READ — typed gate error blocks done */
@@ -70,6 +70,7 @@ export declare const ImplementationReviewV1Schema: z.ZodObject<{
70
70
  approved: "approved";
71
71
  "changes-requested": "changes-requested";
72
72
  }>;
73
+ sessionId: z.ZodOptional<z.ZodString>;
73
74
  }, z.core.$strip>;
74
75
  blockers: z.ZodArray<z.ZodString>;
75
76
  suggestions: z.ZodArray<z.ZodString>;
@@ -356,6 +357,7 @@ export declare const ARTIFACT_SCHEMAS: {
356
357
  approved: "approved";
357
358
  "changes-requested": "changes-requested";
358
359
  }>;
360
+ sessionId: z.ZodOptional<z.ZodString>;
359
361
  }, z.core.$strip>;
360
362
  blockers: z.ZodArray<z.ZodString>;
361
363
  suggestions: z.ZodArray<z.ZodString>;
@@ -47,6 +47,7 @@ export const ImplementationReviewV1Schema = z.object({
47
47
  kind: z.enum(['implementation-review-agent', 'human']),
48
48
  agent: z.string().min(1),
49
49
  verdict: z.enum(['approved', 'changes-requested']),
50
+ sessionId: z.string().optional(),
50
51
  }),
51
52
  blockers: z.array(z.string()),
52
53
  suggestions: z.array(z.string()),
@@ -0,0 +1,36 @@
1
+ import type { HandoffPackage, TokenWasteReport, MinimalImplementationReport } from '../types/index.js';
2
+ export declare function formatHandoff(pkg: HandoffPackage): string;
3
+ export declare function topItems<T>(items: T[], limit: number): T[];
4
+ export declare function buildHandoffSummaryText(args: {
5
+ pkg: HandoffPackage;
6
+ topBlockers: string[];
7
+ topRisks: string[];
8
+ minimalityBlocked: boolean | null;
9
+ tokenWasteRiskCount: number | null;
10
+ }): string;
11
+ export declare function summarizeMinimality(report: MinimalImplementationReport | null): Record<string, unknown> | undefined;
12
+ export declare function summarizeTokenWaste(report: TokenWasteReport | null): Record<string, unknown> | undefined;
13
+ export declare function summarizeGraphContext(graphContext: unknown): Record<string, unknown>;
14
+ export declare function buildHandoffTokenWasteReport(pkg: HandoffPackage, knowledge: {
15
+ projectPath?: string;
16
+ }, spec: {
17
+ id?: string;
18
+ risk?: string;
19
+ scope?: string;
20
+ target?: string;
21
+ }): Promise<TokenWasteReport | null>;
22
+ export declare function buildGraphContextSection(args: {
23
+ projectId: string;
24
+ projectPath?: string;
25
+ specId: string;
26
+ }): Promise<{
27
+ text: string;
28
+ structured: unknown;
29
+ } | null>;
30
+ export declare function buildMinimalImplementationReport(pkg: HandoffPackage, knowledge: {
31
+ projectPath?: string;
32
+ }, spec: {
33
+ id?: string;
34
+ risk?: string;
35
+ }, handoffText: string): Promise<MinimalImplementationReport | null>;
36
+ //# sourceMappingURL=handoff-format.d.ts.map
@@ -0,0 +1,314 @@
1
+ // engine/handoff-format.ts — Handoff package formatting and enrichment summaries (SPEC-1240)
2
+ // Relocated from tools/package-handoff.ts so the durable executor (engine layer) can render
3
+ // results without importing across the tools/engine boundary.
4
+ import { analyzeContextPreflight, buildTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, toolsFromPolicyGroups, } from './token-optimizer/index.js';
5
+ import { formatProjectGraphContext, queryProjectGraphSlice } from './project-graph/index.js';
6
+ import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from './minimality/index.js';
7
+ export function formatHandoff(pkg) {
8
+ const lines = [];
9
+ lines.push(`# Implementation Handoff Package — ${pkg.specId}`);
10
+ lines.push('');
11
+ lines.push(`Generated: ${pkg.generatedAt}`);
12
+ lines.push(`Readiness Score: ${pkg.readinessScore}/100`);
13
+ if (pkg.handoffPath) {
14
+ lines.push(`Handoff Path: ${pkg.handoffPath}`);
15
+ }
16
+ if (pkg.contextHash) {
17
+ lines.push(`Context Hash: ${pkg.contextHash}`);
18
+ }
19
+ lines.push(`Blocked: ${pkg.blocked ? 'yes' : 'no'}`);
20
+ lines.push('');
21
+ lines.push('## Objective');
22
+ lines.push('');
23
+ lines.push(pkg.objective);
24
+ lines.push('');
25
+ lines.push('## Acceptance Criteria');
26
+ lines.push('');
27
+ if (pkg.criteria.length > 0) {
28
+ for (const criterion of pkg.criteria) {
29
+ lines.push(`- [ ] ${criterion}`);
30
+ }
31
+ }
32
+ else {
33
+ lines.push('_(No criteria found — check spec.md acceptance criteria)_');
34
+ }
35
+ lines.push('');
36
+ lines.push('## Files to Modify');
37
+ lines.push('');
38
+ if (pkg.filesToModify.length > 0) {
39
+ for (const f of pkg.filesToModify) {
40
+ lines.push(`- ${f}`);
41
+ }
42
+ }
43
+ else {
44
+ lines.push('_(No files identified — check spec.md ## Technical / ## Files)_');
45
+ }
46
+ lines.push('');
47
+ lines.push('## Files to Create');
48
+ lines.push('');
49
+ if (pkg.filesToCreate.length > 0) {
50
+ for (const f of pkg.filesToCreate) {
51
+ lines.push(`- ${f}`);
52
+ }
53
+ }
54
+ else {
55
+ lines.push('_(None identified)_');
56
+ }
57
+ lines.push('');
58
+ lines.push('## Test Files');
59
+ lines.push('');
60
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
61
+ const filesToTest = pkg.filesToTest ?? [];
62
+ if (filesToTest.length > 0) {
63
+ for (const f of filesToTest) {
64
+ lines.push(`- ${f}`);
65
+ }
66
+ }
67
+ else {
68
+ lines.push('_(None identified)_');
69
+ }
70
+ lines.push('');
71
+ lines.push('## Ownership');
72
+ lines.push('');
73
+ if (pkg.ownership.length > 0) {
74
+ for (const item of pkg.ownership) {
75
+ lines.push(`- ${item}`);
76
+ }
77
+ }
78
+ else {
79
+ lines.push('_(Missing ownership — handoff is blocked)_');
80
+ }
81
+ lines.push('');
82
+ lines.push('## Test Plan');
83
+ lines.push('');
84
+ if (pkg.testPlan.length > 0) {
85
+ for (const item of pkg.testPlan) {
86
+ lines.push(`- ${item}`);
87
+ }
88
+ }
89
+ else {
90
+ lines.push('_(Missing test plan — handoff is blocked)_');
91
+ }
92
+ lines.push('');
93
+ lines.push('## Risks');
94
+ lines.push('');
95
+ if (pkg.risks.length > 0) {
96
+ for (const item of pkg.risks) {
97
+ lines.push(`- ${item}`);
98
+ }
99
+ }
100
+ else {
101
+ lines.push('_(Missing risk analysis — handoff is blocked)_');
102
+ }
103
+ lines.push('');
104
+ lines.push('## OUT OF SCOPE');
105
+ lines.push('');
106
+ lines.push('**The following must NOT be implemented:**');
107
+ lines.push('');
108
+ for (const item of pkg.outOfScope) {
109
+ lines.push(`- ${item}`);
110
+ }
111
+ lines.push('');
112
+ if (pkg.constraints.length > 0) {
113
+ lines.push('## Hard Constraints');
114
+ lines.push('');
115
+ for (const constraint of pkg.constraints) {
116
+ lines.push(`- [${constraint.type.toUpperCase()}] ${constraint.description}`);
117
+ if (constraint.affectedItems.length > 0) {
118
+ lines.push(` Affects: ${constraint.affectedItems.join(', ')}`);
119
+ }
120
+ }
121
+ lines.push('');
122
+ }
123
+ if (pkg.warnings.length > 0) {
124
+ lines.push('## Warnings');
125
+ lines.push('');
126
+ for (const warning of pkg.warnings) {
127
+ lines.push(`- WARNING: ${warning}`);
128
+ }
129
+ lines.push('');
130
+ }
131
+ lines.push('## Current State');
132
+ lines.push('');
133
+ lines.push(pkg.currentState);
134
+ lines.push('');
135
+ lines.push('## Next Action');
136
+ lines.push('');
137
+ lines.push(pkg.nextAction);
138
+ lines.push('');
139
+ if (pkg.blockers.length > 0) {
140
+ lines.push('## Blockers');
141
+ lines.push('');
142
+ for (const blocker of pkg.blockers) {
143
+ lines.push(`- ${blocker}`);
144
+ }
145
+ lines.push('');
146
+ }
147
+ return lines.join('\n');
148
+ }
149
+ export function topItems(items, limit) {
150
+ return items.slice(0, limit);
151
+ }
152
+ export function buildHandoffSummaryText(args) {
153
+ const lines = [];
154
+ lines.push(`# Implementation Handoff Package - ${args.pkg.specId}`);
155
+ lines.push('');
156
+ lines.push(`Readiness Score: ${String(args.pkg.readinessScore)}/100`);
157
+ lines.push(`Blocked: ${args.pkg.blocked ? 'yes' : 'no'}`);
158
+ if (args.pkg.handoffPath) {
159
+ lines.push(`Handoff Path: ${args.pkg.handoffPath}`);
160
+ }
161
+ if (args.pkg.contextHash) {
162
+ lines.push(`Context Hash: ${args.pkg.contextHash}`);
163
+ }
164
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
165
+ const filesToTestCount = (args.pkg.filesToTest ?? []).length;
166
+ lines.push(`Counts: ${String(args.pkg.criteria.length)} criteria, ${String(args.pkg.filesToModify.length)} files to modify, ${String(args.pkg.filesToCreate.length)} files to create, ${String(filesToTestCount)} files to test, ${String(args.pkg.testPlan.length)} tests, ${String(args.pkg.risks.length)} risks.`);
167
+ if (args.topBlockers.length > 0) {
168
+ lines.push(`Top blockers: ${args.topBlockers.join('; ')}`);
169
+ }
170
+ if (args.topRisks.length > 0) {
171
+ lines.push(`Top risks: ${args.topRisks.join('; ')}`);
172
+ }
173
+ if (args.minimalityBlocked !== null) {
174
+ lines.push(`Minimality: ${args.minimalityBlocked ? 'blocked' : 'pass'}`);
175
+ }
176
+ if (args.tokenWasteRiskCount !== null) {
177
+ lines.push(`Token Waste Autopilot risks: ${String(args.tokenWasteRiskCount)}`);
178
+ }
179
+ lines.push(`Next Action: ${args.pkg.nextAction}`);
180
+ lines.push('');
181
+ lines.push('Full handoff is available via the handoffPath artifact; it is not embedded by default.');
182
+ return lines.join('\n');
183
+ }
184
+ export function summarizeMinimality(report) {
185
+ if (report === null) {
186
+ return undefined;
187
+ }
188
+ const blockingFindings = report.findings.filter((finding) => finding.blocksDone);
189
+ return {
190
+ enabled: report.enabled,
191
+ blocked: report.blocked,
192
+ findingCount: report.findings.length,
193
+ blockingFindingCount: blockingFindings.length,
194
+ blockersTop3: topItems(blockingFindings, 3).map((finding) => ({
195
+ ruleId: finding.ruleId,
196
+ target: finding.target,
197
+ evidence: finding.evidence,
198
+ })),
199
+ safetyExceptionCount: report.safetyExceptions.length,
200
+ debtEvidenceCount: report.debtEvidence.length,
201
+ };
202
+ }
203
+ export function summarizeTokenWaste(report) {
204
+ if (report === null) {
205
+ return undefined;
206
+ }
207
+ return {
208
+ actionsTakenCount: report.actionsTaken.length,
209
+ recommendationCount: report.recommendations.length,
210
+ riskCount: report.risks.length,
211
+ risksTop3: topItems(report.risks, 3).map((risk) => ({
212
+ decision: risk.decision,
213
+ target: risk.target,
214
+ reason: risk.reason,
215
+ confidence: risk.confidence,
216
+ })),
217
+ modelEffort: {
218
+ effort: report.modelEffort.effort,
219
+ reason: report.modelEffort.reason,
220
+ },
221
+ };
222
+ }
223
+ export function summarizeGraphContext(graphContext) {
224
+ if (graphContext === null || typeof graphContext !== 'object') {
225
+ return { available: false };
226
+ }
227
+ const graph = graphContext;
228
+ return {
229
+ available: true,
230
+ nodeCount: Array.isArray(graph.nodes) ? graph.nodes.length : 0,
231
+ edgeCount: Array.isArray(graph.edges) ? graph.edges.length : 0,
232
+ summary: graph.summary,
233
+ tokenSavings: graph.tokenSavings,
234
+ freshness: graph.freshness
235
+ ? {
236
+ graphPath: graph.freshness.graphPath,
237
+ stale: graph.freshness.stale,
238
+ reason: graph.freshness.reason,
239
+ }
240
+ : undefined,
241
+ };
242
+ }
243
+ export async function buildHandoffTokenWasteReport(pkg, knowledge, spec) {
244
+ const { policy } = await loadTokenWastePolicy(knowledge.projectPath);
245
+ if (!policy.enabled) {
246
+ return null;
247
+ }
248
+ const context = analyzeContextPreflight({
249
+ action: 'package_handoff',
250
+ policy,
251
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
252
+ candidates: [...pkg.filesToModify, ...pkg.filesToCreate, ...(pkg.filesToTest ?? [])].map((path) => ({
253
+ path,
254
+ reason: 'Referenced by handoff package.',
255
+ })),
256
+ spec: {
257
+ id: spec.id,
258
+ risk: spec.risk,
259
+ target: spec.target,
260
+ },
261
+ });
262
+ const tools = recommendRelevantTools({
263
+ action: 'package_handoff',
264
+ intentSignals: ['handoff', spec.scope ?? '', spec.target ?? ''].filter(Boolean),
265
+ tools: toolsFromPolicyGroups(policy),
266
+ policy,
267
+ });
268
+ return buildTokenWasteReport({
269
+ action: 'package_handoff',
270
+ policy,
271
+ context,
272
+ tools,
273
+ spec: {
274
+ id: spec.id,
275
+ risk: spec.risk,
276
+ scope: spec.scope,
277
+ },
278
+ });
279
+ }
280
+ export async function buildGraphContextSection(args) {
281
+ if (args.projectPath === undefined) {
282
+ return null;
283
+ }
284
+ try {
285
+ const slice = await queryProjectGraphSlice({
286
+ projectId: args.projectId,
287
+ projectPath: args.projectPath,
288
+ specId: args.specId,
289
+ });
290
+ if (slice === null || slice.nodes.length === 0) {
291
+ return null;
292
+ }
293
+ return { text: await formatProjectGraphContext(slice), structured: slice };
294
+ }
295
+ catch {
296
+ return null;
297
+ }
298
+ }
299
+ export async function buildMinimalImplementationReport(pkg, knowledge, spec, handoffText) {
300
+ const { policy, evidence } = await loadMinimalImplementationPolicy(knowledge.projectPath);
301
+ return analyzeMinimalImplementation({
302
+ policy,
303
+ specId: spec.id,
304
+ specRisk: spec.risk,
305
+ handoffText,
306
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
307
+ files: [...pkg.filesToModify, ...pkg.filesToCreate, ...(pkg.filesToTest ?? [])].map((path) => ({
308
+ path,
309
+ content: path,
310
+ })),
311
+ evidence,
312
+ });
313
+ }
314
+ //# sourceMappingURL=handoff-format.js.map
@@ -1,4 +1,4 @@
1
- import type { Spec, ProjectKnowledge, HandoffPackage } from '../types/index.js';
1
+ import type { Spec, ProjectKnowledge, HandoffPackage, HandoffVersionFence } from '../types/index.js';
2
2
  /** Normalize one repository-relative ownership path without retaining unsafe input in diagnostics. */
3
3
  export declare function normalizeOwnedPath(value: string): {
4
4
  status: 'valid';
@@ -7,7 +7,7 @@ export declare function normalizeOwnedPath(value: string): {
7
7
  status: 'none';
8
8
  } | {
9
9
  status: 'invalid';
10
- code: 'INVALID_FILE_OWNERSHIP_EMPTY' | 'INVALID_FILE_OWNERSHIP_FENCED' | 'INVALID_FILE_OWNERSHIP_NUL' | 'INVALID_FILE_OWNERSHIP_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_HOME_RELATIVE' | 'INVALID_FILE_OWNERSHIP_UNC' | 'INVALID_FILE_OWNERSHIP_DRIVE_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_URI' | 'INVALID_FILE_OWNERSHIP_TRAVERSAL' | 'INVALID_FILE_OWNERSHIP_MARKDOWN' | 'INVALID_FILE_OWNERSHIP_DESCRIPTION';
10
+ code: 'INVALID_FILE_OWNERSHIP_EMPTY' | 'INVALID_FILE_OWNERSHIP_FENCED' | 'INVALID_FILE_OWNERSHIP_NUL' | 'INVALID_FILE_OWNERSHIP_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_HOME_RELATIVE' | 'INVALID_FILE_OWNERSHIP_UNC' | 'INVALID_FILE_OWNERSHIP_DRIVE_ABSOLUTE' | 'INVALID_FILE_OWNERSHIP_URI' | 'INVALID_FILE_OWNERSHIP_TRAVERSAL' | 'INVALID_FILE_OWNERSHIP_MARKDOWN' | 'INVALID_FILE_OWNERSHIP_DESCRIPTION' | 'INVALID_FILE_OWNERSHIP_CHARSET';
11
11
  };
12
12
  /** Normalize a raw traceability changed-file value without interpreting Markdown or prose. */
13
13
  export declare function normalizeChangedFilePath(value: string): ReturnType<typeof normalizeOwnedPath>;
@@ -21,5 +21,7 @@ export declare function extractCanonicalFileOwnership(content: string): {
21
21
  toTestDeclared: string[];
22
22
  blockers: string[];
23
23
  };
24
- export declare function packageHandoff(spec: Spec, knowledge: ProjectKnowledge): Promise<HandoffPackage>;
24
+ export declare function packageHandoff(spec: Spec, knowledge: ProjectKnowledge, versionFence?: HandoffVersionFence): Promise<HandoffPackage & {
25
+ readonly staleVersionSkipped?: boolean;
26
+ }>;
25
27
  //# sourceMappingURL=handoff-packager.d.ts.map
@@ -1,6 +1,7 @@
1
1
  // engine/handoff-packager.ts — Assembles handoff context package for AI agents (SPEC-039)
2
- import { createHash } from 'node:crypto';
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ /* eslint-disable max-lines -- package assembly and the SPEC-1240 atomic-publish/version-fence path stay co-located for auditability */
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
4
5
  import { join } from 'node:path';
5
6
  import { stripFrontmatter } from './frontmatter-parser.js';
6
7
  // eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
@@ -118,6 +119,7 @@ function ownershipItems(content) {
118
119
  }
119
120
  return { items: extractListItems(visibleLines.join('\n')), fenced };
120
121
  }
122
+ const OWNERSHIP_DESCRIPTION_SEPARATOR = /\s+(?:—|–|-)\s+/u;
121
123
  /** Normalize one repository-relative ownership path without retaining unsafe input in diagnostics. */
122
124
  export function normalizeOwnedPath(value) {
123
125
  const trimmed = value.trim();
@@ -125,7 +127,9 @@ export function normalizeOwnedPath(value) {
125
127
  return { status: 'none' };
126
128
  }
127
129
  const backticked = /^`([^`]+)`/.exec(trimmed)?.[1];
128
- const candidate = (backticked ?? trimmed.split(/\s+(?:—|–|-)\s+|\s+\(/, 1)[0] ?? '').trim();
130
+ const candidate = (backticked ??
131
+ trimmed.split(new RegExp(`${OWNERSHIP_DESCRIPTION_SEPARATOR.source}|\\s+\\(`, 'u'), 1)[0] ??
132
+ '').trim();
129
133
  if (candidate.length === 0) {
130
134
  return { status: 'invalid', code: 'INVALID_FILE_OWNERSHIP_EMPTY' };
131
135
  }
@@ -323,34 +327,63 @@ function stripInertOwnershipMarkup(content) {
323
327
  function normalizeStrictOwnedPath(rawValue) {
324
328
  const normalized = normalizeOwnedPath(rawValue);
325
329
  if (normalized.status !== 'valid') {
326
- return null;
330
+ return normalized;
327
331
  }
328
- return /^[A-Za-z0-9._/()[\]@-]+$/u.test(normalized.path) ? normalized.path : null;
332
+ return /^[A-Za-z0-9._/()[\]@-]+$/u.test(normalized.path)
333
+ ? normalized
334
+ : { status: 'invalid', code: 'INVALID_FILE_OWNERSHIP_CHARSET' };
335
+ }
336
+ function ownershipHeadEnd(rawPath) {
337
+ const masked = rawPath.replaceAll(/`[^`\n]+`/gu, (match) => 'x'.repeat(match.length));
338
+ return OWNERSHIP_DESCRIPTION_SEPARATOR.exec(masked)?.index ?? rawPath.length;
339
+ }
340
+ const ownershipReasonCode = (result) => result.status === 'invalid' ? result.code : 'INVALID_FILE_OWNERSHIP_EMPTY';
341
+ function ownershipInvalidTokenBlocker(token, classification, reasonCode) {
342
+ const printed = JSON.stringify(token.length > 120 ? token.slice(0, 120) : token);
343
+ const hint = `${OWNERSHIP_LINE_SHAPE_HINT} reason=${reasonCode}`;
344
+ return ownershipBlocker(printed, [classification, 'InvalidPath'], hint);
329
345
  }
330
346
  function collectStrictOwnershipEntry(rawValue, classification, state) {
331
347
  const rawPath = rawValue.trim();
332
- const backtickedValues = [...rawPath.matchAll(/`([^`\n]+)`/gu)].map((match) => match[1] ?? '');
333
- const backtickedPaths = backtickedValues.map(normalizeStrictOwnedPath);
334
- const residual = rawPath.replaceAll(/`[^`\n]+`/gu, '').trim();
335
- const residualIsSafe = isSafeOwnershipResidual(residual);
336
- if (backtickedPaths.some((path) => path === null) ||
337
- (backtickedValues.length > 0 && !residualIsSafe)) {
338
- state.blockers.push(ownershipBlocker('<redacted>', [classification, 'InvalidPath'], OWNERSHIP_LINE_SHAPE_HINT));
348
+ const head = rawPath.slice(0, ownershipHeadEnd(rawPath));
349
+ const backtickMatches = [...head.matchAll(/`([^`\n]+)`/gu)].map((match) => match[1] ?? '');
350
+ const pathCandidates = backtickMatches.filter((token) => /[./]/u.test(token));
351
+ const pushInvalid = (token, code) => {
352
+ state.blockers.push(ownershipInvalidTokenBlocker(token, classification, code));
353
+ };
354
+ if (!/`[^`\n]+`/u.test(rawPath)) {
355
+ const direct = normalizeStrictOwnedPath(head);
356
+ if (direct.status !== 'valid') {
357
+ pushInvalid(head, ownershipReasonCode(direct));
358
+ return;
359
+ }
360
+ collectStrictOwnershipPath(direct.path, classification, state);
339
361
  return;
340
362
  }
341
- const directPath = normalizeStrictOwnedPath(rawPath);
342
- const paths = backtickedPaths.length > 0
343
- ? backtickedPaths
344
- : directPath === null
345
- ? []
346
- : [directPath];
347
- if (paths.length === 0) {
348
- const diagnosticPath = /^`[^`]+`$/u.test(rawPath) ? '<redacted>' : rawPath || '<empty>';
349
- state.blockers.push(ownershipBlocker(diagnosticPath, [classification, 'InvalidPath'], OWNERSHIP_LINE_SHAPE_HINT));
363
+ if (backtickMatches.length === 0) {
364
+ pushInvalid(head, 'INVALID_FILE_OWNERSHIP_HEAD_RESIDUAL');
365
+ return;
366
+ }
367
+ const residual = head.replaceAll(/`[^`\n]+`/gu, '').trim();
368
+ if (!isSafeOwnershipResidual(residual)) {
369
+ pushInvalid(residual, 'INVALID_FILE_OWNERSHIP_HEAD_RESIDUAL');
370
+ return;
371
+ }
372
+ const normalizedCandidates = pathCandidates.map((token) => ({
373
+ token,
374
+ result: normalizeStrictOwnedPath(token),
375
+ }));
376
+ const failed = normalizedCandidates.filter(({ result }) => result.status !== 'valid');
377
+ if (failed.length > 0) {
378
+ for (const { token, result } of failed) {
379
+ pushInvalid(token, ownershipReasonCode(result));
380
+ }
350
381
  return;
351
382
  }
352
- for (const path of paths) {
353
- collectStrictOwnershipPath(path, classification, state);
383
+ for (const { result } of normalizedCandidates) {
384
+ if (result.status === 'valid') {
385
+ collectStrictOwnershipPath(result.path, classification, state);
386
+ }
354
387
  }
355
388
  }
356
389
  function collectStrictOwnershipPath(path, classification, state) {
@@ -435,7 +468,7 @@ function extractFileLevelWorkPlanOwnership(content) {
435
468
  };
436
469
  }
437
470
  function validateOwnershipAgreement(canonical, workPlan, testDeclaredPaths) {
438
- if (!canonical.present || !workPlan.present) {
471
+ if (!canonical.present) {
439
472
  return [];
440
473
  }
441
474
  const testDeclared = new Set(testDeclaredPaths);
@@ -444,6 +477,13 @@ function validateOwnershipAgreement(canonical, workPlan, testDeclaredPaths) {
444
477
  : ownership.toModify.includes(path)
445
478
  ? 'Modify'
446
479
  : 'Absent';
480
+ if (!workPlan.present) {
481
+ const canonicalPaths = [...canonical.toCreate, ...canonical.toModify].filter((path) => !testDeclared.has(path));
482
+ return [...new Set(canonicalPaths)].sort().map((path) => {
483
+ const canonicalClass = classification(canonical, path);
484
+ return ownershipBlocker(path, [`Canonical:${canonicalClass}`, 'WorkPlan:Absent'], ownershipAgreementHint(canonicalClass, 'Absent', path));
485
+ });
486
+ }
447
487
  const paths = new Set([
448
488
  ...canonical.toCreate,
449
489
  ...canonical.toModify,
@@ -463,9 +503,9 @@ function validateOwnershipAgreement(canonical, workPlan, testDeclaredPaths) {
463
503
  function extractBacktickedFiles(content) {
464
504
  const files = new Set();
465
505
  for (const match of content.matchAll(/`([^`\n]+)`/gu)) {
466
- const path = match[1] ? normalizeStrictOwnedPath(match[1]) : null;
467
- if (path) {
468
- files.add(path);
506
+ const normalized = match[1] ? normalizeStrictOwnedPath(match[1]) : null;
507
+ if (normalized?.status === 'valid') {
508
+ files.add(normalized.path);
469
509
  }
470
510
  }
471
511
  return [...files].sort();
@@ -602,7 +642,7 @@ function buildConstraints(knowledge) {
602
642
  return constraints;
603
643
  }
604
644
  // ── Public API ───────────────────────────────────────────────────────────────
605
- export async function packageHandoff(spec, knowledge) {
645
+ export async function packageHandoff(spec, knowledge, versionFence) {
606
646
  const warnings = [];
607
647
  const rawSpecContent = spec.specPath
608
648
  ? await readFile(spec.specPath, 'utf-8').catch(() => '')
@@ -685,7 +725,7 @@ export async function packageHandoff(spec, knowledge) {
685
725
  blocked: blockers.length > 0,
686
726
  blockers,
687
727
  };
688
- return persistHandoffIfPossible(pkg, knowledge);
728
+ return persistHandoffIfPossible(pkg, knowledge, versionFence);
689
729
  }
690
730
  function renderPersistedHandoff(pkg) {
691
731
  return [
@@ -733,19 +773,36 @@ function renderPersistedHandoff(pkg) {
733
773
  '',
734
774
  ].join('\n');
735
775
  }
736
- async function persistHandoffIfPossible(pkg, knowledge) {
776
+ async function persistHandoffIfPossible(pkg, knowledge, versionFence) {
737
777
  if (!knowledge.projectPath) {
738
778
  return pkg;
739
779
  }
740
780
  const handoffDir = join(projectDataDir(hashProjectPath(knowledge.projectPath)), 'handoffs');
741
781
  const handoffPath = join(handoffDir, `${pkg.specId}.md`);
782
+ const tempPath = join(handoffDir, `.${pkg.specId}.md.tmp-${randomUUID()}`);
742
783
  const sessionContextPath = join(knowledge.projectPath, 'planu', 'session-context.md');
743
784
  const markdown = renderPersistedHandoff(pkg);
744
785
  try {
745
786
  await mkdir(handoffDir, { recursive: true });
746
- await writeFile(handoffPath, markdown, 'utf-8');
787
+ await writeFile(tempPath, markdown, 'utf-8');
788
+ if (versionFence) {
789
+ const abandoned = versionFence.signal?.aborted === true;
790
+ const currentSpecVersion = abandoned ? undefined : await versionFence.getCurrentSpecVersion();
791
+ if (abandoned ||
792
+ (currentSpecVersion !== undefined && currentSpecVersion !== versionFence.specVersion)) {
793
+ await rm(tempPath, { force: true });
794
+ return { ...pkg, staleVersionSkipped: true };
795
+ }
796
+ }
797
+ await rename(tempPath, handoffPath);
747
798
  }
748
- catch {
799
+ catch (error) {
800
+ await rm(tempPath, { force: true }).catch(() => {
801
+ /* best-effort temp cleanup — the publish already failed */
802
+ });
803
+ if (versionFence) {
804
+ throw error;
805
+ }
749
806
  return {
750
807
  ...pkg,
751
808
  warnings: [