stageflow 0.9.0 → 0.12.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 +1 -0
- 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 +301 -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 +6 -3
- package/dist/agent/piAdapter.js +129 -21
- package/dist/agent/port.d.ts +50 -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 +10 -1
- package/dist/config/loadStage.js +138 -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 +49 -6
- 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 +16 -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/validateCatalog.d.ts +1 -1
- 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/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 +79 -1
- 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 +29 -0
- package/dist/runtime/verifiedStageExecution.js +119 -0
- package/dist/server/bootstrap.js +1 -0
- package/dist/server/http.js +114 -0
- package/dist/server/operatorResults.js +2 -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 +39 -2
- package/dist/types/preEmitCheck.d.ts +18 -0
- package/dist/types/preEmitCheck.js +1 -0
- package/dist/types/stage.d.ts +13 -0
- package/dist/types/stageflowManifest.d.ts +2 -0
- package/dist/ui/assets/index-CXGz4nL7.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 +2 -1
- package/skills/stageflow-author/references/catalog-mapping.md +26 -2
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/dist/ui/assets/index-CSoGDA3A.js +0 -118
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-session runner for `pre_emit_checks` (`src/types/preEmitCheck.ts`).
|
|
3
|
+
*
|
|
4
|
+
* Distinct from `completionCheckRunner.ts`'s post-hoc `CompletionCheck`
|
|
5
|
+
* runner: this runs synchronously inside `emit_stage_envelope`'s own tool
|
|
6
|
+
* call, before a candidate envelope is even captured, and never touches
|
|
7
|
+
* disk — `artifact_declared` only checks whether a basename is *listed* in
|
|
8
|
+
* `envelope.artifacts`, and `gate` only reads the live, attempt-scoped QA
|
|
9
|
+
* trail via a caller-supplied `readQaTrail`. See the design doc's Q1/Q3 for
|
|
10
|
+
* why this stays separate from `CompletionCheck`'s `artifact`/`gate` runners.
|
|
11
|
+
*/
|
|
12
|
+
import { preEmitGateIssue } from "../hitl/qaTrail.js";
|
|
13
|
+
import { EnvelopeError } from "../types/envelope.js";
|
|
14
|
+
function artifactListsBasename(artifact, basename) {
|
|
15
|
+
return artifact === basename || artifact.endsWith(`/${basename}`);
|
|
16
|
+
}
|
|
17
|
+
export function declaredArtifactIssue(artifacts, check) {
|
|
18
|
+
const listed = artifacts.some((artifact) => artifactListsBasename(artifact, check.basename));
|
|
19
|
+
if (!listed) {
|
|
20
|
+
return `required artifact "${check.basename}" is missing from envelope.artifacts`;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Runs `options.checks` in declaration order and throws `EnvelopeError` on
|
|
26
|
+
* the first failing check, matching the single-message-per-rejected-emit
|
|
27
|
+
* contract the rest of `emitStageEnvelope.ts`'s `assert*` helpers use. A
|
|
28
|
+
* `gate` check with no `readQaTrail` fails closed (cannot be verified). No
|
|
29
|
+
* `checks` (or an empty list) is a no-op.
|
|
30
|
+
*/
|
|
31
|
+
export async function assertPreEmitChecks(envelope, options) {
|
|
32
|
+
const checks = options?.checks;
|
|
33
|
+
if (checks === undefined || checks.length === 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
let exchanges;
|
|
37
|
+
for (const check of checks) {
|
|
38
|
+
if (check.type === "artifact_declared") {
|
|
39
|
+
const issue = declaredArtifactIssue(envelope.artifacts, check);
|
|
40
|
+
if (issue !== undefined) {
|
|
41
|
+
throw new EnvelopeError(issue);
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// check.type === "gate"
|
|
46
|
+
if (options?.readQaTrail === undefined) {
|
|
47
|
+
throw new EnvelopeError("declared gate kinds cannot be verified without a QA trail reader");
|
|
48
|
+
}
|
|
49
|
+
if (exchanges === undefined) {
|
|
50
|
+
exchanges = await options.readQaTrail();
|
|
51
|
+
}
|
|
52
|
+
const issue = preEmitGateIssue(exchanges, check);
|
|
53
|
+
if (issue !== undefined) {
|
|
54
|
+
throw new EnvelopeError(issue);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/hitl/qaTrail.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { OperatorAnswerActivityEvent, OperatorPromptActivityEvent, StageActivityEvent, StageLogLine } from "../agent/activity.js";
|
|
8
8
|
import type { RunStore, StageLogEvent } from "../runstore/port.js";
|
|
9
9
|
import type { AskOperatorAnswer, AskOperatorPrompt } from "../tools/askOperator.js";
|
|
10
|
+
import type { PreEmitCheck } from "../types/preEmitCheck.js";
|
|
10
11
|
export type { OperatorAnswerActivityEvent, OperatorPromptActivityEvent, } from "../agent/activity.js";
|
|
11
12
|
export type { AskOperatorAnswer, AskOperatorPrompt } from "../tools/askOperator.js";
|
|
12
13
|
export type QaExchange = {
|
|
@@ -28,4 +29,28 @@ export declare function appendOperatorAnswer(store: RunStore, runId: string, sta
|
|
|
28
29
|
attempt?: number;
|
|
29
30
|
}): Promise<void>;
|
|
30
31
|
export declare function listQaExchanges(events: readonly StageLogEvent[]): QaExchange[];
|
|
32
|
+
/**
|
|
33
|
+
* Builds a reader over the live, attempt-scoped QA trail: `listStageEvents`
|
|
34
|
+
* for this run/stage/attempt, collapsed via `listQaExchanges`. Read at
|
|
35
|
+
* `emit_stage_envelope` execute time (not at stage-open/factory time) so it
|
|
36
|
+
* reflects exchanges recorded during this attempt, including ones recorded
|
|
37
|
+
* after the reader was constructed.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createAttemptQaTrailReader(store: Pick<RunStore, "listStageEvents">, runId: string, stageId: string, attempt: number): () => Promise<QaExchange[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Pre-emit `gate`-type check predicate: is the *most recent* exchange of the
|
|
42
|
+
* check's declared kind, within this attempt's ordered trail, satisfied?
|
|
43
|
+
* `confirm`/`artifact_backed` require the last such exchange's decision to be
|
|
44
|
+
* "accept"; `free_text`/`multi_question` are satisfied by any completed
|
|
45
|
+
* (answered) exchange of that kind. Returns an issue string on failure,
|
|
46
|
+
* `undefined` on success. Deliberately independent of
|
|
47
|
+
* `completionCheckRunner.ts`'s `runGateCheck` (see design doc Q2): that
|
|
48
|
+
* function asks the looser, retrospective "was this kind ever accepted over
|
|
49
|
+
* the run so far" question via order-erased `GateDecision[]`; this asks the
|
|
50
|
+
* stricter, attempt-scoped "is the last answer of this kind an accept"
|
|
51
|
+
* question, which requires the raw, ordered `QaExchange[]`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function preEmitGateIssue(exchanges: readonly QaExchange[], check: Extract<PreEmitCheck, {
|
|
54
|
+
type: "gate";
|
|
55
|
+
}>): string | undefined;
|
|
31
56
|
export declare function derivePendingPrompt(events: readonly StageLogEvent[]): AskOperatorPrompt | null;
|
package/dist/hitl/qaTrail.js
CHANGED
|
@@ -59,6 +59,57 @@ export function listQaExchanges(events) {
|
|
|
59
59
|
assertAtMostOnePending(exchanges);
|
|
60
60
|
return exchanges;
|
|
61
61
|
}
|
|
62
|
+
const DECISION_GATE_KINDS = new Set([
|
|
63
|
+
"confirm",
|
|
64
|
+
"artifact_backed",
|
|
65
|
+
]);
|
|
66
|
+
/**
|
|
67
|
+
* Builds a reader over the live, attempt-scoped QA trail: `listStageEvents`
|
|
68
|
+
* for this run/stage/attempt, collapsed via `listQaExchanges`. Read at
|
|
69
|
+
* `emit_stage_envelope` execute time (not at stage-open/factory time) so it
|
|
70
|
+
* reflects exchanges recorded during this attempt, including ones recorded
|
|
71
|
+
* after the reader was constructed.
|
|
72
|
+
*/
|
|
73
|
+
export function createAttemptQaTrailReader(store, runId, stageId, attempt) {
|
|
74
|
+
return async () => {
|
|
75
|
+
const events = await store.listStageEvents(runId, stageId, attempt);
|
|
76
|
+
return listQaExchanges(events);
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function lastDecisionIsAccept(answer) {
|
|
80
|
+
return ((answer.kind === "confirm" || answer.kind === "artifact_backed") &&
|
|
81
|
+
answer.decision === "accept");
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Pre-emit `gate`-type check predicate: is the *most recent* exchange of the
|
|
85
|
+
* check's declared kind, within this attempt's ordered trail, satisfied?
|
|
86
|
+
* `confirm`/`artifact_backed` require the last such exchange's decision to be
|
|
87
|
+
* "accept"; `free_text`/`multi_question` are satisfied by any completed
|
|
88
|
+
* (answered) exchange of that kind. Returns an issue string on failure,
|
|
89
|
+
* `undefined` on success. Deliberately independent of
|
|
90
|
+
* `completionCheckRunner.ts`'s `runGateCheck` (see design doc Q2): that
|
|
91
|
+
* function asks the looser, retrospective "was this kind ever accepted over
|
|
92
|
+
* the run so far" question via order-erased `GateDecision[]`; this asks the
|
|
93
|
+
* stricter, attempt-scoped "is the last answer of this kind an accept"
|
|
94
|
+
* question, which requires the raw, ordered `QaExchange[]`.
|
|
95
|
+
*/
|
|
96
|
+
export function preEmitGateIssue(exchanges, check) {
|
|
97
|
+
const ofKind = exchanges.filter((ex) => ex.prompt.kind === check.kind);
|
|
98
|
+
if (DECISION_GATE_KINDS.has(check.kind)) {
|
|
99
|
+
const last = ofKind.at(-1);
|
|
100
|
+
if (last === undefined || last.answer === null) {
|
|
101
|
+
return `declared gate kind "${check.kind}" has not completed this attempt`;
|
|
102
|
+
}
|
|
103
|
+
if (!lastDecisionIsAccept(last.answer)) {
|
|
104
|
+
return `declared gate kind "${check.kind}" last decision must be "accept"`;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
if (!ofKind.some((ex) => ex.answer !== null)) {
|
|
109
|
+
return `declared gate kind "${check.kind}" has not completed this attempt`;
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
62
113
|
export function derivePendingPrompt(events) {
|
|
63
114
|
const pending = listQaExchanges(events).find((ex) => ex.answer === null);
|
|
64
115
|
return pending?.prompt ?? null;
|
package/dist/mcp/catalogTools.js
CHANGED
|
@@ -83,7 +83,7 @@ export function registerCatalogTools(server, deps) {
|
|
|
83
83
|
return textResult({ runId: result.runId });
|
|
84
84
|
});
|
|
85
85
|
server.registerTool("get_run", {
|
|
86
|
-
description: "Poll a run: status, stage statuses, waiting_* / pending_prompt HITL fields, and envelope summary/payload/artifact paths (no events). Use list_stage_events
|
|
86
|
+
description: "Poll a run: status, stage statuses, waiting_* / pending_prompt HITL fields, and envelope summary/payload/artifact paths (no events). Use list_stage_events, get_envelope, or get_stage_verification for stage detail.",
|
|
87
87
|
inputSchema: z.object({
|
|
88
88
|
runId: z.string(),
|
|
89
89
|
}),
|
|
@@ -165,7 +165,9 @@ export function registerCatalogTools(server, deps) {
|
|
|
165
165
|
const gateById = new Map(loaded.stages.map((s) => [s.id, s.gate_kinds]));
|
|
166
166
|
const stages = loaded.dag.nodes.map((node) => ({
|
|
167
167
|
id: node.id,
|
|
168
|
-
needs: node.
|
|
168
|
+
needs: node.needsEdges.length > 1
|
|
169
|
+
? node.needsEdges.map((edge) => ({ id: edge.id, on: [...edge.on] }))
|
|
170
|
+
: node.needs,
|
|
169
171
|
...(node.fork !== undefined ? { fork: node.fork } : {}),
|
|
170
172
|
...(node.clonable !== undefined ? { clonable: node.clonable } : {}),
|
|
171
173
|
...(node.clone_cap !== undefined ? { clone_cap: node.clone_cap } : {}),
|
package/dist/mcp/controlTools.js
CHANGED
|
@@ -3,6 +3,7 @@ import { mapRetryStageFailure, mapStartFailure, } from "../server/operatorResult
|
|
|
3
3
|
import { parseAskOperatorAnswer } from "../tools/askOperator.js";
|
|
4
4
|
import { textResult } from "./toolResults.js";
|
|
5
5
|
import { projectWaitingGates } from "./waitingGates.js";
|
|
6
|
+
import { readStageVerificationHistory } from "../runstore/verificationHistory.js";
|
|
6
7
|
export function registerControlTools(server, deps) {
|
|
7
8
|
const { manager, store } = deps;
|
|
8
9
|
server.registerTool("list_waiting", {
|
|
@@ -36,6 +37,30 @@ export function registerControlTools(server, deps) {
|
|
|
36
37
|
}
|
|
37
38
|
return textResult({ ok: true });
|
|
38
39
|
});
|
|
40
|
+
server.registerTool("decide_feedback_loop", {
|
|
41
|
+
description: "Resolve a feedback-loop wait_for_human decision (extend, continue, or abandon). Same semantics as POST /api/runs/:id/stages/:stageId/feedback-decision.",
|
|
42
|
+
inputSchema: z.object({
|
|
43
|
+
runId: z.string(),
|
|
44
|
+
stageId: z.string(),
|
|
45
|
+
decision: z.enum(["extend", "continue", "abandon"]),
|
|
46
|
+
loopId: z.string().optional(),
|
|
47
|
+
reason: z.string().optional(),
|
|
48
|
+
}),
|
|
49
|
+
}, async ({ runId, stageId, decision, loopId, reason }) => {
|
|
50
|
+
const result = await manager.decideFeedbackLoop(runId, stageId, {
|
|
51
|
+
decision,
|
|
52
|
+
...(loopId !== undefined ? { loopId } : {}),
|
|
53
|
+
...(reason !== undefined ? { reason } : {}),
|
|
54
|
+
});
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
return textResult({ error: result.reason, status: result.status }, true);
|
|
57
|
+
}
|
|
58
|
+
return textResult({
|
|
59
|
+
ok: true,
|
|
60
|
+
effect: result.effect,
|
|
61
|
+
loopId: result.loopId,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
39
64
|
server.registerTool("list_stage_events", {
|
|
40
65
|
description: "List persisted stage log events for a run stage (optional attempt filter). Prefer over bloating get_run.",
|
|
41
66
|
inputSchema: z.object({
|
|
@@ -66,6 +91,53 @@ export function registerControlTools(server, deps) {
|
|
|
66
91
|
return textResult({ error: message, status: notFound ? 404 : 500 }, true);
|
|
67
92
|
}
|
|
68
93
|
});
|
|
94
|
+
server.registerTool("get_stage_verification", {
|
|
95
|
+
description: "Read every execution attempt for a stage, including durable completion-check results and their evidence. Use this to understand an automatic repair or a failed verification.",
|
|
96
|
+
inputSchema: z.object({
|
|
97
|
+
runId: z.string(),
|
|
98
|
+
stageId: z.string(),
|
|
99
|
+
}),
|
|
100
|
+
}, async ({ runId, stageId }) => {
|
|
101
|
+
try {
|
|
102
|
+
return textResult(await readStageVerificationHistory(store, runId, stageId));
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
106
|
+
const notFound = /not found|no such/i.test(message);
|
|
107
|
+
return textResult({ error: message, status: notFound ? 404 : 500 }, true);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
server.registerTool("recover_manual_stage", {
|
|
111
|
+
description: "Explicitly approve one fresh attempt for a manual-recovery stage after a completion verification failure. Optional guidance is recorded and supplied to the agent with the failed-check evidence.",
|
|
112
|
+
inputSchema: z.object({
|
|
113
|
+
runId: z.string(),
|
|
114
|
+
stageId: z.string(),
|
|
115
|
+
guidance: z.string().max(4_000).optional(),
|
|
116
|
+
}),
|
|
117
|
+
}, async ({ runId, stageId, guidance }) => {
|
|
118
|
+
const result = await manager.recoverManualStage(runId, stageId, guidance);
|
|
119
|
+
if (!result.ok) {
|
|
120
|
+
return textResult({ ...mapRetryStageFailure(result), status: result.status }, true);
|
|
121
|
+
}
|
|
122
|
+
return textResult({
|
|
123
|
+
runId: result.runId,
|
|
124
|
+
stageId: result.stageId,
|
|
125
|
+
attemptIndex: result.attemptIndex,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
server.registerTool("stop_manual_recovery", {
|
|
129
|
+
description: "Record the operator's decision to leave a manual-recovery stage failed. This is terminal for that stage; start a fresh run to try again later.",
|
|
130
|
+
inputSchema: z.object({
|
|
131
|
+
runId: z.string(),
|
|
132
|
+
stageId: z.string(),
|
|
133
|
+
}),
|
|
134
|
+
}, async ({ runId, stageId }) => {
|
|
135
|
+
const result = await manager.stopManualRecovery(runId, stageId);
|
|
136
|
+
if (!result.ok) {
|
|
137
|
+
return textResult({ error: result.reason, status: result.status }, true);
|
|
138
|
+
}
|
|
139
|
+
return textResult(result);
|
|
140
|
+
});
|
|
69
141
|
server.registerTool("get_envelope", {
|
|
70
142
|
description: "Read the full StageEnvelope for a run stage (latest attempt). Returns 404 when absent.",
|
|
71
143
|
inputSchema: z.object({
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { RunStore, RunSummary, StageSnapshot } from "../runstore/port.js";
|
|
1
|
+
import type { RunDetail, RunStore, RunSummary, StageSnapshot } from "../runstore/port.js";
|
|
2
2
|
type WaitingSummarySource = Pick<RunSummary, "run_id" | "waiting_stage_id" | "waiting_stage_ids" | "waiting_kind" | "waiting_summary" | "waiting_prompt_id" | "waiting_artifacts" | "waiting_questions">;
|
|
3
3
|
export declare function projectWaitingGate(opts: {
|
|
4
4
|
runId: string;
|
|
5
5
|
stageId: string;
|
|
6
6
|
stage: StageSnapshot;
|
|
7
7
|
summary: WaitingSummarySource;
|
|
8
|
+
activeFeedbackLoop?: RunDetail["active_feedback_loop"];
|
|
8
9
|
}): Record<string, unknown>;
|
|
9
10
|
export declare function projectWaitingGates(store: RunStore, opts?: {
|
|
10
11
|
runId?: string;
|
package/dist/mcp/waitingGates.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function projectWaitingGate(opts) {
|
|
2
|
-
const { runId, stageId, stage, summary } = opts;
|
|
2
|
+
const { runId, stageId, stage, summary, activeFeedbackLoop } = opts;
|
|
3
3
|
const prompt = stage.pending_prompt;
|
|
4
4
|
const item = {
|
|
5
5
|
runId,
|
|
@@ -36,6 +36,21 @@ export function projectWaitingGate(opts) {
|
|
|
36
36
|
item.waiting_questions = summary.waiting_questions;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
if ((item.waiting_kind === "feedback_loop_decision" ||
|
|
40
|
+
activeFeedbackLoop?.state === "waiting_for_human") &&
|
|
41
|
+
activeFeedbackLoop !== undefined) {
|
|
42
|
+
item.feedback_loop_id = activeFeedbackLoop.loop_id;
|
|
43
|
+
if (activeFeedbackLoop.deferred_send_back?.target !== undefined) {
|
|
44
|
+
item.deferred_target = activeFeedbackLoop.deferred_send_back.target;
|
|
45
|
+
}
|
|
46
|
+
if (item.waiting_kind === undefined) {
|
|
47
|
+
item.waiting_kind = "feedback_loop_decision";
|
|
48
|
+
}
|
|
49
|
+
if (item.waiting_summary === undefined) {
|
|
50
|
+
item.waiting_summary =
|
|
51
|
+
"Feedback loop limit reached — extend, continue, or abandon";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
39
54
|
return item;
|
|
40
55
|
}
|
|
41
56
|
function projectGatesForRun(detail, summary) {
|
|
@@ -51,6 +66,7 @@ function projectGatesForRun(detail, summary) {
|
|
|
51
66
|
stageId,
|
|
52
67
|
stage,
|
|
53
68
|
summary,
|
|
69
|
+
activeFeedbackLoop: detail.active_feedback_loop,
|
|
54
70
|
}));
|
|
55
71
|
}
|
|
56
72
|
return items;
|
package/dist/package-meta.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare const PACKAGE_NAME = "stageflow";
|
|
2
|
-
export declare const PACKAGE_VERSION = "0.
|
|
2
|
+
export declare const PACKAGE_VERSION = "0.12.0";
|
package/dist/package-meta.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const PACKAGE_NAME = "stageflow";
|
|
2
|
-
export const PACKAGE_VERSION = "0.
|
|
2
|
+
export const PACKAGE_VERSION = "0.12.0";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RunDetail, StageSnapshot } from "../runstore/port.js";
|
|
1
|
+
import type { FeedbackLoopHistory, FeedbackLoopRecord, RunDetail, StageSnapshot } from "../runstore/port.js";
|
|
2
2
|
import type { PipelineTrackProjection, RunSummary } from "../runstore/port.js";
|
|
3
3
|
import type { AskOperatorPrompt } from "../tools/askOperator.js";
|
|
4
4
|
import type { StageEnvelope } from "../types/envelope.js";
|
|
@@ -13,6 +13,7 @@ export type StageProjection = {
|
|
|
13
13
|
payload?: Record<string, unknown>;
|
|
14
14
|
fork_choice?: string[];
|
|
15
15
|
clone_forks?: CloneForkItem[];
|
|
16
|
+
feedback_loop?: StageEnvelope["feedback_loop"];
|
|
16
17
|
} | null;
|
|
17
18
|
artifacts: string[];
|
|
18
19
|
last_at?: string;
|
|
@@ -39,6 +40,8 @@ export type RunProjection = {
|
|
|
39
40
|
waiting_questions?: RunSummary["waiting_questions"];
|
|
40
41
|
failed_stage_id?: RunSummary["failed_stage_id"];
|
|
41
42
|
failed_reason?: RunSummary["failed_reason"];
|
|
43
|
+
active_feedback_loop?: FeedbackLoopRecord;
|
|
44
|
+
feedback_loops: FeedbackLoopHistory[];
|
|
42
45
|
};
|
|
43
46
|
export declare function projectRun(detail: RunDetail): RunProjection;
|
|
44
47
|
export { projectRun as projectRunForMcp };
|
|
@@ -41,6 +41,10 @@ export function projectRun(detail) {
|
|
|
41
41
|
...(detail.failed_reason !== undefined
|
|
42
42
|
? { failed_reason: detail.failed_reason }
|
|
43
43
|
: {}),
|
|
44
|
+
...(detail.active_feedback_loop !== undefined
|
|
45
|
+
? { active_feedback_loop: detail.active_feedback_loop }
|
|
46
|
+
: {}),
|
|
47
|
+
feedback_loops: detail.feedback_loops ?? [],
|
|
44
48
|
stages: detail.stages.map((stage) => ({
|
|
45
49
|
stage_id: stage.stage_id,
|
|
46
50
|
status: stage.status,
|
|
@@ -58,6 +62,9 @@ export function projectRun(detail) {
|
|
|
58
62
|
...(stage.envelope.clone_forks !== undefined
|
|
59
63
|
? { clone_forks: stage.envelope.clone_forks }
|
|
60
64
|
: {}),
|
|
65
|
+
...(stage.envelope.feedback_loop !== undefined
|
|
66
|
+
? { feedback_loop: stage.envelope.feedback_loop }
|
|
67
|
+
: {}),
|
|
61
68
|
}
|
|
62
69
|
: null,
|
|
63
70
|
artifacts: stage.artifacts,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function resolveFeedbackSessionMode(ctx, sessionMode) {
|
|
2
|
+
if (sessionMode === "new_session" || sessionMode === "feedback_resume") {
|
|
3
|
+
return sessionMode;
|
|
4
|
+
}
|
|
5
|
+
return ctx.replay_session === "new_session" ? "new_session" : "feedback_resume";
|
|
6
|
+
}
|
|
7
|
+
export function formatFeedbackLoopContext(ctx, sessionMode) {
|
|
8
|
+
const mode = resolveFeedbackSessionMode(ctx, sessionMode);
|
|
9
|
+
const continuity = mode === "new_session"
|
|
10
|
+
? "starting a fresh agent session"
|
|
11
|
+
: "continuing the prior agent session";
|
|
12
|
+
return [
|
|
13
|
+
"Feedback Loop Context:",
|
|
14
|
+
`Session mode: ${mode} (${continuity})`,
|
|
15
|
+
JSON.stringify(ctx, null, 2),
|
|
16
|
+
].join("\n");
|
|
17
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { StageEnvelope } from "../types/envelope.js";
|
|
2
|
-
export declare function formatPriorEnvelope(priorEnvelope: StageEnvelope | null, priorEnvelopes?: StageEnvelope[]): string;
|
|
1
|
+
import type { StageEnvelope, TerminalEnvelope } from "../types/envelope.js";
|
|
2
|
+
export declare function formatPriorEnvelope(priorEnvelope: StageEnvelope | null, priorEnvelopes?: StageEnvelope[], priorEnvelopesByStage?: Record<string, TerminalEnvelope | TerminalEnvelope[]>): string;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export function formatPriorEnvelope(priorEnvelope, priorEnvelopes) {
|
|
1
|
+
export function formatPriorEnvelope(priorEnvelope, priorEnvelopes, priorEnvelopesByStage) {
|
|
2
|
+
if (priorEnvelopesByStage !== undefined) {
|
|
3
|
+
return `Prior envelopes by stage (declaration order):\n${JSON.stringify(priorEnvelopesByStage, null, 2)}`;
|
|
4
|
+
}
|
|
2
5
|
if (priorEnvelopes !== undefined && priorEnvelopes.length > 0) {
|
|
3
6
|
return `Prior envelopes (${priorEnvelopes.length} clones, clone-list order):\n${JSON.stringify(priorEnvelopes, null, 2)}`;
|
|
4
7
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import { hydrateResolvedNeeds } from "../config/pipelineNeeds.js";
|
|
1
2
|
import { mintCloneInstanceIds } from "./stageInstanceId.js";
|
|
2
3
|
export function buildPipelineDagSnapshotFromLoaded(loaded) {
|
|
3
4
|
const gate_kinds = {};
|
|
5
|
+
const clone_input_schema = {};
|
|
4
6
|
for (const stage of loaded.stages) {
|
|
5
|
-
if (stage.gate_kinds
|
|
7
|
+
if (stage.gate_kinds !== undefined) {
|
|
6
8
|
gate_kinds[stage.id] = stage.gate_kinds;
|
|
7
9
|
}
|
|
10
|
+
if (stage.clone_input_schema !== undefined) {
|
|
11
|
+
clone_input_schema[stage.id] = stage.clone_input_schema;
|
|
12
|
+
}
|
|
8
13
|
}
|
|
9
14
|
return {
|
|
10
15
|
stage_ids: [...loaded.pipeline.stages],
|
|
@@ -15,12 +20,16 @@ export function buildPipelineDagSnapshotFromLoaded(loaded) {
|
|
|
15
20
|
roots: loaded.dag.roots,
|
|
16
21
|
childrenOf: loaded.dag.childrenOf,
|
|
17
22
|
...(Object.keys(gate_kinds).length > 0 ? { gate_kinds } : {}),
|
|
23
|
+
...(Object.keys(clone_input_schema).length > 0
|
|
24
|
+
? { clone_input_schema }
|
|
25
|
+
: {}),
|
|
18
26
|
};
|
|
19
27
|
}
|
|
20
28
|
export function linearCompatDagSnapshot(stageIds) {
|
|
21
29
|
const nodes = stageIds.map((id, index) => ({
|
|
22
30
|
id,
|
|
23
31
|
needs: index === 0 ? null : stageIds[index - 1],
|
|
32
|
+
needsEdges: index === 0 ? [] : [{ id: stageIds[index - 1], on: ["succeeded"] }],
|
|
24
33
|
ancestors: index === 0 ? [] : stageIds.slice(0, index),
|
|
25
34
|
stageIndex: index,
|
|
26
35
|
definition_id: id,
|
|
@@ -44,7 +53,14 @@ export function parsePipelineDagSnapshot(raw) {
|
|
|
44
53
|
const value = raw;
|
|
45
54
|
if (!Array.isArray(value.stage_ids) || !Array.isArray(value.nodes))
|
|
46
55
|
return null;
|
|
47
|
-
|
|
56
|
+
const nodes = value.nodes.map((node) => {
|
|
57
|
+
if (!node || typeof node !== "object")
|
|
58
|
+
return node;
|
|
59
|
+
const record = node;
|
|
60
|
+
const hydrated = hydrateResolvedNeeds(record);
|
|
61
|
+
return { ...record, ...hydrated };
|
|
62
|
+
});
|
|
63
|
+
return { ...raw, nodes };
|
|
48
64
|
}
|
|
49
65
|
export function instancesOfDefinition(snapshot, catalogId) {
|
|
50
66
|
const nodeById = new Map(snapshot.nodes.map((n) => [n.id, n]));
|
|
@@ -54,15 +70,24 @@ export function instancesOfDefinition(snapshot, catalogId) {
|
|
|
54
70
|
});
|
|
55
71
|
}
|
|
56
72
|
export function appendCloneInstances(snapshot, input) {
|
|
57
|
-
const { catalogId, predecessorId, count } = input;
|
|
58
|
-
|
|
73
|
+
const { catalogId, predecessorId, count, startAt } = input;
|
|
74
|
+
// A catalog id that's already been fanned out once is no longer present as
|
|
75
|
+
// its own stage_ids entry — it was replaced by its instances. Minting
|
|
76
|
+
// *more* instances for it (a retried fanout requesting a larger clone
|
|
77
|
+
// count) is still valid as long as those instances exist.
|
|
78
|
+
const existingInstanceIdxs = snapshot.stage_ids
|
|
79
|
+
.map((id, i) => [id, i])
|
|
80
|
+
.filter(([id]) => id !== catalogId && id.startsWith(`${catalogId}~`))
|
|
81
|
+
.map(([, i]) => i);
|
|
82
|
+
const catalogIdx = snapshot.stage_ids.indexOf(catalogId);
|
|
83
|
+
if (catalogIdx === -1 && existingInstanceIdxs.length === 0) {
|
|
59
84
|
throw new Error(`catalog stage "${catalogId}" is not in the run DAG snapshot`);
|
|
60
85
|
}
|
|
61
86
|
const predNode = snapshot.nodes.find((n) => n.id === predecessorId);
|
|
62
87
|
if (!predNode) {
|
|
63
88
|
throw new Error(`predecessor "${predecessorId}" is not in the run DAG snapshot`);
|
|
64
89
|
}
|
|
65
|
-
const instanceIds = mintCloneInstanceIds(catalogId, count);
|
|
90
|
+
const instanceIds = mintCloneInstanceIds(catalogId, count, startAt);
|
|
66
91
|
for (const id of instanceIds) {
|
|
67
92
|
if (snapshot.stage_ids.includes(id) ||
|
|
68
93
|
snapshot.nodes.some((n) => n.id === id)) {
|
|
@@ -70,22 +95,37 @@ export function appendCloneInstances(snapshot, input) {
|
|
|
70
95
|
}
|
|
71
96
|
}
|
|
72
97
|
const catalogNode = snapshot.nodes.find((n) => n.id === catalogId);
|
|
73
|
-
|
|
98
|
+
const templateNode = catalogNode ?? snapshot.nodes.find((n) => n.definition_id === catalogId);
|
|
99
|
+
if (!templateNode) {
|
|
74
100
|
throw new Error(`catalog stage "${catalogId}" is not in the run DAG snapshot`);
|
|
75
101
|
}
|
|
76
|
-
const
|
|
102
|
+
const insertAt = catalogIdx !== -1 ? catalogIdx : Math.max(...existingInstanceIdxs) + 1;
|
|
103
|
+
const removeAt = catalogIdx !== -1 ? catalogIdx + 1 : insertAt;
|
|
77
104
|
const stage_ids = [
|
|
78
|
-
...snapshot.stage_ids.slice(0,
|
|
105
|
+
...snapshot.stage_ids.slice(0, insertAt),
|
|
79
106
|
...instanceIds,
|
|
80
|
-
...snapshot.stage_ids.slice(
|
|
107
|
+
...snapshot.stage_ids.slice(removeAt),
|
|
81
108
|
];
|
|
82
109
|
const instanceNodes = instanceIds.map((id) => ({
|
|
83
110
|
id,
|
|
84
111
|
definition_id: catalogId,
|
|
85
112
|
needs: predecessorId,
|
|
113
|
+
needsEdges: [{ id: predecessorId, on: ["succeeded"] }],
|
|
86
114
|
ancestors: [...predNode.ancestors, predecessorId],
|
|
87
115
|
stageIndex: 0,
|
|
88
|
-
...(
|
|
116
|
+
...(templateNode.fork !== undefined ? { fork: templateNode.fork } : {}),
|
|
117
|
+
...(templateNode.completion !== undefined
|
|
118
|
+
? { completion: templateNode.completion }
|
|
119
|
+
: {}),
|
|
120
|
+
...(templateNode.recovery !== undefined
|
|
121
|
+
? { recovery: templateNode.recovery }
|
|
122
|
+
: {}),
|
|
123
|
+
...(templateNode.feedback_loop !== undefined
|
|
124
|
+
? { feedback_loop: templateNode.feedback_loop }
|
|
125
|
+
: {}),
|
|
126
|
+
...(templateNode.replay_safe !== undefined
|
|
127
|
+
? { replay_safe: templateNode.replay_safe }
|
|
128
|
+
: {}),
|
|
89
129
|
}));
|
|
90
130
|
const remaining = new Map(snapshot.nodes
|
|
91
131
|
.filter((n) => n.id !== catalogId)
|
|
@@ -112,7 +152,7 @@ export function appendCloneInstances(snapshot, input) {
|
|
|
112
152
|
...instanceIds,
|
|
113
153
|
...predChildren.slice(predIdx + 1),
|
|
114
154
|
];
|
|
115
|
-
const definitionParent = catalogNode
|
|
155
|
+
const definitionParent = catalogNode?.needs;
|
|
116
156
|
if (definitionParent && definitionParent !== predecessorId) {
|
|
117
157
|
const parentChildren = childrenOf[definitionParent] ?? [];
|
|
118
158
|
childrenOf[definitionParent] = parentChildren.filter((id) => id !== catalogId);
|