@mcoda/codali 0.1.134 → 0.1.135-codali.4

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 (230) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/ORCHESTRATION.md +387 -0
  3. package/README.md +103 -1
  4. package/config/alternate-local.example.json +49 -0
  5. package/config/docdex-runtime.json +21 -0
  6. package/config/orchestration.schema.json +548 -0
  7. package/config/raijin-suku.example.json +172 -0
  8. package/config/terminal-long-scenarios.v1.json +70 -0
  9. package/dist/agents/RoleResolution.d.ts.map +1 -1
  10. package/dist/agents/RoleResolution.js +4 -4
  11. package/dist/api/CodaliApi.d.ts +16 -0
  12. package/dist/api/CodaliApi.d.ts.map +1 -1
  13. package/dist/api/CodaliApi.js +39 -10
  14. package/dist/cli/AskCommand.d.ts +3 -0
  15. package/dist/cli/AskCommand.d.ts.map +1 -1
  16. package/dist/cli/AskCommand.js +76 -13
  17. package/dist/cli/RunCommand.d.ts +4 -0
  18. package/dist/cli/RunCommand.d.ts.map +1 -1
  19. package/dist/cli/RunCommand.js +110 -24
  20. package/dist/cli/TerminalCommand.d.ts +35 -0
  21. package/dist/cli/TerminalCommand.d.ts.map +1 -0
  22. package/dist/cli/TerminalCommand.js +616 -0
  23. package/dist/cli/TerminalCredentials.d.ts +3 -0
  24. package/dist/cli/TerminalCredentials.d.ts.map +1 -0
  25. package/dist/cli/TerminalCredentials.js +89 -0
  26. package/dist/cli/TerminalInput.d.ts +24 -0
  27. package/dist/cli/TerminalInput.d.ts.map +1 -0
  28. package/dist/cli/TerminalInput.js +98 -0
  29. package/dist/cli/TerminalText.d.ts +10 -0
  30. package/dist/cli/TerminalText.d.ts.map +1 -0
  31. package/dist/cli/TerminalText.js +53 -0
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +33 -4
  34. package/dist/cognitive/ConfiguredPipeline.d.ts +34 -0
  35. package/dist/cognitive/ConfiguredPipeline.d.ts.map +1 -0
  36. package/dist/cognitive/ConfiguredPipeline.js +50 -0
  37. package/dist/cognitive/ValidationRunner.d.ts +4 -0
  38. package/dist/cognitive/ValidationRunner.d.ts.map +1 -1
  39. package/dist/cognitive/ValidationRunner.js +18 -41
  40. package/dist/config/NodeOrchestration.d.ts +21 -0
  41. package/dist/config/NodeOrchestration.d.ts.map +1 -0
  42. package/dist/config/NodeOrchestration.js +54 -0
  43. package/dist/config/OrchestrationConfig.d.ts +100 -0
  44. package/dist/config/OrchestrationConfig.d.ts.map +1 -0
  45. package/dist/config/OrchestrationConfig.js +245 -0
  46. package/dist/connectors/mcp/McpClient.d.ts +3 -1
  47. package/dist/connectors/mcp/McpClient.d.ts.map +1 -1
  48. package/dist/connectors/mcp/McpClient.js +3 -2
  49. package/dist/connectors/mcp/McpServerRegistry.d.ts +2 -2
  50. package/dist/connectors/mcp/McpServerRegistry.d.ts.map +1 -1
  51. package/dist/connectors/mcp/McpServerRegistry.js +39 -16
  52. package/dist/connectors/mcp/McpToolAdapter.js +2 -2
  53. package/dist/connectors/media/ConfiguredMediaTools.d.ts +6 -0
  54. package/dist/connectors/media/ConfiguredMediaTools.d.ts.map +1 -0
  55. package/dist/connectors/media/ConfiguredMediaTools.js +41 -0
  56. package/dist/connectors/media/ImageGenerationTool.js +3 -3
  57. package/dist/connectors/media/MediaArtifactReceipt.d.ts +5 -0
  58. package/dist/connectors/media/MediaArtifactReceipt.d.ts.map +1 -0
  59. package/dist/connectors/media/MediaArtifactReceipt.js +65 -0
  60. package/dist/connectors/media/MediaJobStore.d.ts +68 -0
  61. package/dist/connectors/media/MediaJobStore.d.ts.map +1 -0
  62. package/dist/connectors/media/MediaJobStore.js +199 -0
  63. package/dist/connectors/media/MediaJobWorker.d.ts +2 -0
  64. package/dist/connectors/media/MediaJobWorker.d.ts.map +1 -0
  65. package/dist/connectors/media/MediaJobWorker.js +65 -0
  66. package/dist/connectors/media/RemoteMediaJob.d.ts +6 -0
  67. package/dist/connectors/media/RemoteMediaJob.d.ts.map +1 -0
  68. package/dist/connectors/media/RemoteMediaJob.js +80 -0
  69. package/dist/docdex/DocdexClient.d.ts +18 -1
  70. package/dist/docdex/DocdexClient.d.ts.map +1 -1
  71. package/dist/docdex/DocdexClient.js +135 -19
  72. package/dist/docdex/ManagedDocdex.d.ts +29 -0
  73. package/dist/docdex/ManagedDocdex.d.ts.map +1 -0
  74. package/dist/docdex/ManagedDocdex.js +215 -0
  75. package/dist/docdex/ManagedRuntime.d.ts +28 -0
  76. package/dist/docdex/ManagedRuntime.d.ts.map +1 -0
  77. package/dist/docdex/ManagedRuntime.js +212 -0
  78. package/dist/eval/EvaluationProcess.d.ts +13 -0
  79. package/dist/eval/EvaluationProcess.d.ts.map +1 -0
  80. package/dist/eval/EvaluationProcess.js +76 -0
  81. package/dist/eval/LongWorkflowJourney.d.ts +34 -0
  82. package/dist/eval/LongWorkflowJourney.d.ts.map +1 -0
  83. package/dist/eval/LongWorkflowJourney.js +243 -0
  84. package/dist/eval/PlanningCorpus.d.ts +45 -0
  85. package/dist/eval/PlanningCorpus.d.ts.map +1 -0
  86. package/dist/eval/PlanningCorpus.js +189 -0
  87. package/dist/eval/PlanningCorpusRunner.d.ts +13 -0
  88. package/dist/eval/PlanningCorpusRunner.d.ts.map +1 -0
  89. package/dist/eval/PlanningCorpusRunner.js +89 -0
  90. package/dist/eval/TerminalCorpusRunner.d.ts +36 -0
  91. package/dist/eval/TerminalCorpusRunner.d.ts.map +1 -0
  92. package/dist/eval/TerminalCorpusRunner.js +222 -0
  93. package/dist/eval/TerminalTaskCorpus.d.ts +26 -0
  94. package/dist/eval/TerminalTaskCorpus.d.ts.map +1 -0
  95. package/dist/eval/TerminalTaskCorpus.js +105 -0
  96. package/dist/eval/TerminalWorkflowCorpus.d.ts +11 -0
  97. package/dist/eval/TerminalWorkflowCorpus.d.ts.map +1 -0
  98. package/dist/eval/TerminalWorkflowCorpus.js +116 -0
  99. package/dist/eval/fixtures/TerminalRecoveryJourney.d.ts +4 -0
  100. package/dist/eval/fixtures/TerminalRecoveryJourney.d.ts.map +1 -0
  101. package/dist/eval/fixtures/TerminalRecoveryJourney.js +132 -0
  102. package/dist/gateway/CodaliGateway.d.ts +2 -0
  103. package/dist/gateway/CodaliGateway.d.ts.map +1 -1
  104. package/dist/gateway/CodaliGateway.js +11 -1
  105. package/dist/gateway/ConfiguredGateway.d.ts +23 -0
  106. package/dist/gateway/ConfiguredGateway.d.ts.map +1 -0
  107. package/dist/gateway/ConfiguredGateway.js +44 -0
  108. package/dist/gateway/GatewayTracer.d.ts +1 -1
  109. package/dist/gateway/GatewayTracer.d.ts.map +1 -1
  110. package/dist/gateway/GatewayTracer.js +2 -27
  111. package/dist/index.d.ts +23 -0
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +12 -0
  114. package/dist/permissions/PermissionGate.d.ts +44 -0
  115. package/dist/permissions/PermissionGate.d.ts.map +1 -0
  116. package/dist/permissions/PermissionGate.js +97 -0
  117. package/dist/providers/ConfiguredEmbeddings.d.ts +43 -0
  118. package/dist/providers/ConfiguredEmbeddings.d.ts.map +1 -0
  119. package/dist/providers/ConfiguredEmbeddings.js +87 -0
  120. package/dist/providers/ConfiguredModelScope.d.ts +69 -0
  121. package/dist/providers/ConfiguredModelScope.d.ts.map +1 -0
  122. package/dist/providers/ConfiguredModelScope.js +113 -0
  123. package/dist/providers/ConfiguredProvider.d.ts +79 -0
  124. package/dist/providers/ConfiguredProvider.d.ts.map +1 -0
  125. package/dist/providers/ConfiguredProvider.js +238 -0
  126. package/dist/providers/ModelServiceError.d.ts +10 -0
  127. package/dist/providers/ModelServiceError.d.ts.map +1 -0
  128. package/dist/providers/ModelServiceError.js +45 -0
  129. package/dist/providers/MswarmWorkerProvider.d.ts.map +1 -1
  130. package/dist/providers/MswarmWorkerProvider.js +4 -0
  131. package/dist/providers/OpenAiCompatibleProvider.d.ts.map +1 -1
  132. package/dist/providers/OpenAiCompatibleProvider.js +91 -50
  133. package/dist/providers/ProviderTypes.d.ts +12 -0
  134. package/dist/providers/ProviderTypes.d.ts.map +1 -1
  135. package/dist/runtime/CodaliJobRuntime.d.ts +6 -0
  136. package/dist/runtime/CodaliJobRuntime.d.ts.map +1 -1
  137. package/dist/runtime/CodaliJobRuntime.js +36 -1
  138. package/dist/runtime/CodaliRuntime.d.ts +28 -0
  139. package/dist/runtime/CodaliRuntime.d.ts.map +1 -1
  140. package/dist/runtime/CodaliRuntime.js +69 -1
  141. package/dist/runtime/CompletionReport.d.ts +5 -0
  142. package/dist/runtime/CompletionReport.d.ts.map +1 -0
  143. package/dist/runtime/CompletionReport.js +12 -0
  144. package/dist/runtime/ProgressTracker.d.ts +17 -0
  145. package/dist/runtime/ProgressTracker.d.ts.map +1 -0
  146. package/dist/runtime/ProgressTracker.js +27 -0
  147. package/dist/runtime/RequestedTestEvidence.d.ts +12 -0
  148. package/dist/runtime/RequestedTestEvidence.d.ts.map +1 -0
  149. package/dist/runtime/RequestedTestEvidence.js +16 -0
  150. package/dist/runtime/RequiredRetrieval.d.ts +13 -0
  151. package/dist/runtime/RequiredRetrieval.d.ts.map +1 -0
  152. package/dist/runtime/RequiredRetrieval.js +25 -0
  153. package/dist/runtime/ReviewSource.d.ts +6 -0
  154. package/dist/runtime/ReviewSource.d.ts.map +1 -0
  155. package/dist/runtime/ReviewSource.js +38 -0
  156. package/dist/runtime/Runner.d.ts +7 -0
  157. package/dist/runtime/Runner.d.ts.map +1 -1
  158. package/dist/runtime/Runner.js +215 -171
  159. package/dist/runtime/SecretRedaction.d.ts +11 -0
  160. package/dist/runtime/SecretRedaction.d.ts.map +1 -0
  161. package/dist/runtime/SecretRedaction.js +85 -0
  162. package/dist/runtime/SessionController.d.ts +148 -0
  163. package/dist/runtime/SessionController.d.ts.map +1 -0
  164. package/dist/runtime/SessionController.js +1357 -0
  165. package/dist/runtime/StructuralEvidence.d.ts +22 -0
  166. package/dist/runtime/StructuralEvidence.d.ts.map +1 -0
  167. package/dist/runtime/StructuralEvidence.js +87 -0
  168. package/dist/runtime/TaskReviewContext.d.ts +34 -0
  169. package/dist/runtime/TaskReviewContext.d.ts.map +1 -0
  170. package/dist/runtime/TaskReviewContext.js +34 -0
  171. package/dist/runtime/ValidationOutput.d.ts +5 -0
  172. package/dist/runtime/ValidationOutput.d.ts.map +1 -0
  173. package/dist/runtime/ValidationOutput.js +32 -0
  174. package/dist/runtime/VerificationCommand.d.ts +5 -0
  175. package/dist/runtime/VerificationCommand.d.ts.map +1 -0
  176. package/dist/runtime/VerificationCommand.js +76 -0
  177. package/dist/runtime/WorkStatusPage.d.ts +15 -0
  178. package/dist/runtime/WorkStatusPage.d.ts.map +1 -0
  179. package/dist/runtime/WorkStatusPage.js +58 -0
  180. package/dist/runtime/WorkspaceLedger.d.ts +13 -0
  181. package/dist/runtime/WorkspaceLedger.d.ts.map +1 -0
  182. package/dist/runtime/WorkspaceLedger.js +145 -0
  183. package/dist/runtime/WorkspaceLock.d.ts.map +1 -1
  184. package/dist/runtime/WorkspaceLock.js +20 -17
  185. package/dist/runtime/WorkspaceSnapshot.d.ts +13 -0
  186. package/dist/runtime/WorkspaceSnapshot.d.ts.map +1 -0
  187. package/dist/runtime/WorkspaceSnapshot.js +88 -0
  188. package/dist/session/AtomicState.d.ts +13 -0
  189. package/dist/session/AtomicState.d.ts.map +1 -0
  190. package/dist/session/AtomicState.js +123 -0
  191. package/dist/session/SessionStoragePath.d.ts +15 -0
  192. package/dist/session/SessionStoragePath.d.ts.map +1 -0
  193. package/dist/session/SessionStoragePath.js +89 -0
  194. package/dist/session/SessionStore.d.ts +9 -0
  195. package/dist/session/SessionStore.d.ts.map +1 -1
  196. package/dist/session/SessionStore.js +181 -47
  197. package/dist/session/WorkState.d.ts +23 -0
  198. package/dist/session/WorkState.d.ts.map +1 -0
  199. package/dist/session/WorkState.js +367 -0
  200. package/dist/session/WorkTypes.d.ts +94 -0
  201. package/dist/session/WorkTypes.d.ts.map +1 -0
  202. package/dist/session/WorkTypes.js +1 -0
  203. package/dist/tools/ToolRegistry.d.ts +2 -0
  204. package/dist/tools/ToolRegistry.d.ts.map +1 -1
  205. package/dist/tools/ToolRegistry.js +12 -2
  206. package/dist/tools/ToolTypes.d.ts +16 -0
  207. package/dist/tools/ToolTypes.d.ts.map +1 -1
  208. package/dist/tools/docdex/DocdexTools.d.ts +2 -0
  209. package/dist/tools/docdex/DocdexTools.d.ts.map +1 -1
  210. package/dist/tools/docdex/DocdexTools.js +17 -2
  211. package/dist/tools/filesystem/FileTools.d.ts.map +1 -1
  212. package/dist/tools/filesystem/FileTools.js +30 -10
  213. package/dist/tools/shell/AsyncProcess.d.ts +19 -0
  214. package/dist/tools/shell/AsyncProcess.d.ts.map +1 -0
  215. package/dist/tools/shell/AsyncProcess.js +94 -0
  216. package/dist/tools/shell/DarwinRuntimeLibraries.d.ts +13 -0
  217. package/dist/tools/shell/DarwinRuntimeLibraries.d.ts.map +1 -0
  218. package/dist/tools/shell/DarwinRuntimeLibraries.js +130 -0
  219. package/dist/tools/shell/HostToolchain.d.ts +19 -0
  220. package/dist/tools/shell/HostToolchain.d.ts.map +1 -0
  221. package/dist/tools/shell/HostToolchain.js +266 -0
  222. package/dist/tools/shell/IsolatedExecutor.d.ts +15 -0
  223. package/dist/tools/shell/IsolatedExecutor.d.ts.map +1 -0
  224. package/dist/tools/shell/IsolatedExecutor.js +138 -0
  225. package/dist/tools/shell/ShellTool.d.ts.map +1 -1
  226. package/dist/tools/shell/ShellTool.js +9 -29
  227. package/dist/tools/shell/ShellWorkingDirectory.d.ts +3 -0
  228. package/dist/tools/shell/ShellWorkingDirectory.d.ts.map +1 -0
  229. package/dist/tools/shell/ShellWorkingDirectory.js +30 -0
  230. package/package.json +27 -24
@@ -0,0 +1,243 @@
1
+ import { createHash } from "node:crypto";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { SessionController } from "../runtime/SessionController.js";
6
+ import { loadOrchestrationConfig } from "../config/OrchestrationConfig.js";
7
+ import { captureWorkspace, changedWorkspacePaths } from "../runtime/WorkspaceSnapshot.js";
8
+ import { runProcess } from "../tools/shell/AsyncProcess.js";
9
+ import { gradeTerminalTask, materializeTerminalTask, terminalClientBuildHash } from "./TerminalCorpusRunner.js";
10
+ import { terminalWorkflowCorpus, terminalWorkflowCorpusHash } from "./TerminalWorkflowCorpus.js";
11
+ import { runEvaluationProcess } from "./EvaluationProcess.js";
12
+ export const LONG_WORKFLOW_VERSION = "long-workflows-v2";
13
+ export const longWorkflowScenarios = [
14
+ { id: "data-delivery", objective: "Deliver a reliable data-processing toolkit with exact money totals, validated imports, CSV export and additive record migration.", modules: ["invoice-cents", "jsonl-import", "csv-export", "additive-record-migration"] },
15
+ { id: "service-adapters", objective: "Deliver a service-adapter toolkit with safe headers, bounded async execution, locale fallback and interval reservations.", modules: ["header-normalization", "async-worker-pool", "locale-fallback", "interval-reservation"] },
16
+ { id: "billing-audit", objective: "Deliver a billing-readiness package: source-backed duplicate-submit and tenant-isolation reports, exact invoice totals and a data-preserving migration.", modules: ["investigate-duplicate-submit", "investigate-tenant-key", "invoice-cents", "additive-record-migration"] },
17
+ { id: "localized-export", objective: "Deliver localized export adapters with CSV correctness, normalized headers and a source-backed fictional render-service upgrade report.", modules: ["locale-fallback", "csv-export", "header-normalization", "research-pinned-upgrade"] },
18
+ ];
19
+ const hash = (value) => createHash("sha256").update(value).digest("hex");
20
+ export const longWorkflowHash = () => hash(JSON.stringify({ version: LONG_WORKFLOW_VERSION, scenarios: longWorkflowScenarios, prompts: longWorkflowScenarios.map(longWorkflowPrompt), steering, workflowHash: terminalWorkflowCorpusHash() }));
21
+ const steering = "Keep every original module contract and protected file. Add DELIVERY.md containing a concise test-and-delivery inventory with the exact path modules/<module-id>/test/task.test.mjs for each of the four modules. Do not claim unavailable checks ran, add features, or remove criteria. This is additional required work, not a replacement objective.";
22
+ const prefixPrompt = (module, prompt) => prompt.replace(/\b(src|test|docs|vendor-docs)\//g, `modules/${module}/$1/`);
23
+ export function longWorkflowPrompt(scenario) {
24
+ const corpus = terminalWorkflowCorpus();
25
+ return `${scenario.objective}\nThe repository has four independent module packages under modules/. Each module's package.json, user notes, and any paths not explicitly writable below must remain byte-identical. Root package.json and USER_NOTES.txt must also remain unchanged. No dependencies or unrelated features. Paths used to locate files in these instructions are workspace-relative. Inside a module JSON report, all source-path values (including owner, client, server, callers, evidence and citations) must be relative to that module directory, without its modules/<module-id>/ prefix. API endpoint values remain unchanged.\nRecord canonical requirements R2-R5, one per module contract below, plus the existing original R1 for final integration after all module tasks. Record phases and a dependency-valid plan; R1 is final integration and must depend on all module deliverables. Record a modular architecture decision with rationale and alternatives. Module tasks may be independent; no inter-module imports are needed.\n${scenario.modules.map((id, index) => `R${index + 2}: ${prefixPrompt(id, corpus.find(task => task.id === id).prompt)}`).join("\n\n")}\nInitially you are in planning-only mode: inspect and record the plan, but do not execute or complete it. Execution will resume in a new process with workspace permissions. The overall delivery includes actual tests and all four module outcomes. Preserve that objective across restarts and corrections.`;
26
+ }
27
+ const saveNew = (file, value) => fs.writeFile(file, JSON.stringify(value, null, 2) + "\n", { flag: "wx", mode: 0o600 });
28
+ /** One invocation is one OS process in the parent runner. Stage 2 deliberately
29
+ * checkpoints after a verified task, never during a pending side effect. */
30
+ export async function runLongWorkflowStage(descriptorPath, stage) {
31
+ if (![1, 2, 3, 4].includes(stage))
32
+ throw new Error("Unknown journey stage");
33
+ const descriptor = JSON.parse(await fs.readFile(descriptorPath, "utf8"));
34
+ const scenario = longWorkflowScenarios.find(item => item.id === descriptor.scenarioId);
35
+ if (descriptor.schemaVersion !== 1 || !scenario || descriptor.corpusHash !== longWorkflowHash())
36
+ throw new Error("Incompatible journey descriptor");
37
+ const resolved = await loadOrchestrationConfig(descriptor.configPath);
38
+ if (resolved.hash !== descriptor.configHash || await terminalClientBuildHash() !== descriptor.clientBuildHash)
39
+ throw new Error("Journey configuration/client changed across processes");
40
+ const root = path.resolve(descriptor.root);
41
+ const workspaceRoot = path.join(root, "workspace");
42
+ const storageRoot = path.join(root, "state");
43
+ const events = [];
44
+ let checkpointTriggered = false;
45
+ let controller;
46
+ controller = new SessionController({ workspaceRoot, storageRoot, resolved, permissions: stage === 1 ? "plan" : "workspace", effort: "max",
47
+ onEvent: event => {
48
+ if (["model_route", "tool_receipt", "turn_failed", "turn_result", "usage"].includes(event.type))
49
+ events.push(event);
50
+ if (stage === 2 && !checkpointTriggered && event.type === "tool_receipt"
51
+ && controller.state?.tasks.some(task => task.status === "verified")) {
52
+ const receipt = event.data.receipt;
53
+ if (receipt.tool === "work_verify" && receipt.status === "success") {
54
+ checkpointTriggered = true;
55
+ controller.cancel();
56
+ }
57
+ }
58
+ } });
59
+ const startedAt = new Date().toISOString();
60
+ let failure;
61
+ let resumePreserved = stage === 1;
62
+ const abort = () => controller.cancel();
63
+ process.once("SIGTERM", abort);
64
+ process.once("SIGINT", abort);
65
+ try {
66
+ if (stage === 1)
67
+ await controller.start(descriptor.objective);
68
+ else {
69
+ const previous = JSON.parse(await fs.readFile(path.join(root, `stage-${stage - 1}.handoff.json`), "utf8"));
70
+ await controller.resume(previous.sessionId);
71
+ resumePreserved = hash(JSON.stringify(controller.state)) === previous.stateHash;
72
+ if (!resumePreserved)
73
+ throw new Error("Canonical state changed across the process boundary");
74
+ }
75
+ if (stage === 3) {
76
+ // Simulates a user edit made while the CLI was stopped. Existing bytes
77
+ // remain protected; the new line must survive subsequent model work.
78
+ await fs.appendFile(path.join(workspaceRoot, "USER_NOTES.txt"), "Concurrent user correction: preserve these notes and every original module contract.\n");
79
+ await controller.steer(steering);
80
+ }
81
+ if (stage < 4) {
82
+ try {
83
+ await controller.run();
84
+ }
85
+ catch (error) {
86
+ if (!checkpointTriggered || !/Cancelled by user/.test(String(error)))
87
+ throw error;
88
+ }
89
+ }
90
+ for (let i = 0; i < (stage === 1 ? 2 : 1); i++)
91
+ await controller.compact();
92
+ }
93
+ catch (error) {
94
+ failure = error instanceof Error ? error.message : String(error);
95
+ }
96
+ finally {
97
+ process.removeListener("SIGTERM", abort);
98
+ process.removeListener("SIGINT", abort);
99
+ }
100
+ const state = controller.state;
101
+ const record = { schemaVersion: 1, stage, startedAt, endedAt: new Date().toISOString(), pid: process.pid,
102
+ sessionId: controller.sessionId, failure, resumePreserved, checkpointTriggered, usage: controller.usageSnapshot(), state,
103
+ routes: events.filter(event => event.type === "model_route").map(event => event.data),
104
+ receipts: events.filter(event => event.type === "tool_receipt").map(event => event.data.receipt) };
105
+ await saveNew(path.join(root, `stage-${stage}.result.json`), record);
106
+ if (state && controller.sessionId)
107
+ await saveNew(path.join(root, `stage-${stage}.handoff.json`), { sessionId: controller.sessionId, stage, stateHash: hash(JSON.stringify(state)) });
108
+ if (failure)
109
+ throw new Error(failure);
110
+ if (stage === 2 && !checkpointTriggered)
111
+ throw new Error("Execution did not reach the required verified-task checkpoint");
112
+ }
113
+ export function gradeLongWorkflowState(objective, state, initial) {
114
+ const failures = [];
115
+ if (state.originalObjective !== objective)
116
+ failures.push("Original objective changed");
117
+ for (const id of ["R1", "R2", "R3", "R4", "R5", "steering-2"]) {
118
+ if (!state.requirements.some(r => r.id === id && r.required))
119
+ failures.push(`Required criterion lost: ${id}`);
120
+ if (!state.tasks.some(task => task.status === "verified" && task.requirementIds.includes(id)))
121
+ failures.push(`No verified owner: ${id}`);
122
+ }
123
+ if (state.status !== "completed")
124
+ failures.push("Goal is not completed");
125
+ if (!state.decisions.length)
126
+ failures.push("Architecture decision missing");
127
+ if (!state.phases?.length)
128
+ failures.push("Phased plan missing");
129
+ if (initial) {
130
+ for (const criterion of initial.requirements)
131
+ if (!state.requirements.some(r => r.id === criterion.id && r.description === criterion.description && r.required === criterion.required))
132
+ failures.push(`Criterion contract changed: ${criterion.id}`);
133
+ for (const decision of initial.decisions)
134
+ if (!state.decisions.some(d => JSON.stringify(d) === JSON.stringify(decision)))
135
+ failures.push(`Architecture history changed: ${decision.id}`);
136
+ }
137
+ const finalOwners = state.tasks.filter(task => task.requirementIds.includes("R1"));
138
+ const reaches = (id, target, seen = new Set()) => {
139
+ if (seen.has(id))
140
+ return false;
141
+ seen.add(id);
142
+ const task = state.tasks.find(item => item.id === id);
143
+ if (!task)
144
+ return false;
145
+ const phase = state.phases?.find(item => item.id === task.phaseId);
146
+ const deps = [...task.dependsOn, ...state.tasks.filter(item => item.phaseId && phase?.dependsOn.includes(item.phaseId)).map(item => item.id)];
147
+ return deps.some(dep => dep === target || reaches(dep, target, seen));
148
+ };
149
+ if (finalOwners.length !== 1 || state.tasks.filter(task => task.requirementIds.some(id => ["R2", "R3", "R4", "R5"].includes(id)))
150
+ .some(task => !reaches(finalOwners[0]?.id ?? "", task.id)))
151
+ failures.push("Original delivery integration does not depend on every module owner");
152
+ if (state.amendments.length !== 1 || state.goalRevision !== 2)
153
+ failures.push("Steering history was lost or unexpectedly replaced");
154
+ if (state.checkpoints.filter(c => c.note.startsWith("Context compacted")).length < 5)
155
+ failures.push("Fewer than five recorded compactions");
156
+ return failures;
157
+ }
158
+ export async function runLongWorkflowJourney(options) {
159
+ const scenario = longWorkflowScenarios.find(item => item.id === options.scenarioId);
160
+ if (!scenario)
161
+ throw new Error("Unknown long workflow scenario");
162
+ const root = path.resolve(options.outputRoot);
163
+ const workspaceRoot = path.join(root, "workspace");
164
+ await fs.mkdir(root, { mode: 0o700 });
165
+ await fs.mkdir(workspaceRoot, { mode: 0o700 });
166
+ await fs.mkdir(path.join(workspaceRoot, "modules"));
167
+ const tasks = scenario.modules.map(id => terminalWorkflowCorpus().find(task => task.id === id));
168
+ for (const task of tasks)
169
+ await materializeTerminalTask(task, path.join(workspaceRoot, "modules", task.id));
170
+ await fs.writeFile(path.join(workspaceRoot, "package.json"), JSON.stringify({ private: true, type: "module", scripts: { test: "node --test" } }) + "\n");
171
+ const notes = "User-owned journey notes. Keep every original module and delivery requirement.\n";
172
+ await fs.writeFile(path.join(workspaceRoot, "USER_NOTES.txt"), notes);
173
+ await runProcess("git", ["-c", "core.hooksPath=/dev/null", "init", "--quiet"], { workspaceRoot, shellTimeoutMs: 10000 });
174
+ await runProcess("git", ["-c", "core.hooksPath=/dev/null", "add", "--all"], { workspaceRoot, shellTimeoutMs: 10000 });
175
+ await runProcess("git", ["-c", "core.hooksPath=/dev/null", "-c", "user.name=Codali evaluation", "-c", "user.email=codali-evaluation@localhost", "-c", "commit.gpgSign=false", "commit", "--quiet", "-m", "Long workflow fixture baseline"], { workspaceRoot, shellTimeoutMs: 10000 });
176
+ const before = await captureWorkspace(workspaceRoot);
177
+ const resolved = await loadOrchestrationConfig(options.configPath);
178
+ const clientBuildHash = await terminalClientBuildHash();
179
+ const descriptor = { schemaVersion: 1, scenarioId: scenario.id, configPath: path.resolve(options.configPath), configHash: resolved.hash, clientBuildHash, root, objective: longWorkflowPrompt(scenario), corpusHash: longWorkflowHash() };
180
+ const descriptorPath = path.join(root, "descriptor.json");
181
+ await saveNew(descriptorPath, descriptor);
182
+ const startedAt = new Date().toISOString();
183
+ await saveNew(path.join(root, "manifest.json"), { ...descriptor, startedAt, version: LONG_WORKFLOW_VERSION,
184
+ limitations: ["Four composed module journeys reuse the workflow corpus; these are retention/recovery probes, not fresh coding hold-outs",
185
+ "A scripted pause after verified work is a clean checkpoint, not a SIGKILL or an uncertain external side effect",
186
+ "Multi-hour qualification is reported separately from correctness; idle time is never added to meet duration",
187
+ "No mixed-media, physical GPU admission, production coexistence or statistical comparison claim"] });
188
+ const failures = [];
189
+ const stages = [];
190
+ for (const stage of [1, 2, 3, 4]) {
191
+ options.signal?.throwIfAborted();
192
+ options.onProgress?.({ type: "journey_stage_start", scenarioId: scenario.id, stage, at: new Date().toISOString() });
193
+ try {
194
+ await runEvaluationProcess({ file: fileURLToPath(import.meta.url), args: ["--stage", descriptorPath, String(stage)], cwd: root,
195
+ config: resolved.config, timeoutMs: 4 * 3600000, signal: options.signal });
196
+ }
197
+ catch (error) {
198
+ failures.push(`Stage ${stage}: ${String(error).slice(0, 1000)}`);
199
+ }
200
+ try {
201
+ stages.push(JSON.parse(await fs.readFile(path.join(root, `stage-${stage}.result.json`), "utf8")));
202
+ }
203
+ catch {
204
+ failures.push(`Stage ${stage} did not produce a durable result`);
205
+ }
206
+ options.onProgress?.({ type: "journey_stage_end", scenarioId: scenario.id, stage, failures: [...failures], at: new Date().toISOString() });
207
+ if (failures.length)
208
+ break;
209
+ }
210
+ const grades = [];
211
+ for (const task of tasks)
212
+ grades.push({ taskId: task.id, grade: await gradeTerminalTask(task, path.join(workspaceRoot, "modules", task.id)) });
213
+ for (const result of grades)
214
+ if (!result.grade.passed)
215
+ failures.push(`Module ${result.taskId}: ${result.grade.failures.join('; ')}`);
216
+ const final = stages.at(-1)?.state;
217
+ if (final)
218
+ failures.push(...gradeLongWorkflowState(descriptor.objective, final, stages[0]?.state));
219
+ else
220
+ failures.push("Final work state missing");
221
+ const after = await captureWorkspace(workspaceRoot);
222
+ const changed = changedWorkspacePaths(before, after);
223
+ const allowed = new Set(["USER_NOTES.txt", "DELIVERY.md", ...tasks.flatMap(task => task.allowedChanges.map(file => `modules/${task.id}/${file}`))]);
224
+ if (changed.some(file => !allowed.has(file)))
225
+ failures.push("Changes escaped the journey scope");
226
+ const expectedNotes = notes + "Concurrent user correction: preserve these notes and every original module contract.\n";
227
+ if (await fs.readFile(path.join(workspaceRoot, "USER_NOTES.txt"), "utf8").catch(() => "") !== expectedNotes)
228
+ failures.push("Concurrent user notes were lost or changed");
229
+ const delivery = await fs.readFile(path.join(workspaceRoot, "DELIVERY.md"), "utf8").catch(() => "");
230
+ if (!scenario.modules.every(id => delivery.includes(`modules/${id}/test/task.test.mjs`)))
231
+ failures.push("Additional delivery inventory is incomplete");
232
+ if (new Set(stages.map(item => item.pid)).size !== 4 || stages.length !== 4 || stages.some(item => !item.resumePreserved))
233
+ failures.push("Four distinct processes did not retain canonical state");
234
+ const endedAt = new Date().toISOString();
235
+ const elapsedMs = Date.parse(endedAt) - Date.parse(startedAt);
236
+ const result = { schemaVersion: 1, scenarioId: scenario.id, startedAt, endedAt, elapsedMs, passed: failures.length === 0,
237
+ multiHourDurationReached: elapsedMs >= 2 * 3600000, failures, grades, changedPaths: changed, stages, configHash: resolved.hash, clientBuildHash };
238
+ await saveNew(path.join(root, "result.json"), result);
239
+ options.onProgress?.({ type: "journey_result", scenarioId: scenario.id, passed: result.passed, multiHourDurationReached: result.multiHourDurationReached, failures, endedAt });
240
+ }
241
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) && process.argv[2] === "--stage") {
242
+ runLongWorkflowStage(process.argv[3], Number(process.argv[4])).catch(error => { process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`); process.exitCode = 1; });
243
+ }
@@ -0,0 +1,45 @@
1
+ import type { WorkState } from "../session/WorkTypes.js";
2
+ export declare const PLANNING_CORPUS_VERSION = "codali-planning-v3";
3
+ export interface PlanningCriterion {
4
+ id: string;
5
+ description: string;
6
+ paths: string[];
7
+ after: string[];
8
+ phase: "design" | "build" | "validate";
9
+ }
10
+ export interface PlanningCase {
11
+ id: string;
12
+ objective: string;
13
+ criteria: PlanningCriterion[];
14
+ constraints: string[];
15
+ nonGoals: string[];
16
+ decision: {
17
+ id: string;
18
+ question: string;
19
+ options: Record<string, string>;
20
+ accepted: string;
21
+ rationale: string;
22
+ };
23
+ }
24
+ /** Closed-world planning probes, not a general architecture benchmark. Expected
25
+ * dependency edges and accepted choices stay outside the model's workspace. */
26
+ export declare function planningCorpus(): PlanningCase[];
27
+ export declare function planningPrompt(item: PlanningCase): string;
28
+ export interface PlanningGrade {
29
+ passed: boolean;
30
+ failures: string[];
31
+ metrics: {
32
+ lostCriteria: number;
33
+ inventedCriteria: number;
34
+ orderingErrors: number;
35
+ scopeErrors: number;
36
+ architectureErrors: number;
37
+ prematureCompletion: boolean;
38
+ };
39
+ }
40
+ export declare function gradePlanningState(item: PlanningCase, state: WorkState): PlanningGrade;
41
+ /** Oracle construction is used only to mutation-test the grader, never supplied
42
+ * to a live planner. Rationale grading is structural, not semantic certification. */
43
+ export declare function referencePlanningState(item: PlanningCase): WorkState;
44
+ export declare const planningCorpusHash: () => string;
45
+ //# sourceMappingURL=PlanningCorpus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlanningCorpus.d.ts","sourceRoot":"","sources":["../../src/eval/PlanningCorpus.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAC5D,MAAM,WAAW,iBAAiB;IAAG,EAAE,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,GAAG,UAAU,CAAC;CAAE;AACjJ,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAClH;AAOD;+EAC+E;AAC/E,wBAAgB,cAAc,IAAI,YAAY,EAAE,CA0D/C;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAEzD;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,kBAAkB,EAAE,MAAM,CAAC;QAAC,mBAAmB,EAAE,OAAO,CAAA;KAAE,CAAC;CACpK;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,GAAG,aAAa,CAuDtF;AAED;qFACqF;AACrF,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,YAAY,GAAG,SAAS,CAapE;AAED,eAAO,MAAM,kBAAkB,QAAO,MAAkI,CAAC"}
@@ -0,0 +1,189 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createWorkState, replaceWorkPlan, validateWorkState } from "../session/WorkState.js";
3
+ export const PLANNING_CORPUS_VERSION = "codali-planning-v3";
4
+ const criterion = (id, description, paths, after = [], phase = id === "R2" ? "design" : ["R3", "R4", "R5"].includes(id) ? "build" : "validate") => ({
5
+ id, description: `${description}${after.length ? ` Prerequisites: ${after.join(", ")} must be complete before this work starts.` : ""}`, paths, after, phase,
6
+ });
7
+ const common = { constraints: ["Preserve existing user edits and public compatibility", "Use existing components; add no runtime dependency"], nonGoals: ["Unrelated cleanup or UI redesign"] };
8
+ /** Closed-world planning probes, not a general architecture benchmark. Expected
9
+ * dependency edges and accepted choices stay outside the model's workspace. */
10
+ export function planningCorpus() {
11
+ const cases = [
12
+ { ...common, id: "additive-migration", objective: "Plan a rolling migration from one display-name column to first/last-name fields without breaking old clients or requiring downtime.",
13
+ criteria: [
14
+ criterion("R2", "Document old/new client contracts and an additive migration decision before implementation.", ["docs/contracts.md"]),
15
+ criterion("R3", "Add nullable fields while retaining the existing column; old clients keep working.", ["db/migration.sql"], ["R2"]),
16
+ criterion("R4", "Implement compatible dual read/write behavior only after the expanded schema exists.", ["src/profile.ts"], ["R3"]),
17
+ criterion("R5", "Backfill idempotently after dual writes protect new traffic.", ["src/backfill.ts"], ["R4"]),
18
+ criterion("R6", "Prove old/new clients and repeated backfill coexist before any cutover.", ["test/migration.test.ts"], ["R5"]),
19
+ criterion("R7", "Document reversible cutover after compatibility verification; retaining the old column is mandatory.", ["docs/rollout.md"], ["R6"]),
20
+ ], decision: { id: "D1", question: "Choose the storage transition compatible with rolling old clients.", options: { replace: "Drop the old column and ship the new client together", expand: "Expand storage, use compatible readers/writers, backfill, then switch reads" }, accepted: "expand", rationale: "Old clients remain active throughout the migration." } },
21
+ { ...common, id: "artifact-recovery", objective: "Plan resumable tenant-scoped media downloads, including disconnected clients and expiring URLs, without duplicate generation.",
22
+ criteria: [
23
+ criterion("R2", "Define tenant ownership and stable remote job identity before implementing downloads.", ["docs/jobs.md"]),
24
+ criterion("R3", "Persist admitted job identity before polling; ambiguous submission must be reconciled before any resubmit.", ["src/jobs.ts"], ["R2"]),
25
+ criterion("R4", "Refresh expired URLs using the same owned job and artifact identity.", ["src/refresh.ts"], ["R3"]),
26
+ criterion("R5", "Download bounded bytes to private temporary storage; verify checksum before atomic final rename.", ["src/download.ts"], ["R4"]),
27
+ criterion("R6", "Test interrupted download, expiry and cross-tenant denial after the recovery path exists.", ["test/recovery.test.ts"], ["R5"]),
28
+ criterion("R7", "Write a resume runbook using the original job; never promise a render was cancelled without a terminal receipt.", ["docs/resume.md"], ["R6"]),
29
+ ], decision: { id: "D1", question: "Choose how a resumed client obtains an artifact whose URL expired.", options: { rerender: "Submit a replacement render", refresh: "Resolve ownership and refresh the artifact reference on the original job" }, accepted: "refresh", rationale: "URL expiry does not mean generation failed." } },
30
+ { ...common, id: "embedding-switch", objective: "Plan switching a repository index to a different embedding dimension while keeping search available and rollback possible.",
31
+ criteria: [
32
+ criterion("R2", "Record embedding model revision, dimension and normalization as one space identity.", ["docs/embedding.md"]),
33
+ criterion("R3", "Create a separate index namespace for the new incompatible space; preserve the active index.", ["src/index.ts"], ["R2"]),
34
+ criterion("R4", "Backfill the new namespace incrementally and record source freshness.", ["src/backfill.ts"], ["R3"]),
35
+ criterion("R5", "Verify coverage, dimensional compatibility and query behavior before activating the new index.", ["test/index.test.ts"], ["R4"], "validate"),
36
+ criterion("R6", "Atomically switch the index pointer only after verification.", ["src/switch.ts"], ["R5"]),
37
+ criterion("R7", "Test reverting the pointer to the preserved compatible old model/index pair.", ["test/rollback.test.ts"], ["R6"]),
38
+ ], decision: { id: "D1", question: "Choose how to store embeddings from the new dimension.", options: { mixed: "Write new vectors into the active index and pad old vectors", separate: "Build a separately identified index and atomically switch after validation" }, accepted: "separate", rationale: "Different embedding spaces cannot be mixed by padding dimensions." } },
39
+ { ...common, id: "lease-recovery", objective: "Plan recovery of distributed jobs after worker crashes without accepting stale results or duplicating external side effects.",
40
+ criteria: [
41
+ criterion("R2", "Specify attempt identity, fencing and external-operation reconciliation contracts.", ["docs/leases.md"]),
42
+ criterion("R3", "Persist each new attempt and its monotonic fence before dispatch.", ["src/claims.ts"], ["R2"]),
43
+ criterion("R4", "Reject stale completion using the current attempt/fence, including after reassignment.", ["src/results.ts"], ["R3"]),
44
+ criterion("R5", "Reconcile uncertain external effects before allowing retry; lease age alone cannot prove absence.", ["src/recovery.ts"], ["R4"]),
45
+ criterion("R6", "Test late results, crashed workers and an operation accepted before connection loss.", ["test/leases.test.ts"], ["R5"]),
46
+ criterion("R7", "Document recovery of blocked uncertain jobs and audit the original attempt.", ["docs/recovery.md"], ["R6"]),
47
+ ], decision: { id: "D1", question: "A lease expired during a non-idempotent remote operation. Choose the recovery policy.", options: { retry: "Retry immediately because the lease is old", reconcile: "Fence old results and reconcile the remote operation before retry" }, accepted: "reconcile", rationale: "Lease expiry does not prove that an external action did not occur." } },
48
+ { ...common, id: "connector-policy", objective: "Plan adding third-party tools without allowing repository instructions or tool metadata to widen user permissions.",
49
+ criteria: [
50
+ criterion("R2", "Record user authorization as the authority and tool output as untrusted data.", ["docs/trust.md"]),
51
+ criterion("R3", "Classify operations centrally; unknown tools require explicit permission.", ["src/policy.ts"], ["R2"]),
52
+ criterion("R4", "Bind grants to concrete operation/scope and recheck current policy immediately before side effects.", ["src/grants.ts"], ["R3"]),
53
+ criterion("R5", "Enforce workspace and network boundaries on connector execution.", ["src/executor.ts"], ["R4"]),
54
+ criterion("R6", "Test forged readOnly metadata, prompt injection and grants invalidated by policy changes.", ["test/policy.test.ts"], ["R5"]),
55
+ criterion("R7", "Document denial and revocation without exposing credentials.", ["docs/permissions.md"], ["R6"]),
56
+ ], decision: { id: "D1", question: "An unknown generation tool declares readOnly=true. Choose authorization behavior.", options: { trust: "Allow it because the connector labels it read-only", classify: "Apply central operation classification and require the corresponding authorization" }, accepted: "classify", rationale: "Connector metadata cannot grant authorization." } },
57
+ { ...common, id: "usage-accounting", objective: "Plan durable token reservations and usage reconciliation across retries, cancellation and reconnects without losing cumulative call counts.",
58
+ criteria: [
59
+ criterion("R2", "Specify cumulative call counters, reserved tokens, actual usage and unknown usage as distinct concepts.", ["docs/accounting.md"]),
60
+ criterion("R3", "Persist a unique reservation before each call; admission failure must leave no partial debit.", ["src/reservations.ts"], ["R2"]),
61
+ criterion("R4", "Settle exactly once against actual tokens without decrementing the cumulative call count.", ["src/settlement.ts"], ["R3"]),
62
+ criterion("R5", "Reconcile pending reservations after reconnect; cancellation without usage leaves an honest unknown state.", ["src/reconcile.ts"], ["R4"]),
63
+ criterion("R6", "Test duplicate settlements, rejected admissions, delayed usage and cancellation.", ["test/accounting.test.ts"], ["R5"]),
64
+ criterion("R7", "Document actual versus estimated/unknown totals and audit retained invocation IDs.", ["docs/usage.md"], ["R6"]),
65
+ ], decision: { id: "D1", question: "A completed call has settled its tokens. Choose how its call count changes.", options: { refund: "Decrement cumulative calls to release the call reservation", retain: "Keep the cumulative call count; settle only the token reservation" }, accepted: "retain", rationale: "A completed invocation remains a consumed call." } },
66
+ ];
67
+ return structuredClone(cases);
68
+ }
69
+ export function planningPrompt(item) {
70
+ return `${item.objective}\n\nPlanning only: do not implement, run shell commands, verify implementation or claim completion. Build the durable plan using work_refine/work_plan and record D1 through work_decision. Use three phases named design, build and validate, with phase dependencies in that order. Preserve these criterion IDs and descriptions verbatim (R1 is already the whole request):\n${JSON.stringify(item.criteria.map(({ id, description }) => ({ id, description })))}\n\nRecord these constraints and non-goals verbatim:\n${JSON.stringify({ constraints: item.constraints, nonGoals: item.nonGoals })}\n\nUse one narrowly scoped task for each R2-R7 criterion, with exactly its listed future write paths. Add one final R1 integration task with no write paths, dependent on every criterion task. Use each criterion's listed phase. Derive dependency ordering from the explicit prerequisites in its description; task dependencies supplement phase dependencies. Requirements in the same phase still need their specified ordering.\n${JSON.stringify(Object.fromEntries(item.criteria.map(c => [c.id, { paths: c.paths, phase: c.phase }])))}\n\nDecision ${item.decision.id}: ${item.decision.question}\nOptions: ${JSON.stringify(item.decision.options)}\nSet the decision title to the chosen option ID, give a concrete rationale and list the other option IDs as alternatives. Do not invent additional requirements, APIs, files or work. End with a brief planning summary, leaving the implementation goal open.`;
71
+ }
72
+ export function gradePlanningState(item, state) {
73
+ const failures = [];
74
+ const metrics = { lostCriteria: 0, inventedCriteria: 0, orderingErrors: 0, scopeErrors: 0, architectureErrors: 0, prematureCompletion: false };
75
+ const fail = (message) => { failures.push(message); };
76
+ try {
77
+ validateWorkState(state);
78
+ }
79
+ catch (error) {
80
+ fail(`Invalid durable plan: ${String(error)}`);
81
+ }
82
+ if (state.originalObjective !== planningPrompt(item) || state.objective !== state.originalObjective)
83
+ fail("Original planning objective changed");
84
+ const expectedIds = new Set(["R1", ...item.criteria.map(c => c.id)]);
85
+ if (!state.requirements.some(r => r.id === "R1" && r.required && r.description === planningPrompt(item)))
86
+ fail("Whole-request criterion R1 changed");
87
+ metrics.inventedCriteria = state.requirements.filter(r => !expectedIds.has(r.id)).length;
88
+ if (metrics.inventedCriteria)
89
+ fail("Invented criteria");
90
+ for (const criterion of item.criteria) {
91
+ const actual = state.requirements.find(r => r.id === criterion.id);
92
+ if (!actual?.required || actual.description !== criterion.description) {
93
+ metrics.lostCriteria++;
94
+ fail(`Lost or rewritten criterion ${criterion.id}`);
95
+ }
96
+ }
97
+ for (const key of ["constraints", "nonGoals"])
98
+ if (JSON.stringify([...state[key]].sort()) !== JSON.stringify([...item[key]].sort()))
99
+ fail(`${key} changed`);
100
+ const active = state.tasks.filter(t => !["cancelled", "superseded"].includes(t.status));
101
+ metrics.prematureCompletion = state.status === "completed" || state.evidence.length > 0 || active.some(t => ["running", "review_pending", "verified"].includes(t.status));
102
+ if (metrics.prematureCompletion)
103
+ fail("Planning falsely claimed execution or verification");
104
+ if (active.length !== item.criteria.length + 1)
105
+ fail("Expected one task per criterion and one final integration task");
106
+ const tasks = new Map(active.map(t => [t.id, t]));
107
+ const reaches = (from, target, seen = new Set()) => {
108
+ if (seen.has(from))
109
+ return false;
110
+ seen.add(from);
111
+ const task = tasks.get(from);
112
+ if (!task)
113
+ return false;
114
+ const phase = state.phases?.find(p => p.id === task.phaseId);
115
+ const dependencies = [...task.dependsOn, ...active.filter(t => t.phaseId && phase?.dependsOn.includes(t.phaseId)).map(t => t.id)];
116
+ return dependencies.includes(target) || dependencies.some(id => reaches(id, target, seen));
117
+ };
118
+ const owner = (id) => active.filter(t => t.requirementIds.includes(id));
119
+ for (const c of item.criteria) {
120
+ const assigned = owner(c.id);
121
+ if (assigned.length !== 1 || assigned[0].requirementIds.length !== 1) {
122
+ fail(`Ambiguous criterion ownership: ${c.id}`);
123
+ continue;
124
+ }
125
+ const task = assigned[0];
126
+ if (JSON.stringify([...task.allowedPaths].sort()) !== JSON.stringify([...c.paths].sort())) {
127
+ metrics.scopeErrors++;
128
+ fail(`Scope mismatch: ${c.id}`);
129
+ }
130
+ if (task.phaseId !== c.phase) {
131
+ metrics.orderingErrors++;
132
+ fail(`Wrong phase: ${c.id}`);
133
+ }
134
+ for (const predecessor of c.after)
135
+ if (owner(predecessor).length !== 1 || !reaches(task.id, owner(predecessor)[0].id)) {
136
+ metrics.orderingErrors++;
137
+ fail(`Missing prerequisite ${predecessor} before ${c.id}`);
138
+ }
139
+ }
140
+ const final = owner("R1");
141
+ if (final.length !== 1 || final[0].requirementIds.length !== 1)
142
+ fail("Final integration must uniquely own R1");
143
+ else {
144
+ if (final[0].allowedPaths.length) {
145
+ metrics.scopeErrors++;
146
+ fail("Final integration must have no write paths");
147
+ }
148
+ if (active.some(t => t !== final[0] && !reaches(final[0].id, t.id))) {
149
+ metrics.orderingErrors++;
150
+ fail("Final integration must follow all criterion tasks");
151
+ }
152
+ // The prompt fixes criterion phases, but permits the final integration task
153
+ // to be unphased. Its explicit dependencies still fence all future work.
154
+ }
155
+ for (const [id, prior] of [["design", undefined], ["build", "design"], ["validate", "build"]]) {
156
+ const phase = state.phases?.find(p => p.id === id);
157
+ if (!phase || prior && !phase.dependsOn.includes(prior)) {
158
+ metrics.orderingErrors++;
159
+ fail(`Missing ordered phase ${id}`);
160
+ }
161
+ }
162
+ if (state.phases?.length !== 3)
163
+ fail("Unexpected phases");
164
+ const decision = state.decisions.find(d => d.id === item.decision.id && !state.decisions.some(next => next.supersedes === d.id));
165
+ const rejected = Object.keys(item.decision.options).filter(id => id !== item.decision.accepted);
166
+ if (!decision || decision.title !== item.decision.accepted || decision.rationale.trim().length < 20 || !rejected.every(id => decision.alternatives.includes(id))) {
167
+ metrics.architectureErrors++;
168
+ fail("Architecture choice, rationale or alternatives violate the supplied constraints");
169
+ }
170
+ return { passed: failures.length === 0, failures, metrics };
171
+ }
172
+ /** Oracle construction is used only to mutation-test the grader, never supplied
173
+ * to a live planner. Rationale grading is structural, not semantic certification. */
174
+ export function referencePlanningState(item) {
175
+ const state = createWorkState(planningPrompt(item));
176
+ state.requirements.push(...item.criteria.map(c => ({ id: c.id, description: c.description, required: true })));
177
+ state.constraints = [...item.constraints];
178
+ state.nonGoals = [...item.nonGoals];
179
+ const tasks = item.criteria.map(c => ({ id: `task-${c.id}`, title: c.description, requirementIds: [c.id], dependsOn: c.after.map(id => `task-${id}`), allowedPaths: [...c.paths], phaseId: c.phase }));
180
+ tasks.push({ id: "integrate", title: "Verify complete delivery", requirementIds: ["R1"], dependsOn: tasks.map(t => t.id), allowedPaths: [], phaseId: "validate" });
181
+ replaceWorkPlan(state, tasks, [
182
+ { id: "design", title: "Architecture", dependsOn: [], exitRequirementIds: ["R2"] },
183
+ { id: "build", title: "Implementation", dependsOn: ["design"], exitRequirementIds: item.criteria.filter(c => c.phase === "build").map(c => c.id) },
184
+ { id: "validate", title: "Validation", dependsOn: ["build"], exitRequirementIds: [...item.criteria.filter(c => c.phase === "validate").map(c => c.id), "R1"] },
185
+ ]);
186
+ state.decisions.push({ id: item.decision.id, title: item.decision.accepted, rationale: item.decision.rationale, alternatives: Object.keys(item.decision.options).filter(id => id !== item.decision.accepted), paths: [] });
187
+ return state;
188
+ }
189
+ export const planningCorpusHash = () => createHash("sha256").update(JSON.stringify({ version: PLANNING_CORPUS_VERSION, cases: planningCorpus() })).digest("hex");
@@ -0,0 +1,13 @@
1
+ import { type EffortLevel } from "../config/OrchestrationConfig.js";
2
+ export interface PlanningRunOptions {
3
+ configPath: string;
4
+ outputRoot: string;
5
+ caseIds?: string[];
6
+ effort?: EffortLevel;
7
+ signal?: AbortSignal;
8
+ onProgress?: (event: Record<string, unknown>) => void;
9
+ }
10
+ /** Uses the real controller, permission gate and configured route. No evaluator
11
+ * model can turn a failing deterministic grade into a passing one. */
12
+ export declare function runPlanningCorpus(options: PlanningRunOptions): Promise<void>;
13
+ //# sourceMappingURL=PlanningCorpusRunner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PlanningCorpusRunner.d.ts","sourceRoot":"","sources":["../../src/eval/PlanningCorpusRunner.ts"],"names":[],"mappings":"AAIA,OAAO,EAA2B,KAAK,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAI7F,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACvD;AAED;sEACsE;AACtE,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0DlF"}
@@ -0,0 +1,89 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { SessionController } from "../runtime/SessionController.js";
4
+ import { captureWorkspace, changedWorkspacePaths } from "../runtime/WorkspaceSnapshot.js";
5
+ import { loadOrchestrationConfig } from "../config/OrchestrationConfig.js";
6
+ import { terminalClientBuildHash } from "./TerminalCorpusRunner.js";
7
+ import { gradePlanningState, planningCorpus, planningCorpusHash, planningPrompt, PLANNING_CORPUS_VERSION } from "./PlanningCorpus.js";
8
+ /** Uses the real controller, permission gate and configured route. No evaluator
9
+ * model can turn a failing deterministic grade into a passing one. */
10
+ export async function runPlanningCorpus(options) {
11
+ const resolved = await loadOrchestrationConfig(options.configPath);
12
+ const clientBuildHash = await terminalClientBuildHash();
13
+ const corpus = planningCorpus();
14
+ if (options.caseIds?.some(id => !corpus.some(item => item.id === id)))
15
+ throw new Error("Unknown planning case");
16
+ const selected = options.caseIds ? corpus.filter(item => options.caseIds.includes(item.id)) : corpus;
17
+ const root = path.resolve(options.outputRoot);
18
+ await fs.mkdir(root, { mode: 0o700 });
19
+ const save = (file, value) => fs.writeFile(path.join(root, file), JSON.stringify(value, null, 2) + "\n", { flag: "wx", mode: 0o600 });
20
+ await save("manifest.json", { schemaVersion: 1, corpusVersion: PLANNING_CORPUS_VERSION, corpusHash: planningCorpusHash(), clientBuildHash,
21
+ configHash: resolved.hash, effort: options.effort ?? "high", caseIds: selected.map(item => item.id), startedAt: new Date().toISOString(),
22
+ limitations: ["Six closed-world planning probes with explicit criterion IDs and constrained architecture options; not general architecture certification",
23
+ "Checks actual durable controller records, not quality of final prose", "Rationale grading checks presence, not semantic correctness",
24
+ "Docdex is disabled for these code-free probes; retrieval/media/implementation quality require separate qualification",
25
+ "Compaction/resume is checked in a new controller instance, not a new OS process", "No helper/review benefit or production performance claim"] });
26
+ for (const item of selected) {
27
+ options.signal?.throwIfAborted();
28
+ if ((await loadOrchestrationConfig(options.configPath)).hash !== resolved.hash || await terminalClientBuildHash() !== clientBuildHash)
29
+ throw new Error("Evaluation config/build changed; start a separately identified run");
30
+ const directory = path.join(root, item.id);
31
+ const workspaceRoot = path.join(directory, "workspace");
32
+ const storageRoot = path.join(directory, "state");
33
+ await fs.mkdir(directory, { mode: 0o700 });
34
+ await fs.mkdir(workspaceRoot, { mode: 0o700 });
35
+ await fs.writeFile(path.join(workspaceRoot, "USER_NOTES.md"), "Uncommitted user notes. Planning must preserve this file.\n", { mode: 0o600, flag: "wx" });
36
+ const before = await captureWorkspace(workspaceRoot);
37
+ const events = [];
38
+ const controller = new SessionController({ workspaceRoot, storageRoot, resolved, docdex: false, permissions: "plan", effort: options.effort ?? "high",
39
+ onEvent: event => { if (["model_route", "tool_receipt", "turn_result", "turn_failed"].includes(event.type))
40
+ events.push(event); } });
41
+ const startedAt = new Date().toISOString();
42
+ options.onProgress?.({ type: "planning_start", caseId: item.id, startedAt });
43
+ let failure;
44
+ const abort = () => controller.cancel();
45
+ options.signal?.addEventListener("abort", abort, { once: true });
46
+ try {
47
+ await controller.start(planningPrompt(item));
48
+ options.signal?.throwIfAborted();
49
+ await controller.run();
50
+ await controller.compact();
51
+ }
52
+ catch (error) {
53
+ failure = error instanceof Error ? error.message : String(error);
54
+ }
55
+ finally {
56
+ options.signal?.removeEventListener("abort", abort);
57
+ }
58
+ if (!controller.state || !controller.sessionId)
59
+ throw new Error(failure ?? "Planner failed before creating state");
60
+ const grade = gradePlanningState(item, controller.state);
61
+ const after = await captureWorkspace(workspaceRoot);
62
+ const changedPaths = changedWorkspacePaths(before, after);
63
+ if (changedPaths.length)
64
+ grade.failures.push("Planning changed workspace files");
65
+ const sideEffects = events.filter(event => event.type === "tool_receipt").map(event => event.data.receipt)
66
+ .filter(receipt => ["write_file", "run_shell", "work_verify", "work_complete"].includes(receipt.tool));
67
+ if (sideEffects.length)
68
+ grade.failures.push("Planner attempted implementation or completion during planning-only work");
69
+ const resumed = new SessionController({ workspaceRoot, storageRoot, resolved, docdex: false, permissions: "plan" });
70
+ let resumePreserved = false;
71
+ try {
72
+ await resumed.resume(controller.sessionId);
73
+ resumePreserved = JSON.stringify(resumed.state) === JSON.stringify(controller.state);
74
+ }
75
+ catch (error) {
76
+ grade.failures.push(`Resume failed: ${String(error)}`);
77
+ }
78
+ if (!resumePreserved)
79
+ grade.failures.push("Durable plan changed or was lost across compaction/resume");
80
+ grade.passed = grade.failures.length === 0 && !failure;
81
+ const record = { caseId: item.id, startedAt, endedAt: new Date().toISOString(), passed: grade.passed, failure, grade, changedPaths, resumePreserved,
82
+ configHash: resolved.hash, clientBuildHash, sessionId: controller.sessionId, usage: controller.usageSnapshot(),
83
+ routes: events.filter(event => event.type === "model_route").map(event => event.data), sideEffects, state: controller.state };
84
+ await save(`${item.id}.result.json`, record);
85
+ await fs.appendFile(path.join(root, "results.jsonl"), JSON.stringify(record) + "\n", { mode: 0o600 });
86
+ options.onProgress?.({ type: "planning_result", caseId: item.id, passed: record.passed, failures: grade.failures, failure, endedAt: record.endedAt });
87
+ }
88
+ await save("completed.json", { endedAt: new Date().toISOString(), cases: selected.length });
89
+ }