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,6 +1,6 @@
|
|
|
1
1
|
import type { ValidationResult } from "../config/validateCatalog.js";
|
|
2
|
-
export declare const VALIDATION_SCOPE_LINE = "Scope:
|
|
3
|
-
export declare const VALIDATION_CHECKS = "
|
|
2
|
+
export declare const VALIDATION_SCOPE_LINE = "Scope: catalog YAML (pipelines, stages, and tasks as selected by flags). Provider auth and checkout paths are not checked.";
|
|
3
|
+
export declare const VALIDATION_CHECKS = "catalog YAML (pipelines, stages, tasks as selected by flags)";
|
|
4
4
|
export declare function formatValidationHuman(result: ValidationResult, options?: {
|
|
5
5
|
strict?: boolean;
|
|
6
6
|
}): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { effectiveSeverity } from "../config/validateCatalog.js";
|
|
2
|
-
export const VALIDATION_SCOPE_LINE = "Scope:
|
|
3
|
-
export const VALIDATION_CHECKS = "
|
|
2
|
+
export const VALIDATION_SCOPE_LINE = "Scope: catalog YAML (pipelines, stages, and tasks as selected by flags). Provider auth and checkout paths are not checked.";
|
|
3
|
+
export const VALIDATION_CHECKS = "catalog YAML (pipelines, stages, tasks as selected by flags)";
|
|
4
4
|
function effectiveSeverityRank(finding, strict) {
|
|
5
5
|
return effectiveSeverity(finding, strict) === "error" ? 0 : 1;
|
|
6
6
|
}
|
package/dist/cli.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { OperatorCatalog } from "./runtime/stageAttemptBootstrap.js";
|
|
|
3
3
|
export declare function parseRunStageArgs(argv: string[]): {
|
|
4
4
|
runId: string;
|
|
5
5
|
stageId: string;
|
|
6
|
-
mode?: "run" | "resume";
|
|
6
|
+
mode?: "run" | "resume" | "feedback_resume" | "new_session";
|
|
7
7
|
resumeAnswer?: unknown;
|
|
8
8
|
attempt?: number;
|
|
9
9
|
sessionFilePath?: string;
|
package/dist/cli.js
CHANGED
|
@@ -3,13 +3,14 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { realpathSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
6
|
+
import { globalAgentBackendFromManifest, resolveAgentPort } from "./agent/resolveAgentPort.js";
|
|
7
7
|
import { ARTIFACT_USAGE, runArtifactCommand } from "./cli/artifactCommand.js";
|
|
8
8
|
import { ENVELOPE_USAGE, runEnvelopeCommand, } from "./cli/envelopeCommand.js";
|
|
9
9
|
import { EXPORT_RUN_USAGE, runExportRunCommand, } from "./cli/exportRunCommand.js";
|
|
10
10
|
import { INIT_USAGE, runInitCommand } from "./cli/initCommand.js";
|
|
11
11
|
import { PROVIDERS_USAGE, runProvidersCommand } from "./cli/providersCommand.js";
|
|
12
12
|
import { RUN_USAGE, runRunCommand } from "./cli/runCommand.js";
|
|
13
|
+
import { RUNS_USAGE, runRunsCommand } from "./cli/runsCommand.js";
|
|
13
14
|
import { resolveOperatorCatalog } from "./cli/operatorCatalog.js";
|
|
14
15
|
import { SKILLS_USAGE, runSkillsCommand } from "./cli/skillsCommand.js";
|
|
15
16
|
import { VALIDATE_USAGE, runValidateCommand } from "./cli/validateCommand.js";
|
|
@@ -20,6 +21,7 @@ import { SF_STAGE_WORKER } from "./runtime/stageWorkerProtocol.js";
|
|
|
20
21
|
import { DEFAULT_PORT, startUiServer } from "./server/http.js";
|
|
21
22
|
import { startMcpServer } from "./server/mcpHost.js";
|
|
22
23
|
import { resolveMcpStateless } from "./mcp/server.js";
|
|
24
|
+
import { PACKAGE_VERSION } from "./package-meta.js";
|
|
23
25
|
const USAGE = `Usage:
|
|
24
26
|
sf init
|
|
25
27
|
sf run --task <path> --pipeline <path> [--checkout <path>] [--json] [--include stages] [--skip-gates] [--git-sha <sha>] [--ci-pr-url <url>] [--ci-job-url <url>] [--operator-cwd <path>] [--operator-agent-dir <path>]
|
|
@@ -27,6 +29,16 @@ const USAGE = `Usage:
|
|
|
27
29
|
sf artifact read --run <runId> --path <relPath> [--out <file>]
|
|
28
30
|
sf envelope get --run <runId> --stage <stageId> [--json] [--from <sf-run.json>] [--detect-stage <id>] [--format envelope|handoff]
|
|
29
31
|
sf export-run --run <runId> [--from <sf-run.json>] [--out <file>]
|
|
32
|
+
sf runs list [--status created|running|succeeded|failed] [--since <iso>] [--pipeline <id-or-path>] [--json]
|
|
33
|
+
sf runs show --run <runId> [--from <sf-run.json>] [--json]
|
|
34
|
+
sf runs verify --run <runId> --stage <stageId> [--json]
|
|
35
|
+
sf runs recover --run <runId> --stage <stageId> [--guidance <text>] [--stop] [--json]
|
|
36
|
+
sf runs waiting [--run <runId>] [--json]
|
|
37
|
+
sf runs wait --run <runId> [--from <sf-run.json>] [--until any|waiting|terminal] [--timeout-ms <n>] [--json]
|
|
38
|
+
sf runs answer --run <runId> --stage <stageId> [--answer '<json>'] [--json]
|
|
39
|
+
sf runs retry --run <runId> --stage <stageId> [--json]
|
|
40
|
+
sf runs abandon --run <runId> --stage <stageId> [--json]
|
|
41
|
+
sf runs rerun --run <runId> [--json]
|
|
30
42
|
sf ui [--port ${DEFAULT_PORT}] [--mcp-stateless]
|
|
31
43
|
sf mcp [--port ${DEFAULT_PORT}] [--mcp-stateless]
|
|
32
44
|
sf providers list
|
|
@@ -38,6 +50,8 @@ const USAGE = `Usage:
|
|
|
38
50
|
sf skills list
|
|
39
51
|
sf skills install --from-path <dir> [--skill-name <name>]
|
|
40
52
|
sf skills install --from-zip <url-or-path> [--skill-name <name>] [--checksum sha256:<hex>]
|
|
53
|
+
sf --version
|
|
54
|
+
sf -V
|
|
41
55
|
sf --help
|
|
42
56
|
|
|
43
57
|
Stageflow (sf) runs YAML-defined automatic stage pipelines.
|
|
@@ -56,6 +70,8 @@ ${ENVELOPE_USAGE}
|
|
|
56
70
|
|
|
57
71
|
${EXPORT_RUN_USAGE}
|
|
58
72
|
|
|
73
|
+
${RUNS_USAGE}
|
|
74
|
+
|
|
59
75
|
${PROVIDERS_USAGE}
|
|
60
76
|
|
|
61
77
|
${SKILLS_USAGE}`;
|
|
@@ -68,6 +84,12 @@ function parseArgs(argv) {
|
|
|
68
84
|
args.length === 1) {
|
|
69
85
|
return { help: true };
|
|
70
86
|
}
|
|
87
|
+
if (args[0] === "--version" || args[0] === "-V") {
|
|
88
|
+
if (args.length > 1) {
|
|
89
|
+
throw new Error(`Unexpected argument: ${args[1]}`);
|
|
90
|
+
}
|
|
91
|
+
return { help: false, version: true };
|
|
92
|
+
}
|
|
71
93
|
const command = args[0];
|
|
72
94
|
if (command === "providers" ||
|
|
73
95
|
command === "validate" ||
|
|
@@ -76,6 +98,7 @@ function parseArgs(argv) {
|
|
|
76
98
|
command === "artifact" ||
|
|
77
99
|
command === "envelope" ||
|
|
78
100
|
command === "export-run" ||
|
|
101
|
+
command === "runs" ||
|
|
79
102
|
command === "skills") {
|
|
80
103
|
return { help: false, command };
|
|
81
104
|
}
|
|
@@ -133,8 +156,11 @@ export function parseRunStageArgs(argv) {
|
|
|
133
156
|
}
|
|
134
157
|
else if (argv[i] === "--mode") {
|
|
135
158
|
const raw = argv[++i];
|
|
136
|
-
if (raw !== "run" &&
|
|
137
|
-
|
|
159
|
+
if (raw !== "run" &&
|
|
160
|
+
raw !== "resume" &&
|
|
161
|
+
raw !== "feedback_resume" &&
|
|
162
|
+
raw !== "new_session") {
|
|
163
|
+
throw new Error("--mode must be run, resume, feedback_resume, or new_session");
|
|
138
164
|
}
|
|
139
165
|
mode = raw;
|
|
140
166
|
}
|
|
@@ -240,6 +266,10 @@ async function main(argv) {
|
|
|
240
266
|
console.log(USAGE);
|
|
241
267
|
return argv.slice(2).length === 0 ? 1 : 0;
|
|
242
268
|
}
|
|
269
|
+
if (parsed.version) {
|
|
270
|
+
console.log(PACKAGE_VERSION);
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
243
273
|
const ctx = await resolveStageflowContext(process.cwd());
|
|
244
274
|
if (parsed.command === "init") {
|
|
245
275
|
return runInitCommand(argv.slice(3), { cwd: ctx.invocationCwd });
|
|
@@ -272,6 +302,13 @@ async function main(argv) {
|
|
|
272
302
|
projectRoot: ctx.projectRoot,
|
|
273
303
|
});
|
|
274
304
|
}
|
|
305
|
+
if (parsed.command === "runs") {
|
|
306
|
+
return runRunsCommand(argv.slice(3), {
|
|
307
|
+
cwd: ctx.invocationCwd,
|
|
308
|
+
projectRoot: ctx.projectRoot,
|
|
309
|
+
isGitProject: ctx.isGitProject,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
275
312
|
if (parsed.command === "providers") {
|
|
276
313
|
return runProvidersCommand(argv.slice(3), ctx.invocationCwd);
|
|
277
314
|
}
|
|
@@ -282,12 +319,13 @@ async function main(argv) {
|
|
|
282
319
|
});
|
|
283
320
|
}
|
|
284
321
|
const store = createRunStore({ rootDir: ctx.projectRoot });
|
|
322
|
+
const globalAgent = globalAgentBackendFromManifest(ctx.manifest);
|
|
285
323
|
if (parsed.command === "ui") {
|
|
286
324
|
const mcpStateless = resolveMcpStateless({
|
|
287
325
|
mcpStateless: parsed.mcpStateless,
|
|
288
326
|
});
|
|
289
327
|
const { url, mcpUrl } = await startUiServer({
|
|
290
|
-
agent:
|
|
328
|
+
agent: resolveAgentPort({ global: globalAgent }),
|
|
291
329
|
store,
|
|
292
330
|
cwd: ctx.invocationCwd,
|
|
293
331
|
rootDir: ctx.projectRoot,
|
|
@@ -305,7 +343,7 @@ async function main(argv) {
|
|
|
305
343
|
mcpStateless: parsed.mcpStateless,
|
|
306
344
|
});
|
|
307
345
|
const { mcpUrl } = await startMcpServer({
|
|
308
|
-
agent:
|
|
346
|
+
agent: resolveAgentPort({ global: globalAgent }),
|
|
309
347
|
store,
|
|
310
348
|
cwd: ctx.invocationCwd,
|
|
311
349
|
rootDir: ctx.projectRoot,
|
|
@@ -14,7 +14,7 @@ function repoRelPath(projectRoot, absPath) {
|
|
|
14
14
|
function mapStageListing(stage, source, projectRoot) {
|
|
15
15
|
const listing = {
|
|
16
16
|
id: stage.id,
|
|
17
|
-
...(stage.gate_kinds ? { gate_kinds: stage.gate_kinds } : {}),
|
|
17
|
+
...(stage.gate_kinds !== undefined ? { gate_kinds: stage.gate_kinds } : {}),
|
|
18
18
|
};
|
|
19
19
|
if (source?.kind === "inline") {
|
|
20
20
|
listing.inline = true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { PipelineNeeds } from "../types/pipeline.js";
|
|
1
2
|
import type { StageGateKind } from "../types/stage.js";
|
|
2
3
|
import type { PipelineListing } from "./listConfig.js";
|
|
3
4
|
export type CreatePipelineStageInline = {
|
|
@@ -7,7 +8,7 @@ export type CreatePipelineStageInline = {
|
|
|
7
8
|
};
|
|
8
9
|
export type CreatePipelineStageRef = {
|
|
9
10
|
id: string;
|
|
10
|
-
needs?:
|
|
11
|
+
needs?: PipelineNeeds;
|
|
11
12
|
uses?: string;
|
|
12
13
|
inline?: CreatePipelineStageInline;
|
|
13
14
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { access, mkdir, readdir, unlink, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { parsePipelineNeeds } from "./pipelineNeeds.js";
|
|
3
4
|
import { STAGE_ID_PATTERN } from "./createStage.js";
|
|
4
5
|
import { inferIdFromUsesPath, normalizePipelineStageEntries, toWiringRefs, } from "./normalizePipelineStageEntry.js";
|
|
5
6
|
import { readYamlObject } from "./readYamlObject.js";
|
|
@@ -149,14 +150,15 @@ function parseStageEntry(entry, index) {
|
|
|
149
150
|
}
|
|
150
151
|
const ref = { id };
|
|
151
152
|
if (entry.needs !== undefined) {
|
|
152
|
-
|
|
153
|
+
const parsedNeeds = parsePipelineNeeds(entry.needs, id);
|
|
154
|
+
if (!parsedNeeds.ok) {
|
|
153
155
|
return {
|
|
154
156
|
ok: false,
|
|
155
157
|
status: 400,
|
|
156
|
-
error: `stages[${index}]
|
|
158
|
+
error: parsedNeeds.message.replace(/^stage "[^"]+": /, `stages[${index}].`),
|
|
157
159
|
};
|
|
158
160
|
}
|
|
159
|
-
ref.needs =
|
|
161
|
+
ref.needs = parsedNeeds.value;
|
|
160
162
|
}
|
|
161
163
|
if (entry.uses !== undefined) {
|
|
162
164
|
if (typeof entry.uses !== "string" || !entry.uses.trim()) {
|
|
@@ -224,6 +226,24 @@ export function parseCreatePipelineBody(body) {
|
|
|
224
226
|
stages: refs,
|
|
225
227
|
};
|
|
226
228
|
}
|
|
229
|
+
function appendNeedsYaml(lines, needs) {
|
|
230
|
+
if (typeof needs === "string") {
|
|
231
|
+
lines.push(` needs: ${needs}`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
lines.push(" needs:");
|
|
235
|
+
for (const edge of needs) {
|
|
236
|
+
if (edge.on.length === 1 && edge.on[0] === "succeeded") {
|
|
237
|
+
lines.push(` - ${edge.id}`);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
lines.push(` - id: ${edge.id}`);
|
|
241
|
+
lines.push(" on:");
|
|
242
|
+
for (const state of edge.on) {
|
|
243
|
+
lines.push(` - ${state}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
227
247
|
function formatInlineYamlScalar(value) {
|
|
228
248
|
if (value.includes("\n") ||
|
|
229
249
|
/[:#"'&*!|>@`[\]{}]/.test(value) ||
|
|
@@ -245,13 +265,18 @@ export function pipelineConfigToYaml(pipeline, options) {
|
|
|
245
265
|
}
|
|
246
266
|
lines.push(` - id: ${stage.id}`);
|
|
247
267
|
if (stage.needs) {
|
|
248
|
-
lines
|
|
268
|
+
appendNeedsYaml(lines, stage.needs);
|
|
249
269
|
}
|
|
250
270
|
if (stage.inline) {
|
|
251
|
-
if (stage.inline.gate_kinds
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
271
|
+
if (stage.inline.gate_kinds !== undefined) {
|
|
272
|
+
if (stage.inline.gate_kinds.length === 0) {
|
|
273
|
+
lines.push(" gate_kinds: []");
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
lines.push(" gate_kinds:");
|
|
277
|
+
for (const kind of stage.inline.gate_kinds) {
|
|
278
|
+
lines.push(` - ${kind}`);
|
|
279
|
+
}
|
|
255
280
|
}
|
|
256
281
|
}
|
|
257
282
|
if (stage.inline.system_prompt.includes("\n")) {
|
|
@@ -364,7 +389,7 @@ function buildPipelineListing(projectRoot, relPath, loaded) {
|
|
|
364
389
|
const source = loaded.stageSources?.[stage.id];
|
|
365
390
|
const listing = {
|
|
366
391
|
id: stage.id,
|
|
367
|
-
...(stage.gate_kinds ? { gate_kinds: stage.gate_kinds } : {}),
|
|
392
|
+
...(stage.gate_kinds !== undefined ? { gate_kinds: stage.gate_kinds } : {}),
|
|
368
393
|
};
|
|
369
394
|
if (source?.kind === "inline") {
|
|
370
395
|
return { ...listing, inline: true };
|
|
@@ -22,7 +22,7 @@ function validateStageId(id) {
|
|
|
22
22
|
function parseGateKinds(raw) {
|
|
23
23
|
if (raw === undefined)
|
|
24
24
|
return undefined;
|
|
25
|
-
if (!Array.isArray(raw)
|
|
25
|
+
if (!Array.isArray(raw))
|
|
26
26
|
return "invalid";
|
|
27
27
|
const kinds = [];
|
|
28
28
|
for (const item of raw) {
|
|
@@ -71,7 +71,7 @@ export function parseCreateStageBody(body) {
|
|
|
71
71
|
return {
|
|
72
72
|
ok: false,
|
|
73
73
|
status: 400,
|
|
74
|
-
error: `gate_kinds must be
|
|
74
|
+
error: `gate_kinds must be an array of: ${STAGE_GATE_KINDS.join(", ")}`,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
return {
|
|
@@ -80,7 +80,7 @@ export function parseCreateStageBody(body) {
|
|
|
80
80
|
id: body.id,
|
|
81
81
|
system_prompt: body.system_prompt,
|
|
82
82
|
model: body.model,
|
|
83
|
-
...(gateKinds ? { gate_kinds: gateKinds } : {}),
|
|
83
|
+
...(gateKinds !== undefined ? { gate_kinds: gateKinds } : {}),
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
function formatInlineYamlScalar(value) {
|
|
@@ -96,10 +96,15 @@ function formatInlineYamlScalar(value) {
|
|
|
96
96
|
}
|
|
97
97
|
export function stageConfigToYaml(stage) {
|
|
98
98
|
const lines = [`id: ${stage.id}`];
|
|
99
|
-
if (stage.gate_kinds
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
if (stage.gate_kinds !== undefined) {
|
|
100
|
+
if (stage.gate_kinds.length === 0) {
|
|
101
|
+
lines.push("gate_kinds: []");
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
lines.push("gate_kinds:");
|
|
105
|
+
for (const kind of stage.gate_kinds) {
|
|
106
|
+
lines.push(` - ${kind}`);
|
|
107
|
+
}
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
if (stage.system_prompt.includes("\n")) {
|
|
@@ -186,7 +191,7 @@ export async function createStage(projectRoot, input) {
|
|
|
186
191
|
id: input.id,
|
|
187
192
|
system_prompt: input.system_prompt,
|
|
188
193
|
model: input.model,
|
|
189
|
-
...(input.gate_kinds ? { gate_kinds: input.gate_kinds } : {}),
|
|
194
|
+
...(input.gate_kinds !== undefined ? { gate_kinds: input.gate_kinds } : {}),
|
|
190
195
|
}), "utf8");
|
|
191
196
|
}
|
|
192
197
|
catch (err) {
|
|
@@ -200,7 +205,7 @@ export async function createStage(projectRoot, input) {
|
|
|
200
205
|
stage: {
|
|
201
206
|
path: relPath,
|
|
202
207
|
id: stage.id,
|
|
203
|
-
...(stage.gate_kinds ? { gate_kinds: stage.gate_kinds } : {}),
|
|
208
|
+
...(stage.gate_kinds !== undefined ? { gate_kinds: stage.gate_kinds } : {}),
|
|
204
209
|
},
|
|
205
210
|
};
|
|
206
211
|
}
|
|
@@ -5,6 +5,7 @@ import { mergePipelineStages } from "./mergePipelineIncludes.js";
|
|
|
5
5
|
import { normalizePipelineStageEntries, toWiringRefs, } from "./normalizePipelineStageEntry.js";
|
|
6
6
|
import { loadStageFromObjectOutcome, loadStageOutcome } from "./loadStage.js";
|
|
7
7
|
import { resolvePipelineDagFromRefs } from "./resolvePipelineDag.js";
|
|
8
|
+
import { validateCompletionContractForStage } from "./validateCompletionContract.js";
|
|
8
9
|
export async function resolvePipelinePath(pipelinePath, cwd) {
|
|
9
10
|
const resolved = path.normalize(path.resolve(cwd, pipelinePath));
|
|
10
11
|
try {
|
|
@@ -21,7 +22,7 @@ async function loadPipelineFromPath(pipelinePath) {
|
|
|
21
22
|
if (!mergeOutcome.ok) {
|
|
22
23
|
return loadFailure(mergeOutcome.issues);
|
|
23
24
|
}
|
|
24
|
-
const { entries: rawEntries, pipelineId } = mergeOutcome.value;
|
|
25
|
+
const { entries: rawEntries, pipelineId, agent: pipelineAgent } = mergeOutcome.value;
|
|
25
26
|
const ctx = { pipelineId, path: normalizedPipelinePath };
|
|
26
27
|
const normalizeOutcome = normalizePipelineStageEntries(rawEntries, ctx);
|
|
27
28
|
if (!normalizeOutcome.ok) {
|
|
@@ -106,7 +107,15 @@ async function loadPipelineFromPath(pipelinePath) {
|
|
|
106
107
|
const pipeline = {
|
|
107
108
|
id: pipelineId,
|
|
108
109
|
stages: stageIds,
|
|
110
|
+
...(pipelineAgent !== undefined ? { agent: pipelineAgent } : {}),
|
|
109
111
|
};
|
|
112
|
+
const nodeById = new Map(dag.nodes.map((node) => [node.id, node]));
|
|
113
|
+
for (const stage of stages) {
|
|
114
|
+
const completion = nodeById.get(stage.id)?.completion;
|
|
115
|
+
const completionOutcome = validateCompletionContractForStage(stage, completion);
|
|
116
|
+
if (!completionOutcome.ok)
|
|
117
|
+
return loadFailure(completionOutcome.issues);
|
|
118
|
+
}
|
|
110
119
|
return loadSuccess({
|
|
111
120
|
pipeline,
|
|
112
121
|
stages,
|
package/dist/config/loadStage.js
CHANGED
|
@@ -1,10 +1,69 @@
|
|
|
1
|
+
import { parseAgentField } from "../agent/agentBackend.js";
|
|
2
|
+
import { CLONE_ACTIONS } from "../types/forkChoice.js";
|
|
1
3
|
import { STAGE_GATE_KINDS, } from "../types/stage.js";
|
|
2
4
|
import { compilePayloadSchema } from "../envelope/payloadSchema.js";
|
|
3
5
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
6
|
+
import { parsePreEmitChecks } from "./parsePreEmitChecks.js";
|
|
4
7
|
import { readYamlObject } from "./readYamlObject.js";
|
|
5
8
|
function isGateKind(value) {
|
|
6
9
|
return STAGE_GATE_KINDS.includes(value);
|
|
7
10
|
}
|
|
11
|
+
function isCloneAction(value) {
|
|
12
|
+
return CLONE_ACTIONS.includes(value);
|
|
13
|
+
}
|
|
14
|
+
function parseCloneActions(raw, label) {
|
|
15
|
+
if (raw === undefined)
|
|
16
|
+
return loadSuccess(undefined);
|
|
17
|
+
if (!Array.isArray(raw) || !raw.every((item) => typeof item === "string")) {
|
|
18
|
+
return loadFailure([
|
|
19
|
+
{
|
|
20
|
+
code: "stage.invalid_clone_actions",
|
|
21
|
+
message: `Invalid stage ${label}: clone_actions must be a non-empty array of strings`,
|
|
22
|
+
category: "stage",
|
|
23
|
+
},
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
if (raw.length === 0) {
|
|
27
|
+
return loadFailure([
|
|
28
|
+
{
|
|
29
|
+
code: "stage.invalid_clone_actions",
|
|
30
|
+
message: `Invalid stage ${label}: clone_actions must not be empty`,
|
|
31
|
+
category: "stage",
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
const actions = [];
|
|
36
|
+
for (const item of raw) {
|
|
37
|
+
if (!isCloneAction(item)) {
|
|
38
|
+
return loadFailure([
|
|
39
|
+
{
|
|
40
|
+
code: "stage.invalid_clone_actions",
|
|
41
|
+
message: `Invalid stage ${label}: unsupported clone_actions value "${item}" (allowed: ${CLONE_ACTIONS.join(", ")})`,
|
|
42
|
+
category: "stage",
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
}
|
|
46
|
+
actions.push(item);
|
|
47
|
+
}
|
|
48
|
+
return loadSuccess(actions);
|
|
49
|
+
}
|
|
50
|
+
function parseTimeoutMs(raw, label) {
|
|
51
|
+
if (raw === undefined)
|
|
52
|
+
return loadSuccess(undefined);
|
|
53
|
+
if (typeof raw !== "number" ||
|
|
54
|
+
!Number.isFinite(raw) ||
|
|
55
|
+
!Number.isInteger(raw) ||
|
|
56
|
+
raw <= 0) {
|
|
57
|
+
return loadFailure([
|
|
58
|
+
{
|
|
59
|
+
code: "stage.invalid_timeout_ms",
|
|
60
|
+
message: `Invalid stage ${label}: timeout_ms must be a positive integer (milliseconds)`,
|
|
61
|
+
category: "stage",
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
65
|
+
return loadSuccess(raw);
|
|
66
|
+
}
|
|
8
67
|
function parseGateKinds(raw, label) {
|
|
9
68
|
if (raw === undefined)
|
|
10
69
|
return loadSuccess(undefined);
|
|
@@ -30,7 +89,7 @@ function parseGateKinds(raw, label) {
|
|
|
30
89
|
}
|
|
31
90
|
kinds.push(item);
|
|
32
91
|
}
|
|
33
|
-
return loadSuccess(kinds
|
|
92
|
+
return loadSuccess(kinds);
|
|
34
93
|
}
|
|
35
94
|
function parseStageFields(raw, label, entryId) {
|
|
36
95
|
if (typeof raw.system_prompt !== "string" ||
|
|
@@ -78,6 +137,46 @@ function parseStageFields(raw, label, entryId) {
|
|
|
78
137
|
}
|
|
79
138
|
stage.payload_schema = raw.payload_schema;
|
|
80
139
|
}
|
|
140
|
+
if (raw.clone_input_schema !== undefined) {
|
|
141
|
+
if (raw.clone_input_schema === null ||
|
|
142
|
+
typeof raw.clone_input_schema !== "object" ||
|
|
143
|
+
Array.isArray(raw.clone_input_schema)) {
|
|
144
|
+
return loadFailure([
|
|
145
|
+
{
|
|
146
|
+
code: "stage.invalid_clone_input_schema",
|
|
147
|
+
message: `Invalid stage ${label}: clone_input_schema must be an object`,
|
|
148
|
+
category: "stage",
|
|
149
|
+
stageId: entryId,
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
compilePayloadSchema(raw.clone_input_schema);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
158
|
+
return loadFailure([
|
|
159
|
+
{
|
|
160
|
+
code: "stage.invalid_clone_input_schema",
|
|
161
|
+
message: `Invalid stage ${label}: invalid clone_input_schema: ${message}`,
|
|
162
|
+
category: "stage",
|
|
163
|
+
stageId: entryId,
|
|
164
|
+
},
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
stage.clone_input_schema = raw.clone_input_schema;
|
|
168
|
+
}
|
|
169
|
+
const cloneActionsOutcome = parseCloneActions(raw.clone_actions, label);
|
|
170
|
+
if (!cloneActionsOutcome.ok) {
|
|
171
|
+
const issues = cloneActionsOutcome.issues.map((issue) => ({
|
|
172
|
+
...issue,
|
|
173
|
+
stageId: entryId,
|
|
174
|
+
}));
|
|
175
|
+
return loadFailure(issues);
|
|
176
|
+
}
|
|
177
|
+
if (cloneActionsOutcome.value !== undefined) {
|
|
178
|
+
stage.clone_actions = cloneActionsOutcome.value;
|
|
179
|
+
}
|
|
81
180
|
const gateKindsOutcome = parseGateKinds(raw.gate_kinds, label);
|
|
82
181
|
if (!gateKindsOutcome.ok) {
|
|
83
182
|
const issues = gateKindsOutcome.issues.map((issue) => ({
|
|
@@ -86,8 +185,31 @@ function parseStageFields(raw, label, entryId) {
|
|
|
86
185
|
}));
|
|
87
186
|
return loadFailure(issues);
|
|
88
187
|
}
|
|
89
|
-
if (gateKindsOutcome.value)
|
|
188
|
+
if (gateKindsOutcome.value !== undefined) {
|
|
90
189
|
stage.gate_kinds = gateKindsOutcome.value;
|
|
190
|
+
}
|
|
191
|
+
const timeoutMsOutcome = parseTimeoutMs(raw.timeout_ms, label);
|
|
192
|
+
if (!timeoutMsOutcome.ok) {
|
|
193
|
+
const issues = timeoutMsOutcome.issues.map((issue) => ({
|
|
194
|
+
...issue,
|
|
195
|
+
stageId: entryId,
|
|
196
|
+
}));
|
|
197
|
+
return loadFailure(issues);
|
|
198
|
+
}
|
|
199
|
+
if (timeoutMsOutcome.value !== undefined) {
|
|
200
|
+
stage.timeout_ms = timeoutMsOutcome.value;
|
|
201
|
+
}
|
|
202
|
+
const preEmitChecksOutcome = parsePreEmitChecks(raw.pre_emit_checks, label);
|
|
203
|
+
if (!preEmitChecksOutcome.ok) {
|
|
204
|
+
const issues = preEmitChecksOutcome.issues.map((issue) => ({
|
|
205
|
+
...issue,
|
|
206
|
+
stageId: entryId,
|
|
207
|
+
}));
|
|
208
|
+
return loadFailure(issues);
|
|
209
|
+
}
|
|
210
|
+
if (preEmitChecksOutcome.value !== undefined) {
|
|
211
|
+
stage.pre_emit_checks = preEmitChecksOutcome.value;
|
|
212
|
+
}
|
|
91
213
|
if (raw.skill !== undefined) {
|
|
92
214
|
if (typeof raw.skill !== "string" || raw.skill.trim() === "") {
|
|
93
215
|
return loadFailure([
|
|
@@ -101,6 +223,20 @@ function parseStageFields(raw, label, entryId) {
|
|
|
101
223
|
}
|
|
102
224
|
stage.skill = raw.skill.trim();
|
|
103
225
|
}
|
|
226
|
+
const agentField = parseAgentField(raw.agent);
|
|
227
|
+
if (!agentField.ok) {
|
|
228
|
+
return loadFailure([
|
|
229
|
+
{
|
|
230
|
+
code: "stage.invalid_agent",
|
|
231
|
+
message: `Invalid stage ${label}: ${agentField.message}`,
|
|
232
|
+
category: "stage",
|
|
233
|
+
stageId: entryId,
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
}
|
|
237
|
+
if (agentField.value !== undefined) {
|
|
238
|
+
stage.agent = agentField.value;
|
|
239
|
+
}
|
|
104
240
|
return loadSuccess(stage);
|
|
105
241
|
}
|
|
106
242
|
export function loadStageFromObjectOutcome(raw, ctx) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { access, readFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { parseAgentField } from "../agent/agentBackend.js";
|
|
4
5
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
5
6
|
const DEFAULT_PIPELINE_PATTERN = "*.pipeline.yaml";
|
|
6
7
|
const DEFAULT_TASK_PATTERN = "*.task.yaml";
|
|
@@ -108,6 +109,14 @@ export function parseStageflowManifestOutcome(yamlText, manifestPath, projectRoo
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
const patterns = parsePatterns(catalogRecord.patterns, issues);
|
|
112
|
+
let agent;
|
|
113
|
+
const agentField = parseAgentField(record.agent);
|
|
114
|
+
if (!agentField.ok) {
|
|
115
|
+
issues.push(catalogIssue("catalog.manifest_invalid", agentField.message));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
agent = agentField.value;
|
|
119
|
+
}
|
|
111
120
|
if (issues.length > 0) {
|
|
112
121
|
return loadFailure(issues);
|
|
113
122
|
}
|
|
@@ -127,7 +136,11 @@ export function parseStageflowManifestOutcome(yamlText, manifestPath, projectRoo
|
|
|
127
136
|
catalog.patterns.task = patterns.task;
|
|
128
137
|
}
|
|
129
138
|
}
|
|
130
|
-
const manifest = {
|
|
139
|
+
const manifest = {
|
|
140
|
+
version: 1,
|
|
141
|
+
catalog,
|
|
142
|
+
...(agent !== undefined ? { agent } : {}),
|
|
143
|
+
};
|
|
131
144
|
return loadSuccess({
|
|
132
145
|
path: manifestPath,
|
|
133
146
|
projectRoot,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { parseAgentField } from "../agent/agentBackend.js";
|
|
2
3
|
import { loadFailure, loadSuccess } from "./loadOutcome.js";
|
|
3
4
|
import { readYamlObject } from "./readYamlObject.js";
|
|
4
5
|
function isPlainObject(value) {
|
|
@@ -123,6 +124,18 @@ export async function mergePipelineStages(rootPath) {
|
|
|
123
124
|
]);
|
|
124
125
|
}
|
|
125
126
|
const pipelineId = raw.id;
|
|
127
|
+
const agentField = parseAgentField(raw.agent);
|
|
128
|
+
if (!agentField.ok) {
|
|
129
|
+
return loadFailure([
|
|
130
|
+
{
|
|
131
|
+
code: "pipeline.invalid_agent",
|
|
132
|
+
message: `Invalid pipeline ${absRoot}: ${agentField.message}`,
|
|
133
|
+
category: "pipeline",
|
|
134
|
+
pipelineId,
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
const agent = agentField.value;
|
|
126
139
|
const idLocations = new Map();
|
|
127
140
|
const entries = [];
|
|
128
141
|
const mergeResult = await visitPipelineFile(absRoot, [], idLocations, entries);
|
|
@@ -137,5 +150,5 @@ export async function mergePipelineStages(rootPath) {
|
|
|
137
150
|
},
|
|
138
151
|
]);
|
|
139
152
|
}
|
|
140
|
-
return loadSuccess({ entries, pipelineId });
|
|
153
|
+
return loadSuccess({ entries, pipelineId, ...(agent !== undefined ? { agent } : {}) });
|
|
141
154
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CompletionContract, RecoveryPolicy } from "../types/completion.js";
|
|
2
|
+
import type { NormalizedPipelineStageEntry, PipelineNeeds } from "../types/pipeline.js";
|
|
2
3
|
import { type LoadOutcome } from "./loadOutcome.js";
|
|
3
4
|
import type { RawMergedEntry } from "./mergePipelineIncludes.js";
|
|
4
5
|
export declare function inferIdFromUsesPath(usesPath: string): string | null;
|
|
@@ -8,11 +9,15 @@ export declare function normalizePipelineStageEntries(rawEntries: RawMergedEntry
|
|
|
8
9
|
}): LoadOutcome<NormalizedPipelineStageEntry[]>;
|
|
9
10
|
export declare function toWiringRefs(entries: NormalizedPipelineStageEntry[]): Array<{
|
|
10
11
|
id: string;
|
|
11
|
-
needs?:
|
|
12
|
+
needs?: PipelineNeeds;
|
|
12
13
|
fork?: {
|
|
13
14
|
select: "one" | "subset";
|
|
14
15
|
allow_none?: boolean;
|
|
15
16
|
};
|
|
16
17
|
clonable?: boolean;
|
|
17
18
|
clone_cap?: number;
|
|
19
|
+
completion?: CompletionContract;
|
|
20
|
+
recovery?: RecoveryPolicy;
|
|
21
|
+
feedback_loop?: NormalizedPipelineStageEntry["feedback_loop"];
|
|
22
|
+
replay_safe?: boolean;
|
|
18
23
|
}>;
|