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.
- package/README.md +6 -1
- package/dist/agent/activity.d.ts +9 -0
- package/dist/agent/agentBackend.d.ts +32 -0
- package/dist/agent/agentBackend.js +38 -0
- package/dist/agent/claudeActivity.d.ts +15 -0
- package/dist/agent/claudeActivity.js +58 -0
- package/dist/agent/claudeAdapter.d.ts +5 -0
- package/dist/agent/claudeAdapter.js +331 -0
- package/dist/agent/claudeSession.d.ts +18 -0
- package/dist/agent/claudeSession.js +54 -0
- package/dist/agent/claudeTools.d.ts +89 -0
- package/dist/agent/claudeTools.js +135 -0
- package/dist/agent/fakeAgent.js +24 -4
- package/dist/agent/piAdapter.d.ts +12 -7
- package/dist/agent/piAdapter.js +194 -44
- package/dist/agent/piIsolatedMcp.d.ts +23 -0
- package/dist/agent/piIsolatedMcp.js +223 -0
- package/dist/agent/piIsolatedMcpProbe.d.ts +15 -0
- package/dist/agent/piIsolatedMcpProbe.js +292 -0
- package/dist/agent/port.d.ts +52 -1
- package/dist/agent/port.js +1 -1
- package/dist/agent/resolveAgentPort.d.ts +26 -0
- package/dist/agent/resolveAgentPort.js +39 -0
- package/dist/cli/runCommand.js +2 -2
- package/dist/cli/runsCommand.d.ts +1 -1
- package/dist/cli/runsCommand.js +184 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +11 -5
- package/dist/config/browseCatalog.js +1 -1
- package/dist/config/createPipeline.d.ts +2 -1
- package/dist/config/createPipeline.js +34 -9
- package/dist/config/createStage.js +14 -9
- package/dist/config/loadPipeline.js +11 -1
- package/dist/config/loadStage.d.ts +1 -0
- package/dist/config/loadStage.js +207 -2
- package/dist/config/loadStageflowManifest.js +14 -1
- package/dist/config/mergePipelineIncludes.d.ts +1 -0
- package/dist/config/mergePipelineIncludes.js +14 -1
- package/dist/config/normalizePipelineStageEntry.d.ts +7 -2
- package/dist/config/normalizePipelineStageEntry.js +56 -7
- package/dist/config/parseCompletionContract.d.ts +8 -0
- package/dist/config/parseCompletionContract.js +218 -0
- package/dist/config/parsePreEmitChecks.d.ts +3 -0
- package/dist/config/parsePreEmitChecks.js +65 -0
- package/dist/config/pipelineNeeds.d.ts +19 -0
- package/dist/config/pipelineNeeds.js +151 -0
- package/dist/config/pipelineStageKeys.js +17 -1
- package/dist/config/resolveForkEmitContext.d.ts +6 -2
- package/dist/config/resolveForkEmitContext.js +9 -2
- package/dist/config/resolvePipelineDag.d.ts +2 -1
- package/dist/config/resolvePipelineDag.js +182 -39
- package/dist/config/resolveStageMcpServers.d.ts +35 -0
- package/dist/config/resolveStageMcpServers.js +207 -0
- package/dist/config/validateCatalog.d.ts +1 -1
- package/dist/config/validateCatalog.js +51 -0
- package/dist/config/validateCompletionContract.d.ts +4 -0
- package/dist/config/validateCompletionContract.js +52 -0
- package/dist/envelope/check.d.ts +1 -1
- package/dist/envelope/check.js +86 -13
- package/dist/envelope/cloneForks.d.ts +1 -1
- package/dist/envelope/cloneForks.js +24 -9
- package/dist/envelope/feedbackLoop.d.ts +4 -0
- package/dist/envelope/feedbackLoop.js +31 -0
- package/dist/envelope/payloadSchema.d.ts +7 -1
- package/dist/envelope/payloadSchema.js +170 -13
- package/dist/envelope/preEmitChecks.d.ts +29 -0
- package/dist/envelope/preEmitChecks.js +57 -0
- package/dist/hitl/qaTrail.d.ts +25 -0
- package/dist/hitl/qaTrail.js +51 -0
- package/dist/mcp/catalogTools.js +4 -2
- package/dist/mcp/controlTools.js +72 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +25 -1
- package/dist/mcp/waitingGates.d.ts +2 -1
- package/dist/mcp/waitingGates.js +17 -1
- package/dist/package-meta.d.ts +1 -1
- package/dist/package-meta.js +1 -1
- package/dist/projection/projectRun.d.ts +4 -1
- package/dist/projection/projectRun.js +7 -0
- package/dist/prompt/feedbackLoopContext.d.ts +2 -0
- package/dist/prompt/feedbackLoopContext.js +17 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -2
- package/dist/prompt/priorEnvelope.js +4 -1
- package/dist/runstore/pipelineDagSnapshot.d.ts +1 -0
- package/dist/runstore/pipelineDagSnapshot.js +51 -11
- package/dist/runstore/port.d.ts +185 -3
- package/dist/runstore/port.js +42 -3
- package/dist/runstore/runProjection.d.ts +7 -3
- package/dist/runstore/runProjection.js +30 -15
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +25 -1
- package/dist/runstore/sqlite/SqliteRunStore.js +626 -8
- package/dist/runstore/sqlite/schema.d.ts +1 -1
- package/dist/runstore/sqlite/schema.js +99 -0
- package/dist/runstore/stageInstanceId.d.ts +1 -1
- package/dist/runstore/stageInstanceId.js +3 -2
- package/dist/runstore/trackProjection.js +59 -27
- package/dist/runstore/verificationHistory.d.ts +19 -0
- package/dist/runstore/verificationHistory.js +30 -0
- package/dist/runtime/cloneSchedule.d.ts +40 -4
- package/dist/runtime/cloneSchedule.js +212 -28
- package/dist/runtime/completionCheckRunner.d.ts +137 -0
- package/dist/runtime/completionCheckRunner.js +365 -0
- package/dist/runtime/completionCheckoutBaseline.d.ts +3 -0
- package/dist/runtime/completionCheckoutBaseline.js +44 -0
- package/dist/runtime/envelopeRouting.d.ts +10 -3
- package/dist/runtime/envelopeRouting.js +146 -5
- package/dist/runtime/feedbackLoopCoordinator.d.ts +74 -0
- package/dist/runtime/feedbackLoopCoordinator.js +446 -0
- package/dist/runtime/feedbackLoopDecision.d.ts +44 -0
- package/dist/runtime/feedbackLoopDecision.js +207 -0
- package/dist/runtime/feedbackLoopRoute.d.ts +5 -0
- package/dist/runtime/feedbackLoopRoute.js +13 -0
- package/dist/runtime/feedbackLoopSchedule.d.ts +38 -0
- package/dist/runtime/feedbackLoopSchedule.js +72 -0
- package/dist/runtime/forkGeneration.d.ts +72 -0
- package/dist/runtime/forkGeneration.js +295 -0
- package/dist/runtime/gitCheckoutCapability.d.ts +7 -0
- package/dist/runtime/gitCheckoutCapability.js +97 -0
- package/dist/runtime/manualRecoveryState.d.ts +40 -0
- package/dist/runtime/manualRecoveryState.js +73 -0
- package/dist/runtime/pipelineScheduler.d.ts +13 -1
- package/dist/runtime/pipelineScheduler.js +328 -45
- package/dist/runtime/replayLifecycle.d.ts +107 -0
- package/dist/runtime/replayLifecycle.js +168 -0
- package/dist/runtime/runManager.d.ts +54 -1
- package/dist/runtime/runManager.js +471 -6
- package/dist/runtime/runRetryCoordinator.d.ts +6 -1
- package/dist/runtime/runRetryCoordinator.js +43 -9
- package/dist/runtime/stageAttemptBootstrap.d.ts +3 -1
- package/dist/runtime/stageAttemptBootstrap.js +117 -3
- package/dist/runtime/stageProcessLauncher.d.ts +1 -1
- package/dist/runtime/stageRecovery.js +1 -1
- package/dist/runtime/stageRunner.d.ts +4 -1
- package/dist/runtime/stageRunner.js +29 -3
- package/dist/runtime/stageWorker.js +69 -2
- package/dist/runtime/stageWorkerProtocol.d.ts +1 -1
- package/dist/runtime/verifiedStageExecution.d.ts +31 -0
- package/dist/runtime/verifiedStageExecution.js +127 -0
- package/dist/server/bootstrap.js +1 -0
- package/dist/server/http.js +137 -4
- package/dist/server/operatorResults.js +2 -0
- package/dist/server/projectMcpRoutes.d.ts +6 -0
- package/dist/server/projectMcpRoutes.js +38 -0
- package/dist/tools/askOperator.d.ts +161 -5
- package/dist/tools/askOperator.js +58 -25
- package/dist/tools/emitStageEnvelope.d.ts +12 -12
- package/dist/tools/emitStageEnvelope.js +112 -24
- package/dist/types/completion.d.ts +43 -0
- package/dist/types/completion.js +1 -0
- package/dist/types/envelope.d.ts +18 -0
- package/dist/types/forkChoice.d.ts +9 -4
- package/dist/types/forkChoice.js +1 -1
- package/dist/types/pipeline.d.ts +41 -2
- package/dist/types/preEmitCheck.d.ts +18 -0
- package/dist/types/preEmitCheck.js +1 -0
- package/dist/types/stage.d.ts +14 -0
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/ui/assets/index-Yzh42vhf.js +118 -0
- package/dist/ui/assets/{index-C3N7MXAC.css → index-uVHiae8s.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +5 -1
- package/skills/stageflow-author/SKILL.md +20 -2
- package/skills/stageflow-author/references/catalog-mapping.md +88 -3
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/skills/stageflow-author/references/stage-prompt-template.md +63 -4
- package/skills/stageflow-session-capture/references/catalog-authoring.md +4 -1
- 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
|
+
}
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -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
|
});
|
package/dist/server/http.js
CHANGED
|
@@ -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
|
|
207
|
-
|
|
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.
|
|
248
|
+
parameters: Type.TObject<{
|
|
156
249
|
kind: Type.TLiteral<"free_text">;
|
|
157
250
|
message: Type.TString;
|
|
158
251
|
id: Type.TOptional<Type.TString>;
|
|
159
|
-
}
|
|
252
|
+
}> | Type.TObject<{
|
|
160
253
|
kind: Type.TLiteral<"confirm">;
|
|
161
254
|
message: Type.TString;
|
|
162
255
|
id: Type.TOptional<Type.TString>;
|
|
163
|
-
}
|
|
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";
|