@interf/compiler 0.9.5 → 0.13.0

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 (214) hide show
  1. package/README.md +96 -92
  2. package/TRADEMARKS.md +2 -13
  3. package/agent-skills/interf-actions/SKILL.md +95 -36
  4. package/agent-skills/interf-actions/references/cli.md +118 -51
  5. package/builtin-methods/interf-default/README.md +3 -4
  6. package/builtin-methods/interf-default/compile/stages/shape/SKILL.md +2 -2
  7. package/builtin-methods/interf-default/compile/stages/summarize/SKILL.md +2 -1
  8. package/builtin-methods/interf-default/improve/SKILL.md +1 -1
  9. package/builtin-methods/interf-default/method.json +10 -4
  10. package/builtin-methods/interf-default/method.schema.json +0 -9
  11. package/builtin-methods/interf-default/use/query/SKILL.md +5 -5
  12. package/dist/cli/commands/compile.d.ts +8 -25
  13. package/dist/cli/commands/compile.js +75 -360
  14. package/dist/cli/commands/doctor.js +1 -1
  15. package/dist/cli/commands/login.d.ts +7 -0
  16. package/dist/cli/commands/login.js +39 -0
  17. package/dist/cli/commands/logout.d.ts +2 -0
  18. package/dist/cli/commands/logout.js +16 -0
  19. package/dist/cli/commands/method.d.ts +2 -0
  20. package/dist/cli/commands/method.js +113 -0
  21. package/dist/cli/commands/prep.d.ts +2 -0
  22. package/dist/cli/commands/prep.js +134 -0
  23. package/dist/cli/commands/reset.d.ts +8 -1
  24. package/dist/cli/commands/reset.js +47 -26
  25. package/dist/cli/commands/runs.d.ts +2 -0
  26. package/dist/cli/commands/runs.js +120 -0
  27. package/dist/cli/commands/status.d.ts +6 -1
  28. package/dist/cli/commands/status.js +68 -111
  29. package/dist/cli/commands/test.d.ts +6 -14
  30. package/dist/cli/commands/test.js +65 -181
  31. package/dist/cli/commands/web.d.ts +0 -9
  32. package/dist/cli/commands/web.js +147 -120
  33. package/dist/cli/commands/wizard.d.ts +9 -0
  34. package/dist/cli/commands/wizard.js +442 -0
  35. package/dist/cli/index.d.ts +7 -6
  36. package/dist/cli/index.js +13 -10
  37. package/dist/compiler-ui/404.html +1 -1
  38. package/dist/compiler-ui/__next.__PAGE__.txt +2 -2
  39. package/dist/compiler-ui/__next._full.txt +3 -3
  40. package/dist/compiler-ui/__next._head.txt +1 -1
  41. package/dist/compiler-ui/__next._index.txt +2 -2
  42. package/dist/compiler-ui/__next._tree.txt +2 -2
  43. package/dist/compiler-ui/_next/static/chunks/{18a8f2jkv3z.c.css → 045gole2ojo3g.css} +1 -1
  44. package/dist/compiler-ui/_next/static/chunks/{177mvn4rse235.js → 17t-lulmyawg5.js} +9 -9
  45. package/dist/compiler-ui/_not-found/__next._full.txt +2 -2
  46. package/dist/compiler-ui/_not-found/__next._head.txt +1 -1
  47. package/dist/compiler-ui/_not-found/__next._index.txt +2 -2
  48. package/dist/compiler-ui/_not-found/__next._not-found.__PAGE__.txt +1 -1
  49. package/dist/compiler-ui/_not-found/__next._not-found.txt +1 -1
  50. package/dist/compiler-ui/_not-found/__next._tree.txt +2 -2
  51. package/dist/compiler-ui/_not-found.html +1 -1
  52. package/dist/compiler-ui/_not-found.txt +2 -2
  53. package/dist/compiler-ui/index.html +1 -1
  54. package/dist/compiler-ui/index.txt +3 -3
  55. package/dist/packages/agents/lib/shells.d.ts +1 -1
  56. package/dist/packages/agents/lib/shells.js +111 -52
  57. package/dist/packages/agents/lib/user-config.d.ts +4 -2
  58. package/dist/packages/agents/lib/user-config.js +15 -7
  59. package/dist/packages/compiler/compiled-paths.d.ts +9 -2
  60. package/dist/packages/compiler/compiled-paths.js +30 -15
  61. package/dist/packages/compiler/compiled-pipeline.js +23 -3
  62. package/dist/packages/compiler/compiled-stage-plan.js +4 -0
  63. package/dist/packages/compiler/compiled-target.d.ts +1 -1
  64. package/dist/packages/compiler/compiled-target.js +1 -1
  65. package/dist/packages/compiler/index.d.ts +1 -0
  66. package/dist/packages/compiler/index.js +1 -0
  67. package/dist/packages/compiler/lib/schema.d.ts +26 -31
  68. package/dist/packages/compiler/lib/schema.js +1 -12
  69. package/dist/packages/compiler/method-runs.d.ts +2 -3
  70. package/dist/packages/compiler/method-runs.js +2 -3
  71. package/dist/packages/compiler/reset.js +3 -1
  72. package/dist/packages/compiler/runtime-contracts.js +0 -3
  73. package/dist/packages/compiler/runtime-prompt.js +1 -1
  74. package/dist/packages/compiler/source-files.d.ts +46 -0
  75. package/dist/packages/compiler/source-files.js +149 -0
  76. package/dist/packages/compiler/state-artifacts.d.ts +3 -2
  77. package/dist/packages/compiler/state-artifacts.js +4 -3
  78. package/dist/packages/compiler/state-io.d.ts +3 -2
  79. package/dist/packages/compiler/state-io.js +11 -5
  80. package/dist/packages/compiler/state-paths.d.ts +2 -1
  81. package/dist/packages/compiler/state-paths.js +6 -3
  82. package/dist/packages/compiler/state-view.d.ts +3 -2
  83. package/dist/packages/compiler/state-view.js +18 -28
  84. package/dist/packages/compiler/state.d.ts +4 -4
  85. package/dist/packages/compiler/state.js +3 -3
  86. package/dist/packages/contracts/index.d.ts +1 -1
  87. package/dist/packages/contracts/lib/preparation-paths.d.ts +117 -0
  88. package/dist/packages/contracts/lib/preparation-paths.js +177 -0
  89. package/dist/packages/contracts/lib/schema.d.ts +85 -5
  90. package/dist/packages/contracts/lib/schema.js +46 -1
  91. package/dist/packages/execution/lib/schema.d.ts +50 -50
  92. package/dist/packages/execution/lib/schema.js +1 -1
  93. package/dist/packages/local-service/action-definitions.d.ts +14 -14
  94. package/dist/packages/local-service/action-definitions.js +27 -28
  95. package/dist/packages/local-service/action-planner.js +2 -1
  96. package/dist/packages/local-service/client.d.ts +51 -52
  97. package/dist/packages/local-service/client.js +132 -140
  98. package/dist/packages/local-service/connection-config.d.ts +38 -0
  99. package/dist/packages/local-service/connection-config.js +75 -0
  100. package/dist/packages/local-service/index.d.ts +11 -7
  101. package/dist/packages/local-service/index.js +6 -4
  102. package/dist/packages/local-service/instance-paths.d.ts +100 -0
  103. package/dist/packages/local-service/instance-paths.js +165 -0
  104. package/dist/packages/local-service/lib/schema.d.ts +405 -2297
  105. package/dist/packages/local-service/lib/schema.js +146 -62
  106. package/dist/packages/local-service/native-run-handlers.js +3 -3
  107. package/dist/packages/local-service/preparation-store.d.ts +92 -0
  108. package/dist/packages/local-service/preparation-store.js +171 -0
  109. package/dist/packages/local-service/routes.d.ts +33 -16
  110. package/dist/packages/local-service/routes.js +44 -20
  111. package/dist/packages/local-service/run-observability.js +11 -11
  112. package/dist/packages/local-service/runtime-caches.d.ts +76 -0
  113. package/dist/packages/local-service/runtime-caches.js +191 -0
  114. package/dist/packages/local-service/runtime-event-applier.d.ts +12 -0
  115. package/dist/packages/local-service/runtime-event-applier.js +177 -0
  116. package/dist/packages/local-service/runtime-persistence.d.ts +47 -0
  117. package/dist/packages/local-service/runtime-persistence.js +137 -0
  118. package/dist/packages/local-service/runtime-proposal-helpers.d.ts +35 -0
  119. package/dist/packages/local-service/runtime-proposal-helpers.js +251 -0
  120. package/dist/packages/local-service/runtime-resource-builders.d.ts +52 -0
  121. package/dist/packages/local-service/runtime-resource-builders.js +149 -0
  122. package/dist/packages/local-service/runtime.d.ts +197 -43
  123. package/dist/packages/local-service/runtime.js +800 -974
  124. package/dist/packages/local-service/server.d.ts +15 -0
  125. package/dist/packages/local-service/server.js +641 -273
  126. package/dist/packages/local-service/service-registry.d.ts +47 -0
  127. package/dist/packages/local-service/service-registry.js +137 -0
  128. package/dist/packages/method-authoring/method-authoring.d.ts +1 -1
  129. package/dist/packages/method-authoring/method-authoring.js +2 -2
  130. package/dist/packages/method-authoring/method-improvement.js +1 -1
  131. package/dist/packages/method-package/builtin-compiled-method.d.ts +4 -5
  132. package/dist/packages/method-package/builtin-compiled-method.js +8 -14
  133. package/dist/packages/method-package/context-interface.d.ts +4 -40
  134. package/dist/packages/method-package/context-interface.js +1 -23
  135. package/dist/packages/method-package/interf-method-package.d.ts +4 -4
  136. package/dist/packages/method-package/interf-method-package.js +21 -33
  137. package/dist/packages/method-package/local-methods.d.ts +10 -6
  138. package/dist/packages/method-package/local-methods.js +57 -39
  139. package/dist/packages/method-package/method-definitions.d.ts +8 -34
  140. package/dist/packages/method-package/method-definitions.js +49 -37
  141. package/dist/packages/method-package/method-helpers.d.ts +1 -13
  142. package/dist/packages/method-package/method-helpers.js +8 -42
  143. package/dist/packages/method-package/method-stage-runner.js +2 -2
  144. package/dist/packages/method-package/user-methods.d.ts +17 -0
  145. package/dist/packages/method-package/user-methods.js +77 -0
  146. package/dist/packages/project-model/index.d.ts +0 -1
  147. package/dist/packages/project-model/index.js +0 -1
  148. package/dist/packages/project-model/interf-detect.d.ts +8 -3
  149. package/dist/packages/project-model/interf-detect.js +34 -34
  150. package/dist/packages/project-model/interf-scaffold.d.ts +3 -3
  151. package/dist/packages/project-model/interf-scaffold.js +23 -32
  152. package/dist/packages/project-model/lib/schema.js +38 -1
  153. package/dist/packages/project-model/preparation-entries.d.ts +5 -5
  154. package/dist/packages/project-model/preparation-entries.js +14 -14
  155. package/dist/packages/project-model/source-config.d.ts +11 -11
  156. package/dist/packages/project-model/source-config.js +74 -46
  157. package/dist/packages/project-model/source-folders.d.ts +5 -5
  158. package/dist/packages/project-model/source-folders.js +14 -14
  159. package/dist/packages/shared/filesystem.d.ts +7 -0
  160. package/dist/packages/shared/filesystem.js +97 -10
  161. package/dist/packages/testing/lib/schema.d.ts +10 -10
  162. package/dist/packages/testing/lib/schema.js +2 -2
  163. package/dist/packages/testing/readiness-check-run.d.ts +4 -4
  164. package/dist/packages/testing/readiness-check-run.js +36 -36
  165. package/dist/packages/testing/test-execution.js +6 -6
  166. package/dist/packages/testing/test-paths.js +4 -3
  167. package/dist/packages/testing/test-sandbox.d.ts +0 -1
  168. package/dist/packages/testing/test-sandbox.js +14 -30
  169. package/dist/packages/testing/test-targets.d.ts +1 -1
  170. package/dist/packages/testing/test-targets.js +6 -6
  171. package/dist/packages/testing/test.d.ts +1 -1
  172. package/dist/packages/testing/test.js +1 -1
  173. package/package.json +3 -4
  174. package/CHANGELOG.md +0 -93
  175. package/LICENSE +0 -183
  176. package/dist/cli/commands/action-input-cli.d.ts +0 -25
  177. package/dist/cli/commands/action-input-cli.js +0 -73
  178. package/dist/cli/commands/control-path.d.ts +0 -11
  179. package/dist/cli/commands/control-path.js +0 -72
  180. package/dist/cli/commands/create-method-wizard.d.ts +0 -64
  181. package/dist/cli/commands/create-method-wizard.js +0 -434
  182. package/dist/cli/commands/create.d.ts +0 -6
  183. package/dist/cli/commands/create.js +0 -183
  184. package/dist/cli/commands/default.d.ts +0 -2
  185. package/dist/cli/commands/default.js +0 -39
  186. package/dist/cli/commands/executor-flow.d.ts +0 -29
  187. package/dist/cli/commands/executor-flow.js +0 -163
  188. package/dist/cli/commands/init.d.ts +0 -26
  189. package/dist/cli/commands/init.js +0 -771
  190. package/dist/cli/commands/list.d.ts +0 -2
  191. package/dist/cli/commands/list.js +0 -30
  192. package/dist/cli/commands/preparation-action.d.ts +0 -8
  193. package/dist/cli/commands/preparation-action.js +0 -29
  194. package/dist/cli/commands/preparation-picker.d.ts +0 -5
  195. package/dist/cli/commands/preparation-picker.js +0 -36
  196. package/dist/cli/commands/preparation-selection.d.ts +0 -6
  197. package/dist/cli/commands/preparation-selection.js +0 -11
  198. package/dist/cli/commands/service-action-flow.d.ts +0 -9
  199. package/dist/cli/commands/service-action-flow.js +0 -19
  200. package/dist/cli/commands/source-config-wizard.d.ts +0 -51
  201. package/dist/cli/commands/source-config-wizard.js +0 -670
  202. package/dist/cli/commands/verify.d.ts +0 -2
  203. package/dist/cli/commands/verify.js +0 -94
  204. package/dist/packages/compiler/raw-snapshot.d.ts +0 -49
  205. package/dist/packages/compiler/raw-snapshot.js +0 -101
  206. package/dist/packages/method-package/index.d.ts +0 -11
  207. package/dist/packages/method-package/index.js +0 -11
  208. package/dist/packages/method-package/method-stage-policy.d.ts +0 -5
  209. package/dist/packages/method-package/method-stage-policy.js +0 -31
  210. package/dist/packages/project-model/project-paths.d.ts +0 -12
  211. package/dist/packages/project-model/project-paths.js +0 -33
  212. /package/dist/compiler-ui/_next/static/{84FaeF3EzBF9kKTMjSEVN → C6vVfy3aeYuIO3d2AoNvC}/_buildManifest.js +0 -0
  213. /package/dist/compiler-ui/_next/static/{84FaeF3EzBF9kKTMjSEVN → C6vVfy3aeYuIO3d2AoNvC}/_clientMiddlewareManifest.js +0 -0
  214. /package/dist/compiler-ui/_next/static/{84FaeF3EzBF9kKTMjSEVN → C6vVfy3aeYuIO3d2AoNvC}/_ssgManifest.js +0 -0
@@ -1,714 +1,233 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
1
+ import { existsSync, mkdirSync, rmSync, statSync, } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
3
  import { CompileRunSchema, } from "../execution/lib/schema.js";
4
4
  import { createRunEventId, createRunEventTimestamp, } from "../execution/events.js";
5
- import { compiledRuntimeRunHistoryPath, compiledRuntimeRoot, testRootForCompiled, } from "../compiler/compiled-paths.js";
6
5
  import { loadState, } from "../compiler/state.js";
7
- import { RuntimeRunSchema, } from "../compiler/lib/schema.js";
6
+ import { actionProposalPath, actionProposalsRoot, byCreatedAtDesc, compileRunPath, compileRunsRoot, listJsonFiles, localJobPath, localJobsRoot, newestFirst, readActionProposalAt, readCompileRunAt, readLocalJobRunAt, readRuntimeRunHistory, readTestRunAt, testRunPath, testRunsRoot, timestampKey, writeJsonFile, } from "./runtime-persistence.js";
7
+ import { MethodListingCache, MtimeListingCache, ReadinessCache, RunListingCache, } from "./runtime-caches.js";
8
+ import { applyEventToCompileRun, applyEventToLocalJob, } from "./runtime-event-applier.js";
9
+ import { buildMethodResource, buildPreparationResource, createRunId, logsForRuntimeRun, logsForStageRun, proofForStage, readinessStateToPreparationReadiness, readinessSummaryForStatus, readinessTargetResult, stageArtifactRefs, } from "./runtime-resource-builders.js";
10
+ import { ACTION_PLANNER_CLARIFICATION_MESSAGE, actionAssistantMessage, actionCommandPreview, actionTypeFromValues, actionValueMethodTaskPrompt, configuredAgentName, createActionProposalId, detachMethodFromPreparation, detectedExecutorOptions, directServiceEndpointForAction, hasCompiledTestTarget, methodAuthoringHintFromPrompt, methodAuthoringPromptFallback, methodIdForProposal, methodLabelFromId, numberValue, requireSelectedMethod, sanitizeActionProposalPlan, stringValue, testModeFromValues, testModeValue, } from "./runtime-proposal-helpers.js";
8
11
  import { ReadinessStateSchema, } from "../contracts/lib/schema.js";
9
12
  import { discoverSourceFiles, } from "../compiler/discovery.js";
10
13
  import { resetCompiledGeneratedState, } from "../compiler/reset.js";
11
14
  import { ensurePortableContextScaffold, readInterfConfig, } from "../project-model/interf.js";
12
15
  import { findSourcePreparationConfig, fingerprintReadinessChecks, listSourcePreparationConfigs, loadSourceFolderConfig, DEFAULT_METHOD_ID, methodIdForSourcePreparationConfig, resolveConfiguredSourceFolderPath, resolveSourcePreparationPath, removeSourcePreparationConfig, saveSourceFolderConfig, syncCompiledInterfConfigFromSourcePreparationConfig, upsertSourcePreparationConfig, } from "../project-model/source-config.js";
13
16
  import { listSourceFolderChoices, } from "../project-model/source-folders.js";
14
- import { portableContextPath, } from "../project-model/project-paths.js";
17
+ import { asPreparationDataDir, preparationPortableContextPath, userMethodsRoot, preparationConfigPath, preparationMethodPackagePath, preparationMethodsRoot, } from "../contracts/lib/preparation-paths.js";
15
18
  import { getCompiledMethod, listCompiledMethodChoices, } from "../method-package/method-definitions.js";
16
19
  import { contextInterfaceArtifactPath, } from "../method-package/context-interface.js";
17
- import { methodDefinitionPath, resolveMethodPackageSourcePath, } from "../method-package/local-methods.js";
20
+ import { methodDefinitionPath, resolveMethodPackageSourcePath, seedLocalDefaultMethod, } from "../method-package/local-methods.js";
18
21
  import { seedLocalMethodPackageFromBase, } from "../method-package/interf-method-package.js";
22
+ import { PACKAGE_ROOT } from "../method-package/lib/package-root.js";
19
23
  import { resolveAgent, detectAgents, supportsAutomatedRuns, } from "../agents/lib/detection.js";
20
- import { AGENTS, } from "../agents/lib/constants.js";
21
24
  import { loadUserConfig, saveUserConfig, } from "../agents/lib/user-config.js";
22
25
  import { readSavedReadinessCheckRun, } from "../testing/readiness-check-run.js";
23
26
  import { createCompiledTestTarget, } from "../testing/test-targets.js";
24
- import { ActionProposalApprovalRequestSchema, ActionProposalCreateRequestSchema, ActionProposalPlanSchema, ActionProposalResourceSchema, ActionProposalPlanActionTypeSchema, ActionProposalTypeSchema, CompileRunCreateRequestSchema, CompileRunResourceSchema, MethodResourceSchema, LocalExecutorStatusSchema, LocalExecutorSelectRequestSchema, LocalServiceHealthSchema, PreparationReadinessStateSchema, LocalRunHandlerResultSchema, LocalJobEventAppendRequestSchema, LocalJobRunCreateRequestSchema, LocalJobRunResourceSchema, SourceFileResourceSchema, WorkspaceFileResourceSchema, PortableContextResourceSchema, PreparationSetupCreateRequestSchema, PreparationSetupResultSchema, PreparationResourceSchema, MethodChangeCreateRequestSchema, MethodChangeResultSchema, PreparationChangeCreateRequestSchema, PreparationChangeResultSchema, ReadinessCheckDraftCreateRequestSchema, ReadinessCheckDraftResultSchema, ResetRequestSchema, ResetResultSchema, TestRunCreateRequestSchema, TestRunResourceSchema, MethodAuthoringCreateRequestSchema, MethodAuthoringResultSchema, } from "./lib/schema.js";
27
+ import { ActionProposalApprovalRequestSchema, ActionProposalCreateRequestSchema, ActionProposalPlanSchema, ActionProposalResourceSchema, ActionProposalTypeSchema, CompileRunCreateRequestSchema, CompileRunResourceSchema, LocalExecutorStatusSchema, LocalExecutorSelectRequestSchema, LocalServiceHealthSchema, LocalRunHandlerResultSchema, LocalJobEventAppendRequestSchema, LocalJobRunCreateRequestSchema, LocalJobRunResourceSchema, SourceFileResourceSchema, WorkspaceFileResourceSchema, PortableContextResourceSchema, PreparationSetupCreateRequestSchema, PreparationSetupResultSchema, WorkspaceBootstrapCreateRequestSchema, WorkspaceBootstrapResultSchema, MethodChangeCreateRequestSchema, MethodChangeResultSchema, PreparationChangeCreateRequestSchema, PreparationChangeResultSchema, ReadinessCheckDraftCreateRequestSchema, ReadinessCheckDraftResultSchema, ResetRequestSchema, ResetResultSchema, ServiceRegistryWorkspaceSchema, TestRunCreateRequestSchema, TestRunResourceSchema, MethodAuthoringCreateRequestSchema, MethodAuthoringResultSchema, } from "./lib/schema.js";
25
28
  import { buildLocalServiceUrl, } from "./routes.js";
26
- import { methodAuthoringTaskPrompt, MethodAuthoringActionValuesSchema, PreparationSetupActionValuesSchema, } from "./action-values.js";
29
+ import { MethodAuthoringActionValuesSchema, PreparationSetupActionValuesSchema, } from "./action-values.js";
27
30
  import { compileRunToObservability, jobRunToObservability, testRunToObservability, uniqueArtifacts, } from "./run-observability.js";
28
- function createRunId(prefix) {
29
- return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
30
- }
31
- function createActionProposalId() {
32
- return `action_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
33
- }
34
- const ACTION_PLANNER_CLARIFICATION_MESSAGE = "I can help with this Interf Workspace. Ask a question about Interf, or ask me to create a Preparation, prepare, check readiness, improve, or draft a Method and I will prepare an approval proposal.";
35
- function readJsonFile(filePath) {
36
- try {
37
- return JSON.parse(readFileSync(filePath, "utf8"));
38
- }
39
- catch {
40
- return null;
41
- }
42
- }
43
- function writeJsonFile(filePath, value) {
44
- mkdirSync(dirname(filePath), { recursive: true });
45
- writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
46
- }
47
- function sanitizeActionProposalPlan(value) {
48
- if (!value || typeof value !== "object" || Array.isArray(value))
49
- return value;
50
- const plan = { ...value };
51
- for (const key of ["preparation", "method", "title", "summary", "assistant_message", "command_preview"]) {
52
- const current = plan[key];
53
- if (current === null || current === undefined) {
54
- delete plan[key];
55
- continue;
56
- }
57
- if (typeof current === "string" && current.trim().length === 0) {
58
- delete plan[key];
59
- }
60
- }
61
- return plan;
62
- }
63
- function compileRunsRoot(compiledPath) {
64
- return join(compiledRuntimeRoot(compiledPath), "compile-runs");
65
- }
66
- function compileRunPath(compiledPath, runId) {
67
- return join(compileRunsRoot(compiledPath), `${runId}.json`);
68
- }
69
- function testRunsRoot(compiledPath) {
70
- return join(testRootForCompiled(compiledPath), "service-runs");
71
- }
72
- function testRunPath(compiledPath, runId) {
73
- return join(testRunsRoot(compiledPath), `${runId}.json`);
74
- }
75
- function localJobsRoot(rootPath) {
76
- return join(rootPath, "interf", ".service", "jobs");
77
- }
78
- function localJobPath(rootPath, runId) {
79
- return join(localJobsRoot(rootPath), `${runId}.json`);
80
- }
81
- function actionProposalsRoot(rootPath) {
82
- return join(rootPath, "interf", ".service", "action-proposals");
83
- }
84
- function actionProposalPath(rootPath, proposalId) {
85
- return join(actionProposalsRoot(rootPath), `${proposalId}.json`);
86
- }
87
- function listJsonFiles(dirPath) {
88
- if (!existsSync(dirPath))
89
- return [];
90
- try {
91
- if (!statSync(dirPath).isDirectory())
92
- return [];
93
- }
94
- catch {
95
- return [];
96
- }
97
- return readdirSync(dirPath)
98
- .filter((entry) => entry.endsWith(".json"))
99
- .map((entry) => join(dirPath, entry));
100
- }
101
- function readCompileRunAt(filePath) {
102
- const parsed = CompileRunSchema.safeParse(readJsonFile(filePath));
103
- return parsed.success ? parsed.data : null;
104
- }
105
- function readRuntimeRunHistory(compiledPath) {
106
- const historyPath = compiledRuntimeRunHistoryPath(compiledPath);
107
- if (!existsSync(historyPath))
108
- return [];
109
- try {
110
- return readFileSync(historyPath, "utf8")
111
- .split(/\r?\n/)
112
- .map((line) => line.trim())
113
- .filter((line) => line.length > 0)
114
- .map((line) => {
115
- try {
116
- return RuntimeRunSchema.safeParse(JSON.parse(line));
117
- }
118
- catch {
119
- return { success: false };
120
- }
121
- })
122
- .filter((parsed) => parsed.success)
123
- .map((parsed) => parsed.data);
124
- }
125
- catch {
126
- return [];
127
- }
128
- }
129
- function readTestRunAt(filePath) {
130
- const parsed = TestRunResourceSchema.safeParse(readJsonFile(filePath));
131
- return parsed.success ? parsed.data : null;
132
- }
133
- function readLocalJobRunAt(filePath) {
134
- const parsed = LocalJobRunResourceSchema.safeParse(readJsonFile(filePath));
135
- return parsed.success ? parsed.data : null;
136
- }
137
- function readActionProposalAt(filePath) {
138
- const parsed = ActionProposalResourceSchema.safeParse(readJsonFile(filePath));
139
- return parsed.success ? parsed.data : null;
140
- }
141
- function newestFirst(items) {
142
- return [...items].sort((left, right) => {
143
- const leftTime = Date.parse(left.started_at ?? left.finished_at ?? "");
144
- const rightTime = Date.parse(right.started_at ?? right.finished_at ?? "");
145
- return (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0);
146
- });
147
- }
148
- function newestJobFirst(items) {
149
- return [...items].sort((left, right) => Date.parse(right.created_at) - Date.parse(left.created_at));
150
- }
151
- function newestActionProposalFirst(items) {
152
- return [...items].sort((left, right) => Date.parse(right.created_at) - Date.parse(left.created_at));
153
- }
154
- function newestCompileFirst(items) {
155
- return [...items].sort((left, right) => Date.parse(right.created_at) - Date.parse(left.created_at));
156
- }
157
- function configuredAgentName() {
158
- const config = loadUserConfig();
159
- if (!config)
160
- return null;
161
- const configured = AGENTS.find((agent) => agent.command === config.agentCommand) ??
162
- AGENTS.find((agent) => agent.name === config.agent) ??
163
- AGENTS.find((agent) => agent.displayName === config.agent);
164
- return configured?.name ?? null;
165
- }
166
- function detectedExecutorOptions(currentAgentName) {
167
- return detectAgents()
168
- .filter(supportsAutomatedRuns)
169
- .map((agent) => ({
170
- name: agent.name,
171
- display_name: agent.displayName,
172
- command: agent.command,
173
- current: agent.name === currentAgentName,
174
- }));
175
- }
176
- function stageArtifactRefs(stageId, artifacts) {
177
- return (artifacts ?? []).map((path) => ({
178
- path,
179
- role: "output",
180
- stage_id: stageId,
181
- label: path,
182
- }));
183
- }
184
- function proofForStage(options) {
185
- return {
186
- id: `${options.runId}-${options.stageId}-proof`,
187
- run_id: options.runId,
188
- stage_id: options.stageId,
189
- generated_at: options.stageState.finished_at ?? new Date().toISOString(),
190
- summary: options.summary ?? `${options.stageId} produced stage evidence.`,
191
- files_processed: options.stageState.counts?.source_total,
192
- artifacts: options.artifacts,
193
- checks: [
194
- {
195
- id: `${options.stageId}-status`,
196
- label: "stage completed",
197
- ok: options.stageState.status === "succeeded",
198
- ...(options.stageState.status === "succeeded"
199
- ? {}
200
- : { detail: options.stageState.summary ?? "Stage did not complete successfully." }),
201
- },
202
- {
203
- id: `${options.stageId}-artifacts`,
204
- label: "artifacts recorded",
205
- ok: options.artifacts.length > 0,
206
- ...(options.artifacts.length > 0
207
- ? {}
208
- : { detail: "No stage artifacts were recorded." }),
209
- },
210
- ],
211
- };
212
- }
213
- function logsForStageRun(stageState) {
214
- const runId = stageState?.run_id;
215
- if (!runId)
216
- return undefined;
217
- return {
218
- prompt_path: `.interf/runtime/logs/${runId}.prompt.txt`,
219
- event_stream_path: `.interf/runtime/logs/${runId}.events.ndjson`,
220
- status_path: `.interf/runtime/logs/${runId}.status.log`,
221
- contract_path: `.interf/runtime/logs/${runId}.stage-contract.json`,
222
- };
223
- }
224
- function logsForRuntimeRun(run) {
225
- if (!run)
226
- return undefined;
227
- return {
228
- ...(run.logs?.prompt_path ? { prompt_path: run.logs.prompt_path } : {}),
229
- ...(run.logs?.event_stream_path ? { event_stream_path: run.logs.event_stream_path } : {}),
230
- ...(run.logs?.status_path ? { status_path: run.logs.status_path } : {}),
231
- contract_path: run.contract_path,
232
- };
233
- }
234
- function timestampKey(value) {
235
- const parsed = Date.parse(value ?? "");
236
- return Number.isFinite(parsed) ? parsed : 0;
237
- }
238
- function applyEventToCompileRun(run, event) {
239
- const now = event.timestamp;
240
- const stageFor = (stageId) => {
241
- const existing = run.stages.find((stage) => stage.stage_id === stageId);
242
- if (existing)
243
- return existing;
244
- const created = {
245
- run_id: run.run_id,
246
- stage_id: stageId,
247
- status: "queued",
248
- artifacts: [],
249
- };
250
- run.stages.push(created);
251
- return created;
252
- };
253
- const updateStage = (stageId, patch) => {
254
- const current = stageFor(stageId);
255
- Object.assign(current, patch);
256
- };
257
- switch (event.type) {
258
- case "run.started":
259
- return {
260
- ...run,
261
- status: "running",
262
- started_at: run.started_at ?? now,
263
- events: [...run.events, event],
264
- };
265
- case "stage.started":
266
- updateStage(event.stage_id, {
267
- status: "running",
268
- started_at: now,
269
- stage_index: event.stage_index,
270
- stage_total: event.stage_total,
271
- });
272
- break;
273
- case "artifact.written": {
274
- const stage = stageFor(event.stage_id);
275
- updateStage(event.stage_id, {
276
- artifacts: uniqueArtifacts([...(stage.artifacts ?? []), event.artifact]),
277
- });
278
- break;
279
- }
280
- case "proof.updated":
281
- if (event.stage_id) {
282
- updateStage(event.stage_id, {
283
- latest_proof: event.proof,
284
- });
285
- }
286
- return {
287
- ...run,
288
- latest_proof: event.proof,
289
- events: [...run.events, event],
290
- };
291
- case "log.appended":
292
- break;
293
- case "stage.passed":
294
- updateStage(event.stage_id, {
295
- status: "succeeded",
296
- finished_at: now,
297
- summary: event.summary ?? null,
298
- failure: null,
299
- });
300
- break;
301
- case "stage.failed":
302
- updateStage(event.stage_id, {
303
- status: "failed",
304
- finished_at: now,
305
- summary: event.error,
306
- failure: event.error,
307
- });
308
- break;
309
- case "run.completed":
310
- return {
311
- ...run,
312
- status: "succeeded",
313
- finished_at: run.finished_at ?? now,
314
- events: [...run.events, event],
315
- };
316
- case "run.failed":
317
- return {
318
- ...run,
319
- status: "failed",
320
- finished_at: run.finished_at ?? now,
321
- events: [...run.events, event],
322
- };
323
- case "readiness.updated":
324
- return {
325
- ...run,
326
- readiness: event.readiness,
327
- events: [...run.events, event],
328
- };
329
- default:
330
- break;
331
- }
332
- return {
333
- ...run,
334
- events: [...run.events, event],
335
- };
336
- }
337
- function applyEventToLocalJob(run, event) {
338
- const stepFor = (stepId) => {
339
- const existing = run.steps.find((step) => step.id === stepId);
340
- if (existing)
341
- return existing;
342
- const created = {
343
- id: stepId,
344
- label: stepId,
345
- status: "queued",
346
- };
347
- run.steps.push(created);
348
- return created;
349
- };
350
- const updateStep = (stepId, patch) => {
351
- if (!stepId)
352
- return;
353
- Object.assign(stepFor(stepId), patch);
354
- };
355
- switch (event.type) {
356
- case "job.started":
357
- return {
358
- ...run,
359
- status: "running",
360
- started_at: run.started_at ?? event.timestamp,
361
- events: [...run.events, event],
362
- };
363
- case "step.started":
364
- updateStep(event.step_id, {
365
- status: "running",
366
- started_at: event.timestamp,
367
- ...(event.input ? { input: event.input } : {}),
368
- });
369
- break;
370
- case "step.completed":
371
- updateStep(event.step_id, {
372
- status: "succeeded",
373
- finished_at: event.timestamp,
374
- summary: event.message ?? null,
375
- ...(event.output ? { output: event.output } : {}),
376
- });
377
- break;
378
- case "step.failed":
379
- updateStep(event.step_id, {
380
- status: "failed",
381
- finished_at: event.timestamp,
382
- summary: event.message ?? null,
383
- ...(event.output ? { output: event.output } : {}),
384
- });
385
- break;
386
- case "job.completed":
387
- return {
388
- ...run,
389
- status: "succeeded",
390
- finished_at: run.finished_at ?? event.timestamp,
391
- events: [...run.events, event],
392
- };
393
- case "job.failed":
394
- return {
395
- ...run,
396
- status: "failed",
397
- finished_at: run.finished_at ?? event.timestamp,
398
- error: event.message ?? "Job failed.",
399
- events: [...run.events, event],
400
- };
401
- case "artifact.written":
402
- case "log.appended":
403
- break;
404
- default:
405
- break;
406
- }
407
- return {
408
- ...run,
409
- events: [...run.events, event],
410
- };
411
- }
412
- function slugFromText(value) {
413
- const slug = value
414
- .toLowerCase()
415
- .replace(/[^a-z0-9]+/g, "-")
416
- .replace(/^-+|-+$/g, "")
417
- .slice(0, 42)
418
- .replace(/-+$/g, "");
419
- return slug || "custom";
420
- }
421
- function escapeRegExp(value) {
422
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
423
- }
424
- function stringValue(values, key) {
425
- const value = values?.[key];
426
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
427
- }
428
- function actionTypeFromValues(values) {
429
- const explicit = stringValue(values, "action_type") ?? stringValue(values, "service_action");
430
- if (explicit) {
431
- const parsed = ActionProposalPlanActionTypeSchema.safeParse(explicit);
432
- if (parsed.success && parsed.data !== "clarification")
433
- return parsed.data;
434
- }
435
- const action = stringValue(values, "action");
436
- if (action === "compile" || action === "prepare-run" || action === "run-preparation")
437
- return "compile";
438
- if (action === "test" || action === "check-readiness")
439
- return "test";
440
- if (action === "draft-readiness-checks" || action === "readiness-check-draft")
441
- return "readiness-check-draft";
442
- if (action === "create-method" || action === "method-authoring")
443
- return "method-authoring";
444
- if (action === "method-duplicate" || action === "method-remove" || action === "method-change")
445
- return "method-change";
446
- if (action === "preparation-remove" || action === "preparation-change")
447
- return "preparation-change";
448
- if (action === "improve-preparation" || action === "method-improvement")
449
- return "method-improvement";
450
- return null;
451
- }
452
- function directServiceEndpointForAction(actionType) {
453
- if (actionType === "preparation-setup")
454
- return "/v1/preparation-setups";
455
- if (actionType === "method-change")
456
- return "/v1/method-changes";
457
- if (actionType === "preparation-change")
458
- return "/v1/preparation-changes";
459
- return null;
460
- }
461
- function numberValue(values, key) {
462
- const value = values?.[key];
463
- return typeof value === "number" && Number.isFinite(value) ? value : null;
464
- }
465
- function testModeFromValues(values) {
466
- const value = stringValue(values, "mode") ?? stringValue(values, "target");
467
- if (value === "source-files")
468
- return "raw";
469
- if (value === "portable-context")
470
- return "compiled";
471
- return value === "raw" || value === "compiled" || value === "both" ? value : null;
472
- }
473
- function testModeValue(values, defaultMode = "both") {
474
- return testModeFromValues(values) ?? defaultMode;
475
- }
476
- function testModeCliTarget(mode) {
477
- if (mode === "raw")
478
- return "source-files";
479
- if (mode === "compiled")
480
- return "portable-context";
481
- return "both";
482
- }
483
- function methodIdForProposal(message, values) {
484
- const explicit = stringValue(values, "method_id") ??
485
- stringValue(values, "method");
486
- const fromMessage = message.match(/\b(?:create|reate|eate|draft|author|build|make)\s+(?:a\s+new\s+)?(?:interf\s+)?method\s+([a-z0-9][a-z0-9-]{0,79})\b/i)?.[1];
487
- return explicit ?? fromMessage ?? `custom-${slugFromText(message)}`;
488
- }
489
- function actionValueMethodTaskPrompt(values) {
490
- const parsed = MethodAuthoringActionValuesSchema.safeParse(values);
491
- return parsed.success ? methodAuthoringTaskPrompt(parsed.data) : null;
492
- }
493
- const METHOD_AUTHORING_INTERNAL_INSTRUCTION = /Use the attached values as the Method authoring request before proposing the action\.?/gi;
494
- const METHOD_AUTHORING_LABELS = [
495
- "Agent work",
496
- "Portable-context output",
497
- "Readiness checks",
498
- "CLI preview",
499
- ];
500
- function normalizeMethodAuthoringText(value) {
501
- return value
502
- .replace(/[│]+/g, " ")
503
- .replace(METHOD_AUTHORING_INTERNAL_INSTRUCTION, " ")
504
- .replace(/\s+/g, " ")
505
- .trim();
506
- }
507
- function extractMethodAuthoringSection(value, label) {
508
- const otherLabels = METHOD_AUTHORING_LABELS
509
- .filter((candidate) => candidate !== label)
510
- .map(escapeRegExp)
511
- .join("|");
512
- const match = value.match(new RegExp(`${escapeRegExp(label)}\\s*:\\s*([\\s\\S]*?)(?=\\s*(?:${otherLabels})\\s*:|$)`, "i"));
513
- return match?.[1] ? normalizeMethodAuthoringText(match[1]) : null;
514
- }
515
- function stripMethodCommandPrefix(value, methodId) {
516
- const specific = new RegExp(`^(?:create|reate|eate|draft|author|build|make)\\s+(?:a\\s+new\\s+)?(?:interf\\s+)?method\\s+${escapeRegExp(methodId)}\\.?\\s*`, "i");
517
- const generic = /^(?:create|reate|eate|draft|author|build|make)\s+(?:a\s+new\s+)?(?:interf\s+)?method\b[^.]{0,180}\.\s*/i;
518
- const stripped = normalizeMethodAuthoringText(value)
519
- .replace(specific, "")
520
- .replace(generic, "")
521
- .trim();
522
- return stripped || normalizeMethodAuthoringText(value);
523
- }
524
- function methodAuthoringPromptFallback(message, methodId) {
525
- const cleaned = message
526
- .replace(/[│]+/g, " ")
527
- .replace(METHOD_AUTHORING_INTERNAL_INSTRUCTION, " ")
528
- .trim();
529
- const agentWork = extractMethodAuthoringSection(cleaned, "Agent work");
530
- const portableOutput = extractMethodAuthoringSection(cleaned, "Portable-context output");
531
- const readinessNotes = extractMethodAuthoringSection(cleaned, "Readiness checks");
532
- const lines = [
533
- agentWork ? `Agent work: ${stripMethodCommandPrefix(agentWork, methodId)}` : null,
534
- portableOutput ? `Portable-context output: ${portableOutput}` : null,
535
- readinessNotes ? `Readiness checks: ${readinessNotes}` : null,
536
- ].filter((line) => Boolean(line));
537
- if (lines.length > 0)
538
- return lines.join("\n");
539
- return stripMethodCommandPrefix(cleaned, methodId);
540
- }
541
- function methodAuthoringHintFromPrompt(prompt) {
542
- const plain = normalizeMethodAuthoringText(prompt.replace(/\b(?:Agent work|Portable-context output|Readiness checks)\s*:/gi, " "));
543
- if (plain.length <= 180)
544
- return plain || "Custom Method";
545
- const clipped = plain.slice(0, 177).replace(/\s+\S*$/, "").trim();
546
- return `${clipped || plain.slice(0, 177)}...`;
547
- }
548
- function methodLabelFromId(methodId) {
549
- return methodId
550
- .split("-")
551
- .filter(Boolean)
552
- .map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
553
- .join(" ") || methodId;
554
- }
555
- function detachMethodFromPreparation(preparation, methodId) {
556
- if (methodIdForSourcePreparationConfig(preparation) !== methodId)
557
- return preparation;
558
- const { method: _removedMethod, ...detachedPreparation } = preparation;
559
- return detachedPreparation;
560
- }
561
- function requireSelectedMethod(preparation) {
562
- const methodId = methodIdForSourcePreparationConfig(preparation);
563
- if (methodId)
564
- return methodId;
565
- throw new Error(`Select a Method for Preparation "${preparation.name}" before preparing it.`);
566
- }
567
- function actionCommandPreview(actionType, preparationName, methodId, values) {
568
- if (actionType === "compile") {
569
- const methodSuffix = methodId ? ` # Method: ${methodId}` : "";
570
- return preparationName
571
- ? `interf compile --preparation ${preparationName}${methodSuffix}`
572
- : `interf compile${methodSuffix}`;
573
- }
574
- if (actionType === "test") {
575
- const mode = testModeCliTarget(testModeValue(values));
576
- return preparationName
577
- ? `interf test --preparation ${preparationName} --target ${mode}`
578
- : `interf test --target ${mode}`;
579
- }
580
- if (actionType === "readiness-check-draft") {
581
- return "interf # choose Auto-create readiness checks";
582
- }
583
- if (actionType === "method-authoring" || actionType === "method-improvement") {
584
- return "interf create method";
585
- }
586
- return "Try: create a Preparation, prepare, check readiness, draft readiness checks, or draft a Method.";
587
- }
588
- function hasCompiledTestTarget(sourcePath, preparationConfig) {
589
- const compiledPath = portableContextPath(sourcePath, preparationConfig.name);
590
- if (!existsSync(compiledPath))
591
- return false;
592
- return createCompiledTestTarget(compiledPath, preparationConfig.name, methodIdForSourcePreparationConfig(preparationConfig) ?? DEFAULT_METHOD_ID).eligible;
593
- }
594
- function actionAssistantMessage(actionType, preparationName, commandPreview) {
595
- const preparationSuffix = preparationName ? ` for Preparation "${preparationName}"` : "";
596
- if (actionType === "compile") {
597
- return `Interf prepared a prepare-run proposal${preparationSuffix}. Approve to submit it through the local Interf service and watch the run in Interf. CLI equivalent: ${commandPreview}`;
598
- }
599
- if (actionType === "test") {
600
- return `Interf prepared a readiness-check proposal${preparationSuffix}. Approve to run the requested target against saved readiness checks. CLI equivalent: ${commandPreview}`;
601
- }
602
- if (actionType === "readiness-check-draft") {
603
- return `Interf prepared a proposal to draft readiness checks${preparationSuffix}. Approve to ask the configured local executor to draft saved checks as a visible run. CLI equivalent: ${commandPreview}`;
604
- }
605
- if (actionType === "method-authoring" || actionType === "method-improvement") {
606
- return `Interf prepared a Method draft proposal${preparationSuffix}. Approve to draft a reusable local Method as a visible run. CLI equivalent: ${commandPreview}`;
607
- }
608
- return "I could not map that to a safe Interf action. Ask for one action in plain English, such as create a Preparation, prepare the data, check readiness, draft readiness checks, or draft a Method.";
609
- }
610
- function passRate(passed, total) {
611
- if (total <= 0)
612
- return null;
613
- return Math.round((passed / total) * 100);
614
- }
615
- function readinessTargetResult(summary, currentFingerprint, readinessRunFingerprint) {
616
- if (!summary)
617
- return null;
618
- const resultFingerprint = readinessRunFingerprint ?? null;
619
- return {
620
- passed: summary.passed_cases,
621
- total: summary.total_cases,
622
- pass_rate: passRate(summary.passed_cases, summary.total_cases),
623
- checks_fingerprint: resultFingerprint,
624
- stale: Boolean(currentFingerprint && resultFingerprint && currentFingerprint !== resultFingerprint),
625
- run_id: null,
626
- run_path: summary.run_path,
627
- };
628
- }
629
- function readinessSummaryForStatus(status) {
630
- if (status === "ready")
631
- return "Ready for agent work.";
632
- if (status === "not-ready")
633
- return "Readiness checks did not pass.";
634
- if (status === "stale")
635
- return "Readiness checks are stale for the current saved checks.";
636
- if (status === "checking")
637
- return "Readiness checks are running.";
638
- if (status === "building")
639
- return "Portable context is building.";
640
- if (status === "built")
641
- return "Portable context is built; readiness has not been proven yet.";
642
- if (status === "not-built")
643
- return "Portable context has not been built yet.";
644
- if (status === "not-configured")
645
- return "No readiness checks are configured.";
646
- return "Latest preparation failed.";
647
- }
648
- function readinessStateToPreparationReadiness(readiness) {
649
- return PreparationReadinessStateSchema.parse({
650
- ...readiness,
651
- checks: readiness.checks.map((check) => ({ ...check })),
652
- });
653
- }
654
- function buildPreparationResource(rootPath, preparation, readiness, latestCompileRunId, latestTestRunId) {
655
- const methodId = methodIdForSourcePreparationConfig(preparation);
656
- return PreparationResourceSchema.parse({
657
- id: preparation.name,
658
- name: preparation.name,
659
- preparation,
660
- source_path: resolveSourcePreparationPath(rootPath, preparation),
661
- method_id: methodId,
662
- checks: preparation.checks,
663
- portable_context: {
664
- preparation: preparation.name,
665
- path: readiness.portable_context_path,
666
- exists: readiness.portable_context_path !== null,
667
- method_id: methodId,
668
- latest_compile_run_id: latestCompileRunId,
669
- latest_test_run_id: latestTestRunId,
670
- },
671
- portable_context_path: readiness.portable_context_path,
672
- readiness: readinessStateToPreparationReadiness(readiness),
673
- runs: {
674
- latest_compile_run_id: latestCompileRunId,
675
- latest_test_run_id: latestTestRunId,
676
- },
677
- latest_compile_run_id: latestCompileRunId,
678
- latest_test_run_id: latestTestRunId,
679
- });
680
- }
681
- function buildMethodResource(resource) {
682
- return MethodResourceSchema.parse({
683
- id: resource.id,
684
- method_id: resource.id,
685
- path: resource.path,
686
- ...(resource.label ? { label: resource.label } : {}),
687
- ...(resource.hint ? { hint: resource.hint } : {}),
688
- source_kind: resource.source_kind,
689
- built_in: resource.built_in,
690
- active_for_preparations: resource.active_for_preparations,
691
- output_paths: resource.output_paths,
692
- stages: resource.stages,
693
- });
694
- }
31
+ /** TTL for `POST /v1/compile-runs` idempotency-key dedupe entries. */
32
+ const IDEMPOTENCY_TTL_MS = 60 * 60 * 1000;
33
+ /** Idempotency cache size at which to schedule an opportunistic prune. */
34
+ const IDEMPOTENCY_PRUNE_THRESHOLD = 64;
695
35
  export class LocalServiceRuntime {
696
- rootPath;
697
36
  host;
698
37
  port;
699
38
  startedAt;
700
39
  packageVersion;
701
40
  handlers;
41
+ /**
42
+ * The seed root path the runtime was constructed with. Used as a
43
+ * non-preparation fallback when a preparation-independent route
44
+ * (methods, action proposals, runs listings) needs an anchor to load
45
+ * shared state (user-library methods, bundled methods, etc).
46
+ */
47
+ rootPath;
48
+ /**
49
+ * Per-instance bearer token. Mutating routes require this on the
50
+ * Authorization header. `null` means token-less mode (test harness).
51
+ */
52
+ authToken;
53
+ /** Map of prepDataDir -> PreparationContext. */
54
+ workspaces = new Map();
55
+ /** Hook called whenever a workspace is registered or deregistered. */
56
+ onRegistryChanged = null;
57
+ /** In-flight runs across all workspaces. Used for `idle_for_seconds`. */
58
+ activeRunCount = 0;
59
+ /**
60
+ * Active compile-run cancellation handles, keyed by run id. Populated
61
+ * when a compile run is launched and cleared once the run reaches a
62
+ * terminal state. Each entry remembers where the persisted record lives
63
+ * so cancel can mark it without re-resolving the Preparation.
64
+ */
65
+ activeCompileRuns = new Map();
66
+ /**
67
+ * Idempotency-key cache for `POST /v1/compile-runs`. Outer key is the
68
+ * resolved workspace root; inner key is the client-supplied idempotency
69
+ * value. Namespacing per workspace prevents key collisions across
70
+ * tenants on the same engine (CSO finding: a malicious workspace could
71
+ * otherwise hijack another workspace's run id by reusing its key).
72
+ * Entries expire after `IDEMPOTENCY_TTL_MS`.
73
+ */
74
+ idempotencyKeyCache = new Map();
75
+ /**
76
+ * Read-side caches. Polling clients (Compiler UI, CLI status loops)
77
+ * hit list/get endpoints multiple times per second; without these,
78
+ * every request re-walks the filesystem and re-parses every JSON
79
+ * record through Zod. The runtime invalidates each cache on the
80
+ * matching write path. See {@link runtime-caches} for design notes.
81
+ */
82
+ compileRunCache = new RunListingCache();
83
+ testRunCache = new RunListingCache();
84
+ readinessCache = new ReadinessCache();
85
+ sourceFilesCache = new MtimeListingCache();
86
+ methodListingCache = new MethodListingCache();
702
87
  constructor(options) {
703
- this.rootPath = resolve(options.rootPath);
704
88
  this.host = options.host;
705
89
  this.port = options.port;
706
90
  this.startedAt = options.startedAt ?? new Date().toISOString();
707
91
  this.packageVersion = options.packageVersion;
708
92
  this.handlers = options.handlers ?? {};
93
+ this.authToken = options.authToken ?? null;
94
+ this.rootPath = resolve(options.rootPath);
95
+ // Auto-register the initial workspace so single-workspace callers
96
+ // (existing tests, the current `interf web` command) work without
97
+ // additional bootstrapping. The constructor seed is the only role
98
+ // `options.rootPath` plays; runtime methods take `prepDataDir`
99
+ // explicitly afterwards.
100
+ this.registerPreparation(this.rootPath);
101
+ }
102
+ setBoundPort(port) {
103
+ this.port = port;
104
+ }
105
+ /** Set a hook that fires whenever the registered workspaces change. */
106
+ setOnRegistryChanged(handler) {
107
+ this.onRegistryChanged = handler;
108
+ }
109
+ /**
110
+ * Register a workspace with this runtime. Returns the PreparationContext.
111
+ * Idempotent: re-registering an existing workspace updates `lastActivity`.
112
+ */
113
+ registerPreparation(prepDataDir) {
114
+ const resolved = resolve(prepDataDir);
115
+ const now = new Date().toISOString();
116
+ const existing = this.workspaces.get(resolved);
117
+ if (existing) {
118
+ existing.lastActivity = now;
119
+ this.onRegistryChanged?.();
120
+ return existing;
121
+ }
122
+ const context = {
123
+ rootPath: resolved,
124
+ startedAt: now,
125
+ lastActivity: now,
126
+ };
127
+ this.workspaces.set(resolved, context);
128
+ this.onRegistryChanged?.();
129
+ return context;
130
+ }
131
+ /**
132
+ * Remove a workspace from the runtime. Returns true if a workspace was
133
+ * removed.
134
+ */
135
+ deregisterPreparation(prepDataDir) {
136
+ const resolved = resolve(prepDataDir);
137
+ const removed = this.workspaces.delete(resolved);
138
+ if (removed) {
139
+ this.onRegistryChanged?.();
140
+ }
141
+ return removed;
142
+ }
143
+ /**
144
+ * Most recently active workspace, or the first registered if none has
145
+ * activity yet. Server code uses this as the fallback when a request
146
+ * omits the workspace header. Throws if no workspace is registered.
147
+ */
148
+ defaultPreparationDataDir() {
149
+ if (this.workspaces.size === 0) {
150
+ throw new Error("Local service has no registered workspaces.");
151
+ }
152
+ let best = null;
153
+ let bestKey = -Infinity;
154
+ for (const context of this.workspaces.values()) {
155
+ const key = Date.parse(context.lastActivity);
156
+ if (Number.isFinite(key) && key > bestKey) {
157
+ best = context;
158
+ bestKey = key;
159
+ }
160
+ }
161
+ return (best ?? this.workspaces.values().next().value).rootPath;
162
+ }
163
+ /** Look up a workspace context by rootPath. */
164
+ getPreparationContext(prepDataDir) {
165
+ return this.workspaces.get(resolve(prepDataDir)) ?? null;
166
+ }
167
+ /** All registered workspaces, ordered by registration time. */
168
+ listRegisteredPreparations() {
169
+ return Array.from(this.workspaces.values()).sort((left, right) => Date.parse(left.startedAt) - Date.parse(right.startedAt));
170
+ }
171
+ /** True when no workspaces are registered. */
172
+ hasNoWorkspaces() {
173
+ return this.workspaces.size === 0;
174
+ }
175
+ /** Number of registered workspaces. */
176
+ registeredPreparationCount() {
177
+ return this.workspaces.size;
178
+ }
179
+ /** Increment in-flight run counter. Call when a long-running run starts. */
180
+ beginActiveRun() {
181
+ this.activeRunCount += 1;
182
+ }
183
+ /** Decrement in-flight run counter. Pair with `beginActiveRun`. */
184
+ endActiveRun() {
185
+ if (this.activeRunCount > 0)
186
+ this.activeRunCount -= 1;
187
+ }
188
+ /** Sum of in-flight runs across all workspaces. */
189
+ activeRuns() {
190
+ return this.activeRunCount;
191
+ }
192
+ /**
193
+ * Mark the workspace as recently active. Routes call this on entry so
194
+ * `idleForSeconds` and the registry snapshots stay in sync with the
195
+ * actual request cadence.
196
+ */
197
+ touchPreparation(prepDataDir) {
198
+ const context = this.workspaces.get(resolve(prepDataDir));
199
+ if (context) {
200
+ context.lastActivity = new Date().toISOString();
201
+ }
202
+ }
203
+ /** Snapshot of registered workspaces for the registry / status output. */
204
+ registeredPreparationSnapshots() {
205
+ return this.listRegisteredPreparations().map((context) => ServiceRegistryWorkspaceSchema.parse({
206
+ control_path: context.rootPath,
207
+ registered_at: context.startedAt,
208
+ last_activity: context.lastActivity,
209
+ }));
210
+ }
211
+ /** Seconds since the most recent workspace activity (0 if active). */
212
+ idleForSeconds() {
213
+ const all = this.listRegisteredPreparations();
214
+ if (all.length === 0)
215
+ return 0;
216
+ if (this.activeRunCount > 0)
217
+ return 0;
218
+ let mostRecent = 0;
219
+ for (const context of all) {
220
+ const ts = Date.parse(context.lastActivity);
221
+ if (Number.isFinite(ts) && ts > mostRecent)
222
+ mostRecent = ts;
223
+ }
224
+ if (mostRecent <= 0)
225
+ return 0;
226
+ const elapsed = Math.max(0, Date.now() - mostRecent);
227
+ return Math.floor(elapsed / 1000);
709
228
  }
710
- health() {
711
- const sourceFolderPath = resolveConfiguredSourceFolderPath(this.rootPath);
229
+ health(prepDataDir) {
230
+ const sourceFolderPath = prepDataDir ? resolveConfiguredSourceFolderPath(prepDataDir) : null;
712
231
  return LocalServiceHealthSchema.parse({
713
232
  kind: "interf-local-service-health",
714
233
  version: 1,
@@ -716,52 +235,59 @@ export class LocalServiceRuntime {
716
235
  host: this.host,
717
236
  port: this.port,
718
237
  service_url: buildLocalServiceUrl({ host: this.host, port: this.port }),
719
- control_path: this.rootPath,
238
+ ...(prepDataDir ? { control_path: prepDataDir } : {}),
720
239
  source_folder_path: sourceFolderPath,
721
240
  started_at: this.startedAt,
722
241
  ...(this.packageVersion ? { package_version: this.packageVersion } : {}),
242
+ instance_started_at: this.startedAt,
243
+ registered_workspaces: this.registeredPreparationSnapshots(),
244
+ active_runs: this.activeRunCount,
245
+ idle_for_seconds: this.idleForSeconds(),
723
246
  });
724
247
  }
725
- listPreparations() {
726
- const config = loadSourceFolderConfig(this.rootPath);
248
+ listPreparations(prepDataDir) {
249
+ const config = loadSourceFolderConfig(prepDataDir);
727
250
  return listSourcePreparationConfigs(config).map((preparation) => {
728
- const compileRuns = this.listCompileRunsForPreparation(preparation.name);
729
- const testRuns = this.listTestRunsForPreparation(preparation.name);
730
- const readiness = this.computePreparationReadiness(preparation);
731
- return buildPreparationResource(this.rootPath, preparation, readiness, compileRuns[0]?.run_id ?? null, testRuns[0]?.run_id ?? null);
251
+ const compileRuns = this.listCompileRunsForPreparation(prepDataDir, preparation.name);
252
+ const testRuns = this.listTestRunsForPreparation(prepDataDir, preparation.name);
253
+ const readiness = this.computePreparationReadiness(prepDataDir, preparation);
254
+ return buildPreparationResource(prepDataDir, preparation, readiness, compileRuns[0]?.run_id ?? null, testRuns[0]?.run_id ?? null);
732
255
  });
733
256
  }
734
- getPreparation(preparationName) {
735
- return this.listPreparations().find((preparation) => preparation.name === preparationName) ?? null;
257
+ getPreparation(prepDataDir, preparationName) {
258
+ return this.listPreparations(prepDataDir).find((preparation) => preparation.name === preparationName) ?? null;
736
259
  }
737
- listPreparationReadiness() {
738
- return this.listReadiness().map(readinessStateToPreparationReadiness);
260
+ listPreparationReadiness(prepDataDir) {
261
+ return this.listReadiness(prepDataDir).map(readinessStateToPreparationReadiness);
739
262
  }
740
- getPreparationReadiness(preparationName) {
741
- const readiness = this.getReadiness(preparationName);
263
+ getPreparationReadiness(prepDataDir, preparationName) {
264
+ const readiness = this.getReadiness(prepDataDir, preparationName);
742
265
  return readiness ? readinessStateToPreparationReadiness(readiness) : null;
743
266
  }
744
- listReadiness() {
745
- const config = loadSourceFolderConfig(this.rootPath);
746
- return listSourcePreparationConfigs(config).map((preparation) => this.computePreparationReadiness(preparation));
267
+ listReadiness(prepDataDir) {
268
+ const config = loadSourceFolderConfig(prepDataDir);
269
+ return listSourcePreparationConfigs(config).map((preparation) => this.computePreparationReadiness(prepDataDir, preparation));
270
+ }
271
+ getReadiness(prepDataDir, preparationName) {
272
+ const preparation = findSourcePreparationConfig(loadSourceFolderConfig(prepDataDir), preparationName);
273
+ return preparation ? this.computePreparationReadiness(prepDataDir, preparation) : null;
747
274
  }
748
- getReadiness(preparationName) {
749
- const preparation = findSourcePreparationConfig(loadSourceFolderConfig(this.rootPath), preparationName);
750
- return preparation ? this.computePreparationReadiness(preparation) : null;
275
+ computePreparationReadiness(prepDataDir, preparation) {
276
+ return this.readinessCache.get(prepDataDir, preparation.name, () => this.computePreparationReadinessUncached(prepDataDir, preparation));
751
277
  }
752
- computePreparationReadiness(preparation) {
278
+ computePreparationReadinessUncached(prepDataDir, preparation) {
753
279
  const generatedAt = new Date().toISOString();
754
- const compiledPath = portableContextPath(this.rootPath, preparation.name);
280
+ const compiledPath = preparationPortableContextPath(asPreparationDataDir(prepDataDir), preparation.name);
755
281
  const contextExists = existsSync(compiledPath);
756
282
  const compiledTarget = createCompiledTestTarget(compiledPath, preparation.name, methodIdForSourcePreparationConfig(preparation) ?? DEFAULT_METHOD_ID);
757
283
  const contextReady = compiledTarget.eligible;
758
- const compileRun = this.listCompileRunsForPreparation(preparation.name)[0] ?? null;
759
- const testRun = this.listTestRunsForPreparation(preparation.name)[0] ?? null;
760
- const readinessRun = this.readLatestReadinessRun(preparation.name);
284
+ const compileRun = this.listCompileRunsForPreparation(prepDataDir, preparation.name)[0] ?? null;
285
+ const testRun = this.listTestRunsForPreparation(prepDataDir, preparation.name)[0] ?? null;
286
+ const readinessRun = this.readLatestReadinessRun(prepDataDir, preparation.name);
761
287
  const configuredChecks = preparation.checks.length;
762
288
  const currentFingerprint = configuredChecks > 0 ? fingerprintReadinessChecks(preparation.checks) : null;
763
289
  const readinessRunFingerprint = readinessRun?.checks_fingerprint ?? null;
764
- const sourceResult = readinessTargetResult(readinessRun?.raw, currentFingerprint, readinessRunFingerprint);
290
+ const sourceResult = readinessTargetResult(readinessRun?.source_files, currentFingerprint, readinessRunFingerprint);
765
291
  const contextResult = readinessTargetResult(readinessRun?.compiled, currentFingerprint, readinessRunFingerprint);
766
292
  const checksStale = Boolean(currentFingerprint && readinessRunFingerprint && currentFingerprint !== readinessRunFingerprint);
767
293
  const compileCheck = (() => {
@@ -879,39 +405,46 @@ export class LocalServiceRuntime {
879
405
  checks,
880
406
  });
881
407
  }
882
- listSourceFiles(preparationName) {
883
- const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath))
408
+ listSourceFiles(prepDataDir, preparationName) {
409
+ const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir))
884
410
  .filter((preparation) => !preparationName || preparation.name === preparationName);
885
411
  return preparations.flatMap((preparation) => {
886
- const sourceFolderPath = resolveSourcePreparationPath(this.rootPath, preparation);
887
- const compiledPath = portableContextPath(this.rootPath, preparation.name);
888
- return discoverSourceFiles(sourceFolderPath, compiledPath).sourceFiles.map((relativePath) => {
889
- const absolutePath = join(sourceFolderPath, relativePath);
890
- let sizeBytes = 0;
891
- let modifiedAt = null;
892
- try {
893
- const stat = statSync(absolutePath);
894
- sizeBytes = stat.size;
895
- modifiedAt = stat.mtime.toISOString();
896
- }
897
- catch {
898
- sizeBytes = 0;
899
- modifiedAt = null;
900
- }
901
- return SourceFileResourceSchema.parse({
902
- preparation: preparation.name,
903
- path: relativePath,
904
- absolute_path: absolutePath,
905
- size_bytes: sizeBytes,
906
- modified_at: modifiedAt,
907
- source_folder_path: sourceFolderPath,
412
+ const sourceFolderPath = resolveSourcePreparationPath(prepDataDir, preparation);
413
+ const compiledPath = preparationPortableContextPath(asPreparationDataDir(prepDataDir), preparation.name);
414
+ // Cache by source-folder root mtime so identical UI polls do not
415
+ // re-walk and re-stat the entire tree. The cache imposes a short
416
+ // TTL (see runtime-caches.ts) so deeper changes are still picked
417
+ // up promptly.
418
+ const cacheKey = `${preparation.name}\0${sourceFolderPath}\0${compiledPath}`;
419
+ return this.sourceFilesCache.get(cacheKey, sourceFolderPath, () => {
420
+ return discoverSourceFiles(sourceFolderPath, compiledPath).sourceFiles.map((relativePath) => {
421
+ const absolutePath = join(sourceFolderPath, relativePath);
422
+ let sizeBytes = 0;
423
+ let modifiedAt = null;
424
+ try {
425
+ const stat = statSync(absolutePath);
426
+ sizeBytes = stat.size;
427
+ modifiedAt = stat.mtime.toISOString();
428
+ }
429
+ catch {
430
+ sizeBytes = 0;
431
+ modifiedAt = null;
432
+ }
433
+ return SourceFileResourceSchema.parse({
434
+ preparation: preparation.name,
435
+ path: relativePath,
436
+ absolute_path: absolutePath,
437
+ size_bytes: sizeBytes,
438
+ modified_at: modifiedAt,
439
+ source_folder_path: sourceFolderPath,
440
+ });
908
441
  });
909
442
  });
910
443
  });
911
444
  }
912
- listWorkspaceFiles() {
913
- const sourceFolderPath = resolveConfiguredSourceFolderPath(this.rootPath) ?? this.rootPath;
914
- return discoverSourceFiles(sourceFolderPath, join(this.rootPath, "interf")).sourceFiles.map((relativePath) => {
445
+ listWorkspaceFiles(prepDataDir) {
446
+ const sourceFolderPath = resolveConfiguredSourceFolderPath(prepDataDir) ?? prepDataDir;
447
+ return discoverSourceFiles(sourceFolderPath, prepDataDir).sourceFiles.map((relativePath) => {
915
448
  const absolutePath = join(sourceFolderPath, relativePath);
916
449
  let sizeBytes = 0;
917
450
  let modifiedAt = null;
@@ -932,51 +465,61 @@ export class LocalServiceRuntime {
932
465
  });
933
466
  });
934
467
  }
935
- listMethods() {
936
- const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath));
937
- const choices = listCompiledMethodChoices(this.rootPath);
938
- return choices.map((method) => {
939
- const activeForPreparations = preparations
940
- .filter((preparation) => methodIdForSourcePreparationConfig(preparation) === method.id)
941
- .map((preparation) => preparation.name);
942
- return buildMethodResource({
943
- id: method.id,
944
- path: resolveMethodPackageSourcePath(this.rootPath, method.id) ?? method.id,
945
- label: method.label,
946
- hint: method.hint,
947
- source_kind: method.scope === "builtin" ? "builtin" : "local",
948
- built_in: method.scope === "builtin",
949
- active_for_preparations: activeForPreparations,
950
- output_paths: (method.contextInterface?.zones ?? [])
951
- .filter((zone) => zone.role === "output")
952
- .map((zone) => contextInterfaceArtifactPath(zone))
953
- .sort(),
954
- stages: method.stages.map((stage) => ({
955
- id: stage.id,
956
- label: stage.label,
957
- description: stage.description,
958
- contract_type: stage.contractType,
959
- skill_dir: stage.skillDir,
960
- reads: stage.reads,
961
- writes: stage.writes,
962
- ...(stage.acceptance ? { acceptance: stage.acceptance } : {}),
963
- })),
468
+ listMethods(prepDataDir) {
469
+ // The Method choices list is dominated by repeated reads of
470
+ // method.json + context-interface across builtin / user / workspace
471
+ // method roots. Key the cache off mtimes for the three roots; if
472
+ // any of them changes (a new local Method, an edit to the user
473
+ // library, etc.) the cache misses and we re-resolve.
474
+ const builtinRoot = join(PACKAGE_ROOT, "builtin-methods");
475
+ const localRoot = preparationMethodsRoot(asPreparationDataDir(prepDataDir));
476
+ const userRoot = userMethodsRoot();
477
+ return this.methodListingCache.get(prepDataDir, [builtinRoot, localRoot, userRoot], () => {
478
+ const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir));
479
+ const choices = listCompiledMethodChoices(prepDataDir);
480
+ return choices.map((method) => {
481
+ const activeForPreparations = preparations
482
+ .filter((preparation) => methodIdForSourcePreparationConfig(preparation) === method.id)
483
+ .map((preparation) => preparation.name);
484
+ return buildMethodResource({
485
+ id: method.id,
486
+ path: resolveMethodPackageSourcePath(prepDataDir, method.id) ?? method.id,
487
+ label: method.label,
488
+ hint: method.hint,
489
+ source_kind: method.scope === "builtin" ? "builtin" : "local",
490
+ built_in: method.scope === "builtin",
491
+ active_for_preparations: activeForPreparations,
492
+ output_paths: (method.contextInterface?.zones ?? [])
493
+ .filter((zone) => zone.role === "output")
494
+ .map((zone) => contextInterfaceArtifactPath(zone))
495
+ .sort(),
496
+ stages: method.stages.map((stage) => ({
497
+ id: stage.id,
498
+ label: stage.label,
499
+ description: stage.description,
500
+ contract_type: stage.contractType,
501
+ skill_dir: stage.skillDir,
502
+ reads: stage.reads,
503
+ writes: stage.writes,
504
+ ...(stage.acceptance ? { acceptance: stage.acceptance } : {}),
505
+ })),
506
+ });
964
507
  });
965
508
  });
966
509
  }
967
- getMethod(methodId) {
968
- return this.listMethods().find((method) => method.id === methodId) ?? null;
510
+ getMethod(prepDataDir, methodId) {
511
+ return this.listMethods(prepDataDir).find((method) => method.id === methodId) ?? null;
969
512
  }
970
- listJobs() {
971
- return newestJobFirst(listJsonFiles(localJobsRoot(this.rootPath))
513
+ listJobs(prepDataDir) {
514
+ return byCreatedAtDesc(listJsonFiles(localJobsRoot(prepDataDir))
972
515
  .map(readLocalJobRunAt)
973
516
  .filter((run) => run !== null));
974
517
  }
975
- getJob(runId) {
976
- return this.listJobs().find((run) => run.run_id === runId) ?? null;
518
+ getJob(prepDataDir, runId) {
519
+ return this.listJobs(prepDataDir).find((run) => run.run_id === runId) ?? null;
977
520
  }
978
- getJobEvents(runId) {
979
- return this.getJob(runId)?.events ?? null;
521
+ getJobEvents(prepDataDir, runId) {
522
+ return this.getJob(prepDataDir, runId)?.events ?? null;
980
523
  }
981
524
  getExecutorStatus() {
982
525
  const checkedAt = new Date().toISOString();
@@ -1046,26 +589,26 @@ export class LocalServiceRuntime {
1046
589
  });
1047
590
  return this.getExecutorStatus();
1048
591
  }
1049
- listActionProposals() {
1050
- return newestActionProposalFirst(listJsonFiles(actionProposalsRoot(this.rootPath))
592
+ listActionProposals(prepDataDir) {
593
+ return byCreatedAtDesc(listJsonFiles(actionProposalsRoot(prepDataDir))
1051
594
  .map(readActionProposalAt)
1052
595
  .filter((proposal) => proposal !== null));
1053
596
  }
1054
- getActionProposal(proposalId) {
1055
- return this.listActionProposals().find((proposal) => proposal.proposal_id === proposalId) ?? null;
597
+ getActionProposal(prepDataDir, proposalId) {
598
+ return this.listActionProposals(prepDataDir).find((proposal) => proposal.proposal_id === proposalId) ?? null;
1056
599
  }
1057
- async createActionProposal(requestValue) {
600
+ async createActionProposal(prepDataDir, requestValue) {
1058
601
  const request = ActionProposalCreateRequestSchema.parse(requestValue);
1059
602
  const proposal = ActionProposalResourceSchema.parse({
1060
- ...(await this.buildActionProposal(request)),
603
+ ...(await this.buildActionProposal(prepDataDir, request)),
1061
604
  client_origin: request.client_origin,
1062
605
  });
1063
- this.writeActionProposal(proposal);
606
+ this.writeActionProposal(prepDataDir, proposal);
1064
607
  return proposal;
1065
608
  }
1066
- async decideActionProposal(proposalId, requestValue) {
609
+ async decideActionProposal(prepDataDir, proposalId, requestValue) {
1067
610
  const decision = ActionProposalApprovalRequestSchema.parse(requestValue);
1068
- const current = this.getActionProposal(proposalId);
611
+ const current = this.getActionProposal(prepDataDir, proposalId);
1069
612
  if (!current)
1070
613
  return null;
1071
614
  if (current.status !== "awaiting_approval") {
@@ -1082,11 +625,11 @@ export class LocalServiceRuntime {
1082
625
  ...(decision.note ? { note: decision.note } : {}),
1083
626
  },
1084
627
  });
1085
- this.writeActionProposal(decided);
628
+ this.writeActionProposal(prepDataDir, decided);
1086
629
  if (!decision.approved)
1087
630
  return decided;
1088
631
  try {
1089
- const submission = await this.submitActionProposal(decided);
632
+ const submission = await this.submitActionProposal(prepDataDir, decided);
1090
633
  const submitted = ActionProposalResourceSchema.parse({
1091
634
  ...decided,
1092
635
  status: "submitted",
@@ -1094,7 +637,7 @@ export class LocalServiceRuntime {
1094
637
  submitted_run_id: submission.runId,
1095
638
  submitted_run_type: submission.runType,
1096
639
  });
1097
- this.writeActionProposal(submitted);
640
+ this.writeActionProposal(prepDataDir, submitted);
1098
641
  return submitted;
1099
642
  }
1100
643
  catch (error) {
@@ -1104,25 +647,25 @@ export class LocalServiceRuntime {
1104
647
  updated_at: new Date().toISOString(),
1105
648
  error: error instanceof Error ? error.message : String(error),
1106
649
  });
1107
- this.writeActionProposal(failed);
650
+ this.writeActionProposal(prepDataDir, failed);
1108
651
  return failed;
1109
652
  }
1110
653
  }
1111
- listRunObservability() {
654
+ listRunObservability(prepDataDir) {
1112
655
  return [
1113
- ...this.listCompileRuns().map((resource) => compileRunToObservability(resource.run)),
1114
- ...this.listTestRuns().map(testRunToObservability),
1115
- ...this.listJobs().map(jobRunToObservability),
656
+ ...this.listCompileRuns(prepDataDir).map((resource) => compileRunToObservability(resource.run)),
657
+ ...this.listTestRuns(prepDataDir).map(testRunToObservability),
658
+ ...this.listJobs(prepDataDir).map(jobRunToObservability),
1116
659
  ].sort((left, right) => {
1117
660
  const leftTime = timestampKey(left.started_at ?? left.created_at ?? left.finished_at);
1118
661
  const rightTime = timestampKey(right.started_at ?? right.created_at ?? right.finished_at);
1119
662
  return rightTime - leftTime;
1120
663
  });
1121
664
  }
1122
- getRunObservability(runId) {
1123
- return this.listRunObservability().find((run) => run.run_id === runId) ?? null;
665
+ getRunObservability(prepDataDir, runId) {
666
+ return this.listRunObservability(prepDataDir).find((run) => run.run_id === runId) ?? null;
1124
667
  }
1125
- createJobRun(requestValue) {
668
+ createJobRun(prepDataDir, requestValue) {
1126
669
  const request = LocalJobRunCreateRequestSchema.parse(requestValue);
1127
670
  const runId = createRunId("job");
1128
671
  const now = new Date().toISOString();
@@ -1154,12 +697,12 @@ export class LocalServiceRuntime {
1154
697
  },
1155
698
  ],
1156
699
  });
1157
- this.writeJobRun(run);
700
+ this.writeJobRun(prepDataDir, run);
1158
701
  return run;
1159
702
  }
1160
- appendJobRunEvent(runId, requestValue) {
703
+ appendJobRunEvent(prepDataDir, runId, requestValue) {
1161
704
  const request = LocalJobEventAppendRequestSchema.parse(requestValue);
1162
- const current = this.getJob(runId);
705
+ const current = this.getJob(prepDataDir, runId);
1163
706
  if (!current)
1164
707
  return null;
1165
708
  const event = {
@@ -1174,12 +717,12 @@ export class LocalServiceRuntime {
1174
717
  ...(request.output ? { output: request.output } : {}),
1175
718
  };
1176
719
  const next = LocalJobRunResourceSchema.parse(applyEventToLocalJob(current, event));
1177
- this.writeJobRun(next);
720
+ this.writeJobRun(prepDataDir, next);
1178
721
  return next;
1179
722
  }
1180
- async createReadinessCheckDraftRun(requestValue) {
723
+ async createReadinessCheckDraftRun(prepDataDir, requestValue) {
1181
724
  const request = ReadinessCheckDraftCreateRequestSchema.parse(requestValue);
1182
- const job = this.createJobRun({
725
+ const job = this.createJobRun(prepDataDir, {
1183
726
  job_type: "readiness-check-draft",
1184
727
  title: `Draft readiness checks for ${request.preparation}`,
1185
728
  preparation: request.preparation,
@@ -1207,7 +750,7 @@ export class LocalServiceRuntime {
1207
750
  },
1208
751
  ],
1209
752
  });
1210
- this.appendJobRunEvent(job.run_id, {
753
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1211
754
  type: "step.started",
1212
755
  step_id: "read-source",
1213
756
  message: "Reading source files for readiness-check evidence.",
@@ -1216,7 +759,7 @@ export class LocalServiceRuntime {
1216
759
  source_folder_path: request.source_folder_path,
1217
760
  },
1218
761
  });
1219
- this.appendJobRunEvent(job.run_id, {
762
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1220
763
  type: "step.completed",
1221
764
  step_id: "read-source",
1222
765
  message: "Source folder is ready for drafting readiness checks.",
@@ -1225,7 +768,7 @@ export class LocalServiceRuntime {
1225
768
  source_folder_path: request.source_folder_path,
1226
769
  },
1227
770
  });
1228
- this.appendJobRunEvent(job.run_id, {
771
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1229
772
  type: "step.started",
1230
773
  step_id: "agent-draft",
1231
774
  message: "Drafting saved readiness checks from the source files.",
@@ -1234,30 +777,31 @@ export class LocalServiceRuntime {
1234
777
  target_count: request.target_count,
1235
778
  },
1236
779
  });
1237
- void this.runReadinessCheckDraftInBackground(request, job.run_id);
1238
- return this.getJob(job.run_id) ?? job;
780
+ void this.runReadinessCheckDraftInBackground(prepDataDir, request, job.run_id);
781
+ return this.getJob(prepDataDir, job.run_id) ?? job;
1239
782
  }
1240
- applyMethodChange(requestValue) {
783
+ applyMethodChange(prepDataDir, requestValue) {
1241
784
  const request = MethodChangeCreateRequestSchema.parse(requestValue);
1242
785
  const outputPath = request.operation === "duplicate"
1243
- ? methodDefinitionPath(this.rootPath, request.new_method_id)
1244
- : methodDefinitionPath(this.rootPath, request.method);
786
+ ? methodDefinitionPath(prepDataDir, request.new_method_id)
787
+ : methodDefinitionPath(prepDataDir, request.method);
1245
788
  if (request.operation === "duplicate") {
1246
- if (resolveMethodPackageSourcePath(this.rootPath, request.new_method_id)) {
789
+ if (resolveMethodPackageSourcePath(prepDataDir, request.new_method_id)) {
1247
790
  throw new Error(`Method "${request.new_method_id}" already exists.`);
1248
791
  }
1249
- if (!resolveMethodPackageSourcePath(this.rootPath, request.method)) {
792
+ if (!resolveMethodPackageSourcePath(prepDataDir, request.method)) {
1250
793
  throw new Error(`Method "${request.method}" does not exist.`);
1251
794
  }
1252
795
  const label = request.label ?? methodLabelFromId(request.new_method_id);
1253
796
  const hint = request.hint ?? `Duplicate of ${request.method}`;
1254
797
  const methodPath = seedLocalMethodPackageFromBase({
1255
- sourcePath: this.rootPath,
798
+ prepDataDir,
1256
799
  baseMethodId: request.method,
1257
800
  methodId: request.new_method_id,
1258
801
  label,
1259
802
  hint,
1260
803
  });
804
+ this.methodListingCache.invalidate(prepDataDir);
1261
805
  return MethodChangeResultSchema.parse({
1262
806
  kind: "interf-method-change-result",
1263
807
  version: 1,
@@ -1273,19 +817,24 @@ export class LocalServiceRuntime {
1273
817
  if (request.confirmation !== request.method) {
1274
818
  throw new Error(`Type ${request.method} to confirm Method removal.`);
1275
819
  }
1276
- const localMethodPath = methodDefinitionPath(this.rootPath, request.method);
820
+ const localMethodPath = methodDefinitionPath(prepDataDir, request.method);
1277
821
  if (request.method === DEFAULT_METHOD_ID || !existsSync(localMethodPath)) {
1278
822
  throw new Error(`Method "${request.method}" is not a removable local Method.`);
1279
823
  }
1280
- const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath));
824
+ const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir));
1281
825
  const updatedPreparations = preparations
1282
826
  .filter((preparation) => methodIdForSourcePreparationConfig(preparation) === request.method);
1283
827
  if (updatedPreparations.length > 0) {
1284
- saveSourceFolderConfig(this.rootPath, {
828
+ saveSourceFolderConfig(prepDataDir, {
1285
829
  preparations: preparations.map((preparation) => detachMethodFromPreparation(preparation, request.method)),
1286
830
  });
831
+ // Detaching a Method changes readiness shape for those Preparations.
832
+ for (const preparation of updatedPreparations) {
833
+ this.readinessCache.invalidatePreparation(prepDataDir, preparation.name);
834
+ }
1287
835
  }
1288
836
  rmSync(outputPath, { recursive: true, force: true });
837
+ this.methodListingCache.invalidate(prepDataDir);
1289
838
  return MethodChangeResultSchema.parse({
1290
839
  kind: "interf-method-change-result",
1291
840
  version: 1,
@@ -1299,7 +848,75 @@ export class LocalServiceRuntime {
1299
848
  : `Removed Method ${request.method}.`,
1300
849
  });
1301
850
  }
1302
- applyPreparationSetup(requestValue) {
851
+ /**
852
+ * Bootstrap the workspace's source-folder binding and seed the default
853
+ * Method. Idempotent: re-running with the same source folder preserves
854
+ * existing preparations and reports `changed: false`.
855
+ *
856
+ * The CLI calls this in place of writing `interf.json` directly, so the
857
+ * operation is recorded by the service (and visible to other clients).
858
+ */
859
+ bootstrapWorkspace(prepDataDir, requestValue) {
860
+ const request = WorkspaceBootstrapCreateRequestSchema.parse(requestValue);
861
+ const requestedPath = request.source_folder?.path?.trim() ?? null;
862
+ const existing = loadSourceFolderConfig(prepDataDir);
863
+ const existingSourcePath = existing?.source_folder?.path ?? null;
864
+ const preparations = existing?.preparations ?? [];
865
+ let sourceFolderPath = existingSourcePath;
866
+ let sourceChanged = false;
867
+ if (requestedPath && requestedPath.length > 0) {
868
+ // Validate that the source folder exists relative to the workspace.
869
+ const resolvedSourcePath = resolve(prepDataDir, requestedPath);
870
+ if (!existsSync(resolvedSourcePath) || !statSync(resolvedSourcePath).isDirectory()) {
871
+ throw new Error(`Source folder "${requestedPath}" is not available.`);
872
+ }
873
+ if (existingSourcePath !== requestedPath) {
874
+ sourceFolderPath = requestedPath;
875
+ sourceChanged = true;
876
+ }
877
+ }
878
+ const writeNeeded = !existing || sourceChanged;
879
+ if (writeNeeded) {
880
+ saveSourceFolderConfig(prepDataDir, {
881
+ ...(sourceFolderPath ? { source_folder: { path: sourceFolderPath } } : {}),
882
+ preparations,
883
+ });
884
+ // Source folder rebound: drop every cached read for this workspace.
885
+ this.readinessCache.invalidateWorkspace(prepDataDir);
886
+ this.compileRunCache.invalidateWorkspace(prepDataDir);
887
+ this.testRunCache.invalidateWorkspace(prepDataDir);
888
+ this.sourceFilesCache.invalidateAll();
889
+ this.methodListingCache.invalidate(prepDataDir);
890
+ }
891
+ let seededMethodId = null;
892
+ let seededDefaultMethod = false;
893
+ if (request.seed_default_method) {
894
+ const seeded = seedLocalDefaultMethod({ prepDataDir });
895
+ seededMethodId = seeded.methodId;
896
+ seededDefaultMethod = !seeded.alreadyExisted;
897
+ if (seededDefaultMethod) {
898
+ this.methodListingCache.invalidate(prepDataDir);
899
+ }
900
+ }
901
+ const changed = writeNeeded || seededDefaultMethod;
902
+ return WorkspaceBootstrapResultSchema.parse({
903
+ kind: "interf-workspace-bootstrap-result",
904
+ version: 1,
905
+ control_path: prepDataDir,
906
+ config_path: preparationConfigPath(asPreparationDataDir(prepDataDir)),
907
+ source_folder_path: sourceFolderPath,
908
+ preparations: preparations.length,
909
+ seeded_default_method: seededDefaultMethod,
910
+ default_method_id: seededMethodId,
911
+ changed,
912
+ message: changed
913
+ ? sourceFolderPath
914
+ ? `Workspace ready. Source Folder: ${sourceFolderPath}.`
915
+ : `Workspace ready.`
916
+ : `Workspace already initialized.`,
917
+ });
918
+ }
919
+ applyPreparationSetup(prepDataDir, requestValue) {
1303
920
  const request = PreparationSetupCreateRequestSchema.parse(requestValue);
1304
921
  const preparationConfig = request.preparation;
1305
922
  const methodId = methodIdForSourcePreparationConfig(preparationConfig) ?? DEFAULT_METHOD_ID;
@@ -1307,11 +924,18 @@ export class LocalServiceRuntime {
1307
924
  ...preparationConfig,
1308
925
  method: methodId,
1309
926
  };
1310
- const sourceFolderPath = resolveSourcePreparationPath(this.rootPath, normalizedPreparationConfig);
927
+ const sourceFolderPath = resolveSourcePreparationPath(prepDataDir, normalizedPreparationConfig);
1311
928
  if (!existsSync(sourceFolderPath) || !statSync(sourceFolderPath).isDirectory()) {
1312
929
  throw new Error(`Source folder "${preparationConfig.path}" is not available.`);
1313
930
  }
1314
- upsertSourcePreparationConfig(this.rootPath, normalizedPreparationConfig);
931
+ upsertSourcePreparationConfig(prepDataDir, normalizedPreparationConfig);
932
+ // The Preparation's bound source folder + Method may have changed:
933
+ // bust the per-preparation readiness, runs, and method-listing
934
+ // caches so the next read reflects the new shape.
935
+ this.readinessCache.invalidatePreparation(prepDataDir, normalizedPreparationConfig.name);
936
+ this.compileRunCache.invalidatePreparation(prepDataDir, normalizedPreparationConfig.name);
937
+ this.testRunCache.invalidatePreparation(prepDataDir, normalizedPreparationConfig.name);
938
+ this.methodListingCache.invalidate(prepDataDir);
1315
939
  const operation = request.setup_mode === "select-method" ? "select-method" : "create";
1316
940
  return PreparationSetupResultSchema.parse({
1317
941
  kind: "interf-preparation-setup-result",
@@ -1320,47 +944,55 @@ export class LocalServiceRuntime {
1320
944
  preparation: normalizedPreparationConfig.name,
1321
945
  method: methodId,
1322
946
  source_folder_path: sourceFolderPath,
1323
- config_path: join(this.rootPath, "interf", "interf.json"),
1324
- portable_context_path: portableContextPath(this.rootPath, normalizedPreparationConfig.name),
947
+ config_path: preparationConfigPath(asPreparationDataDir(prepDataDir)),
948
+ portable_context_path: preparationPortableContextPath(asPreparationDataDir(prepDataDir), normalizedPreparationConfig.name),
1325
949
  changed: true,
1326
950
  message: operation === "select-method"
1327
951
  ? `Preparation ${normalizedPreparationConfig.name} now uses Method ${methodId}.`
1328
952
  : `Preparation ${normalizedPreparationConfig.name} is saved.`,
1329
953
  });
1330
954
  }
1331
- applyPreparationChange(requestValue) {
955
+ applyPreparationChange(prepDataDir, requestValue) {
1332
956
  const request = PreparationChangeCreateRequestSchema.parse(requestValue);
1333
957
  if (request.confirmation !== request.preparation) {
1334
958
  throw new Error(`Type ${request.preparation} to confirm Preparation removal.`);
1335
959
  }
1336
- const preparation = findSourcePreparationConfig(loadSourceFolderConfig(this.rootPath), request.preparation);
960
+ const preparation = findSourcePreparationConfig(loadSourceFolderConfig(prepDataDir), request.preparation);
1337
961
  if (!preparation) {
1338
962
  throw new Error(`Preparation "${request.preparation}" is not saved.`);
1339
963
  }
1340
- removeSourcePreparationConfig(this.rootPath, request.preparation);
964
+ removeSourcePreparationConfig(prepDataDir, request.preparation);
965
+ this.readinessCache.invalidatePreparation(prepDataDir, request.preparation);
966
+ this.compileRunCache.invalidatePreparation(prepDataDir, request.preparation);
967
+ this.testRunCache.invalidatePreparation(prepDataDir, request.preparation);
968
+ this.methodListingCache.invalidate(prepDataDir);
1341
969
  return PreparationChangeResultSchema.parse({
1342
970
  kind: "interf-preparation-change-result",
1343
971
  version: 1,
1344
972
  operation: "remove",
1345
973
  preparation: request.preparation,
1346
- config_path: join(this.rootPath, "interf", "interf.json"),
1347
- portable_context_path: portableContextPath(this.rootPath, request.preparation),
974
+ config_path: preparationConfigPath(asPreparationDataDir(prepDataDir)),
975
+ portable_context_path: preparationPortableContextPath(asPreparationDataDir(prepDataDir), request.preparation),
1348
976
  portable_context_retained: true,
1349
977
  changed: true,
1350
978
  message: `Removed Preparation ${request.preparation}. Portable Context files were retained.`,
1351
979
  });
1352
980
  }
1353
- applyReset(requestValue) {
981
+ applyReset(prepDataDir, requestValue) {
1354
982
  const request = ResetRequestSchema.parse(requestValue);
1355
- const preparation = findSourcePreparationConfig(loadSourceFolderConfig(this.rootPath), request.preparation);
983
+ const preparation = findSourcePreparationConfig(loadSourceFolderConfig(prepDataDir), request.preparation);
1356
984
  if (!preparation) {
1357
985
  throw new Error(`Preparation "${request.preparation}" is not saved.`);
1358
986
  }
1359
- const compiledPath = portableContextPath(this.rootPath, request.preparation);
987
+ const compiledPath = preparationPortableContextPath(asPreparationDataDir(prepDataDir), request.preparation);
1360
988
  if (!existsSync(compiledPath)) {
1361
989
  throw new Error(`Portable Context for Preparation "${request.preparation}" does not exist.`);
1362
990
  }
1363
991
  resetCompiledGeneratedState(compiledPath, request.scope);
992
+ // Reset wipes generated state, including saved compile/test/readiness records.
993
+ this.compileRunCache.invalidatePreparation(prepDataDir, request.preparation);
994
+ this.testRunCache.invalidatePreparation(prepDataDir, request.preparation);
995
+ this.readinessCache.invalidatePreparation(prepDataDir, request.preparation);
1364
996
  return ResetResultSchema.parse({
1365
997
  kind: "interf-reset-result",
1366
998
  version: 1,
@@ -1371,16 +1003,16 @@ export class LocalServiceRuntime {
1371
1003
  message: `Reset ${request.scope} state for Preparation ${request.preparation}.`,
1372
1004
  });
1373
1005
  }
1374
- async createMethodAuthoringRun(requestValue, jobType = "method-authoring") {
1006
+ async createMethodAuthoringRun(prepDataDir, requestValue, jobType = "method-authoring") {
1375
1007
  const request = MethodAuthoringCreateRequestSchema.parse(requestValue);
1376
1008
  const isImprovement = jobType === "method-improvement";
1377
- const job = this.createJobRun({
1009
+ const job = this.createJobRun(prepDataDir, {
1378
1010
  job_type: jobType,
1379
1011
  title: isImprovement ? `Improve Method ${request.method_id}` : `Draft Method ${request.method_id}`,
1380
1012
  preparation: request.preparation ?? null,
1381
1013
  method: request.method_id,
1382
1014
  source_path: request.source_folder_path,
1383
- output_path: join(this.rootPath, "interf", "methods", request.method_id),
1015
+ output_path: preparationMethodPackagePath(asPreparationDataDir(prepDataDir), request.method_id),
1384
1016
  steps: [
1385
1017
  {
1386
1018
  id: "inspect-source",
@@ -1409,7 +1041,7 @@ export class LocalServiceRuntime {
1409
1041
  },
1410
1042
  ],
1411
1043
  });
1412
- this.appendJobRunEvent(job.run_id, {
1044
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1413
1045
  type: "step.started",
1414
1046
  step_id: "inspect-source",
1415
1047
  message: isImprovement ? "Inspecting source files for Method improvement." : "Inspecting source files for Method drafting.",
@@ -1419,7 +1051,7 @@ export class LocalServiceRuntime {
1419
1051
  checks: request.checks.length,
1420
1052
  },
1421
1053
  });
1422
- this.appendJobRunEvent(job.run_id, {
1054
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1423
1055
  type: "step.completed",
1424
1056
  step_id: "inspect-source",
1425
1057
  message: "Source context is ready.",
@@ -1428,7 +1060,7 @@ export class LocalServiceRuntime {
1428
1060
  checks: request.checks.length,
1429
1061
  },
1430
1062
  });
1431
- this.appendJobRunEvent(job.run_id, {
1063
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1432
1064
  type: "step.started",
1433
1065
  step_id: "draft-package",
1434
1066
  message: isImprovement ? "Improving Method package." : "Drafting Method package.",
@@ -1438,23 +1070,23 @@ export class LocalServiceRuntime {
1438
1070
  task_prompt: request.task_prompt,
1439
1071
  },
1440
1072
  });
1441
- void this.runMethodAuthoringInBackground(request, job.run_id);
1442
- return this.getJob(job.run_id) ?? job;
1073
+ void this.runMethodAuthoringInBackground(prepDataDir, request, job.run_id);
1074
+ return this.getJob(prepDataDir, job.run_id) ?? job;
1443
1075
  }
1444
- listPortableContexts() {
1445
- return listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath))
1446
- .map((preparation) => this.getPortableContext(preparation.name))
1076
+ listPortableContexts(prepDataDir) {
1077
+ return listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir))
1078
+ .map((preparation) => this.getPortableContext(prepDataDir, preparation.name))
1447
1079
  .filter((context) => context !== null);
1448
1080
  }
1449
- getPortableContext(preparationName) {
1450
- const preparation = findSourcePreparationConfig(loadSourceFolderConfig(this.rootPath), preparationName);
1081
+ getPortableContext(prepDataDir, preparationName) {
1082
+ const preparation = findSourcePreparationConfig(loadSourceFolderConfig(prepDataDir), preparationName);
1451
1083
  if (!preparation)
1452
1084
  return null;
1453
- const path = portableContextPath(this.rootPath, preparation.name);
1085
+ const path = preparationPortableContextPath(asPreparationDataDir(prepDataDir), preparation.name);
1454
1086
  const config = readInterfConfig(path);
1455
- const compileRuns = this.listCompileRunsForPreparation(preparation.name);
1456
- const testRuns = this.listTestRunsForPreparation(preparation.name);
1457
- const readiness = this.computePreparationReadiness(preparation);
1087
+ const compileRuns = this.listCompileRunsForPreparation(prepDataDir, preparation.name);
1088
+ const testRuns = this.listTestRunsForPreparation(prepDataDir, preparation.name);
1089
+ const readiness = this.computePreparationReadiness(prepDataDir, preparation);
1458
1090
  const method = config?.method ?? methodIdForSourcePreparationConfig(preparation);
1459
1091
  return PortableContextResourceSchema.parse({
1460
1092
  preparation: preparation.name,
@@ -1467,52 +1099,65 @@ export class LocalServiceRuntime {
1467
1099
  artifacts: uniqueArtifacts(compileRuns[0]?.stages.flatMap((stage) => stage.artifacts) ?? []),
1468
1100
  });
1469
1101
  }
1470
- listCompileRuns() {
1471
- return newestCompileFirst(listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath))
1472
- .flatMap((preparation) => this.listCompileRunsForPreparation(preparation.name))).map((run) => CompileRunResourceSchema.parse({ run }));
1473
- }
1474
- listCompileRunsForPreparation(preparationName) {
1475
- const compiledPath = portableContextPath(this.rootPath, preparationName);
1476
- return newestCompileFirst(listJsonFiles(compileRunsRoot(compiledPath))
1477
- .map(readCompileRunAt)
1478
- .filter((run) => run !== null));
1479
- }
1480
- getCompileRun(runId) {
1481
- for (const resource of this.listCompileRuns()) {
1102
+ listCompileRuns(prepDataDir) {
1103
+ return byCreatedAtDesc(listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir))
1104
+ .flatMap((preparation) => this.listCompileRunsForPreparation(prepDataDir, preparation.name))).map((run) => CompileRunResourceSchema.parse({ run }));
1105
+ }
1106
+ listCompileRunsForPreparation(prepDataDir, preparationName) {
1107
+ return this.compileRunCache.get(prepDataDir, preparationName, () => {
1108
+ const compiledPath = preparationPortableContextPath(asPreparationDataDir(prepDataDir), preparationName);
1109
+ return byCreatedAtDesc(listJsonFiles(compileRunsRoot(compiledPath))
1110
+ .map(readCompileRunAt)
1111
+ .filter((run) => run !== null));
1112
+ }, (run) => run.run_id);
1113
+ }
1114
+ getCompileRun(prepDataDir, runId) {
1115
+ // Fast path: if the runId was seen during a recent listing, look up
1116
+ // its owning preparation directly and return that preparation's
1117
+ // cached entry instead of scanning every preparation on disk.
1118
+ const known = this.compileRunCache.preparationFor(prepDataDir, runId);
1119
+ if (known) {
1120
+ const found = this.listCompileRunsForPreparation(prepDataDir, known).find((entry) => entry.run_id === runId);
1121
+ if (found)
1122
+ return CompileRunResourceSchema.parse({ run: found });
1123
+ }
1124
+ // Slow path: scan all preparations. Falls through after a cache
1125
+ // miss for an in-flight run created before this process restarted.
1126
+ for (const resource of this.listCompileRuns(prepDataDir)) {
1482
1127
  if (resource.run.run_id === runId)
1483
1128
  return resource;
1484
1129
  }
1485
1130
  return null;
1486
1131
  }
1487
- getCompileRunEvents(runId) {
1488
- return this.getCompileRun(runId)?.run.events ?? null;
1132
+ getCompileRunEvents(prepDataDir, runId) {
1133
+ return this.getCompileRun(prepDataDir, runId)?.run.events ?? null;
1489
1134
  }
1490
- getCompileRunProof(runId) {
1491
- const run = this.getCompileRun(runId)?.run;
1135
+ getCompileRunProof(prepDataDir, runId) {
1136
+ const run = this.getCompileRun(prepDataDir, runId)?.run;
1492
1137
  if (!run)
1493
1138
  return null;
1494
1139
  return run.stages
1495
1140
  .map((stage) => stage.latest_proof ?? null)
1496
1141
  .filter((proof) => proof !== null);
1497
1142
  }
1498
- getCompileRunArtifacts(runId) {
1499
- const run = this.getCompileRun(runId)?.run;
1143
+ getCompileRunArtifacts(prepDataDir, runId) {
1144
+ const run = this.getCompileRun(prepDataDir, runId)?.run;
1500
1145
  if (!run)
1501
1146
  return null;
1502
1147
  return uniqueArtifacts(run.stages.flatMap((stage) => stage.artifacts));
1503
1148
  }
1504
- async createCompileRun(requestValue) {
1149
+ async createCompileRun(prepDataDir, requestValue) {
1505
1150
  const request = CompileRunCreateRequestSchema.parse(requestValue);
1506
- const preparationConfig = this.resolvePreparationConfig(request.preparation, {
1151
+ const preparationConfig = this.resolvePreparationConfig(prepDataDir, request.preparation, {
1507
1152
  method: request.method,
1508
1153
  max_attempts: request.max_attempts,
1509
1154
  max_loops: request.max_loops,
1510
1155
  });
1511
- const compiledPath = this.ensureCompiledForRun(preparationConfig);
1156
+ const compiledPath = this.ensureCompiledForRun(prepDataDir, preparationConfig);
1512
1157
  const runId = createRunId("compile");
1513
1158
  const now = new Date().toISOString();
1514
1159
  const method = getCompiledMethod(requireSelectedMethod(preparationConfig), {
1515
- sourcePath: this.rootPath,
1160
+ prepDataDir,
1516
1161
  });
1517
1162
  const stageTotal = method.stages.length;
1518
1163
  const run = CompileRunSchema.parse({
@@ -1523,7 +1168,7 @@ export class LocalServiceRuntime {
1523
1168
  preparation: preparationConfig.name,
1524
1169
  method: method.id,
1525
1170
  backend: "native",
1526
- source_path: resolveSourcePreparationPath(this.rootPath, preparationConfig),
1171
+ source_path: resolveSourcePreparationPath(prepDataDir, preparationConfig),
1527
1172
  portable_context_path: compiledPath,
1528
1173
  created_at: now,
1529
1174
  started_at: now,
@@ -1549,8 +1194,14 @@ export class LocalServiceRuntime {
1549
1194
  }),
1550
1195
  events: [],
1551
1196
  });
1552
- this.writeCompileRun(compiledPath, run);
1553
- await this.recordCompileRunEvent(compiledPath, runId, {
1197
+ this.writeCompileRun(prepDataDir, compiledPath, run);
1198
+ this.activeCompileRuns.set(runId, {
1199
+ prepDataDir,
1200
+ compiledPath,
1201
+ preparation: preparationConfig.name,
1202
+ cancelled: false,
1203
+ });
1204
+ await this.recordCompileRunEvent(prepDataDir, compiledPath, runId, {
1554
1205
  type: "run.started",
1555
1206
  event_id: createRunEventId("event"),
1556
1207
  run_id: runId,
@@ -1561,11 +1212,11 @@ export class LocalServiceRuntime {
1561
1212
  backend: "native",
1562
1213
  });
1563
1214
  const sink = {
1564
- emit: (event) => this.recordCompileRunEvent(compiledPath, runId, event),
1215
+ emit: (event) => this.recordCompileRunEvent(prepDataDir, compiledPath, runId, event),
1565
1216
  };
1566
- void this.runCompileInBackground(request, {
1217
+ void this.runCompileInBackground(prepDataDir, request, {
1567
1218
  runId,
1568
- sourcePath: this.rootPath,
1219
+ sourcePath: prepDataDir,
1569
1220
  compiledPath,
1570
1221
  preparationConfig,
1571
1222
  events: sink,
@@ -1573,23 +1224,155 @@ export class LocalServiceRuntime {
1573
1224
  const saved = this.readCompileRun(compiledPath, runId) ?? run;
1574
1225
  return CompileRunResourceSchema.parse({ run: saved });
1575
1226
  }
1576
- listTestRuns() {
1577
- return newestFirst(listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath))
1578
- .flatMap((preparation) => this.listTestRunsForPreparation(preparation.name)));
1227
+ /**
1228
+ * Cancel an in-flight compile run. Marks the persisted record as
1229
+ * `cancelled`, emits a `run.failed` event to capture the cancellation in
1230
+ * the run timeline, and clears the active handle so retries may start a
1231
+ * fresh run. If the run already finished, returns
1232
+ * `{ cancelled: false, reason: "already finished" }` and persists nothing.
1233
+ */
1234
+ cancelCompileRun(runId) {
1235
+ const handle = this.activeCompileRuns.get(runId);
1236
+ if (!handle) {
1237
+ // Either unknown or already terminal. The server route already 404s
1238
+ // unknown ids before calling this, so anything reaching here is a run
1239
+ // we already finalized.
1240
+ return { cancelled: false, reason: "already finished" };
1241
+ }
1242
+ if (handle.cancelled) {
1243
+ return { cancelled: false, reason: "already cancelled" };
1244
+ }
1245
+ const cancelledAt = new Date().toISOString();
1246
+ handle.cancelled = true;
1247
+ handle.cancelledAt = cancelledAt;
1248
+ const current = this.readCompileRun(handle.compiledPath, runId);
1249
+ if (current && current.status !== "succeeded" && current.status !== "failed" && current.status !== "cancelled") {
1250
+ const cancelledRun = {
1251
+ ...current,
1252
+ status: "cancelled",
1253
+ finished_at: current.finished_at ?? cancelledAt,
1254
+ events: [
1255
+ ...current.events,
1256
+ {
1257
+ type: "run.failed",
1258
+ event_id: createRunEventId("event"),
1259
+ run_id: runId,
1260
+ timestamp: cancelledAt,
1261
+ error: "Compile run cancelled by request.",
1262
+ },
1263
+ ],
1264
+ };
1265
+ this.writeCompileRun(handle.prepDataDir, handle.compiledPath, cancelledRun);
1266
+ }
1267
+ return { cancelled: true };
1268
+ }
1269
+ /**
1270
+ * Look up the run id previously associated with this idempotency key in
1271
+ * `prepDataDir`. Returns null when the key is unknown or its TTL has
1272
+ * elapsed. The workspace argument is required so that the same key in
1273
+ * two different workspaces always returns two different runs.
1274
+ */
1275
+ findIdempotentCompileRun(prepDataDir, key) {
1276
+ const resolvedRoot = resolve(prepDataDir);
1277
+ const bucket = this.idempotencyKeyCache.get(resolvedRoot);
1278
+ if (!bucket)
1279
+ return null;
1280
+ const entry = bucket.get(key);
1281
+ if (!entry)
1282
+ return null;
1283
+ if (entry.expiresAt <= Date.now()) {
1284
+ // Opportunistic single-key prune. The bulk prune runs on writes
1285
+ // when the cache crosses the size threshold (see
1286
+ // {@link recordIdempotentCompileRun}).
1287
+ bucket.delete(key);
1288
+ if (bucket.size === 0)
1289
+ this.idempotencyKeyCache.delete(resolvedRoot);
1290
+ return null;
1291
+ }
1292
+ return entry.runId;
1293
+ }
1294
+ /**
1295
+ * Cache the run id created (or returned) for this idempotency key in
1296
+ * `prepDataDir`. Entries expire after `IDEMPOTENCY_TTL_MS`. Pruning
1297
+ * is opportunistic: the previous implementation walked every entry on
1298
+ * every read AND write, which was O(N) per request. Now we only sweep
1299
+ * when the cache grows past {@link IDEMPOTENCY_PRUNE_THRESHOLD}.
1300
+ */
1301
+ recordIdempotentCompileRun(prepDataDir, key, runId) {
1302
+ const resolvedRoot = resolve(prepDataDir);
1303
+ let bucket = this.idempotencyKeyCache.get(resolvedRoot);
1304
+ if (!bucket) {
1305
+ bucket = new Map();
1306
+ this.idempotencyKeyCache.set(resolvedRoot, bucket);
1307
+ }
1308
+ bucket.set(key, {
1309
+ runId,
1310
+ expiresAt: Date.now() + IDEMPOTENCY_TTL_MS,
1311
+ });
1312
+ if (this.totalIdempotencyEntries() > IDEMPOTENCY_PRUNE_THRESHOLD) {
1313
+ this.pruneIdempotencyKeyCache();
1314
+ }
1579
1315
  }
1580
- listTestRunsForPreparation(preparationName) {
1581
- const compiledPath = portableContextPath(this.rootPath, preparationName);
1582
- return newestFirst(listJsonFiles(testRunsRoot(compiledPath))
1583
- .map(readTestRunAt)
1584
- .filter((run) => run !== null));
1316
+ /** Total cached idempotency entries across all workspaces. */
1317
+ totalIdempotencyEntries() {
1318
+ let total = 0;
1319
+ for (const bucket of this.idempotencyKeyCache.values())
1320
+ total += bucket.size;
1321
+ return total;
1322
+ }
1323
+ pruneIdempotencyKeyCache() {
1324
+ const now = Date.now();
1325
+ for (const [prepDataDir, bucket] of this.idempotencyKeyCache) {
1326
+ for (const [key, entry] of bucket) {
1327
+ if (entry.expiresAt <= now)
1328
+ bucket.delete(key);
1329
+ }
1330
+ if (bucket.size === 0)
1331
+ this.idempotencyKeyCache.delete(prepDataDir);
1332
+ }
1585
1333
  }
1586
- getTestRun(runId) {
1587
- return this.listTestRuns().find((run) => run.run_id === runId) ?? null;
1334
+ /**
1335
+ * Test seam: force the cached entry for `key` in `prepDataDir` to be
1336
+ * expired so the next lookup returns null. Returns true when an entry was
1337
+ * found and expired. Tests use this in place of fake timers because the
1338
+ * idempotency TTL is one hour and faking `Date.now()` would destabilize
1339
+ * unrelated runtime state.
1340
+ */
1341
+ expireIdempotencyKeyForTesting(prepDataDir, key) {
1342
+ const bucket = this.idempotencyKeyCache.get(resolve(prepDataDir));
1343
+ if (!bucket)
1344
+ return false;
1345
+ const entry = bucket.get(key);
1346
+ if (!entry)
1347
+ return false;
1348
+ entry.expiresAt = Date.now() - 1;
1349
+ return true;
1350
+ }
1351
+ listTestRuns(prepDataDir) {
1352
+ return newestFirst(listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir))
1353
+ .flatMap((preparation) => this.listTestRunsForPreparation(prepDataDir, preparation.name)));
1354
+ }
1355
+ listTestRunsForPreparation(prepDataDir, preparationName) {
1356
+ return this.testRunCache.get(prepDataDir, preparationName, () => {
1357
+ const compiledPath = preparationPortableContextPath(asPreparationDataDir(prepDataDir), preparationName);
1358
+ return newestFirst(listJsonFiles(testRunsRoot(compiledPath))
1359
+ .map(readTestRunAt)
1360
+ .filter((run) => run !== null));
1361
+ }, (run) => run.run_id);
1362
+ }
1363
+ getTestRun(prepDataDir, runId) {
1364
+ const known = this.testRunCache.preparationFor(prepDataDir, runId);
1365
+ if (known) {
1366
+ const found = this.listTestRunsForPreparation(prepDataDir, known).find((run) => run.run_id === runId);
1367
+ if (found)
1368
+ return found;
1369
+ }
1370
+ return this.listTestRuns(prepDataDir).find((run) => run.run_id === runId) ?? null;
1588
1371
  }
1589
- async createTestRun(requestValue) {
1372
+ async createTestRun(prepDataDir, requestValue) {
1590
1373
  const request = TestRunCreateRequestSchema.parse(requestValue);
1591
- const preparationConfig = this.resolvePreparationConfig(request.preparation);
1592
- const compiledPath = portableContextPath(this.rootPath, preparationConfig.name);
1374
+ const preparationConfig = this.resolvePreparationConfig(prepDataDir, request.preparation);
1375
+ const compiledPath = preparationPortableContextPath(asPreparationDataDir(prepDataDir), preparationConfig.name);
1593
1376
  const compiledTarget = createCompiledTestTarget(compiledPath, preparationConfig.name, methodIdForSourcePreparationConfig(preparationConfig) ?? DEFAULT_METHOD_ID);
1594
1377
  const runId = createRunId("test");
1595
1378
  const now = new Date().toISOString();
@@ -1598,31 +1381,42 @@ export class LocalServiceRuntime {
1598
1381
  status: "running",
1599
1382
  preparation: preparationConfig.name,
1600
1383
  mode: request.mode,
1601
- source_path: this.rootPath,
1384
+ source_path: prepDataDir,
1602
1385
  portable_context_path: compiledTarget.eligible ? compiledPath : null,
1603
1386
  started_at: now,
1604
1387
  readiness_run: null,
1605
1388
  events: [],
1606
1389
  });
1607
- this.writeTestRun(compiledPath, initial);
1608
- void this.runTestInBackground(request, {
1390
+ this.writeTestRun(prepDataDir, compiledPath, initial);
1391
+ void this.runTestInBackground(prepDataDir, request, {
1609
1392
  runId,
1610
- sourcePath: this.rootPath,
1393
+ sourcePath: prepDataDir,
1611
1394
  compiledPath,
1612
1395
  preparationConfig,
1613
1396
  }, initial);
1614
1397
  return initial;
1615
1398
  }
1616
- async runCompileInBackground(request, context) {
1399
+ async runCompileInBackground(prepDataDir, request, context) {
1400
+ this.beginActiveRun();
1617
1401
  try {
1618
1402
  if (!this.handlers.createCompileRun) {
1619
1403
  throw new Error("No compile-run handler is configured for this local service.");
1620
1404
  }
1621
1405
  const result = LocalRunHandlerResultSchema.parse(await this.handlers.createCompileRun(request, context));
1622
- this.refreshCompileRunFromRuntime(context.compiledPath, context.runId);
1623
- await this.emitRuntimeDerivedEvents(context.compiledPath, context.runId);
1406
+ const wasCancelled = this.activeCompileRuns.get(context.runId)?.cancelled === true;
1407
+ if (wasCancelled) {
1408
+ // The run was cancelled while the handler was still running. The
1409
+ // cancellation path already wrote a `cancelled` record; just refresh
1410
+ // observability and skip emitting a second terminal event.
1411
+ this.refreshCompileRunFromRuntime(prepDataDir, context.compiledPath, context.runId);
1412
+ await this.emitRuntimeDerivedEvents(prepDataDir, context.compiledPath, context.runId);
1413
+ await this.recordCompileRunEvent(prepDataDir, context.compiledPath, context.runId, this.readinessUpdatedEvent(context.runId, context.preparationConfig.name, this.computePreparationReadiness(prepDataDir, context.preparationConfig)));
1414
+ return;
1415
+ }
1416
+ this.refreshCompileRunFromRuntime(prepDataDir, context.compiledPath, context.runId);
1417
+ await this.emitRuntimeDerivedEvents(prepDataDir, context.compiledPath, context.runId);
1624
1418
  if (!result.ok) {
1625
- await this.recordCompileRunEvent(context.compiledPath, context.runId, {
1419
+ await this.recordCompileRunEvent(prepDataDir, context.compiledPath, context.runId, {
1626
1420
  type: "run.failed",
1627
1421
  event_id: createRunEventId("event"),
1628
1422
  run_id: context.runId,
@@ -1631,7 +1425,7 @@ export class LocalServiceRuntime {
1631
1425
  });
1632
1426
  }
1633
1427
  else {
1634
- await this.recordCompileRunEvent(context.compiledPath, context.runId, {
1428
+ await this.recordCompileRunEvent(prepDataDir, context.compiledPath, context.runId, {
1635
1429
  type: "run.completed",
1636
1430
  event_id: createRunEventId("event"),
1637
1431
  run_id: context.runId,
@@ -1639,26 +1433,31 @@ export class LocalServiceRuntime {
1639
1433
  summary: "Portable context ready.",
1640
1434
  });
1641
1435
  }
1642
- await this.recordCompileRunEvent(context.compiledPath, context.runId, this.readinessUpdatedEvent(context.runId, context.preparationConfig.name, this.computePreparationReadiness(context.preparationConfig)));
1436
+ await this.recordCompileRunEvent(prepDataDir, context.compiledPath, context.runId, this.readinessUpdatedEvent(context.runId, context.preparationConfig.name, this.computePreparationReadiness(prepDataDir, context.preparationConfig)));
1643
1437
  }
1644
1438
  catch (error) {
1645
- await this.recordCompileRunEvent(context.compiledPath, context.runId, {
1439
+ await this.recordCompileRunEvent(prepDataDir, context.compiledPath, context.runId, {
1646
1440
  type: "run.failed",
1647
1441
  event_id: createRunEventId("event"),
1648
1442
  run_id: context.runId,
1649
1443
  timestamp: createRunEventTimestamp(),
1650
1444
  error: error instanceof Error ? error.message : String(error),
1651
1445
  });
1652
- await this.recordCompileRunEvent(context.compiledPath, context.runId, this.readinessUpdatedEvent(context.runId, context.preparationConfig.name, this.computePreparationReadiness(context.preparationConfig)));
1446
+ await this.recordCompileRunEvent(prepDataDir, context.compiledPath, context.runId, this.readinessUpdatedEvent(context.runId, context.preparationConfig.name, this.computePreparationReadiness(prepDataDir, context.preparationConfig)));
1447
+ }
1448
+ finally {
1449
+ this.activeCompileRuns.delete(context.runId);
1450
+ this.endActiveRun();
1653
1451
  }
1654
1452
  }
1655
- async runTestInBackground(request, context, initial) {
1453
+ async runTestInBackground(prepDataDir, request, context, initial) {
1454
+ this.beginActiveRun();
1656
1455
  try {
1657
1456
  if (!this.handlers.createTestRun) {
1658
1457
  throw new Error("No test-run handler is configured for this local service.");
1659
1458
  }
1660
1459
  const result = LocalRunHandlerResultSchema.parse(await this.handlers.createTestRun(request, context));
1661
- const readinessRun = result.readiness_run ?? this.readLatestReadinessRun(context.preparationConfig.name);
1460
+ const readinessRun = result.readiness_run ?? this.readLatestReadinessRun(prepDataDir, context.preparationConfig.name);
1662
1461
  const resultEvent = readinessRun
1663
1462
  ? this.checksEvaluatedEvent(context.runId, readinessRun)
1664
1463
  : null;
@@ -1670,8 +1469,8 @@ export class LocalServiceRuntime {
1670
1469
  events: resultEvent ? [resultEvent] : [],
1671
1470
  ...(!result.ok ? { error: result.error ?? "Readiness check failed." } : {}),
1672
1471
  });
1673
- this.writeTestRun(context.compiledPath, nextWithoutReadiness);
1674
- const readiness = this.computePreparationReadiness(context.preparationConfig);
1472
+ this.writeTestRun(prepDataDir, context.compiledPath, nextWithoutReadiness);
1473
+ const readiness = this.computePreparationReadiness(prepDataDir, context.preparationConfig);
1675
1474
  const next = TestRunResourceSchema.parse({
1676
1475
  ...nextWithoutReadiness,
1677
1476
  readiness,
@@ -1680,7 +1479,7 @@ export class LocalServiceRuntime {
1680
1479
  this.readinessUpdatedEvent(context.runId, context.preparationConfig.name, readiness),
1681
1480
  ],
1682
1481
  });
1683
- this.writeTestRun(context.compiledPath, next);
1482
+ this.writeTestRun(prepDataDir, context.compiledPath, next);
1684
1483
  }
1685
1484
  catch (error) {
1686
1485
  const failedWithoutReadiness = TestRunResourceSchema.parse({
@@ -1689,23 +1488,32 @@ export class LocalServiceRuntime {
1689
1488
  finished_at: new Date().toISOString(),
1690
1489
  error: error instanceof Error ? error.message : String(error),
1691
1490
  });
1692
- this.writeTestRun(context.compiledPath, failedWithoutReadiness);
1693
- const readiness = this.computePreparationReadiness(context.preparationConfig);
1491
+ this.writeTestRun(prepDataDir, context.compiledPath, failedWithoutReadiness);
1492
+ const readiness = this.computePreparationReadiness(prepDataDir, context.preparationConfig);
1694
1493
  const next = TestRunResourceSchema.parse({
1695
1494
  ...failedWithoutReadiness,
1696
1495
  readiness,
1697
1496
  events: [this.readinessUpdatedEvent(context.runId, context.preparationConfig.name, readiness)],
1698
1497
  });
1699
- this.writeTestRun(context.compiledPath, next);
1498
+ this.writeTestRun(prepDataDir, context.compiledPath, next);
1499
+ }
1500
+ finally {
1501
+ this.endActiveRun();
1700
1502
  }
1701
1503
  }
1702
- async runReadinessCheckDraftInBackground(request, runId) {
1504
+ async runReadinessCheckDraftInBackground(prepDataDir, request, runId) {
1505
+ this.beginActiveRun();
1506
+ return this.runReadinessCheckDraftInBackgroundInner(prepDataDir, request, runId).finally(() => {
1507
+ this.endActiveRun();
1508
+ });
1509
+ }
1510
+ async runReadinessCheckDraftInBackgroundInner(prepDataDir, request, runId) {
1703
1511
  try {
1704
1512
  if (!this.handlers.createReadinessCheckDraft) {
1705
1513
  throw new Error("No readiness-check-draft handler is configured for this local service.");
1706
1514
  }
1707
- const result = ReadinessCheckDraftResultSchema.parse(await this.handlers.createReadinessCheckDraft(request, this.jobRunContext(runId)));
1708
- this.appendJobRunEvent(runId, {
1515
+ const result = ReadinessCheckDraftResultSchema.parse(await this.handlers.createReadinessCheckDraft(request, this.jobRunContext(prepDataDir, runId)));
1516
+ this.appendJobRunEvent(prepDataDir, runId, {
1709
1517
  type: "step.completed",
1710
1518
  step_id: "agent-draft",
1711
1519
  message: `Drafted ${result.checks.length} readiness checks.`,
@@ -1713,7 +1521,7 @@ export class LocalServiceRuntime {
1713
1521
  checks: result.checks,
1714
1522
  },
1715
1523
  });
1716
- this.appendJobRunEvent(runId, {
1524
+ this.appendJobRunEvent(prepDataDir, runId, {
1717
1525
  type: "step.started",
1718
1526
  step_id: "normalize-checks",
1719
1527
  message: "Normalizing drafted readiness checks into saved check records.",
@@ -1721,7 +1529,7 @@ export class LocalServiceRuntime {
1721
1529
  checks: result.checks.length,
1722
1530
  },
1723
1531
  });
1724
- this.appendJobRunEvent(runId, {
1532
+ this.appendJobRunEvent(prepDataDir, runId, {
1725
1533
  type: "step.completed",
1726
1534
  step_id: "normalize-checks",
1727
1535
  message: `${result.checks.length} readiness checks ready for review.`,
@@ -1729,15 +1537,15 @@ export class LocalServiceRuntime {
1729
1537
  checks: result.checks.length,
1730
1538
  },
1731
1539
  });
1732
- this.setJobRunResult(runId, result);
1733
- this.appendJobRunEvent(runId, {
1540
+ this.setJobRunResult(prepDataDir, runId, result);
1541
+ this.appendJobRunEvent(prepDataDir, runId, {
1734
1542
  type: "job.completed",
1735
1543
  message: `Drafted ${result.checks.length} readiness checks.`,
1736
1544
  });
1737
1545
  }
1738
1546
  catch (error) {
1739
1547
  const message = error instanceof Error ? error.message : String(error);
1740
- this.appendJobRunEvent(runId, {
1548
+ this.appendJobRunEvent(prepDataDir, runId, {
1741
1549
  type: "step.failed",
1742
1550
  step_id: "agent-draft",
1743
1551
  message,
@@ -1745,20 +1553,26 @@ export class LocalServiceRuntime {
1745
1553
  error: message,
1746
1554
  },
1747
1555
  });
1748
- this.appendJobRunEvent(runId, {
1556
+ this.appendJobRunEvent(prepDataDir, runId, {
1749
1557
  type: "job.failed",
1750
1558
  message,
1751
1559
  });
1752
1560
  }
1753
1561
  }
1754
- async runMethodAuthoringInBackground(request, runId) {
1562
+ async runMethodAuthoringInBackground(prepDataDir, request, runId) {
1563
+ this.beginActiveRun();
1564
+ return this.runMethodAuthoringInBackgroundInner(prepDataDir, request, runId).finally(() => {
1565
+ this.endActiveRun();
1566
+ });
1567
+ }
1568
+ async runMethodAuthoringInBackgroundInner(prepDataDir, request, runId) {
1755
1569
  try {
1756
1570
  if (!this.handlers.createMethodAuthoringRun) {
1757
1571
  throw new Error("No Method-authoring handler is configured for this local service.");
1758
1572
  }
1759
- const result = MethodAuthoringResultSchema.parse(await this.handlers.createMethodAuthoringRun(request, this.jobRunContext(runId)));
1760
- this.setJobRunResult(runId, result);
1761
- this.appendJobRunEvent(runId, {
1573
+ const result = MethodAuthoringResultSchema.parse(await this.handlers.createMethodAuthoringRun(request, this.jobRunContext(prepDataDir, runId)));
1574
+ this.setJobRunResult(prepDataDir, runId, result);
1575
+ this.appendJobRunEvent(prepDataDir, runId, {
1762
1576
  type: result.status === "executor-failed" ? "step.failed" : "step.completed",
1763
1577
  step_id: "draft-package",
1764
1578
  message: result.summary,
@@ -1769,7 +1583,7 @@ export class LocalServiceRuntime {
1769
1583
  shell_path: result.shell_path,
1770
1584
  },
1771
1585
  });
1772
- this.appendJobRunEvent(runId, {
1586
+ this.appendJobRunEvent(prepDataDir, runId, {
1773
1587
  type: "step.started",
1774
1588
  step_id: "validate-package",
1775
1589
  message: "Validating Method package structure and stage contract.",
@@ -1778,7 +1592,7 @@ export class LocalServiceRuntime {
1778
1592
  },
1779
1593
  });
1780
1594
  if (result.status === "updated" || result.status === "no-change") {
1781
- this.appendJobRunEvent(runId, {
1595
+ this.appendJobRunEvent(prepDataDir, runId, {
1782
1596
  type: "step.completed",
1783
1597
  step_id: "validate-package",
1784
1598
  message: result.summary,
@@ -1787,13 +1601,13 @@ export class LocalServiceRuntime {
1787
1601
  validation: result.validation ?? null,
1788
1602
  },
1789
1603
  });
1790
- this.appendJobRunEvent(runId, {
1604
+ this.appendJobRunEvent(prepDataDir, runId, {
1791
1605
  type: "job.completed",
1792
1606
  message: result.summary,
1793
1607
  });
1794
1608
  }
1795
1609
  else {
1796
- this.appendJobRunEvent(runId, {
1610
+ this.appendJobRunEvent(prepDataDir, runId, {
1797
1611
  type: "step.failed",
1798
1612
  step_id: "validate-package",
1799
1613
  message: result.summary,
@@ -1802,7 +1616,7 @@ export class LocalServiceRuntime {
1802
1616
  validation: result.validation ?? null,
1803
1617
  },
1804
1618
  });
1805
- this.appendJobRunEvent(runId, {
1619
+ this.appendJobRunEvent(prepDataDir, runId, {
1806
1620
  type: "job.failed",
1807
1621
  message: result.summary,
1808
1622
  });
@@ -1810,7 +1624,7 @@ export class LocalServiceRuntime {
1810
1624
  }
1811
1625
  catch (error) {
1812
1626
  const message = error instanceof Error ? error.message : String(error);
1813
- this.appendJobRunEvent(runId, {
1627
+ this.appendJobRunEvent(prepDataDir, runId, {
1814
1628
  type: "step.failed",
1815
1629
  step_id: "draft-package",
1816
1630
  message,
@@ -1818,29 +1632,29 @@ export class LocalServiceRuntime {
1818
1632
  error: message,
1819
1633
  },
1820
1634
  });
1821
- this.appendJobRunEvent(runId, {
1635
+ this.appendJobRunEvent(prepDataDir, runId, {
1822
1636
  type: "job.failed",
1823
1637
  message,
1824
1638
  });
1825
1639
  }
1826
1640
  }
1827
- jobRunContext(runId) {
1641
+ jobRunContext(prepDataDir, runId) {
1828
1642
  return {
1829
1643
  runId,
1830
- sourcePath: this.rootPath,
1644
+ sourcePath: prepDataDir,
1831
1645
  emit: (event) => {
1832
- this.appendJobRunEvent(runId, event);
1646
+ this.appendJobRunEvent(prepDataDir, runId, event);
1833
1647
  },
1834
1648
  };
1835
1649
  }
1836
- defaultPreparationName() {
1837
- const preparation = listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath))[0];
1650
+ defaultPreparationName(prepDataDir) {
1651
+ const preparation = listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir))[0];
1838
1652
  if (!preparation) {
1839
1653
  throw new Error("No Preparation is saved in this control plane folder.");
1840
1654
  }
1841
1655
  return preparation.name;
1842
1656
  }
1843
- async planActionProposal(request) {
1657
+ async planActionProposal(prepDataDir, request) {
1844
1658
  if (!this.handlers.planActionProposal) {
1845
1659
  return ActionProposalPlanSchema.parse({
1846
1660
  action_type: "clarification",
@@ -1848,15 +1662,15 @@ export class LocalServiceRuntime {
1848
1662
  assistant_message: "No local action planner is configured for this Interf Workspace.",
1849
1663
  });
1850
1664
  }
1851
- const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath));
1665
+ const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir));
1852
1666
  let rawPlan;
1853
1667
  try {
1854
1668
  rawPlan = await this.handlers.planActionProposal(request, {
1855
- sourcePath: this.rootPath,
1669
+ sourcePath: prepDataDir,
1856
1670
  preparations,
1857
1671
  preparationHealth: preparations.map((preparation) => {
1858
1672
  const readinessChecks = preparation.checks?.length ?? 0;
1859
- const portableContextReady = hasCompiledTestTarget(this.rootPath, preparation);
1673
+ const portableContextReady = hasCompiledTestTarget(prepDataDir, preparation);
1860
1674
  return {
1861
1675
  name: preparation.name,
1862
1676
  readiness_checks: readinessChecks,
@@ -1869,8 +1683,8 @@ export class LocalServiceRuntime {
1869
1683
  : ["prepare"],
1870
1684
  };
1871
1685
  }),
1872
- sourceFolders: listSourceFolderChoices(this.rootPath),
1873
- recentProposals: this.listActionProposals().slice(0, 5),
1686
+ sourceFolders: listSourceFolderChoices(prepDataDir),
1687
+ recentProposals: this.listActionProposals(prepDataDir).slice(0, 5),
1874
1688
  });
1875
1689
  }
1876
1690
  catch {
@@ -1927,7 +1741,7 @@ export class LocalServiceRuntime {
1927
1741
  error: null,
1928
1742
  });
1929
1743
  }
1930
- async buildActionProposal(request) {
1744
+ async buildActionProposal(prepDataDir, request) {
1931
1745
  const structuredPreparationSetup = PreparationSetupActionValuesSchema.safeParse(request.values);
1932
1746
  const structuredMethodAuthoring = MethodAuthoringActionValuesSchema.safeParse(request.values);
1933
1747
  const structuredActionType = actionTypeFromValues(request.values);
@@ -1952,7 +1766,7 @@ export class LocalServiceRuntime {
1952
1766
  action_type: structuredPlanActionType,
1953
1767
  ...(request.preparation ? { preparation: request.preparation } : {}),
1954
1768
  })
1955
- : await this.planActionProposal(request);
1769
+ : await this.planActionProposal(prepDataDir, request);
1956
1770
  const actionType = structuredPlanActionType ?? plan.action_type;
1957
1771
  if (directServiceEndpointForAction(actionType)) {
1958
1772
  return this.directServiceActionClarification({
@@ -2003,13 +1817,13 @@ export class LocalServiceRuntime {
2003
1817
  const requestedPreparationName = plan.preparation ?? request.preparation ?? null;
2004
1818
  const fallbackPreparation = requestedPreparationName
2005
1819
  ? null
2006
- : listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath))[0] ?? null;
1820
+ : listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir))[0] ?? null;
2007
1821
  const preparationConfig = requestedPreparationName
2008
- ? this.resolvePreparationConfig(requestedPreparationName)
1822
+ ? this.resolvePreparationConfig(prepDataDir, requestedPreparationName)
2009
1823
  : fallbackPreparation;
2010
1824
  const preparationPath = preparationConfig
2011
- ? resolveSourcePreparationPath(this.rootPath, preparationConfig)
2012
- : resolveConfiguredSourceFolderPath(this.rootPath) ?? this.rootPath;
1825
+ ? resolveSourcePreparationPath(prepDataDir, preparationConfig)
1826
+ : resolveConfiguredSourceFolderPath(prepDataDir) ?? prepDataDir;
2013
1827
  const requestedMethodId = stringValue(request.values, "method_id") ??
2014
1828
  stringValue(request.values, "method");
2015
1829
  const plannedMethodId = plan.method ??
@@ -2057,9 +1871,9 @@ export class LocalServiceRuntime {
2057
1871
  error: null,
2058
1872
  });
2059
1873
  }
2060
- const preparationConfig = this.resolvePreparationConfig(plan.preparation ?? request.preparation ?? this.defaultPreparationName());
1874
+ const preparationConfig = this.resolvePreparationConfig(prepDataDir, plan.preparation ?? request.preparation ?? this.defaultPreparationName(prepDataDir));
2061
1875
  const proposalActionType = ActionProposalTypeSchema.parse(actionType);
2062
- const preparationPath = resolveSourcePreparationPath(this.rootPath, preparationConfig);
1876
+ const preparationPath = resolveSourcePreparationPath(prepDataDir, preparationConfig);
2063
1877
  const requestedMethodId = stringValue(request.values, "method_id") ??
2064
1878
  stringValue(request.values, "method");
2065
1879
  const plannedMethodId = plan.method ??
@@ -2093,7 +1907,7 @@ export class LocalServiceRuntime {
2093
1907
  if (actionType === "test") {
2094
1908
  const requestedMode = testModeFromValues(proposalValues);
2095
1909
  const hasReadinessChecks = (preparationConfig.checks ?? []).length > 0;
2096
- const portableContextReady = hasCompiledTestTarget(this.rootPath, preparationConfig);
1910
+ const portableContextReady = hasCompiledTestTarget(prepDataDir, preparationConfig);
2097
1911
  if (!hasReadinessChecks) {
2098
1912
  return clarifyResolvedAction({
2099
1913
  title: `Add readiness checks for ${preparationConfig.name}`,
@@ -2101,7 +1915,7 @@ export class LocalServiceRuntime {
2101
1915
  assistantMessage: `Preparation "${preparationConfig.name}" does not have saved readiness checks yet. Ask me to draft readiness checks after the Source Folder is prepared, or add readiness guidance first.`,
2102
1916
  });
2103
1917
  }
2104
- if (!portableContextReady && requestedMode !== "raw") {
1918
+ if (!portableContextReady && requestedMode !== "source-files") {
2105
1919
  return clarifyResolvedAction({
2106
1920
  title: `Prepare ${preparationConfig.name} first`,
2107
1921
  summary: "Readiness checks need portable context unless you explicitly ask for a source-files-only baseline.",
@@ -2117,7 +1931,7 @@ export class LocalServiceRuntime {
2117
1931
  };
2118
1932
  }
2119
1933
  if (actionType === "test") {
2120
- const defaultMode = hasCompiledTestTarget(this.rootPath, preparationConfig) ? "both" : "raw";
1934
+ const defaultMode = hasCompiledTestTarget(prepDataDir, preparationConfig) ? "both" : "source-files";
2121
1935
  return {
2122
1936
  preparation: preparationConfig.name,
2123
1937
  mode: testModeValue(proposalValues, defaultMode),
@@ -2197,26 +2011,26 @@ export class LocalServiceRuntime {
2197
2011
  error: null,
2198
2012
  });
2199
2013
  }
2200
- async submitActionProposal(proposal) {
2014
+ async submitActionProposal(prepDataDir, proposal) {
2201
2015
  if (proposal.action_type === "clarification") {
2202
2016
  throw new Error("Clarification proposals cannot be submitted.");
2203
2017
  }
2204
2018
  if (proposal.action_type === "compile") {
2205
- const resource = await this.createCompileRun(proposal.request);
2019
+ const resource = await this.createCompileRun(prepDataDir, proposal.request);
2206
2020
  return {
2207
2021
  runId: resource.run.run_id,
2208
2022
  runType: "compile-run",
2209
2023
  };
2210
2024
  }
2211
2025
  if (proposal.action_type === "test") {
2212
- const resource = await this.createTestRun(proposal.request);
2026
+ const resource = await this.createTestRun(prepDataDir, proposal.request);
2213
2027
  return {
2214
2028
  runId: resource.run_id,
2215
2029
  runType: "test-run",
2216
2030
  };
2217
2031
  }
2218
2032
  if (proposal.action_type === "readiness-check-draft") {
2219
- const job = await this.createReadinessCheckDraftRun(proposal.request);
2033
+ const job = await this.createReadinessCheckDraftRun(prepDataDir, proposal.request);
2220
2034
  return {
2221
2035
  runId: job.run_id,
2222
2036
  runType: "job-run",
@@ -2226,14 +2040,14 @@ export class LocalServiceRuntime {
2226
2040
  if (directEndpoint) {
2227
2041
  throw new Error(`Action "${proposal.action_type}" must be submitted directly to ${directEndpoint}.`);
2228
2042
  }
2229
- const job = await this.createMethodAuthoringRun(proposal.request, proposal.action_type === "method-improvement" ? "method-improvement" : "method-authoring");
2043
+ const job = await this.createMethodAuthoringRun(prepDataDir, proposal.request, proposal.action_type === "method-improvement" ? "method-improvement" : "method-authoring");
2230
2044
  return {
2231
2045
  runId: job.run_id,
2232
2046
  runType: "job-run",
2233
2047
  };
2234
2048
  }
2235
- resolvePreparationConfig(preparationName, overrides = {}) {
2236
- const preparation = findSourcePreparationConfig(loadSourceFolderConfig(this.rootPath), preparationName);
2049
+ resolvePreparationConfig(prepDataDir, preparationName, overrides = {}) {
2050
+ const preparation = findSourcePreparationConfig(loadSourceFolderConfig(prepDataDir), preparationName);
2237
2051
  if (!preparation) {
2238
2052
  throw new Error(`Preparation "${preparationName}" is not saved in this control plane folder.`);
2239
2053
  }
@@ -2245,49 +2059,59 @@ export class LocalServiceRuntime {
2245
2059
  ...(typeof overrides.max_loops === "number" ? { max_loops: overrides.max_loops } : {}),
2246
2060
  };
2247
2061
  }
2248
- ensureCompiledForRun(preparationConfig) {
2062
+ ensureCompiledForRun(prepDataDir, preparationConfig) {
2249
2063
  const methodId = requireSelectedMethod(preparationConfig);
2250
- const compiledPath = ensurePortableContextScaffold(this.rootPath, preparationConfig.name, methodId);
2064
+ const compiledPath = ensurePortableContextScaffold(prepDataDir, preparationConfig.name, methodId);
2251
2065
  syncCompiledInterfConfigFromSourcePreparationConfig(compiledPath, preparationConfig);
2252
2066
  return compiledPath;
2253
2067
  }
2254
2068
  readCompileRun(compiledPath, runId) {
2255
2069
  return readCompileRunAt(compileRunPath(compiledPath, runId));
2256
2070
  }
2257
- writeCompileRun(compiledPath, run) {
2071
+ writeCompileRun(prepDataDir, compiledPath, run) {
2258
2072
  mkdirSync(compileRunsRoot(compiledPath), { recursive: true });
2259
2073
  writeJsonFile(compileRunPath(compiledPath, run.run_id), CompileRunSchema.parse(run));
2074
+ // Bust per-preparation list + readiness caches so the next read
2075
+ // reflects the write. We invalidate broadly (per-preparation, not
2076
+ // per-record) because list recompute cost is bounded by run count
2077
+ // and the simpler model avoids fan-out bugs.
2078
+ this.compileRunCache.invalidatePreparation(prepDataDir, run.preparation);
2079
+ this.readinessCache.invalidatePreparation(prepDataDir, run.preparation);
2080
+ }
2081
+ writeJobRun(prepDataDir, run) {
2082
+ mkdirSync(localJobsRoot(prepDataDir), { recursive: true });
2083
+ writeJsonFile(localJobPath(prepDataDir, run.run_id), LocalJobRunResourceSchema.parse(run));
2084
+ if (run.preparation) {
2085
+ // Some job runs (readiness-check drafts) flip readiness state.
2086
+ this.readinessCache.invalidatePreparation(prepDataDir, run.preparation);
2087
+ }
2260
2088
  }
2261
- writeJobRun(run) {
2262
- mkdirSync(localJobsRoot(this.rootPath), { recursive: true });
2263
- writeJsonFile(localJobPath(this.rootPath, run.run_id), LocalJobRunResourceSchema.parse(run));
2264
- }
2265
- writeActionProposal(proposal) {
2266
- mkdirSync(actionProposalsRoot(this.rootPath), { recursive: true });
2267
- writeJsonFile(actionProposalPath(this.rootPath, proposal.proposal_id), ActionProposalResourceSchema.parse(proposal));
2089
+ writeActionProposal(prepDataDir, proposal) {
2090
+ mkdirSync(actionProposalsRoot(prepDataDir), { recursive: true });
2091
+ writeJsonFile(actionProposalPath(prepDataDir, proposal.proposal_id), ActionProposalResourceSchema.parse(proposal));
2268
2092
  }
2269
- setJobRunResult(runId, result) {
2270
- const current = this.getJob(runId);
2093
+ setJobRunResult(prepDataDir, runId, result) {
2094
+ const current = this.getJob(prepDataDir, runId);
2271
2095
  if (!current)
2272
2096
  return;
2273
2097
  const normalizedResult = result && typeof result === "object" && !Array.isArray(result)
2274
2098
  ? result
2275
2099
  : { value: result };
2276
- this.writeJobRun(LocalJobRunResourceSchema.parse({
2100
+ this.writeJobRun(prepDataDir, LocalJobRunResourceSchema.parse({
2277
2101
  ...current,
2278
2102
  result: normalizedResult,
2279
2103
  }));
2280
2104
  }
2281
- async recordCompileRunEvent(compiledPath, runId, event) {
2105
+ async recordCompileRunEvent(prepDataDir, compiledPath, runId, event) {
2282
2106
  const current = this.readCompileRun(compiledPath, runId);
2283
2107
  if (!current)
2284
2108
  return;
2285
- this.writeCompileRun(compiledPath, applyEventToCompileRun(current, event));
2109
+ this.writeCompileRun(prepDataDir, compiledPath, applyEventToCompileRun(current, event));
2286
2110
  if (event.type === "stage.passed" || event.type === "stage.failed") {
2287
- this.refreshCompileRunFromRuntime(compiledPath, runId);
2111
+ this.refreshCompileRunFromRuntime(prepDataDir, compiledPath, runId);
2288
2112
  }
2289
2113
  }
2290
- refreshCompileRunFromRuntime(compiledPath, runId) {
2114
+ refreshCompileRunFromRuntime(prepDataDir, compiledPath, runId) {
2291
2115
  const current = this.readCompileRun(compiledPath, runId);
2292
2116
  if (!current)
2293
2117
  return;
@@ -2334,9 +2158,9 @@ export class LocalServiceRuntime {
2334
2158
  }),
2335
2159
  };
2336
2160
  next.latest_proof = [...next.stages].reverse().find((stage) => Boolean(stage.latest_proof))?.latest_proof ?? next.latest_proof;
2337
- this.writeCompileRun(compiledPath, next);
2161
+ this.writeCompileRun(prepDataDir, compiledPath, next);
2338
2162
  }
2339
- async emitRuntimeDerivedEvents(compiledPath, runId) {
2163
+ async emitRuntimeDerivedEvents(prepDataDir, compiledPath, runId) {
2340
2164
  const state = loadState(compiledPath);
2341
2165
  const run = this.readCompileRun(compiledPath, runId);
2342
2166
  if (!state?.stages || !run)
@@ -2347,7 +2171,7 @@ export class LocalServiceRuntime {
2347
2171
  continue;
2348
2172
  const artifacts = stageArtifactRefs(stage.stage_id, stageState.artifacts);
2349
2173
  for (const artifact of artifacts) {
2350
- await this.recordCompileRunEvent(compiledPath, runId, {
2174
+ await this.recordCompileRunEvent(prepDataDir, compiledPath, runId, {
2351
2175
  type: "artifact.written",
2352
2176
  event_id: createRunEventId("event"),
2353
2177
  run_id: runId,
@@ -2356,7 +2180,7 @@ export class LocalServiceRuntime {
2356
2180
  artifact,
2357
2181
  });
2358
2182
  }
2359
- await this.recordCompileRunEvent(compiledPath, runId, {
2183
+ await this.recordCompileRunEvent(prepDataDir, compiledPath, runId, {
2360
2184
  type: "proof.updated",
2361
2185
  event_id: createRunEventId("event"),
2362
2186
  run_id: runId,
@@ -2372,11 +2196,11 @@ export class LocalServiceRuntime {
2372
2196
  });
2373
2197
  }
2374
2198
  }
2375
- readLatestReadinessRun(preparationName) {
2376
- return readSavedReadinessCheckRun(this.rootPath, preparationName);
2199
+ readLatestReadinessRun(prepDataDir, preparationName) {
2200
+ return readSavedReadinessCheckRun(prepDataDir, preparationName);
2377
2201
  }
2378
2202
  checksEvaluatedEvent(runId, readinessRun) {
2379
- const target = readinessRun.compiled ?? readinessRun.raw;
2203
+ const target = readinessRun.compiled ?? readinessRun.source_files;
2380
2204
  return {
2381
2205
  type: "checks.evaluated",
2382
2206
  event_id: createRunEventId("event"),
@@ -2397,9 +2221,11 @@ export class LocalServiceRuntime {
2397
2221
  readiness,
2398
2222
  };
2399
2223
  }
2400
- writeTestRun(compiledPath, run) {
2224
+ writeTestRun(prepDataDir, compiledPath, run) {
2401
2225
  mkdirSync(testRunsRoot(compiledPath), { recursive: true });
2402
2226
  writeJsonFile(testRunPath(compiledPath, run.run_id), TestRunResourceSchema.parse(run));
2227
+ this.testRunCache.invalidatePreparation(prepDataDir, run.preparation);
2228
+ this.readinessCache.invalidatePreparation(prepDataDir, run.preparation);
2403
2229
  }
2404
2230
  }
2405
2231
  export function createLocalServiceRuntime(options) {