stageflow 0.1.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/LICENSE +21 -0
- package/README.md +56 -0
- package/dist/agent/activity.d.ts +76 -0
- package/dist/agent/activity.js +89 -0
- package/dist/agent/activityObserver.d.ts +17 -0
- package/dist/agent/activityObserver.js +66 -0
- package/dist/agent/cursorExtension.d.ts +12 -0
- package/dist/agent/cursorExtension.js +88 -0
- package/dist/agent/cursorProvider.d.ts +14 -0
- package/dist/agent/cursorProvider.js +139 -0
- package/dist/agent/fakeAgent.d.ts +29 -0
- package/dist/agent/fakeAgent.js +295 -0
- package/dist/agent/oauthSessionManager.d.ts +33 -0
- package/dist/agent/oauthSessionManager.js +214 -0
- package/dist/agent/piAdapter.d.ts +128 -0
- package/dist/agent/piAdapter.js +967 -0
- package/dist/agent/port.d.ts +63 -0
- package/dist/agent/port.js +46 -0
- package/dist/agent/providerAuth.d.ts +101 -0
- package/dist/agent/providerAuth.js +282 -0
- package/dist/agent/providerSupport.d.ts +28 -0
- package/dist/agent/providerSupport.js +19 -0
- package/dist/cli/providersCommand.d.ts +11 -0
- package/dist/cli/providersCommand.js +275 -0
- package/dist/cli/terminalAuthInteraction.d.ts +8 -0
- package/dist/cli/terminalAuthInteraction.js +65 -0
- package/dist/cli/terminalSecret.d.ts +2 -0
- package/dist/cli/terminalSecret.js +67 -0
- package/dist/cli/validateCommand.d.ts +11 -0
- package/dist/cli/validateCommand.js +81 -0
- package/dist/cli/validateOutput.d.ts +8 -0
- package/dist/cli/validateOutput.js +75 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +273 -0
- package/dist/config/createPipeline.d.ts +31 -0
- package/dist/config/createPipeline.js +255 -0
- package/dist/config/createStage.d.ts +25 -0
- package/dist/config/createStage.js +181 -0
- package/dist/config/listConfig.d.ts +33 -0
- package/dist/config/listConfig.js +195 -0
- package/dist/config/listExtensions.d.ts +26 -0
- package/dist/config/listExtensions.js +50 -0
- package/dist/config/listSkills.d.ts +29 -0
- package/dist/config/listSkills.js +55 -0
- package/dist/config/loadOutcome.d.ts +27 -0
- package/dist/config/loadOutcome.js +6 -0
- package/dist/config/loadPipeline.d.ts +25 -0
- package/dist/config/loadPipeline.js +196 -0
- package/dist/config/loadStage.d.ts +4 -0
- package/dist/config/loadStage.js +127 -0
- package/dist/config/loadTask.d.ts +7 -0
- package/dist/config/loadTask.js +69 -0
- package/dist/config/readYamlObject.d.ts +1 -0
- package/dist/config/readYamlObject.js +5 -0
- package/dist/config/resolvePipelineDag.d.ts +12 -0
- package/dist/config/resolvePipelineDag.js +282 -0
- package/dist/config/validateCatalog.d.ts +36 -0
- package/dist/config/validateCatalog.js +322 -0
- package/dist/envelope/check.d.ts +3 -0
- package/dist/envelope/check.js +52 -0
- package/dist/envelope/payloadSchema.d.ts +10 -0
- package/dist/envelope/payloadSchema.js +91 -0
- package/dist/hitl/qaTrail.d.ts +31 -0
- package/dist/hitl/qaTrail.js +65 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +7 -0
- package/dist/mcp/projectRun.d.ts +37 -0
- package/dist/mcp/projectRun.js +57 -0
- package/dist/mcp/readArtifact.d.ts +2 -0
- package/dist/mcp/readArtifact.js +46 -0
- package/dist/mcp/server.d.ts +7 -0
- package/dist/mcp/server.js +26 -0
- package/dist/mcp/tools.d.ts +9 -0
- package/dist/mcp/tools.js +96 -0
- package/dist/package-meta.d.ts +1 -0
- package/dist/package-meta.js +1 -0
- package/dist/prompt/priorEnvelope.d.ts +2 -0
- package/dist/prompt/priorEnvelope.js +5 -0
- package/dist/runstore/catalog.d.ts +8 -0
- package/dist/runstore/catalog.js +16 -0
- package/dist/runstore/createStore.d.ts +15 -0
- package/dist/runstore/createStore.js +23 -0
- package/dist/runstore/disk/DiskRunStore.d.ts +30 -0
- package/dist/runstore/disk/DiskRunStore.js +238 -0
- package/dist/runstore/disk/catalogHelpers.d.ts +11 -0
- package/dist/runstore/disk/catalogHelpers.js +102 -0
- package/dist/runstore/layout.d.ts +22 -0
- package/dist/runstore/layout.js +58 -0
- package/dist/runstore/paths.d.ts +7 -0
- package/dist/runstore/paths.js +60 -0
- package/dist/runstore/pipelineDagSnapshot.d.ts +5 -0
- package/dist/runstore/pipelineDagSnapshot.js +43 -0
- package/dist/runstore/port.d.ts +146 -0
- package/dist/runstore/port.js +28 -0
- package/dist/runstore/runProjection.d.ts +6 -0
- package/dist/runstore/runProjection.js +102 -0
- package/dist/runstore/sqlite/SqliteRunStore.d.ts +48 -0
- package/dist/runstore/sqlite/SqliteRunStore.js +460 -0
- package/dist/runstore/sqlite/migrateFromDisk.d.ts +5 -0
- package/dist/runstore/sqlite/migrateFromDisk.js +108 -0
- package/dist/runstore/sqlite/schema.d.ts +1 -0
- package/dist/runstore/sqlite/schema.js +59 -0
- package/dist/runstore/stageExecution.d.ts +13 -0
- package/dist/runstore/stageExecution.js +22 -0
- package/dist/runstore/stageSnapshot.d.ts +2 -0
- package/dist/runstore/stageSnapshot.js +46 -0
- package/dist/runstore/syntheticStageSnapshot.d.ts +2 -0
- package/dist/runstore/syntheticStageSnapshot.js +10 -0
- package/dist/runstore/trackProjection.d.ts +7 -0
- package/dist/runstore/trackProjection.js +94 -0
- package/dist/runstore/workspaceLayout.d.ts +31 -0
- package/dist/runstore/workspaceLayout.js +172 -0
- package/dist/runtime/answerResume.d.ts +16 -0
- package/dist/runtime/answerResume.js +22 -0
- package/dist/runtime/credentialBinding.d.ts +16 -0
- package/dist/runtime/credentialBinding.js +80 -0
- package/dist/runtime/dagTraversal.d.ts +3 -0
- package/dist/runtime/dagTraversal.js +26 -0
- package/dist/runtime/envelopeRouting.d.ts +20 -0
- package/dist/runtime/envelopeRouting.js +59 -0
- package/dist/runtime/hitlSeams.d.ts +38 -0
- package/dist/runtime/hitlSeams.js +2 -0
- package/dist/runtime/pipelineRunner.d.ts +77 -0
- package/dist/runtime/pipelineRunner.js +141 -0
- package/dist/runtime/pipelineScheduler.d.ts +90 -0
- package/dist/runtime/pipelineScheduler.js +494 -0
- package/dist/runtime/pipelineValidationError.d.ts +6 -0
- package/dist/runtime/pipelineValidationError.js +8 -0
- package/dist/runtime/resumeReconstruct.d.ts +33 -0
- package/dist/runtime/resumeReconstruct.js +144 -0
- package/dist/runtime/runManager.d.ts +129 -0
- package/dist/runtime/runManager.js +818 -0
- package/dist/runtime/runRetryCoordinator.d.ts +85 -0
- package/dist/runtime/runRetryCoordinator.js +372 -0
- package/dist/runtime/settingsFile.d.ts +17 -0
- package/dist/runtime/settingsFile.js +88 -0
- package/dist/runtime/stageAttemptBootstrap.d.ts +40 -0
- package/dist/runtime/stageAttemptBootstrap.js +82 -0
- package/dist/runtime/stageAttemptContext.d.ts +11 -0
- package/dist/runtime/stageAttemptContext.js +26 -0
- package/dist/runtime/stageConcurrency.d.ts +10 -0
- package/dist/runtime/stageConcurrency.js +51 -0
- package/dist/runtime/stageHitl.d.ts +95 -0
- package/dist/runtime/stageHitl.js +198 -0
- package/dist/runtime/stageProcessLauncher.d.ts +45 -0
- package/dist/runtime/stageProcessLauncher.js +199 -0
- package/dist/runtime/stageRecovery.d.ts +10 -0
- package/dist/runtime/stageRecovery.js +43 -0
- package/dist/runtime/stageRoots.d.ts +22 -0
- package/dist/runtime/stageRoots.js +93 -0
- package/dist/runtime/stageRunner.d.ts +59 -0
- package/dist/runtime/stageRunner.js +221 -0
- package/dist/runtime/stageWorker.d.ts +6 -0
- package/dist/runtime/stageWorker.js +109 -0
- package/dist/runtime/stageWorkerProtocol.d.ts +27 -0
- package/dist/runtime/stageWorkerProtocol.js +16 -0
- package/dist/runtime/taskInput.d.ts +15 -0
- package/dist/runtime/taskInput.js +38 -0
- package/dist/server/http.d.ts +31 -0
- package/dist/server/http.js +561 -0
- package/dist/server/providerRoutes.d.ts +15 -0
- package/dist/server/providerRoutes.js +186 -0
- package/dist/tools/askOperator.d.ts +185 -0
- package/dist/tools/askOperator.js +409 -0
- package/dist/tools/emitStageEnvelope.d.ts +42 -0
- package/dist/tools/emitStageEnvelope.js +61 -0
- package/dist/tools/writeStageArtifact.d.ts +26 -0
- package/dist/tools/writeStageArtifact.js +103 -0
- package/dist/types/envelope.d.ts +12 -0
- package/dist/types/envelope.js +6 -0
- package/dist/types/pipeline.d.ts +25 -0
- package/dist/types/pipeline.js +1 -0
- package/dist/types/stage.d.ts +12 -0
- package/dist/types/stage.js +6 -0
- package/dist/types/task.d.ts +7 -0
- package/dist/types/task.js +1 -0
- package/dist/ui/assets/index-Cry0Tpfx.js +118 -0
- package/dist/ui/assets/index-DCsDopak.css +1 -0
- package/dist/ui/index.html +21 -0
- package/package.json +72 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ask-operator prompt/answer vocabulary and tool factory (T2).
|
|
3
|
+
*
|
|
4
|
+
* Types, TypeBox schemas, and validators have no Pi or RunStore dependency.
|
|
5
|
+
* The factory blocks on an injected wait bridge; adapter registration is U3.
|
|
6
|
+
*
|
|
7
|
+
* Do not abort the session from inside `execute`: the tool Promise is the
|
|
8
|
+
* live blocking surface until the operator answers (or stage teardown).
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { Type } from "typebox";
|
|
12
|
+
export class AskOperatorError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "AskOperatorError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export const ASK_OPERATOR_KINDS = [
|
|
19
|
+
"free_text",
|
|
20
|
+
"confirm",
|
|
21
|
+
"multi_question",
|
|
22
|
+
"artifact_backed",
|
|
23
|
+
];
|
|
24
|
+
const subQuestionKindSchema = Type.Union([
|
|
25
|
+
Type.Literal("free_text"),
|
|
26
|
+
Type.Literal("confirm"),
|
|
27
|
+
]);
|
|
28
|
+
const multiQuestionItemSchema = Type.Object({
|
|
29
|
+
kind: subQuestionKindSchema,
|
|
30
|
+
message: Type.String({ minLength: 1 }),
|
|
31
|
+
id: Type.Optional(Type.String()),
|
|
32
|
+
});
|
|
33
|
+
export const askOperatorParamsSchema = Type.Union([
|
|
34
|
+
Type.Object({
|
|
35
|
+
kind: Type.Literal("free_text"),
|
|
36
|
+
message: Type.String({ minLength: 1 }),
|
|
37
|
+
id: Type.Optional(Type.String()),
|
|
38
|
+
}),
|
|
39
|
+
Type.Object({
|
|
40
|
+
kind: Type.Literal("confirm"),
|
|
41
|
+
message: Type.String({ minLength: 1 }),
|
|
42
|
+
id: Type.Optional(Type.String()),
|
|
43
|
+
}),
|
|
44
|
+
Type.Object({
|
|
45
|
+
kind: Type.Literal("multi_question"),
|
|
46
|
+
questions: Type.Array(multiQuestionItemSchema, { minItems: 1 }),
|
|
47
|
+
id: Type.Optional(Type.String()),
|
|
48
|
+
}),
|
|
49
|
+
Type.Object({
|
|
50
|
+
kind: Type.Literal("artifact_backed"),
|
|
51
|
+
message: Type.String({ minLength: 1 }),
|
|
52
|
+
artifacts: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
|
|
53
|
+
id: Type.Optional(Type.String()),
|
|
54
|
+
}),
|
|
55
|
+
]);
|
|
56
|
+
const decisionSchema = Type.Union([
|
|
57
|
+
Type.Literal("accept"),
|
|
58
|
+
Type.Literal("reject"),
|
|
59
|
+
]);
|
|
60
|
+
const freeTextOrConfirmPayloadSchema = Type.Union([
|
|
61
|
+
Type.Object({
|
|
62
|
+
kind: Type.Literal("free_text"),
|
|
63
|
+
text: Type.String(),
|
|
64
|
+
}),
|
|
65
|
+
Type.Object({
|
|
66
|
+
kind: Type.Literal("confirm"),
|
|
67
|
+
decision: decisionSchema,
|
|
68
|
+
text: Type.Optional(Type.String()),
|
|
69
|
+
}),
|
|
70
|
+
]);
|
|
71
|
+
export const askOperatorAnswerSchema = Type.Union([
|
|
72
|
+
Type.Object({
|
|
73
|
+
promptId: Type.String({ minLength: 1 }),
|
|
74
|
+
kind: Type.Literal("free_text"),
|
|
75
|
+
text: Type.String(),
|
|
76
|
+
}),
|
|
77
|
+
Type.Object({
|
|
78
|
+
promptId: Type.String({ minLength: 1 }),
|
|
79
|
+
kind: Type.Literal("confirm"),
|
|
80
|
+
decision: decisionSchema,
|
|
81
|
+
text: Type.Optional(Type.String()),
|
|
82
|
+
}),
|
|
83
|
+
Type.Object({
|
|
84
|
+
promptId: Type.String({ minLength: 1 }),
|
|
85
|
+
kind: Type.Literal("artifact_backed"),
|
|
86
|
+
decision: decisionSchema,
|
|
87
|
+
text: Type.Optional(Type.String()),
|
|
88
|
+
}),
|
|
89
|
+
Type.Object({
|
|
90
|
+
promptId: Type.String({ minLength: 1 }),
|
|
91
|
+
kind: Type.Literal("multi_question"),
|
|
92
|
+
answers: Type.Record(Type.String(), freeTextOrConfirmPayloadSchema),
|
|
93
|
+
}),
|
|
94
|
+
]);
|
|
95
|
+
function isRecord(value) {
|
|
96
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
97
|
+
}
|
|
98
|
+
function requireNonEmptyString(value, field) {
|
|
99
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
100
|
+
throw new AskOperatorError(`${field} must be a non-empty string`);
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
function optionalId(value, field) {
|
|
105
|
+
if (value === undefined) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
if (typeof value !== "string") {
|
|
109
|
+
throw new AskOperatorError(`${field} must be a string when present`);
|
|
110
|
+
}
|
|
111
|
+
const trimmed = value.trim();
|
|
112
|
+
return trimmed === "" ? undefined : trimmed;
|
|
113
|
+
}
|
|
114
|
+
function parseDecision(value, field) {
|
|
115
|
+
if (value !== "accept" && value !== "reject") {
|
|
116
|
+
throw new AskOperatorError(`${field} must be "accept" or "reject" (decision is required)`);
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
function parseOptionalText(value, field) {
|
|
121
|
+
if (value === undefined) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
if (typeof value !== "string") {
|
|
125
|
+
throw new AskOperatorError(`${field} must be a string when present`);
|
|
126
|
+
}
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
function parseSubQuestion(value, path) {
|
|
130
|
+
if (!isRecord(value)) {
|
|
131
|
+
throw new AskOperatorError(`${path} must be an object`);
|
|
132
|
+
}
|
|
133
|
+
const kind = value.kind;
|
|
134
|
+
if (kind !== "free_text" && kind !== "confirm") {
|
|
135
|
+
if (typeof kind === "string") {
|
|
136
|
+
throw new AskOperatorError(`${path}: unsupported sub-question kind "${kind}" (allowed: free_text, confirm; nested multi_question and artifact_backed are not allowed)`);
|
|
137
|
+
}
|
|
138
|
+
throw new AskOperatorError(`${path}: kind must be "free_text" or "confirm"`);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
kind,
|
|
142
|
+
message: requireNonEmptyString(value.message, `${path}.message`),
|
|
143
|
+
id: optionalId(value.id, `${path}.id`),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export function parseAskOperatorParams(value) {
|
|
147
|
+
if (!isRecord(value)) {
|
|
148
|
+
throw new AskOperatorError("ask_operator params must be an object");
|
|
149
|
+
}
|
|
150
|
+
const kind = value.kind;
|
|
151
|
+
if (typeof kind !== "string") {
|
|
152
|
+
throw new AskOperatorError("kind is required");
|
|
153
|
+
}
|
|
154
|
+
if (kind !== "free_text" &&
|
|
155
|
+
kind !== "confirm" &&
|
|
156
|
+
kind !== "multi_question" &&
|
|
157
|
+
kind !== "artifact_backed") {
|
|
158
|
+
throw new AskOperatorError(`unsupported prompt kind "${kind}" (allowed: free_text, confirm, multi_question, artifact_backed)`);
|
|
159
|
+
}
|
|
160
|
+
const id = optionalId(value.id, "id");
|
|
161
|
+
if (kind === "free_text" || kind === "confirm") {
|
|
162
|
+
return {
|
|
163
|
+
kind,
|
|
164
|
+
message: requireNonEmptyString(value.message, "message"),
|
|
165
|
+
...(id !== undefined ? { id } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (kind === "artifact_backed") {
|
|
169
|
+
if (!Array.isArray(value.artifacts)) {
|
|
170
|
+
throw new AskOperatorError("artifacts must be an array");
|
|
171
|
+
}
|
|
172
|
+
if (value.artifacts.length < 1) {
|
|
173
|
+
throw new AskOperatorError("artifacts must contain at least one path");
|
|
174
|
+
}
|
|
175
|
+
const artifacts = value.artifacts.map((item, index) => requireNonEmptyString(item, `artifacts[${index}]`));
|
|
176
|
+
return {
|
|
177
|
+
kind,
|
|
178
|
+
message: requireNonEmptyString(value.message, "message"),
|
|
179
|
+
artifacts,
|
|
180
|
+
...(id !== undefined ? { id } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (!Array.isArray(value.questions)) {
|
|
184
|
+
throw new AskOperatorError("questions must be an array");
|
|
185
|
+
}
|
|
186
|
+
if (value.questions.length < 1) {
|
|
187
|
+
throw new AskOperatorError("questions must contain at least one item");
|
|
188
|
+
}
|
|
189
|
+
const questions = value.questions.map((item, index) => parseSubQuestion(item, `questions[${index}]`));
|
|
190
|
+
return {
|
|
191
|
+
kind,
|
|
192
|
+
questions,
|
|
193
|
+
...(id !== undefined ? { id } : {}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function newId() {
|
|
197
|
+
return randomUUID();
|
|
198
|
+
}
|
|
199
|
+
function resolveId(supplied) {
|
|
200
|
+
return supplied !== undefined && supplied.trim() !== ""
|
|
201
|
+
? supplied.trim()
|
|
202
|
+
: newId();
|
|
203
|
+
}
|
|
204
|
+
export function normalizePromptIds(params) {
|
|
205
|
+
const id = resolveId(params.id);
|
|
206
|
+
if (params.kind === "free_text" || params.kind === "confirm") {
|
|
207
|
+
return {
|
|
208
|
+
kind: params.kind,
|
|
209
|
+
message: params.message,
|
|
210
|
+
id,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (params.kind === "artifact_backed") {
|
|
214
|
+
return {
|
|
215
|
+
kind: "artifact_backed",
|
|
216
|
+
message: params.message,
|
|
217
|
+
artifacts: params.artifacts,
|
|
218
|
+
id,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
kind: "multi_question",
|
|
223
|
+
id,
|
|
224
|
+
questions: params.questions.map((q) => ({
|
|
225
|
+
kind: q.kind,
|
|
226
|
+
message: q.message,
|
|
227
|
+
id: resolveId(q.id),
|
|
228
|
+
})),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function parseFreeTextOrConfirmPayload(value, path) {
|
|
232
|
+
if (!isRecord(value)) {
|
|
233
|
+
throw new AskOperatorError(`${path} must be an object`);
|
|
234
|
+
}
|
|
235
|
+
const kind = value.kind;
|
|
236
|
+
if (kind === "free_text") {
|
|
237
|
+
if (typeof value.text !== "string") {
|
|
238
|
+
throw new AskOperatorError(`${path}.text must be a string`);
|
|
239
|
+
}
|
|
240
|
+
return { kind: "free_text", text: value.text };
|
|
241
|
+
}
|
|
242
|
+
if (kind === "confirm") {
|
|
243
|
+
const decision = parseDecision(value.decision, `${path}.decision`);
|
|
244
|
+
const text = parseOptionalText(value.text, `${path}.text`);
|
|
245
|
+
return text !== undefined
|
|
246
|
+
? { kind: "confirm", decision, text }
|
|
247
|
+
: { kind: "confirm", decision };
|
|
248
|
+
}
|
|
249
|
+
if (typeof kind === "string") {
|
|
250
|
+
throw new AskOperatorError(`${path}: unsupported answer kind "${kind}" (allowed: free_text, confirm)`);
|
|
251
|
+
}
|
|
252
|
+
throw new AskOperatorError(`${path}: kind must be "free_text" or "confirm"`);
|
|
253
|
+
}
|
|
254
|
+
export function parseAskOperatorAnswer(value) {
|
|
255
|
+
if (!isRecord(value)) {
|
|
256
|
+
throw new AskOperatorError("ask_operator answer must be an object");
|
|
257
|
+
}
|
|
258
|
+
const promptId = requireNonEmptyString(value.promptId, "promptId");
|
|
259
|
+
const kind = value.kind;
|
|
260
|
+
if (typeof kind !== "string") {
|
|
261
|
+
throw new AskOperatorError("kind is required");
|
|
262
|
+
}
|
|
263
|
+
if (kind !== "free_text" &&
|
|
264
|
+
kind !== "confirm" &&
|
|
265
|
+
kind !== "multi_question" &&
|
|
266
|
+
kind !== "artifact_backed") {
|
|
267
|
+
throw new AskOperatorError(`unsupported answer kind "${kind}" (allowed: free_text, confirm, multi_question, artifact_backed)`);
|
|
268
|
+
}
|
|
269
|
+
if (kind === "free_text") {
|
|
270
|
+
if (typeof value.text !== "string") {
|
|
271
|
+
throw new AskOperatorError("text must be a string");
|
|
272
|
+
}
|
|
273
|
+
return { promptId, kind: "free_text", text: value.text };
|
|
274
|
+
}
|
|
275
|
+
if (kind === "confirm" || kind === "artifact_backed") {
|
|
276
|
+
const decision = parseDecision(value.decision, "decision");
|
|
277
|
+
const text = parseOptionalText(value.text, "text");
|
|
278
|
+
return text !== undefined
|
|
279
|
+
? { promptId, kind, decision, text }
|
|
280
|
+
: { promptId, kind, decision };
|
|
281
|
+
}
|
|
282
|
+
if (!isRecord(value.answers)) {
|
|
283
|
+
throw new AskOperatorError("answers must be an object");
|
|
284
|
+
}
|
|
285
|
+
const answers = {};
|
|
286
|
+
for (const [subId, payload] of Object.entries(value.answers)) {
|
|
287
|
+
answers[subId] = parseFreeTextOrConfirmPayload(payload, `answers[${subId}]`);
|
|
288
|
+
}
|
|
289
|
+
return { promptId, kind: "multi_question", answers };
|
|
290
|
+
}
|
|
291
|
+
function assertSubAnswerMatches(question, payload) {
|
|
292
|
+
if (payload === undefined) {
|
|
293
|
+
throw new AskOperatorError(`multi_question answer missing response for sub-question id "${question.id}"`);
|
|
294
|
+
}
|
|
295
|
+
if (payload.kind !== question.kind) {
|
|
296
|
+
throw new AskOperatorError(`answer kind "${payload.kind}" does not match sub-question kind "${question.kind}" for id "${question.id}"`);
|
|
297
|
+
}
|
|
298
|
+
if (payload.kind === "free_text") {
|
|
299
|
+
if (typeof payload.text !== "string") {
|
|
300
|
+
throw new AskOperatorError(`free_text answer for sub-question id "${question.id}" requires text`);
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (payload.decision !== "accept" && payload.decision !== "reject") {
|
|
305
|
+
throw new AskOperatorError(`confirm answer for sub-question id "${question.id}" requires decision`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
export function assertAnswerMatchesPrompt(prompt, answer) {
|
|
309
|
+
if (answer.promptId !== prompt.id) {
|
|
310
|
+
throw new AskOperatorError(`answer promptId "${answer.promptId}" does not match prompt id "${prompt.id}"`);
|
|
311
|
+
}
|
|
312
|
+
if (answer.kind !== prompt.kind) {
|
|
313
|
+
throw new AskOperatorError(`answer kind "${answer.kind}" does not match prompt kind "${prompt.kind}"`);
|
|
314
|
+
}
|
|
315
|
+
if (prompt.kind === "free_text" && answer.kind === "free_text") {
|
|
316
|
+
if (typeof answer.text !== "string") {
|
|
317
|
+
throw new AskOperatorError("free_text answer requires text");
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if ((prompt.kind === "confirm" && answer.kind === "confirm") ||
|
|
322
|
+
(prompt.kind === "artifact_backed" && answer.kind === "artifact_backed")) {
|
|
323
|
+
if (answer.decision !== "accept" && answer.decision !== "reject") {
|
|
324
|
+
throw new AskOperatorError(`${prompt.kind} answer requires decision ("accept" or "reject")`);
|
|
325
|
+
}
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (prompt.kind === "multi_question" && answer.kind === "multi_question") {
|
|
329
|
+
const expectedIds = new Set(prompt.questions.map((q) => q.id));
|
|
330
|
+
const actualIds = Object.keys(answer.answers);
|
|
331
|
+
for (const question of prompt.questions) {
|
|
332
|
+
assertSubAnswerMatches(question, answer.answers[question.id]);
|
|
333
|
+
}
|
|
334
|
+
for (const subId of actualIds) {
|
|
335
|
+
if (!expectedIds.has(subId)) {
|
|
336
|
+
throw new AskOperatorError(`multi_question answer includes unknown sub-question id "${subId}"`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (actualIds.length !== expectedIds.size) {
|
|
340
|
+
throw new AskOperatorError(`multi_question answer must cover every sub-question id (expected ${expectedIds.size}, got ${actualIds.length})`);
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
throw new AskOperatorError("answer does not match prompt");
|
|
345
|
+
}
|
|
346
|
+
export const parsePrompt = parseAskOperatorParams;
|
|
347
|
+
export const parseAnswer = parseAskOperatorAnswer;
|
|
348
|
+
function toolResult(text, details, options) {
|
|
349
|
+
return {
|
|
350
|
+
content: [{ type: "text", text }],
|
|
351
|
+
details,
|
|
352
|
+
...(options?.isError ? { isError: true } : {}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function summarizeAnswer(answer) {
|
|
356
|
+
if (answer.kind === "free_text") {
|
|
357
|
+
return `Operator answered: ${answer.text}`;
|
|
358
|
+
}
|
|
359
|
+
if (answer.kind === "confirm" || answer.kind === "artifact_backed") {
|
|
360
|
+
return answer.text !== undefined && answer.text !== ""
|
|
361
|
+
? `Operator ${answer.decision}: ${answer.text}`
|
|
362
|
+
: `Operator ${answer.decision}`;
|
|
363
|
+
}
|
|
364
|
+
const count = Object.keys(answer.answers).length;
|
|
365
|
+
return `Operator answered ${count} question${count === 1 ? "" : "s"}`;
|
|
366
|
+
}
|
|
367
|
+
export function createAskOperatorTool(options) {
|
|
368
|
+
const { requestWait } = options;
|
|
369
|
+
return {
|
|
370
|
+
name: "ask_operator",
|
|
371
|
+
label: "Ask operator",
|
|
372
|
+
description: "Ask the operator a question and wait for their answer. Supports free_text, confirm, multi_question, and artifact_backed prompts. Does not complete the stage — call emit_stage_envelope when finished.",
|
|
373
|
+
parameters: askOperatorParamsSchema,
|
|
374
|
+
execute: async (_toolCallId, params) => {
|
|
375
|
+
let prompt;
|
|
376
|
+
try {
|
|
377
|
+
const parsed = parseAskOperatorParams(params);
|
|
378
|
+
prompt = normalizePromptIds(parsed);
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
382
|
+
return toolResult(`Invalid ask_operator params: ${message}`, {
|
|
383
|
+
prompt: null,
|
|
384
|
+
answer: null,
|
|
385
|
+
error: message,
|
|
386
|
+
}, { isError: true });
|
|
387
|
+
}
|
|
388
|
+
const rawAnswer = await requestWait(prompt);
|
|
389
|
+
let answer;
|
|
390
|
+
try {
|
|
391
|
+
answer = parseAskOperatorAnswer(rawAnswer);
|
|
392
|
+
assertAnswerMatchesPrompt(prompt, answer);
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
396
|
+
return toolResult(`Invalid operator answer: ${message}`, {
|
|
397
|
+
prompt,
|
|
398
|
+
answer: null,
|
|
399
|
+
error: message,
|
|
400
|
+
}, { isError: true });
|
|
401
|
+
}
|
|
402
|
+
return toolResult(summarizeAnswer(answer), {
|
|
403
|
+
prompt,
|
|
404
|
+
answer,
|
|
405
|
+
error: "",
|
|
406
|
+
});
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Pi tool the stage agent must call once when finished.
|
|
3
|
+
*
|
|
4
|
+
* Parameter schemas use `typebox` (Pi's `defineTool` TSchema), not
|
|
5
|
+
* `@sinclair/typebox`. An accepted emit returns `terminate: true` so Pi
|
|
6
|
+
* ends the turn instead of asking the model to continue.
|
|
7
|
+
*
|
|
8
|
+
* Do not abort the session from inside `execute`: providers that bridge tools
|
|
9
|
+
* (e.g. MCP) may still be waiting for this result, and abort-while-running
|
|
10
|
+
* deadlocks waitForIdle so later pipeline stages never start.
|
|
11
|
+
*/
|
|
12
|
+
import { Type } from "typebox";
|
|
13
|
+
import type { StageEnvelope } from "../types/envelope.js";
|
|
14
|
+
export type EmitCapture = {
|
|
15
|
+
envelope?: StageEnvelope;
|
|
16
|
+
error?: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function createEmitStageEnvelopeTool(capture: EmitCapture, payloadSchema?: unknown): {
|
|
19
|
+
name: string;
|
|
20
|
+
label: string;
|
|
21
|
+
description: string;
|
|
22
|
+
parameters: Type.TObject<{
|
|
23
|
+
status: Type.TUnion<[Type.TLiteral<"success">, Type.TLiteral<"failure">]>;
|
|
24
|
+
summary: Type.TString;
|
|
25
|
+
artifacts: Type.TArray<Type.TString>;
|
|
26
|
+
payload: Type.TSchema;
|
|
27
|
+
stage_id: Type.TOptional<Type.TString>;
|
|
28
|
+
notes: Type.TOptional<Type.TString>;
|
|
29
|
+
}>;
|
|
30
|
+
execute: (_toolCallId: string, params: unknown) => Promise<{
|
|
31
|
+
content: {
|
|
32
|
+
type: "text";
|
|
33
|
+
text: string;
|
|
34
|
+
}[];
|
|
35
|
+
details: {
|
|
36
|
+
advancing: boolean;
|
|
37
|
+
error: string;
|
|
38
|
+
};
|
|
39
|
+
isError?: boolean | undefined;
|
|
40
|
+
terminate?: boolean | undefined;
|
|
41
|
+
}>;
|
|
42
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Pi tool the stage agent must call once when finished.
|
|
3
|
+
*
|
|
4
|
+
* Parameter schemas use `typebox` (Pi's `defineTool` TSchema), not
|
|
5
|
+
* `@sinclair/typebox`. An accepted emit returns `terminate: true` so Pi
|
|
6
|
+
* ends the turn instead of asking the model to continue.
|
|
7
|
+
*
|
|
8
|
+
* Do not abort the session from inside `execute`: providers that bridge tools
|
|
9
|
+
* (e.g. MCP) may still be waiting for this result, and abort-while-running
|
|
10
|
+
* deadlocks waitForIdle so later pipeline stages never start.
|
|
11
|
+
*/
|
|
12
|
+
import { Type } from "typebox";
|
|
13
|
+
import { assertRequiredEnvelope, isAdvancingEnvelope, } from "../envelope/check.js";
|
|
14
|
+
import { assertEnvelopePayload, compilePayloadSchema, } from "../envelope/payloadSchema.js";
|
|
15
|
+
function toolResult(text, details, options) {
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: "text", text }],
|
|
18
|
+
details,
|
|
19
|
+
...(options?.isError ? { isError: true } : {}),
|
|
20
|
+
...(options?.terminate ? { terminate: true } : {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function createEmitStageEnvelopeTool(capture, payloadSchema) {
|
|
24
|
+
const compiledPayload = payloadSchema !== undefined
|
|
25
|
+
? compilePayloadSchema(payloadSchema)
|
|
26
|
+
: undefined;
|
|
27
|
+
return {
|
|
28
|
+
name: "emit_stage_envelope",
|
|
29
|
+
label: "Emit stage envelope",
|
|
30
|
+
description: "Emit the shared stage handoff envelope. Call exactly once when the stage is complete. The pipeline cannot advance until this tool succeeds.",
|
|
31
|
+
parameters: Type.Object({
|
|
32
|
+
status: Type.Union([Type.Literal("success"), Type.Literal("failure")]),
|
|
33
|
+
summary: Type.String({ minLength: 1 }),
|
|
34
|
+
artifacts: Type.Array(Type.String()),
|
|
35
|
+
payload: compiledPayload !== undefined
|
|
36
|
+
? compiledPayload
|
|
37
|
+
: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
|
|
38
|
+
stage_id: Type.Optional(Type.String()),
|
|
39
|
+
notes: Type.Optional(Type.String()),
|
|
40
|
+
}),
|
|
41
|
+
execute: async (_toolCallId, params) => {
|
|
42
|
+
if (capture.envelope) {
|
|
43
|
+
return toolResult("Envelope already accepted; later emits are ignored.", { advancing: isAdvancingEnvelope(capture.envelope), error: "" }, { terminate: true });
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const envelope = assertRequiredEnvelope(params);
|
|
47
|
+
assertEnvelopePayload(envelope, payloadSchema);
|
|
48
|
+
capture.envelope = envelope;
|
|
49
|
+
const advancing = isAdvancingEnvelope(envelope);
|
|
50
|
+
return toolResult(advancing
|
|
51
|
+
? "Envelope accepted; stage may advance."
|
|
52
|
+
: "Envelope accepted with status=failure; pipeline will stop.", { advancing, error: "" }, { terminate: true });
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
+
capture.error = message;
|
|
57
|
+
return toolResult(`Invalid envelope: ${message}`, { advancing: false, error: message }, { isError: true });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
export type WriteStageArtifactOptions = {
|
|
3
|
+
runWorkspaceDir: string;
|
|
4
|
+
stageId: string;
|
|
5
|
+
attempt: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function createWriteStageArtifactTool(options: WriteStageArtifactOptions): {
|
|
8
|
+
name: string;
|
|
9
|
+
label: string;
|
|
10
|
+
description: string;
|
|
11
|
+
parameters: Type.TObject<{
|
|
12
|
+
path: Type.TString;
|
|
13
|
+
content: Type.TString;
|
|
14
|
+
}>;
|
|
15
|
+
execute: (_toolCallId: string, params: unknown) => Promise<{
|
|
16
|
+
content: {
|
|
17
|
+
type: "text";
|
|
18
|
+
text: string;
|
|
19
|
+
}[];
|
|
20
|
+
details: {
|
|
21
|
+
path: string;
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
isError?: boolean | undefined;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Pi tool for writing factory stage artifact files under the run
|
|
3
|
+
* workspace. Paths are relative to
|
|
4
|
+
* `stages/<stageId>/attempts/<attempt>/artifacts/` and must resolve inside
|
|
5
|
+
* that directory.
|
|
6
|
+
*
|
|
7
|
+
* Parameter schemas use `typebox` (Pi's `defineTool` TSchema), not
|
|
8
|
+
* `@sinclair/typebox`. Unlike emit, this tool does not terminate the turn.
|
|
9
|
+
*/
|
|
10
|
+
import { randomBytes } from "node:crypto";
|
|
11
|
+
import { lstat, realpath, rename, unlink, writeFile } from "node:fs/promises";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { Type } from "typebox";
|
|
14
|
+
import { ensureRealArtifactsDir, isInsideDir, resolveArtifactTarget, resolveContainedParent, } from "../runstore/workspaceLayout.js";
|
|
15
|
+
function toolResult(text, details, options) {
|
|
16
|
+
return {
|
|
17
|
+
content: [{ type: "text", text }],
|
|
18
|
+
details,
|
|
19
|
+
...(options?.isError ? { isError: true } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async function assertSafeWriteTarget(targetPath) {
|
|
23
|
+
try {
|
|
24
|
+
const st = await lstat(targetPath);
|
|
25
|
+
if (st.isSymbolicLink()) {
|
|
26
|
+
throw new Error("path escapes the stage artifacts directory");
|
|
27
|
+
}
|
|
28
|
+
if (st.nlink > 1) {
|
|
29
|
+
throw new Error("refusing to overwrite hard-linked file");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err.code !== "ENOENT") {
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function writeContainedArtifact(runWorkspaceDir, artifactsDirectory, absolutePath, content) {
|
|
39
|
+
const { realArtifacts } = await ensureRealArtifactsDir(runWorkspaceDir, artifactsDirectory);
|
|
40
|
+
const relativeToArtifacts = path.relative(artifactsDirectory, absolutePath);
|
|
41
|
+
const parent = await resolveContainedParent(realArtifacts, relativeToArtifacts);
|
|
42
|
+
const baseName = path.basename(absolutePath);
|
|
43
|
+
const targetPath = path.join(parent, baseName);
|
|
44
|
+
await assertSafeWriteTarget(targetPath);
|
|
45
|
+
const tmpName = `.sf-artifact-${randomBytes(8).toString("hex")}.tmp`;
|
|
46
|
+
const tmpPath = path.join(parent, tmpName);
|
|
47
|
+
try {
|
|
48
|
+
await writeFile(tmpPath, content, "utf8");
|
|
49
|
+
await rename(tmpPath, targetPath);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
try {
|
|
53
|
+
await unlink(tmpPath);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// best-effort cleanup
|
|
57
|
+
}
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
const realWritten = await realpath(targetPath);
|
|
61
|
+
if (!isInsideDir(realWritten, realArtifacts)) {
|
|
62
|
+
throw new Error("path escapes the stage artifacts directory");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function createWriteStageArtifactTool(options) {
|
|
66
|
+
const { runWorkspaceDir, stageId, attempt } = options;
|
|
67
|
+
return {
|
|
68
|
+
name: "write_stage_artifact",
|
|
69
|
+
label: "Write stage artifact",
|
|
70
|
+
description: "Write a factory artifact file under this stage attempt's artifacts directory in the run workspace. Pass a path relative to stages/<stageId>/attempts/<attempt>/artifacts/ and the file content. Returns the run-relative path to include in emit_stage_envelope artifacts.",
|
|
71
|
+
parameters: Type.Object({
|
|
72
|
+
path: Type.String({ minLength: 1 }),
|
|
73
|
+
content: Type.String(),
|
|
74
|
+
}),
|
|
75
|
+
execute: async (_toolCallId, params) => {
|
|
76
|
+
try {
|
|
77
|
+
if (params === null || typeof params !== "object") {
|
|
78
|
+
throw new Error("parameters must be an object");
|
|
79
|
+
}
|
|
80
|
+
const record = params;
|
|
81
|
+
if (typeof record.path !== "string") {
|
|
82
|
+
throw new Error("path must be a string");
|
|
83
|
+
}
|
|
84
|
+
if (typeof record.content !== "string") {
|
|
85
|
+
throw new Error("content must be a string");
|
|
86
|
+
}
|
|
87
|
+
const { absolutePath, runRelativePath, artifactsDir } = resolveArtifactTarget(runWorkspaceDir, stageId, attempt, record.path);
|
|
88
|
+
await writeContainedArtifact(runWorkspaceDir, artifactsDir, absolutePath, record.content);
|
|
89
|
+
return toolResult(`Wrote artifact: ${runRelativePath}`, {
|
|
90
|
+
path: runRelativePath,
|
|
91
|
+
error: "",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
96
|
+
return toolResult(`Failed to write artifact: ${message}`, {
|
|
97
|
+
path: "",
|
|
98
|
+
error: message,
|
|
99
|
+
}, { isError: true });
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type EnvelopeStatus = "success" | "failure";
|
|
2
|
+
export type StageEnvelope = {
|
|
3
|
+
status: EnvelopeStatus;
|
|
4
|
+
summary: string;
|
|
5
|
+
artifacts: string[];
|
|
6
|
+
payload?: Record<string, unknown>;
|
|
7
|
+
stage_id?: string;
|
|
8
|
+
notes?: string;
|
|
9
|
+
};
|
|
10
|
+
export declare class EnvelopeError extends Error {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type PipelineConfig = {
|
|
2
|
+
id: string;
|
|
3
|
+
stages: string[];
|
|
4
|
+
};
|
|
5
|
+
export type PipelineStageRef = {
|
|
6
|
+
id: string;
|
|
7
|
+
needs?: string;
|
|
8
|
+
};
|
|
9
|
+
export type PipelineStageYamlEntry = string | PipelineStageRef;
|
|
10
|
+
export type ResolvedPipelineStageNode = {
|
|
11
|
+
id: string;
|
|
12
|
+
needs: string | null;
|
|
13
|
+
ancestors: string[];
|
|
14
|
+
stageIndex: number;
|
|
15
|
+
};
|
|
16
|
+
export type ResolvedPipelineDag = {
|
|
17
|
+
nodes: ResolvedPipelineStageNode[];
|
|
18
|
+
roots: string[];
|
|
19
|
+
childrenOf: Record<string, string[]>;
|
|
20
|
+
};
|
|
21
|
+
export type LoadedPipeline = {
|
|
22
|
+
pipeline: PipelineConfig;
|
|
23
|
+
stages: import("./stage.js").StageConfig[];
|
|
24
|
+
dag: ResolvedPipelineDag;
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|