stageflow 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/dist/agent/activity.d.ts +76 -0
  4. package/dist/agent/activity.js +89 -0
  5. package/dist/agent/activityObserver.d.ts +17 -0
  6. package/dist/agent/activityObserver.js +66 -0
  7. package/dist/agent/cursorExtension.d.ts +12 -0
  8. package/dist/agent/cursorExtension.js +88 -0
  9. package/dist/agent/cursorProvider.d.ts +14 -0
  10. package/dist/agent/cursorProvider.js +139 -0
  11. package/dist/agent/fakeAgent.d.ts +29 -0
  12. package/dist/agent/fakeAgent.js +295 -0
  13. package/dist/agent/oauthSessionManager.d.ts +33 -0
  14. package/dist/agent/oauthSessionManager.js +214 -0
  15. package/dist/agent/piAdapter.d.ts +128 -0
  16. package/dist/agent/piAdapter.js +967 -0
  17. package/dist/agent/port.d.ts +63 -0
  18. package/dist/agent/port.js +46 -0
  19. package/dist/agent/providerAuth.d.ts +101 -0
  20. package/dist/agent/providerAuth.js +282 -0
  21. package/dist/agent/providerSupport.d.ts +28 -0
  22. package/dist/agent/providerSupport.js +19 -0
  23. package/dist/cli/providersCommand.d.ts +11 -0
  24. package/dist/cli/providersCommand.js +275 -0
  25. package/dist/cli/terminalAuthInteraction.d.ts +8 -0
  26. package/dist/cli/terminalAuthInteraction.js +65 -0
  27. package/dist/cli/terminalSecret.d.ts +2 -0
  28. package/dist/cli/terminalSecret.js +67 -0
  29. package/dist/cli/validateCommand.d.ts +11 -0
  30. package/dist/cli/validateCommand.js +81 -0
  31. package/dist/cli/validateOutput.d.ts +8 -0
  32. package/dist/cli/validateOutput.js +75 -0
  33. package/dist/cli.d.ts +2 -0
  34. package/dist/cli.js +273 -0
  35. package/dist/config/createPipeline.d.ts +31 -0
  36. package/dist/config/createPipeline.js +255 -0
  37. package/dist/config/createStage.d.ts +25 -0
  38. package/dist/config/createStage.js +181 -0
  39. package/dist/config/listConfig.d.ts +33 -0
  40. package/dist/config/listConfig.js +195 -0
  41. package/dist/config/listExtensions.d.ts +26 -0
  42. package/dist/config/listExtensions.js +50 -0
  43. package/dist/config/listSkills.d.ts +29 -0
  44. package/dist/config/listSkills.js +55 -0
  45. package/dist/config/loadOutcome.d.ts +27 -0
  46. package/dist/config/loadOutcome.js +6 -0
  47. package/dist/config/loadPipeline.d.ts +25 -0
  48. package/dist/config/loadPipeline.js +196 -0
  49. package/dist/config/loadStage.d.ts +4 -0
  50. package/dist/config/loadStage.js +127 -0
  51. package/dist/config/loadTask.d.ts +7 -0
  52. package/dist/config/loadTask.js +69 -0
  53. package/dist/config/readYamlObject.d.ts +1 -0
  54. package/dist/config/readYamlObject.js +5 -0
  55. package/dist/config/resolvePipelineDag.d.ts +12 -0
  56. package/dist/config/resolvePipelineDag.js +282 -0
  57. package/dist/config/validateCatalog.d.ts +36 -0
  58. package/dist/config/validateCatalog.js +322 -0
  59. package/dist/envelope/check.d.ts +3 -0
  60. package/dist/envelope/check.js +52 -0
  61. package/dist/envelope/payloadSchema.d.ts +10 -0
  62. package/dist/envelope/payloadSchema.js +91 -0
  63. package/dist/hitl/qaTrail.d.ts +31 -0
  64. package/dist/hitl/qaTrail.js +65 -0
  65. package/dist/index.d.ts +13 -0
  66. package/dist/index.js +7 -0
  67. package/dist/mcp/projectRun.d.ts +37 -0
  68. package/dist/mcp/projectRun.js +57 -0
  69. package/dist/mcp/readArtifact.d.ts +2 -0
  70. package/dist/mcp/readArtifact.js +46 -0
  71. package/dist/mcp/server.d.ts +7 -0
  72. package/dist/mcp/server.js +26 -0
  73. package/dist/mcp/tools.d.ts +9 -0
  74. package/dist/mcp/tools.js +96 -0
  75. package/dist/package-meta.d.ts +1 -0
  76. package/dist/package-meta.js +1 -0
  77. package/dist/prompt/priorEnvelope.d.ts +2 -0
  78. package/dist/prompt/priorEnvelope.js +5 -0
  79. package/dist/runstore/catalog.d.ts +8 -0
  80. package/dist/runstore/catalog.js +16 -0
  81. package/dist/runstore/createStore.d.ts +15 -0
  82. package/dist/runstore/createStore.js +23 -0
  83. package/dist/runstore/disk/DiskRunStore.d.ts +30 -0
  84. package/dist/runstore/disk/DiskRunStore.js +238 -0
  85. package/dist/runstore/disk/catalogHelpers.d.ts +11 -0
  86. package/dist/runstore/disk/catalogHelpers.js +102 -0
  87. package/dist/runstore/layout.d.ts +22 -0
  88. package/dist/runstore/layout.js +58 -0
  89. package/dist/runstore/paths.d.ts +7 -0
  90. package/dist/runstore/paths.js +60 -0
  91. package/dist/runstore/pipelineDagSnapshot.d.ts +5 -0
  92. package/dist/runstore/pipelineDagSnapshot.js +43 -0
  93. package/dist/runstore/port.d.ts +146 -0
  94. package/dist/runstore/port.js +28 -0
  95. package/dist/runstore/runProjection.d.ts +6 -0
  96. package/dist/runstore/runProjection.js +102 -0
  97. package/dist/runstore/sqlite/SqliteRunStore.d.ts +48 -0
  98. package/dist/runstore/sqlite/SqliteRunStore.js +460 -0
  99. package/dist/runstore/sqlite/migrateFromDisk.d.ts +5 -0
  100. package/dist/runstore/sqlite/migrateFromDisk.js +108 -0
  101. package/dist/runstore/sqlite/schema.d.ts +1 -0
  102. package/dist/runstore/sqlite/schema.js +59 -0
  103. package/dist/runstore/stageExecution.d.ts +13 -0
  104. package/dist/runstore/stageExecution.js +22 -0
  105. package/dist/runstore/stageSnapshot.d.ts +2 -0
  106. package/dist/runstore/stageSnapshot.js +46 -0
  107. package/dist/runstore/syntheticStageSnapshot.d.ts +2 -0
  108. package/dist/runstore/syntheticStageSnapshot.js +10 -0
  109. package/dist/runstore/trackProjection.d.ts +7 -0
  110. package/dist/runstore/trackProjection.js +94 -0
  111. package/dist/runstore/workspaceLayout.d.ts +31 -0
  112. package/dist/runstore/workspaceLayout.js +172 -0
  113. package/dist/runtime/answerResume.d.ts +16 -0
  114. package/dist/runtime/answerResume.js +22 -0
  115. package/dist/runtime/credentialBinding.d.ts +16 -0
  116. package/dist/runtime/credentialBinding.js +80 -0
  117. package/dist/runtime/dagTraversal.d.ts +3 -0
  118. package/dist/runtime/dagTraversal.js +26 -0
  119. package/dist/runtime/envelopeRouting.d.ts +20 -0
  120. package/dist/runtime/envelopeRouting.js +59 -0
  121. package/dist/runtime/hitlSeams.d.ts +38 -0
  122. package/dist/runtime/hitlSeams.js +2 -0
  123. package/dist/runtime/pipelineRunner.d.ts +77 -0
  124. package/dist/runtime/pipelineRunner.js +141 -0
  125. package/dist/runtime/pipelineScheduler.d.ts +90 -0
  126. package/dist/runtime/pipelineScheduler.js +494 -0
  127. package/dist/runtime/pipelineValidationError.d.ts +6 -0
  128. package/dist/runtime/pipelineValidationError.js +8 -0
  129. package/dist/runtime/resumeReconstruct.d.ts +33 -0
  130. package/dist/runtime/resumeReconstruct.js +144 -0
  131. package/dist/runtime/runManager.d.ts +129 -0
  132. package/dist/runtime/runManager.js +818 -0
  133. package/dist/runtime/runRetryCoordinator.d.ts +85 -0
  134. package/dist/runtime/runRetryCoordinator.js +372 -0
  135. package/dist/runtime/settingsFile.d.ts +17 -0
  136. package/dist/runtime/settingsFile.js +88 -0
  137. package/dist/runtime/stageAttemptBootstrap.d.ts +40 -0
  138. package/dist/runtime/stageAttemptBootstrap.js +82 -0
  139. package/dist/runtime/stageAttemptContext.d.ts +11 -0
  140. package/dist/runtime/stageAttemptContext.js +26 -0
  141. package/dist/runtime/stageConcurrency.d.ts +10 -0
  142. package/dist/runtime/stageConcurrency.js +51 -0
  143. package/dist/runtime/stageHitl.d.ts +95 -0
  144. package/dist/runtime/stageHitl.js +198 -0
  145. package/dist/runtime/stageProcessLauncher.d.ts +45 -0
  146. package/dist/runtime/stageProcessLauncher.js +199 -0
  147. package/dist/runtime/stageRecovery.d.ts +10 -0
  148. package/dist/runtime/stageRecovery.js +43 -0
  149. package/dist/runtime/stageRoots.d.ts +22 -0
  150. package/dist/runtime/stageRoots.js +93 -0
  151. package/dist/runtime/stageRunner.d.ts +59 -0
  152. package/dist/runtime/stageRunner.js +221 -0
  153. package/dist/runtime/stageWorker.d.ts +6 -0
  154. package/dist/runtime/stageWorker.js +109 -0
  155. package/dist/runtime/stageWorkerProtocol.d.ts +27 -0
  156. package/dist/runtime/stageWorkerProtocol.js +16 -0
  157. package/dist/runtime/taskInput.d.ts +15 -0
  158. package/dist/runtime/taskInput.js +38 -0
  159. package/dist/server/http.d.ts +31 -0
  160. package/dist/server/http.js +561 -0
  161. package/dist/server/providerRoutes.d.ts +15 -0
  162. package/dist/server/providerRoutes.js +186 -0
  163. package/dist/tools/askOperator.d.ts +185 -0
  164. package/dist/tools/askOperator.js +409 -0
  165. package/dist/tools/emitStageEnvelope.d.ts +42 -0
  166. package/dist/tools/emitStageEnvelope.js +61 -0
  167. package/dist/tools/writeStageArtifact.d.ts +26 -0
  168. package/dist/tools/writeStageArtifact.js +103 -0
  169. package/dist/types/envelope.d.ts +12 -0
  170. package/dist/types/envelope.js +6 -0
  171. package/dist/types/pipeline.d.ts +25 -0
  172. package/dist/types/pipeline.js +1 -0
  173. package/dist/types/stage.d.ts +12 -0
  174. package/dist/types/stage.js +6 -0
  175. package/dist/types/task.d.ts +7 -0
  176. package/dist/types/task.js +1 -0
  177. package/dist/ui/assets/index-Cry0Tpfx.js +118 -0
  178. package/dist/ui/assets/index-DCsDopak.css +1 -0
  179. package/dist/ui/index.html +21 -0
  180. package/package.json +72 -0
@@ -0,0 +1,26 @@
1
+ import { attemptAgentDir, attemptSessionPath, } from "../runstore/workspaceLayout.js";
2
+ function createAttemptContext(attempt) {
3
+ return {
4
+ attempt,
5
+ eventOptions() {
6
+ if (attempt <= 1)
7
+ return undefined;
8
+ return { attempt };
9
+ },
10
+ sessionPath(workspaceDir, stageId) {
11
+ return attemptSessionPath(workspaceDir, stageId, attempt);
12
+ },
13
+ agentDirPath(workspaceDir, stageId) {
14
+ return attemptAgentDir(workspaceDir, stageId, attempt);
15
+ },
16
+ };
17
+ }
18
+ export function attemptContext(attempt) {
19
+ return createAttemptContext(attempt);
20
+ }
21
+ export function noAttemptContext() {
22
+ return createAttemptContext(1);
23
+ }
24
+ export function resumeSessionFilePath(workspaceDir, stageId, attempt) {
25
+ return attemptContext(attempt).sessionPath(workspaceDir, stageId);
26
+ }
@@ -0,0 +1,10 @@
1
+ export declare const UNLIMITED_CONCURRENCY: number;
2
+ export declare const DEFAULT_MAX_ACTIVE_STAGES_PER_RUN: number;
3
+ export declare const MAX_ACTIVE_STAGES_PER_RUN_ENV = "STAGEFLOW_MAX_ACTIVE_STAGES_PER_RUN";
4
+ export declare const DEFAULT_MAX_ACTIVE_STAGE_PROCESSES: number;
5
+ export declare const MAX_ACTIVE_STAGE_PROCESSES_ENV = "STAGEFLOW_MAX_ACTIVE_STAGE_PROCESSES";
6
+ export declare const STAGE_EXECUTION_ENV = "STAGEFLOW_STAGE_EXECUTION";
7
+ export type StageExecutionMode = "process" | "inprocess";
8
+ export declare function readStageExecutionMode(env?: Record<string, string | undefined>, override?: StageExecutionMode): StageExecutionMode;
9
+ export declare function readMaxActiveStagesPerRun(env?: Record<string, string | undefined>, override?: number): number;
10
+ export declare function readMaxActiveStageProcesses(env?: Record<string, string | undefined>, override?: number): number;
@@ -0,0 +1,51 @@
1
+ export const UNLIMITED_CONCURRENCY = Number.POSITIVE_INFINITY;
2
+ export const DEFAULT_MAX_ACTIVE_STAGES_PER_RUN = UNLIMITED_CONCURRENCY;
3
+ export const MAX_ACTIVE_STAGES_PER_RUN_ENV = "STAGEFLOW_MAX_ACTIVE_STAGES_PER_RUN";
4
+ export const DEFAULT_MAX_ACTIVE_STAGE_PROCESSES = UNLIMITED_CONCURRENCY;
5
+ export const MAX_ACTIVE_STAGE_PROCESSES_ENV = "STAGEFLOW_MAX_ACTIVE_STAGE_PROCESSES";
6
+ export const STAGE_EXECUTION_ENV = "STAGEFLOW_STAGE_EXECUTION";
7
+ export function readStageExecutionMode(env = process.env, override) {
8
+ if (override !== undefined) {
9
+ return override;
10
+ }
11
+ if (env.VITEST === "true") {
12
+ return "inprocess";
13
+ }
14
+ const raw = env[STAGE_EXECUTION_ENV];
15
+ if (raw === "inprocess" || raw === "process") {
16
+ return raw;
17
+ }
18
+ return "process";
19
+ }
20
+ export function readMaxActiveStagesPerRun(env = process.env, override) {
21
+ if (override !== undefined) {
22
+ if (Number.isFinite(override) && override >= 1)
23
+ return override;
24
+ return DEFAULT_MAX_ACTIVE_STAGES_PER_RUN;
25
+ }
26
+ const raw = env[MAX_ACTIVE_STAGES_PER_RUN_ENV];
27
+ if (raw === undefined || raw.trim() === "") {
28
+ return DEFAULT_MAX_ACTIVE_STAGES_PER_RUN;
29
+ }
30
+ const n = Number.parseInt(raw, 10);
31
+ if (!Number.isFinite(n) || n < 1) {
32
+ return DEFAULT_MAX_ACTIVE_STAGES_PER_RUN;
33
+ }
34
+ return n;
35
+ }
36
+ export function readMaxActiveStageProcesses(env = process.env, override) {
37
+ if (override !== undefined) {
38
+ if (Number.isFinite(override) && override >= 1)
39
+ return override;
40
+ return DEFAULT_MAX_ACTIVE_STAGE_PROCESSES;
41
+ }
42
+ const raw = env[MAX_ACTIVE_STAGE_PROCESSES_ENV];
43
+ if (raw === undefined || raw.trim() === "") {
44
+ return DEFAULT_MAX_ACTIVE_STAGE_PROCESSES;
45
+ }
46
+ const n = Number.parseInt(raw, 10);
47
+ if (!Number.isFinite(n) || n < 1) {
48
+ return DEFAULT_MAX_ACTIVE_STAGE_PROCESSES;
49
+ }
50
+ return n;
51
+ }
@@ -0,0 +1,95 @@
1
+ import type { OpaqueAnswer, OpaqueWaitRequest, StageHandle } from "../agent/port.js";
2
+ import type { RunStore } from "../runstore/port.js";
3
+ import { type StageAttemptContext } from "./stageAttemptContext.js";
4
+ import { type AskOperatorPrompt } from "../tools/askOperator.js";
5
+ export type DeliverAnswerResult = {
6
+ ok: true;
7
+ } | {
8
+ ok: false;
9
+ reason: string;
10
+ status: 404 | 409 | 400 | 500;
11
+ };
12
+ export type DeliverAnswerPrefixResult = {
13
+ ok: true;
14
+ mode: "live";
15
+ } | {
16
+ ok: true;
17
+ mode: "restart";
18
+ } | {
19
+ ok: false;
20
+ reason: string;
21
+ status: 400;
22
+ };
23
+ export type AnswerPromptAssertContext = {
24
+ runId: string;
25
+ stageId: string;
26
+ answer: OpaqueAnswer;
27
+ pendingRequest?: OpaqueWaitRequest;
28
+ };
29
+ export type AssertAnswerMatchesPrompt = (ctx: AnswerPromptAssertContext) => void | Promise<void>;
30
+ export type HitlWaitEnterContext = {
31
+ runId: string;
32
+ stageId: string;
33
+ request: OpaqueWaitRequest;
34
+ attempt?: number;
35
+ };
36
+ export type HitlAnswerDeliverContext = {
37
+ runId: string;
38
+ stageId: string;
39
+ answer: OpaqueAnswer;
40
+ pendingRequest?: OpaqueWaitRequest;
41
+ attempt?: number;
42
+ };
43
+ export type HitlQaHooks = {
44
+ onWaitEnter?: (ctx: HitlWaitEnterContext) => void | Promise<void>;
45
+ onAnswerDeliver?: (ctx: HitlAnswerDeliverContext) => void | Promise<void>;
46
+ };
47
+ export type HitlSeams = {
48
+ assertAnswerMatchesPrompt?: AssertAnswerMatchesPrompt;
49
+ qaHooks?: HitlQaHooks;
50
+ };
51
+ export declare function tryParsePendingPrompt(pendingRequest: OpaqueWaitRequest | undefined): AskOperatorPrompt | undefined;
52
+ export declare const t2AssertAnswerMatchesPrompt: AssertAnswerMatchesPrompt;
53
+ export declare function createDefaultHitlQaHooks(store: RunStore): HitlQaHooks;
54
+ export declare function recordWaitEnter(options: {
55
+ store: RunStore;
56
+ runId: string;
57
+ stageId: string;
58
+ request: OpaqueWaitRequest;
59
+ attemptCtx?: StageAttemptContext;
60
+ qaHooks: HitlQaHooks;
61
+ }): Promise<void>;
62
+ export declare function waitKey(runId: string, stageId: string): string;
63
+ /**
64
+ * HITL coordinator: wait-enter + answer-deliver (live and restart), with a
65
+ * single pending source for the T2 assert gate.
66
+ */
67
+ export declare class StageHitlController {
68
+ private readonly options;
69
+ private readonly live;
70
+ private readonly assertAnswerMatchesPrompt;
71
+ private readonly qaHooks;
72
+ constructor(options: {
73
+ store: RunStore;
74
+ seams?: HitlSeams;
75
+ });
76
+ hasLiveWait(runId: string, stageId: string): boolean;
77
+ private latestAttemptForStage;
78
+ private attemptEventOptions;
79
+ /**
80
+ * KTD6 wait-enter: T3 wait-enter hook → lifecycle waiting_for_input → park.
81
+ */
82
+ enterWait(runId: string, stageId: string, handle: StageHandle, request: OpaqueWaitRequest, attemptCtx?: StageAttemptContext): Promise<void>;
83
+ private parkUntilAnswer;
84
+ clearLiveWait(runId: string, stageId: string): void;
85
+ /**
86
+ * Resolve pending for assert: live park map, else trail-derived T2 prompt.
87
+ * Non-T2 string fixtures yield undefined (assert pass-through).
88
+ */
89
+ private resolvePendingRequest;
90
+ /**
91
+ * KTD6 answer-deliver prefix for live and restart:
92
+ * resolve pending → assert → answer hook → resumed → live unpark or restart ok.
93
+ */
94
+ deliverAnswerPrefix(runId: string, stageId: string, opaqueAnswer: OpaqueAnswer, attemptCtx?: StageAttemptContext): Promise<DeliverAnswerPrefixResult>;
95
+ }
@@ -0,0 +1,198 @@
1
+ import { appendOperatorAnswer, appendOperatorPrompt, derivePendingPrompt, } from "../hitl/qaTrail.js";
2
+ import { deriveExecutionPatchFromEvent } from "../runstore/stageExecution.js";
3
+ import { attemptContext } from "./stageAttemptContext.js";
4
+ import { AskOperatorError, assertAnswerMatchesPrompt, normalizePromptIds, parseAskOperatorAnswer, parseAskOperatorParams, } from "../tools/askOperator.js";
5
+ export function tryParsePendingPrompt(pendingRequest) {
6
+ if (pendingRequest === undefined) {
7
+ return undefined;
8
+ }
9
+ try {
10
+ return normalizePromptIds(parseAskOperatorParams(pendingRequest));
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ export const t2AssertAnswerMatchesPrompt = (ctx) => {
17
+ const prompt = tryParsePendingPrompt(ctx.pendingRequest);
18
+ if (prompt === undefined) {
19
+ return;
20
+ }
21
+ try {
22
+ const answer = parseAskOperatorAnswer(ctx.answer);
23
+ assertAnswerMatchesPrompt(prompt, answer);
24
+ }
25
+ catch (err) {
26
+ if (err instanceof AskOperatorError) {
27
+ throw err;
28
+ }
29
+ throw new AskOperatorError(err instanceof Error ? err.message : String(err));
30
+ }
31
+ };
32
+ export function createDefaultHitlQaHooks(store) {
33
+ return {
34
+ async onWaitEnter(ctx) {
35
+ const prompt = tryParsePendingPrompt(ctx.request);
36
+ if (!prompt)
37
+ return;
38
+ const options = attemptContext(ctx.attempt ?? 1).eventOptions();
39
+ await appendOperatorPrompt(store, ctx.runId, ctx.stageId, prompt, options);
40
+ },
41
+ async onAnswerDeliver(ctx) {
42
+ let answer;
43
+ try {
44
+ answer = parseAskOperatorAnswer(ctx.answer);
45
+ }
46
+ catch {
47
+ return;
48
+ }
49
+ const options = attemptContext(ctx.attempt ?? 1).eventOptions();
50
+ await appendOperatorAnswer(store, ctx.runId, ctx.stageId, answer, options);
51
+ },
52
+ };
53
+ }
54
+ export async function recordWaitEnter(options) {
55
+ const { store, runId, stageId, request, qaHooks } = options;
56
+ const resolvedAttempt = options.attemptCtx?.attempt ??
57
+ ((await store.getLatestStageExecution(runId, stageId))?.attempt ?? 1);
58
+ const resolvedCtx = options.attemptCtx ?? attemptContext(resolvedAttempt);
59
+ await qaHooks.onWaitEnter?.({
60
+ runId,
61
+ stageId,
62
+ request,
63
+ attempt: resolvedAttempt,
64
+ });
65
+ const event = { event: "waiting_for_input" };
66
+ await store.appendStageEvent(runId, stageId, event, resolvedCtx.eventOptions());
67
+ try {
68
+ const execution = await store.getStageExecution(runId, stageId, resolvedAttempt);
69
+ const patch = deriveExecutionPatchFromEvent(event, execution);
70
+ if (Object.keys(patch).length === 0)
71
+ return;
72
+ await store.updateStageExecution(runId, stageId, resolvedAttempt, patch);
73
+ }
74
+ catch {
75
+ // no execution row yet
76
+ }
77
+ }
78
+ export function waitKey(runId, stageId) {
79
+ return `${runId}\0${stageId}`;
80
+ }
81
+ /**
82
+ * HITL coordinator: wait-enter + answer-deliver (live and restart), with a
83
+ * single pending source for the T2 assert gate.
84
+ */
85
+ export class StageHitlController {
86
+ options;
87
+ live = new Map();
88
+ assertAnswerMatchesPrompt;
89
+ qaHooks;
90
+ constructor(options) {
91
+ this.options = options;
92
+ this.assertAnswerMatchesPrompt =
93
+ options.seams?.assertAnswerMatchesPrompt ?? t2AssertAnswerMatchesPrompt;
94
+ this.qaHooks =
95
+ options.seams?.qaHooks ?? createDefaultHitlQaHooks(options.store);
96
+ }
97
+ hasLiveWait(runId, stageId) {
98
+ return this.live.has(waitKey(runId, stageId));
99
+ }
100
+ async latestAttemptForStage(runId, stageId) {
101
+ const latest = await this.options.store.getLatestStageExecution(runId, stageId);
102
+ return latest?.attempt ?? 1;
103
+ }
104
+ attemptEventOptions(attempt) {
105
+ return attemptContext(attempt).eventOptions();
106
+ }
107
+ /**
108
+ * KTD6 wait-enter: T3 wait-enter hook → lifecycle waiting_for_input → park.
109
+ */
110
+ async enterWait(runId, stageId, handle, request, attemptCtx) {
111
+ await recordWaitEnter({
112
+ store: this.options.store,
113
+ runId,
114
+ stageId,
115
+ request,
116
+ attemptCtx,
117
+ qaHooks: this.qaHooks,
118
+ });
119
+ console.error(`Stage ${stageId} waiting for input.`);
120
+ await this.parkUntilAnswer(runId, stageId, handle, request);
121
+ }
122
+ parkUntilAnswer(runId, stageId, handle, pendingRequest) {
123
+ const key = waitKey(runId, stageId);
124
+ const existing = this.live.get(key);
125
+ if (existing) {
126
+ existing.reject(new Error(`duplicate wait registration for ${runId}/${stageId}`));
127
+ this.live.delete(key);
128
+ }
129
+ return new Promise((resolve, reject) => {
130
+ this.live.set(key, { handle, pendingRequest, resolve, reject });
131
+ });
132
+ }
133
+ clearLiveWait(runId, stageId) {
134
+ const key = waitKey(runId, stageId);
135
+ const entry = this.live.get(key);
136
+ if (!entry)
137
+ return;
138
+ this.live.delete(key);
139
+ entry.reject(new Error("wait cancelled"));
140
+ }
141
+ /**
142
+ * Resolve pending for assert: live park map, else trail-derived T2 prompt.
143
+ * Non-T2 string fixtures yield undefined (assert pass-through).
144
+ */
145
+ async resolvePendingRequest(runId, stageId) {
146
+ const live = this.live.get(waitKey(runId, stageId));
147
+ if (live) {
148
+ return live.pendingRequest;
149
+ }
150
+ const attempt = await this.latestAttemptForStage(runId, stageId);
151
+ const events = await this.options.store.listStageEvents(runId, stageId, attempt);
152
+ const pending = derivePendingPrompt(events);
153
+ return pending ?? undefined;
154
+ }
155
+ /**
156
+ * KTD6 answer-deliver prefix for live and restart:
157
+ * resolve pending → assert → answer hook → resumed → live unpark or restart ok.
158
+ */
159
+ async deliverAnswerPrefix(runId, stageId, opaqueAnswer, attemptCtx) {
160
+ const key = waitKey(runId, stageId);
161
+ const live = this.live.get(key);
162
+ const attempt = attemptCtx?.attempt ?? (await this.latestAttemptForStage(runId, stageId));
163
+ const resolvedCtx = attemptCtx ?? attemptContext(attempt);
164
+ const pendingRequest = await this.resolvePendingRequest(runId, stageId);
165
+ try {
166
+ await this.assertAnswerMatchesPrompt({
167
+ runId,
168
+ stageId,
169
+ answer: opaqueAnswer,
170
+ pendingRequest,
171
+ });
172
+ }
173
+ catch (err) {
174
+ return {
175
+ ok: false,
176
+ reason: err instanceof Error ? err.message : String(err),
177
+ status: 400,
178
+ };
179
+ }
180
+ await this.qaHooks.onAnswerDeliver?.({
181
+ runId,
182
+ stageId,
183
+ answer: opaqueAnswer,
184
+ pendingRequest,
185
+ attempt,
186
+ });
187
+ await this.options.store.appendStageEvent(runId, stageId, {
188
+ event: "resumed",
189
+ }, this.attemptEventOptions(resolvedCtx.attempt));
190
+ if (live) {
191
+ this.live.delete(key);
192
+ live.handle.deliverAnswer(opaqueAnswer);
193
+ live.resolve();
194
+ return { ok: true, mode: "live" };
195
+ }
196
+ return { ok: true, mode: "restart" };
197
+ }
198
+ }
@@ -0,0 +1,45 @@
1
+ import type { OperatorCatalog } from "./stageAttemptBootstrap.js";
2
+ export type StageLaunchInput = {
3
+ runId: string;
4
+ stageId: string;
5
+ rootDir: string;
6
+ mode?: "run" | "resume";
7
+ resumeAnswer?: unknown;
8
+ attempt?: number;
9
+ sessionFilePath?: string;
10
+ operatorCatalog?: OperatorCatalog;
11
+ };
12
+ export type StageLaunchResult = {
13
+ type: "succeeded";
14
+ } | {
15
+ type: "failed";
16
+ reason: string;
17
+ } | {
18
+ type: "waiting";
19
+ };
20
+ export type ActiveStageProcess = {
21
+ runId: string;
22
+ stageId: string;
23
+ startedAt: number;
24
+ };
25
+ export type StageProcessLauncherOptions = {
26
+ maxActiveStageProcesses?: number;
27
+ env?: Record<string, string | undefined>;
28
+ cliEntry?: string;
29
+ };
30
+ export declare class StageProcessLauncher {
31
+ private readonly maxActive;
32
+ private readonly env;
33
+ private readonly cliEntry;
34
+ private readonly active;
35
+ private readonly waitQueue;
36
+ private slotsHeld;
37
+ constructor(options?: StageProcessLauncherOptions);
38
+ activeCount(): number;
39
+ getActiveStageProcesses(): ActiveStageProcess[];
40
+ launch(input: StageLaunchInput): Promise<StageLaunchResult>;
41
+ cancelRun(runId: string, killAfterMs?: number): Promise<void>;
42
+ private waitForCapacity;
43
+ private releaseCapacity;
44
+ private spawnAndWait;
45
+ }
@@ -0,0 +1,199 @@
1
+ import { fork } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import { readMaxActiveStageProcesses, } from "./stageConcurrency.js";
4
+ import { SF_STAGE_WORKER, STAGE_WORKER_EXIT, } from "./stageWorkerProtocol.js";
5
+ function activeKey(runId, stageId) {
6
+ return `${runId}\0${stageId}`;
7
+ }
8
+ function isStageWorkerResult(value) {
9
+ if (!value || typeof value !== "object")
10
+ return false;
11
+ const type = value.type;
12
+ return type === "succeeded" || type === "failed" || type === "waiting";
13
+ }
14
+ function resultFromExitCode(code) {
15
+ if (code === STAGE_WORKER_EXIT.SUCCEEDED) {
16
+ return { type: "succeeded" };
17
+ }
18
+ if (code === STAGE_WORKER_EXIT.WAITING) {
19
+ return { type: "waiting" };
20
+ }
21
+ if (code === STAGE_WORKER_EXIT.FAILED) {
22
+ return { type: "failed", reason: "stage failed" };
23
+ }
24
+ return {
25
+ type: "failed",
26
+ reason: code === null ? "stage process exited" : `stage process exit ${code}`,
27
+ };
28
+ }
29
+ function resultFromWorkerMessage(msg) {
30
+ if (msg.type === "succeeded") {
31
+ return { type: "succeeded" };
32
+ }
33
+ if (msg.type === "waiting") {
34
+ return { type: "waiting" };
35
+ }
36
+ return { type: "failed", reason: msg.reason };
37
+ }
38
+ export class StageProcessLauncher {
39
+ maxActive;
40
+ env;
41
+ cliEntry;
42
+ active = new Map();
43
+ waitQueue = [];
44
+ slotsHeld = 0;
45
+ constructor(options = {}) {
46
+ this.env = options.env ?? process.env;
47
+ this.maxActive = readMaxActiveStageProcesses(this.env, options.maxActiveStageProcesses);
48
+ this.cliEntry =
49
+ options.cliEntry ??
50
+ fileURLToPath(new URL("../cli.js", import.meta.url));
51
+ }
52
+ activeCount() {
53
+ return this.active.size;
54
+ }
55
+ getActiveStageProcesses() {
56
+ return [...this.active.values()].map(({ runId, stageId, startedAt }) => ({
57
+ runId,
58
+ stageId,
59
+ startedAt,
60
+ }));
61
+ }
62
+ async launch(input) {
63
+ await this.waitForCapacity();
64
+ return this.spawnAndWait(input);
65
+ }
66
+ async cancelRun(runId, killAfterMs = 5000) {
67
+ const children = [...this.active.values()].filter((entry) => entry.runId === runId);
68
+ if (children.length === 0) {
69
+ return;
70
+ }
71
+ await Promise.all(children.map((entry) => new Promise((resolve) => {
72
+ const child = entry.child;
73
+ let settled = false;
74
+ const finish = () => {
75
+ if (settled)
76
+ return;
77
+ settled = true;
78
+ resolve();
79
+ };
80
+ child.once("exit", finish);
81
+ child.kill("SIGTERM");
82
+ if (killAfterMs > 0) {
83
+ setTimeout(() => {
84
+ if (!child.killed) {
85
+ child.kill("SIGKILL");
86
+ }
87
+ }, killAfterMs);
88
+ }
89
+ })));
90
+ }
91
+ async waitForCapacity() {
92
+ if (!Number.isFinite(this.maxActive)) {
93
+ return;
94
+ }
95
+ if (this.slotsHeld < this.maxActive) {
96
+ this.slotsHeld += 1;
97
+ return;
98
+ }
99
+ await new Promise((resolve) => {
100
+ this.waitQueue.push(resolve);
101
+ });
102
+ return this.waitForCapacity();
103
+ }
104
+ releaseCapacity() {
105
+ if (!Number.isFinite(this.maxActive)) {
106
+ return;
107
+ }
108
+ this.slotsHeld -= 1;
109
+ if (this.waitQueue.length > 0) {
110
+ const next = this.waitQueue.shift();
111
+ next?.();
112
+ }
113
+ }
114
+ spawnAndWait(input) {
115
+ const mode = input.mode ?? (input.resumeAnswer !== undefined ? "resume" : "run");
116
+ const args = [
117
+ "internal",
118
+ "run-stage",
119
+ "--run-id",
120
+ input.runId,
121
+ "--stage-id",
122
+ input.stageId,
123
+ "--mode",
124
+ mode,
125
+ ];
126
+ if (input.resumeAnswer !== undefined) {
127
+ args.push("--resume-answer", JSON.stringify(input.resumeAnswer));
128
+ }
129
+ if (input.attempt !== undefined) {
130
+ args.push("--attempt", String(input.attempt));
131
+ }
132
+ if (input.sessionFilePath !== undefined) {
133
+ args.push("--session-file", input.sessionFilePath);
134
+ }
135
+ if (input.operatorCatalog?.agentDir !== undefined) {
136
+ args.push("--operator-agent-dir", input.operatorCatalog.agentDir);
137
+ }
138
+ const child = fork(this.cliEntry, args, {
139
+ cwd: input.rootDir,
140
+ env: { ...process.env, ...this.env, [SF_STAGE_WORKER]: "1" },
141
+ stdio: ["pipe", "pipe", "pipe", "ipc"],
142
+ });
143
+ const key = activeKey(input.runId, input.stageId);
144
+ const tracked = {
145
+ child,
146
+ runId: input.runId,
147
+ stageId: input.stageId,
148
+ startedAt: Date.now(),
149
+ };
150
+ this.active.set(key, tracked);
151
+ if (child.stderr) {
152
+ let stderrBuffer = "";
153
+ child.stderr.on("data", (chunk) => {
154
+ stderrBuffer += chunk.toString();
155
+ const lines = stderrBuffer.split("\n");
156
+ stderrBuffer = lines.pop() ?? "";
157
+ for (const line of lines) {
158
+ process.stderr.write(`[stage:${input.stageId}] ${line}\n`);
159
+ }
160
+ });
161
+ child.stderr.on("end", () => {
162
+ if (stderrBuffer.length > 0) {
163
+ process.stderr.write(`[stage:${input.stageId}] ${stderrBuffer}\n`);
164
+ stderrBuffer = "";
165
+ }
166
+ });
167
+ }
168
+ return new Promise((resolve) => {
169
+ let settled = false;
170
+ const cleanup = () => {
171
+ this.active.delete(key);
172
+ this.releaseCapacity();
173
+ };
174
+ const finish = (result) => {
175
+ if (settled)
176
+ return;
177
+ settled = true;
178
+ cleanup();
179
+ resolve(result);
180
+ };
181
+ child.on("message", (message) => {
182
+ if (!isStageWorkerResult(message))
183
+ return;
184
+ finish(resultFromWorkerMessage(message));
185
+ });
186
+ child.on("exit", (code) => {
187
+ if (settled)
188
+ return;
189
+ finish(resultFromExitCode(code));
190
+ });
191
+ child.on("error", (err) => {
192
+ finish({
193
+ type: "failed",
194
+ reason: err instanceof Error ? err.message : String(err),
195
+ });
196
+ });
197
+ });
198
+ }
199
+ }
@@ -0,0 +1,10 @@
1
+ import type { RunStore } from "../runstore/port.js";
2
+ import { type StageAttemptContext } from "./stageAttemptContext.js";
3
+ export declare function failStageAsInterrupted(options: {
4
+ store: RunStore;
5
+ runId: string;
6
+ stageId: string;
7
+ reason: string;
8
+ attemptCtx?: StageAttemptContext;
9
+ }): Promise<void>;
10
+ export declare function syncRunStatusFromStages(store: RunStore, runId: string): Promise<void>;
@@ -0,0 +1,43 @@
1
+ import { deriveStatusFromStages } from "../runstore/port.js";
2
+ import { deriveExecutionPatchFromEvent } from "../runstore/stageExecution.js";
3
+ import { attemptContext, } from "./stageAttemptContext.js";
4
+ export async function failStageAsInterrupted(options) {
5
+ const { store, runId, stageId, reason, attemptCtx } = options;
6
+ let attemptNum;
7
+ let attemptOpt;
8
+ if (attemptCtx !== undefined) {
9
+ attemptNum = attemptCtx.attempt;
10
+ attemptOpt = attemptCtx.eventOptions();
11
+ }
12
+ else {
13
+ const latest = await store.getLatestStageExecution(runId, stageId);
14
+ if (latest !== null) {
15
+ attemptNum = latest.attempt;
16
+ attemptOpt = attemptContext(latest.attempt).eventOptions();
17
+ }
18
+ else {
19
+ attemptNum = 1;
20
+ attemptOpt = undefined;
21
+ }
22
+ }
23
+ const event = { event: "failed", reason };
24
+ await store.appendStageEvent(runId, stageId, event, attemptOpt);
25
+ try {
26
+ const execution = await store.getStageExecution(runId, stageId, attemptNum);
27
+ const patch = deriveExecutionPatchFromEvent(event, execution);
28
+ if (Object.keys(patch).length === 0)
29
+ return;
30
+ await store.updateStageExecution(runId, stageId, attemptNum, patch);
31
+ }
32
+ catch {
33
+ // no execution row yet
34
+ }
35
+ }
36
+ export async function syncRunStatusFromStages(store, runId) {
37
+ const run = await store.readRun(runId);
38
+ const derived = deriveStatusFromStages(run.stages);
39
+ const meta = await store.readRunMeta(runId);
40
+ if ((meta.status ?? "created") !== derived) {
41
+ await store.updateRunStatus(runId, derived);
42
+ }
43
+ }