stageflow 0.23.0 → 0.25.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 (44) hide show
  1. package/README.md +15 -3
  2. package/dist/a2a/configDiscovery.d.ts +9 -0
  3. package/dist/a2a/configDiscovery.js +18 -0
  4. package/dist/a2a/contracts.d.ts +93 -0
  5. package/dist/a2a/contracts.js +163 -0
  6. package/dist/a2a/limits.d.ts +14 -0
  7. package/dist/a2a/limits.js +29 -0
  8. package/dist/a2a/registry.d.ts +44 -0
  9. package/dist/a2a/registry.js +159 -0
  10. package/dist/a2a/server.d.ts +29 -0
  11. package/dist/a2a/server.js +341 -0
  12. package/dist/a2a/service.d.ts +97 -0
  13. package/dist/a2a/service.js +496 -0
  14. package/dist/a2a/store.d.ts +106 -0
  15. package/dist/a2a/store.js +245 -0
  16. package/dist/cli/a2aCommand.d.ts +8 -0
  17. package/dist/cli/a2aCommand.js +131 -0
  18. package/dist/cli/runStageCommand.d.ts +33 -0
  19. package/dist/cli/runStageCommand.js +407 -0
  20. package/dist/cli.js +19 -0
  21. package/dist/mcp/catalogTools.js +139 -0
  22. package/dist/package-meta.d.ts +1 -1
  23. package/dist/package-meta.js +1 -1
  24. package/dist/runstore/createStore.d.ts +10 -0
  25. package/dist/runstore/createStore.js +13 -4
  26. package/dist/runstore/port.d.ts +14 -1
  27. package/dist/runstore/sqlite/SqliteRunStore.d.ts +9 -0
  28. package/dist/runstore/sqlite/SqliteRunStore.js +66 -25
  29. package/dist/runstore/sqlite/schema.d.ts +1 -1
  30. package/dist/runstore/sqlite/schema.js +6 -0
  31. package/dist/runstore/submission.d.ts +11 -0
  32. package/dist/runstore/submission.js +7 -0
  33. package/dist/runtime/pipelineRunner.d.ts +2 -0
  34. package/dist/runtime/pipelineRunner.js +3 -0
  35. package/dist/runtime/pipelineScheduler.js +32 -15
  36. package/dist/runtime/reloadRunCatalog.js +7 -1
  37. package/dist/runtime/resolveEnvelopeRef.d.ts +30 -0
  38. package/dist/runtime/resolveEnvelopeRef.js +79 -0
  39. package/dist/runtime/runManager.d.ts +22 -1
  40. package/dist/runtime/runManager.js +48 -4
  41. package/dist/server/bootstrap.d.ts +2 -0
  42. package/dist/server/bootstrap.js +14 -3
  43. package/dist/server/createHttpHost.js +6 -0
  44. package/package.json +2 -1
@@ -11,13 +11,22 @@ function resolveKind(kind) {
11
11
  return "sqlite";
12
12
  }
13
13
  /**
14
- * Factory at the application edge. Call sites should receive the returned `RunStore`
15
- * and must not branch on backend kind.
14
+ * Composition-root factory: hands back the concrete SQLite connection alongside the `RunStore`,
15
+ * for the one caller wiring a second, colocated store (A2A's tables) into the same `state.db`
16
+ * file. Ordinary call sites use `createRunStore` below and never see this.
16
17
  */
17
- export function createRunStore(config) {
18
+ export function createRunStoreWithConnection(config) {
18
19
  resolveKind(config.kind);
19
20
  migrateLegacyStoreRoot(config.rootDir);
20
21
  const storeRoot = storeRootFor(config.rootDir);
21
22
  mkdirSync(storeRoot, { recursive: true });
22
- return new SqliteRunStore(storeRoot);
23
+ const store = new SqliteRunStore(storeRoot);
24
+ return { store, connection: store.connection };
25
+ }
26
+ /**
27
+ * Factory at the application edge. Call sites should receive the returned `RunStore`
28
+ * and must not branch on backend kind.
29
+ */
30
+ export function createRunStore(config) {
31
+ return createRunStoreWithConnection(config).store;
23
32
  }
@@ -1,7 +1,8 @@
1
1
  import type { StageLogLine } from "../agent/activity.js";
2
+ import type { RunSubmission, RunSubmissionRecord } from "./submission.js";
2
3
  import type { AskOperatorPrompt } from "../tools/askOperator.js";
3
4
  import type { StageEnvelope } from "../types/envelope.js";
4
- import type { FeedbackLoopConfig, ResolvedPipelineDag } from "../types/pipeline.js";
5
+ import type { FeedbackLoopConfig, InlinePipelineDefinition, ResolvedPipelineDag } from "../types/pipeline.js";
5
6
  import type { CompletionCheck } from "../types/completion.js";
6
7
  import type { StageGateKind } from "../types/stage.js";
7
8
  import type { StageUsage } from "../types/usage.js";
@@ -51,6 +52,14 @@ export type RunMeta = {
51
52
  pipeline_path?: string;
52
53
  task_path?: string;
53
54
  project_root?: string;
55
+ /**
56
+ * The raw pipeline body for a run started from an inline pipeline
57
+ * (no pipeline_path). Lets a fresh stage-worker process — which has no
58
+ * memory of the original in-process call — reconstruct the pipeline
59
+ * without needing a catalog file, mirroring how task_yaml already lets
60
+ * task reconstruction work without a task_path.
61
+ */
62
+ inline_pipeline?: InlinePipelineDefinition;
54
63
  };
55
64
  export type CreatedRun = {
56
65
  runId: string;
@@ -275,6 +284,7 @@ export type FeedbackLoopHistory = {
275
284
  fork_generations: ForkGenerationRecord[];
276
285
  };
277
286
  export type CreateRunInput = {
287
+ submission?: RunSubmission;
278
288
  pipelineId: string;
279
289
  taskYaml: string;
280
290
  taskId?: string;
@@ -286,6 +296,8 @@ export type CreateRunInput = {
286
296
  pipelinePath?: string;
287
297
  taskPath?: string;
288
298
  projectRoot?: string;
299
+ /** Raw inline pipeline body, persisted only when pipelinePath is absent. */
300
+ inlinePipeline?: InlinePipelineDefinition;
289
301
  };
290
302
  export type ListRunsFilter = {
291
303
  status?: RunStatus;
@@ -301,6 +313,7 @@ export type ListRunsFilter = {
301
313
  * run id is all you have (catalog / rehydrate).
302
314
  */
303
315
  export interface RunStore {
316
+ getRunBySubmission(key: string): Promise<RunSubmissionRecord | null>;
304
317
  createRun(input: CreateRunInput): Promise<CreatedRun>;
305
318
  updateRunStatus(runId: string, status: RunStatus): Promise<void>;
306
319
  readRunMeta(runId: string): Promise<RunMeta>;
@@ -1,6 +1,8 @@
1
+ import Database from "better-sqlite3";
1
2
  import type { StageEnvelope } from "../../types/envelope.js";
2
3
  import type { StageLogLine } from "../../agent/activity.js";
3
4
  import { type CreateFeedbackLoopInput, type CreateFeedbackReplayInput, type CreateFeedbackReplayStagePassInput, type CreateForkGenerationInput, type CreateRunInput, type CreatedRun, type FeedbackLoopPatch, type FeedbackLoopRecord, type FeedbackLoopState, type FeedbackReplayPatch, type FeedbackReplayRecord, type FeedbackReplayStagePassPatch, type FeedbackReplayStagePassRecord, type ForkGenerationPatch, type ForkGenerationRecord, type ListRunsFilter, type RunDetail, type RunMeta, type RunPipelineDagSnapshot, type RunStatus, type RunStore, type RunSummary, type StageExecution, type StageExecutionPatch, type StageLogEvent, type VerificationCheckResult, type VerificationCheckResultPatch } from "../port.js";
5
+ import { type RunSubmissionRecord } from "../submission.js";
4
6
  export declare class SqliteRunStore implements RunStore {
5
7
  private readonly storeRoot;
6
8
  private readonly db;
@@ -8,8 +10,15 @@ export declare class SqliteRunStore implements RunStore {
8
10
  constructor(storeRoot: string);
9
11
  /** Await one-shot disk import before catalog reads (create paths also wait). */
10
12
  ready(): Promise<void>;
13
+ /**
14
+ * The connection this store owns, for the one caller allowed to share it: the composition root
15
+ * wiring the A2A tables into the same `state.db` file. Not part of the `RunStore` interface.
16
+ */
17
+ get connection(): Database.Database;
11
18
  getWorkspaceDir(runId: string): string;
12
19
  createRun(input: CreateRunInput): Promise<CreatedRun>;
20
+ private readSubmission;
21
+ getRunBySubmission(key: string): Promise<RunSubmissionRecord | null>;
13
22
  updateRunStatus(runId: string, status: RunStatus): Promise<void>;
14
23
  updatePipelineDag(runId: string, dag: RunPipelineDagSnapshot): Promise<void>;
15
24
  readRunMeta(runId: string): Promise<RunMeta>;
@@ -9,6 +9,7 @@ import { newRunId, runWorkspaceDir } from "../paths.js";
9
9
  import { attemptAgentDir, attemptArtifactsDir, stageDir, } from "../workspaceLayout.js";
10
10
  import { importDiskRunsIfEmpty } from "./migrateFromDisk.js";
11
11
  import { SCHEMA_SQL } from "./schema.js";
12
+ import { RunSubmissionExistsError } from "../submission.js";
12
13
  const ensuredStageDirs = new Set();
13
14
  const DEFAULT_SQLITE_BUSY_TIMEOUT_MS = 5000;
14
15
  function readSqliteBusyTimeoutMs() {
@@ -52,6 +53,12 @@ function ensureRunLocatorColumns(db) {
52
53
  }
53
54
  }
54
55
  }
56
+ function ensureInlinePipelineColumn(db) {
57
+ const cols = db.prepare(`PRAGMA table_info(runs)`).all();
58
+ if (!cols.some((c) => c.name === "inline_pipeline_json")) {
59
+ db.exec(`ALTER TABLE runs ADD COLUMN inline_pipeline_json TEXT`);
60
+ }
61
+ }
55
62
  function ensureStageExecutionsTable(db) {
56
63
  db.exec(`
57
64
  CREATE TABLE IF NOT EXISTS stage_executions (
@@ -304,6 +311,7 @@ export class SqliteRunStore {
304
311
  ensureCiIdentityColumns(this.db);
305
312
  ensurePipelineDagColumn(this.db);
306
313
  ensureRunLocatorColumns(this.db);
314
+ ensureInlinePipelineColumn(this.db);
307
315
  ensureStageExecutionsTable(this.db);
308
316
  ensureStageExecutionVerificationOutcomeColumn(this.db);
309
317
  ensureStageExecutionCostColumns(this.db);
@@ -319,6 +327,13 @@ export class SqliteRunStore {
319
327
  async ready() {
320
328
  await this.migratePromise;
321
329
  }
330
+ /**
331
+ * The connection this store owns, for the one caller allowed to share it: the composition root
332
+ * wiring the A2A tables into the same `state.db` file. Not part of the `RunStore` interface.
333
+ */
334
+ get connection() {
335
+ return this.db;
336
+ }
322
337
  getWorkspaceDir(runId) {
323
338
  return runWorkspaceDir(this.storeRoot, runId);
324
339
  }
@@ -337,32 +352,55 @@ export class SqliteRunStore {
337
352
  const projectRoot = input.projectRoot
338
353
  ? normalizeCatalogPath(input.projectRoot)
339
354
  : null;
340
- this.db
341
- .prepare(`INSERT INTO runs
342
- (run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root)
355
+ this.db.transaction(() => {
356
+ if (input.submission) {
357
+ const existing = this.readSubmission(input.submission.key);
358
+ if (existing)
359
+ throw new RunSubmissionExistsError(existing);
360
+ }
361
+ this.db
362
+ .prepare(`INSERT INTO runs
363
+ (run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, inline_pipeline_json)
343
364
  VALUES
344
- (@run_id, @pipeline_id, @task_id, @task_yaml, @status, @created_at, @updated_at, @checkout_root, @pipeline_dag_json, @git_sha, @ci_pr_url, @ci_job_url, @pipeline_path, @task_path, @project_root)`)
345
- .run({
346
- run_id: runId,
347
- pipeline_id: input.pipelineId,
348
- task_id: input.taskId ?? null,
349
- task_yaml: input.taskYaml,
350
- status: "running",
351
- created_at: now,
352
- updated_at: now,
353
- checkout_root: input.checkoutRoot ?? null,
354
- pipeline_dag_json: input.pipelineDag
355
- ? JSON.stringify(input.pipelineDag)
356
- : null,
357
- git_sha: input.gitSha ?? null,
358
- ci_pr_url: input.ciPrUrl ?? null,
359
- ci_job_url: input.ciJobUrl ?? null,
360
- pipeline_path: pipelinePath,
361
- task_path: taskPath,
362
- project_root: projectRoot,
363
- });
365
+ (@run_id, @pipeline_id, @task_id, @task_yaml, @status, @created_at, @updated_at, @checkout_root, @pipeline_dag_json, @git_sha, @ci_pr_url, @ci_job_url, @pipeline_path, @task_path, @project_root, @inline_pipeline_json)`)
366
+ .run({
367
+ run_id: runId,
368
+ pipeline_id: input.pipelineId,
369
+ task_id: input.taskId ?? null,
370
+ task_yaml: input.taskYaml,
371
+ status: "running",
372
+ created_at: now,
373
+ updated_at: now,
374
+ checkout_root: input.checkoutRoot ?? null,
375
+ pipeline_dag_json: input.pipelineDag
376
+ ? JSON.stringify(input.pipelineDag)
377
+ : null,
378
+ git_sha: input.gitSha ?? null,
379
+ ci_pr_url: input.ciPrUrl ?? null,
380
+ ci_job_url: input.ciJobUrl ?? null,
381
+ pipeline_path: pipelinePath,
382
+ task_path: taskPath,
383
+ project_root: projectRoot,
384
+ inline_pipeline_json: input.inlinePipeline
385
+ ? JSON.stringify(input.inlinePipeline)
386
+ : null,
387
+ });
388
+ if (input.submission) {
389
+ this.db.prepare("INSERT INTO run_submissions (submission_key, request_hash, run_id) VALUES (?, ?, ?)")
390
+ .run(input.submission.key, input.submission.requestHash, runId);
391
+ }
392
+ })();
364
393
  return { runId, workspaceDir };
365
394
  }
395
+ readSubmission(key) {
396
+ const row = this.db.prepare("SELECT submission_key AS key, request_hash AS requestHash, run_id AS runId FROM run_submissions WHERE submission_key = ?")
397
+ .get(key);
398
+ return row ?? null;
399
+ }
400
+ async getRunBySubmission(key) {
401
+ await this.ready();
402
+ return this.readSubmission(key);
403
+ }
366
404
  async updateRunStatus(runId, status) {
367
405
  await this.ready();
368
406
  const result = this.db
@@ -710,7 +748,7 @@ export class SqliteRunStore {
710
748
  }
711
749
  const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
712
750
  const rows = this.db
713
- .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root
751
+ .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, inline_pipeline_json
714
752
  FROM runs ${where} ORDER BY created_at DESC`)
715
753
  .all(...params);
716
754
  const summaries = [];
@@ -1139,11 +1177,14 @@ export class SqliteRunStore {
1139
1177
  ...(row.task_path != null ? { task_path: row.task_path } : {}),
1140
1178
  ...(row.project_root != null ? { project_root: row.project_root } : {}),
1141
1179
  ...(pipeline_dag ? { pipeline_dag } : {}),
1180
+ ...(row.inline_pipeline_json != null
1181
+ ? { inline_pipeline: JSON.parse(row.inline_pipeline_json) }
1182
+ : {}),
1142
1183
  };
1143
1184
  }
1144
1185
  getRunRow(runId) {
1145
1186
  const row = this.db
1146
- .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root
1187
+ .prepare(`SELECT run_id, pipeline_id, task_id, task_yaml, status, created_at, updated_at, checkout_root, pipeline_dag_json, git_sha, ci_pr_url, ci_job_url, pipeline_path, task_path, project_root, inline_pipeline_json
1147
1188
  FROM runs WHERE run_id = ?`)
1148
1189
  .get(runId);
1149
1190
  if (!row)
@@ -1 +1 @@
1
- export declare const SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS runs (\n run_id TEXT PRIMARY KEY,\n pipeline_id TEXT NOT NULL,\n task_id TEXT,\n task_yaml TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n checkout_root TEXT,\n pipeline_dag_json TEXT,\n git_sha TEXT,\n ci_pr_url TEXT,\n ci_job_url TEXT,\n pipeline_path TEXT,\n task_path TEXT,\n project_root TEXT\n);\n\nCREATE TABLE IF NOT EXISTS stages (\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n status TEXT,\n summary TEXT,\n envelope_json TEXT,\n started_at TEXT,\n finished_at TEXT,\n PRIMARY KEY (run_id, stage_id),\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS stage_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n at TEXT NOT NULL,\n event TEXT NOT NULL,\n payload_json TEXT,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_runs_status_created\n ON runs (status, created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_runs_pipeline_created\n ON runs (pipeline_id, created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_stage_events_run_stage_at\n ON stage_events (run_id, stage_id, at);\n\nCREATE TABLE IF NOT EXISTS stage_executions (\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n attempt INTEGER NOT NULL,\n status TEXT NOT NULL,\n verification_outcome TEXT NOT NULL DEFAULT 'not_run',\n started_at TEXT,\n finished_at TEXT,\n envelope_json TEXT,\n cost_usd REAL,\n usage_json TEXT,\n PRIMARY KEY (run_id, stage_id, attempt),\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_stage_executions_run_stage\n ON stage_executions (run_id, stage_id, attempt);\n\nCREATE TABLE IF NOT EXISTS verification_check_results (\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n attempt INTEGER NOT NULL,\n check_id TEXT NOT NULL,\n check_type TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n evidence_json TEXT,\n PRIMARY KEY (run_id, stage_id, attempt, check_id),\n FOREIGN KEY (run_id, stage_id, attempt)\n REFERENCES stage_executions(run_id, stage_id, attempt)\n);\n\nCREATE INDEX IF NOT EXISTS idx_verification_check_results_execution\n ON verification_check_results (run_id, stage_id, attempt, check_id);\n\nCREATE TABLE IF NOT EXISTS feedback_loops (\n run_id TEXT NOT NULL,\n loop_id TEXT NOT NULL,\n source_stage_id TEXT NOT NULL,\n source_attempt INTEGER NOT NULL,\n policy_json TEXT NOT NULL,\n state TEXT NOT NULL,\n current_replay_id TEXT,\n current_replay_number INTEGER,\n deferred_send_back_json TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, loop_id),\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_feedback_loops_run_state\n ON feedback_loops (run_id, state, created_at);\n\nCREATE TABLE IF NOT EXISTS feedback_replays (\n run_id TEXT NOT NULL,\n replay_id TEXT NOT NULL,\n loop_id TEXT NOT NULL,\n source_stage_id TEXT NOT NULL,\n source_attempt INTEGER NOT NULL,\n target_stage_id TEXT NOT NULL,\n replay_number INTEGER NOT NULL,\n max_replays INTEGER NOT NULL,\n replay_session TEXT NOT NULL,\n route_stage_ids_json TEXT NOT NULL,\n feedback_envelope_json TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, replay_id),\n FOREIGN KEY (run_id, loop_id) REFERENCES feedback_loops(run_id, loop_id)\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_feedback_replays_loop_number\n ON feedback_replays (run_id, loop_id, replay_number);\nCREATE INDEX IF NOT EXISTS idx_feedback_replays_run_loop\n ON feedback_replays (run_id, loop_id, created_at);\n\nCREATE TABLE IF NOT EXISTS feedback_replay_stage_passes (\n run_id TEXT NOT NULL,\n replay_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n stage_attempt INTEGER NOT NULL,\n session_origin_attempt INTEGER,\n session_mode TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n emitted_envelope_json TEXT,\n PRIMARY KEY (run_id, replay_id, stage_id),\n FOREIGN KEY (run_id, replay_id) REFERENCES feedback_replays(run_id, replay_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_feedback_replay_stage_passes_replay\n ON feedback_replay_stage_passes (run_id, replay_id, stage_id);\n\nCREATE TABLE IF NOT EXISTS fork_generations (\n run_id TEXT NOT NULL,\n generation_id TEXT NOT NULL,\n replay_id TEXT,\n fork_parent_stage_id TEXT NOT NULL,\n generation_number INTEGER NOT NULL,\n clone_stage_ids_json TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, generation_id),\n FOREIGN KEY (run_id, replay_id) REFERENCES feedback_replays(run_id, replay_id)\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_fork_generations_parent_number\n ON fork_generations (run_id, fork_parent_stage_id, generation_number);\nCREATE INDEX IF NOT EXISTS idx_fork_generations_run_replay\n ON fork_generations (run_id, replay_id, created_at);\n";
1
+ export declare const SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS runs (\n run_id TEXT PRIMARY KEY,\n pipeline_id TEXT NOT NULL,\n task_id TEXT,\n task_yaml TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n checkout_root TEXT,\n pipeline_dag_json TEXT,\n git_sha TEXT,\n ci_pr_url TEXT,\n ci_job_url TEXT,\n pipeline_path TEXT,\n task_path TEXT,\n project_root TEXT\n);\n\nCREATE TABLE IF NOT EXISTS run_submissions (\n submission_key TEXT PRIMARY KEY,\n request_hash TEXT NOT NULL,\n run_id TEXT NOT NULL UNIQUE REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS stages (\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n status TEXT,\n summary TEXT,\n envelope_json TEXT,\n started_at TEXT,\n finished_at TEXT,\n PRIMARY KEY (run_id, stage_id),\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE TABLE IF NOT EXISTS stage_events (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n at TEXT NOT NULL,\n event TEXT NOT NULL,\n payload_json TEXT,\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_runs_status_created\n ON runs (status, created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_runs_pipeline_created\n ON runs (pipeline_id, created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_stage_events_run_stage_at\n ON stage_events (run_id, stage_id, at);\n\nCREATE TABLE IF NOT EXISTS stage_executions (\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n attempt INTEGER NOT NULL,\n status TEXT NOT NULL,\n verification_outcome TEXT NOT NULL DEFAULT 'not_run',\n started_at TEXT,\n finished_at TEXT,\n envelope_json TEXT,\n cost_usd REAL,\n usage_json TEXT,\n PRIMARY KEY (run_id, stage_id, attempt),\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_stage_executions_run_stage\n ON stage_executions (run_id, stage_id, attempt);\n\nCREATE TABLE IF NOT EXISTS verification_check_results (\n run_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n attempt INTEGER NOT NULL,\n check_id TEXT NOT NULL,\n check_type TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n evidence_json TEXT,\n PRIMARY KEY (run_id, stage_id, attempt, check_id),\n FOREIGN KEY (run_id, stage_id, attempt)\n REFERENCES stage_executions(run_id, stage_id, attempt)\n);\n\nCREATE INDEX IF NOT EXISTS idx_verification_check_results_execution\n ON verification_check_results (run_id, stage_id, attempt, check_id);\n\nCREATE TABLE IF NOT EXISTS feedback_loops (\n run_id TEXT NOT NULL,\n loop_id TEXT NOT NULL,\n source_stage_id TEXT NOT NULL,\n source_attempt INTEGER NOT NULL,\n policy_json TEXT NOT NULL,\n state TEXT NOT NULL,\n current_replay_id TEXT,\n current_replay_number INTEGER,\n deferred_send_back_json TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, loop_id),\n FOREIGN KEY (run_id) REFERENCES runs(run_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_feedback_loops_run_state\n ON feedback_loops (run_id, state, created_at);\n\nCREATE TABLE IF NOT EXISTS feedback_replays (\n run_id TEXT NOT NULL,\n replay_id TEXT NOT NULL,\n loop_id TEXT NOT NULL,\n source_stage_id TEXT NOT NULL,\n source_attempt INTEGER NOT NULL,\n target_stage_id TEXT NOT NULL,\n replay_number INTEGER NOT NULL,\n max_replays INTEGER NOT NULL,\n replay_session TEXT NOT NULL,\n route_stage_ids_json TEXT NOT NULL,\n feedback_envelope_json TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, replay_id),\n FOREIGN KEY (run_id, loop_id) REFERENCES feedback_loops(run_id, loop_id)\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_feedback_replays_loop_number\n ON feedback_replays (run_id, loop_id, replay_number);\nCREATE INDEX IF NOT EXISTS idx_feedback_replays_run_loop\n ON feedback_replays (run_id, loop_id, created_at);\n\nCREATE TABLE IF NOT EXISTS feedback_replay_stage_passes (\n run_id TEXT NOT NULL,\n replay_id TEXT NOT NULL,\n stage_id TEXT NOT NULL,\n stage_attempt INTEGER NOT NULL,\n session_origin_attempt INTEGER,\n session_mode TEXT NOT NULL,\n status TEXT NOT NULL,\n started_at TEXT,\n finished_at TEXT,\n emitted_envelope_json TEXT,\n PRIMARY KEY (run_id, replay_id, stage_id),\n FOREIGN KEY (run_id, replay_id) REFERENCES feedback_replays(run_id, replay_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_feedback_replay_stage_passes_replay\n ON feedback_replay_stage_passes (run_id, replay_id, stage_id);\n\nCREATE TABLE IF NOT EXISTS fork_generations (\n run_id TEXT NOT NULL,\n generation_id TEXT NOT NULL,\n replay_id TEXT,\n fork_parent_stage_id TEXT NOT NULL,\n generation_number INTEGER NOT NULL,\n clone_stage_ids_json TEXT NOT NULL,\n status TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (run_id, generation_id),\n FOREIGN KEY (run_id, replay_id) REFERENCES feedback_replays(run_id, replay_id)\n);\n\nCREATE UNIQUE INDEX IF NOT EXISTS idx_fork_generations_parent_number\n ON fork_generations (run_id, fork_parent_stage_id, generation_number);\nCREATE INDEX IF NOT EXISTS idx_fork_generations_run_replay\n ON fork_generations (run_id, replay_id, created_at);\n";
@@ -17,6 +17,12 @@ CREATE TABLE IF NOT EXISTS runs (
17
17
  project_root TEXT
18
18
  );
19
19
 
20
+ CREATE TABLE IF NOT EXISTS run_submissions (
21
+ submission_key TEXT PRIMARY KEY,
22
+ request_hash TEXT NOT NULL,
23
+ run_id TEXT NOT NULL UNIQUE REFERENCES runs(run_id)
24
+ );
25
+
20
26
  CREATE TABLE IF NOT EXISTS stages (
21
27
  run_id TEXT NOT NULL,
22
28
  stage_id TEXT NOT NULL,
@@ -0,0 +1,11 @@
1
+ export type RunSubmission = {
2
+ key: string;
3
+ requestHash: string;
4
+ };
5
+ export type RunSubmissionRecord = RunSubmission & {
6
+ runId: string;
7
+ };
8
+ export declare class RunSubmissionExistsError extends Error {
9
+ readonly submission: RunSubmissionRecord;
10
+ constructor(submission: RunSubmissionRecord);
11
+ }
@@ -0,0 +1,7 @@
1
+ export class RunSubmissionExistsError extends Error {
2
+ submission;
3
+ constructor(submission) {
4
+ super("Run submission already exists");
5
+ this.submission = submission;
6
+ }
7
+ }
@@ -1,3 +1,4 @@
1
+ import type { RunSubmission } from "../runstore/submission.js";
1
2
  import type { AgentPort } from "../agent/port.js";
2
3
  import { type ValidationFinding } from "../config/validateCatalog.js";
3
4
  import type { RunStore } from "../runstore/port.js";
@@ -70,6 +71,7 @@ export declare function runPipeline(options: {
70
71
  }): Promise<PipelineRunResult>;
71
72
  /** Create the run immediately, then execute stages in the returned promise. */
72
73
  export declare function startPipeline(options: {
74
+ submission?: RunSubmission;
73
75
  agent: AgentPort;
74
76
  store: RunStore;
75
77
  taskPath?: string;
@@ -65,6 +65,7 @@ async function preparePipeline(options) {
65
65
  : undefined;
66
66
  const projectRoot = normalizeCatalogPath(options.projectRoot ?? options.cwd);
67
67
  const run = await options.store.createRun({
68
+ submission: options.submission,
68
69
  pipelineId: loaded.pipeline.id,
69
70
  taskYaml,
70
71
  taskId: task.id,
@@ -76,6 +77,7 @@ async function preparePipeline(options) {
76
77
  pipelinePath,
77
78
  taskPath,
78
79
  projectRoot,
80
+ inlinePipeline: typeof options.pipeline === "string" ? undefined : options.pipeline,
79
81
  });
80
82
  const executionMode = readStageExecutionMode(process.env, options.executionMode);
81
83
  const stageProcessLauncher = resolveStageProcessLauncher(executionMode, options.stageProcessLauncher);
@@ -145,6 +147,7 @@ export async function startPipeline(options) {
145
147
  const cwd = options.cwd ?? process.cwd();
146
148
  const projectRoot = options.projectRoot ?? cwd;
147
149
  const prepared = await preparePipeline({
150
+ submission: options.submission,
148
151
  agent: options.agent,
149
152
  store: options.store,
150
153
  taskPath: options.taskPath,
@@ -220,6 +220,25 @@ export function applyForkSkipsFromEnvelopes(dag, states, completedEnvelopes) {
220
220
  applyForkChoiceToSchedule(dag, node.id, chosen, states);
221
221
  }
222
222
  }
223
+ async function applyEagerRouteIfSkipsFromEnvelopes(dag, states, completedEnvelopes, persistSkip) {
224
+ for (const [parentId, envelope] of completedEnvelopes) {
225
+ if (states.get(parentId) !== "succeeded")
226
+ continue;
227
+ for (const childId of dag.childrenOf[parentId] ?? []) {
228
+ const child = dag.nodes.find((n) => n.id === childId);
229
+ if (!child)
230
+ continue;
231
+ const result = classifyInboundAfterSuccess(child, parentId, envelope.payload);
232
+ if (result === "missing_field") {
233
+ return parentId;
234
+ }
235
+ if (isEagerSingleParentIfSkip(child, parentId, envelope.payload)) {
236
+ await persistSkip(childId);
237
+ }
238
+ }
239
+ }
240
+ return undefined;
241
+ }
223
242
  export function applyRetryRootDelta(dag, states, completedEnvelopes, retryRoots, stageId, attempt, clearSchedulingHalt, alreadyLaunchedIds) {
224
243
  retryRoots.set(stageId, attempt);
225
244
  const downstream = cloneRetryDownstream(dag, [stageId]);
@@ -472,6 +491,13 @@ export async function runPipelineDag(options) {
472
491
  await persistSkipPending(id);
473
492
  }
474
493
  }
494
+ const missingRouteIfParent = await applyEagerRouteIfSkipsFromEnvelopes(dag, states, completedEnvelopes, persistSkipPending);
495
+ if (missingRouteIfParent !== undefined) {
496
+ schedulingHalted = true;
497
+ if (firstFailureReason === undefined) {
498
+ firstFailureReason = `stage "${missingRouteIfParent}": route if field missing from payload`;
499
+ }
500
+ }
475
501
  const markSkippedPending = () => {
476
502
  for (const node of dag.nodes) {
477
503
  if (states.get(node.id) === "pending") {
@@ -756,22 +782,13 @@ export async function runPipelineDag(options) {
756
782
  await store.appendStageEvent(run.runId, id, { event: "skipped" });
757
783
  }
758
784
  }
759
- const childIds = dag.childrenOf[stageId] ?? [];
760
- for (const childId of childIds) {
761
- const child = dag.nodes.find((node) => node.id === childId);
762
- if (!child)
763
- continue;
764
- const result = classifyInboundAfterSuccess(child, stageId, envelope.payload);
765
- if (result === "missing_field") {
766
- schedulingHalted = true;
767
- if (firstFailureReason === undefined) {
768
- firstFailureReason = `stage "${stageId}": route if field missing from payload`;
769
- }
770
- return;
771
- }
772
- if (isEagerSingleParentIfSkip(child, stageId, envelope.payload)) {
773
- await persistSkipPending(childId);
785
+ const missingParent = await applyEagerRouteIfSkipsFromEnvelopes(dag, states, new Map([[stageId, envelope]]), persistSkipPending);
786
+ if (missingParent !== undefined) {
787
+ schedulingHalted = true;
788
+ if (firstFailureReason === undefined) {
789
+ firstFailureReason = `stage "${missingParent}": route if field missing from payload`;
774
790
  }
791
+ return;
775
792
  }
776
793
  await reopenRunnableSkippedStages(store, run.runId, dag, states, completedEnvelopes);
777
794
  };
@@ -1,5 +1,5 @@
1
1
  import { access } from "node:fs/promises";
2
- import { loadPipeline } from "../config/loadPipeline.js";
2
+ import { loadPipeline, loadPipelineFromObject } from "../config/loadPipeline.js";
3
3
  import { loadTask } from "../config/loadTask.js";
4
4
  import { normalizeCatalogPath } from "../runstore/normalizeCatalogPath.js";
5
5
  function missingLocator(field, runId) {
@@ -33,6 +33,12 @@ async function assertCatalogFileExists(filePath, label) {
33
33
  }
34
34
  export async function reloadPipelineForRun(meta) {
35
35
  const projectRoot = requireProjectRoot(meta);
36
+ // An inline-pipeline run (no pipeline_path — nothing was ever written to a
37
+ // catalog file) carries its own pipeline body in meta.inline_pipeline
38
+ // instead, the same way a task without task_path reloads from task_yaml.
39
+ if (meta.inline_pipeline) {
40
+ return loadPipelineFromObject(meta.inline_pipeline, { projectRoot });
41
+ }
36
42
  const pipelinePath = requirePipelinePath(meta);
37
43
  await assertCatalogFileExists(pipelinePath, "Pipeline");
38
44
  return loadPipeline(pipelinePath, { cwd: projectRoot });
@@ -0,0 +1,30 @@
1
+ import { type StoreLookupKind } from "../server/operatorResults.js";
2
+ import type { RunStore } from "../runstore/port.js";
3
+ import type { TaskFile } from "../types/task.js";
4
+ export type EnvelopeRefInput = {
5
+ runId: string;
6
+ stageId: string;
7
+ attempt?: number;
8
+ };
9
+ type EnvelopeRefStore = Pick<RunStore, "readRunMeta" | "readRun" | "readEnvelope">;
10
+ /** Carries enough to map onto either MCP's textResult shape or an A2aApplicationError. */
11
+ export declare class EnvelopeRefError extends Error {
12
+ readonly status: number;
13
+ readonly kind: StoreLookupKind;
14
+ constructor(message: string, status: number, kind: StoreLookupKind);
15
+ }
16
+ /**
17
+ * Resolves one or more Envelope References (ADR-0002) into a TaskFile for
18
+ * the next standalone stage call — shared by the MCP run_stage tool and the
19
+ * A2A run_stage operation so this resolution logic exists in exactly one
20
+ * place (it was duplicated between them once already; see the reuse finding
21
+ * from this feature's code review).
22
+ *
23
+ * A single ref keeps today's exact shape: goal = its summary, input = its
24
+ * payload verbatim. Multiple refs namespace each resolved payload under its
25
+ * stageId (disambiguated by runId only if two refs share a stageId), so a
26
+ * stage author addresses each source by name in their own io.input.schema
27
+ * instead of one silently overwriting another on a key collision.
28
+ */
29
+ export declare function resolveEnvelopeRefsToTask(store: EnvelopeRefStore, refs: EnvelopeRefInput | EnvelopeRefInput[], checkout?: string): Promise<TaskFile>;
30
+ export {};
@@ -0,0 +1,79 @@
1
+ import { mapStoreLookupError } from "../server/operatorResults.js";
2
+ /** Carries enough to map onto either MCP's textResult shape or an A2aApplicationError. */
3
+ export class EnvelopeRefError extends Error {
4
+ status;
5
+ kind;
6
+ constructor(message, status, kind) {
7
+ super(message);
8
+ this.status = status;
9
+ this.kind = kind;
10
+ }
11
+ }
12
+ async function resolveOne(store, ref) {
13
+ try {
14
+ await store.readRunMeta(ref.runId);
15
+ }
16
+ catch (err) {
17
+ const mapped = mapStoreLookupError(err, { policy: "run" });
18
+ throw new EnvelopeRefError(mapped.error, mapped.status, mapped.kind);
19
+ }
20
+ try {
21
+ const detail = await store.readRun(ref.runId);
22
+ if (!detail.stages.some((s) => s.stage_id === ref.stageId)) {
23
+ throw new EnvelopeRefError(`Stage not found: ${ref.stageId}`, 404, "not_found");
24
+ }
25
+ return await store.readEnvelope(ref.runId, ref.stageId, ref.attempt);
26
+ }
27
+ catch (err) {
28
+ if (err instanceof EnvelopeRefError)
29
+ throw err;
30
+ const mapped = mapStoreLookupError(err, { policy: "envelope" });
31
+ throw new EnvelopeRefError(mapped.error, mapped.status, mapped.kind);
32
+ }
33
+ }
34
+ /**
35
+ * Resolves one or more Envelope References (ADR-0002) into a TaskFile for
36
+ * the next standalone stage call — shared by the MCP run_stage tool and the
37
+ * A2A run_stage operation so this resolution logic exists in exactly one
38
+ * place (it was duplicated between them once already; see the reuse finding
39
+ * from this feature's code review).
40
+ *
41
+ * A single ref keeps today's exact shape: goal = its summary, input = its
42
+ * payload verbatim. Multiple refs namespace each resolved payload under its
43
+ * stageId (disambiguated by runId only if two refs share a stageId), so a
44
+ * stage author addresses each source by name in their own io.input.schema
45
+ * instead of one silently overwriting another on a key collision.
46
+ */
47
+ export async function resolveEnvelopeRefsToTask(store, refs, checkout) {
48
+ const list = Array.isArray(refs) ? refs : [refs];
49
+ const resolved = [];
50
+ for (const ref of list) {
51
+ resolved.push({ ref, envelope: await resolveOne(store, ref) });
52
+ }
53
+ if (resolved.length === 1) {
54
+ const { ref, envelope } = resolved[0];
55
+ return {
56
+ id: `ref-${ref.runId}-${ref.stageId}`,
57
+ goal: envelope.summary,
58
+ input: envelope.payload ?? {},
59
+ ...(checkout ? { checkout } : {}),
60
+ };
61
+ }
62
+ const usedKeys = new Set();
63
+ const input = {};
64
+ const summaryLines = [];
65
+ for (const { ref, envelope } of resolved) {
66
+ let key = ref.stageId;
67
+ if (usedKeys.has(key))
68
+ key = `${ref.stageId}:${ref.runId}`;
69
+ usedKeys.add(key);
70
+ input[key] = envelope.payload ?? {};
71
+ summaryLines.push(`${key}: ${envelope.summary}`);
72
+ }
73
+ return {
74
+ id: `ref-${list.map((r) => r.stageId).join("+")}`,
75
+ goal: summaryLines.join("\n"),
76
+ input,
77
+ ...(checkout ? { checkout } : {}),
78
+ };
79
+ }
@@ -1,3 +1,4 @@
1
+ import { type RunSubmission } from "../runstore/submission.js";
1
2
  import type { AgentPort, OpaqueAnswer } from "../agent/port.js";
2
3
  import type { InlinePipelineDefinition } from "../types/pipeline.js";
3
4
  import { type RunStore } from "../runstore/port.js";
@@ -35,6 +36,23 @@ export type CapacityHealth = {
35
36
  activeStageProcesses: number;
36
37
  maxActiveStageProcesses: number | null;
37
38
  };
39
+ export type StartRunOnceResult = {
40
+ ok: true;
41
+ runId: string;
42
+ /** False only for the caller whose submission durably created this run for the first time. */
43
+ reused: boolean;
44
+ /**
45
+ * Present when this call observed the run being launched in this process — either it launched
46
+ * the run itself, or it piggybacked on another in-flight call to the same submission key while
47
+ * that launch was still starting. Absent when the submission was already durably committed by
48
+ * an earlier, separate call: `reused: true` does not by itself imply `done` is absent, since a
49
+ * piggybacked call is also `reused: true`. Callers that need completion regardless of which case
50
+ * they hit must still fall back to polling run status by `runId`.
51
+ */
52
+ done?: Promise<PipelineRunResult>;
53
+ } | Extract<StartRunResult, {
54
+ ok: false;
55
+ }>;
38
56
  export type { DeliverAnswerResult };
39
57
  export type { RetryStageResult };
40
58
  export type AbandonStageResult = {
@@ -66,6 +84,7 @@ export type StopManualRecoveryResult = {
66
84
  };
67
85
  export declare class RunManager {
68
86
  private readonly options;
87
+ private readonly submissionsInFlight;
69
88
  private readonly active;
70
89
  private readonly checkoutLeases;
71
90
  private readonly provisionalIds;
@@ -125,6 +144,8 @@ export declare class RunManager {
125
144
  runId: string;
126
145
  }>>;
127
146
  abandonStage(runId: string, stageId: string): Promise<AbandonStageResult>;
147
+ startRunOnce(input: Parameters<RunManager["startRun"]>[0], submission: RunSubmission): Promise<StartRunOnceResult>;
148
+ private startSubmittedRun;
128
149
  startRun(input: StartTaskInput & {
129
150
  pipeline: string | InlinePipelineDefinition;
130
151
  task?: string | TaskFile;
@@ -133,7 +154,7 @@ export declare class RunManager {
133
154
  gitSha?: string;
134
155
  ciPrUrl?: string;
135
156
  ciJobUrl?: string;
136
- }): Promise<StartRunResult>;
157
+ }, submission?: RunSubmission): Promise<StartRunResult>;
137
158
  rerun(runId: string): Promise<StartRunResult>;
138
159
  retryStage(runId: string, stageId: string): Promise<RetryStageResult>;
139
160
  resumeTimedOutStage(runId: string, stageId: string): Promise<RetryStageResult>;