stageflow 0.8.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 +24 -14
- 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 +22 -0
- package/dist/cli/runsCommand.js +773 -0
- package/dist/cli/validateOutput.d.ts +2 -2
- package/dist/cli/validateOutput.js +2 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +43 -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/index.d.ts +1 -1
- package/dist/index.js +1 -1
- 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 -0
- package/dist/package-meta.js +1 -0
- 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 +61 -1
- package/dist/runtime/runManager.js +501 -7
- package/dist/runtime/runRetryCoordinator.d.ts +8 -1
- package/dist/runtime/runRetryCoordinator.js +47 -10
- 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/references/control-surface.md +4 -2
- package/skills/stageflow-author/references/catalog-mapping.md +26 -2
- package/skills/stageflow-author/references/catalog-write-conventions.md +1 -1
- package/skills/stageflow-run/SKILL.md +56 -25
- package/skills/stageflow-run/references/mcp-call.md +2 -2
- package/skills/stageflow-run/references/native-question-ui.md +78 -0
- package/dist/ui/assets/index-CSoGDA3A.js +0 -118
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { parseCloneForks } from "./check.js";
|
|
2
|
+
import { assertCloneAssignmentPayload } from "./payloadSchema.js";
|
|
2
3
|
import { EnvelopeError } from "../types/envelope.js";
|
|
4
|
+
import { CLONE_ACTIONS } from "../types/forkChoice.js";
|
|
3
5
|
function fieldPresent(item, key) {
|
|
4
6
|
return item[key] !== undefined;
|
|
5
7
|
}
|
|
@@ -17,40 +19,53 @@ export function assertCloneForks(params, cloneEmitContext) {
|
|
|
17
19
|
const expectedIds = cloneEmitContext.clonableSuccessors.map((s) => s.successorId);
|
|
18
20
|
const expectedSet = new Set(expectedIds);
|
|
19
21
|
const seen = new Set();
|
|
20
|
-
for (const item of items) {
|
|
22
|
+
for (const [i, item] of items.entries()) {
|
|
23
|
+
const itemPath = `clone_forks[${i}]`;
|
|
21
24
|
if (seen.has(item.successor_id)) {
|
|
22
|
-
throw new EnvelopeError(
|
|
25
|
+
throw new EnvelopeError(`${itemPath} contains duplicate successor_id: ${item.successor_id}`);
|
|
23
26
|
}
|
|
24
27
|
if (!expectedSet.has(item.successor_id)) {
|
|
25
|
-
throw new EnvelopeError(
|
|
28
|
+
throw new EnvelopeError(`${itemPath} contains extra successor_id: ${item.successor_id}`);
|
|
26
29
|
}
|
|
27
30
|
seen.add(item.successor_id);
|
|
28
31
|
const capEntry = cloneEmitContext.clonableSuccessors.find((s) => s.successorId === item.successor_id);
|
|
29
32
|
const cloneCap = capEntry?.cloneCap;
|
|
33
|
+
const allowedActions = cloneEmitContext.allowedActions ?? [...CLONE_ACTIONS];
|
|
34
|
+
if (!allowedActions.includes(item.action)) {
|
|
35
|
+
throw new EnvelopeError(`${itemPath} action "${item.action}" is not allowed for ${item.successor_id} (allowed: ${allowedActions.join(", ")})`);
|
|
36
|
+
}
|
|
30
37
|
if (item.action === "skip") {
|
|
31
38
|
if (fieldPresent(item, "envelope") ||
|
|
32
39
|
fieldPresent(item, "mode") ||
|
|
33
40
|
fieldPresent(item, "clones")) {
|
|
34
|
-
throw new EnvelopeError(
|
|
41
|
+
throw new EnvelopeError(`${itemPath} skip for ${item.successor_id} must not include envelope, mode, or clones`);
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
else if (item.action === "once") {
|
|
38
45
|
if (!fieldPresent(item, "envelope")) {
|
|
39
|
-
throw new EnvelopeError(
|
|
46
|
+
throw new EnvelopeError(`${itemPath} once for ${item.successor_id} requires envelope`);
|
|
40
47
|
}
|
|
41
48
|
if (fieldPresent(item, "mode") || fieldPresent(item, "clones")) {
|
|
42
|
-
throw new EnvelopeError(
|
|
49
|
+
throw new EnvelopeError(`${itemPath} once for ${item.successor_id} must not include mode or clones`);
|
|
50
|
+
}
|
|
51
|
+
if (capEntry?.cloneInputSchema !== undefined) {
|
|
52
|
+
assertCloneAssignmentPayload(item.envelope, capEntry.cloneInputSchema, item.successor_id, `${itemPath}.envelope`);
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
else if (item.action === "fanout") {
|
|
46
56
|
if (fieldPresent(item, "envelope")) {
|
|
47
|
-
throw new EnvelopeError(
|
|
57
|
+
throw new EnvelopeError(`${itemPath} fanout for ${item.successor_id} must not include a top-level envelope`);
|
|
48
58
|
}
|
|
49
59
|
if (!fieldPresent(item, "mode") || !fieldPresent(item, "clones") || item.clones === undefined) {
|
|
50
|
-
throw new EnvelopeError(
|
|
60
|
+
throw new EnvelopeError(`${itemPath} fanout for ${item.successor_id} requires mode and clones`);
|
|
51
61
|
}
|
|
52
62
|
if (cloneCap === undefined || item.clones.length < 2 || item.clones.length > cloneCap) {
|
|
53
|
-
throw new EnvelopeError(
|
|
63
|
+
throw new EnvelopeError(`${itemPath} fanout for ${item.successor_id} must have between 2 and ${cloneCap} clones`);
|
|
64
|
+
}
|
|
65
|
+
if (capEntry?.cloneInputSchema !== undefined) {
|
|
66
|
+
for (const [j, clone] of item.clones.entries()) {
|
|
67
|
+
assertCloneAssignmentPayload(clone.envelope, capEntry.cloneInputSchema, item.successor_id, `${itemPath}.clones[${j}].envelope`);
|
|
68
|
+
}
|
|
54
69
|
}
|
|
55
70
|
}
|
|
56
71
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type StageEnvelope } from "../types/envelope.js";
|
|
2
|
+
import type { FeedbackLoopConfig } from "../types/pipeline.js";
|
|
3
|
+
/** Validates an action against the source stage's declared feedback-loop policy. */
|
|
4
|
+
export declare function assertFeedbackLoopAction(envelope: StageEnvelope, context: FeedbackLoopConfig | undefined): void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { EnvelopeError } from "../types/envelope.js";
|
|
2
|
+
/** Validates an action against the source stage's declared feedback-loop policy. */
|
|
3
|
+
export function assertFeedbackLoopAction(envelope, context) {
|
|
4
|
+
if (context === undefined) {
|
|
5
|
+
if (envelope.feedback_loop !== undefined) {
|
|
6
|
+
throw new EnvelopeError("feedback_loop is only allowed for a configured feedback-loop stage");
|
|
7
|
+
}
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (envelope.status === "failure") {
|
|
11
|
+
if (envelope.feedback_loop !== undefined) {
|
|
12
|
+
throw new EnvelopeError("feedback_loop action is not allowed when status is failure");
|
|
13
|
+
}
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const action = envelope.feedback_loop;
|
|
17
|
+
if (action === undefined) {
|
|
18
|
+
throw new EnvelopeError("feedback_loop action is required for a successful feedback-loop stage");
|
|
19
|
+
}
|
|
20
|
+
if (action.action === "continue")
|
|
21
|
+
return;
|
|
22
|
+
if (envelope.fork_choice !== undefined) {
|
|
23
|
+
throw new EnvelopeError("feedback_loop send_back cannot be combined with fork_choice");
|
|
24
|
+
}
|
|
25
|
+
if (envelope.clone_forks !== undefined) {
|
|
26
|
+
throw new EnvelopeError("feedback_loop send_back cannot be combined with clone_forks");
|
|
27
|
+
}
|
|
28
|
+
if (action.target !== context.target) {
|
|
29
|
+
throw new EnvelopeError(`feedback_loop.target "${action.target}" is not an allowed feedback-loop target`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -4,7 +4,13 @@ export type CompiledPayloadSchema = TSchema;
|
|
|
4
4
|
/**
|
|
5
5
|
* Compile a JSON Schema subset used for stage payload_schema.
|
|
6
6
|
* Supported: type object/string/number/integer/boolean/array,
|
|
7
|
-
* properties, required, items, additionalProperties (boolean)
|
|
7
|
+
* properties, required, items, additionalProperties (boolean),
|
|
8
|
+
* minItems, enum, minimum, maximum. String nodes also accept
|
|
9
|
+
* pattern (a JS RegExp validated with the unicode flag), minLength,
|
|
10
|
+
* and maxLength (non-negative integers). Nested nodes accept
|
|
11
|
+
* nullable (boolean), compiling to a union of that type with null;
|
|
12
|
+
* the root object cannot be nullable. Unknown keywords are ignored.
|
|
8
13
|
*/
|
|
9
14
|
export declare function compilePayloadSchema(raw: unknown): CompiledPayloadSchema;
|
|
10
15
|
export declare function assertEnvelopePayload(envelope: StageEnvelope, payloadSchema: unknown | undefined): void;
|
|
16
|
+
export declare function assertCloneAssignmentPayload(envelope: StageEnvelope, cloneInputSchema: unknown, successorId: string, itemPath?: string): void;
|
|
@@ -1,6 +1,89 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { EnvelopeError } from "../types/envelope.js";
|
|
4
|
+
function readEnum(schema, path, valueType) {
|
|
5
|
+
if (schema.enum === undefined) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
if (!Array.isArray(schema.enum) || schema.enum.length === 0) {
|
|
9
|
+
throw new Error(`${path}: enum must be a non-empty array when present`);
|
|
10
|
+
}
|
|
11
|
+
if (valueType === "string") {
|
|
12
|
+
if (!schema.enum.every((value) => typeof value === "string")) {
|
|
13
|
+
throw new Error(`${path}: enum values must be strings`);
|
|
14
|
+
}
|
|
15
|
+
return schema.enum;
|
|
16
|
+
}
|
|
17
|
+
if (!schema.enum.every((value) => typeof value === "number" && Number.isInteger(value))) {
|
|
18
|
+
throw new Error(`${path}: enum values must be integers`);
|
|
19
|
+
}
|
|
20
|
+
return schema.enum;
|
|
21
|
+
}
|
|
22
|
+
function readBound(value, path, keyword) {
|
|
23
|
+
if (value === undefined) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
27
|
+
throw new Error(`${path}: ${keyword} must be a number when present`);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function numericOptions(schema, path) {
|
|
32
|
+
const minimum = readBound(schema.minimum, path, "minimum");
|
|
33
|
+
const maximum = readBound(schema.maximum, path, "maximum");
|
|
34
|
+
if (minimum === undefined && maximum === undefined) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
...(minimum !== undefined ? { minimum } : {}),
|
|
39
|
+
...(maximum !== undefined ? { maximum } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function readPattern(value, path) {
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
if (typeof value !== "string") {
|
|
47
|
+
throw new Error(`${path}: pattern must be a string when present`);
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
// eslint-disable-next-line no-new
|
|
51
|
+
new RegExp(value, "u");
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error(`${path}: pattern must be a valid regular expression`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function readNonNegativeInt(value, path, keyword) {
|
|
59
|
+
if (value === undefined) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
if (typeof value !== "number" ||
|
|
63
|
+
!Number.isInteger(value) ||
|
|
64
|
+
value < 0) {
|
|
65
|
+
throw new Error(`${path}: ${keyword} must be a non-negative integer when present`);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
function stringOptions(schema, path) {
|
|
70
|
+
const pattern = readPattern(schema.pattern, path);
|
|
71
|
+
const minLength = readNonNegativeInt(schema.minLength, path, "minLength");
|
|
72
|
+
const maxLength = readNonNegativeInt(schema.maxLength, path, "maxLength");
|
|
73
|
+
if (minLength !== undefined &&
|
|
74
|
+
maxLength !== undefined &&
|
|
75
|
+
minLength > maxLength) {
|
|
76
|
+
throw new Error(`${path}: minLength must be <= maxLength`);
|
|
77
|
+
}
|
|
78
|
+
if (pattern === undefined && minLength === undefined && maxLength === undefined) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
...(pattern !== undefined ? { pattern } : {}),
|
|
83
|
+
...(minLength !== undefined ? { minLength } : {}),
|
|
84
|
+
...(maxLength !== undefined ? { maxLength } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
4
87
|
function compileNode(node, path) {
|
|
5
88
|
if (node === null || typeof node !== "object" || Array.isArray(node)) {
|
|
6
89
|
throw new Error(`${path}: schema must be an object`);
|
|
@@ -9,20 +92,58 @@ function compileNode(node, path) {
|
|
|
9
92
|
if (typeof schema.type !== "string") {
|
|
10
93
|
throw new Error(`${path}: type is required`);
|
|
11
94
|
}
|
|
95
|
+
if (schema.nullable !== undefined && typeof schema.nullable !== "boolean") {
|
|
96
|
+
throw new Error(`${path}: nullable must be a boolean when present`);
|
|
97
|
+
}
|
|
98
|
+
const nullable = schema.nullable === true;
|
|
99
|
+
let base;
|
|
12
100
|
switch (schema.type) {
|
|
13
|
-
case "string":
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
101
|
+
case "string": {
|
|
102
|
+
const enumerated = readEnum(schema, path, "string");
|
|
103
|
+
const opts = stringOptions(schema, path);
|
|
104
|
+
base =
|
|
105
|
+
enumerated !== undefined
|
|
106
|
+
? Type.Enum(enumerated, opts)
|
|
107
|
+
: opts !== undefined
|
|
108
|
+
? Type.String(opts)
|
|
109
|
+
: Type.String();
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "number": {
|
|
113
|
+
const options = numericOptions(schema, path);
|
|
114
|
+
base = options !== undefined ? Type.Number(options) : Type.Number();
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
case "integer": {
|
|
118
|
+
const enumerated = readEnum(schema, path, "integer");
|
|
119
|
+
const options = numericOptions(schema, path);
|
|
120
|
+
if (enumerated !== undefined) {
|
|
121
|
+
base = Type.Enum(enumerated, options);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
base = options !== undefined ? Type.Integer(options) : Type.Integer();
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
19
128
|
case "boolean":
|
|
20
|
-
|
|
129
|
+
base = Type.Boolean();
|
|
130
|
+
break;
|
|
21
131
|
case "array": {
|
|
22
132
|
if (schema.items === undefined) {
|
|
23
133
|
throw new Error(`${path}: array requires items`);
|
|
24
134
|
}
|
|
25
|
-
|
|
135
|
+
if (schema.minItems !== undefined &&
|
|
136
|
+
(typeof schema.minItems !== "number" ||
|
|
137
|
+
!Number.isInteger(schema.minItems) ||
|
|
138
|
+
schema.minItems < 0)) {
|
|
139
|
+
throw new Error(`${path}: minItems must be a non-negative integer when present`);
|
|
140
|
+
}
|
|
141
|
+
const items = compileNode(schema.items, `${path}.items`);
|
|
142
|
+
base =
|
|
143
|
+
schema.minItems !== undefined
|
|
144
|
+
? Type.Array(items, { minItems: schema.minItems })
|
|
145
|
+
: Type.Array(items);
|
|
146
|
+
break;
|
|
26
147
|
}
|
|
27
148
|
case "object": {
|
|
28
149
|
const properties = schema.properties ?? {};
|
|
@@ -51,24 +172,46 @@ function compileNode(node, path) {
|
|
|
51
172
|
const options = schema.additionalProperties === false
|
|
52
173
|
? { additionalProperties: false }
|
|
53
174
|
: undefined;
|
|
54
|
-
|
|
175
|
+
base = Type.Object(compiledProps, options);
|
|
176
|
+
break;
|
|
55
177
|
}
|
|
56
178
|
default:
|
|
57
179
|
throw new Error(`${path}: unsupported type "${schema.type}" (supported: object, string, number, integer, boolean, array)`);
|
|
58
180
|
}
|
|
181
|
+
return nullable ? Type.Union([base, Type.Null()]) : base;
|
|
59
182
|
}
|
|
60
183
|
/**
|
|
61
184
|
* Compile a JSON Schema subset used for stage payload_schema.
|
|
62
185
|
* Supported: type object/string/number/integer/boolean/array,
|
|
63
|
-
* properties, required, items, additionalProperties (boolean)
|
|
186
|
+
* properties, required, items, additionalProperties (boolean),
|
|
187
|
+
* minItems, enum, minimum, maximum. String nodes also accept
|
|
188
|
+
* pattern (a JS RegExp validated with the unicode flag), minLength,
|
|
189
|
+
* and maxLength (non-negative integers). Nested nodes accept
|
|
190
|
+
* nullable (boolean), compiling to a union of that type with null;
|
|
191
|
+
* the root object cannot be nullable. Unknown keywords are ignored.
|
|
64
192
|
*/
|
|
65
193
|
export function compilePayloadSchema(raw) {
|
|
194
|
+
if (raw !== null &&
|
|
195
|
+
typeof raw === "object" &&
|
|
196
|
+
!Array.isArray(raw) &&
|
|
197
|
+
raw.nullable === true) {
|
|
198
|
+
throw new Error("payload_schema: root cannot be nullable");
|
|
199
|
+
}
|
|
66
200
|
const compiled = compileNode(raw, "payload_schema");
|
|
67
201
|
if (!Type.IsObject(compiled)) {
|
|
68
202
|
throw new Error("payload_schema: root type must be object");
|
|
69
203
|
}
|
|
70
204
|
return compiled;
|
|
71
205
|
}
|
|
206
|
+
function formatPayloadInstancePath(instancePath) {
|
|
207
|
+
const translated = instancePath.replace(/\//g, ".").replace(/^\./, "");
|
|
208
|
+
return translated ? `payload.${translated}` : "payload";
|
|
209
|
+
}
|
|
210
|
+
function payloadSchemaMismatchDetails(schema, payload) {
|
|
211
|
+
return [...Value.Errors(schema, payload)]
|
|
212
|
+
.map((err) => `${formatPayloadInstancePath(err.instancePath)} ${err.message}`)
|
|
213
|
+
.join("; ");
|
|
214
|
+
}
|
|
72
215
|
export function assertEnvelopePayload(envelope, payloadSchema) {
|
|
73
216
|
if (payloadSchema === undefined) {
|
|
74
217
|
return;
|
|
@@ -81,11 +224,25 @@ export function assertEnvelopePayload(envelope, payloadSchema) {
|
|
|
81
224
|
throw new EnvelopeError("payload is required by stage payload_schema");
|
|
82
225
|
}
|
|
83
226
|
if (!Value.Check(schema, envelope.payload)) {
|
|
84
|
-
const details =
|
|
85
|
-
.map((err) => `${err.instancePath || "/"} ${err.message}`)
|
|
86
|
-
.join("; ");
|
|
227
|
+
const details = payloadSchemaMismatchDetails(schema, envelope.payload);
|
|
87
228
|
throw new EnvelopeError(details
|
|
88
229
|
? `payload does not match payload_schema: ${details}`
|
|
89
230
|
: "payload does not match payload_schema");
|
|
90
231
|
}
|
|
91
232
|
}
|
|
233
|
+
export function assertCloneAssignmentPayload(envelope, cloneInputSchema, successorId, itemPath = "") {
|
|
234
|
+
if (envelope.status !== "success") {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const schema = compilePayloadSchema(cloneInputSchema);
|
|
238
|
+
const pathLead = itemPath ? `${itemPath}: ` : "";
|
|
239
|
+
if (envelope.payload === undefined) {
|
|
240
|
+
throw new EnvelopeError(`${pathLead}clone assignment payload is required by clone_input_schema for ${successorId}`);
|
|
241
|
+
}
|
|
242
|
+
if (!Value.Check(schema, envelope.payload)) {
|
|
243
|
+
const details = payloadSchemaMismatchDetails(schema, envelope.payload);
|
|
244
|
+
throw new EnvelopeError(details
|
|
245
|
+
? `${pathLead}clone assignment payload does not match clone_input_schema for ${successorId}: ${details}`
|
|
246
|
+
: `${pathLead}clone assignment payload does not match clone_input_schema for ${successorId}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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 { type QaExchange } from "../hitl/qaTrail.js";
|
|
13
|
+
import { type StageEnvelope } from "../types/envelope.js";
|
|
14
|
+
import type { PreEmitCheck } from "../types/preEmitCheck.js";
|
|
15
|
+
export type PreEmitCheckOptions = {
|
|
16
|
+
checks?: PreEmitCheck[];
|
|
17
|
+
readQaTrail?: () => QaExchange[] | Promise<QaExchange[]>;
|
|
18
|
+
};
|
|
19
|
+
export declare function declaredArtifactIssue(artifacts: readonly string[], check: Extract<PreEmitCheck, {
|
|
20
|
+
type: "artifact_declared";
|
|
21
|
+
}>): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Runs `options.checks` in declaration order and throws `EnvelopeError` on
|
|
24
|
+
* the first failing check, matching the single-message-per-rejected-emit
|
|
25
|
+
* contract the rest of `emitStageEnvelope.ts`'s `assert*` helpers use. A
|
|
26
|
+
* `gate` check with no `readQaTrail` fails closed (cannot be verified). No
|
|
27
|
+
* `checks` (or an empty list) is a no-op.
|
|
28
|
+
*/
|
|
29
|
+
export declare function assertPreEmitChecks(envelope: Pick<StageEnvelope, "artifacts">, options: PreEmitCheckOptions | undefined): Promise<void>;
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/f
|
|
|
2
2
|
export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
|
|
3
3
|
export { resolveProjectContext, legacyProjectContext, type ProjectContext, } from "./project/resolveProjectContext.js";
|
|
4
4
|
export { resolveStageflowContext, projectContextFromStageflow, type StageflowContext, type CatalogManifestStatus, } from "./project/resolveStageflowContext.js";
|
|
5
|
-
export { PACKAGE_NAME } from "./package-meta.js";
|
|
5
|
+
export { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
|
|
6
6
|
export type { StageEnvelope } from "./types/envelope.js";
|
|
7
7
|
export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
|
|
8
8
|
export type { AgentPort } from "./agent/port.js";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { findProjectRoot, clearFindProjectRootCacheForTests, } from "./project/f
|
|
|
2
2
|
export { globalStageflowHome, ensureGlobalHome, } from "./project/globalHome.js";
|
|
3
3
|
export { resolveProjectContext, legacyProjectContext, } from "./project/resolveProjectContext.js";
|
|
4
4
|
export { resolveStageflowContext, projectContextFromStageflow, } from "./project/resolveStageflowContext.js";
|
|
5
|
-
export { PACKAGE_NAME } from "./package-meta.js";
|
|
5
|
+
export { PACKAGE_NAME, PACKAGE_VERSION } from "./package-meta.js";
|
|
6
6
|
export { assertRequiredEnvelope, isAdvancingEnvelope, } from "./envelope/check.js";
|
|
7
7
|
export { runPipeline, startPipeline } from "./runtime/pipelineRunner.js";
|
|
8
8
|
export { createRunStore } from "./runstore/createStore.js";
|
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 } : {}),
|