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
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { RunSubmissionExistsError } from "../runstore/submission.js";
2
3
  import { readFile, realpath } from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { findProjectRoot } from "../project/findProjectRoot.js";
@@ -22,6 +23,12 @@ import { resumeSessionFilePath } from "./stageAttemptContext.js";
22
23
  import { resolveStartTaskInput } from "./taskInput.js";
23
24
  import { failStageAsInterrupted, syncRunStatusFromStages, } from "./stageRecovery.js";
24
25
  import { blocksGenericRetry, manualRecoveryEligibility, readManualRecoveryState, } from "./manualRecoveryState.js";
26
+ function existingSubmissionResult(existing, request) {
27
+ if (existing.requestHash !== request.requestHash) {
28
+ return { ok: false, status: 409, reason: "Submission key was already used for different input" };
29
+ }
30
+ return { ok: true, runId: existing.runId, reused: true };
31
+ }
25
32
  const DEFAULT_MAX_CONCURRENT = 3;
26
33
  const STARTUP_RECONCILE_REASON = "process_interrupted: no active worker (server restart)";
27
34
  const OPERATOR_ABANDON_REASON = "process_interrupted: operator abandoned stage";
@@ -45,6 +52,7 @@ async function toCheckoutLeaseKey(absPath) {
45
52
  }
46
53
  export class RunManager {
47
54
  options;
55
+ submissionsInFlight = new Map();
48
56
  active = new Map();
49
57
  checkoutLeases = new Map();
50
58
  provisionalIds = new Set();
@@ -348,7 +356,42 @@ export class RunManager {
348
356
  }
349
357
  return { ok: true, runId, stageId };
350
358
  }
351
- async startRun(input) {
359
+ async startRunOnce(input, submission) {
360
+ if (!submission.key.trim() || submission.key.length > 256 || !/^[a-f0-9]{64}$/.test(submission.requestHash)) {
361
+ return { ok: false, status: 400, reason: "Submission requires a key and SHA-256 request hash" };
362
+ }
363
+ const pending = this.submissionsInFlight.get(submission.key);
364
+ if (pending) {
365
+ if (pending.requestHash !== submission.requestHash) {
366
+ return { ok: false, status: 409, reason: "Submission key was already used for different input" };
367
+ }
368
+ const result = await pending.result;
369
+ return result.ok ? { ...result, reused: true } : result;
370
+ }
371
+ const result = this.startSubmittedRun(input, submission);
372
+ this.submissionsInFlight.set(submission.key, { requestHash: submission.requestHash, result });
373
+ try {
374
+ return await result;
375
+ }
376
+ finally {
377
+ this.submissionsInFlight.delete(submission.key);
378
+ }
379
+ }
380
+ async startSubmittedRun(input, submission) {
381
+ const existing = await this.options.store.getRunBySubmission(submission.key);
382
+ if (existing)
383
+ return existingSubmissionResult(existing, submission);
384
+ try {
385
+ const result = await this.startRun(input, submission);
386
+ return result.ok ? { ok: true, runId: result.runId, reused: false, done: result.done } : result;
387
+ }
388
+ catch (error) {
389
+ if (error instanceof RunSubmissionExistsError)
390
+ return existingSubmissionResult(error.submission, submission);
391
+ throw error;
392
+ }
393
+ }
394
+ async startRun(input, submission) {
352
395
  const cwd = this.options.cwd ?? process.cwd();
353
396
  // An inline pipeline has no filesystem anchor to derive a project root from.
354
397
  const derivedProjectRoot = typeof input.pipeline === "string"
@@ -389,7 +432,7 @@ export class RunManager {
389
432
  gitSha: input.gitSha,
390
433
  ciPrUrl: input.ciPrUrl,
391
434
  ciJobUrl: input.ciJobUrl,
392
- }, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined);
435
+ }, derivedProjectRoot, resolved.kind === "path" ? resolved.taskPath : undefined, submission);
393
436
  }
394
437
  async rerun(runId) {
395
438
  const cwd = this.options.cwd ?? process.cwd();
@@ -1156,7 +1199,7 @@ export class RunManager {
1156
1199
  this.attachedWaiting.delete(`${runId}\0${stageId}`);
1157
1200
  }
1158
1201
  }
1159
- async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath) {
1202
+ async reserveAndStartPipeline(taskYaml, pipeline, taskLabel, cwd, checkoutOverride, skipGates, ciIdentity, projectRoot, taskPath, submission) {
1160
1203
  let checkoutKey;
1161
1204
  try {
1162
1205
  const task = loadTaskFromYaml(taskYaml, taskLabel);
@@ -1178,6 +1221,7 @@ export class RunManager {
1178
1221
  return reserved.failure;
1179
1222
  try {
1180
1223
  const started = await startPipeline({
1224
+ submission,
1181
1225
  agent: this.options.agent,
1182
1226
  store: this.options.store,
1183
1227
  taskYaml,
@@ -1201,7 +1245,7 @@ export class RunManager {
1201
1245
  }
1202
1246
  catch (err) {
1203
1247
  this.clearReservation(reserved.provisionalId);
1204
- if (err instanceof PipelineValidationError) {
1248
+ if (err instanceof PipelineValidationError || err instanceof RunSubmissionExistsError) {
1205
1249
  throw err;
1206
1250
  }
1207
1251
  return {
@@ -1,3 +1,4 @@
1
+ import { type A2aHost } from "../a2a/server.js";
1
2
  import type { AgentPort } from "../agent/port.js";
2
3
  import type { ProviderAuthContext } from "../agent/providerAuth.js";
3
4
  import { type RunStoreKind } from "../runstore/createStore.js";
@@ -18,6 +19,7 @@ export type StageflowHostOptions = {
18
19
  runChangeBus?: RunChangeBus;
19
20
  };
20
21
  export type StageflowHostBootstrap = {
22
+ a2a?: A2aHost;
21
23
  cwd: string;
22
24
  agentDir: string;
23
25
  rootDir: string;
@@ -1,5 +1,7 @@
1
1
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
2
- import { createRunStore } from "../runstore/createStore.js";
2
+ import { createA2aHost } from "../a2a/server.js";
3
+ import { createRunStoreWithConnection } from "../runstore/createStore.js";
4
+ import { resolveA2aConfigPath } from "../a2a/configDiscovery.js";
3
5
  import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
4
6
  import { findProjectRoot } from "../project/findProjectRoot.js";
5
7
  import { RunManager } from "../runtime/runManager.js";
@@ -14,8 +16,16 @@ export async function bootstrapStageflowHost(options) {
14
16
  const isGitProject = options.rootDir !== undefined
15
17
  ? findProjectRoot(rootDir) !== null
16
18
  : ctx.isGitProject;
17
- const rawStore = options.store ??
18
- createRunStore({ rootDir: ctx.globalHome, kind: options.storeKind });
19
+ let rawStore;
20
+ let sqliteConnection;
21
+ if (options.store) {
22
+ rawStore = options.store;
23
+ }
24
+ else {
25
+ const created = createRunStoreWithConnection({ rootDir: ctx.globalHome, kind: options.storeKind });
26
+ rawStore = created.store;
27
+ sqliteConnection = created.connection;
28
+ }
19
29
  const boundBus = isRunStoreWrapped(rawStore)
20
30
  ? getRunChangeBusFromWrappedStore(rawStore)
21
31
  : undefined;
@@ -50,6 +60,7 @@ export async function bootstrapStageflowHost(options) {
50
60
  projectRoot: rootDir,
51
61
  }, { mcpStateless });
52
62
  return {
63
+ a2a: await createA2aHost({ manager, runStore: store, rootDir, connection: sqliteConnection }, resolveA2aConfigPath(rootDir)),
53
64
  cwd,
54
65
  agentDir,
55
66
  rootDir,
@@ -17,6 +17,12 @@ export async function createHttpHost(options) {
17
17
  const method = req.method ?? "GET";
18
18
  const url = new URL(req.url ?? "/", `http://${host}:${port}`);
19
19
  const pathname = url.pathname;
20
+ if (pathname === "/api/a2a/status" && method === "GET") {
21
+ json(res, 200, boot.a2a?.status ?? { state: "disabled" });
22
+ return;
23
+ }
24
+ if (await boot.a2a?.handle(req, res, pathname))
25
+ return;
20
26
  if (pathname === "/mcp") {
21
27
  if (!validateMcpHost(req, res) || !validateMcpOrigin(req, res)) {
22
28
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stageflow",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "Stageflow — CLI pipeline runtime for configurable stages on Pi",
5
5
  "type": "module",
6
6
  "bin": {
@@ -61,6 +61,7 @@
61
61
  "vitest": "^4.1.10"
62
62
  },
63
63
  "dependencies": {
64
+ "@a2a-js/sdk": "1.1.0",
64
65
  "@anthropic-ai/claude-agent-sdk": "^0.3.263",
65
66
  "@earendil-works/pi-ai": "^0.84.1",
66
67
  "@earendil-works/pi-coding-agent": "^0.84.1",