@interf/compiler 0.9.4 → 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 (222) hide show
  1. package/README.md +96 -91
  2. package/TRADEMARKS.md +2 -13
  3. package/agent-skills/interf-actions/SKILL.md +97 -32
  4. package/agent-skills/interf-actions/references/cli.md +124 -71
  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 +9 -31
  13. package/dist/cli/commands/compile.js +75 -388
  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 -15
  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 +61 -220
  29. package/dist/cli/commands/test.d.ts +6 -15
  30. package/dist/cli/commands/test.js +63 -342
  31. package/dist/cli/commands/web.d.ts +0 -9
  32. package/dist/cli/commands/web.js +140 -367
  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/045gole2ojo3g.css +3 -0
  44. package/dist/compiler-ui/_next/static/chunks/17t-lulmyawg5.js +89 -0
  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/index.d.ts +0 -23
  56. package/dist/index.js +0 -16
  57. package/dist/packages/agents/lib/shells.d.ts +1 -1
  58. package/dist/packages/agents/lib/shells.js +113 -54
  59. package/dist/packages/agents/lib/user-config.d.ts +4 -2
  60. package/dist/packages/agents/lib/user-config.js +15 -7
  61. package/dist/packages/compiler/compiled-paths.d.ts +9 -2
  62. package/dist/packages/compiler/compiled-paths.js +30 -15
  63. package/dist/packages/compiler/compiled-pipeline.js +23 -3
  64. package/dist/packages/compiler/compiled-stage-plan.js +4 -0
  65. package/dist/packages/compiler/compiled-target.d.ts +1 -1
  66. package/dist/packages/compiler/compiled-target.js +1 -1
  67. package/dist/packages/compiler/index.d.ts +1 -0
  68. package/dist/packages/compiler/index.js +1 -0
  69. package/dist/packages/compiler/lib/schema.d.ts +27 -32
  70. package/dist/packages/compiler/lib/schema.js +2 -13
  71. package/dist/packages/compiler/method-runs.d.ts +2 -3
  72. package/dist/packages/compiler/method-runs.js +2 -3
  73. package/dist/packages/compiler/reset.js +3 -1
  74. package/dist/packages/compiler/runtime-contracts.js +0 -3
  75. package/dist/packages/compiler/runtime-prompt.js +1 -1
  76. package/dist/packages/compiler/source-files.d.ts +46 -0
  77. package/dist/packages/compiler/source-files.js +149 -0
  78. package/dist/packages/compiler/state-artifacts.d.ts +3 -2
  79. package/dist/packages/compiler/state-artifacts.js +4 -3
  80. package/dist/packages/compiler/state-io.d.ts +3 -2
  81. package/dist/packages/compiler/state-io.js +11 -5
  82. package/dist/packages/compiler/state-paths.d.ts +2 -1
  83. package/dist/packages/compiler/state-paths.js +6 -3
  84. package/dist/packages/compiler/state-view.d.ts +3 -2
  85. package/dist/packages/compiler/state-view.js +18 -28
  86. package/dist/packages/compiler/state.d.ts +4 -4
  87. package/dist/packages/compiler/state.js +3 -3
  88. package/dist/packages/contracts/index.d.ts +1 -1
  89. package/dist/packages/contracts/lib/preparation-paths.d.ts +117 -0
  90. package/dist/packages/contracts/lib/preparation-paths.js +177 -0
  91. package/dist/packages/contracts/lib/schema.d.ts +85 -6
  92. package/dist/packages/contracts/lib/schema.js +46 -2
  93. package/dist/packages/execution/lib/schema.d.ts +50 -57
  94. package/dist/packages/execution/lib/schema.js +1 -2
  95. package/dist/packages/local-service/action-definitions.d.ts +246 -0
  96. package/dist/packages/local-service/action-definitions.js +1147 -0
  97. package/dist/packages/local-service/action-planner.d.ts +9 -0
  98. package/dist/packages/local-service/action-planner.js +135 -0
  99. package/dist/packages/local-service/action-values.d.ts +1 -23
  100. package/dist/packages/local-service/action-values.js +1 -31
  101. package/dist/packages/local-service/client.d.ts +76 -46
  102. package/dist/packages/local-service/client.js +184 -149
  103. package/dist/packages/local-service/connection-config.d.ts +38 -0
  104. package/dist/packages/local-service/connection-config.js +75 -0
  105. package/dist/packages/local-service/index.d.ts +14 -7
  106. package/dist/packages/local-service/index.js +8 -4
  107. package/dist/packages/local-service/instance-paths.d.ts +100 -0
  108. package/dist/packages/local-service/instance-paths.js +165 -0
  109. package/dist/packages/local-service/lib/schema.d.ts +689 -2575
  110. package/dist/packages/local-service/lib/schema.js +260 -101
  111. package/dist/packages/local-service/native-run-handlers.d.ts +23 -0
  112. package/dist/{cli/commands/compile-controller.js → packages/local-service/native-run-handlers.js} +204 -20
  113. package/dist/packages/local-service/preparation-store.d.ts +92 -0
  114. package/dist/packages/local-service/preparation-store.js +171 -0
  115. package/dist/{cli/commands/check-draft.d.ts → packages/local-service/readiness-check-draft.d.ts} +2 -2
  116. package/dist/packages/local-service/routes.d.ts +33 -11
  117. package/dist/packages/local-service/routes.js +44 -15
  118. package/dist/packages/local-service/run-observability.js +25 -27
  119. package/dist/packages/local-service/runtime-caches.d.ts +76 -0
  120. package/dist/packages/local-service/runtime-caches.js +191 -0
  121. package/dist/packages/local-service/runtime-event-applier.d.ts +12 -0
  122. package/dist/packages/local-service/runtime-event-applier.js +177 -0
  123. package/dist/packages/local-service/runtime-persistence.d.ts +47 -0
  124. package/dist/packages/local-service/runtime-persistence.js +137 -0
  125. package/dist/packages/local-service/runtime-proposal-helpers.d.ts +35 -0
  126. package/dist/packages/local-service/runtime-proposal-helpers.js +251 -0
  127. package/dist/packages/local-service/runtime-resource-builders.d.ts +52 -0
  128. package/dist/packages/local-service/runtime-resource-builders.js +149 -0
  129. package/dist/packages/local-service/runtime.d.ts +201 -44
  130. package/dist/packages/local-service/runtime.js +1062 -1106
  131. package/dist/packages/local-service/server.d.ts +15 -0
  132. package/dist/packages/local-service/server.js +651 -233
  133. package/dist/packages/local-service/service-registry.d.ts +47 -0
  134. package/dist/packages/local-service/service-registry.js +137 -0
  135. package/dist/packages/method-authoring/method-authoring.d.ts +1 -1
  136. package/dist/packages/method-authoring/method-authoring.js +2 -2
  137. package/dist/packages/method-authoring/method-improvement.js +1 -1
  138. package/dist/packages/method-package/builtin-compiled-method.d.ts +4 -5
  139. package/dist/packages/method-package/builtin-compiled-method.js +8 -14
  140. package/dist/packages/method-package/context-interface.d.ts +4 -40
  141. package/dist/packages/method-package/context-interface.js +1 -23
  142. package/dist/packages/method-package/interf-method-package.d.ts +4 -4
  143. package/dist/packages/method-package/interf-method-package.js +21 -33
  144. package/dist/packages/method-package/local-methods.d.ts +10 -6
  145. package/dist/packages/method-package/local-methods.js +57 -39
  146. package/dist/packages/method-package/method-definitions.d.ts +8 -34
  147. package/dist/packages/method-package/method-definitions.js +49 -37
  148. package/dist/packages/method-package/method-helpers.d.ts +1 -13
  149. package/dist/packages/method-package/method-helpers.js +8 -42
  150. package/dist/packages/method-package/method-review-paths.d.ts +1 -1
  151. package/dist/packages/method-package/method-review-paths.js +5 -5
  152. package/dist/packages/method-package/method-stage-runner.js +2 -2
  153. package/dist/packages/method-package/user-methods.d.ts +17 -0
  154. package/dist/packages/method-package/user-methods.js +77 -0
  155. package/dist/packages/project-model/index.d.ts +1 -1
  156. package/dist/packages/project-model/index.js +1 -1
  157. package/dist/packages/project-model/interf-detect.d.ts +8 -3
  158. package/dist/packages/project-model/interf-detect.js +34 -34
  159. package/dist/packages/project-model/interf-scaffold.d.ts +3 -3
  160. package/dist/packages/project-model/interf-scaffold.js +23 -32
  161. package/dist/packages/project-model/lib/schema.js +38 -1
  162. package/dist/packages/project-model/preparation-entries.d.ts +11 -0
  163. package/dist/packages/project-model/preparation-entries.js +49 -0
  164. package/dist/packages/project-model/source-config.d.ts +11 -10
  165. package/dist/packages/project-model/source-config.js +83 -44
  166. package/dist/packages/project-model/source-folders.d.ts +5 -5
  167. package/dist/packages/project-model/source-folders.js +14 -14
  168. package/dist/packages/shared/filesystem.d.ts +7 -0
  169. package/dist/packages/shared/filesystem.js +97 -10
  170. package/dist/packages/testing/lib/schema.d.ts +12 -13
  171. package/dist/packages/testing/lib/schema.js +4 -5
  172. package/dist/packages/testing/readiness-check-run.d.ts +7 -7
  173. package/dist/packages/testing/readiness-check-run.js +46 -51
  174. package/dist/packages/testing/test-execution.js +6 -6
  175. package/dist/packages/testing/test-paths.js +4 -3
  176. package/dist/packages/testing/test-sandbox.d.ts +0 -1
  177. package/dist/packages/testing/test-sandbox.js +14 -30
  178. package/dist/packages/testing/test-targets.d.ts +1 -1
  179. package/dist/packages/testing/test-targets.js +6 -6
  180. package/dist/packages/testing/test.d.ts +1 -1
  181. package/dist/packages/testing/test.js +1 -1
  182. package/package.json +6 -26
  183. package/LICENSE +0 -183
  184. package/dist/cli/commands/compile-controller.d.ts +0 -17
  185. package/dist/cli/commands/compiled-flow.d.ts +0 -25
  186. package/dist/cli/commands/compiled-flow.js +0 -112
  187. package/dist/cli/commands/control-path.d.ts +0 -11
  188. package/dist/cli/commands/control-path.js +0 -72
  189. package/dist/cli/commands/create-method-wizard.d.ts +0 -76
  190. package/dist/cli/commands/create-method-wizard.js +0 -465
  191. package/dist/cli/commands/create.d.ts +0 -8
  192. package/dist/cli/commands/create.js +0 -189
  193. package/dist/cli/commands/default.d.ts +0 -2
  194. package/dist/cli/commands/default.js +0 -39
  195. package/dist/cli/commands/executor-flow.d.ts +0 -29
  196. package/dist/cli/commands/executor-flow.js +0 -163
  197. package/dist/cli/commands/init.d.ts +0 -11
  198. package/dist/cli/commands/init.js +0 -784
  199. package/dist/cli/commands/list.d.ts +0 -2
  200. package/dist/cli/commands/list.js +0 -30
  201. package/dist/cli/commands/preparation-selection.d.ts +0 -6
  202. package/dist/cli/commands/preparation-selection.js +0 -11
  203. package/dist/cli/commands/source-config-wizard.d.ts +0 -52
  204. package/dist/cli/commands/source-config-wizard.js +0 -680
  205. package/dist/cli/commands/test-flow.d.ts +0 -58
  206. package/dist/cli/commands/test-flow.js +0 -231
  207. package/dist/cli/commands/verify.d.ts +0 -2
  208. package/dist/cli/commands/verify.js +0 -94
  209. package/dist/compiler-ui/_next/static/chunks/0d~8t0zm6545p.js +0 -118
  210. package/dist/compiler-ui/_next/static/chunks/0xnel.ax9a.2c.css +0 -3
  211. package/dist/packages/compiler/raw-snapshot.d.ts +0 -49
  212. package/dist/packages/compiler/raw-snapshot.js +0 -101
  213. package/dist/packages/method-package/index.d.ts +0 -11
  214. package/dist/packages/method-package/index.js +0 -11
  215. package/dist/packages/method-package/method-stage-policy.d.ts +0 -5
  216. package/dist/packages/method-package/method-stage-policy.js +0 -31
  217. package/dist/packages/project-model/project-paths.d.ts +0 -12
  218. package/dist/packages/project-model/project-paths.js +0 -33
  219. /package/dist/compiler-ui/_next/static/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_buildManifest.js +0 -0
  220. /package/dist/compiler-ui/_next/static/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_clientMiddlewareManifest.js +0 -0
  221. /package/dist/compiler-ui/_next/static/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_ssgManifest.js +0 -0
  222. /package/dist/{cli/commands/check-draft.js → packages/local-service/readiness-check-draft.js} +0 -0
@@ -1,635 +1,233 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, 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";
13
+ import { resetCompiledGeneratedState, } from "../compiler/reset.js";
10
14
  import { ensurePortableContextScaffold, readInterfConfig, } from "../project-model/interf.js";
11
- import { findSourcePreparationConfig, fingerprintReadinessChecks, listSourcePreparationConfigs, loadSourceFolderConfig, DEFAULT_METHOD_ID, methodIdForSourcePreparationConfig, resolveConfiguredSourceFolderPath, resolveSourcePreparationPath, syncCompiledInterfConfigFromSourcePreparationConfig, upsertSourcePreparationConfig, } from "../project-model/source-config.js";
12
- import { defaultPreparationNameForPath, listSourceFolderChoices, normalizeSourcePreparationPathForConfig, } from "../project-model/source-folders.js";
13
- import { portableContextPath, } from "../project-model/project-paths.js";
15
+ import { findSourcePreparationConfig, fingerprintReadinessChecks, listSourcePreparationConfigs, loadSourceFolderConfig, DEFAULT_METHOD_ID, methodIdForSourcePreparationConfig, resolveConfiguredSourceFolderPath, resolveSourcePreparationPath, removeSourcePreparationConfig, saveSourceFolderConfig, syncCompiledInterfConfigFromSourcePreparationConfig, upsertSourcePreparationConfig, } from "../project-model/source-config.js";
16
+ import { listSourceFolderChoices, } from "../project-model/source-folders.js";
17
+ import { asPreparationDataDir, preparationPortableContextPath, userMethodsRoot, preparationConfigPath, preparationMethodPackagePath, preparationMethodsRoot, } from "../contracts/lib/preparation-paths.js";
14
18
  import { getCompiledMethod, listCompiledMethodChoices, } from "../method-package/method-definitions.js";
15
- import { resolveMethodPackageSourcePath, } from "../method-package/local-methods.js";
19
+ import { contextInterfaceArtifactPath, } from "../method-package/context-interface.js";
20
+ import { methodDefinitionPath, resolveMethodPackageSourcePath, seedLocalDefaultMethod, } from "../method-package/local-methods.js";
21
+ import { seedLocalMethodPackageFromBase, } from "../method-package/interf-method-package.js";
22
+ import { PACKAGE_ROOT } from "../method-package/lib/package-root.js";
16
23
  import { resolveAgent, detectAgents, supportsAutomatedRuns, } from "../agents/lib/detection.js";
17
- import { AGENTS, } from "../agents/lib/constants.js";
18
24
  import { loadUserConfig, saveUserConfig, } from "../agents/lib/user-config.js";
19
25
  import { readSavedReadinessCheckRun, } from "../testing/readiness-check-run.js";
20
26
  import { createCompiledTestTarget, } from "../testing/test-targets.js";
21
- import { ActionProposalApprovalRequestSchema, ActionProposalCreateRequestSchema, ActionProposalPlanSchema, ActionProposalResourceSchema, CompileRunCreateRequestSchema, CompileRunResourceSchema, MethodResourceSchema, LocalExecutorStatusSchema, LocalExecutorSelectRequestSchema, LocalServiceHealthSchema, PreparationReadinessStateSchema, LocalRunHandlerResultSchema, LocalJobEventAppendRequestSchema, LocalJobRunCreateRequestSchema, LocalJobRunResourceSchema, SourceFileResourceSchema, WorkspaceFileResourceSchema, PortableContextResourceSchema, PreparationSetupCreateRequestSchema, PreparationResourceSchema, ReadinessCheckDraftCreateRequestSchema, ReadinessCheckDraftResultSchema, 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";
22
28
  import { buildLocalServiceUrl, } from "./routes.js";
23
- import { methodAuthoringTaskPrompt, MethodAuthoringActionValuesSchema, PreparationSetupActionValuesSchema, } from "./action-values.js";
29
+ import { MethodAuthoringActionValuesSchema, PreparationSetupActionValuesSchema, } from "./action-values.js";
24
30
  import { compileRunToObservability, jobRunToObservability, testRunToObservability, uniqueArtifacts, } from "./run-observability.js";
25
- function createRunId(prefix) {
26
- return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
27
- }
28
- function createActionProposalId() {
29
- return `action_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
30
- }
31
- 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.";
32
- function readJsonFile(filePath) {
33
- try {
34
- return JSON.parse(readFileSync(filePath, "utf8"));
35
- }
36
- catch {
37
- return null;
38
- }
39
- }
40
- function writeJsonFile(filePath, value) {
41
- mkdirSync(dirname(filePath), { recursive: true });
42
- writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
43
- }
44
- function sanitizeActionProposalPlan(value) {
45
- if (!value || typeof value !== "object" || Array.isArray(value))
46
- return value;
47
- const plan = { ...value };
48
- for (const key of ["preparation", "method", "title", "summary", "assistant_message", "command_preview"]) {
49
- const current = plan[key];
50
- if (current === null || current === undefined) {
51
- delete plan[key];
52
- continue;
53
- }
54
- if (typeof current === "string" && current.trim().length === 0) {
55
- delete plan[key];
56
- }
57
- }
58
- return plan;
59
- }
60
- function compileRunsRoot(compiledPath) {
61
- return join(compiledRuntimeRoot(compiledPath), "compile-runs");
62
- }
63
- function compileRunPath(compiledPath, runId) {
64
- return join(compileRunsRoot(compiledPath), `${runId}.json`);
65
- }
66
- function testRunsRoot(compiledPath) {
67
- return join(testRootForCompiled(compiledPath), "service-runs");
68
- }
69
- function testRunPath(compiledPath, runId) {
70
- return join(testRunsRoot(compiledPath), `${runId}.json`);
71
- }
72
- function localJobsRoot(rootPath) {
73
- return join(rootPath, "interf", ".service", "jobs");
74
- }
75
- function localJobPath(rootPath, runId) {
76
- return join(localJobsRoot(rootPath), `${runId}.json`);
77
- }
78
- function actionProposalsRoot(rootPath) {
79
- return join(rootPath, "interf", ".service", "action-proposals");
80
- }
81
- function actionProposalPath(rootPath, proposalId) {
82
- return join(actionProposalsRoot(rootPath), `${proposalId}.json`);
83
- }
84
- function listJsonFiles(dirPath) {
85
- if (!existsSync(dirPath))
86
- return [];
87
- try {
88
- if (!statSync(dirPath).isDirectory())
89
- return [];
90
- }
91
- catch {
92
- return [];
93
- }
94
- return readdirSync(dirPath)
95
- .filter((entry) => entry.endsWith(".json"))
96
- .map((entry) => join(dirPath, entry));
97
- }
98
- function readCompileRunAt(filePath) {
99
- const parsed = CompileRunSchema.safeParse(readJsonFile(filePath));
100
- return parsed.success ? parsed.data : null;
101
- }
102
- function readRuntimeRunHistory(compiledPath) {
103
- const historyPath = compiledRuntimeRunHistoryPath(compiledPath);
104
- if (!existsSync(historyPath))
105
- return [];
106
- try {
107
- return readFileSync(historyPath, "utf8")
108
- .split(/\r?\n/)
109
- .map((line) => line.trim())
110
- .filter((line) => line.length > 0)
111
- .map((line) => {
112
- try {
113
- return RuntimeRunSchema.safeParse(JSON.parse(line));
114
- }
115
- catch {
116
- return { success: false };
117
- }
118
- })
119
- .filter((parsed) => parsed.success)
120
- .map((parsed) => parsed.data);
121
- }
122
- catch {
123
- return [];
124
- }
125
- }
126
- function readTestRunAt(filePath) {
127
- const parsed = TestRunResourceSchema.safeParse(readJsonFile(filePath));
128
- return parsed.success ? parsed.data : null;
129
- }
130
- function readLocalJobRunAt(filePath) {
131
- const parsed = LocalJobRunResourceSchema.safeParse(readJsonFile(filePath));
132
- return parsed.success ? parsed.data : null;
133
- }
134
- function readActionProposalAt(filePath) {
135
- const parsed = ActionProposalResourceSchema.safeParse(readJsonFile(filePath));
136
- return parsed.success ? parsed.data : null;
137
- }
138
- function newestFirst(items) {
139
- return [...items].sort((left, right) => {
140
- const leftTime = Date.parse(left.started_at ?? left.finished_at ?? "");
141
- const rightTime = Date.parse(right.started_at ?? right.finished_at ?? "");
142
- return (Number.isFinite(rightTime) ? rightTime : 0) - (Number.isFinite(leftTime) ? leftTime : 0);
143
- });
144
- }
145
- function newestJobFirst(items) {
146
- return [...items].sort((left, right) => Date.parse(right.created_at) - Date.parse(left.created_at));
147
- }
148
- function newestActionProposalFirst(items) {
149
- return [...items].sort((left, right) => Date.parse(right.created_at) - Date.parse(left.created_at));
150
- }
151
- function newestCompileFirst(items) {
152
- return [...items].sort((left, right) => Date.parse(right.created_at) - Date.parse(left.created_at));
153
- }
154
- function configuredAgentName() {
155
- const config = loadUserConfig();
156
- if (!config)
157
- return null;
158
- const configured = AGENTS.find((agent) => agent.command === config.agentCommand) ??
159
- AGENTS.find((agent) => agent.name === config.agent) ??
160
- AGENTS.find((agent) => agent.displayName === config.agent);
161
- return configured?.name ?? null;
162
- }
163
- function detectedExecutorOptions(currentAgentName) {
164
- return detectAgents()
165
- .filter(supportsAutomatedRuns)
166
- .map((agent) => ({
167
- name: agent.name,
168
- display_name: agent.displayName,
169
- command: agent.command,
170
- current: agent.name === currentAgentName,
171
- }));
172
- }
173
- function stageArtifactRefs(stageId, artifacts) {
174
- return (artifacts ?? []).map((path) => ({
175
- path,
176
- role: "output",
177
- stage_id: stageId,
178
- label: path,
179
- }));
180
- }
181
- function proofForStage(options) {
182
- return {
183
- id: `${options.runId}-${options.stageId}-proof`,
184
- run_id: options.runId,
185
- stage_id: options.stageId,
186
- generated_at: options.stageState.finished_at ?? new Date().toISOString(),
187
- summary: options.summary ?? `${options.stageId} produced stage evidence.`,
188
- files_processed: options.stageState.counts?.source_total,
189
- artifacts: options.artifacts,
190
- checks: [
191
- {
192
- id: `${options.stageId}-status`,
193
- label: "stage completed",
194
- ok: options.stageState.status === "succeeded",
195
- ...(options.stageState.status === "succeeded"
196
- ? {}
197
- : { detail: options.stageState.summary ?? "Stage did not complete successfully." }),
198
- },
199
- {
200
- id: `${options.stageId}-artifacts`,
201
- label: "artifacts recorded",
202
- ok: options.artifacts.length > 0,
203
- ...(options.artifacts.length > 0
204
- ? {}
205
- : { detail: "No stage artifacts were recorded." }),
206
- },
207
- ],
208
- };
209
- }
210
- function logsForStageRun(stageState) {
211
- const runId = stageState?.run_id;
212
- if (!runId)
213
- return undefined;
214
- return {
215
- prompt_path: `.interf/runtime/logs/${runId}.prompt.txt`,
216
- event_stream_path: `.interf/runtime/logs/${runId}.events.ndjson`,
217
- status_path: `.interf/runtime/logs/${runId}.status.log`,
218
- contract_path: `.interf/runtime/logs/${runId}.stage-contract.json`,
219
- };
220
- }
221
- function logsForRuntimeRun(run) {
222
- if (!run)
223
- return undefined;
224
- return {
225
- ...(run.logs?.prompt_path ? { prompt_path: run.logs.prompt_path } : {}),
226
- ...(run.logs?.event_stream_path ? { event_stream_path: run.logs.event_stream_path } : {}),
227
- ...(run.logs?.status_path ? { status_path: run.logs.status_path } : {}),
228
- contract_path: run.contract_path,
229
- };
230
- }
231
- function timestampKey(value) {
232
- const parsed = Date.parse(value ?? "");
233
- return Number.isFinite(parsed) ? parsed : 0;
234
- }
235
- function applyEventToCompileRun(run, event) {
236
- const now = event.timestamp;
237
- const stageFor = (stageId) => {
238
- const existing = run.stages.find((stage) => stage.stage_id === stageId);
239
- if (existing)
240
- return existing;
241
- const created = {
242
- run_id: run.run_id,
243
- stage_id: stageId,
244
- status: "queued",
245
- artifacts: [],
246
- };
247
- run.stages.push(created);
248
- return created;
249
- };
250
- const updateStage = (stageId, patch) => {
251
- const current = stageFor(stageId);
252
- Object.assign(current, patch);
253
- };
254
- switch (event.type) {
255
- case "run.started":
256
- return {
257
- ...run,
258
- status: "running",
259
- started_at: run.started_at ?? now,
260
- events: [...run.events, event],
261
- };
262
- case "stage.started":
263
- updateStage(event.stage_id, {
264
- status: "running",
265
- started_at: now,
266
- stage_index: event.stage_index,
267
- stage_total: event.stage_total,
268
- });
269
- break;
270
- case "artifact.written": {
271
- const stage = stageFor(event.stage_id);
272
- updateStage(event.stage_id, {
273
- artifacts: uniqueArtifacts([...(stage.artifacts ?? []), event.artifact]),
274
- });
275
- break;
276
- }
277
- case "proof.updated":
278
- if (event.stage_id) {
279
- updateStage(event.stage_id, {
280
- latest_proof: event.proof,
281
- });
282
- }
283
- return {
284
- ...run,
285
- latest_proof: event.proof,
286
- events: [...run.events, event],
287
- };
288
- case "log.appended":
289
- break;
290
- case "stage.passed":
291
- updateStage(event.stage_id, {
292
- status: "succeeded",
293
- finished_at: now,
294
- summary: event.summary ?? null,
295
- failure: null,
296
- });
297
- break;
298
- case "stage.failed":
299
- updateStage(event.stage_id, {
300
- status: "failed",
301
- finished_at: now,
302
- summary: event.error,
303
- failure: event.error,
304
- });
305
- break;
306
- case "run.completed":
307
- return {
308
- ...run,
309
- status: "succeeded",
310
- finished_at: run.finished_at ?? now,
311
- events: [...run.events, event],
312
- };
313
- case "run.failed":
314
- return {
315
- ...run,
316
- status: "failed",
317
- finished_at: run.finished_at ?? now,
318
- events: [...run.events, event],
319
- };
320
- default:
321
- break;
322
- }
323
- return {
324
- ...run,
325
- events: [...run.events, event],
326
- };
327
- }
328
- function applyEventToLocalJob(run, event) {
329
- const stepFor = (stepId) => {
330
- const existing = run.steps.find((step) => step.id === stepId);
331
- if (existing)
332
- return existing;
333
- const created = {
334
- id: stepId,
335
- label: stepId,
336
- status: "queued",
337
- };
338
- run.steps.push(created);
339
- return created;
340
- };
341
- const updateStep = (stepId, patch) => {
342
- if (!stepId)
343
- return;
344
- Object.assign(stepFor(stepId), patch);
345
- };
346
- switch (event.type) {
347
- case "job.started":
348
- return {
349
- ...run,
350
- status: "running",
351
- started_at: run.started_at ?? event.timestamp,
352
- events: [...run.events, event],
353
- };
354
- case "step.started":
355
- updateStep(event.step_id, {
356
- status: "running",
357
- started_at: event.timestamp,
358
- ...(event.input ? { input: event.input } : {}),
359
- });
360
- break;
361
- case "step.completed":
362
- updateStep(event.step_id, {
363
- status: "succeeded",
364
- finished_at: event.timestamp,
365
- summary: event.message ?? null,
366
- ...(event.output ? { output: event.output } : {}),
367
- });
368
- break;
369
- case "step.failed":
370
- updateStep(event.step_id, {
371
- status: "failed",
372
- finished_at: event.timestamp,
373
- summary: event.message ?? null,
374
- ...(event.output ? { output: event.output } : {}),
375
- });
376
- break;
377
- case "job.completed":
378
- return {
379
- ...run,
380
- status: "succeeded",
381
- finished_at: run.finished_at ?? event.timestamp,
382
- events: [...run.events, event],
383
- };
384
- case "job.failed":
385
- return {
386
- ...run,
387
- status: "failed",
388
- finished_at: run.finished_at ?? event.timestamp,
389
- error: event.message ?? "Job failed.",
390
- events: [...run.events, event],
391
- };
392
- case "artifact.written":
393
- case "log.appended":
394
- break;
395
- default:
396
- break;
397
- }
398
- return {
399
- ...run,
400
- events: [...run.events, event],
401
- };
402
- }
403
- function slugFromText(value) {
404
- const slug = value
405
- .toLowerCase()
406
- .replace(/[^a-z0-9]+/g, "-")
407
- .replace(/^-+|-+$/g, "")
408
- .slice(0, 42)
409
- .replace(/-+$/g, "");
410
- return slug || "custom";
411
- }
412
- function stringValue(values, key) {
413
- const value = values?.[key];
414
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
415
- }
416
- function numberValue(values, key) {
417
- const value = values?.[key];
418
- return typeof value === "number" && Number.isFinite(value) ? value : null;
419
- }
420
- function testModeFromValues(values) {
421
- const value = stringValue(values, "mode") ?? stringValue(values, "target");
422
- if (value === "source-files")
423
- return "raw";
424
- if (value === "portable-context")
425
- return "compiled";
426
- return value === "raw" || value === "compiled" || value === "both" ? value : null;
427
- }
428
- function testModeValue(values, defaultMode = "both") {
429
- return testModeFromValues(values) ?? defaultMode;
430
- }
431
- function testModeCliTarget(mode) {
432
- if (mode === "raw")
433
- return "source-files";
434
- if (mode === "compiled")
435
- return "portable-context";
436
- return "both";
437
- }
438
- function methodIdForProposal(message, values) {
439
- const explicit = stringValue(values, "method_id") ??
440
- stringValue(values, "method");
441
- return explicit ?? `custom-${slugFromText(message)}`;
442
- }
443
- function actionValueMethodTaskPrompt(values) {
444
- const parsed = MethodAuthoringActionValuesSchema.safeParse(values);
445
- return parsed.success ? methodAuthoringTaskPrompt(parsed.data) : null;
446
- }
447
- function booleanValue(values, key) {
448
- const value = values?.[key];
449
- if (typeof value === "boolean")
450
- return value;
451
- if (typeof value === "string") {
452
- const normalized = value.trim().toLowerCase();
453
- if (normalized === "true" || normalized === "yes" || normalized === "1")
454
- return true;
455
- if (normalized === "false" || normalized === "no" || normalized === "0")
456
- return false;
457
- }
458
- return null;
459
- }
460
- function preparationSetupPathValue(values) {
461
- return stringValue(values, "path") ??
462
- stringValue(values, "source_folder_path") ??
463
- stringValue(values, "source_path") ??
464
- stringValue(values, "source_folder") ??
465
- stringValue(values, "folder");
466
- }
467
- function preparationSetupNameValue(values) {
468
- const parsed = PreparationSetupActionValuesSchema.safeParse(values);
469
- return parsed.success ? parsed.data.name : stringValue(values, "name") ??
470
- stringValue(values, "preparation") ??
471
- stringValue(values, "preparation_name");
472
- }
473
- function actionCommandPreview(actionType, preparationName, methodId, values) {
474
- if (actionType === "preparation-setup") {
475
- const preparationPart = preparationName ? ` # Preparation: ${preparationName}` : "";
476
- const pathPart = preparationSetupPathValue(values);
477
- const methodSuffix = methodId ? ` # Method: ${methodId}` : "";
478
- const setupPreview = pathPart ? `interf init # source: ${pathPart}${preparationPart}` : `interf init${preparationPart}`;
479
- if (booleanValue(values, "prepare_after_setup") && preparationName) {
480
- return `${setupPreview}\ninterf compile --preparation ${preparationName}${methodSuffix}`;
481
- }
482
- return setupPreview;
483
- }
484
- if (actionType === "compile") {
485
- const methodSuffix = methodId ? ` # Method: ${methodId}` : "";
486
- return preparationName
487
- ? `interf compile --preparation ${preparationName}${methodSuffix}`
488
- : `interf compile${methodSuffix}`;
489
- }
490
- if (actionType === "test") {
491
- const mode = testModeCliTarget(testModeValue(values));
492
- return preparationName
493
- ? `interf test --preparation ${preparationName} --target ${mode}`
494
- : `interf test --target ${mode}`;
495
- }
496
- if (actionType === "readiness-check-draft") {
497
- return "interf # choose Auto-create readiness checks";
498
- }
499
- if (actionType === "method-authoring" || actionType === "method-improvement") {
500
- return "interf create method";
501
- }
502
- return "Try: create a Preparation, prepare, check readiness, draft readiness checks, or draft a Method.";
503
- }
504
- function hasCompiledTestTarget(sourcePath, preparationConfig) {
505
- const compiledPath = portableContextPath(sourcePath, preparationConfig.name);
506
- if (!existsSync(compiledPath))
507
- return false;
508
- return createCompiledTestTarget(compiledPath, preparationConfig.name, methodIdForSourcePreparationConfig(preparationConfig) ?? DEFAULT_METHOD_ID).eligible;
509
- }
510
- function actionAssistantMessage(actionType, preparationName, commandPreview, options = {}) {
511
- const preparationSuffix = preparationName ? ` for Preparation "${preparationName}"` : "";
512
- if (actionType === "preparation-setup") {
513
- if (options.prepareAfterSetup) {
514
- return `Interf prepared a prepare proposal${preparationSuffix}. Approve to save the Preparation and run the selected Method against the Source Folder. CLI equivalent: ${commandPreview}`;
515
- }
516
- return `Interf prepared a Preparation setup proposal${preparationSuffix}. Approve to save the source folder as a Preparation. Preparing portable context is a separate compile run. CLI equivalent: ${commandPreview}`;
517
- }
518
- if (actionType === "compile") {
519
- 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}`;
520
- }
521
- if (actionType === "test") {
522
- return `Interf prepared a readiness-check proposal${preparationSuffix}. Approve to run the requested target against saved readiness checks. CLI equivalent: ${commandPreview}`;
523
- }
524
- if (actionType === "readiness-check-draft") {
525
- return `Interf prepared a readiness-check draft proposal${preparationSuffix}. Approve to ask the configured local executor to draft checks as a visible run. CLI equivalent: ${commandPreview}`;
526
- }
527
- if (actionType === "method-authoring" || actionType === "method-improvement") {
528
- return `Interf prepared a Method draft proposal${preparationSuffix}. Approve to draft a reusable local Method as a visible run. CLI equivalent: ${commandPreview}`;
529
- }
530
- 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.";
531
- }
532
- function passRate(passed, total) {
533
- if (total <= 0)
534
- return null;
535
- return Math.round((passed / total) * 100);
536
- }
537
- function readinessTargetResult(summary, currentFingerprint, comparisonFingerprint) {
538
- if (!summary)
539
- return null;
540
- const resultFingerprint = comparisonFingerprint ?? null;
541
- return {
542
- passed: summary.passed_cases,
543
- total: summary.total_cases,
544
- pass_rate: passRate(summary.passed_cases, summary.total_cases),
545
- checks_fingerprint: resultFingerprint,
546
- stale: Boolean(currentFingerprint && resultFingerprint && currentFingerprint !== resultFingerprint),
547
- run_id: null,
548
- run_path: summary.run_path,
549
- };
550
- }
551
- function readinessSummaryForStatus(status) {
552
- if (status === "ready")
553
- return "Ready for agent work.";
554
- if (status === "not-ready")
555
- return "Readiness checks did not pass.";
556
- if (status === "stale")
557
- return "Readiness checks are stale for the current saved checks.";
558
- if (status === "checking")
559
- return "Readiness checks are running.";
560
- if (status === "building")
561
- return "Portable context is building.";
562
- if (status === "built")
563
- return "Portable context is built; readiness has not been proven yet.";
564
- if (status === "not-built")
565
- return "Portable context has not been built yet.";
566
- if (status === "not-configured")
567
- return "No readiness checks are configured.";
568
- return "Latest preparation failed.";
569
- }
570
- function readinessStateToPreparationReadiness(readiness) {
571
- return PreparationReadinessStateSchema.parse({
572
- ...readiness,
573
- checks: readiness.checks.map((check) => ({ ...check })),
574
- });
575
- }
576
- function buildPreparationResource(rootPath, preparation, readiness, latestCompileRunId, latestTestRunId) {
577
- const methodId = methodIdForSourcePreparationConfig(preparation);
578
- return PreparationResourceSchema.parse({
579
- id: preparation.name,
580
- name: preparation.name,
581
- preparation,
582
- source_path: resolveSourcePreparationPath(rootPath, preparation),
583
- method_id: methodId,
584
- checks: preparation.checks,
585
- portable_context: {
586
- preparation: preparation.name,
587
- path: readiness.portable_context_path,
588
- exists: readiness.portable_context_path !== null,
589
- method_id: methodId,
590
- latest_compile_run_id: latestCompileRunId,
591
- latest_test_run_id: latestTestRunId,
592
- },
593
- portable_context_path: readiness.portable_context_path,
594
- readiness: readinessStateToPreparationReadiness(readiness),
595
- runs: {
596
- latest_compile_run_id: latestCompileRunId,
597
- latest_test_run_id: latestTestRunId,
598
- },
599
- latest_compile_run_id: latestCompileRunId,
600
- latest_test_run_id: latestTestRunId,
601
- });
602
- }
603
- function buildMethodResource(resource) {
604
- return MethodResourceSchema.parse({
605
- id: resource.id,
606
- method_id: resource.id,
607
- path: resource.path,
608
- ...(resource.label ? { label: resource.label } : {}),
609
- ...(resource.hint ? { hint: resource.hint } : {}),
610
- source_kind: resource.source_kind,
611
- built_in: resource.built_in,
612
- active_for_preparations: resource.active_for_preparations,
613
- stages: resource.stages,
614
- });
615
- }
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;
616
35
  export class LocalServiceRuntime {
617
- rootPath;
618
36
  host;
619
37
  port;
620
38
  startedAt;
621
39
  packageVersion;
622
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();
623
87
  constructor(options) {
624
- this.rootPath = resolve(options.rootPath);
625
88
  this.host = options.host;
626
89
  this.port = options.port;
627
90
  this.startedAt = options.startedAt ?? new Date().toISOString();
628
91
  this.packageVersion = options.packageVersion;
629
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
+ }
630
202
  }
631
- health() {
632
- const sourceFolderPath = resolveConfiguredSourceFolderPath(this.rootPath);
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);
228
+ }
229
+ health(prepDataDir) {
230
+ const sourceFolderPath = prepDataDir ? resolveConfiguredSourceFolderPath(prepDataDir) : null;
633
231
  return LocalServiceHealthSchema.parse({
634
232
  kind: "interf-local-service-health",
635
233
  version: 1,
@@ -637,54 +235,61 @@ export class LocalServiceRuntime {
637
235
  host: this.host,
638
236
  port: this.port,
639
237
  service_url: buildLocalServiceUrl({ host: this.host, port: this.port }),
640
- control_path: this.rootPath,
238
+ ...(prepDataDir ? { control_path: prepDataDir } : {}),
641
239
  source_folder_path: sourceFolderPath,
642
240
  started_at: this.startedAt,
643
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(),
644
246
  });
645
247
  }
646
- listPreparations() {
647
- const config = loadSourceFolderConfig(this.rootPath);
248
+ listPreparations(prepDataDir) {
249
+ const config = loadSourceFolderConfig(prepDataDir);
648
250
  return listSourcePreparationConfigs(config).map((preparation) => {
649
- const compileRuns = this.listCompileRunsForPreparation(preparation.name);
650
- const testRuns = this.listTestRunsForPreparation(preparation.name);
651
- const readiness = this.computePreparationReadiness(preparation);
652
- 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);
653
255
  });
654
256
  }
655
- getPreparation(preparationName) {
656
- return this.listPreparations().find((preparation) => preparation.name === preparationName) ?? null;
257
+ getPreparation(prepDataDir, preparationName) {
258
+ return this.listPreparations(prepDataDir).find((preparation) => preparation.name === preparationName) ?? null;
657
259
  }
658
- listPreparationReadiness() {
659
- return this.listReadiness().map(readinessStateToPreparationReadiness);
260
+ listPreparationReadiness(prepDataDir) {
261
+ return this.listReadiness(prepDataDir).map(readinessStateToPreparationReadiness);
660
262
  }
661
- getPreparationReadiness(preparationName) {
662
- const readiness = this.getReadiness(preparationName);
263
+ getPreparationReadiness(prepDataDir, preparationName) {
264
+ const readiness = this.getReadiness(prepDataDir, preparationName);
663
265
  return readiness ? readinessStateToPreparationReadiness(readiness) : null;
664
266
  }
665
- listReadiness() {
666
- const config = loadSourceFolderConfig(this.rootPath);
667
- 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;
668
274
  }
669
- getReadiness(preparationName) {
670
- const preparation = findSourcePreparationConfig(loadSourceFolderConfig(this.rootPath), preparationName);
671
- return preparation ? this.computePreparationReadiness(preparation) : null;
275
+ computePreparationReadiness(prepDataDir, preparation) {
276
+ return this.readinessCache.get(prepDataDir, preparation.name, () => this.computePreparationReadinessUncached(prepDataDir, preparation));
672
277
  }
673
- computePreparationReadiness(preparation) {
278
+ computePreparationReadinessUncached(prepDataDir, preparation) {
674
279
  const generatedAt = new Date().toISOString();
675
- const compiledPath = portableContextPath(this.rootPath, preparation.name);
280
+ const compiledPath = preparationPortableContextPath(asPreparationDataDir(prepDataDir), preparation.name);
676
281
  const contextExists = existsSync(compiledPath);
677
282
  const compiledTarget = createCompiledTestTarget(compiledPath, preparation.name, methodIdForSourcePreparationConfig(preparation) ?? DEFAULT_METHOD_ID);
678
283
  const contextReady = compiledTarget.eligible;
679
- const compileRun = this.listCompileRunsForPreparation(preparation.name)[0] ?? null;
680
- const testRun = this.listTestRunsForPreparation(preparation.name)[0] ?? null;
681
- const comparison = this.readLatestComparison(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);
682
287
  const configuredChecks = preparation.checks.length;
683
288
  const currentFingerprint = configuredChecks > 0 ? fingerprintReadinessChecks(preparation.checks) : null;
684
- const comparisonFingerprint = comparison?.checks_fingerprint ?? null;
685
- const sourceResult = readinessTargetResult(comparison?.raw, currentFingerprint, comparisonFingerprint);
686
- const contextResult = readinessTargetResult(comparison?.compiled, currentFingerprint, comparisonFingerprint);
687
- const checksStale = Boolean(currentFingerprint && comparisonFingerprint && currentFingerprint !== comparisonFingerprint);
289
+ const readinessRunFingerprint = readinessRun?.checks_fingerprint ?? null;
290
+ const sourceResult = readinessTargetResult(readinessRun?.source_files, currentFingerprint, readinessRunFingerprint);
291
+ const contextResult = readinessTargetResult(readinessRun?.compiled, currentFingerprint, readinessRunFingerprint);
292
+ const checksStale = Boolean(currentFingerprint && readinessRunFingerprint && currentFingerprint !== readinessRunFingerprint);
688
293
  const compileCheck = (() => {
689
294
  if (!compileRun) {
690
295
  return {
@@ -796,44 +401,50 @@ export class LocalServiceRuntime {
796
401
  fingerprint: currentFingerprint,
797
402
  source_files: sourceResult,
798
403
  portable_context: contextResult,
799
- delta: comparison?.summary.pass_rate_delta ?? null,
800
404
  },
801
405
  checks,
802
406
  });
803
407
  }
804
- listSourceFiles(preparationName) {
805
- const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath))
408
+ listSourceFiles(prepDataDir, preparationName) {
409
+ const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir))
806
410
  .filter((preparation) => !preparationName || preparation.name === preparationName);
807
411
  return preparations.flatMap((preparation) => {
808
- const sourceFolderPath = resolveSourcePreparationPath(this.rootPath, preparation);
809
- const compiledPath = portableContextPath(this.rootPath, preparation.name);
810
- return discoverSourceFiles(sourceFolderPath, compiledPath).sourceFiles.map((relativePath) => {
811
- const absolutePath = join(sourceFolderPath, relativePath);
812
- let sizeBytes = 0;
813
- let modifiedAt = null;
814
- try {
815
- const stat = statSync(absolutePath);
816
- sizeBytes = stat.size;
817
- modifiedAt = stat.mtime.toISOString();
818
- }
819
- catch {
820
- sizeBytes = 0;
821
- modifiedAt = null;
822
- }
823
- return SourceFileResourceSchema.parse({
824
- preparation: preparation.name,
825
- path: relativePath,
826
- absolute_path: absolutePath,
827
- size_bytes: sizeBytes,
828
- modified_at: modifiedAt,
829
- 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
+ });
830
441
  });
831
442
  });
832
443
  });
833
444
  }
834
- listWorkspaceFiles() {
835
- const sourceFolderPath = resolveConfiguredSourceFolderPath(this.rootPath) ?? this.rootPath;
836
- 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) => {
837
448
  const absolutePath = join(sourceFolderPath, relativePath);
838
449
  let sizeBytes = 0;
839
450
  let modifiedAt = null;
@@ -854,47 +465,61 @@ export class LocalServiceRuntime {
854
465
  });
855
466
  });
856
467
  }
857
- listMethods() {
858
- const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath));
859
- const choices = listCompiledMethodChoices(this.rootPath);
860
- return choices.map((method) => {
861
- const activeForPreparations = preparations
862
- .filter((preparation) => (methodIdForSourcePreparationConfig(preparation) ?? DEFAULT_METHOD_ID) === method.id)
863
- .map((preparation) => preparation.name);
864
- return buildMethodResource({
865
- id: method.id,
866
- path: resolveMethodPackageSourcePath(this.rootPath, method.id) ?? method.id,
867
- label: method.label,
868
- hint: method.hint,
869
- source_kind: method.scope === "builtin" ? "builtin" : "local",
870
- built_in: method.scope === "builtin",
871
- active_for_preparations: activeForPreparations,
872
- stages: method.stages.map((stage) => ({
873
- id: stage.id,
874
- label: stage.label,
875
- description: stage.description,
876
- contract_type: stage.contractType,
877
- skill_dir: stage.skillDir,
878
- reads: stage.reads,
879
- writes: stage.writes,
880
- ...(stage.acceptance ? { acceptance: stage.acceptance } : {}),
881
- })),
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
+ });
882
507
  });
883
508
  });
884
509
  }
885
- getMethod(methodId) {
886
- return this.listMethods().find((method) => method.id === methodId) ?? null;
510
+ getMethod(prepDataDir, methodId) {
511
+ return this.listMethods(prepDataDir).find((method) => method.id === methodId) ?? null;
887
512
  }
888
- listJobs() {
889
- return newestJobFirst(listJsonFiles(localJobsRoot(this.rootPath))
513
+ listJobs(prepDataDir) {
514
+ return byCreatedAtDesc(listJsonFiles(localJobsRoot(prepDataDir))
890
515
  .map(readLocalJobRunAt)
891
516
  .filter((run) => run !== null));
892
517
  }
893
- getJob(runId) {
894
- 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;
895
520
  }
896
- getJobEvents(runId) {
897
- return this.getJob(runId)?.events ?? null;
521
+ getJobEvents(prepDataDir, runId) {
522
+ return this.getJob(prepDataDir, runId)?.events ?? null;
898
523
  }
899
524
  getExecutorStatus() {
900
525
  const checkedAt = new Date().toISOString();
@@ -964,23 +589,26 @@ export class LocalServiceRuntime {
964
589
  });
965
590
  return this.getExecutorStatus();
966
591
  }
967
- listActionProposals() {
968
- return newestActionProposalFirst(listJsonFiles(actionProposalsRoot(this.rootPath))
592
+ listActionProposals(prepDataDir) {
593
+ return byCreatedAtDesc(listJsonFiles(actionProposalsRoot(prepDataDir))
969
594
  .map(readActionProposalAt)
970
595
  .filter((proposal) => proposal !== null));
971
596
  }
972
- getActionProposal(proposalId) {
973
- 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;
974
599
  }
975
- async createActionProposal(requestValue) {
600
+ async createActionProposal(prepDataDir, requestValue) {
976
601
  const request = ActionProposalCreateRequestSchema.parse(requestValue);
977
- const proposal = await this.buildActionProposal(request);
978
- this.writeActionProposal(proposal);
602
+ const proposal = ActionProposalResourceSchema.parse({
603
+ ...(await this.buildActionProposal(prepDataDir, request)),
604
+ client_origin: request.client_origin,
605
+ });
606
+ this.writeActionProposal(prepDataDir, proposal);
979
607
  return proposal;
980
608
  }
981
- async decideActionProposal(proposalId, requestValue) {
609
+ async decideActionProposal(prepDataDir, proposalId, requestValue) {
982
610
  const decision = ActionProposalApprovalRequestSchema.parse(requestValue);
983
- const current = this.getActionProposal(proposalId);
611
+ const current = this.getActionProposal(prepDataDir, proposalId);
984
612
  if (!current)
985
613
  return null;
986
614
  if (current.status !== "awaiting_approval") {
@@ -997,11 +625,11 @@ export class LocalServiceRuntime {
997
625
  ...(decision.note ? { note: decision.note } : {}),
998
626
  },
999
627
  });
1000
- this.writeActionProposal(decided);
628
+ this.writeActionProposal(prepDataDir, decided);
1001
629
  if (!decision.approved)
1002
630
  return decided;
1003
631
  try {
1004
- const submission = await this.submitActionProposal(decided);
632
+ const submission = await this.submitActionProposal(prepDataDir, decided);
1005
633
  const submitted = ActionProposalResourceSchema.parse({
1006
634
  ...decided,
1007
635
  status: "submitted",
@@ -1009,7 +637,7 @@ export class LocalServiceRuntime {
1009
637
  submitted_run_id: submission.runId,
1010
638
  submitted_run_type: submission.runType,
1011
639
  });
1012
- this.writeActionProposal(submitted);
640
+ this.writeActionProposal(prepDataDir, submitted);
1013
641
  return submitted;
1014
642
  }
1015
643
  catch (error) {
@@ -1019,25 +647,25 @@ export class LocalServiceRuntime {
1019
647
  updated_at: new Date().toISOString(),
1020
648
  error: error instanceof Error ? error.message : String(error),
1021
649
  });
1022
- this.writeActionProposal(failed);
650
+ this.writeActionProposal(prepDataDir, failed);
1023
651
  return failed;
1024
652
  }
1025
653
  }
1026
- listRunObservability() {
654
+ listRunObservability(prepDataDir) {
1027
655
  return [
1028
- ...this.listCompileRuns().map((resource) => compileRunToObservability(resource.run)),
1029
- ...this.listTestRuns().map(testRunToObservability),
1030
- ...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),
1031
659
  ].sort((left, right) => {
1032
660
  const leftTime = timestampKey(left.started_at ?? left.created_at ?? left.finished_at);
1033
661
  const rightTime = timestampKey(right.started_at ?? right.created_at ?? right.finished_at);
1034
662
  return rightTime - leftTime;
1035
663
  });
1036
664
  }
1037
- getRunObservability(runId) {
1038
- 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;
1039
667
  }
1040
- createJobRun(requestValue) {
668
+ createJobRun(prepDataDir, requestValue) {
1041
669
  const request = LocalJobRunCreateRequestSchema.parse(requestValue);
1042
670
  const runId = createRunId("job");
1043
671
  const now = new Date().toISOString();
@@ -1069,12 +697,12 @@ export class LocalServiceRuntime {
1069
697
  },
1070
698
  ],
1071
699
  });
1072
- this.writeJobRun(run);
700
+ this.writeJobRun(prepDataDir, run);
1073
701
  return run;
1074
702
  }
1075
- appendJobRunEvent(runId, requestValue) {
703
+ appendJobRunEvent(prepDataDir, runId, requestValue) {
1076
704
  const request = LocalJobEventAppendRequestSchema.parse(requestValue);
1077
- const current = this.getJob(runId);
705
+ const current = this.getJob(prepDataDir, runId);
1078
706
  if (!current)
1079
707
  return null;
1080
708
  const event = {
@@ -1089,12 +717,12 @@ export class LocalServiceRuntime {
1089
717
  ...(request.output ? { output: request.output } : {}),
1090
718
  };
1091
719
  const next = LocalJobRunResourceSchema.parse(applyEventToLocalJob(current, event));
1092
- this.writeJobRun(next);
720
+ this.writeJobRun(prepDataDir, next);
1093
721
  return next;
1094
722
  }
1095
- async createReadinessCheckDraftRun(requestValue) {
723
+ async createReadinessCheckDraftRun(prepDataDir, requestValue) {
1096
724
  const request = ReadinessCheckDraftCreateRequestSchema.parse(requestValue);
1097
- const job = this.createJobRun({
725
+ const job = this.createJobRun(prepDataDir, {
1098
726
  job_type: "readiness-check-draft",
1099
727
  title: `Draft readiness checks for ${request.preparation}`,
1100
728
  preparation: request.preparation,
@@ -1122,7 +750,7 @@ export class LocalServiceRuntime {
1122
750
  },
1123
751
  ],
1124
752
  });
1125
- this.appendJobRunEvent(job.run_id, {
753
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1126
754
  type: "step.started",
1127
755
  step_id: "read-source",
1128
756
  message: "Reading source files for readiness-check evidence.",
@@ -1131,16 +759,16 @@ export class LocalServiceRuntime {
1131
759
  source_folder_path: request.source_folder_path,
1132
760
  },
1133
761
  });
1134
- this.appendJobRunEvent(job.run_id, {
762
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1135
763
  type: "step.completed",
1136
764
  step_id: "read-source",
1137
- message: "Source folder is ready for readiness-check drafting.",
765
+ message: "Source folder is ready for drafting readiness checks.",
1138
766
  output: {
1139
767
  preparation: request.preparation,
1140
768
  source_folder_path: request.source_folder_path,
1141
769
  },
1142
770
  });
1143
- this.appendJobRunEvent(job.run_id, {
771
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1144
772
  type: "step.started",
1145
773
  step_id: "agent-draft",
1146
774
  message: "Drafting saved readiness checks from the source files.",
@@ -1149,121 +777,242 @@ export class LocalServiceRuntime {
1149
777
  target_count: request.target_count,
1150
778
  },
1151
779
  });
1152
- void this.runReadinessCheckDraftInBackground(request, job.run_id);
1153
- 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;
782
+ }
783
+ applyMethodChange(prepDataDir, requestValue) {
784
+ const request = MethodChangeCreateRequestSchema.parse(requestValue);
785
+ const outputPath = request.operation === "duplicate"
786
+ ? methodDefinitionPath(prepDataDir, request.new_method_id)
787
+ : methodDefinitionPath(prepDataDir, request.method);
788
+ if (request.operation === "duplicate") {
789
+ if (resolveMethodPackageSourcePath(prepDataDir, request.new_method_id)) {
790
+ throw new Error(`Method "${request.new_method_id}" already exists.`);
791
+ }
792
+ if (!resolveMethodPackageSourcePath(prepDataDir, request.method)) {
793
+ throw new Error(`Method "${request.method}" does not exist.`);
794
+ }
795
+ const label = request.label ?? methodLabelFromId(request.new_method_id);
796
+ const hint = request.hint ?? `Duplicate of ${request.method}`;
797
+ const methodPath = seedLocalMethodPackageFromBase({
798
+ prepDataDir,
799
+ baseMethodId: request.method,
800
+ methodId: request.new_method_id,
801
+ label,
802
+ hint,
803
+ });
804
+ this.methodListingCache.invalidate(prepDataDir);
805
+ return MethodChangeResultSchema.parse({
806
+ kind: "interf-method-change-result",
807
+ version: 1,
808
+ operation: "duplicate",
809
+ method: request.method,
810
+ new_method_id: request.new_method_id,
811
+ updated_preparations: [],
812
+ method_path: methodPath,
813
+ changed: true,
814
+ message: `Duplicated Method ${request.method} as ${request.new_method_id}.`,
815
+ });
816
+ }
817
+ if (request.confirmation !== request.method) {
818
+ throw new Error(`Type ${request.method} to confirm Method removal.`);
819
+ }
820
+ const localMethodPath = methodDefinitionPath(prepDataDir, request.method);
821
+ if (request.method === DEFAULT_METHOD_ID || !existsSync(localMethodPath)) {
822
+ throw new Error(`Method "${request.method}" is not a removable local Method.`);
823
+ }
824
+ const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir));
825
+ const updatedPreparations = preparations
826
+ .filter((preparation) => methodIdForSourcePreparationConfig(preparation) === request.method);
827
+ if (updatedPreparations.length > 0) {
828
+ saveSourceFolderConfig(prepDataDir, {
829
+ preparations: preparations.map((preparation) => detachMethodFromPreparation(preparation, request.method)),
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
+ }
835
+ }
836
+ rmSync(outputPath, { recursive: true, force: true });
837
+ this.methodListingCache.invalidate(prepDataDir);
838
+ return MethodChangeResultSchema.parse({
839
+ kind: "interf-method-change-result",
840
+ version: 1,
841
+ operation: "remove",
842
+ method: request.method,
843
+ updated_preparations: updatedPreparations.map((preparation) => preparation.name),
844
+ method_path: outputPath,
845
+ changed: true,
846
+ message: updatedPreparations.length > 0
847
+ ? `Removed Method ${request.method} and cleared it from ${updatedPreparations.length} Preparation(s).`
848
+ : `Removed Method ${request.method}.`,
849
+ });
850
+ }
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
+ });
1154
918
  }
1155
- async createPreparationSetupRun(requestValue) {
919
+ applyPreparationSetup(prepDataDir, requestValue) {
1156
920
  const request = PreparationSetupCreateRequestSchema.parse(requestValue);
1157
921
  const preparationConfig = request.preparation;
1158
- const methodId = methodIdForSourcePreparationConfig(preparationConfig) ?? "interf-default";
922
+ const methodId = methodIdForSourcePreparationConfig(preparationConfig) ?? DEFAULT_METHOD_ID;
1159
923
  const normalizedPreparationConfig = {
1160
924
  ...preparationConfig,
1161
925
  method: methodId,
1162
926
  };
1163
- const sourceFolderPath = resolveSourcePreparationPath(this.rootPath, preparationConfig);
1164
- const job = this.createJobRun({
1165
- job_type: "preparation-setup",
1166
- title: `Create Preparation ${preparationConfig.name}`,
1167
- preparation: preparationConfig.name,
927
+ const sourceFolderPath = resolveSourcePreparationPath(prepDataDir, normalizedPreparationConfig);
928
+ if (!existsSync(sourceFolderPath) || !statSync(sourceFolderPath).isDirectory()) {
929
+ throw new Error(`Source folder "${preparationConfig.path}" is not available.`);
930
+ }
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);
939
+ const operation = request.setup_mode === "select-method" ? "select-method" : "create";
940
+ return PreparationSetupResultSchema.parse({
941
+ kind: "interf-preparation-setup-result",
942
+ version: 1,
943
+ operation,
944
+ preparation: normalizedPreparationConfig.name,
1168
945
  method: methodId,
1169
- source_path: sourceFolderPath,
1170
- agent: this.getExecutorStatus().executor,
1171
- steps: [
1172
- {
1173
- id: "validate-source",
1174
- label: "Validate source folder",
1175
- input: {
1176
- preparation: preparationConfig.name,
1177
- path: preparationConfig.path,
1178
- },
1179
- },
1180
- {
1181
- id: "write-config",
1182
- label: "Save Preparation config",
1183
- input: {
1184
- config_path: join(this.rootPath, "interf", "interf.json"),
1185
- },
1186
- },
1187
- ],
946
+ source_folder_path: sourceFolderPath,
947
+ config_path: preparationConfigPath(asPreparationDataDir(prepDataDir)),
948
+ portable_context_path: preparationPortableContextPath(asPreparationDataDir(prepDataDir), normalizedPreparationConfig.name),
949
+ changed: true,
950
+ message: operation === "select-method"
951
+ ? `Preparation ${normalizedPreparationConfig.name} now uses Method ${methodId}.`
952
+ : `Preparation ${normalizedPreparationConfig.name} is saved.`,
1188
953
  });
1189
- let activeStep = "validate-source";
1190
- try {
1191
- this.appendJobRunEvent(job.run_id, {
1192
- type: "step.started",
1193
- step_id: "validate-source",
1194
- message: "Validating source folder.",
1195
- input: {
1196
- path: preparationConfig.path,
1197
- source_folder_path: sourceFolderPath,
1198
- },
1199
- });
1200
- if (!existsSync(sourceFolderPath) || !statSync(sourceFolderPath).isDirectory()) {
1201
- throw new Error(`Source folder "${preparationConfig.path}" is not available.`);
1202
- }
1203
- this.appendJobRunEvent(job.run_id, {
1204
- type: "step.completed",
1205
- step_id: "validate-source",
1206
- message: "Source folder is available.",
1207
- output: {
1208
- source_folder_path: sourceFolderPath,
1209
- },
1210
- });
1211
- activeStep = "write-config";
1212
- this.appendJobRunEvent(job.run_id, {
1213
- type: "step.started",
1214
- step_id: "write-config",
1215
- message: "Saving Preparation in the control plane config.",
1216
- input: {
1217
- preparation: preparationConfig.name,
1218
- path: preparationConfig.path,
1219
- },
1220
- });
1221
- upsertSourcePreparationConfig(this.rootPath, normalizedPreparationConfig);
1222
- this.appendJobRunEvent(job.run_id, {
1223
- type: "step.completed",
1224
- step_id: "write-config",
1225
- message: "Preparation config saved.",
1226
- output: {
1227
- config_path: join(this.rootPath, "interf", "interf.json"),
1228
- preparation: preparationConfig.name,
1229
- },
1230
- });
1231
- this.setJobRunResult(job.run_id, {
1232
- preparation: normalizedPreparationConfig,
1233
- source_folder_path: sourceFolderPath,
1234
- config_path: join(this.rootPath, "interf", "interf.json"),
1235
- });
1236
- this.appendJobRunEvent(job.run_id, {
1237
- type: "job.completed",
1238
- message: `Preparation ${preparationConfig.name} is saved. Run prepare to build portable context.`,
1239
- });
954
+ }
955
+ applyPreparationChange(prepDataDir, requestValue) {
956
+ const request = PreparationChangeCreateRequestSchema.parse(requestValue);
957
+ if (request.confirmation !== request.preparation) {
958
+ throw new Error(`Type ${request.preparation} to confirm Preparation removal.`);
1240
959
  }
1241
- catch (error) {
1242
- const message = error instanceof Error ? error.message : String(error);
1243
- this.appendJobRunEvent(job.run_id, {
1244
- type: "step.failed",
1245
- step_id: activeStep,
1246
- message,
1247
- output: {
1248
- error: message,
1249
- },
1250
- });
1251
- this.appendJobRunEvent(job.run_id, {
1252
- type: "job.failed",
1253
- message,
1254
- });
960
+ const preparation = findSourcePreparationConfig(loadSourceFolderConfig(prepDataDir), request.preparation);
961
+ if (!preparation) {
962
+ throw new Error(`Preparation "${request.preparation}" is not saved.`);
1255
963
  }
1256
- return this.getJob(job.run_id) ?? job;
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);
969
+ return PreparationChangeResultSchema.parse({
970
+ kind: "interf-preparation-change-result",
971
+ version: 1,
972
+ operation: "remove",
973
+ preparation: request.preparation,
974
+ config_path: preparationConfigPath(asPreparationDataDir(prepDataDir)),
975
+ portable_context_path: preparationPortableContextPath(asPreparationDataDir(prepDataDir), request.preparation),
976
+ portable_context_retained: true,
977
+ changed: true,
978
+ message: `Removed Preparation ${request.preparation}. Portable Context files were retained.`,
979
+ });
1257
980
  }
1258
- async createMethodAuthoringRun(requestValue) {
981
+ applyReset(prepDataDir, requestValue) {
982
+ const request = ResetRequestSchema.parse(requestValue);
983
+ const preparation = findSourcePreparationConfig(loadSourceFolderConfig(prepDataDir), request.preparation);
984
+ if (!preparation) {
985
+ throw new Error(`Preparation "${request.preparation}" is not saved.`);
986
+ }
987
+ const compiledPath = preparationPortableContextPath(asPreparationDataDir(prepDataDir), request.preparation);
988
+ if (!existsSync(compiledPath)) {
989
+ throw new Error(`Portable Context for Preparation "${request.preparation}" does not exist.`);
990
+ }
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);
996
+ return ResetResultSchema.parse({
997
+ kind: "interf-reset-result",
998
+ version: 1,
999
+ preparation: request.preparation,
1000
+ scope: request.scope,
1001
+ portable_context_path: compiledPath,
1002
+ changed: true,
1003
+ message: `Reset ${request.scope} state for Preparation ${request.preparation}.`,
1004
+ });
1005
+ }
1006
+ async createMethodAuthoringRun(prepDataDir, requestValue, jobType = "method-authoring") {
1259
1007
  const request = MethodAuthoringCreateRequestSchema.parse(requestValue);
1260
- const job = this.createJobRun({
1261
- job_type: "method-authoring",
1262
- title: `Draft Method ${request.method_id}`,
1008
+ const isImprovement = jobType === "method-improvement";
1009
+ const job = this.createJobRun(prepDataDir, {
1010
+ job_type: jobType,
1011
+ title: isImprovement ? `Improve Method ${request.method_id}` : `Draft Method ${request.method_id}`,
1263
1012
  preparation: request.preparation ?? null,
1264
1013
  method: request.method_id,
1265
1014
  source_path: request.source_folder_path,
1266
- output_path: join(this.rootPath, "interf", "methods", request.method_id),
1015
+ output_path: preparationMethodPackagePath(asPreparationDataDir(prepDataDir), request.method_id),
1267
1016
  steps: [
1268
1017
  {
1269
1018
  id: "inspect-source",
@@ -1276,7 +1025,7 @@ export class LocalServiceRuntime {
1276
1025
  },
1277
1026
  {
1278
1027
  id: "draft-package",
1279
- label: "Draft Method package",
1028
+ label: isImprovement ? "Improve Method package" : "Draft Method package",
1280
1029
  input: {
1281
1030
  method_id: request.method_id,
1282
1031
  label: request.label,
@@ -1292,17 +1041,17 @@ export class LocalServiceRuntime {
1292
1041
  },
1293
1042
  ],
1294
1043
  });
1295
- this.appendJobRunEvent(job.run_id, {
1044
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1296
1045
  type: "step.started",
1297
1046
  step_id: "inspect-source",
1298
- message: "Inspecting source files for Method drafting.",
1047
+ message: isImprovement ? "Inspecting source files for Method improvement." : "Inspecting source files for Method drafting.",
1299
1048
  input: {
1300
1049
  preparation: request.preparation ?? null,
1301
1050
  source_folder_path: request.source_folder_path,
1302
1051
  checks: request.checks.length,
1303
1052
  },
1304
1053
  });
1305
- this.appendJobRunEvent(job.run_id, {
1054
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1306
1055
  type: "step.completed",
1307
1056
  step_id: "inspect-source",
1308
1057
  message: "Source context is ready.",
@@ -1311,33 +1060,33 @@ export class LocalServiceRuntime {
1311
1060
  checks: request.checks.length,
1312
1061
  },
1313
1062
  });
1314
- this.appendJobRunEvent(job.run_id, {
1063
+ this.appendJobRunEvent(prepDataDir, job.run_id, {
1315
1064
  type: "step.started",
1316
1065
  step_id: "draft-package",
1317
- message: "Drafting Method package.",
1066
+ message: isImprovement ? "Improving Method package." : "Drafting Method package.",
1318
1067
  input: {
1319
1068
  method_id: request.method_id,
1320
1069
  label: request.label,
1321
1070
  task_prompt: request.task_prompt,
1322
1071
  },
1323
1072
  });
1324
- void this.runMethodAuthoringInBackground(request, job.run_id);
1325
- 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;
1326
1075
  }
1327
- listPortableContexts() {
1328
- return listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath))
1329
- .map((preparation) => this.getPortableContext(preparation.name))
1076
+ listPortableContexts(prepDataDir) {
1077
+ return listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir))
1078
+ .map((preparation) => this.getPortableContext(prepDataDir, preparation.name))
1330
1079
  .filter((context) => context !== null);
1331
1080
  }
1332
- getPortableContext(preparationName) {
1333
- const preparation = findSourcePreparationConfig(loadSourceFolderConfig(this.rootPath), preparationName);
1081
+ getPortableContext(prepDataDir, preparationName) {
1082
+ const preparation = findSourcePreparationConfig(loadSourceFolderConfig(prepDataDir), preparationName);
1334
1083
  if (!preparation)
1335
1084
  return null;
1336
- const path = portableContextPath(this.rootPath, preparation.name);
1085
+ const path = preparationPortableContextPath(asPreparationDataDir(prepDataDir), preparation.name);
1337
1086
  const config = readInterfConfig(path);
1338
- const compileRuns = this.listCompileRunsForPreparation(preparation.name);
1339
- const testRuns = this.listTestRunsForPreparation(preparation.name);
1340
- 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);
1341
1090
  const method = config?.method ?? methodIdForSourcePreparationConfig(preparation);
1342
1091
  return PortableContextResourceSchema.parse({
1343
1092
  preparation: preparation.name,
@@ -1350,52 +1099,65 @@ export class LocalServiceRuntime {
1350
1099
  artifacts: uniqueArtifacts(compileRuns[0]?.stages.flatMap((stage) => stage.artifacts) ?? []),
1351
1100
  });
1352
1101
  }
1353
- listCompileRuns() {
1354
- return newestCompileFirst(listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath))
1355
- .flatMap((preparation) => this.listCompileRunsForPreparation(preparation.name))).map((run) => CompileRunResourceSchema.parse({ run }));
1356
- }
1357
- listCompileRunsForPreparation(preparationName) {
1358
- const compiledPath = portableContextPath(this.rootPath, preparationName);
1359
- return newestCompileFirst(listJsonFiles(compileRunsRoot(compiledPath))
1360
- .map(readCompileRunAt)
1361
- .filter((run) => run !== null));
1362
- }
1363
- getCompileRun(runId) {
1364
- 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)) {
1365
1127
  if (resource.run.run_id === runId)
1366
1128
  return resource;
1367
1129
  }
1368
1130
  return null;
1369
1131
  }
1370
- getCompileRunEvents(runId) {
1371
- return this.getCompileRun(runId)?.run.events ?? null;
1132
+ getCompileRunEvents(prepDataDir, runId) {
1133
+ return this.getCompileRun(prepDataDir, runId)?.run.events ?? null;
1372
1134
  }
1373
- getCompileRunProof(runId) {
1374
- const run = this.getCompileRun(runId)?.run;
1135
+ getCompileRunProof(prepDataDir, runId) {
1136
+ const run = this.getCompileRun(prepDataDir, runId)?.run;
1375
1137
  if (!run)
1376
1138
  return null;
1377
1139
  return run.stages
1378
1140
  .map((stage) => stage.latest_proof ?? null)
1379
1141
  .filter((proof) => proof !== null);
1380
1142
  }
1381
- getCompileRunArtifacts(runId) {
1382
- const run = this.getCompileRun(runId)?.run;
1143
+ getCompileRunArtifacts(prepDataDir, runId) {
1144
+ const run = this.getCompileRun(prepDataDir, runId)?.run;
1383
1145
  if (!run)
1384
1146
  return null;
1385
1147
  return uniqueArtifacts(run.stages.flatMap((stage) => stage.artifacts));
1386
1148
  }
1387
- async createCompileRun(requestValue) {
1149
+ async createCompileRun(prepDataDir, requestValue) {
1388
1150
  const request = CompileRunCreateRequestSchema.parse(requestValue);
1389
- const preparationConfig = this.resolvePreparationConfig(request.preparation, {
1151
+ const preparationConfig = this.resolvePreparationConfig(prepDataDir, request.preparation, {
1390
1152
  method: request.method,
1391
1153
  max_attempts: request.max_attempts,
1392
1154
  max_loops: request.max_loops,
1393
1155
  });
1394
- const compiledPath = this.ensureCompiledForRun(preparationConfig);
1156
+ const compiledPath = this.ensureCompiledForRun(prepDataDir, preparationConfig);
1395
1157
  const runId = createRunId("compile");
1396
1158
  const now = new Date().toISOString();
1397
- const method = getCompiledMethod(methodIdForSourcePreparationConfig(preparationConfig) ?? DEFAULT_METHOD_ID, {
1398
- sourcePath: this.rootPath,
1159
+ const method = getCompiledMethod(requireSelectedMethod(preparationConfig), {
1160
+ prepDataDir,
1399
1161
  });
1400
1162
  const stageTotal = method.stages.length;
1401
1163
  const run = CompileRunSchema.parse({
@@ -1406,7 +1168,7 @@ export class LocalServiceRuntime {
1406
1168
  preparation: preparationConfig.name,
1407
1169
  method: method.id,
1408
1170
  backend: "native",
1409
- source_path: resolveSourcePreparationPath(this.rootPath, preparationConfig),
1171
+ source_path: resolveSourcePreparationPath(prepDataDir, preparationConfig),
1410
1172
  portable_context_path: compiledPath,
1411
1173
  created_at: now,
1412
1174
  started_at: now,
@@ -1432,8 +1194,14 @@ export class LocalServiceRuntime {
1432
1194
  }),
1433
1195
  events: [],
1434
1196
  });
1435
- this.writeCompileRun(compiledPath, run);
1436
- 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, {
1437
1205
  type: "run.started",
1438
1206
  event_id: createRunEventId("event"),
1439
1207
  run_id: runId,
@@ -1444,11 +1212,11 @@ export class LocalServiceRuntime {
1444
1212
  backend: "native",
1445
1213
  });
1446
1214
  const sink = {
1447
- emit: (event) => this.recordCompileRunEvent(compiledPath, runId, event),
1215
+ emit: (event) => this.recordCompileRunEvent(prepDataDir, compiledPath, runId, event),
1448
1216
  };
1449
- void this.runCompileInBackground(request, {
1217
+ void this.runCompileInBackground(prepDataDir, request, {
1450
1218
  runId,
1451
- sourcePath: this.rootPath,
1219
+ sourcePath: prepDataDir,
1452
1220
  compiledPath,
1453
1221
  preparationConfig,
1454
1222
  events: sink,
@@ -1456,23 +1224,155 @@ export class LocalServiceRuntime {
1456
1224
  const saved = this.readCompileRun(compiledPath, runId) ?? run;
1457
1225
  return CompileRunResourceSchema.parse({ run: saved });
1458
1226
  }
1459
- listTestRuns() {
1460
- return newestFirst(listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath))
1461
- .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
+ }
1462
1315
  }
1463
- listTestRunsForPreparation(preparationName) {
1464
- const compiledPath = portableContextPath(this.rootPath, preparationName);
1465
- return newestFirst(listJsonFiles(testRunsRoot(compiledPath))
1466
- .map(readTestRunAt)
1467
- .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
+ }
1468
1333
  }
1469
- getTestRun(runId) {
1470
- 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;
1471
1371
  }
1472
- async createTestRun(requestValue) {
1372
+ async createTestRun(prepDataDir, requestValue) {
1473
1373
  const request = TestRunCreateRequestSchema.parse(requestValue);
1474
- const preparationConfig = this.resolvePreparationConfig(request.preparation);
1475
- const compiledPath = portableContextPath(this.rootPath, preparationConfig.name);
1374
+ const preparationConfig = this.resolvePreparationConfig(prepDataDir, request.preparation);
1375
+ const compiledPath = preparationPortableContextPath(asPreparationDataDir(prepDataDir), preparationConfig.name);
1476
1376
  const compiledTarget = createCompiledTestTarget(compiledPath, preparationConfig.name, methodIdForSourcePreparationConfig(preparationConfig) ?? DEFAULT_METHOD_ID);
1477
1377
  const runId = createRunId("test");
1478
1378
  const now = new Date().toISOString();
@@ -1481,31 +1381,42 @@ export class LocalServiceRuntime {
1481
1381
  status: "running",
1482
1382
  preparation: preparationConfig.name,
1483
1383
  mode: request.mode,
1484
- source_path: this.rootPath,
1384
+ source_path: prepDataDir,
1485
1385
  portable_context_path: compiledTarget.eligible ? compiledPath : null,
1486
1386
  started_at: now,
1487
- comparison: null,
1387
+ readiness_run: null,
1488
1388
  events: [],
1489
1389
  });
1490
- this.writeTestRun(compiledPath, initial);
1491
- void this.runTestInBackground(request, {
1390
+ this.writeTestRun(prepDataDir, compiledPath, initial);
1391
+ void this.runTestInBackground(prepDataDir, request, {
1492
1392
  runId,
1493
- sourcePath: this.rootPath,
1393
+ sourcePath: prepDataDir,
1494
1394
  compiledPath,
1495
1395
  preparationConfig,
1496
1396
  }, initial);
1497
1397
  return initial;
1498
1398
  }
1499
- async runCompileInBackground(request, context) {
1399
+ async runCompileInBackground(prepDataDir, request, context) {
1400
+ this.beginActiveRun();
1500
1401
  try {
1501
1402
  if (!this.handlers.createCompileRun) {
1502
1403
  throw new Error("No compile-run handler is configured for this local service.");
1503
1404
  }
1504
1405
  const result = LocalRunHandlerResultSchema.parse(await this.handlers.createCompileRun(request, context));
1505
- this.refreshCompileRunFromRuntime(context.compiledPath, context.runId);
1506
- 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);
1507
1418
  if (!result.ok) {
1508
- await this.recordCompileRunEvent(context.compiledPath, context.runId, {
1419
+ await this.recordCompileRunEvent(prepDataDir, context.compiledPath, context.runId, {
1509
1420
  type: "run.failed",
1510
1421
  event_id: createRunEventId("event"),
1511
1422
  run_id: context.runId,
@@ -1514,7 +1425,7 @@ export class LocalServiceRuntime {
1514
1425
  });
1515
1426
  }
1516
1427
  else {
1517
- await this.recordCompileRunEvent(context.compiledPath, context.runId, {
1428
+ await this.recordCompileRunEvent(prepDataDir, context.compiledPath, context.runId, {
1518
1429
  type: "run.completed",
1519
1430
  event_id: createRunEventId("event"),
1520
1431
  run_id: context.runId,
@@ -1522,37 +1433,44 @@ export class LocalServiceRuntime {
1522
1433
  summary: "Portable context ready.",
1523
1434
  });
1524
1435
  }
1436
+ await this.recordCompileRunEvent(prepDataDir, context.compiledPath, context.runId, this.readinessUpdatedEvent(context.runId, context.preparationConfig.name, this.computePreparationReadiness(prepDataDir, context.preparationConfig)));
1525
1437
  }
1526
1438
  catch (error) {
1527
- await this.recordCompileRunEvent(context.compiledPath, context.runId, {
1439
+ await this.recordCompileRunEvent(prepDataDir, context.compiledPath, context.runId, {
1528
1440
  type: "run.failed",
1529
1441
  event_id: createRunEventId("event"),
1530
1442
  run_id: context.runId,
1531
1443
  timestamp: createRunEventTimestamp(),
1532
1444
  error: error instanceof Error ? error.message : String(error),
1533
1445
  });
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();
1534
1451
  }
1535
1452
  }
1536
- async runTestInBackground(request, context, initial) {
1453
+ async runTestInBackground(prepDataDir, request, context, initial) {
1454
+ this.beginActiveRun();
1537
1455
  try {
1538
1456
  if (!this.handlers.createTestRun) {
1539
1457
  throw new Error("No test-run handler is configured for this local service.");
1540
1458
  }
1541
1459
  const result = LocalRunHandlerResultSchema.parse(await this.handlers.createTestRun(request, context));
1542
- const comparison = result.comparison ?? this.readLatestComparison(context.preparationConfig.name);
1543
- const resultEvent = comparison
1544
- ? this.checksEvaluatedEvent(context.runId, comparison)
1460
+ const readinessRun = result.readiness_run ?? this.readLatestReadinessRun(prepDataDir, context.preparationConfig.name);
1461
+ const resultEvent = readinessRun
1462
+ ? this.checksEvaluatedEvent(context.runId, readinessRun)
1545
1463
  : null;
1546
1464
  const nextWithoutReadiness = TestRunResourceSchema.parse({
1547
1465
  ...initial,
1548
1466
  status: result.ok ? "succeeded" : "failed",
1549
1467
  finished_at: new Date().toISOString(),
1550
- comparison,
1468
+ readiness_run: readinessRun,
1551
1469
  events: resultEvent ? [resultEvent] : [],
1552
1470
  ...(!result.ok ? { error: result.error ?? "Readiness check failed." } : {}),
1553
1471
  });
1554
- this.writeTestRun(context.compiledPath, nextWithoutReadiness);
1555
- const readiness = this.computePreparationReadiness(context.preparationConfig);
1472
+ this.writeTestRun(prepDataDir, context.compiledPath, nextWithoutReadiness);
1473
+ const readiness = this.computePreparationReadiness(prepDataDir, context.preparationConfig);
1556
1474
  const next = TestRunResourceSchema.parse({
1557
1475
  ...nextWithoutReadiness,
1558
1476
  readiness,
@@ -1561,7 +1479,7 @@ export class LocalServiceRuntime {
1561
1479
  this.readinessUpdatedEvent(context.runId, context.preparationConfig.name, readiness),
1562
1480
  ],
1563
1481
  });
1564
- this.writeTestRun(context.compiledPath, next);
1482
+ this.writeTestRun(prepDataDir, context.compiledPath, next);
1565
1483
  }
1566
1484
  catch (error) {
1567
1485
  const failedWithoutReadiness = TestRunResourceSchema.parse({
@@ -1570,23 +1488,32 @@ export class LocalServiceRuntime {
1570
1488
  finished_at: new Date().toISOString(),
1571
1489
  error: error instanceof Error ? error.message : String(error),
1572
1490
  });
1573
- this.writeTestRun(context.compiledPath, failedWithoutReadiness);
1574
- const readiness = this.computePreparationReadiness(context.preparationConfig);
1491
+ this.writeTestRun(prepDataDir, context.compiledPath, failedWithoutReadiness);
1492
+ const readiness = this.computePreparationReadiness(prepDataDir, context.preparationConfig);
1575
1493
  const next = TestRunResourceSchema.parse({
1576
1494
  ...failedWithoutReadiness,
1577
1495
  readiness,
1578
1496
  events: [this.readinessUpdatedEvent(context.runId, context.preparationConfig.name, readiness)],
1579
1497
  });
1580
- this.writeTestRun(context.compiledPath, next);
1498
+ this.writeTestRun(prepDataDir, context.compiledPath, next);
1499
+ }
1500
+ finally {
1501
+ this.endActiveRun();
1581
1502
  }
1582
1503
  }
1583
- 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) {
1584
1511
  try {
1585
1512
  if (!this.handlers.createReadinessCheckDraft) {
1586
1513
  throw new Error("No readiness-check-draft handler is configured for this local service.");
1587
1514
  }
1588
- const result = ReadinessCheckDraftResultSchema.parse(await this.handlers.createReadinessCheckDraft(request, this.jobRunContext(runId)));
1589
- this.appendJobRunEvent(runId, {
1515
+ const result = ReadinessCheckDraftResultSchema.parse(await this.handlers.createReadinessCheckDraft(request, this.jobRunContext(prepDataDir, runId)));
1516
+ this.appendJobRunEvent(prepDataDir, runId, {
1590
1517
  type: "step.completed",
1591
1518
  step_id: "agent-draft",
1592
1519
  message: `Drafted ${result.checks.length} readiness checks.`,
@@ -1594,15 +1521,15 @@ export class LocalServiceRuntime {
1594
1521
  checks: result.checks,
1595
1522
  },
1596
1523
  });
1597
- this.appendJobRunEvent(runId, {
1524
+ this.appendJobRunEvent(prepDataDir, runId, {
1598
1525
  type: "step.started",
1599
1526
  step_id: "normalize-checks",
1600
- message: "Normalizing readiness-check draft into saved check records.",
1527
+ message: "Normalizing drafted readiness checks into saved check records.",
1601
1528
  input: {
1602
1529
  checks: result.checks.length,
1603
1530
  },
1604
1531
  });
1605
- this.appendJobRunEvent(runId, {
1532
+ this.appendJobRunEvent(prepDataDir, runId, {
1606
1533
  type: "step.completed",
1607
1534
  step_id: "normalize-checks",
1608
1535
  message: `${result.checks.length} readiness checks ready for review.`,
@@ -1610,15 +1537,15 @@ export class LocalServiceRuntime {
1610
1537
  checks: result.checks.length,
1611
1538
  },
1612
1539
  });
1613
- this.setJobRunResult(runId, result);
1614
- this.appendJobRunEvent(runId, {
1540
+ this.setJobRunResult(prepDataDir, runId, result);
1541
+ this.appendJobRunEvent(prepDataDir, runId, {
1615
1542
  type: "job.completed",
1616
1543
  message: `Drafted ${result.checks.length} readiness checks.`,
1617
1544
  });
1618
1545
  }
1619
1546
  catch (error) {
1620
1547
  const message = error instanceof Error ? error.message : String(error);
1621
- this.appendJobRunEvent(runId, {
1548
+ this.appendJobRunEvent(prepDataDir, runId, {
1622
1549
  type: "step.failed",
1623
1550
  step_id: "agent-draft",
1624
1551
  message,
@@ -1626,20 +1553,26 @@ export class LocalServiceRuntime {
1626
1553
  error: message,
1627
1554
  },
1628
1555
  });
1629
- this.appendJobRunEvent(runId, {
1556
+ this.appendJobRunEvent(prepDataDir, runId, {
1630
1557
  type: "job.failed",
1631
1558
  message,
1632
1559
  });
1633
1560
  }
1634
1561
  }
1635
- 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) {
1636
1569
  try {
1637
1570
  if (!this.handlers.createMethodAuthoringRun) {
1638
1571
  throw new Error("No Method-authoring handler is configured for this local service.");
1639
1572
  }
1640
- const result = MethodAuthoringResultSchema.parse(await this.handlers.createMethodAuthoringRun(request, this.jobRunContext(runId)));
1641
- this.setJobRunResult(runId, result);
1642
- 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, {
1643
1576
  type: result.status === "executor-failed" ? "step.failed" : "step.completed",
1644
1577
  step_id: "draft-package",
1645
1578
  message: result.summary,
@@ -1650,7 +1583,7 @@ export class LocalServiceRuntime {
1650
1583
  shell_path: result.shell_path,
1651
1584
  },
1652
1585
  });
1653
- this.appendJobRunEvent(runId, {
1586
+ this.appendJobRunEvent(prepDataDir, runId, {
1654
1587
  type: "step.started",
1655
1588
  step_id: "validate-package",
1656
1589
  message: "Validating Method package structure and stage contract.",
@@ -1659,7 +1592,7 @@ export class LocalServiceRuntime {
1659
1592
  },
1660
1593
  });
1661
1594
  if (result.status === "updated" || result.status === "no-change") {
1662
- this.appendJobRunEvent(runId, {
1595
+ this.appendJobRunEvent(prepDataDir, runId, {
1663
1596
  type: "step.completed",
1664
1597
  step_id: "validate-package",
1665
1598
  message: result.summary,
@@ -1668,13 +1601,13 @@ export class LocalServiceRuntime {
1668
1601
  validation: result.validation ?? null,
1669
1602
  },
1670
1603
  });
1671
- this.appendJobRunEvent(runId, {
1604
+ this.appendJobRunEvent(prepDataDir, runId, {
1672
1605
  type: "job.completed",
1673
1606
  message: result.summary,
1674
1607
  });
1675
1608
  }
1676
1609
  else {
1677
- this.appendJobRunEvent(runId, {
1610
+ this.appendJobRunEvent(prepDataDir, runId, {
1678
1611
  type: "step.failed",
1679
1612
  step_id: "validate-package",
1680
1613
  message: result.summary,
@@ -1683,7 +1616,7 @@ export class LocalServiceRuntime {
1683
1616
  validation: result.validation ?? null,
1684
1617
  },
1685
1618
  });
1686
- this.appendJobRunEvent(runId, {
1619
+ this.appendJobRunEvent(prepDataDir, runId, {
1687
1620
  type: "job.failed",
1688
1621
  message: result.summary,
1689
1622
  });
@@ -1691,7 +1624,7 @@ export class LocalServiceRuntime {
1691
1624
  }
1692
1625
  catch (error) {
1693
1626
  const message = error instanceof Error ? error.message : String(error);
1694
- this.appendJobRunEvent(runId, {
1627
+ this.appendJobRunEvent(prepDataDir, runId, {
1695
1628
  type: "step.failed",
1696
1629
  step_id: "draft-package",
1697
1630
  message,
@@ -1699,29 +1632,29 @@ export class LocalServiceRuntime {
1699
1632
  error: message,
1700
1633
  },
1701
1634
  });
1702
- this.appendJobRunEvent(runId, {
1635
+ this.appendJobRunEvent(prepDataDir, runId, {
1703
1636
  type: "job.failed",
1704
1637
  message,
1705
1638
  });
1706
1639
  }
1707
1640
  }
1708
- jobRunContext(runId) {
1641
+ jobRunContext(prepDataDir, runId) {
1709
1642
  return {
1710
1643
  runId,
1711
- sourcePath: this.rootPath,
1644
+ sourcePath: prepDataDir,
1712
1645
  emit: (event) => {
1713
- this.appendJobRunEvent(runId, event);
1646
+ this.appendJobRunEvent(prepDataDir, runId, event);
1714
1647
  },
1715
1648
  };
1716
1649
  }
1717
- defaultPreparationName() {
1718
- const preparation = listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath))[0];
1650
+ defaultPreparationName(prepDataDir) {
1651
+ const preparation = listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir))[0];
1719
1652
  if (!preparation) {
1720
1653
  throw new Error("No Preparation is saved in this control plane folder.");
1721
1654
  }
1722
1655
  return preparation.name;
1723
1656
  }
1724
- async planActionProposal(request) {
1657
+ async planActionProposal(prepDataDir, request) {
1725
1658
  if (!this.handlers.planActionProposal) {
1726
1659
  return ActionProposalPlanSchema.parse({
1727
1660
  action_type: "clarification",
@@ -1729,15 +1662,15 @@ export class LocalServiceRuntime {
1729
1662
  assistant_message: "No local action planner is configured for this Interf Workspace.",
1730
1663
  });
1731
1664
  }
1732
- const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(this.rootPath));
1665
+ const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir));
1733
1666
  let rawPlan;
1734
1667
  try {
1735
1668
  rawPlan = await this.handlers.planActionProposal(request, {
1736
- sourcePath: this.rootPath,
1669
+ sourcePath: prepDataDir,
1737
1670
  preparations,
1738
1671
  preparationHealth: preparations.map((preparation) => {
1739
1672
  const readinessChecks = preparation.checks?.length ?? 0;
1740
- const portableContextReady = hasCompiledTestTarget(this.rootPath, preparation);
1673
+ const portableContextReady = hasCompiledTestTarget(prepDataDir, preparation);
1741
1674
  return {
1742
1675
  name: preparation.name,
1743
1676
  readiness_checks: readinessChecks,
@@ -1750,8 +1683,8 @@ export class LocalServiceRuntime {
1750
1683
  : ["prepare"],
1751
1684
  };
1752
1685
  }),
1753
- sourceFolders: listSourceFolderChoices(this.rootPath),
1754
- recentProposals: this.listActionProposals().slice(0, 5),
1686
+ sourceFolders: listSourceFolderChoices(prepDataDir),
1687
+ recentProposals: this.listActionProposals(prepDataDir).slice(0, 5),
1755
1688
  });
1756
1689
  }
1757
1690
  catch {
@@ -1770,60 +1703,84 @@ export class LocalServiceRuntime {
1770
1703
  assistant_message: ACTION_PLANNER_CLARIFICATION_MESSAGE,
1771
1704
  });
1772
1705
  }
1773
- buildPreparationSetupRequest(request, plan, values) {
1774
- const sourceFolderChoices = listSourceFolderChoices(this.rootPath);
1775
- const selectedPath = preparationSetupPathValue(values) ??
1776
- (sourceFolderChoices.length === 1 ? sourceFolderChoices[0].value : null);
1777
- if (!selectedPath) {
1778
- return {
1779
- error: "Tell Interf which source folder to use for the Preparation.",
1780
- };
1781
- }
1782
- let normalizedPath;
1783
- try {
1784
- normalizedPath = normalizeSourcePreparationPathForConfig(this.rootPath, selectedPath);
1785
- }
1786
- catch (error) {
1787
- return {
1788
- error: error instanceof Error ? error.message : String(error),
1789
- };
1790
- }
1791
- const explicitName = preparationSetupNameValue(values) ?? plan.preparation ?? request.preparation;
1792
- const preparationName = explicitName
1793
- ? slugFromText(explicitName)
1794
- : defaultPreparationNameForPath(normalizedPath);
1795
- const requestMethodId = stringValue(request.values, "method");
1796
- const valueMethodId = stringValue(values, "method");
1797
- const methodId = requestMethodId ?? plan.method ?? valueMethodId ?? "interf-default";
1798
- const prepareAfterSetup = booleanValue(values, "prepare_after_setup") ?? false;
1799
- try {
1800
- return {
1801
- preparationName,
1802
- methodId,
1803
- prepareAfterSetup,
1804
- request: PreparationSetupCreateRequestSchema.parse({
1805
- prepare_after_setup: prepareAfterSetup,
1806
- preparation: {
1807
- name: preparationName,
1808
- path: normalizedPath,
1809
- about: stringValue(values, "about") ?? stringValue(values, "task_prompt") ?? request.message,
1810
- method: methodId,
1811
- checks: [],
1812
- },
1813
- }),
1814
- };
1815
- }
1816
- catch (error) {
1817
- return {
1818
- error: error instanceof Error ? error.message : String(error),
1819
- };
1706
+ directServiceActionClarification(options) {
1707
+ const endpoint = directServiceEndpointForAction(options.actionType);
1708
+ if (!endpoint) {
1709
+ throw new Error(`Action "${options.actionType}" is not a direct deterministic service action.`);
1820
1710
  }
1711
+ const label = options.actionType === "preparation-setup"
1712
+ ? "Preparation setup"
1713
+ : options.actionType === "method-change"
1714
+ ? "Method change"
1715
+ : "Preparation change";
1716
+ const now = new Date().toISOString();
1717
+ return ActionProposalResourceSchema.parse({
1718
+ kind: "interf-action-proposal",
1719
+ version: 1,
1720
+ proposal_id: createActionProposalId(),
1721
+ status: "needs_clarification",
1722
+ action_type: "clarification",
1723
+ title: `${label} uses a direct service endpoint`,
1724
+ summary: `${label} is deterministic and is not accepted through action proposals.`,
1725
+ assistant_message: `${label} must be submitted directly to ${endpoint}. Action proposals are only for freeform planning or async local-agent-backed work.`,
1726
+ message: options.message,
1727
+ preparation: options.preparation ?? null,
1728
+ method: options.method ?? null,
1729
+ request: {
1730
+ message: options.message,
1731
+ endpoint,
1732
+ action_type: options.actionType,
1733
+ ...(options.values ? { values: options.values } : {}),
1734
+ },
1735
+ created_at: now,
1736
+ updated_at: now,
1737
+ proposed_by_executor: this.getExecutorStatus().executor,
1738
+ approval: null,
1739
+ submitted_run_id: null,
1740
+ submitted_run_type: null,
1741
+ error: null,
1742
+ });
1821
1743
  }
1822
- async buildActionProposal(request) {
1823
- const plan = await this.planActionProposal(request);
1744
+ async buildActionProposal(prepDataDir, request) {
1824
1745
  const structuredPreparationSetup = PreparationSetupActionValuesSchema.safeParse(request.values);
1825
- const actionType = structuredPreparationSetup.success ? "preparation-setup" : plan.action_type;
1826
- const usePlannerText = plan.action_type === actionType;
1746
+ const structuredMethodAuthoring = MethodAuthoringActionValuesSchema.safeParse(request.values);
1747
+ const structuredActionType = actionTypeFromValues(request.values);
1748
+ const structuredDirectActionType = structuredPreparationSetup.success
1749
+ ? "preparation-setup"
1750
+ : structuredActionType && directServiceEndpointForAction(structuredActionType)
1751
+ ? structuredActionType
1752
+ : null;
1753
+ if (structuredDirectActionType) {
1754
+ return this.directServiceActionClarification({
1755
+ actionType: structuredDirectActionType,
1756
+ message: request.message,
1757
+ method: stringValue(request.values, "method") ?? stringValue(request.values, "new_method_id"),
1758
+ preparation: request.preparation ?? stringValue(request.values, "preparation") ?? stringValue(request.values, "name"),
1759
+ values: request.values,
1760
+ });
1761
+ }
1762
+ const structuredPlanActionType = structuredMethodAuthoring.success ? "method-authoring" : structuredActionType;
1763
+ // Typed UI/CLI actions already carry the service contract; only freeform chat needs planner inference.
1764
+ const plan = structuredPlanActionType
1765
+ ? ActionProposalPlanSchema.parse({
1766
+ action_type: structuredPlanActionType,
1767
+ ...(request.preparation ? { preparation: request.preparation } : {}),
1768
+ })
1769
+ : await this.planActionProposal(prepDataDir, request);
1770
+ const actionType = structuredPlanActionType ?? plan.action_type;
1771
+ if (directServiceEndpointForAction(actionType)) {
1772
+ return this.directServiceActionClarification({
1773
+ actionType,
1774
+ message: request.message,
1775
+ method: plan.method ?? stringValue(plan.values, "method") ?? stringValue(request.values, "method"),
1776
+ preparation: plan.preparation ?? request.preparation ?? stringValue(plan.values, "preparation") ?? stringValue(request.values, "preparation"),
1777
+ values: {
1778
+ ...(plan.values ?? {}),
1779
+ ...(request.values ?? {}),
1780
+ },
1781
+ });
1782
+ }
1783
+ const usePlannerText = !structuredPlanActionType && plan.action_type === actionType;
1827
1784
  const now = new Date().toISOString();
1828
1785
  if (actionType === "clarification") {
1829
1786
  return ActionProposalResourceSchema.parse({
@@ -1856,61 +1813,55 @@ export class LocalServiceRuntime {
1856
1813
  ...(plan.values ?? {}),
1857
1814
  ...(request.values ?? {}),
1858
1815
  };
1859
- if (actionType === "preparation-setup") {
1860
- const setup = this.buildPreparationSetupRequest(request, plan, proposalValues);
1861
- if ("error" in setup) {
1862
- return ActionProposalResourceSchema.parse({
1863
- kind: "interf-action-proposal",
1864
- version: 1,
1865
- proposal_id: createActionProposalId(),
1866
- status: "needs_clarification",
1867
- action_type: "clarification",
1868
- title: "Clarify Preparation setup",
1869
- summary: "Interf needs a source folder before it can save a Preparation.",
1870
- assistant_message: setup.error,
1871
- message: request.message,
1872
- preparation: plan.preparation ?? request.preparation ?? null,
1873
- method: plan.method ?? null,
1874
- request: {
1875
- message: request.message,
1876
- ...(proposalValues ? { values: proposalValues } : {}),
1877
- },
1878
- created_at: now,
1879
- updated_at: now,
1880
- proposed_by_executor: this.getExecutorStatus().executor,
1881
- approval: null,
1882
- submitted_run_id: null,
1883
- submitted_run_type: null,
1884
- error: null,
1885
- });
1886
- }
1887
- const commandValues = {
1888
- ...proposalValues,
1889
- path: setup.request.preparation.path,
1890
- prepare_after_setup: setup.prepareAfterSetup,
1816
+ if (actionType === "method-authoring" || actionType === "method-improvement") {
1817
+ const requestedPreparationName = plan.preparation ?? request.preparation ?? null;
1818
+ const fallbackPreparation = requestedPreparationName
1819
+ ? null
1820
+ : listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir))[0] ?? null;
1821
+ const preparationConfig = requestedPreparationName
1822
+ ? this.resolvePreparationConfig(prepDataDir, requestedPreparationName)
1823
+ : fallbackPreparation;
1824
+ const preparationPath = preparationConfig
1825
+ ? resolveSourcePreparationPath(prepDataDir, preparationConfig)
1826
+ : resolveConfiguredSourceFolderPath(prepDataDir) ?? prepDataDir;
1827
+ const requestedMethodId = stringValue(request.values, "method_id") ??
1828
+ stringValue(request.values, "method");
1829
+ const plannedMethodId = plan.method ??
1830
+ stringValue(plan.values, "method_id") ??
1831
+ stringValue(plan.values, "method");
1832
+ const methodId = requestedMethodId ?? plannedMethodId ?? methodIdForProposal(request.message, proposalValues);
1833
+ const taskPrompt = actionValueMethodTaskPrompt(proposalValues) ??
1834
+ stringValue(proposalValues, "task_prompt") ??
1835
+ methodAuthoringPromptFallback(request.message, methodId);
1836
+ const hint = stringValue(proposalValues, "hint") ?? methodAuthoringHintFromPrompt(taskPrompt);
1837
+ const actionRequest = {
1838
+ preparation: preparationConfig?.name ?? null,
1839
+ source_folder_path: preparationPath,
1840
+ method_id: methodId,
1841
+ ...(stringValue(proposalValues, "base_method_id") ? { base_method_id: stringValue(proposalValues, "base_method_id") } : {}),
1842
+ ...(stringValue(proposalValues, "reference_method_id") ? { reference_method_id: stringValue(proposalValues, "reference_method_id") } : {}),
1843
+ label: stringValue(proposalValues, "label") ?? methodLabelFromId(methodId),
1844
+ hint,
1845
+ task_prompt: taskPrompt,
1846
+ checks: preparationConfig?.checks ?? [],
1891
1847
  };
1892
1848
  const commandPreview = (usePlannerText ? plan.command_preview : undefined) ??
1893
- actionCommandPreview(actionType, setup.preparationName, setup.methodId, {
1894
- ...commandValues,
1895
- });
1849
+ actionCommandPreview(actionType, preparationConfig?.name ?? null, methodId, proposalValues);
1896
1850
  return ActionProposalResourceSchema.parse({
1897
1851
  kind: "interf-action-proposal",
1898
1852
  version: 1,
1899
1853
  proposal_id: createActionProposalId(),
1900
1854
  status: "awaiting_approval",
1901
1855
  action_type: actionType,
1902
- title: (usePlannerText ? plan.title : undefined) ?? (setup.prepareAfterSetup ? `Prepare ${setup.preparationName}` : `Create Preparation ${setup.preparationName}`),
1903
- summary: (usePlannerText ? plan.summary : undefined) ?? (setup.prepareAfterSetup
1904
- ? "Save this source folder as a Preparation and run the selected Method."
1905
- : "Save this source folder as an Interf Preparation."),
1906
- assistant_message: (usePlannerText ? plan.assistant_message : undefined) ?? actionAssistantMessage(actionType, setup.preparationName, commandPreview, {
1907
- prepareAfterSetup: setup.prepareAfterSetup,
1908
- }),
1856
+ title: (usePlannerText ? plan.title : undefined) ?? `Draft Method ${methodId}`,
1857
+ summary: (usePlannerText ? plan.summary : undefined) ?? "Ask the configured local executor to create a reusable local Method.",
1858
+ assistant_message: (usePlannerText ? plan.assistant_message : undefined) ??
1859
+ actionAssistantMessage(actionType, preparationConfig?.name ?? null, commandPreview),
1909
1860
  command_preview: commandPreview,
1910
1861
  message: request.message,
1911
- preparation: setup.preparationName,
1912
- method: setup.methodId,
1913
- request: setup.request,
1862
+ preparation: preparationConfig?.name ?? null,
1863
+ method: methodId,
1864
+ request: actionRequest,
1914
1865
  created_at: now,
1915
1866
  updated_at: now,
1916
1867
  proposed_by_executor: this.getExecutorStatus().executor,
@@ -1920,16 +1871,15 @@ export class LocalServiceRuntime {
1920
1871
  error: null,
1921
1872
  });
1922
1873
  }
1923
- const preparationConfig = this.resolvePreparationConfig(plan.preparation ?? request.preparation ?? this.defaultPreparationName());
1924
- const preparationPath = resolveSourcePreparationPath(this.rootPath, preparationConfig);
1874
+ const preparationConfig = this.resolvePreparationConfig(prepDataDir, plan.preparation ?? request.preparation ?? this.defaultPreparationName(prepDataDir));
1875
+ const proposalActionType = ActionProposalTypeSchema.parse(actionType);
1876
+ const preparationPath = resolveSourcePreparationPath(prepDataDir, preparationConfig);
1925
1877
  const requestedMethodId = stringValue(request.values, "method_id") ??
1926
1878
  stringValue(request.values, "method");
1927
1879
  const plannedMethodId = plan.method ??
1928
1880
  stringValue(plan.values, "method_id") ??
1929
1881
  stringValue(plan.values, "method");
1930
- const methodId = actionType === "method-authoring"
1931
- ? requestedMethodId ?? plannedMethodId ?? methodIdForProposal(request.message, proposalValues)
1932
- : requestedMethodId ?? plannedMethodId ?? methodIdForSourcePreparationConfig(preparationConfig);
1882
+ const methodId = requestedMethodId ?? plannedMethodId ?? methodIdForSourcePreparationConfig(preparationConfig);
1933
1883
  const clarifyResolvedAction = (options) => ActionProposalResourceSchema.parse({
1934
1884
  kind: "interf-action-proposal",
1935
1885
  version: 1,
@@ -1957,7 +1907,7 @@ export class LocalServiceRuntime {
1957
1907
  if (actionType === "test") {
1958
1908
  const requestedMode = testModeFromValues(proposalValues);
1959
1909
  const hasReadinessChecks = (preparationConfig.checks ?? []).length > 0;
1960
- const portableContextReady = hasCompiledTestTarget(this.rootPath, preparationConfig);
1910
+ const portableContextReady = hasCompiledTestTarget(prepDataDir, preparationConfig);
1961
1911
  if (!hasReadinessChecks) {
1962
1912
  return clarifyResolvedAction({
1963
1913
  title: `Add readiness checks for ${preparationConfig.name}`,
@@ -1965,7 +1915,7 @@ export class LocalServiceRuntime {
1965
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.`,
1966
1916
  });
1967
1917
  }
1968
- if (!portableContextReady && requestedMode !== "raw") {
1918
+ if (!portableContextReady && requestedMode !== "source-files") {
1969
1919
  return clarifyResolvedAction({
1970
1920
  title: `Prepare ${preparationConfig.name} first`,
1971
1921
  summary: "Readiness checks need portable context unless you explicitly ask for a source-files-only baseline.",
@@ -1981,7 +1931,7 @@ export class LocalServiceRuntime {
1981
1931
  };
1982
1932
  }
1983
1933
  if (actionType === "test") {
1984
- const defaultMode = hasCompiledTestTarget(this.rootPath, preparationConfig) ? "both" : "raw";
1934
+ const defaultMode = hasCompiledTestTarget(prepDataDir, preparationConfig) ? "both" : "source-files";
1985
1935
  return {
1986
1936
  preparation: preparationConfig.name,
1987
1937
  mode: testModeValue(proposalValues, defaultMode),
@@ -1995,51 +1945,58 @@ export class LocalServiceRuntime {
1995
1945
  target_count: Math.max(1, Math.min(8, Math.round(numberValue(proposalValues, "target_count") ?? 4))),
1996
1946
  };
1997
1947
  }
1948
+ const fallbackMethodId = methodId ?? methodIdForProposal(request.message, proposalValues);
1949
+ const taskPrompt = actionValueMethodTaskPrompt(proposalValues) ??
1950
+ stringValue(proposalValues, "task_prompt") ??
1951
+ methodAuthoringPromptFallback(request.message, fallbackMethodId);
1952
+ const hint = stringValue(proposalValues, "hint") ?? methodAuthoringHintFromPrompt(taskPrompt);
1998
1953
  return {
1999
1954
  preparation: preparationConfig.name,
2000
1955
  source_folder_path: preparationPath,
2001
- method_id: methodId,
2002
- label: stringValue(proposalValues, "label") ?? `Custom ${preparationConfig.name}`,
2003
- hint: stringValue(proposalValues, "hint") ?? request.message,
2004
- task_prompt: actionValueMethodTaskPrompt(proposalValues) ?? stringValue(proposalValues, "task_prompt") ?? request.message,
1956
+ method_id: fallbackMethodId,
1957
+ ...(stringValue(proposalValues, "base_method_id") ? { base_method_id: stringValue(proposalValues, "base_method_id") } : {}),
1958
+ ...(stringValue(proposalValues, "reference_method_id") ? { reference_method_id: stringValue(proposalValues, "reference_method_id") } : {}),
1959
+ label: stringValue(proposalValues, "label") ?? methodLabelFromId(fallbackMethodId),
1960
+ hint,
1961
+ task_prompt: taskPrompt,
2005
1962
  checks: preparationConfig.checks ?? [],
2006
1963
  };
2007
1964
  })();
2008
1965
  const title = (() => {
2009
1966
  if (plan.title)
2010
1967
  return plan.title;
2011
- if (actionType === "compile")
1968
+ if (proposalActionType === "compile")
2012
1969
  return `Prepare ${preparationConfig.name}`;
2013
- if (actionType === "test")
1970
+ if (proposalActionType === "test")
2014
1971
  return `Check readiness for ${preparationConfig.name}`;
2015
- if (actionType === "readiness-check-draft")
1972
+ if (proposalActionType === "readiness-check-draft")
2016
1973
  return `Draft readiness checks for ${preparationConfig.name}`;
2017
1974
  return `Draft Method ${methodId}`;
2018
1975
  })();
2019
1976
  const summary = (() => {
2020
1977
  if (plan.summary)
2021
1978
  return plan.summary;
2022
- if (actionType === "compile")
1979
+ if (proposalActionType === "compile")
2023
1980
  return "Build portable context agents can use.";
2024
- if (actionType === "test")
1981
+ if (proposalActionType === "test")
2025
1982
  return "Run readiness checks against source files and portable context.";
2026
- if (actionType === "readiness-check-draft")
1983
+ if (proposalActionType === "readiness-check-draft")
2027
1984
  return "Ask the configured local executor to draft saved readiness checks.";
2028
1985
  return "Ask the configured local executor to create a reusable local Method.";
2029
1986
  })();
2030
- const previewValues = actionType === "test"
1987
+ const previewValues = proposalActionType === "test"
2031
1988
  ? { mode: actionRequest.mode }
2032
1989
  : proposalValues;
2033
- const commandPreview = plan.command_preview ?? actionCommandPreview(actionType, preparationConfig.name, methodId, previewValues);
1990
+ const commandPreview = plan.command_preview ?? actionCommandPreview(proposalActionType, preparationConfig.name, methodId, previewValues);
2034
1991
  return ActionProposalResourceSchema.parse({
2035
1992
  kind: "interf-action-proposal",
2036
1993
  version: 1,
2037
1994
  proposal_id: createActionProposalId(),
2038
1995
  status: "awaiting_approval",
2039
- action_type: actionType,
1996
+ action_type: proposalActionType,
2040
1997
  title,
2041
1998
  summary,
2042
- assistant_message: plan.assistant_message ?? actionAssistantMessage(actionType, preparationConfig.name, commandPreview),
1999
+ assistant_message: plan.assistant_message ?? actionAssistantMessage(proposalActionType, preparationConfig.name, commandPreview),
2043
2000
  command_preview: commandPreview,
2044
2001
  message: request.message,
2045
2002
  preparation: preparationConfig.name,
@@ -2054,56 +2011,43 @@ export class LocalServiceRuntime {
2054
2011
  error: null,
2055
2012
  });
2056
2013
  }
2057
- async submitActionProposal(proposal) {
2014
+ async submitActionProposal(prepDataDir, proposal) {
2058
2015
  if (proposal.action_type === "clarification") {
2059
2016
  throw new Error("Clarification proposals cannot be submitted.");
2060
2017
  }
2061
2018
  if (proposal.action_type === "compile") {
2062
- const resource = await this.createCompileRun(proposal.request);
2019
+ const resource = await this.createCompileRun(prepDataDir, proposal.request);
2063
2020
  return {
2064
2021
  runId: resource.run.run_id,
2065
2022
  runType: "compile-run",
2066
2023
  };
2067
2024
  }
2068
2025
  if (proposal.action_type === "test") {
2069
- const resource = await this.createTestRun(proposal.request);
2026
+ const resource = await this.createTestRun(prepDataDir, proposal.request);
2070
2027
  return {
2071
2028
  runId: resource.run_id,
2072
2029
  runType: "test-run",
2073
2030
  };
2074
2031
  }
2075
- if (proposal.action_type === "preparation-setup") {
2076
- const job = await this.createPreparationSetupRun(proposal.request);
2077
- if (proposal.request.prepare_after_setup) {
2078
- const resource = await this.createCompileRun({
2079
- preparation: proposal.request.preparation.name,
2080
- method: methodIdForSourcePreparationConfig(proposal.request.preparation) ?? DEFAULT_METHOD_ID,
2081
- });
2082
- return {
2083
- runId: resource.run.run_id,
2084
- runType: "compile-run",
2085
- };
2086
- }
2087
- return {
2088
- runId: job.run_id,
2089
- runType: "job-run",
2090
- };
2091
- }
2092
2032
  if (proposal.action_type === "readiness-check-draft") {
2093
- const job = await this.createReadinessCheckDraftRun(proposal.request);
2033
+ const job = await this.createReadinessCheckDraftRun(prepDataDir, proposal.request);
2094
2034
  return {
2095
2035
  runId: job.run_id,
2096
2036
  runType: "job-run",
2097
2037
  };
2098
2038
  }
2099
- const job = await this.createMethodAuthoringRun(proposal.request);
2039
+ const directEndpoint = directServiceEndpointForAction(proposal.action_type);
2040
+ if (directEndpoint) {
2041
+ throw new Error(`Action "${proposal.action_type}" must be submitted directly to ${directEndpoint}.`);
2042
+ }
2043
+ const job = await this.createMethodAuthoringRun(prepDataDir, proposal.request, proposal.action_type === "method-improvement" ? "method-improvement" : "method-authoring");
2100
2044
  return {
2101
2045
  runId: job.run_id,
2102
2046
  runType: "job-run",
2103
2047
  };
2104
2048
  }
2105
- resolvePreparationConfig(preparationName, overrides = {}) {
2106
- const preparation = findSourcePreparationConfig(loadSourceFolderConfig(this.rootPath), preparationName);
2049
+ resolvePreparationConfig(prepDataDir, preparationName, overrides = {}) {
2050
+ const preparation = findSourcePreparationConfig(loadSourceFolderConfig(prepDataDir), preparationName);
2107
2051
  if (!preparation) {
2108
2052
  throw new Error(`Preparation "${preparationName}" is not saved in this control plane folder.`);
2109
2053
  }
@@ -2115,49 +2059,59 @@ export class LocalServiceRuntime {
2115
2059
  ...(typeof overrides.max_loops === "number" ? { max_loops: overrides.max_loops } : {}),
2116
2060
  };
2117
2061
  }
2118
- ensureCompiledForRun(preparationConfig) {
2119
- const methodId = methodIdForSourcePreparationConfig(preparationConfig) ?? DEFAULT_METHOD_ID;
2120
- const compiledPath = ensurePortableContextScaffold(this.rootPath, preparationConfig.name, methodId);
2062
+ ensureCompiledForRun(prepDataDir, preparationConfig) {
2063
+ const methodId = requireSelectedMethod(preparationConfig);
2064
+ const compiledPath = ensurePortableContextScaffold(prepDataDir, preparationConfig.name, methodId);
2121
2065
  syncCompiledInterfConfigFromSourcePreparationConfig(compiledPath, preparationConfig);
2122
2066
  return compiledPath;
2123
2067
  }
2124
2068
  readCompileRun(compiledPath, runId) {
2125
2069
  return readCompileRunAt(compileRunPath(compiledPath, runId));
2126
2070
  }
2127
- writeCompileRun(compiledPath, run) {
2071
+ writeCompileRun(prepDataDir, compiledPath, run) {
2128
2072
  mkdirSync(compileRunsRoot(compiledPath), { recursive: true });
2129
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
+ }
2130
2088
  }
2131
- writeJobRun(run) {
2132
- mkdirSync(localJobsRoot(this.rootPath), { recursive: true });
2133
- writeJsonFile(localJobPath(this.rootPath, run.run_id), LocalJobRunResourceSchema.parse(run));
2134
- }
2135
- writeActionProposal(proposal) {
2136
- mkdirSync(actionProposalsRoot(this.rootPath), { recursive: true });
2137
- 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));
2138
2092
  }
2139
- setJobRunResult(runId, result) {
2140
- const current = this.getJob(runId);
2093
+ setJobRunResult(prepDataDir, runId, result) {
2094
+ const current = this.getJob(prepDataDir, runId);
2141
2095
  if (!current)
2142
2096
  return;
2143
2097
  const normalizedResult = result && typeof result === "object" && !Array.isArray(result)
2144
2098
  ? result
2145
2099
  : { value: result };
2146
- this.writeJobRun(LocalJobRunResourceSchema.parse({
2100
+ this.writeJobRun(prepDataDir, LocalJobRunResourceSchema.parse({
2147
2101
  ...current,
2148
2102
  result: normalizedResult,
2149
2103
  }));
2150
2104
  }
2151
- async recordCompileRunEvent(compiledPath, runId, event) {
2105
+ async recordCompileRunEvent(prepDataDir, compiledPath, runId, event) {
2152
2106
  const current = this.readCompileRun(compiledPath, runId);
2153
2107
  if (!current)
2154
2108
  return;
2155
- this.writeCompileRun(compiledPath, applyEventToCompileRun(current, event));
2109
+ this.writeCompileRun(prepDataDir, compiledPath, applyEventToCompileRun(current, event));
2156
2110
  if (event.type === "stage.passed" || event.type === "stage.failed") {
2157
- this.refreshCompileRunFromRuntime(compiledPath, runId);
2111
+ this.refreshCompileRunFromRuntime(prepDataDir, compiledPath, runId);
2158
2112
  }
2159
2113
  }
2160
- refreshCompileRunFromRuntime(compiledPath, runId) {
2114
+ refreshCompileRunFromRuntime(prepDataDir, compiledPath, runId) {
2161
2115
  const current = this.readCompileRun(compiledPath, runId);
2162
2116
  if (!current)
2163
2117
  return;
@@ -2204,9 +2158,9 @@ export class LocalServiceRuntime {
2204
2158
  }),
2205
2159
  };
2206
2160
  next.latest_proof = [...next.stages].reverse().find((stage) => Boolean(stage.latest_proof))?.latest_proof ?? next.latest_proof;
2207
- this.writeCompileRun(compiledPath, next);
2161
+ this.writeCompileRun(prepDataDir, compiledPath, next);
2208
2162
  }
2209
- async emitRuntimeDerivedEvents(compiledPath, runId) {
2163
+ async emitRuntimeDerivedEvents(prepDataDir, compiledPath, runId) {
2210
2164
  const state = loadState(compiledPath);
2211
2165
  const run = this.readCompileRun(compiledPath, runId);
2212
2166
  if (!state?.stages || !run)
@@ -2217,7 +2171,7 @@ export class LocalServiceRuntime {
2217
2171
  continue;
2218
2172
  const artifacts = stageArtifactRefs(stage.stage_id, stageState.artifacts);
2219
2173
  for (const artifact of artifacts) {
2220
- await this.recordCompileRunEvent(compiledPath, runId, {
2174
+ await this.recordCompileRunEvent(prepDataDir, compiledPath, runId, {
2221
2175
  type: "artifact.written",
2222
2176
  event_id: createRunEventId("event"),
2223
2177
  run_id: runId,
@@ -2226,7 +2180,7 @@ export class LocalServiceRuntime {
2226
2180
  artifact,
2227
2181
  });
2228
2182
  }
2229
- await this.recordCompileRunEvent(compiledPath, runId, {
2183
+ await this.recordCompileRunEvent(prepDataDir, compiledPath, runId, {
2230
2184
  type: "proof.updated",
2231
2185
  event_id: createRunEventId("event"),
2232
2186
  run_id: runId,
@@ -2242,11 +2196,11 @@ export class LocalServiceRuntime {
2242
2196
  });
2243
2197
  }
2244
2198
  }
2245
- readLatestComparison(preparationName) {
2246
- return readSavedReadinessCheckRun(this.rootPath, preparationName);
2199
+ readLatestReadinessRun(prepDataDir, preparationName) {
2200
+ return readSavedReadinessCheckRun(prepDataDir, preparationName);
2247
2201
  }
2248
- checksEvaluatedEvent(runId, comparison) {
2249
- const target = comparison.compiled ?? comparison.raw;
2202
+ checksEvaluatedEvent(runId, readinessRun) {
2203
+ const target = readinessRun.compiled ?? readinessRun.source_files;
2250
2204
  return {
2251
2205
  type: "checks.evaluated",
2252
2206
  event_id: createRunEventId("event"),
@@ -2267,9 +2221,11 @@ export class LocalServiceRuntime {
2267
2221
  readiness,
2268
2222
  };
2269
2223
  }
2270
- writeTestRun(compiledPath, run) {
2224
+ writeTestRun(prepDataDir, compiledPath, run) {
2271
2225
  mkdirSync(testRunsRoot(compiledPath), { recursive: true });
2272
2226
  writeJsonFile(testRunPath(compiledPath, run.run_id), TestRunResourceSchema.parse(run));
2227
+ this.testRunCache.invalidatePreparation(prepDataDir, run.preparation);
2228
+ this.readinessCache.invalidatePreparation(prepDataDir, run.preparation);
2273
2229
  }
2274
2230
  }
2275
2231
  export function createLocalServiceRuntime(options) {