stageflow 0.19.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 (120) hide show
  1. package/README.md +16 -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/agent/activity.d.ts +5 -0
  17. package/dist/agent/activityObserver.d.ts +3 -0
  18. package/dist/agent/activityObserver.js +6 -1
  19. package/dist/agent/piAdapter.js +7 -3
  20. package/dist/agent/port.d.ts +2 -0
  21. package/dist/agent/providerInspect.d.ts +19 -0
  22. package/dist/agent/providerInspect.js +35 -0
  23. package/dist/agent/streamLogRedact.d.ts +2 -0
  24. package/dist/agent/streamLogRedact.js +13 -0
  25. package/dist/cli/a2aCommand.d.ts +8 -0
  26. package/dist/cli/a2aCommand.js +131 -0
  27. package/dist/cli/artifactCommand.js +2 -2
  28. package/dist/cli/envelopeCommand.js +2 -2
  29. package/dist/cli/exportRunCommand.js +2 -2
  30. package/dist/cli/hostClient.d.ts +43 -0
  31. package/dist/cli/hostClient.js +182 -0
  32. package/dist/cli/providersCommand.js +5 -3
  33. package/dist/cli/runCommand.d.ts +6 -5
  34. package/dist/cli/runCommand.js +23 -31
  35. package/dist/cli/runOutput.d.ts +1 -1
  36. package/dist/cli/runStageCommand.d.ts +33 -0
  37. package/dist/cli/runStageCommand.js +407 -0
  38. package/dist/cli/runsCommand.d.ts +3 -7
  39. package/dist/cli/runsCommand.js +33 -94
  40. package/dist/cli.js +20 -5
  41. package/dist/config/describePipeline.d.ts +26 -0
  42. package/dist/config/describePipeline.js +37 -0
  43. package/dist/config/loadPipeline.d.ts +13 -1
  44. package/dist/config/loadPipeline.js +34 -4
  45. package/dist/config/mergePipelineIncludes.d.ts +11 -0
  46. package/dist/config/mergePipelineIncludes.js +75 -0
  47. package/dist/config/validateCatalog.d.ts +2 -2
  48. package/dist/config/validateCatalog.js +24 -17
  49. package/dist/mcp/catalogTools.js +285 -52
  50. package/dist/mcp/controlTools.js +77 -19
  51. package/dist/mcp/deps.d.ts +3 -0
  52. package/dist/mcp/projectMcpTools.d.ts +3 -0
  53. package/dist/mcp/projectMcpTools.js +25 -0
  54. package/dist/mcp/providerTools.d.ts +3 -0
  55. package/dist/mcp/providerTools.js +17 -0
  56. package/dist/mcp/readArtifact.d.ts +9 -0
  57. package/dist/mcp/readArtifact.js +11 -0
  58. package/dist/mcp/server.js +9 -1
  59. package/dist/mcp/tailStreamLog.d.ts +8 -0
  60. package/dist/mcp/tailStreamLog.js +28 -0
  61. package/dist/mcp/toolResults.d.ts +17 -0
  62. package/dist/mcp/toolResults.js +15 -0
  63. package/dist/mcp/tools.js +4 -0
  64. package/dist/mcp/waitRun.js +4 -4
  65. package/dist/mcp/waitingGates.d.ts +1 -0
  66. package/dist/mcp/waitingGates.js +3 -2
  67. package/dist/package-meta.d.ts +1 -1
  68. package/dist/package-meta.js +1 -1
  69. package/dist/projection/projectRun.d.ts +3 -0
  70. package/dist/projection/projectRun.js +7 -0
  71. package/dist/runstore/createStore.d.ts +10 -0
  72. package/dist/runstore/createStore.js +13 -4
  73. package/dist/runstore/port.d.ts +17 -2
  74. package/dist/runstore/sqlite/SqliteRunStore.d.ts +11 -1
  75. package/dist/runstore/sqlite/SqliteRunStore.js +81 -26
  76. package/dist/runstore/sqlite/schema.d.ts +1 -1
  77. package/dist/runstore/sqlite/schema.js +6 -0
  78. package/dist/runstore/submission.d.ts +11 -0
  79. package/dist/runstore/submission.js +7 -0
  80. package/dist/runstore/workspaceLayout.d.ts +1 -0
  81. package/dist/runstore/workspaceLayout.js +3 -0
  82. package/dist/runtime/feedbackLoopCoordinator.d.ts +9 -1
  83. package/dist/runtime/feedbackLoopCoordinator.js +50 -20
  84. package/dist/runtime/feedbackLoopDecision.js +33 -1
  85. package/dist/runtime/pipelineRunner.d.ts +5 -3
  86. package/dist/runtime/pipelineRunner.js +6 -1
  87. package/dist/runtime/pipelineScheduler.js +32 -15
  88. package/dist/runtime/reloadRunCatalog.js +7 -1
  89. package/dist/runtime/resolveEnvelopeRef.d.ts +30 -0
  90. package/dist/runtime/resolveEnvelopeRef.js +79 -0
  91. package/dist/runtime/resumeReconstruct.js +2 -2
  92. package/dist/runtime/runManager.d.ts +24 -2
  93. package/dist/runtime/runManager.js +83 -19
  94. package/dist/runtime/settingsFile.d.ts +2 -0
  95. package/dist/runtime/settingsFile.js +19 -0
  96. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -0
  97. package/dist/runtime/stageAttemptBootstrap.js +8 -2
  98. package/dist/runtime/stageStreamLog.d.ts +17 -0
  99. package/dist/runtime/stageStreamLog.js +89 -0
  100. package/dist/runtime/stageWorker.js +2 -1
  101. package/dist/server/bootstrap.d.ts +2 -0
  102. package/dist/server/bootstrap.js +22 -4
  103. package/dist/server/createHttpHost.js +6 -0
  104. package/dist/server/ensureGlobalService.d.ts +35 -0
  105. package/dist/server/ensureGlobalService.js +138 -0
  106. package/dist/server/http.d.ts +20 -1
  107. package/dist/server/http.js +450 -402
  108. package/dist/server/mcpHost.js +7 -8
  109. package/dist/server/operatorResults.d.ts +10 -0
  110. package/dist/server/operatorResults.js +24 -0
  111. package/dist/server/providerRoutes.d.ts +0 -6
  112. package/dist/server/providerRoutes.js +10 -18
  113. package/dist/types/pipeline.d.ts +8 -0
  114. package/dist/ui/assets/{index-DMVdl1qB.js → index-Da5EHU6M.js} +5 -5
  115. package/dist/ui/index.html +1 -1
  116. package/package.json +2 -1
  117. package/skills/stageflow/references/control-surface.md +2 -2
  118. package/skills/stageflow-run/SKILL.md +27 -9
  119. package/skills/stageflow-run/references/mcp-call.md +1 -1
  120. package/skills/stageflow-run/scripts/mcp-call.mjs +9 -0
@@ -0,0 +1,245 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, unlink, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import Database from "better-sqlite3";
5
+ import { storeRootFor } from "../runstore/paths.js";
6
+ import { MESSAGE_TOMBSTONE_RETENTION_MS, TERMINAL_RETENTION_MS } from "./limits.js";
7
+ const SCHEMA_SQL = `
8
+ CREATE TABLE IF NOT EXISTS a2a_contexts (
9
+ context_id TEXT PRIMARY KEY,
10
+ caller_id TEXT NOT NULL,
11
+ created_at TEXT NOT NULL
12
+ );
13
+
14
+ CREATE TABLE IF NOT EXISTS a2a_tasks (
15
+ task_id TEXT PRIMARY KEY,
16
+ context_id TEXT NOT NULL,
17
+ caller_id TEXT NOT NULL,
18
+ publication_id TEXT NOT NULL,
19
+ publication_revision TEXT NOT NULL,
20
+ submission_key TEXT NOT NULL UNIQUE,
21
+ run_id TEXT,
22
+ state TEXT NOT NULL,
23
+ result_json TEXT,
24
+ kind TEXT NOT NULL DEFAULT 'invoke',
25
+ result_stage_id TEXT,
26
+ created_at TEXT NOT NULL,
27
+ updated_at TEXT NOT NULL
28
+ );
29
+ CREATE INDEX IF NOT EXISTS a2a_tasks_context ON a2a_tasks(context_id);
30
+ CREATE INDEX IF NOT EXISTS a2a_tasks_caller ON a2a_tasks(caller_id);
31
+
32
+ CREATE TABLE IF NOT EXISTS a2a_messages (
33
+ caller_id TEXT NOT NULL,
34
+ message_id TEXT NOT NULL,
35
+ task_id TEXT NOT NULL,
36
+ operation TEXT NOT NULL,
37
+ request_hash TEXT NOT NULL,
38
+ outcome_json TEXT,
39
+ created_at TEXT NOT NULL,
40
+ PRIMARY KEY (caller_id, message_id)
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS a2a_artifacts (
44
+ artifact_id TEXT PRIMARY KEY,
45
+ task_id TEXT NOT NULL,
46
+ name TEXT NOT NULL,
47
+ media_type TEXT,
48
+ size INTEGER NOT NULL,
49
+ content_path TEXT NOT NULL,
50
+ hash TEXT NOT NULL,
51
+ created_at TEXT NOT NULL
52
+ );
53
+ CREATE INDEX IF NOT EXISTS a2a_artifacts_task ON a2a_artifacts(task_id);
54
+ `;
55
+ function nowIso() {
56
+ return new Date().toISOString();
57
+ }
58
+ export class A2aStore {
59
+ db;
60
+ artifactsRoot;
61
+ ownsConnection;
62
+ /**
63
+ * `connection`, when passed, is the SqliteRunStore's own connection to the same `state.db` file
64
+ * (see `createRunStoreWithConnection`) so the two stores share one transaction domain. Falls
65
+ * back to opening its own connection when no shared one is available (e.g. a caller-supplied
66
+ * non-SQLite `RunStore` in bootstrap).
67
+ */
68
+ constructor(rootDir, connection) {
69
+ const storeRoot = storeRootFor(rootDir);
70
+ if (connection) {
71
+ this.db = connection;
72
+ this.ownsConnection = false;
73
+ }
74
+ else {
75
+ this.db = new Database(path.join(storeRoot, "state.db"));
76
+ this.db.pragma("journal_mode = WAL");
77
+ this.db.pragma("busy_timeout = 5000");
78
+ this.ownsConnection = true;
79
+ }
80
+ this.db.exec(SCHEMA_SQL);
81
+ this.migrateAddStandaloneColumns();
82
+ this.artifactsRoot = path.join(storeRoot, "a2a-artifacts");
83
+ }
84
+ /**
85
+ * Additive migration for a `state.db` created before the run_stage A2A
86
+ * operation existed: `CREATE TABLE IF NOT EXISTS` above never alters an
87
+ * already-existing a2a_tasks table, so a pre-existing one is missing these
88
+ * two columns. Same PRAGMA table_info-guarded ADD COLUMN pattern as
89
+ * src/runstore/sqlite/SqliteRunStore.ts.
90
+ */
91
+ migrateAddStandaloneColumns() {
92
+ const cols = this.db.prepare(`PRAGMA table_info(a2a_tasks)`).all();
93
+ if (!cols.some((c) => c.name === "kind")) {
94
+ this.db.exec(`ALTER TABLE a2a_tasks ADD COLUMN kind TEXT NOT NULL DEFAULT 'invoke'`);
95
+ }
96
+ if (!cols.some((c) => c.name === "result_stage_id")) {
97
+ this.db.exec(`ALTER TABLE a2a_tasks ADD COLUMN result_stage_id TEXT`);
98
+ }
99
+ }
100
+ close() {
101
+ if (this.ownsConnection)
102
+ this.db.close();
103
+ }
104
+ ensureContext(callerId, contextId) {
105
+ if (contextId) {
106
+ const row = this.db
107
+ .prepare("SELECT caller_id FROM a2a_contexts WHERE context_id = ?")
108
+ .get(contextId);
109
+ if (row && row.caller_id !== callerId) {
110
+ throw new Error("Context belongs to a different caller");
111
+ }
112
+ if (row)
113
+ return contextId;
114
+ this.db
115
+ .prepare("INSERT INTO a2a_contexts (context_id, caller_id, created_at) VALUES (?, ?, ?)")
116
+ .run(contextId, callerId, nowIso());
117
+ return contextId;
118
+ }
119
+ const generated = randomUUID();
120
+ this.db
121
+ .prepare("INSERT INTO a2a_contexts (context_id, caller_id, created_at) VALUES (?, ?, ?)")
122
+ .run(generated, callerId, nowIso());
123
+ return generated;
124
+ }
125
+ getMessage(callerId, messageId) {
126
+ const row = this.db
127
+ .prepare("SELECT task_id AS taskId, request_hash AS requestHash, outcome_json AS outcomeJson FROM a2a_messages WHERE caller_id = ? AND message_id = ?")
128
+ .get(callerId, messageId);
129
+ if (!row)
130
+ return undefined;
131
+ return { taskId: row.taskId, requestHash: row.requestHash, outcome: row.outcomeJson ? JSON.parse(row.outcomeJson) : undefined };
132
+ }
133
+ recordMessage(params) {
134
+ this.db
135
+ .prepare("INSERT INTO a2a_messages (caller_id, message_id, task_id, operation, request_hash, outcome_json, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
136
+ .run(params.callerId, params.messageId, params.taskId, params.operation, params.requestHash, JSON.stringify(params.outcome), nowIso());
137
+ }
138
+ getTaskBySubmissionKey(key) {
139
+ return this.db.prepare("SELECT * FROM a2a_tasks WHERE submission_key = ?").get(key);
140
+ }
141
+ createTask(params) {
142
+ const at = nowIso();
143
+ this.db
144
+ .prepare(`INSERT INTO a2a_tasks
145
+ (task_id, context_id, caller_id, publication_id, publication_revision, submission_key, run_id, state, result_json, kind, result_stage_id, created_at, updated_at)
146
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'submitted', NULL, ?, ?, ?, ?)`)
147
+ .run(params.taskId, params.contextId, params.callerId, params.publicationId, params.publicationRevision, params.submissionKey, params.runId, params.kind ?? "invoke", params.resultStageId ?? null, at, at);
148
+ return this.getTask(params.taskId);
149
+ }
150
+ getTask(taskId) {
151
+ return this.db.prepare("SELECT * FROM a2a_tasks WHERE task_id = ?").get(taskId);
152
+ }
153
+ listTasksForCaller(callerId, contextId) {
154
+ if (contextId) {
155
+ return this.db
156
+ .prepare("SELECT * FROM a2a_tasks WHERE caller_id = ? AND context_id = ? ORDER BY created_at ASC")
157
+ .all(callerId, contextId);
158
+ }
159
+ return this.db
160
+ .prepare("SELECT * FROM a2a_tasks WHERE caller_id = ? ORDER BY created_at ASC")
161
+ .all(callerId);
162
+ }
163
+ getArtifact(taskId, artifactId) {
164
+ return this.db
165
+ .prepare("SELECT * FROM a2a_artifacts WHERE task_id = ? AND artifact_id = ?")
166
+ .get(taskId, artifactId);
167
+ }
168
+ listArtifacts(taskId) {
169
+ return this.db.prepare("SELECT * FROM a2a_artifacts WHERE task_id = ?").all(taskId);
170
+ }
171
+ /** Freezes a successful outcome exactly once; a second call for an already-terminal task is a no-op. */
172
+ async freezeCompleted(taskId, result) {
173
+ return this.freeze(taskId, "completed", result);
174
+ }
175
+ /** Freezes a failure exactly once; a second call for an already-terminal task is a no-op. */
176
+ async freezeFailed(taskId) {
177
+ return this.freeze(taskId, "failed", undefined);
178
+ }
179
+ async freeze(taskId, state, result) {
180
+ const current = this.getTask(taskId);
181
+ if (!current || current.state === "completed" || current.state === "failed")
182
+ return;
183
+ const artifactRows = [];
184
+ if (result) {
185
+ for (const artifact of result.artifacts) {
186
+ const artifactId = randomUUID();
187
+ const hash = createHash("sha256").update(artifact.bytes).digest("hex");
188
+ const dir = path.join(this.artifactsRoot, taskId);
189
+ await mkdir(dir, { recursive: true });
190
+ const contentPath = path.join(dir, artifactId);
191
+ await writeFile(contentPath, artifact.bytes);
192
+ artifactRows.push({
193
+ artifact_id: artifactId,
194
+ task_id: taskId,
195
+ name: artifact.name,
196
+ media_type: artifact.mediaType ?? null,
197
+ size: artifact.bytes.length,
198
+ content_path: contentPath,
199
+ hash,
200
+ created_at: nowIso(),
201
+ });
202
+ }
203
+ }
204
+ const resultJson = result ? JSON.stringify({ summary: result.summary, payload: result.payload }) : null;
205
+ const transact = this.db.transaction(() => {
206
+ const outcome = this.db
207
+ .prepare("UPDATE a2a_tasks SET state = ?, result_json = ?, updated_at = ? WHERE task_id = ? AND state NOT IN ('completed', 'failed')")
208
+ .run(state, resultJson, nowIso(), taskId);
209
+ if (outcome.changes === 0)
210
+ return;
211
+ for (const row of artifactRows) {
212
+ this.db
213
+ .prepare("INSERT INTO a2a_artifacts (artifact_id, task_id, name, media_type, size, content_path, hash, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
214
+ .run(row.artifact_id, row.task_id, row.name, row.media_type, row.size, row.content_path, row.hash, row.created_at);
215
+ }
216
+ });
217
+ transact();
218
+ }
219
+ countNonterminal(callerId) {
220
+ const row = this.db
221
+ .prepare("SELECT COUNT(*) AS n FROM a2a_tasks WHERE caller_id = ? AND state NOT IN ('completed', 'failed')")
222
+ .get(callerId);
223
+ return row.n;
224
+ }
225
+ /** Deletes terminal tasks (and their frozen artifact files) and message tombstones older than the retention window. */
226
+ async pruneExpired(now, terminalRetentionMs = TERMINAL_RETENTION_MS, messageRetentionMs = MESSAGE_TOMBSTONE_RETENTION_MS) {
227
+ const terminalCutoff = new Date(now.getTime() - terminalRetentionMs).toISOString();
228
+ const messageCutoff = new Date(now.getTime() - messageRetentionMs).toISOString();
229
+ const expiredTasks = this.db
230
+ .prepare("SELECT task_id FROM a2a_tasks WHERE state IN ('completed', 'failed') AND updated_at < ?")
231
+ .all(terminalCutoff);
232
+ for (const { task_id: taskId } of expiredTasks) {
233
+ for (const artifact of this.listArtifacts(taskId)) {
234
+ await unlink(artifact.content_path).catch(() => undefined);
235
+ }
236
+ this.db.transaction(() => {
237
+ this.db.prepare("DELETE FROM a2a_artifacts WHERE task_id = ?").run(taskId);
238
+ this.db.prepare("DELETE FROM a2a_messages WHERE task_id = ?").run(taskId);
239
+ this.db.prepare("DELETE FROM a2a_tasks WHERE task_id = ?").run(taskId);
240
+ })();
241
+ }
242
+ const removedMessages = this.db.prepare("DELETE FROM a2a_messages WHERE created_at < ?").run(messageCutoff).changes;
243
+ return { removedTasks: expiredTasks.length, removedMessages };
244
+ }
245
+ }
@@ -86,6 +86,11 @@ export type StageLifecycleEvent = {
86
86
  /** Operator chose to leave the verified failure terminal. */
87
87
  | {
88
88
  event: "manual_recovery_stopped";
89
+ } | {
90
+ event: "feedback_loop_decided";
91
+ decision: "extend" | "continue" | "abandon";
92
+ loopId: string;
93
+ reason?: string;
89
94
  };
90
95
  export type StageLogLine = StageActivityEvent | StageLifecycleEvent;
91
96
  export declare function truncateActivityText(value: unknown, limit?: number): string | undefined;
@@ -1,5 +1,6 @@
1
1
  import { type StageActivityEvent } from "./activity.js";
2
2
  export declare const TOOL_PROGRESS_THROTTLE_MS = 300;
3
+ export declare const STREAM_FLUSH_THROTTLE_MS = 300;
3
4
  export type StageActivityObserver = {
4
5
  onAssistantTextDelta(delta: string): void;
5
6
  onThinkingDelta(delta: string): void;
@@ -10,6 +11,8 @@ export type StageActivityObserver = {
10
11
  };
11
12
  export type CreateStageActivityObserverOptions = {
12
13
  onActivity?: (event: StageActivityEvent) => void;
14
+ /** Fired for every non-empty assistant text delta, regardless of writeStderr. */
15
+ onAssistantTextDelta?: (delta: string) => void;
13
16
  writeStderr?: boolean;
14
17
  toolProgressThrottleMs?: number;
15
18
  };
@@ -1,5 +1,6 @@
1
1
  import { truncateActivityText, } from "./activity.js";
2
2
  export const TOOL_PROGRESS_THROTTLE_MS = 300;
3
+ export const STREAM_FLUSH_THROTTLE_MS = 300;
3
4
  function writeIndentedPreview(preview) {
4
5
  for (const line of preview.split("\n")) {
5
6
  process.stderr.write(` ${line}\n`);
@@ -127,7 +128,11 @@ export function createStageActivityObserver(options = {}) {
127
128
  };
128
129
  return {
129
130
  onAssistantTextDelta(delta) {
130
- if (!writeStderr || !delta) {
131
+ if (!delta) {
132
+ return;
133
+ }
134
+ options.onAssistantTextDelta?.(delta);
135
+ if (!writeStderr) {
131
136
  return;
132
137
  }
133
138
  assistantStreamedForMessage = true;
@@ -371,8 +371,12 @@ export function routeSessionEventToProgress(event, options) {
371
371
  * Map Pi session events to StageActivityEvent at the adapter edge, then
372
372
  * forward to the activity observer (stderr + onActivity).
373
373
  */
374
- function attachStageProgress(session, onActivity, usage) {
375
- const observer = createStageActivityObserver({ onActivity, writeStderr: true });
374
+ function attachStageProgress(session, onActivity, usage, onAssistantTextDelta) {
375
+ const observer = createStageActivityObserver({
376
+ onActivity,
377
+ onAssistantTextDelta,
378
+ writeStderr: true,
379
+ });
376
380
  const verbose = readActivityVerbose();
377
381
  const unsubscribe = session.subscribe((event) => {
378
382
  routeSessionEventToProgress(event, {
@@ -1119,7 +1123,7 @@ export class PiAgentAdapter {
1119
1123
  return;
1120
1124
  }
1121
1125
  session = bound;
1122
- unsubscribeProgress = attachStageProgress(session, input.onActivity, wiring.usage);
1126
+ unsubscribeProgress = attachStageProgress(session, input.onActivity, wiring.usage, input.onAssistantTextDelta);
1123
1127
  })();
1124
1128
  const runWithTimeout = async (work) => {
1125
1129
  await preparePromise;
@@ -60,6 +60,8 @@ export type StageRunInput = {
60
60
  sessionMode?: StageSessionMode;
61
61
  /** Optional observe hook; HITL wait/answer uses openStage beside this. */
62
62
  onActivity?: (event: StageActivityEvent) => void;
63
+ /** Optional raw assistant-text-delta sink, fired alongside onActivity, not instead of it. */
64
+ onAssistantTextDelta?: (delta: string) => void;
63
65
  skillFilePath?: string;
64
66
  forkEmitContext?: ForkEmitContext;
65
67
  cloneEmitContext?: CloneEmitContext;
@@ -0,0 +1,19 @@
1
+ import { type PiHomeDetectResult, type ProviderAuthContext, type ProviderAuthStatus, type ProviderSummary, type ProvidersListResult } from "./providerAuth.js";
2
+ export type ProviderReadinessRow = ProviderSummary & {
3
+ configured: boolean;
4
+ authKind?: ProviderAuthStatus["authKind"];
5
+ source?: string;
6
+ };
7
+ export type ProviderReadinessInspect = {
8
+ authShell: ProvidersListResult["authShell"];
9
+ via: ProvidersListResult["via"];
10
+ detect: PiHomeDetectResult;
11
+ providers: ProviderReadinessRow[];
12
+ };
13
+ export declare function inspectProviderReadiness(cwd: string, ctx?: ProviderAuthContext): Promise<ProviderReadinessInspect>;
14
+ export declare function mapProviderAuthError(err: unknown): {
15
+ status: number;
16
+ body: {
17
+ error: string;
18
+ };
19
+ };
@@ -0,0 +1,35 @@
1
+ import { defaultContext, detectPiHome, getAuthStatus, listProviders, ProviderAuthError, } from "./providerAuth.js";
2
+ function statusList(statuses) {
3
+ return Array.isArray(statuses) ? statuses : [statuses];
4
+ }
5
+ export async function inspectProviderReadiness(cwd, ctx = defaultContext) {
6
+ const listed = await listProviders(cwd, ctx);
7
+ const statuses = statusList(await getAuthStatus(cwd, undefined, ctx));
8
+ const statusById = new Map(statuses.map((status) => [status.providerId, status]));
9
+ const detect = detectPiHome(cwd);
10
+ return {
11
+ authShell: listed.authShell,
12
+ via: listed.via,
13
+ detect,
14
+ providers: listed.providers.map((provider) => {
15
+ const status = statusById.get(provider.id);
16
+ return {
17
+ ...provider,
18
+ configured: status?.configured ?? false,
19
+ ...(status?.authKind !== undefined
20
+ ? { authKind: status.authKind }
21
+ : {}),
22
+ ...(status?.source !== undefined ? { source: status.source } : {}),
23
+ };
24
+ }),
25
+ };
26
+ }
27
+ export function mapProviderAuthError(err) {
28
+ if (err instanceof ProviderAuthError) {
29
+ return { status: err.status, body: { error: err.message } };
30
+ }
31
+ return {
32
+ status: 500,
33
+ body: { error: "Provider auth operation failed" },
34
+ };
35
+ }
@@ -0,0 +1,2 @@
1
+ /** Scrub common secret-shaped substrings before text is persisted or returned. */
2
+ export declare function redactSecrets(text: string): string;
@@ -0,0 +1,13 @@
1
+ const SECRET_PATTERNS = [
2
+ /\bsk-[A-Za-z0-9_-]{16,}\b/g,
3
+ /\bghp_[A-Za-z0-9]{20,}\b/g,
4
+ /\b[A-Za-z_]*KEY[A-Za-z_]*\s*[:=]\s*['"]?[A-Za-z0-9+/_-]{16,}['"]?/gi,
5
+ ];
6
+ /** Scrub common secret-shaped substrings before text is persisted or returned. */
7
+ export function redactSecrets(text) {
8
+ let out = text;
9
+ for (const pattern of SECRET_PATTERNS) {
10
+ out = out.replace(pattern, "[redacted]");
11
+ }
12
+ return out;
13
+ }
@@ -0,0 +1,8 @@
1
+ export declare const A2A_USAGE = "sf a2a validate [--config <path>]\nsf a2a list [--config <path>]\nsf a2a add-caller <id> [--config <path>] [--token-env <NAME>]\n\nValidate or inspect an A2A publication configuration, or register a new caller.\n--config defaults to <project-root>/a2a.yaml when omitted -- the same file\nSTAGEFLOW_A2A_CONFIG would point at if set. Caller credentials are read from\nthe environment variables named in the configuration; add-caller only writes\nthe caller's id and its token_env name to the file, never a secret value.";
2
+ export declare function runA2aCommand(args: string[], options: {
3
+ cwd: string;
4
+ projectRoot?: string;
5
+ env?: NodeJS.ProcessEnv;
6
+ log?: (line: string) => void;
7
+ error?: (line: string) => void;
8
+ }): Promise<number>;
@@ -0,0 +1,131 @@
1
+ import path from "node:path";
2
+ import { randomBytes } from "node:crypto";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { parse, stringify } from "yaml";
5
+ import { loadPublicationRegistry } from "../a2a/registry.js";
6
+ import { a2aConfigPathFor, resolveA2aConfigPath } from "../a2a/configDiscovery.js";
7
+ export const A2A_USAGE = `sf a2a validate [--config <path>]
8
+ sf a2a list [--config <path>]
9
+ sf a2a add-caller <id> [--config <path>] [--token-env <NAME>]
10
+
11
+ Validate or inspect an A2A publication configuration, or register a new caller.
12
+ --config defaults to <project-root>/a2a.yaml when omitted -- the same file
13
+ STAGEFLOW_A2A_CONFIG would point at if set. Caller credentials are read from
14
+ the environment variables named in the configuration; add-caller only writes
15
+ the caller's id and its token_env name to the file, never a secret value.`;
16
+ const SCAFFOLD = { version: 1, callers: [], publications: [] };
17
+ function deriveTokenEnvName(callerId) {
18
+ const normalized = callerId
19
+ .trim()
20
+ .toUpperCase()
21
+ .replace(/[^A-Z0-9]+/g, "_")
22
+ .replace(/^_+|_+$/g, "");
23
+ return (normalized || "CALLER") + "_TOKEN";
24
+ }
25
+ function parseFlags(args) {
26
+ let config;
27
+ let tokenEnv;
28
+ const positionals = [];
29
+ for (let i = 0; i < args.length; i += 1) {
30
+ if (args[i] === "--config") {
31
+ config = args[(i += 1)];
32
+ }
33
+ else if (args[i] === "--token-env") {
34
+ tokenEnv = args[(i += 1)];
35
+ }
36
+ else {
37
+ positionals.push(args[i]);
38
+ }
39
+ }
40
+ return { config, tokenEnv, positionals };
41
+ }
42
+ async function addCaller(configPath, callerId, tokenEnvOverride) {
43
+ let raw;
44
+ try {
45
+ raw = await readFile(configPath, "utf8");
46
+ }
47
+ catch {
48
+ raw = undefined;
49
+ }
50
+ const config = raw ? (parse(raw) ?? {}) : { ...SCAFFOLD };
51
+ if (typeof config !== "object" || Array.isArray(config)) {
52
+ throw new Error(configPath + " does not contain a YAML mapping");
53
+ }
54
+ if (config.version === undefined)
55
+ config.version = 1;
56
+ if (config.callers === undefined)
57
+ config.callers = [];
58
+ if (config.publications === undefined)
59
+ config.publications = [];
60
+ const callers = config.callers;
61
+ if (!Array.isArray(callers)) {
62
+ throw new Error('"callers" in ' + configPath + " is not a list");
63
+ }
64
+ if (callers.some((entry) => entry && typeof entry === "object" && entry.id === callerId)) {
65
+ throw new Error('Caller "' + callerId + '" already exists in ' + configPath);
66
+ }
67
+ const tokenEnv = tokenEnvOverride ?? deriveTokenEnvName(callerId);
68
+ callers.push({ id: callerId, token_env: tokenEnv });
69
+ await mkdir(path.dirname(configPath), { recursive: true });
70
+ await writeFile(configPath, stringify(config));
71
+ const token = randomBytes(24).toString("hex");
72
+ return { tokenEnv, token, created: raw === undefined };
73
+ }
74
+ export async function runA2aCommand(args, options) {
75
+ const log = options.log ?? console.log;
76
+ const error = options.error ?? console.error;
77
+ const env = options.env ?? process.env;
78
+ const projectRoot = options.projectRoot ?? options.cwd;
79
+ if (args[0] === "--help" || args[0] === "-h") {
80
+ log(A2A_USAGE);
81
+ return 0;
82
+ }
83
+ const sub = args[0];
84
+ if (sub !== "validate" && sub !== "list" && sub !== "add-caller") {
85
+ error(A2A_USAGE);
86
+ return 1;
87
+ }
88
+ const { config, tokenEnv, positionals } = parseFlags(args.slice(1));
89
+ if (sub === "add-caller") {
90
+ if (positionals.length !== 1 || !positionals[0]) {
91
+ error(A2A_USAGE);
92
+ return 1;
93
+ }
94
+ const configPath = config ? path.resolve(options.cwd, config) : a2aConfigPathFor(projectRoot);
95
+ try {
96
+ const result = await addCaller(configPath, positionals[0], tokenEnv);
97
+ log(JSON.stringify({ ok: true, configPath, callerId: positionals[0], tokenEnv: result.tokenEnv, created: result.created }));
98
+ log('Generated a token for caller "' + positionals[0] + '". Export it before starting the host (or store it in your secrets manager):');
99
+ log(" export " + result.tokenEnv + "=" + result.token);
100
+ if (result.created) {
101
+ log("Created " + configPath + " with no publications yet -- add at least one under `publications:` before this config can be loaded.");
102
+ }
103
+ return 0;
104
+ }
105
+ catch (err) {
106
+ error(err instanceof Error ? err.message : String(err));
107
+ return 1;
108
+ }
109
+ }
110
+ if (positionals.length !== 0) {
111
+ error(A2A_USAGE);
112
+ return 1;
113
+ }
114
+ const configPath = config ? path.resolve(options.cwd, config) : resolveA2aConfigPath(projectRoot, env);
115
+ if (!configPath) {
116
+ error("No A2A configuration found. Pass --config <path>, or create " + a2aConfigPathFor(projectRoot) + ".");
117
+ return 1;
118
+ }
119
+ try {
120
+ const registry = await loadPublicationRegistry(configPath, env);
121
+ log(JSON.stringify({ ok: true, configPath: registry.configPath, publicUrl: registry.publicUrl, mode: "discovery", publications: registry.summaries() }));
122
+ if (sub === "list") {
123
+ log("Configuration validated locally. An already-running host must be restarted explicitly to pick up changes.");
124
+ }
125
+ return 0;
126
+ }
127
+ catch (err) {
128
+ error(err instanceof Error ? err.message : String(err));
129
+ return 1;
130
+ }
131
+ }
@@ -1,6 +1,7 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { readRunArtifact } from "../mcp/readArtifact.js";
4
+ import { globalStageflowHome } from "../project/globalHome.js";
4
5
  import { createRunStore } from "../runstore/createStore.js";
5
6
  import { isInsideDir } from "../runstore/workspaceLayout.js";
6
7
  export const ARTIFACT_USAGE = `Usage:
@@ -73,7 +74,6 @@ function resolveSafeOutPath(outPath, cwd) {
73
74
  }
74
75
  export async function runArtifactCommand(args, options = {}) {
75
76
  const cwd = options.cwd ?? process.cwd();
76
- const projectRoot = options.projectRoot ?? cwd;
77
77
  const out = { ...defaultIo, ...options.io };
78
78
  let parsed;
79
79
  try {
@@ -99,7 +99,7 @@ export async function runArtifactCommand(args, options = {}) {
99
99
  out.error(ARTIFACT_USAGE);
100
100
  return 1;
101
101
  }
102
- const store = createRunStore({ rootDir: projectRoot });
102
+ const store = createRunStore({ rootDir: globalStageflowHome() });
103
103
  try {
104
104
  const contents = await readRunArtifact(store, parsed.runId, parsed.artifactPath);
105
105
  if (parsed.outPath !== undefined) {
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { buildHandoffDeliverable, isHandoffSkipped, } from "./handoffFormat.js";
4
+ import { globalStageflowHome } from "../project/globalHome.js";
4
5
  import { createRunStore } from "../runstore/createStore.js";
5
6
  export const ENVELOPE_USAGE = `Usage:
6
7
  sf envelope get --run <runId> --stage <stageId> [--json] [--from <sf-run.json>] [--detect-stage <id>] [--format envelope|handoff]`;
@@ -198,7 +199,6 @@ function formatHandoffHuman(result) {
198
199
  }
199
200
  export async function runEnvelopeCommand(args, options = {}) {
200
201
  const cwd = options.cwd ?? process.cwd();
201
- const projectRoot = options.projectRoot ?? cwd;
202
202
  const out = { ...defaultIo, ...options.io };
203
203
  let parsed;
204
204
  try {
@@ -224,7 +224,7 @@ export async function runEnvelopeCommand(args, options = {}) {
224
224
  out.error(ENVELOPE_USAGE);
225
225
  return 1;
226
226
  }
227
- const store = createRunStore({ rootDir: projectRoot });
227
+ const store = createRunStore({ rootDir: globalStageflowHome() });
228
228
  let runContext;
229
229
  try {
230
230
  runContext = resolveRunContext(parsed, cwd, store);
@@ -1,6 +1,7 @@
1
1
  import { readFileSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { projectRun } from "../projection/projectRun.js";
4
+ import { globalStageflowHome } from "../project/globalHome.js";
4
5
  import { createRunStore } from "../runstore/createStore.js";
5
6
  import { isInsideDir } from "../runstore/workspaceLayout.js";
6
7
  export const EXPORT_RUN_USAGE = `Usage:
@@ -94,7 +95,6 @@ function assertRunComplete(status, runId) {
94
95
  }
95
96
  export async function runExportRunCommand(args, options = {}) {
96
97
  const cwd = options.cwd ?? process.cwd();
97
- const projectRoot = options.projectRoot ?? cwd;
98
98
  const out = { ...defaultIo, ...options.io };
99
99
  let parsed;
100
100
  try {
@@ -126,7 +126,7 @@ export async function runExportRunCommand(args, options = {}) {
126
126
  out.error(EXPORT_RUN_USAGE);
127
127
  return 1;
128
128
  }
129
- const store = createRunStore({ rootDir: projectRoot });
129
+ const store = createRunStore({ rootDir: globalStageflowHome() });
130
130
  try {
131
131
  const detail = await store.readRun(runId);
132
132
  assertRunComplete(detail.status, runId);
@@ -0,0 +1,43 @@
1
+ import type { PipelineRunResult } from "../runtime/pipelineRunner.js";
2
+ import type { AbandonStageResult, DecideFeedbackLoopResult, StartRunResult, StopManualRecoveryResult } from "../runtime/runManager.js";
3
+ import type { DeliverAnswerResult } from "../runtime/stageHitl.js";
4
+ import type { RetryStageResult } from "../runtime/runRetryCoordinator.js";
5
+ import type { RunDetail, RunStore } from "../runstore/port.js";
6
+ export declare function computeRunDir(runId: string): string;
7
+ export declare function httpReadRun(base: string, runId: string): Promise<RunDetail>;
8
+ /** Thin RunStore-shaped reader backed by the global service, for the CLI output helpers that only need `readRun`. */
9
+ export declare function httpStoreReader(base: string): Pick<RunStore, "readRun">;
10
+ export declare function pollRunUntilTerminal(base: string, runId: string, options?: {
11
+ pollIntervalMs?: number;
12
+ }): Promise<RunDetail>;
13
+ export declare function runDetailToPipelineRunResult(detail: RunDetail): PipelineRunResult;
14
+ export type HttpStartRunInput = {
15
+ pipeline: string;
16
+ task: string;
17
+ checkoutOverride?: string;
18
+ skipGates?: boolean;
19
+ gitSha?: string;
20
+ ciPrUrl?: string;
21
+ ciJobUrl?: string;
22
+ };
23
+ export declare function httpStartRun(base: string, input: HttpStartRunInput): Promise<StartRunResult>;
24
+ export declare function httpRerun(base: string, runId: string): Promise<StartRunResult>;
25
+ export declare function httpDeliverAnswer(base: string, runId: string, stageId: string, answer: unknown): Promise<DeliverAnswerResult>;
26
+ export declare function httpDecideFeedbackLoop(base: string, runId: string, stageId: string, input: {
27
+ decision: "extend" | "continue" | "abandon";
28
+ loopId?: string;
29
+ reason?: string;
30
+ }): Promise<DecideFeedbackLoopResult>;
31
+ type RetryOrchestrationResult = {
32
+ ok: true;
33
+ pipeline: PipelineRunResult;
34
+ } | Extract<RetryStageResult, {
35
+ ok: false;
36
+ }>;
37
+ export declare function httpRetryStageUntilStop(base: string, runId: string, stageId: string): Promise<RetryOrchestrationResult>;
38
+ export declare function httpResumeTimedOutStage(base: string, runId: string, stageId: string): Promise<RetryStageResult>;
39
+ export declare function httpRecoverManualStageUntilStop(base: string, runId: string, stageId: string, guidance?: string): Promise<RetryOrchestrationResult>;
40
+ export declare function httpStopManualRecovery(base: string, runId: string, stageId: string): Promise<StopManualRecoveryResult>;
41
+ export declare function httpAbandonStage(base: string, runId: string, stageId: string): Promise<AbandonStageResult>;
42
+ export declare function resolveAbsolute(cwd: string, maybeRelative: string): string;
43
+ export {};