@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.
- package/README.md +96 -91
- package/TRADEMARKS.md +2 -13
- package/agent-skills/interf-actions/SKILL.md +97 -32
- package/agent-skills/interf-actions/references/cli.md +124 -71
- package/builtin-methods/interf-default/README.md +3 -4
- package/builtin-methods/interf-default/compile/stages/shape/SKILL.md +2 -2
- package/builtin-methods/interf-default/compile/stages/summarize/SKILL.md +2 -1
- package/builtin-methods/interf-default/improve/SKILL.md +1 -1
- package/builtin-methods/interf-default/method.json +10 -4
- package/builtin-methods/interf-default/method.schema.json +0 -9
- package/builtin-methods/interf-default/use/query/SKILL.md +5 -5
- package/dist/cli/commands/compile.d.ts +9 -31
- package/dist/cli/commands/compile.js +75 -388
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/login.d.ts +7 -0
- package/dist/cli/commands/login.js +39 -0
- package/dist/cli/commands/logout.d.ts +2 -0
- package/dist/cli/commands/logout.js +16 -0
- package/dist/cli/commands/method.d.ts +2 -0
- package/dist/cli/commands/method.js +113 -0
- package/dist/cli/commands/prep.d.ts +2 -0
- package/dist/cli/commands/prep.js +134 -0
- package/dist/cli/commands/reset.d.ts +8 -1
- package/dist/cli/commands/reset.js +47 -15
- package/dist/cli/commands/runs.d.ts +2 -0
- package/dist/cli/commands/runs.js +120 -0
- package/dist/cli/commands/status.d.ts +6 -1
- package/dist/cli/commands/status.js +61 -220
- package/dist/cli/commands/test.d.ts +6 -15
- package/dist/cli/commands/test.js +63 -342
- package/dist/cli/commands/web.d.ts +0 -9
- package/dist/cli/commands/web.js +140 -367
- package/dist/cli/commands/wizard.d.ts +9 -0
- package/dist/cli/commands/wizard.js +442 -0
- package/dist/cli/index.d.ts +7 -6
- package/dist/cli/index.js +13 -10
- package/dist/compiler-ui/404.html +1 -1
- package/dist/compiler-ui/__next.__PAGE__.txt +2 -2
- package/dist/compiler-ui/__next._full.txt +3 -3
- package/dist/compiler-ui/__next._head.txt +1 -1
- package/dist/compiler-ui/__next._index.txt +2 -2
- package/dist/compiler-ui/__next._tree.txt +2 -2
- package/dist/compiler-ui/_next/static/chunks/045gole2ojo3g.css +3 -0
- package/dist/compiler-ui/_next/static/chunks/17t-lulmyawg5.js +89 -0
- package/dist/compiler-ui/_not-found/__next._full.txt +2 -2
- package/dist/compiler-ui/_not-found/__next._head.txt +1 -1
- package/dist/compiler-ui/_not-found/__next._index.txt +2 -2
- package/dist/compiler-ui/_not-found/__next._not-found.__PAGE__.txt +1 -1
- package/dist/compiler-ui/_not-found/__next._not-found.txt +1 -1
- package/dist/compiler-ui/_not-found/__next._tree.txt +2 -2
- package/dist/compiler-ui/_not-found.html +1 -1
- package/dist/compiler-ui/_not-found.txt +2 -2
- package/dist/compiler-ui/index.html +1 -1
- package/dist/compiler-ui/index.txt +3 -3
- package/dist/index.d.ts +0 -23
- package/dist/index.js +0 -16
- package/dist/packages/agents/lib/shells.d.ts +1 -1
- package/dist/packages/agents/lib/shells.js +113 -54
- package/dist/packages/agents/lib/user-config.d.ts +4 -2
- package/dist/packages/agents/lib/user-config.js +15 -7
- package/dist/packages/compiler/compiled-paths.d.ts +9 -2
- package/dist/packages/compiler/compiled-paths.js +30 -15
- package/dist/packages/compiler/compiled-pipeline.js +23 -3
- package/dist/packages/compiler/compiled-stage-plan.js +4 -0
- package/dist/packages/compiler/compiled-target.d.ts +1 -1
- package/dist/packages/compiler/compiled-target.js +1 -1
- package/dist/packages/compiler/index.d.ts +1 -0
- package/dist/packages/compiler/index.js +1 -0
- package/dist/packages/compiler/lib/schema.d.ts +27 -32
- package/dist/packages/compiler/lib/schema.js +2 -13
- package/dist/packages/compiler/method-runs.d.ts +2 -3
- package/dist/packages/compiler/method-runs.js +2 -3
- package/dist/packages/compiler/reset.js +3 -1
- package/dist/packages/compiler/runtime-contracts.js +0 -3
- package/dist/packages/compiler/runtime-prompt.js +1 -1
- package/dist/packages/compiler/source-files.d.ts +46 -0
- package/dist/packages/compiler/source-files.js +149 -0
- package/dist/packages/compiler/state-artifacts.d.ts +3 -2
- package/dist/packages/compiler/state-artifacts.js +4 -3
- package/dist/packages/compiler/state-io.d.ts +3 -2
- package/dist/packages/compiler/state-io.js +11 -5
- package/dist/packages/compiler/state-paths.d.ts +2 -1
- package/dist/packages/compiler/state-paths.js +6 -3
- package/dist/packages/compiler/state-view.d.ts +3 -2
- package/dist/packages/compiler/state-view.js +18 -28
- package/dist/packages/compiler/state.d.ts +4 -4
- package/dist/packages/compiler/state.js +3 -3
- package/dist/packages/contracts/index.d.ts +1 -1
- package/dist/packages/contracts/lib/preparation-paths.d.ts +117 -0
- package/dist/packages/contracts/lib/preparation-paths.js +177 -0
- package/dist/packages/contracts/lib/schema.d.ts +85 -6
- package/dist/packages/contracts/lib/schema.js +46 -2
- package/dist/packages/execution/lib/schema.d.ts +50 -57
- package/dist/packages/execution/lib/schema.js +1 -2
- package/dist/packages/local-service/action-definitions.d.ts +246 -0
- package/dist/packages/local-service/action-definitions.js +1147 -0
- package/dist/packages/local-service/action-planner.d.ts +9 -0
- package/dist/packages/local-service/action-planner.js +135 -0
- package/dist/packages/local-service/action-values.d.ts +1 -23
- package/dist/packages/local-service/action-values.js +1 -31
- package/dist/packages/local-service/client.d.ts +76 -46
- package/dist/packages/local-service/client.js +184 -149
- package/dist/packages/local-service/connection-config.d.ts +38 -0
- package/dist/packages/local-service/connection-config.js +75 -0
- package/dist/packages/local-service/index.d.ts +14 -7
- package/dist/packages/local-service/index.js +8 -4
- package/dist/packages/local-service/instance-paths.d.ts +100 -0
- package/dist/packages/local-service/instance-paths.js +165 -0
- package/dist/packages/local-service/lib/schema.d.ts +689 -2575
- package/dist/packages/local-service/lib/schema.js +260 -101
- package/dist/packages/local-service/native-run-handlers.d.ts +23 -0
- package/dist/{cli/commands/compile-controller.js → packages/local-service/native-run-handlers.js} +204 -20
- package/dist/packages/local-service/preparation-store.d.ts +92 -0
- package/dist/packages/local-service/preparation-store.js +171 -0
- package/dist/{cli/commands/check-draft.d.ts → packages/local-service/readiness-check-draft.d.ts} +2 -2
- package/dist/packages/local-service/routes.d.ts +33 -11
- package/dist/packages/local-service/routes.js +44 -15
- package/dist/packages/local-service/run-observability.js +25 -27
- package/dist/packages/local-service/runtime-caches.d.ts +76 -0
- package/dist/packages/local-service/runtime-caches.js +191 -0
- package/dist/packages/local-service/runtime-event-applier.d.ts +12 -0
- package/dist/packages/local-service/runtime-event-applier.js +177 -0
- package/dist/packages/local-service/runtime-persistence.d.ts +47 -0
- package/dist/packages/local-service/runtime-persistence.js +137 -0
- package/dist/packages/local-service/runtime-proposal-helpers.d.ts +35 -0
- package/dist/packages/local-service/runtime-proposal-helpers.js +251 -0
- package/dist/packages/local-service/runtime-resource-builders.d.ts +52 -0
- package/dist/packages/local-service/runtime-resource-builders.js +149 -0
- package/dist/packages/local-service/runtime.d.ts +201 -44
- package/dist/packages/local-service/runtime.js +1062 -1106
- package/dist/packages/local-service/server.d.ts +15 -0
- package/dist/packages/local-service/server.js +651 -233
- package/dist/packages/local-service/service-registry.d.ts +47 -0
- package/dist/packages/local-service/service-registry.js +137 -0
- package/dist/packages/method-authoring/method-authoring.d.ts +1 -1
- package/dist/packages/method-authoring/method-authoring.js +2 -2
- package/dist/packages/method-authoring/method-improvement.js +1 -1
- package/dist/packages/method-package/builtin-compiled-method.d.ts +4 -5
- package/dist/packages/method-package/builtin-compiled-method.js +8 -14
- package/dist/packages/method-package/context-interface.d.ts +4 -40
- package/dist/packages/method-package/context-interface.js +1 -23
- package/dist/packages/method-package/interf-method-package.d.ts +4 -4
- package/dist/packages/method-package/interf-method-package.js +21 -33
- package/dist/packages/method-package/local-methods.d.ts +10 -6
- package/dist/packages/method-package/local-methods.js +57 -39
- package/dist/packages/method-package/method-definitions.d.ts +8 -34
- package/dist/packages/method-package/method-definitions.js +49 -37
- package/dist/packages/method-package/method-helpers.d.ts +1 -13
- package/dist/packages/method-package/method-helpers.js +8 -42
- package/dist/packages/method-package/method-review-paths.d.ts +1 -1
- package/dist/packages/method-package/method-review-paths.js +5 -5
- package/dist/packages/method-package/method-stage-runner.js +2 -2
- package/dist/packages/method-package/user-methods.d.ts +17 -0
- package/dist/packages/method-package/user-methods.js +77 -0
- package/dist/packages/project-model/index.d.ts +1 -1
- package/dist/packages/project-model/index.js +1 -1
- package/dist/packages/project-model/interf-detect.d.ts +8 -3
- package/dist/packages/project-model/interf-detect.js +34 -34
- package/dist/packages/project-model/interf-scaffold.d.ts +3 -3
- package/dist/packages/project-model/interf-scaffold.js +23 -32
- package/dist/packages/project-model/lib/schema.js +38 -1
- package/dist/packages/project-model/preparation-entries.d.ts +11 -0
- package/dist/packages/project-model/preparation-entries.js +49 -0
- package/dist/packages/project-model/source-config.d.ts +11 -10
- package/dist/packages/project-model/source-config.js +83 -44
- package/dist/packages/project-model/source-folders.d.ts +5 -5
- package/dist/packages/project-model/source-folders.js +14 -14
- package/dist/packages/shared/filesystem.d.ts +7 -0
- package/dist/packages/shared/filesystem.js +97 -10
- package/dist/packages/testing/lib/schema.d.ts +12 -13
- package/dist/packages/testing/lib/schema.js +4 -5
- package/dist/packages/testing/readiness-check-run.d.ts +7 -7
- package/dist/packages/testing/readiness-check-run.js +46 -51
- package/dist/packages/testing/test-execution.js +6 -6
- package/dist/packages/testing/test-paths.js +4 -3
- package/dist/packages/testing/test-sandbox.d.ts +0 -1
- package/dist/packages/testing/test-sandbox.js +14 -30
- package/dist/packages/testing/test-targets.d.ts +1 -1
- package/dist/packages/testing/test-targets.js +6 -6
- package/dist/packages/testing/test.d.ts +1 -1
- package/dist/packages/testing/test.js +1 -1
- package/package.json +6 -26
- package/LICENSE +0 -183
- package/dist/cli/commands/compile-controller.d.ts +0 -17
- package/dist/cli/commands/compiled-flow.d.ts +0 -25
- package/dist/cli/commands/compiled-flow.js +0 -112
- package/dist/cli/commands/control-path.d.ts +0 -11
- package/dist/cli/commands/control-path.js +0 -72
- package/dist/cli/commands/create-method-wizard.d.ts +0 -76
- package/dist/cli/commands/create-method-wizard.js +0 -465
- package/dist/cli/commands/create.d.ts +0 -8
- package/dist/cli/commands/create.js +0 -189
- package/dist/cli/commands/default.d.ts +0 -2
- package/dist/cli/commands/default.js +0 -39
- package/dist/cli/commands/executor-flow.d.ts +0 -29
- package/dist/cli/commands/executor-flow.js +0 -163
- package/dist/cli/commands/init.d.ts +0 -11
- package/dist/cli/commands/init.js +0 -784
- package/dist/cli/commands/list.d.ts +0 -2
- package/dist/cli/commands/list.js +0 -30
- package/dist/cli/commands/preparation-selection.d.ts +0 -6
- package/dist/cli/commands/preparation-selection.js +0 -11
- package/dist/cli/commands/source-config-wizard.d.ts +0 -52
- package/dist/cli/commands/source-config-wizard.js +0 -680
- package/dist/cli/commands/test-flow.d.ts +0 -58
- package/dist/cli/commands/test-flow.js +0 -231
- package/dist/cli/commands/verify.d.ts +0 -2
- package/dist/cli/commands/verify.js +0 -94
- package/dist/compiler-ui/_next/static/chunks/0d~8t0zm6545p.js +0 -118
- package/dist/compiler-ui/_next/static/chunks/0xnel.ax9a.2c.css +0 -3
- package/dist/packages/compiler/raw-snapshot.d.ts +0 -49
- package/dist/packages/compiler/raw-snapshot.js +0 -101
- package/dist/packages/method-package/index.d.ts +0 -11
- package/dist/packages/method-package/index.js +0 -11
- package/dist/packages/method-package/method-stage-policy.d.ts +0 -5
- package/dist/packages/method-package/method-stage-policy.js +0 -31
- package/dist/packages/project-model/project-paths.d.ts +0 -12
- package/dist/packages/project-model/project-paths.js +0 -33
- /package/dist/compiler-ui/_next/static/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_buildManifest.js +0 -0
- /package/dist/compiler-ui/_next/static/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_clientMiddlewareManifest.js +0 -0
- /package/dist/compiler-ui/_next/static/{j7pdoqWrl4YJrJUVnksbl → C6vVfy3aeYuIO3d2AoNvC}/_ssgManifest.js +0 -0
- /package/dist/{cli/commands/check-draft.js → packages/local-service/readiness-check-draft.js} +0 -0
|
@@ -1,635 +1,233 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
2
|
-
import {
|
|
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 {
|
|
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 {
|
|
13
|
-
import {
|
|
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 {
|
|
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,
|
|
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 {
|
|
29
|
+
import { MethodAuthoringActionValuesSchema, PreparationSetupActionValuesSchema, } from "./action-values.js";
|
|
24
30
|
import { compileRunToObservability, jobRunToObservability, testRunToObservability, uniqueArtifacts, } from "./run-observability.js";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
632
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
670
|
-
|
|
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
|
-
|
|
278
|
+
computePreparationReadinessUncached(prepDataDir, preparation) {
|
|
674
279
|
const generatedAt = new Date().toISOString();
|
|
675
|
-
const compiledPath =
|
|
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
|
|
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
|
|
685
|
-
const sourceResult = readinessTargetResult(
|
|
686
|
-
const contextResult = readinessTargetResult(
|
|
687
|
-
const checksStale = Boolean(currentFingerprint &&
|
|
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(
|
|
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(
|
|
809
|
-
const compiledPath =
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
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(
|
|
836
|
-
return discoverSourceFiles(sourceFolderPath,
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
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
|
|
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
|
|
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 =
|
|
978
|
-
|
|
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
|
|
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
|
-
|
|
919
|
+
applyPreparationSetup(prepDataDir, requestValue) {
|
|
1156
920
|
const request = PreparationSetupCreateRequestSchema.parse(requestValue);
|
|
1157
921
|
const preparationConfig = request.preparation;
|
|
1158
|
-
const methodId = methodIdForSourcePreparationConfig(preparationConfig) ??
|
|
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(
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1261
|
-
|
|
1262
|
-
|
|
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:
|
|
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(
|
|
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(
|
|
1081
|
+
getPortableContext(prepDataDir, preparationName) {
|
|
1082
|
+
const preparation = findSourcePreparationConfig(loadSourceFolderConfig(prepDataDir), preparationName);
|
|
1334
1083
|
if (!preparation)
|
|
1335
1084
|
return null;
|
|
1336
|
-
const path =
|
|
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
|
|
1355
|
-
.flatMap((preparation) => this.listCompileRunsForPreparation(preparation.name))).map((run) => CompileRunResourceSchema.parse({ run }));
|
|
1356
|
-
}
|
|
1357
|
-
listCompileRunsForPreparation(preparationName) {
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
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(
|
|
1398
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
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
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
-
|
|
1470
|
-
|
|
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 =
|
|
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:
|
|
1384
|
+
source_path: prepDataDir,
|
|
1485
1385
|
portable_context_path: compiledTarget.eligible ? compiledPath : null,
|
|
1486
1386
|
started_at: now,
|
|
1487
|
-
|
|
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:
|
|
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.
|
|
1506
|
-
|
|
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
|
|
1543
|
-
const resultEvent =
|
|
1544
|
-
? this.checksEvaluatedEvent(context.runId,
|
|
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
|
-
|
|
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
|
|
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:
|
|
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(
|
|
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(
|
|
1665
|
+
const preparations = listSourcePreparationConfigs(loadSourceFolderConfig(prepDataDir));
|
|
1733
1666
|
let rawPlan;
|
|
1734
1667
|
try {
|
|
1735
1668
|
rawPlan = await this.handlers.planActionProposal(request, {
|
|
1736
|
-
sourcePath:
|
|
1669
|
+
sourcePath: prepDataDir,
|
|
1737
1670
|
preparations,
|
|
1738
1671
|
preparationHealth: preparations.map((preparation) => {
|
|
1739
1672
|
const readinessChecks = preparation.checks?.length ?? 0;
|
|
1740
|
-
const portableContextReady = hasCompiledTestTarget(
|
|
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(
|
|
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
|
-
|
|
1774
|
-
const
|
|
1775
|
-
|
|
1776
|
-
(
|
|
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
|
|
1826
|
-
const
|
|
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 === "
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
})
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
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,
|
|
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) ??
|
|
1903
|
-
summary: (usePlannerText ? plan.summary : undefined) ??
|
|
1904
|
-
|
|
1905
|
-
|
|
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:
|
|
1912
|
-
method:
|
|
1913
|
-
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
|
|
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 =
|
|
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(
|
|
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 !== "
|
|
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(
|
|
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:
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
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 (
|
|
1968
|
+
if (proposalActionType === "compile")
|
|
2012
1969
|
return `Prepare ${preparationConfig.name}`;
|
|
2013
|
-
if (
|
|
1970
|
+
if (proposalActionType === "test")
|
|
2014
1971
|
return `Check readiness for ${preparationConfig.name}`;
|
|
2015
|
-
if (
|
|
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 (
|
|
1979
|
+
if (proposalActionType === "compile")
|
|
2023
1980
|
return "Build portable context agents can use.";
|
|
2024
|
-
if (
|
|
1981
|
+
if (proposalActionType === "test")
|
|
2025
1982
|
return "Run readiness checks against source files and portable context.";
|
|
2026
|
-
if (
|
|
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 =
|
|
1987
|
+
const previewValues = proposalActionType === "test"
|
|
2031
1988
|
? { mode: actionRequest.mode }
|
|
2032
1989
|
: proposalValues;
|
|
2033
|
-
const commandPreview = plan.command_preview ?? actionCommandPreview(
|
|
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:
|
|
1996
|
+
action_type: proposalActionType,
|
|
2040
1997
|
title,
|
|
2041
1998
|
summary,
|
|
2042
|
-
assistant_message: plan.assistant_message ?? actionAssistantMessage(
|
|
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
|
|
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(
|
|
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 =
|
|
2120
|
-
const compiledPath = ensurePortableContextScaffold(
|
|
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
|
-
|
|
2132
|
-
mkdirSync(
|
|
2133
|
-
writeJsonFile(
|
|
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
|
-
|
|
2246
|
-
return readSavedReadinessCheckRun(
|
|
2199
|
+
readLatestReadinessRun(prepDataDir, preparationName) {
|
|
2200
|
+
return readSavedReadinessCheckRun(prepDataDir, preparationName);
|
|
2247
2201
|
}
|
|
2248
|
-
checksEvaluatedEvent(runId,
|
|
2249
|
-
const target =
|
|
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) {
|