@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
@@ -1,5 +1,5 @@
1
- import { stat } from 'node:fs/promises';
2
1
  import { isAbsolute, join, normalize } from 'node:path';
2
+ import { pathExistsByStat as pathExists } from '../../core/shared/fs.js';
3
3
  import { createCriterionIdentity, criterionIdentitiesMatch } from '../criterion-identity.js';
4
4
  export function criterionKey(criterion) {
5
5
  return createCriterionIdentity(criterion).stableKey;
@@ -9,6 +9,10 @@ function truncateAtEvidenceMarker(value) {
9
9
  const markerIndex = value.search(EVIDENCE_MARKER);
10
10
  return markerIndex === -1 ? value : value.slice(0, markerIndex);
11
11
  }
12
+ const LINE_CITATION_SUFFIX = /:\d+(?:-\d+)?$/;
13
+ function pathForExistenceCheck(value) {
14
+ return value.replace(LINE_CITATION_SUFFIX, '');
15
+ }
12
16
  export async function buildSpecEvidenceIndex(args) {
13
17
  const diagnostics = [...args.artifacts.invalidArtifacts];
14
18
  const matrixRows = args.artifacts.traceabilityMatrix?.rows ?? [];
@@ -129,12 +133,25 @@ async function pathRecord(kind, value, source, args, fieldName) {
129
133
  if (!args.projectPath) {
130
134
  return record(kind, value, 'missing', source, evidencePathReason(fieldName, 'projectPath unavailable'));
131
135
  }
132
- const exists = await stat(safe.path)
133
- .then(() => true)
134
- .catch(() => false);
136
+ const literalExists = await pathExists(safe.path);
137
+ const exists = literalExists || (await fallbackExists(value, fieldName, args.projectPath));
135
138
  const missingReason = evidencePathReason(fieldName, `Missing path: ${value}`);
136
139
  return record(kind, value, exists ? 'valid' : missingPathStatus(fieldName), source, exists ? undefined : missingReason);
137
140
  }
141
+ async function fallbackExists(value, fieldName, projectPath) {
142
+ if (fieldName === 'changedFiles') {
143
+ return false;
144
+ }
145
+ const strippedValue = pathForExistenceCheck(value);
146
+ if (strippedValue === value || strippedValue.length === 0) {
147
+ return false;
148
+ }
149
+ const strippedSafe = resolveSafeProjectPath(strippedValue, projectPath, fieldName);
150
+ if (!strippedSafe.ok) {
151
+ return false;
152
+ }
153
+ return pathExists(strippedSafe.path);
154
+ }
138
155
  function missingPathStatus(fieldName) {
139
156
  return fieldName === 'changedFiles' ? 'missing' : 'stale';
140
157
  }
@@ -249,7 +249,7 @@ function isQueueRelevant(job) {
249
249
  return true;
250
250
  }
251
251
  const kind = job.payload.kind;
252
- return kind === undefined || kind === 'validate';
252
+ return kind === undefined || kind === 'validate' || kind === 'package-handoff';
253
253
  }
254
254
  function remainingBudgetMs(job, now = Date.now()) {
255
255
  const payload = job.payload && typeof job.payload === 'object'
@@ -0,0 +1,22 @@
1
+ import { DurableJobRuntime } from './job-runtime.js';
2
+ import type { DurableJobRecord, PackageHandoffDurableJobPayload, PackageHandoffStageResult } from '../../types/durable-job.js';
3
+ export declare function isPackageHandoffJob(value: unknown): boolean;
4
+ export declare function parsePackageHandoffPayload(value: unknown): PackageHandoffDurableJobPayload | undefined;
5
+ /** Bounds one stage with AbortSignal.timeout + Promise.race; the stage's own promise is orphaned on timeout. */
6
+ export declare function runStageWithDeadline<T>(stage: string, budgetMs: number, fn: (signal: AbortSignal) => Promise<T>): Promise<PackageHandoffStageResult<T>>;
7
+ /** Executes accepted package-handoff jobs directly against the packaging engine, never through MCP. */
8
+ export declare class PackageHandoffJobExecutor {
9
+ private readonly runtime;
10
+ private readonly workerId;
11
+ constructor(runtime: DurableJobRuntime, workerId?: string);
12
+ runOnce(projectId: string, workspaceId?: string): Promise<readonly DurableJobRecord[]>;
13
+ runUntilIdle(projectId: string, workspaceId?: string): Promise<void>;
14
+ private candidates;
15
+ private execute;
16
+ private runJob;
17
+ }
18
+ /** Schedules background execution after the submit response can be flushed. */
19
+ export declare function schedulePackageHandoffJobExecution(projectId: string, workspaceId?: string): void;
20
+ /** Re-enqueues accepted/checkpointed package-handoff work discovered after a process restart. */
21
+ export declare function recoverPackageHandoffJobsAtStartup(): void;
22
+ //# sourceMappingURL=package-handoff-job-executor.d.ts.map
@@ -0,0 +1,490 @@
1
+ // engine/execution/package-handoff-job-executor.ts — Durable executor for package_handoff (SPEC-1240)
2
+ // Mirrors the validate-job-executor submit/claim/execute lifecycle, bounding every stage with
3
+ // its own deadline so a slow readiness check, packager write, or enrichment stage can never hang
4
+ // the MCP transport. Core stages fail the job classified; enrichment stages degrade and the job
5
+ // still completes.
6
+ /* eslint-disable max-lines -- stage deadlines, core/enrichment orchestration, and the durable executor lifecycle stay co-located for auditability */
7
+ import { randomUUID } from 'node:crypto';
8
+ import { DurableJobRuntime } from './job-runtime.js';
9
+ import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
10
+ // eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
11
+ import { specStore, knowledgeStore } from '../../storage/index.js';
12
+ import { checkSpecReadiness } from '../readiness-checker.js';
13
+ import { packageHandoff } from '../handoff-packager.js';
14
+ import { detectParadigms } from '../paradigm-detector.js';
15
+ // eslint-disable-next-line no-restricted-imports -- grandfathered layer violation, remediation SPEC-1661 SPEC-1662 SPEC-1663
16
+ import { appendTransitionEvent } from '../../storage/transition-log.js';
17
+ import { formatHandoff, topItems, buildHandoffSummaryText, summarizeMinimality, summarizeTokenWaste, summarizeGraphContext, buildHandoffTokenWasteReport, buildGraphContextSection, buildMinimalImplementationReport, } from '../handoff-format.js';
18
+ const WORKSPACE_ID = 'global';
19
+ const CORE_BUDGET_DEFAULT_MS = 20_000;
20
+ const ENRICHMENT_BUDGET_DEFAULT_MS = 8_000;
21
+ const ENRICHMENT_STAGE_COUNT = 4;
22
+ const LEASE_SAFETY_MARGIN_MS = 15_000;
23
+ const activeScopes = new Map();
24
+ export function isPackageHandoffJob(value) {
25
+ return Boolean(value && typeof value === 'object' && value.kind === 'package-handoff');
26
+ }
27
+ export function parsePackageHandoffPayload(value) {
28
+ if (!value || typeof value !== 'object') {
29
+ return undefined;
30
+ }
31
+ const candidate = value;
32
+ if (candidate.kind === 'package-handoff' &&
33
+ candidate.schemaVersion === 1 &&
34
+ typeof candidate.projectId === 'string' &&
35
+ typeof candidate.specId === 'string' &&
36
+ typeof candidate.specVersion === 'string') {
37
+ return candidate;
38
+ }
39
+ return undefined;
40
+ }
41
+ function envBudgetMs(variableName, defaultMs) {
42
+ const raw = process.env[variableName];
43
+ if (raw === undefined || raw.trim() === '') {
44
+ return defaultMs;
45
+ }
46
+ const parsed = Number.parseInt(raw, 10);
47
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : defaultMs;
48
+ }
49
+ function coreBudgetMs() {
50
+ return envBudgetMs('PLANU_HANDOFF_CORE_BUDGET_MS', CORE_BUDGET_DEFAULT_MS);
51
+ }
52
+ function enrichmentBudgetMs() {
53
+ return envBudgetMs('PLANU_HANDOFF_ENRICHMENT_BUDGET_MS', ENRICHMENT_BUDGET_DEFAULT_MS);
54
+ }
55
+ /** Bounds one stage with AbortSignal.timeout + Promise.race; the stage's own promise is orphaned on timeout. */
56
+ export async function runStageWithDeadline(stage, budgetMs, fn) {
57
+ const startedAt = Date.now();
58
+ if (budgetMs <= 0) {
59
+ return {
60
+ stage,
61
+ durationMs: 0,
62
+ outcome: {
63
+ ok: false,
64
+ timedOut: true,
65
+ error: new Error(`[Planu] stage "${stage}" had no remaining budget`),
66
+ },
67
+ };
68
+ }
69
+ const timeoutSignal = AbortSignal.timeout(budgetMs);
70
+ let timedOut = false;
71
+ const timeoutPromise = new Promise((_resolve, reject) => {
72
+ timeoutSignal.addEventListener('abort', () => {
73
+ timedOut = true;
74
+ reject(new Error(`[Planu] stage "${stage}" exceeded its ${String(budgetMs)}ms budget`));
75
+ }, { once: true });
76
+ });
77
+ try {
78
+ const value = await Promise.race([fn(timeoutSignal), timeoutPromise]);
79
+ return {
80
+ stage,
81
+ durationMs: Date.now() - startedAt,
82
+ outcome: { ok: true, value, timedOut: false },
83
+ };
84
+ }
85
+ catch (error) {
86
+ reportClassifiedDegradation('HANDOFF_STAGE_FAILURE', error);
87
+ return { stage, durationMs: Date.now() - startedAt, outcome: { ok: false, timedOut, error } };
88
+ }
89
+ }
90
+ class CoreStageFailure extends Error {
91
+ stage;
92
+ timedOut;
93
+ cause;
94
+ stageTimings;
95
+ constructor(stage, timedOut, cause, stageTimings) {
96
+ super(`[Planu] package-handoff core stage "${stage}" failed`);
97
+ this.stage = stage;
98
+ this.timedOut = timedOut;
99
+ this.cause = cause;
100
+ this.stageTimings = stageTimings;
101
+ this.name = 'CoreStageFailure';
102
+ }
103
+ }
104
+ function classifiedCoreFailureMessage(failure) {
105
+ const code = failure.timedOut ? 'HANDOFF_CORE_TIMEOUT' : 'HANDOFF_CORE_FAILURE';
106
+ const remediation = failure.timedOut
107
+ ? `The ${failure.stage} stage exceeded its budget. Increase PLANU_HANDOFF_CORE_BUDGET_MS or investigate why ${failure.stage} is slow, then retry with restart_job.`
108
+ : `The ${failure.stage} stage failed. Inspect the underlying cause, then retry with restart_job once fixed.`;
109
+ const cause = failure.cause instanceof Error ? failure.cause.message : String(failure.cause);
110
+ return (`[Planu] ${code} stage=${failure.stage} remediation=${remediation} cause=${cause} ` +
111
+ `stageTimings=${JSON.stringify(failure.stageTimings)}`);
112
+ }
113
+ function enrichmentDegradation(stage, timedOut) {
114
+ return {
115
+ stage,
116
+ code: timedOut ? 'HANDOFF_ENRICHMENT_TIMEOUT' : 'HANDOFF_ENRICHMENT_FAILURE',
117
+ remediation: timedOut
118
+ ? `The ${stage} enrichment stage exceeded its budget; the core handoff artifact is still valid. Increase PLANU_HANDOFF_ENRICHMENT_BUDGET_MS to include it next run.`
119
+ : `The ${stage} enrichment stage failed; the core handoff artifact is still valid. Re-run package_handoff once the underlying issue is fixed.`,
120
+ };
121
+ }
122
+ async function currentSpecVersion(projectId, specId, projectPath) {
123
+ const current = await specStore.getSpec(projectId, specId, projectPath);
124
+ return current?.spec_version;
125
+ }
126
+ async function runCoreStages(payload, spec, knowledge) {
127
+ const deadlineAt = Date.now() + coreBudgetMs();
128
+ const stageTimings = {};
129
+ const readinessResult = await runStageWithDeadline('readiness', Math.max(0, deadlineAt - Date.now()), () => checkSpecReadiness(spec, 'lenient'));
130
+ stageTimings.readiness = readinessResult.durationMs;
131
+ if (!readinessResult.outcome.ok) {
132
+ throw new CoreStageFailure('readiness', readinessResult.outcome.timedOut, readinessResult.outcome.error, stageTimings);
133
+ }
134
+ const packageCoreResult = await runStageWithDeadline('package-core', Math.max(0, deadlineAt - Date.now()), (signal) => packageHandoff(spec, knowledge, {
135
+ specVersion: payload.specVersion,
136
+ getCurrentSpecVersion: () => currentSpecVersion(payload.projectId, payload.specId, knowledge.projectPath),
137
+ signal,
138
+ }));
139
+ stageTimings['package-core'] = packageCoreResult.durationMs;
140
+ if (!packageCoreResult.outcome.ok) {
141
+ throw new CoreStageFailure('package-core', packageCoreResult.outcome.timedOut, packageCoreResult.outcome.error, stageTimings);
142
+ }
143
+ return {
144
+ readinessScore: readinessResult.outcome.value?.score ?? 0,
145
+ pkg: packageCoreResult.outcome.value,
146
+ stageTimings,
147
+ };
148
+ }
149
+ async function runParadigmStage(knowledge, constraints) {
150
+ if (!knowledge.projectPath) {
151
+ return { durationMs: 0, constraints, degradation: undefined };
152
+ }
153
+ const projectPath = knowledge.projectPath;
154
+ const paradigmResult = await runStageWithDeadline('paradigms', enrichmentBudgetMs(), () => detectParadigms(projectPath));
155
+ if (!paradigmResult.outcome.ok) {
156
+ return {
157
+ durationMs: paradigmResult.durationMs,
158
+ constraints,
159
+ degradation: enrichmentDegradation('paradigms', paradigmResult.outcome.timedOut),
160
+ };
161
+ }
162
+ const dominant = paradigmResult.outcome.value?.dominant;
163
+ if (!dominant || dominant.confidence < 0.5) {
164
+ return { durationMs: paradigmResult.durationMs, constraints, degradation: undefined };
165
+ }
166
+ return {
167
+ durationMs: paradigmResult.durationMs,
168
+ constraints: [
169
+ ...constraints,
170
+ {
171
+ type: 'interface',
172
+ description: `Follow ${dominant.name} paradigm patterns — dominant paradigm (confidence: ${(dominant.confidence * 100).toFixed(0)}%)`,
173
+ affectedItems: ['all new code'],
174
+ },
175
+ ],
176
+ degradation: undefined,
177
+ };
178
+ }
179
+ async function runGraphContextStage(payload, knowledge) {
180
+ const graphResult = await runStageWithDeadline('graph-context', enrichmentBudgetMs(), () => buildGraphContextSection({
181
+ projectId: payload.projectId,
182
+ projectPath: knowledge.projectPath,
183
+ specId: payload.specId,
184
+ }));
185
+ if (!graphResult.outcome.ok) {
186
+ return {
187
+ durationMs: graphResult.durationMs,
188
+ graphContext: null,
189
+ degradation: enrichmentDegradation('graph-context', graphResult.outcome.timedOut),
190
+ };
191
+ }
192
+ const graphContext = graphResult.outcome.value ?? null;
193
+ const unavailable = graphContext === null;
194
+ return {
195
+ durationMs: graphResult.durationMs,
196
+ graphContext,
197
+ degradation: unavailable
198
+ ? {
199
+ stage: 'graph-context',
200
+ code: 'HANDOFF_ENRICHMENT_FAILURE',
201
+ remediation: 'The project graph had no data for this spec — not a failure. Run configure_code_graph or index the project to include graph context in future handoffs.',
202
+ }
203
+ : undefined,
204
+ };
205
+ }
206
+ async function runEnrichmentStages(payload, spec, knowledge, pkgWithScore) {
207
+ const stageTimings = {};
208
+ const degradations = [];
209
+ const budgetMs = enrichmentBudgetMs();
210
+ const paradigmStage = await runParadigmStage(knowledge, pkgWithScore.constraints);
211
+ stageTimings.paradigms = paradigmStage.durationMs;
212
+ if (paradigmStage.degradation) {
213
+ degradations.push(paradigmStage.degradation);
214
+ }
215
+ const pkgWithConstraints = {
216
+ ...pkgWithScore,
217
+ constraints: paradigmStage.constraints,
218
+ };
219
+ const tokenWasteResult = await runStageWithDeadline('token-waste', budgetMs, () => buildHandoffTokenWasteReport(pkgWithConstraints, knowledge, spec));
220
+ stageTimings['token-waste'] = tokenWasteResult.durationMs;
221
+ const tokenWasteReport = tokenWasteResult.outcome.ok
222
+ ? (tokenWasteResult.outcome.value ?? null)
223
+ : null;
224
+ if (!tokenWasteResult.outcome.ok) {
225
+ degradations.push(enrichmentDegradation('token-waste', tokenWasteResult.outcome.timedOut));
226
+ }
227
+ const graphStage = await runGraphContextStage(payload, knowledge);
228
+ stageTimings['graph-context'] = graphStage.durationMs;
229
+ if (graphStage.degradation) {
230
+ degradations.push(graphStage.degradation);
231
+ }
232
+ const graphContext = graphStage.graphContext;
233
+ const baseHandoffText = graphContext !== null
234
+ ? `${formatHandoff(pkgWithConstraints)}\n${graphContext.text}`
235
+ : formatHandoff(pkgWithConstraints);
236
+ const minimalityResult = await runStageWithDeadline('minimality', budgetMs, () => buildMinimalImplementationReport(pkgWithConstraints, knowledge, spec, baseHandoffText));
237
+ stageTimings.minimality = minimalityResult.durationMs;
238
+ const minimalityReport = minimalityResult.outcome.ok
239
+ ? (minimalityResult.outcome.value ?? null)
240
+ : null;
241
+ if (!minimalityResult.outcome.ok) {
242
+ degradations.push(enrichmentDegradation('minimality', minimalityResult.outcome.timedOut));
243
+ }
244
+ return {
245
+ pkgWithConstraints,
246
+ graphContext,
247
+ minimalityBlocked: minimalityReport?.blocked ?? null,
248
+ minimalitySummary: summarizeMinimality(minimalityReport),
249
+ tokenWasteRiskCount: tokenWasteReport?.risks.length ?? null,
250
+ tokenWasteSummary: summarizeTokenWaste(tokenWasteReport),
251
+ stageTimings,
252
+ degradations,
253
+ };
254
+ }
255
+ function buildCompletionResult(args) {
256
+ const { specId, pkg, enrichment, stageTimings, degradations } = args;
257
+ const topBlockers = topItems(pkg.blockers, 3);
258
+ const ownershipBlockers = pkg.blockers.filter((blocker) => blocker.startsWith('handoff_ownership_invalid'));
259
+ const topRisks = topItems(pkg.risks, 5);
260
+ const graphSummary = enrichment.graphContext !== null
261
+ ? summarizeGraphContext(enrichment.graphContext.structured)
262
+ : undefined;
263
+ return {
264
+ content: [
265
+ {
266
+ type: 'text',
267
+ text: buildHandoffSummaryText({
268
+ pkg,
269
+ topBlockers,
270
+ topRisks,
271
+ minimalityBlocked: enrichment.minimalityBlocked,
272
+ tokenWasteRiskCount: enrichment.tokenWasteRiskCount,
273
+ }),
274
+ },
275
+ ],
276
+ structuredContent: {
277
+ resultSchema: 'package-handoff-completion/v1',
278
+ specId,
279
+ blocked: pkg.blocked,
280
+ readinessScore: pkg.readinessScore,
281
+ blockersCount: pkg.blockers.length,
282
+ blockersTop3: topBlockers,
283
+ ownershipBlockers,
284
+ risksTop5: topRisks,
285
+ criteriaTop5: topItems(pkg.criteria, 5),
286
+ constraintsTop5: topItems(pkg.constraints, 5),
287
+ warningsTop5: topItems(pkg.warnings, 5),
288
+ handoffPath: pkg.handoffPath,
289
+ contextHash: pkg.contextHash,
290
+ artifactRefs: { handoff: pkg.handoffPath },
291
+ counts: {
292
+ criteria: pkg.criteria.length,
293
+ filesToModify: pkg.filesToModify.length,
294
+ filesToCreate: pkg.filesToCreate.length,
295
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- compat guard: callers may construct a HandoffPackage object predating this field
296
+ filesToTest: (pkg.filesToTest ?? []).length,
297
+ ownership: pkg.ownership.length,
298
+ testPlan: pkg.testPlan.length,
299
+ risks: pkg.risks.length,
300
+ constraints: pkg.constraints.length,
301
+ warnings: pkg.warnings.length,
302
+ outOfScope: pkg.outOfScope.length,
303
+ },
304
+ nextAction: pkg.nextAction,
305
+ ...(enrichment.minimalitySummary !== undefined
306
+ ? { minimality: enrichment.minimalitySummary }
307
+ : {}),
308
+ ...(enrichment.tokenWasteSummary !== undefined
309
+ ? { tokenWaste: enrichment.tokenWasteSummary }
310
+ : {}),
311
+ ...(graphSummary !== undefined ? { graphContext: graphSummary } : {}),
312
+ stageTimings,
313
+ degradations,
314
+ },
315
+ };
316
+ }
317
+ /** Executes accepted package-handoff jobs directly against the packaging engine, never through MCP. */
318
+ export class PackageHandoffJobExecutor {
319
+ runtime;
320
+ workerId;
321
+ constructor(runtime, workerId = `handoff-${randomUUID()}`) {
322
+ this.runtime = runtime;
323
+ this.workerId = workerId;
324
+ }
325
+ async runOnce(projectId, workspaceId = WORKSPACE_ID) {
326
+ const candidates = this.candidates(projectId, workspaceId);
327
+ return Promise.all(candidates.map((job) => this.execute(job)));
328
+ }
329
+ async runUntilIdle(projectId, workspaceId = WORKSPACE_ID) {
330
+ for (;;) {
331
+ const candidates = this.candidates(projectId, workspaceId);
332
+ if (candidates.length === 0) {
333
+ return;
334
+ }
335
+ await Promise.all(candidates.map((job) => this.execute(job).catch((error) => {
336
+ reportClassifiedDegradation('HANDOFF_JOB_EXECUTION_FAILURE', error);
337
+ })));
338
+ }
339
+ }
340
+ candidates(projectId, workspaceId) {
341
+ return this.runtime
342
+ .list(projectId, workspaceId)
343
+ .filter((job) => (job.state === 'accepted' || job.state === 'checkpointed') &&
344
+ isPackageHandoffJob(job.payload));
345
+ }
346
+ async execute(job) {
347
+ const identity = {
348
+ operationId: job.id,
349
+ projectId: job.projectId,
350
+ workspaceId: job.workspaceId,
351
+ };
352
+ const leaseMs = coreBudgetMs() + enrichmentBudgetMs() * ENRICHMENT_STAGE_COUNT + LEASE_SAFETY_MARGIN_MS;
353
+ const claimed = this.runtime.claim({ ...identity, workerId: this.workerId, leaseMs });
354
+ if (!claimed) {
355
+ return this.runtime.get(identity);
356
+ }
357
+ const payload = parsePackageHandoffPayload(claimed.payload);
358
+ if (!payload) {
359
+ return this.runtime.fail({
360
+ ...identity,
361
+ workerId: this.workerId,
362
+ fencingToken: claimed.fencingToken,
363
+ error: '[Planu] Invalid package-handoff job payload',
364
+ });
365
+ }
366
+ try {
367
+ const result = await this.runJob(payload);
368
+ const completed = this.runtime.complete({
369
+ ...identity,
370
+ workerId: this.workerId,
371
+ fencingToken: claimed.fencingToken,
372
+ result,
373
+ });
374
+ const structured = result.structuredContent;
375
+ try {
376
+ await appendTransitionEvent({
377
+ projectId: payload.projectId,
378
+ specId: payload.specId,
379
+ eventType: 'handoff.intake',
380
+ gateResults: { handoff: structured?.blocked === true ? 'fail' : 'pass' },
381
+ meta: {
382
+ handoffPath: structured?.handoffPath,
383
+ contextHash: structured?.contextHash,
384
+ blockers: structured?.blockersTop3 ?? [],
385
+ },
386
+ });
387
+ }
388
+ catch (error) {
389
+ reportClassifiedDegradation('HANDOFF_INTAKE_EVENT_FAILURE', error);
390
+ }
391
+ return completed;
392
+ }
393
+ catch (error) {
394
+ if (error instanceof CoreStageFailure) {
395
+ return this.runtime.fail({
396
+ ...identity,
397
+ workerId: this.workerId,
398
+ fencingToken: claimed.fencingToken,
399
+ error: classifiedCoreFailureMessage(error),
400
+ });
401
+ }
402
+ reportClassifiedDegradation('HANDOFF_JOB_EXECUTION_FAILURE', error);
403
+ return this.runtime.fail({
404
+ ...identity,
405
+ workerId: this.workerId,
406
+ fencingToken: claimed.fencingToken,
407
+ error: (error instanceof Error ? error.message : String(error)).slice(0, 4_096),
408
+ });
409
+ }
410
+ }
411
+ async runJob(payload) {
412
+ const spec = await specStore.getSpec(payload.projectId, payload.specId);
413
+ if (!spec) {
414
+ throw new Error(`[Planu] Spec '${payload.specId}' not found in project '${payload.projectId}'`);
415
+ }
416
+ const knowledge = await knowledgeStore.getKnowledge(payload.projectId);
417
+ if (!knowledge) {
418
+ throw new Error(`[Planu] Project '${payload.projectId}' not found. Run init_project first.`);
419
+ }
420
+ const core = await runCoreStages(payload, spec, knowledge);
421
+ const pkgWithScore = { ...core.pkg, readinessScore: core.readinessScore };
422
+ const publishDegradation = core.pkg.staleVersionSkipped === true
423
+ ? {
424
+ stage: 'artifact-publish',
425
+ code: 'HANDOFF_STALE_VERSION_SKIPPED',
426
+ remediation: 'The spec advanced to a newer spec_version before this job published its artifact; the newer artifact was preserved untouched. Re-run package_handoff to publish the current version.',
427
+ }
428
+ : undefined;
429
+ const enrichment = await runEnrichmentStages(payload, spec, knowledge, pkgWithScore);
430
+ return buildCompletionResult({
431
+ specId: payload.specId,
432
+ pkg: enrichment.pkgWithConstraints,
433
+ enrichment,
434
+ stageTimings: { ...core.stageTimings, ...enrichment.stageTimings },
435
+ degradations: publishDegradation
436
+ ? [publishDegradation, ...enrichment.degradations]
437
+ : [...enrichment.degradations],
438
+ });
439
+ }
440
+ }
441
+ /** Schedules background execution after the submit response can be flushed. */
442
+ export function schedulePackageHandoffJobExecution(projectId, workspaceId = WORKSPACE_ID) {
443
+ const scope = `${projectId}:${workspaceId}`;
444
+ const active = activeScopes.get(scope);
445
+ if (active) {
446
+ active.rescheduleRequested = true;
447
+ return;
448
+ }
449
+ const state = { rescheduleRequested: false };
450
+ activeScopes.set(scope, state);
451
+ setImmediate(() => {
452
+ const runtime = new DurableJobRuntime();
453
+ const executor = new PackageHandoffJobExecutor(runtime);
454
+ void (async () => {
455
+ try {
456
+ do {
457
+ state.rescheduleRequested = false;
458
+ await executor.runUntilIdle(projectId, workspaceId);
459
+ } while (rescheduleRequested(state));
460
+ }
461
+ finally {
462
+ runtime.close();
463
+ activeScopes.delete(scope);
464
+ }
465
+ })();
466
+ });
467
+ }
468
+ function rescheduleRequested(state) {
469
+ return state.rescheduleRequested;
470
+ }
471
+ /** Re-enqueues accepted/checkpointed package-handoff work discovered after a process restart. */
472
+ export function recoverPackageHandoffJobsAtStartup() {
473
+ const runtime = new DurableJobRuntime();
474
+ try {
475
+ const scopes = runtime.listRunnableScopes();
476
+ for (const scope of scopes) {
477
+ const hasHandoffWork = runtime
478
+ .list(scope.projectId, scope.workspaceId)
479
+ .some((job) => (job.state === 'accepted' || job.state === 'checkpointed') &&
480
+ isPackageHandoffJob(job.payload));
481
+ if (hasHandoffWork) {
482
+ schedulePackageHandoffJobExecution(scope.projectId, scope.workspaceId);
483
+ }
484
+ }
485
+ }
486
+ finally {
487
+ runtime.close();
488
+ }
489
+ }
490
+ //# sourceMappingURL=package-handoff-job-executor.js.map
@@ -13,7 +13,7 @@ import { resolveStorageLayout } from '../../storage/storage-layout.js';
13
13
  import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
14
14
  import { createValidationReceiptAuthority, ensureValidationIssuerKey, issueValidationReceipt, LOCAL_VALIDATION_RECEIPT_ISSUER, preflightValidationReceiptSigner, } from '../validation/validation-receipt.js';
15
15
  import { appendArtifact, readArtifact } from '../handoff-artifacts/io.js';
16
- import { assertValidationFreshnessBarrier, beginValidationFreshnessBarrier, beginValidationFreshnessPublicationWindow, bindValidationFreshnessLease, closeValidationFreshnessLease, rebaselineValidationFreshnessLeaseAfterRecompute, } from '../validation/validation-freshness.js';
16
+ import { beginValidationFreshnessBarrier, beginValidationFreshnessPublicationWindow, bindValidationFreshnessLease, closeValidationFreshnessLease, evaluateValidationFreshnessBarrier, rebaselineValidationFreshnessLeaseAfterRecompute, } from '../validation/validation-freshness.js';
17
17
  import { recoverValidateJobs } from './validate-job-recovery.js';
18
18
  import { getRuntimePolicy } from '../runtime-policy.js';
19
19
  import { isValidateJob, parseValidatePayload, rehydrateValidationPayload, } from './validate-job-payload.js';
@@ -148,6 +148,16 @@ function toolResultError(result) {
148
148
  function executionAborted(signal) {
149
149
  return signal.aborted;
150
150
  }
151
+ function actionableFreshnessMessage(prefix, reason, changedInput) {
152
+ const changedSuffix = changedInput ? ` (changed: ${changedInput})` : '';
153
+ return `${prefix}: ${reason}${changedSuffix} Re-run validate to issue a fresh receipt, then retry the original call.`;
154
+ }
155
+ function freshnessBarrierErrorMessage(phase, result) {
156
+ return actionableFreshnessMessage(`[Planu] Validation inputs changed ${phase} receipt publication`, result.reason, result.changedInput);
157
+ }
158
+ function rebaselineRejectionErrorMessage(rejection) {
159
+ return actionableFreshnessMessage('[Planu] Validation inputs changed before receipt publication', rejection.reason, rejection.changedInput);
160
+ }
151
161
  function hasReceiptInputs(payload) {
152
162
  return Boolean(payload.bindings && payload.artifactDigests && payload.freshnessLease);
153
163
  }
@@ -688,8 +698,9 @@ export class ValidateJobExecutor {
688
698
  if (executionAborted(signal)) {
689
699
  signal.throwIfAborted();
690
700
  }
691
- if (!(await assertValidationFreshnessBarrier(publicationLease, receiptBindings))) {
692
- throw new Error('[Planu] Validation inputs changed before receipt publication');
701
+ const barrierBeforePublication = await evaluateValidationFreshnessBarrier(publicationLease, receiptBindings);
702
+ if (!barrierBeforePublication.fresh) {
703
+ throw new Error(freshnessBarrierErrorMessage('before', barrierBeforePublication));
693
704
  }
694
705
  const database = new RuntimeDatabase({ path: resolveStorageLayout().runtimeDatabase });
695
706
  try {
@@ -723,9 +734,10 @@ export class ValidateJobExecutor {
723
734
  if (executionAborted(signal)) {
724
735
  signal.throwIfAborted();
725
736
  }
726
- if (!(await assertValidationFreshnessBarrier(publicationLease, receiptBindings))) {
737
+ const barrierDuringPublication = await evaluateValidationFreshnessBarrier(publicationLease, receiptBindings);
738
+ if (!barrierDuringPublication.fresh) {
727
739
  await store.invalidateCurrentBindings(identity.projectId, payload.bindings.specId);
728
- throw new Error('[Planu] Validation inputs changed during receipt publication');
740
+ throw new Error(freshnessBarrierErrorMessage('during', barrierDuringPublication));
729
741
  }
730
742
  const receiptKey = `validation-receipt:${receipt.receiptId.slice('sha256:'.length)}`;
731
743
  let enrichedReportSha;
@@ -775,10 +787,10 @@ export class ValidateJobExecutor {
775
787
  return toValidationReceiptBindings(observed.bindings, observed.artifactDigests);
776
788
  },
777
789
  });
778
- if (!rebaselined) {
779
- throw new Error('[Planu] Validation inputs changed before receipt publication');
790
+ if ('rejection' in rebaselined) {
791
+ throw new Error(rebaselineRejectionErrorMessage(rebaselined.rejection));
780
792
  }
781
- lease = rebaselined;
793
+ lease = rebaselined.lease;
782
794
  return lease;
783
795
  }
784
796
  async persistReceiptArtifacts(identity, payload, receiptId, receiptKey) {
@@ -0,0 +1,4 @@
1
+ export declare const GIT_EXEC_TIMEOUT_MS = 5000;
2
+ export declare const GIT_EXEC_MAX_BUFFER_BYTES: number;
3
+ export declare function execGitCapture(command: string, cwd: string): string;
4
+ //# sourceMappingURL=exec-git.d.ts.map
@@ -0,0 +1,14 @@
1
+ // engine/git/exec-git.ts — SPEC-1560: bounded synchronous git capture helper
2
+ import { execSync } from 'node:child_process';
3
+ export const GIT_EXEC_TIMEOUT_MS = 5_000;
4
+ export const GIT_EXEC_MAX_BUFFER_BYTES = 10 * 1024 * 1024;
5
+ export function execGitCapture(command, cwd) {
6
+ return execSync(command, {
7
+ cwd,
8
+ timeout: GIT_EXEC_TIMEOUT_MS,
9
+ maxBuffer: GIT_EXEC_MAX_BUFFER_BYTES,
10
+ stdio: ['pipe', 'pipe', 'pipe'],
11
+ encoding: 'utf-8',
12
+ });
13
+ }
14
+ //# sourceMappingURL=exec-git.js.map