stageflow 0.9.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +6 -1
  2. package/dist/agent/activity.d.ts +9 -0
  3. package/dist/agent/agentBackend.d.ts +32 -0
  4. package/dist/agent/agentBackend.js +38 -0
  5. package/dist/agent/claudeActivity.d.ts +15 -0
  6. package/dist/agent/claudeActivity.js +58 -0
  7. package/dist/agent/claudeAdapter.d.ts +5 -0
  8. package/dist/agent/claudeAdapter.js +331 -0
  9. package/dist/agent/claudeSession.d.ts +18 -0
  10. package/dist/agent/claudeSession.js +54 -0
  11. package/dist/agent/claudeTools.d.ts +89 -0
  12. package/dist/agent/claudeTools.js +135 -0
  13. package/dist/agent/fakeAgent.js +24 -4
  14. package/dist/agent/piAdapter.d.ts +12 -7
  15. package/dist/agent/piAdapter.js +194 -44
  16. package/dist/agent/piIsolatedMcp.d.ts +23 -0
  17. package/dist/agent/piIsolatedMcp.js +223 -0
  18. package/dist/agent/piIsolatedMcpProbe.d.ts +15 -0
  19. package/dist/agent/piIsolatedMcpProbe.js +292 -0
  20. package/dist/agent/port.d.ts +52 -1
  21. package/dist/agent/port.js +1 -1
  22. package/dist/agent/resolveAgentPort.d.ts +26 -0
  23. package/dist/agent/resolveAgentPort.js +39 -0
  24. package/dist/cli/runCommand.js +2 -2
  25. package/dist/cli/runsCommand.d.ts +1 -1
  26. package/dist/cli/runsCommand.js +184 -2
  27. package/dist/cli.d.ts +1 -1
  28. package/dist/cli.js +11 -5
  29. package/dist/config/browseCatalog.js +1 -1
  30. package/dist/config/createPipeline.d.ts +2 -1
  31. package/dist/config/createPipeline.js +34 -9
  32. package/dist/config/createStage.js +14 -9
  33. package/dist/config/loadPipeline.js +11 -1
  34. package/dist/config/loadStage.d.ts +1 -0
  35. package/dist/config/loadStage.js +207 -2
  36. package/dist/config/loadStageflowManifest.js +14 -1
  37. package/dist/config/mergePipelineIncludes.d.ts +1 -0
  38. package/dist/config/mergePipelineIncludes.js +14 -1
  39. package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
  40. package/dist/config/normalizePipelineStageEntry.js +56 -7
  41. package/dist/config/parseCompletionContract.d.ts +8 -0
  42. package/dist/config/parseCompletionContract.js +218 -0
  43. package/dist/config/parsePreEmitChecks.d.ts +3 -0
  44. package/dist/config/parsePreEmitChecks.js +65 -0
  45. package/dist/config/pipelineNeeds.d.ts +19 -0
  46. package/dist/config/pipelineNeeds.js +151 -0
  47. package/dist/config/pipelineStageKeys.js +17 -1
  48. package/dist/config/resolveForkEmitContext.d.ts +6 -2
  49. package/dist/config/resolveForkEmitContext.js +9 -2
  50. package/dist/config/resolvePipelineDag.d.ts +2 -1
  51. package/dist/config/resolvePipelineDag.js +182 -39
  52. package/dist/config/resolveStageMcpServers.d.ts +35 -0
  53. package/dist/config/resolveStageMcpServers.js +207 -0
  54. package/dist/config/validateCatalog.d.ts +1 -1
  55. package/dist/config/validateCatalog.js +51 -0
  56. package/dist/config/validateCompletionContract.d.ts +4 -0
  57. package/dist/config/validateCompletionContract.js +52 -0
  58. package/dist/envelope/check.d.ts +1 -1
  59. package/dist/envelope/check.js +86 -13
  60. package/dist/envelope/cloneForks.d.ts +1 -1
  61. package/dist/envelope/cloneForks.js +24 -9
  62. package/dist/envelope/feedbackLoop.d.ts +4 -0
  63. package/dist/envelope/feedbackLoop.js +31 -0
  64. package/dist/envelope/payloadSchema.d.ts +7 -1
  65. package/dist/envelope/payloadSchema.js +170 -13
  66. package/dist/envelope/preEmitChecks.d.ts +29 -0
  67. package/dist/envelope/preEmitChecks.js +57 -0
  68. package/dist/hitl/qaTrail.d.ts +25 -0
  69. package/dist/hitl/qaTrail.js +51 -0
  70. package/dist/mcp/catalogTools.js +4 -2
  71. package/dist/mcp/controlTools.js +72 -0
  72. package/dist/mcp/readArtifact.d.ts +2 -0
  73. package/dist/mcp/readArtifact.js +25 -1
  74. package/dist/mcp/waitingGates.d.ts +2 -1
  75. package/dist/mcp/waitingGates.js +17 -1
  76. package/dist/package-meta.d.ts +1 -1
  77. package/dist/package-meta.js +1 -1
  78. package/dist/projection/projectRun.d.ts +4 -1
  79. package/dist/projection/projectRun.js +7 -0
  80. package/dist/prompt/feedbackLoopContext.d.ts +2 -0
  81. package/dist/prompt/feedbackLoopContext.js +17 -0
  82. package/dist/prompt/priorEnvelope.d.ts +2 -2
  83. package/dist/prompt/priorEnvelope.js +4 -1
  84. package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
  85. package/dist/runstore/pipelineDagSnapshot.js +51 -11
  86. package/dist/runstore/port.d.ts +185 -3
  87. package/dist/runstore/port.js +42 -3
  88. package/dist/runstore/runProjection.d.ts +7 -3
  89. package/dist/runstore/runProjection.js +30 -15
  90. package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
  91. package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
  92. package/dist/runstore/sqlite/schema.d.ts +1 -1
  93. package/dist/runstore/sqlite/schema.js +99 -0
  94. package/dist/runstore/stageInstanceId.d.ts +1 -1
  95. package/dist/runstore/stageInstanceId.js +3 -2
  96. package/dist/runstore/trackProjection.js +59 -27
  97. package/dist/runstore/verificationHistory.d.ts +19 -0
  98. package/dist/runstore/verificationHistory.js +30 -0
  99. package/dist/runtime/cloneSchedule.d.ts +40 -4
  100. package/dist/runtime/cloneSchedule.js +212 -28
  101. package/dist/runtime/completionCheckRunner.d.ts +137 -0
  102. package/dist/runtime/completionCheckRunner.js +365 -0
  103. package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
  104. package/dist/runtime/completionCheckoutBaseline.js +44 -0
  105. package/dist/runtime/envelopeRouting.d.ts +10 -3
  106. package/dist/runtime/envelopeRouting.js +146 -5
  107. package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
  108. package/dist/runtime/feedbackLoopCoordinator.js +446 -0
  109. package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
  110. package/dist/runtime/feedbackLoopDecision.js +207 -0
  111. package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
  112. package/dist/runtime/feedbackLoopRoute.js +13 -0
  113. package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
  114. package/dist/runtime/feedbackLoopSchedule.js +72 -0
  115. package/dist/runtime/forkGeneration.d.ts +72 -0
  116. package/dist/runtime/forkGeneration.js +295 -0
  117. package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
  118. package/dist/runtime/gitCheckoutCapability.js +97 -0
  119. package/dist/runtime/manualRecoveryState.d.ts +40 -0
  120. package/dist/runtime/manualRecoveryState.js +73 -0
  121. package/dist/runtime/pipelineScheduler.d.ts +13 -1
  122. package/dist/runtime/pipelineScheduler.js +328 -45
  123. package/dist/runtime/replayLifecycle.d.ts +107 -0
  124. package/dist/runtime/replayLifecycle.js +168 -0
  125. package/dist/runtime/runManager.d.ts +54 -1
  126. package/dist/runtime/runManager.js +471 -6
  127. package/dist/runtime/runRetryCoordinator.d.ts +6 -1
  128. package/dist/runtime/runRetryCoordinator.js +43 -9
  129. package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
  130. package/dist/runtime/stageAttemptBootstrap.js +117 -3
  131. package/dist/runtime/stageProcessLauncher.d.ts +1 -1
  132. package/dist/runtime/stageRecovery.js +1 -1
  133. package/dist/runtime/stageRunner.d.ts +4 -1
  134. package/dist/runtime/stageRunner.js +29 -3
  135. package/dist/runtime/stageWorker.js +69 -2
  136. package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
  137. package/dist/runtime/verifiedStageExecution.d.ts +31 -0
  138. package/dist/runtime/verifiedStageExecution.js +127 -0
  139. package/dist/server/bootstrap.js +1 -0
  140. package/dist/server/http.js +137 -4
  141. package/dist/server/operatorResults.js +2 -0
  142. package/dist/server/projectMcpRoutes.d.ts +6 -0
  143. package/dist/server/projectMcpRoutes.js +38 -0
  144. package/dist/tools/askOperator.d.ts +161 -5
  145. package/dist/tools/askOperator.js +58 -25
  146. package/dist/tools/emitStageEnvelope.d.ts +12 -12
  147. package/dist/tools/emitStageEnvelope.js +112 -24
  148. package/dist/types/completion.d.ts +43 -0
  149. package/dist/types/completion.js +1 -0
  150. package/dist/types/envelope.d.ts +18 -0
  151. package/dist/types/forkChoice.d.ts +9 -4
  152. package/dist/types/forkChoice.js +1 -1
  153. package/dist/types/pipeline.d.ts +41 -2
  154. package/dist/types/preEmitCheck.d.ts +18 -0
  155. package/dist/types/preEmitCheck.js +1 -0
  156. package/dist/types/stage.d.ts +14 -0
  157. package/dist/types/stageflowManifest.d.ts +2 -0
  158. package/dist/ui/assets/index-Yzh42vhf.js +118 -0
  159. package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
  160. package/dist/ui/index.html +2 -2
  161. package/package.json +5 -1
  162. package/skills/stageflow-author/SKILL.md +20 -2
  163. package/skills/stageflow-author/references/catalog-mapping.md +88 -3
  164. package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
  165. package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
  166. package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -1
  167. package/dist/ui/assets/index-CSoGDA3A.js +0 -118
@@ -0,0 +1,127 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { listQaExchanges } from "../hitl/qaTrail.js";
4
+ import { attemptArtifactsDir, attemptWorkspaceDir, } from "../runstore/workspaceLayout.js";
5
+ import { COMPLETION_VERIFICATION_FAILURE_PREFIX, } from "../types/completion.js";
6
+ import { runCompletionContract, } from "./completionCheckRunner.js";
7
+ import { readCompletionCheckoutBaseline, writeCompletionCheckoutBaseline, } from "./completionCheckoutBaseline.js";
8
+ import { createGitCheckoutCapability } from "./gitCheckoutCapability.js";
9
+ /** Sidecar written before completion command checks so they can inspect fork_choice. */
10
+ export const COMPLETION_CANDIDATE_ENVELOPE_FILENAME = "completion-candidate-envelope.json";
11
+ export function completionVerificationFailureReason(failedCheckIds) {
12
+ return `${COMPLETION_VERIFICATION_FAILURE_PREFIX} ${failedCheckIds.join(", ")}`;
13
+ }
14
+ function gateDecisionsForAttempt(options) {
15
+ const { store, runId, stageId, attempt } = options;
16
+ return {
17
+ async listDecisions(kind) {
18
+ const events = await store.listStageEvents(runId, stageId, attempt);
19
+ return listQaExchanges(events)
20
+ .filter((exchange) => exchange.prompt.kind === kind && exchange.answer !== null)
21
+ .map((exchange) => {
22
+ const answer = exchange.answer;
23
+ if (answer.kind === "confirm" || answer.kind === "artifact_backed") {
24
+ return {
25
+ kind,
26
+ prompt_id: exchange.prompt.id,
27
+ status: answer.decision === "accept" ? "accepted" : "rejected",
28
+ };
29
+ }
30
+ return { kind, prompt_id: exchange.prompt.id, status: "answered" };
31
+ });
32
+ },
33
+ };
34
+ }
35
+ /**
36
+ * Owns the verification lifecycle for one immutable stage attempt: establish
37
+ * observation before agent work, then persist evidence before accepting a
38
+ * candidate handoff. Agent lifecycle and final stage status stay outside this
39
+ * module in stageRunner.
40
+ */
41
+ export function createVerifiedStageExecution(options) {
42
+ const { store, runId, stageId, attempt, stage, dag, roots } = options;
43
+ const contract = dag?.nodes.find((node) => node.id === stageId)?.completion;
44
+ let checkout;
45
+ let checkoutBefore;
46
+ let checkoutError;
47
+ return {
48
+ async prepare() {
49
+ if (!contract?.checks.some((check) => check.type === "checkout_changes")) {
50
+ return;
51
+ }
52
+ if (roots.checkoutRoot === undefined) {
53
+ checkoutError = "checkout check requires a bound checkout";
54
+ return;
55
+ }
56
+ checkout = createGitCheckoutCapability(roots.checkoutRoot);
57
+ try {
58
+ checkoutBefore = await readCompletionCheckoutBaseline(roots.runWorkspaceDir, stageId, attempt);
59
+ if (checkoutBefore === undefined) {
60
+ checkoutBefore = await checkout.capture();
61
+ await writeCompletionCheckoutBaseline(roots.runWorkspaceDir, stageId, attempt, checkoutBefore);
62
+ }
63
+ }
64
+ catch (error) {
65
+ checkoutError = `could not capture checkout baseline: ${error instanceof Error ? error.message : String(error)}`;
66
+ }
67
+ },
68
+ async verify(candidate) {
69
+ if (contract === undefined)
70
+ return candidate;
71
+ try {
72
+ const artifactsDir = attemptArtifactsDir(roots.runWorkspaceDir, stageId, attempt);
73
+ const attemptDir = attemptWorkspaceDir(roots.runWorkspaceDir, stageId, attempt);
74
+ await mkdir(attemptDir, { recursive: true });
75
+ await writeFile(path.join(attemptDir, COMPLETION_CANDIDATE_ENVELOPE_FILENAME), `${JSON.stringify(candidate.envelope, null, 2)}\n`, "utf8");
76
+ const verification = await runCompletionContract({
77
+ contract,
78
+ envelope: candidate.envelope,
79
+ payloadSchema: stage.payload_schema,
80
+ artifactsDir,
81
+ commandWorkingDirectory: roots.cwd,
82
+ gates: gateDecisionsForAttempt({ store, runId, stageId, attempt }),
83
+ checkout,
84
+ checkoutBefore,
85
+ checkoutError,
86
+ checklistAttestations: candidate.envelope.checklist_attestations,
87
+ onCheckStart: async (check) => {
88
+ await store.upsertVerificationCheckResult(runId, stageId, {
89
+ check_id: check.id,
90
+ check_type: check.type,
91
+ status: "running",
92
+ started_at: new Date().toISOString(),
93
+ }, { attempt });
94
+ },
95
+ onCheckComplete: async (check) => {
96
+ await store.upsertVerificationCheckResult(runId, stageId, {
97
+ check_id: check.check_id,
98
+ check_type: check.check_type,
99
+ status: check.outcome === "passed" ? "passed" : "failed",
100
+ finished_at: new Date().toISOString(),
101
+ evidence: {
102
+ ...check.evidence,
103
+ ...(check.message !== undefined ? { message: check.message } : {}),
104
+ ...(check.outcome === "error" ? { runner_outcome: "error" } : {}),
105
+ },
106
+ }, { attempt });
107
+ },
108
+ });
109
+ await store.updateStageExecution(runId, stageId, attempt, {
110
+ verification_outcome: verification.outcome,
111
+ });
112
+ if (verification.outcome === "passed")
113
+ return candidate;
114
+ return {
115
+ ok: false,
116
+ reason: completionVerificationFailureReason(verification.failed_check_ids),
117
+ };
118
+ }
119
+ catch (error) {
120
+ await store.updateStageExecution(runId, stageId, attempt, {
121
+ verification_outcome: "error",
122
+ });
123
+ throw error;
124
+ }
125
+ },
126
+ };
127
+ }
@@ -37,6 +37,7 @@ export async function bootstrapStageflowHost(options) {
37
37
  });
38
38
  await manager.attachWaitingStages();
39
39
  await manager.reconcileOrphanedStages();
40
+ await manager.resumeStalledSchedules();
40
41
  const mcpStateless = resolveMcpStateless({
41
42
  mcpStateless: options.mcpStateless,
42
43
  });
@@ -3,12 +3,14 @@ import path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { getCredentialSourceSettings, setCredentialSource, } from "../agent/providerAuth.js";
5
5
  import { handleProviderRoutes, providerAuthErrorBody, } from "./providerRoutes.js";
6
+ import { handleProjectMcpRoutes } from "./projectMcpRoutes.js";
6
7
  import { createPipeline, parseCreatePipelineBody } from "../config/createPipeline.js";
7
8
  import { createStage, parseCreateStageBody } from "../config/createStage.js";
8
9
  import { browseCatalog } from "../config/browseCatalog.js";
9
10
  import { listExtensions } from "../config/listExtensions.js";
10
11
  import { listSkills } from "../config/listSkills.js";
11
- import { readRunArtifact } from "../mcp/readArtifact.js";
12
+ import { artifactMediaType, readRunArtifact, readRunArtifactBytes, } from "../mcp/readArtifact.js";
13
+ import { readStageVerificationHistory } from "../runstore/verificationHistory.js";
12
14
  import { resolveStageflowContext } from "../project/resolveStageflowContext.js";
13
15
  import { INVALID_SLOT_COUNT_MESSAGE, parseSlotCount, } from "../runtime/settingsFile.js";
14
16
  import { isTaskFile } from "../runtime/taskInput.js";
@@ -87,12 +89,16 @@ function isMutatingApi(method, pathname) {
87
89
  pathname === "/api/pipelines" ||
88
90
  /^\/api\/runs\/[^/]+\/rerun$/.test(pathname) ||
89
91
  /^\/api\/runs\/[^/]+\/stages\/[^/]+\/answer$/.test(pathname) ||
92
+ /^\/api\/runs\/[^/]+\/stages\/[^/]+\/feedback-decision$/.test(pathname) ||
90
93
  /^\/api\/runs\/[^/]+\/stages\/[^/]+\/retry$/.test(pathname) ||
94
+ /^\/api\/runs\/[^/]+\/stages\/[^/]+\/recovery$/.test(pathname) ||
95
+ /^\/api\/runs\/[^/]+\/stages\/[^/]+\/recovery\/stop$/.test(pathname) ||
91
96
  /^\/api\/runs\/[^/]+\/stages\/[^/]+\/abandon$/.test(pathname) ||
92
97
  /^\/api\/providers\/[^/]+\/login$/.test(pathname) ||
93
98
  /^\/api\/providers\/[^/]+\/login\/[^/]+\/answer$/.test(pathname) ||
94
99
  /^\/api\/providers\/[^/]+\/login\/[^/]+\/cancel$/.test(pathname) ||
95
- /^\/api\/providers\/[^/]+\/logout$/.test(pathname));
100
+ /^\/api\/providers\/[^/]+\/logout$/.test(pathname) ||
101
+ /^\/api\/project-mcp\/[^/]+\/probe$/.test(pathname));
96
102
  }
97
103
  function isCredentialMutatingApi(method, pathname) {
98
104
  if (method !== "POST")
@@ -166,6 +172,9 @@ function assertLoopbackHttpAccess(req, res) {
166
172
  }
167
173
  export function defaultUiDistDir() {
168
174
  const here = path.dirname(fileURLToPath(import.meta.url));
175
+ if (path.basename(path.dirname(here)) === "src") {
176
+ return path.resolve(here, "../../dist/ui");
177
+ }
169
178
  return path.resolve(here, "../ui");
170
179
  }
171
180
  export async function startUiServer(options) {
@@ -203,8 +212,19 @@ export async function startUiServer(options) {
203
212
  return true;
204
213
  }
205
214
  try {
206
- const content = await readRunArtifact(store, runId, artifactPath);
207
- textPlain(res, 200, content);
215
+ const mediaType = artifactMediaType(artifactPath);
216
+ if (mediaType !== undefined) {
217
+ const bytes = await readRunArtifactBytes(store, runId, artifactPath);
218
+ res.writeHead(200, {
219
+ "Content-Type": mediaType,
220
+ "Content-Length": bytes.length,
221
+ });
222
+ res.end(bytes);
223
+ }
224
+ else {
225
+ const content = await readRunArtifact(store, runId, artifactPath);
226
+ textPlain(res, 200, content);
227
+ }
208
228
  }
209
229
  catch (err) {
210
230
  const message = err instanceof Error ? err.message : String(err);
@@ -219,6 +239,20 @@ export async function startUiServer(options) {
219
239
  }
220
240
  return true;
221
241
  }
242
+ const verificationMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/verification$/);
243
+ if (method === "GET" && verificationMatch) {
244
+ const runId = decodeURIComponent(verificationMatch[1] ?? "");
245
+ const stageId = decodeURIComponent(verificationMatch[2] ?? "");
246
+ try {
247
+ json(res, 200, await readStageVerificationHistory(store, runId, stageId));
248
+ }
249
+ catch (err) {
250
+ const message = err instanceof Error ? err.message : String(err);
251
+ const notFound = /not found|no such/i.test(message);
252
+ json(res, notFound ? 404 : 500, { error: message });
253
+ }
254
+ return true;
255
+ }
222
256
  if (method === "GET" && pathname.startsWith("/api/runs/")) {
223
257
  const rest = pathname.slice("/api/runs/".length);
224
258
  if (rest && !rest.includes("/")) {
@@ -290,6 +324,55 @@ export async function startUiServer(options) {
290
324
  json(res, 202, { ok: true });
291
325
  return true;
292
326
  }
327
+ const feedbackDecisionMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/feedback-decision$/);
328
+ if (method === "POST" && feedbackDecisionMatch) {
329
+ const runId = decodeURIComponent(feedbackDecisionMatch[1] ?? "");
330
+ const stageId = decodeURIComponent(feedbackDecisionMatch[2] ?? "");
331
+ let body;
332
+ try {
333
+ body = await readJsonBody(req);
334
+ }
335
+ catch {
336
+ json(res, 400, { error: "Invalid JSON body" });
337
+ return true;
338
+ }
339
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
340
+ json(res, 400, { error: "Body must be a JSON object" });
341
+ return true;
342
+ }
343
+ const raw = body;
344
+ if (raw.decision !== "extend" &&
345
+ raw.decision !== "continue" &&
346
+ raw.decision !== "abandon") {
347
+ json(res, 400, {
348
+ error: "decision must be extend, continue, or abandon",
349
+ });
350
+ return true;
351
+ }
352
+ if (raw.loopId !== undefined && typeof raw.loopId !== "string") {
353
+ json(res, 400, { error: "loopId must be a string" });
354
+ return true;
355
+ }
356
+ if (raw.reason !== undefined && typeof raw.reason !== "string") {
357
+ json(res, 400, { error: "reason must be a string" });
358
+ return true;
359
+ }
360
+ const result = await manager.decideFeedbackLoop(runId, stageId, {
361
+ decision: raw.decision,
362
+ ...(raw.loopId !== undefined ? { loopId: raw.loopId } : {}),
363
+ ...(raw.reason !== undefined ? { reason: raw.reason } : {}),
364
+ });
365
+ if (!result.ok) {
366
+ json(res, result.status ?? 500, { error: result.reason });
367
+ return true;
368
+ }
369
+ json(res, 202, {
370
+ ok: true,
371
+ effect: result.effect,
372
+ loopId: result.loopId,
373
+ });
374
+ return true;
375
+ }
293
376
  const retryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/retry$/);
294
377
  if (method === "POST" && retryMatch) {
295
378
  await readJsonBody(req);
@@ -307,6 +390,50 @@ export async function startUiServer(options) {
307
390
  });
308
391
  return true;
309
392
  }
393
+ const recoverMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery$/);
394
+ if (method === "POST" && recoverMatch) {
395
+ let body;
396
+ try {
397
+ body = await readJsonBody(req);
398
+ }
399
+ catch {
400
+ json(res, 400, { error: "Invalid JSON body" });
401
+ return true;
402
+ }
403
+ const guidance = body !== null && typeof body === "object" && !Array.isArray(body)
404
+ ? body.guidance
405
+ : undefined;
406
+ if (guidance !== undefined && typeof guidance !== "string") {
407
+ json(res, 400, { error: "guidance must be a string" });
408
+ return true;
409
+ }
410
+ const runId = decodeURIComponent(recoverMatch[1] ?? "");
411
+ const stageId = decodeURIComponent(recoverMatch[2] ?? "");
412
+ const result = await manager.recoverManualStage(runId, stageId, guidance);
413
+ if (!result.ok) {
414
+ json(res, result.status ?? 500, mapRetryStageFailure(result));
415
+ return true;
416
+ }
417
+ json(res, 202, {
418
+ runId: result.runId,
419
+ stageId: result.stageId,
420
+ attemptIndex: result.attemptIndex,
421
+ });
422
+ return true;
423
+ }
424
+ const stopRecoveryMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/recovery\/stop$/);
425
+ if (method === "POST" && stopRecoveryMatch) {
426
+ await readJsonBody(req);
427
+ const runId = decodeURIComponent(stopRecoveryMatch[1] ?? "");
428
+ const stageId = decodeURIComponent(stopRecoveryMatch[2] ?? "");
429
+ const result = await manager.stopManualRecovery(runId, stageId);
430
+ if (!result.ok) {
431
+ json(res, result.status ?? 500, { error: result.reason });
432
+ return true;
433
+ }
434
+ json(res, 202, result);
435
+ return true;
436
+ }
310
437
  const abandonMatch = pathname.match(/^\/api\/runs\/([^/]+)\/stages\/([^/]+)\/abandon$/);
311
438
  if (method === "POST" && abandonMatch) {
312
439
  await readJsonBody(req);
@@ -413,6 +540,12 @@ export async function startUiServer(options) {
413
540
  })) {
414
541
  return true;
415
542
  }
543
+ if (await handleProjectMcpRoutes(req, res, {
544
+ projectRoot: boot.rootDir,
545
+ json,
546
+ })) {
547
+ return true;
548
+ }
416
549
  if (method === "GET" && pathname === "/api/skills") {
417
550
  json(res, 200, await listSkills({ cwd, agentDir }));
418
551
  return true;
@@ -9,6 +9,8 @@ export function inferRetryStageErrorCode(reason) {
9
9
  return "run_not_retryable";
10
10
  if (/stage is not failed/i.test(reason))
11
11
  return "stage_not_failed";
12
+ if (/manual recovery/i.test(reason))
13
+ return "manual_recovery_required";
12
14
  return undefined;
13
15
  }
14
16
  export function mapRetryStageFailure(result) {
@@ -0,0 +1,6 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export type ProjectMcpRoutesCtx = {
3
+ projectRoot: string;
4
+ json: (res: ServerResponse, status: number, body: unknown) => void;
5
+ };
6
+ export declare function handleProjectMcpRoutes(req: IncomingMessage, res: ServerResponse, ctx: ProjectMcpRoutesCtx): Promise<boolean>;
@@ -0,0 +1,38 @@
1
+ import { probeProjectMcpServer } from "../agent/piIsolatedMcpProbe.js";
2
+ import { listProjectMcpCatalog } from "../config/resolveStageMcpServers.js";
3
+ function requestAbortSignal(req) {
4
+ const controller = new AbortController();
5
+ const abort = () => {
6
+ if (!controller.signal.aborted)
7
+ controller.abort();
8
+ };
9
+ if (req.aborted || req.destroyed) {
10
+ abort();
11
+ return controller.signal;
12
+ }
13
+ req.once("aborted", abort);
14
+ req.once("close", abort);
15
+ return controller.signal;
16
+ }
17
+ export async function handleProjectMcpRoutes(req, res, ctx) {
18
+ const { projectRoot, json } = ctx;
19
+ const method = req.method ?? "GET";
20
+ const rawUrl = req.url ?? "/";
21
+ const pathname = new URL(rawUrl, "http://localhost").pathname;
22
+ if (method === "GET" && pathname === "/api/project-mcp") {
23
+ json(res, 200, await listProjectMcpCatalog(projectRoot));
24
+ return true;
25
+ }
26
+ const probeMatch = pathname.match(/^\/api\/project-mcp\/([^/]+)\/probe$/);
27
+ if (method === "POST" && probeMatch) {
28
+ const name = decodeURIComponent(probeMatch[1] ?? "");
29
+ const result = await probeProjectMcpServer({
30
+ projectRoot,
31
+ name,
32
+ signal: requestAbortSignal(req),
33
+ });
34
+ json(res, 200, result);
35
+ return true;
36
+ }
37
+ return false;
38
+ }
@@ -107,6 +107,96 @@ export declare const askOperatorParamsSchema: Type.TUnion<[Type.TObject<{
107
107
  artifacts: Type.TArray<Type.TString>;
108
108
  id: Type.TOptional<Type.TString>;
109
109
  }>]>;
110
+ /**
111
+ * Narrows the ask_operator params schema to only the declared gate kinds
112
+ * (stage `gate_kinds` allowlist). Omitted/empty falls back to the full
113
+ * union so back-compat stages keep accepting every kind.
114
+ */
115
+ export declare function askOperatorParamsSchemaFor(allowedKinds?: readonly AskOperatorKind[]): Type.TObject<{
116
+ kind: Type.TLiteral<"free_text">;
117
+ message: Type.TString;
118
+ id: Type.TOptional<Type.TString>;
119
+ }> | Type.TObject<{
120
+ kind: Type.TLiteral<"confirm">;
121
+ message: Type.TString;
122
+ id: Type.TOptional<Type.TString>;
123
+ }> | Type.TObject<{
124
+ kind: Type.TLiteral<"multi_question">;
125
+ questions: Type.TArray<Type.TObject<{
126
+ kind: Type.TUnion<[Type.TLiteral<"free_text">, Type.TLiteral<"confirm">]>;
127
+ message: Type.TString;
128
+ id: Type.TOptional<Type.TString>;
129
+ }>>;
130
+ id: Type.TOptional<Type.TString>;
131
+ }> | Type.TObject<{
132
+ kind: Type.TLiteral<"artifact_backed">;
133
+ message: Type.TString;
134
+ artifacts: Type.TArray<Type.TString>;
135
+ id: Type.TOptional<Type.TString>;
136
+ }> | Type.TUnion<[Type.TObject<{
137
+ kind: Type.TLiteral<"free_text">;
138
+ message: Type.TString;
139
+ id: Type.TOptional<Type.TString>;
140
+ }> | Type.TObject<{
141
+ kind: Type.TLiteral<"confirm">;
142
+ message: Type.TString;
143
+ id: Type.TOptional<Type.TString>;
144
+ }> | Type.TObject<{
145
+ kind: Type.TLiteral<"multi_question">;
146
+ questions: Type.TArray<Type.TObject<{
147
+ kind: Type.TUnion<[Type.TLiteral<"free_text">, Type.TLiteral<"confirm">]>;
148
+ message: Type.TString;
149
+ id: Type.TOptional<Type.TString>;
150
+ }>>;
151
+ id: Type.TOptional<Type.TString>;
152
+ }> | Type.TObject<{
153
+ kind: Type.TLiteral<"artifact_backed">;
154
+ message: Type.TString;
155
+ artifacts: Type.TArray<Type.TString>;
156
+ id: Type.TOptional<Type.TString>;
157
+ }>, Type.TObject<{
158
+ kind: Type.TLiteral<"free_text">;
159
+ message: Type.TString;
160
+ id: Type.TOptional<Type.TString>;
161
+ }> | Type.TObject<{
162
+ kind: Type.TLiteral<"confirm">;
163
+ message: Type.TString;
164
+ id: Type.TOptional<Type.TString>;
165
+ }> | Type.TObject<{
166
+ kind: Type.TLiteral<"multi_question">;
167
+ questions: Type.TArray<Type.TObject<{
168
+ kind: Type.TUnion<[Type.TLiteral<"free_text">, Type.TLiteral<"confirm">]>;
169
+ message: Type.TString;
170
+ id: Type.TOptional<Type.TString>;
171
+ }>>;
172
+ id: Type.TOptional<Type.TString>;
173
+ }> | Type.TObject<{
174
+ kind: Type.TLiteral<"artifact_backed">;
175
+ message: Type.TString;
176
+ artifacts: Type.TArray<Type.TString>;
177
+ id: Type.TOptional<Type.TString>;
178
+ }>, ...(Type.TObject<{
179
+ kind: Type.TLiteral<"free_text">;
180
+ message: Type.TString;
181
+ id: Type.TOptional<Type.TString>;
182
+ }> | Type.TObject<{
183
+ kind: Type.TLiteral<"confirm">;
184
+ message: Type.TString;
185
+ id: Type.TOptional<Type.TString>;
186
+ }> | Type.TObject<{
187
+ kind: Type.TLiteral<"multi_question">;
188
+ questions: Type.TArray<Type.TObject<{
189
+ kind: Type.TUnion<[Type.TLiteral<"free_text">, Type.TLiteral<"confirm">]>;
190
+ message: Type.TString;
191
+ id: Type.TOptional<Type.TString>;
192
+ }>>;
193
+ id: Type.TOptional<Type.TString>;
194
+ }> | Type.TObject<{
195
+ kind: Type.TLiteral<"artifact_backed">;
196
+ message: Type.TString;
197
+ artifacts: Type.TArray<Type.TString>;
198
+ id: Type.TOptional<Type.TString>;
199
+ }>)[]]>;
110
200
  export declare const askOperatorAnswerSchema: Type.TUnion<[Type.TObject<{
111
201
  promptId: Type.TString;
112
202
  kind: Type.TLiteral<"free_text">;
@@ -142,25 +232,28 @@ export declare const parseAnswer: typeof parseAskOperatorAnswer;
142
232
  export type AskOperatorWaitBridge = {
143
233
  requestWait: (prompt: AskOperatorPrompt) => Promise<unknown>;
144
234
  };
145
- export type AskOperatorToolOptions = AskOperatorWaitBridge;
235
+ export type AskOperatorToolOptions = AskOperatorWaitBridge & {
236
+ allowedKinds?: readonly AskOperatorKind[];
237
+ };
146
238
  export type AskOperatorToolDetails = {
147
239
  prompt: AskOperatorPrompt | null;
148
240
  answer: AskOperatorAnswer | null;
149
241
  error: string;
150
242
  };
243
+ export declare function summarizeAnswer(answer: AskOperatorAnswer): string;
151
244
  export declare function createAskOperatorTool(options: AskOperatorToolOptions): {
152
245
  name: string;
153
246
  label: string;
154
247
  description: string;
155
- parameters: Type.TUnion<[Type.TObject<{
248
+ parameters: Type.TObject<{
156
249
  kind: Type.TLiteral<"free_text">;
157
250
  message: Type.TString;
158
251
  id: Type.TOptional<Type.TString>;
159
- }>, Type.TObject<{
252
+ }> | Type.TObject<{
160
253
  kind: Type.TLiteral<"confirm">;
161
254
  message: Type.TString;
162
255
  id: Type.TOptional<Type.TString>;
163
- }>, Type.TObject<{
256
+ }> | Type.TObject<{
164
257
  kind: Type.TLiteral<"multi_question">;
165
258
  questions: Type.TArray<Type.TObject<{
166
259
  kind: Type.TUnion<[Type.TLiteral<"free_text">, Type.TLiteral<"confirm">]>;
@@ -168,12 +261,75 @@ export declare function createAskOperatorTool(options: AskOperatorToolOptions):
168
261
  id: Type.TOptional<Type.TString>;
169
262
  }>>;
170
263
  id: Type.TOptional<Type.TString>;
264
+ }> | Type.TObject<{
265
+ kind: Type.TLiteral<"artifact_backed">;
266
+ message: Type.TString;
267
+ artifacts: Type.TArray<Type.TString>;
268
+ id: Type.TOptional<Type.TString>;
269
+ }> | Type.TUnion<[Type.TObject<{
270
+ kind: Type.TLiteral<"free_text">;
271
+ message: Type.TString;
272
+ id: Type.TOptional<Type.TString>;
273
+ }> | Type.TObject<{
274
+ kind: Type.TLiteral<"confirm">;
275
+ message: Type.TString;
276
+ id: Type.TOptional<Type.TString>;
277
+ }> | Type.TObject<{
278
+ kind: Type.TLiteral<"multi_question">;
279
+ questions: Type.TArray<Type.TObject<{
280
+ kind: Type.TUnion<[Type.TLiteral<"free_text">, Type.TLiteral<"confirm">]>;
281
+ message: Type.TString;
282
+ id: Type.TOptional<Type.TString>;
283
+ }>>;
284
+ id: Type.TOptional<Type.TString>;
285
+ }> | Type.TObject<{
286
+ kind: Type.TLiteral<"artifact_backed">;
287
+ message: Type.TString;
288
+ artifacts: Type.TArray<Type.TString>;
289
+ id: Type.TOptional<Type.TString>;
171
290
  }>, Type.TObject<{
291
+ kind: Type.TLiteral<"free_text">;
292
+ message: Type.TString;
293
+ id: Type.TOptional<Type.TString>;
294
+ }> | Type.TObject<{
295
+ kind: Type.TLiteral<"confirm">;
296
+ message: Type.TString;
297
+ id: Type.TOptional<Type.TString>;
298
+ }> | Type.TObject<{
299
+ kind: Type.TLiteral<"multi_question">;
300
+ questions: Type.TArray<Type.TObject<{
301
+ kind: Type.TUnion<[Type.TLiteral<"free_text">, Type.TLiteral<"confirm">]>;
302
+ message: Type.TString;
303
+ id: Type.TOptional<Type.TString>;
304
+ }>>;
305
+ id: Type.TOptional<Type.TString>;
306
+ }> | Type.TObject<{
307
+ kind: Type.TLiteral<"artifact_backed">;
308
+ message: Type.TString;
309
+ artifacts: Type.TArray<Type.TString>;
310
+ id: Type.TOptional<Type.TString>;
311
+ }>, ...(Type.TObject<{
312
+ kind: Type.TLiteral<"free_text">;
313
+ message: Type.TString;
314
+ id: Type.TOptional<Type.TString>;
315
+ }> | Type.TObject<{
316
+ kind: Type.TLiteral<"confirm">;
317
+ message: Type.TString;
318
+ id: Type.TOptional<Type.TString>;
319
+ }> | Type.TObject<{
320
+ kind: Type.TLiteral<"multi_question">;
321
+ questions: Type.TArray<Type.TObject<{
322
+ kind: Type.TUnion<[Type.TLiteral<"free_text">, Type.TLiteral<"confirm">]>;
323
+ message: Type.TString;
324
+ id: Type.TOptional<Type.TString>;
325
+ }>>;
326
+ id: Type.TOptional<Type.TString>;
327
+ }> | Type.TObject<{
172
328
  kind: Type.TLiteral<"artifact_backed">;
173
329
  message: Type.TString;
174
330
  artifacts: Type.TArray<Type.TString>;
175
331
  id: Type.TOptional<Type.TString>;
176
- }>]>;
332
+ }>)[]]>;
177
333
  execute: (_toolCallId: string, params: unknown) => Promise<{
178
334
  content: {
179
335
  type: "text";