taskflow-core 0.2.0 → 0.2.2
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 +247 -862
- package/dist/agents.d.ts +12 -0
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +29 -1
- package/dist/agents.js.map +1 -1
- package/dist/build-info.d.ts +48 -0
- package/dist/build-info.d.ts.map +1 -0
- package/dist/build-info.js +112 -0
- package/dist/build-info.js.map +1 -0
- package/dist/build-info.json +4 -0
- package/dist/cwd-bridge.d.ts +61 -0
- package/dist/cwd-bridge.d.ts.map +1 -0
- package/dist/cwd-bridge.js +136 -0
- package/dist/cwd-bridge.js.map +1 -0
- package/dist/detached-runner.js +31 -5
- package/dist/detached-runner.js.map +1 -1
- package/dist/exec/driver.d.ts +6 -0
- package/dist/exec/driver.d.ts.map +1 -1
- package/dist/exec/driver.js +40 -18
- package/dist/exec/driver.js.map +1 -1
- package/dist/exec/kernel-policy.d.ts.map +1 -1
- package/dist/exec/kernel-policy.js +10 -0
- package/dist/exec/kernel-policy.js.map +1 -1
- package/dist/exec/step-kinds.d.ts +14 -1
- package/dist/exec/step-kinds.d.ts.map +1 -1
- package/dist/exec/step-kinds.js +126 -6
- package/dist/exec/step-kinds.js.map +1 -1
- package/dist/exec/step.d.ts +22 -0
- package/dist/exec/step.d.ts.map +1 -1
- package/dist/exec/step.js +40 -5
- package/dist/exec/step.js.map +1 -1
- package/dist/final-output.d.ts +53 -0
- package/dist/final-output.d.ts.map +1 -0
- package/dist/final-output.js +65 -0
- package/dist/final-output.js.map +1 -0
- package/dist/flowir/canonical-hash.d.ts.map +1 -1
- package/dist/flowir/canonical-hash.js +2 -0
- package/dist/flowir/canonical-hash.js.map +1 -1
- package/dist/flowir/compile.d.ts.map +1 -1
- package/dist/flowir/compile.js +17 -0
- package/dist/flowir/compile.js.map +1 -1
- package/dist/flowir/meta.d.ts +3 -0
- package/dist/flowir/meta.d.ts.map +1 -1
- package/dist/flowir/schema.d.ts +3 -0
- package/dist/flowir/schema.d.ts.map +1 -1
- package/dist/flowir/schema.js.map +1 -1
- package/dist/flowir/translate.d.ts.map +1 -1
- package/dist/flowir/translate.js +4 -0
- package/dist/flowir/translate.js.map +1 -1
- package/dist/host/runner-types.d.ts +16 -0
- package/dist/host/runner-types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/interpolate.d.ts +4 -0
- package/dist/interpolate.d.ts.map +1 -1
- package/dist/interpolate.js +14 -0
- package/dist/interpolate.js.map +1 -1
- package/dist/resources/authority.d.ts +35 -0
- package/dist/resources/authority.d.ts.map +1 -0
- package/dist/resources/authority.js +67 -0
- package/dist/resources/authority.js.map +1 -0
- package/dist/resources/backend.d.ts +302 -0
- package/dist/resources/backend.d.ts.map +1 -0
- package/dist/resources/backend.js +16 -0
- package/dist/resources/backend.js.map +1 -0
- package/dist/resources/baseline.d.ts +116 -0
- package/dist/resources/baseline.d.ts.map +1 -0
- package/dist/resources/baseline.js +447 -0
- package/dist/resources/baseline.js.map +1 -0
- package/dist/resources/canonical-json.d.ts +7 -0
- package/dist/resources/canonical-json.d.ts.map +1 -0
- package/dist/resources/canonical-json.js +75 -0
- package/dist/resources/canonical-json.js.map +1 -0
- package/dist/resources/errors.d.ts +23 -0
- package/dist/resources/errors.d.ts.map +1 -0
- package/dist/resources/errors.js +89 -0
- package/dist/resources/errors.js.map +1 -0
- package/dist/resources/execution.d.ts +90 -0
- package/dist/resources/execution.d.ts.map +1 -0
- package/dist/resources/execution.js +581 -0
- package/dist/resources/execution.js.map +1 -0
- package/dist/resources/index.d.ts +15 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +15 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/journal.d.ts +138 -0
- package/dist/resources/journal.d.ts.map +1 -0
- package/dist/resources/journal.js +438 -0
- package/dist/resources/journal.js.map +1 -0
- package/dist/resources/leases.d.ts +51 -0
- package/dist/resources/leases.d.ts.map +1 -0
- package/dist/resources/leases.js +354 -0
- package/dist/resources/leases.js.map +1 -0
- package/dist/resources/permits.d.ts +52 -0
- package/dist/resources/permits.d.ts.map +1 -0
- package/dist/resources/permits.js +240 -0
- package/dist/resources/permits.js.map +1 -0
- package/dist/resources/persistence.d.ts +63 -0
- package/dist/resources/persistence.d.ts.map +1 -0
- package/dist/resources/persistence.js +522 -0
- package/dist/resources/persistence.js.map +1 -0
- package/dist/resources/registry.d.ts +56 -0
- package/dist/resources/registry.d.ts.map +1 -0
- package/dist/resources/registry.js +139 -0
- package/dist/resources/registry.js.map +1 -0
- package/dist/resources/resolve.d.ts +49 -0
- package/dist/resources/resolve.d.ts.map +1 -0
- package/dist/resources/resolve.js +309 -0
- package/dist/resources/resolve.js.map +1 -0
- package/dist/resources/sandbox.d.ts +72 -0
- package/dist/resources/sandbox.d.ts.map +1 -0
- package/dist/resources/sandbox.js +952 -0
- package/dist/resources/sandbox.js.map +1 -0
- package/dist/resources/schema.d.ts +195 -0
- package/dist/resources/schema.d.ts.map +1 -0
- package/dist/resources/schema.js +231 -0
- package/dist/resources/schema.js.map +1 -0
- package/dist/resources/types.d.ts +36 -0
- package/dist/resources/types.d.ts.map +1 -0
- package/dist/resources/types.js +75 -0
- package/dist/resources/types.js.map +1 -0
- package/dist/resume.d.ts +76 -0
- package/dist/resume.d.ts.map +1 -0
- package/dist/resume.js +190 -0
- package/dist/resume.js.map +1 -0
- package/dist/runner-core.d.ts +29 -0
- package/dist/runner-core.d.ts.map +1 -1
- package/dist/runner-core.js +311 -36
- package/dist/runner-core.js.map +1 -1
- package/dist/runtime/phases/parallel.d.ts +3 -0
- package/dist/runtime/phases/parallel.d.ts.map +1 -1
- package/dist/runtime/phases/parallel.js.map +1 -1
- package/dist/runtime/phases/reduce.d.ts +47 -0
- package/dist/runtime/phases/reduce.d.ts.map +1 -0
- package/dist/runtime/phases/reduce.js +195 -0
- package/dist/runtime/phases/reduce.js.map +1 -0
- package/dist/runtime/phases/script.d.ts.map +1 -1
- package/dist/runtime/phases/script.js +47 -9
- package/dist/runtime/phases/script.js.map +1 -1
- package/dist/runtime.d.ts +67 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +904 -79
- package/dist/runtime.js.map +1 -1
- package/dist/schema.d.ts +135 -5
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +338 -21
- package/dist/schema.js.map +1 -1
- package/dist/store.d.ts +56 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +5 -1
- package/dist/store.js.map +1 -1
- package/dist/trace.d.ts +3 -0
- package/dist/trace.d.ts.map +1 -1
- package/dist/trace.js.map +1 -1
- package/package.json +4 -3
package/dist/schema.js
CHANGED
|
@@ -10,10 +10,14 @@ import { contractShapeErrors } from "./contract.js";
|
|
|
10
10
|
import { scorerShapeErrors } from "./scorers.js";
|
|
11
11
|
import { Type } from "typebox";
|
|
12
12
|
import { Errors as SchemaErrors } from "typebox/value";
|
|
13
|
+
import { cwdArgName, hasCwdPlaceholder, normalizeRelativePath } from "./cwd-bridge.js";
|
|
13
14
|
import { WORKSPACE_KEYWORDS } from "./workspace.js";
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Phase types
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
18
|
+
/** Hard cap on subagent calls made by one tree-reduce phase. This bounds
|
|
19
|
+
* author mistakes even when no run budget was declared. */
|
|
20
|
+
export const TREE_REDUCE_HARD_MAX_CALLS = 256;
|
|
17
21
|
/** Closed set of native phase kinds — single source of truth for DSL + FlowIR. */
|
|
18
22
|
export const PHASE_TYPES = [
|
|
19
23
|
"agent",
|
|
@@ -31,6 +35,18 @@ export const PHASE_TYPES = [
|
|
|
31
35
|
/** Dynamic sub-DAG: nested sub-flow or graft-promote into parent (Horizon B). */
|
|
32
36
|
"expand",
|
|
33
37
|
];
|
|
38
|
+
/** Phase kinds that execute one or more subagents and therefore require an
|
|
39
|
+
* effective idle watchdog (or a finite wall timeout when the watchdog is off). */
|
|
40
|
+
export const AGENT_RUNNING_PHASE_TYPES = [
|
|
41
|
+
"agent",
|
|
42
|
+
"gate",
|
|
43
|
+
"reduce",
|
|
44
|
+
"map",
|
|
45
|
+
"parallel",
|
|
46
|
+
"loop",
|
|
47
|
+
"tournament",
|
|
48
|
+
"race",
|
|
49
|
+
];
|
|
34
50
|
/** Loop iteration bounds. Authors may lower the max; the hard cap is a runaway guard. */
|
|
35
51
|
export const LOOP_DEFAULT_MAX_ITERATIONS = 10;
|
|
36
52
|
export const LOOP_HARD_MAX_ITERATIONS = 100;
|
|
@@ -58,11 +74,12 @@ const CACHE_SCOPES = ["run-only", "cross-run", "off"];
|
|
|
58
74
|
const CACHE_FINGERPRINT_PREFIXES = ["git:", "glob:", "glob!:", "file:", "env:"];
|
|
59
75
|
/** Phase types that must NOT be cached across runs (a fresh result is required each run). */
|
|
60
76
|
const CACHE_CROSS_RUN_BLOCKED_TYPES = ["gate", "approval", "loop", "tournament", "script", "race", "expand"];
|
|
61
|
-
/** `cwd` is a literal path / workspace keyword, not an interpolated field. */
|
|
62
|
-
const CWD_PLACEHOLDER_RE = /\{[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)*\}/;
|
|
63
77
|
const ParallelTaskSchema = Type.Object({
|
|
64
78
|
task: Type.String({ description: "Task for this parallel branch (supports interpolation)" }),
|
|
65
79
|
agent: Type.Optional(Type.String({ description: "Override the phase agent for this branch" })),
|
|
80
|
+
cwd: Type.Optional(Type.String({
|
|
81
|
+
description: "Working directory for this parallel branch's subagent (a literal path). Overrides the phase-level cwd for this branch only. Reserved workspace keywords ('temp'/'dedicated'/'worktree') are NOT supported per-branch (the workspace lifecycle is per-phase) — use the phase-level cwd for those.",
|
|
82
|
+
})),
|
|
66
83
|
}, { additionalProperties: false });
|
|
67
84
|
/** Declarative retry policy for a phase's subagent call(s). */
|
|
68
85
|
const RetrySchema = Type.Object({
|
|
@@ -112,6 +129,24 @@ const PhaseSchema = Type.Object({
|
|
|
112
129
|
cancelLosers: Type.Optional(Type.Boolean({ default: true })),
|
|
113
130
|
// reduce
|
|
114
131
|
from: Type.Optional(Type.Array(Type.String(), { description: "[reduce] Phase ids whose outputs are aggregated" })),
|
|
132
|
+
/**
|
|
133
|
+
* [reduce] How the aggregated `from[]` inputs are reduced. `'one-shot'`
|
|
134
|
+
* (default) feeds all inputs to a single reducer call as `{previous.output}`.
|
|
135
|
+
* `'tree'` batches the inputs (see `batchSize`) and runs intermediate
|
|
136
|
+
* reducer calls over each batch, then reduces the round outputs until one
|
|
137
|
+
* remains — useful when the aggregated input would exceed a single prompt.
|
|
138
|
+
* Tree reduction uses the SAME agent/model/options/timeout/idleTimeout as
|
|
139
|
+
* the phase and forces the imperative runtime (the event kernel falls back).
|
|
140
|
+
* The corrected `{previous.output}` aggregation (all `from[]` sources) always
|
|
141
|
+
* applies regardless of strategy.
|
|
142
|
+
*/
|
|
143
|
+
reduceStrategy: Type.Optional(StringEnum(["one-shot", "tree"], {
|
|
144
|
+
description: "[reduce] 'one-shot' (default) = single reducer call; 'tree' = batched intermediate rounds (see batchSize). Forces imperative runtime.",
|
|
145
|
+
default: "one-shot",
|
|
146
|
+
})),
|
|
147
|
+
/** [reduce] With `reduceStrategy:'tree'`, the max number of aggregated inputs
|
|
148
|
+
* fed to each intermediate reducer call (>= 2). Ignored for one-shot. */
|
|
149
|
+
batchSize: Type.Optional(Type.Integer({ minimum: 2, description: "[reduce] Batch size for reduceStrategy:'tree' (integer >= 2). Ignored for one-shot." })),
|
|
115
150
|
// sub-workflow (flow) + expand fragment
|
|
116
151
|
use: Type.Optional(Type.String({ description: "[flow] Name of a saved taskflow to run as this phase" })),
|
|
117
152
|
/** [expand] Fragment source — string interpolation or inline Taskflow / phases (same as flow.def). */
|
|
@@ -139,6 +174,16 @@ const PhaseSchema = Type.Object({
|
|
|
139
174
|
timeout: Type.Optional(Type.Number({
|
|
140
175
|
description: "Max execution time in milliseconds. For script phases: caps the shell command (default 60000, max 300000). For agent-running phases (agent/gate/reduce/map/parallel/loop/tournament): caps EACH subagent call — on expiry the subagent is aborted and the phase fails with a 'timedOut' marker (never retried). Not supported for approval/flow phases. Must be >= 1000.",
|
|
141
176
|
})),
|
|
177
|
+
/**
|
|
178
|
+
* Idle watchdog override (ms) for agent-running phases. A positive value (>= 1000)
|
|
179
|
+
* replaces the host default (300000ms): if a subagent produces no output for this
|
|
180
|
+
* long it is killed as stalled. `0` DISABLES the watchdog — but then a finite
|
|
181
|
+
* wall `timeout` (>= 1000) is REQUIRED on this phase so it can never hang forever.
|
|
182
|
+
* Overrides the flow-level `idleTimeout`. Absent → flow-level or host default.
|
|
183
|
+
*/
|
|
184
|
+
idleTimeout: Type.Optional(Type.Number({
|
|
185
|
+
description: "[agent-running] Idle watchdog in ms (>= 1000, or 0 to disable). 0 requires a finite wall 'timeout' >= 1000. Overrides the flow-level idleTimeout.",
|
|
186
|
+
})),
|
|
142
187
|
// loop-until-done
|
|
143
188
|
until: Type.Optional(Type.String({
|
|
144
189
|
description: "[loop] Stop condition evaluated after each iteration. The iteration's output is exposed as {steps.<thisId>.output}/.json. Supports the same operators as `when`. The loop stops when this is truthy, on convergence, or at maxIterations. A parse error stops the loop (fail-safe).",
|
|
@@ -186,7 +231,7 @@ const PhaseSchema = Type.Object({
|
|
|
186
231
|
description: "Thinking level override for this phase. Unknown values are rejected instead of silently inheriting the host default.",
|
|
187
232
|
})),
|
|
188
233
|
tools: Type.Optional(Type.Array(Type.String(), { description: "Restrict tools for this phase's agent" })),
|
|
189
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for this phase
|
|
234
|
+
cwd: Type.Optional(Type.String({ description: "Working directory for this phase. Accepts a literal path; a reserved keyword ('temp', 'dedicated', 'worktree'); or the exact whole placeholder {args.X} when X is declared type:'relative-path'. The 0.2.1 argument bridge requires an explicit host resolve-only opt-in." })),
|
|
190
235
|
final: Type.Optional(Type.Boolean({ description: "Mark this phase's output as the workflow result" })),
|
|
191
236
|
optional: Type.Optional(Type.Boolean({ description: "If true, a failure does not abort the run", default: false })),
|
|
192
237
|
idempotent: Type.Optional(Type.Boolean({
|
|
@@ -216,11 +261,53 @@ const PhaseSchema = Type.Object({
|
|
|
216
261
|
description: "Opt into the Shared Context Tree for this phase: the subagent gets ctx_read/ctx_write (a blackboard shared with siblings/ancestors, to avoid re-reading files) and ctx_report/ctx_spawn (report upward + queue child tasks the runtime picks up). Default false — existing flows are unaffected.",
|
|
217
262
|
})),
|
|
218
263
|
}, { additionalProperties: false });
|
|
219
|
-
const
|
|
264
|
+
const LegacyArgSpecSchema = Type.Object({
|
|
265
|
+
type: Type.Optional(Type.Never()),
|
|
220
266
|
default: Type.Optional(Type.Unknown()),
|
|
221
267
|
description: Type.Optional(Type.String()),
|
|
222
268
|
required: Type.Optional(Type.Boolean()),
|
|
223
269
|
}, { additionalProperties: false });
|
|
270
|
+
const StringArgSpecSchema = Type.Object({
|
|
271
|
+
type: Type.Literal("string"),
|
|
272
|
+
default: Type.Optional(Type.String()),
|
|
273
|
+
description: Type.Optional(Type.String()),
|
|
274
|
+
required: Type.Optional(Type.Boolean()),
|
|
275
|
+
}, { additionalProperties: false });
|
|
276
|
+
const RelativePathArgSpecSchema = Type.Object({
|
|
277
|
+
type: Type.Literal("relative-path"),
|
|
278
|
+
default: Type.Optional(Type.String()),
|
|
279
|
+
description: Type.Optional(Type.String()),
|
|
280
|
+
required: Type.Optional(Type.Boolean()),
|
|
281
|
+
}, { additionalProperties: false });
|
|
282
|
+
const NumberArgSpecSchema = Type.Object({
|
|
283
|
+
type: Type.Literal("number"),
|
|
284
|
+
default: Type.Optional(Type.Number()),
|
|
285
|
+
minimum: Type.Optional(Type.Number()),
|
|
286
|
+
maximum: Type.Optional(Type.Number()),
|
|
287
|
+
description: Type.Optional(Type.String()),
|
|
288
|
+
required: Type.Optional(Type.Boolean()),
|
|
289
|
+
}, { additionalProperties: false });
|
|
290
|
+
const BooleanArgSpecSchema = Type.Object({
|
|
291
|
+
type: Type.Literal("boolean"),
|
|
292
|
+
default: Type.Optional(Type.Boolean()),
|
|
293
|
+
description: Type.Optional(Type.String()),
|
|
294
|
+
required: Type.Optional(Type.Boolean()),
|
|
295
|
+
}, { additionalProperties: false });
|
|
296
|
+
const EnumArgSpecSchema = Type.Object({
|
|
297
|
+
type: Type.Literal("enum"),
|
|
298
|
+
values: Type.Array(Type.Union([Type.String(), Type.Number()]), { minItems: 1 }),
|
|
299
|
+
default: Type.Optional(Type.Union([Type.String(), Type.Number()])),
|
|
300
|
+
description: Type.Optional(Type.String()),
|
|
301
|
+
required: Type.Optional(Type.Boolean()),
|
|
302
|
+
}, { additionalProperties: false });
|
|
303
|
+
const ArgSpecSchema = Type.Union([
|
|
304
|
+
LegacyArgSpecSchema,
|
|
305
|
+
StringArgSpecSchema,
|
|
306
|
+
RelativePathArgSpecSchema,
|
|
307
|
+
NumberArgSpecSchema,
|
|
308
|
+
BooleanArgSpecSchema,
|
|
309
|
+
EnumArgSpecSchema,
|
|
310
|
+
]);
|
|
224
311
|
export const TaskflowSchema = Type.Object({
|
|
225
312
|
name: Type.String({ minLength: 1, description: "Workflow name (becomes /tf:<name> command when saved)" }),
|
|
226
313
|
description: Type.Optional(Type.String()),
|
|
@@ -239,6 +326,16 @@ export const TaskflowSchema = Type.Object({
|
|
|
239
326
|
incremental: Type.Optional(Type.Boolean({
|
|
240
327
|
description: "Default every phase to cross-run caching (scope:'cross-run') so re-running this flow reuses unchanged phases across runs/sessions. Equivalent to setting cache:{scope:'cross-run'} on every phase; per-phase cache settings and the cross-run-blocked types (gate/approval/loop/tournament) still take precedence. Default false (run-only — each run starts fresh unless a phase opts in). A run-time `incremental` argument overrides this.",
|
|
241
328
|
})),
|
|
329
|
+
/**
|
|
330
|
+
* Flow-level idle watchdog (ms) for all agent-running phases that don't set
|
|
331
|
+
* their own `idleTimeout`. Positive (>= 1000) overrides the host default
|
|
332
|
+
* (300000ms); `0` disables the watchdog for those phases — but then every
|
|
333
|
+
* agent-running phase MUST declare a finite wall `timeout` (>= 1000) so the
|
|
334
|
+
* flow can never hang forever. A per-phase `idleTimeout` overrides this.
|
|
335
|
+
*/
|
|
336
|
+
idleTimeout: Type.Optional(Type.Number({
|
|
337
|
+
description: "Flow-level idle watchdog in ms (>= 1000, or 0 to disable). Per-phase idleTimeout overrides. 0 requires every agent-running phase to declare a finite wall 'timeout' >= 1000.",
|
|
338
|
+
})),
|
|
242
339
|
phases: Type.Array(PhaseSchema, { minItems: 1, description: "Ordered phase definitions (DAG via dependsOn)" }),
|
|
243
340
|
}, { additionalProperties: false });
|
|
244
341
|
/** True when `def` is a shorthand spec (no `phases`, but a task/tasks/chain field). */
|
|
@@ -270,6 +367,8 @@ function readStep(s) {
|
|
|
270
367
|
step.context = ctx;
|
|
271
368
|
if (typeof o.contextLimit === "number")
|
|
272
369
|
step.contextLimit = o.contextLimit;
|
|
370
|
+
if (typeof o.cwd === "string")
|
|
371
|
+
step.cwd = o.cwd;
|
|
273
372
|
return step;
|
|
274
373
|
}
|
|
275
374
|
return { task: "" };
|
|
@@ -296,6 +395,13 @@ export function desugar(def) {
|
|
|
296
395
|
meta.budget = d.budget;
|
|
297
396
|
if (typeof d.strictInterpolation === "boolean")
|
|
298
397
|
meta.strictInterpolation = d.strictInterpolation;
|
|
398
|
+
/** Top-level shorthand cwd (literal path or workspace keyword). Becomes the
|
|
399
|
+
* default for every step; a per-step cwd overrides it. For single/chain it
|
|
400
|
+
* lands on each Phase.cwd (full workspace lifecycle). For parallel it lands
|
|
401
|
+
* on the phase cwd (shared), with per-branch cwds overriding per-branch.
|
|
402
|
+
* Note: `cwd` is a Phase field, NOT a Taskflow field, so it is distributed
|
|
403
|
+
* to each phase below rather than placed on the Taskflow object. */
|
|
404
|
+
const topCwd = typeof d.cwd === "string" && d.cwd.trim() ? d.cwd : undefined;
|
|
299
405
|
const nameOf = (fallback) => (typeof d.name === "string" && d.name.trim() ? d.name.trim() : fallback);
|
|
300
406
|
// chain → sequential agent phases
|
|
301
407
|
if (Array.isArray(d.chain) && d.chain.length > 0) {
|
|
@@ -313,6 +419,9 @@ export function desugar(def) {
|
|
|
313
419
|
phase.context = s.context;
|
|
314
420
|
if (s.contextLimit !== undefined)
|
|
315
421
|
phase.contextLimit = s.contextLimit;
|
|
422
|
+
const stepCwd = s.cwd ?? topCwd;
|
|
423
|
+
if (stepCwd)
|
|
424
|
+
phase.cwd = stepCwd;
|
|
316
425
|
if (i > 0)
|
|
317
426
|
phase.dependsOn = [`step${i}`];
|
|
318
427
|
if (i === steps.length - 1)
|
|
@@ -326,8 +435,18 @@ export function desugar(def) {
|
|
|
326
435
|
// per branch): spec-level context plus the union of step-level contexts.
|
|
327
436
|
if (Array.isArray(d.tasks) && d.tasks.length > 0) {
|
|
328
437
|
const steps = d.tasks.map(readStep);
|
|
329
|
-
const branches = steps.map((s) =>
|
|
438
|
+
const branches = steps.map((s) => {
|
|
439
|
+
const b = { task: s.task };
|
|
440
|
+
if (s.agent)
|
|
441
|
+
b.agent = s.agent;
|
|
442
|
+
// Per-branch literal cwd (workspace keywords are rejected by validation).
|
|
443
|
+
if (s.cwd)
|
|
444
|
+
b.cwd = s.cwd;
|
|
445
|
+
return b;
|
|
446
|
+
});
|
|
330
447
|
const phase = { id: "parallel", type: "parallel", branches, final: true };
|
|
448
|
+
if (topCwd)
|
|
449
|
+
phase.cwd = topCwd;
|
|
331
450
|
const shared = [...(readContextList(d.context) ?? []), ...steps.flatMap((s) => s.context ?? [])];
|
|
332
451
|
if (shared.length)
|
|
333
452
|
phase.context = Array.from(new Set(shared));
|
|
@@ -349,6 +468,8 @@ export function desugar(def) {
|
|
|
349
468
|
phase.context = ctx;
|
|
350
469
|
if (typeof d.contextLimit === "number")
|
|
351
470
|
phase.contextLimit = d.contextLimit;
|
|
471
|
+
if (topCwd)
|
|
472
|
+
phase.cwd = topCwd;
|
|
352
473
|
return { name: nameOf("task"), ...meta, phases: [phase] };
|
|
353
474
|
}
|
|
354
475
|
throw new Error("Shorthand spec needs one of: 'task' (single), 'tasks' (parallel), or 'chain' (sequential)");
|
|
@@ -370,6 +491,58 @@ export function parseTtlMs(ttl) {
|
|
|
370
491
|
const mult = { ms: 1, s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
|
|
371
492
|
return n * mult[unit];
|
|
372
493
|
}
|
|
494
|
+
function typedArgValueErrors(name, spec, value, source) {
|
|
495
|
+
const prefix = `Argument '${name}' ${source}`;
|
|
496
|
+
if (spec.type === "relative-path") {
|
|
497
|
+
const r = normalizeRelativePath(value);
|
|
498
|
+
return r.ok ? [] : [`${prefix} ${r.message}`];
|
|
499
|
+
}
|
|
500
|
+
if (spec.type === "string") {
|
|
501
|
+
if (typeof value !== "string")
|
|
502
|
+
return [`${prefix} must be a string, got ${value === null ? "null" : typeof value}`];
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
if (spec.type === "number") {
|
|
506
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
507
|
+
return [`${prefix} must be a finite number`];
|
|
508
|
+
if (spec.minimum !== undefined && value < spec.minimum)
|
|
509
|
+
return [`${prefix} must be >= ${spec.minimum}`];
|
|
510
|
+
if (spec.maximum !== undefined && value > spec.maximum)
|
|
511
|
+
return [`${prefix} must be <= ${spec.maximum}`];
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
if (spec.type === "boolean") {
|
|
515
|
+
return typeof value === "boolean" ? [] : [`${prefix} must be a boolean, got ${value === null ? "null" : typeof value}`];
|
|
516
|
+
}
|
|
517
|
+
if (spec.type === "enum") {
|
|
518
|
+
const values = Array.isArray(spec.values) ? spec.values : [];
|
|
519
|
+
return values.some((v) => Object.is(v, value)) ? [] : [`${prefix} must be one of ${JSON.stringify(values)}`];
|
|
520
|
+
}
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
/** Validate resolved invocation values independently of full flow structure.
|
|
524
|
+
* Runtime calls this at the Core boundary so direct, resume, and detached
|
|
525
|
+
* execution cannot bypass adapter-level typed-arg checks. */
|
|
526
|
+
export function validateInvocationArgs(def, args) {
|
|
527
|
+
const errors = [];
|
|
528
|
+
const specs = (def.args ?? {});
|
|
529
|
+
for (const [name, spec] of Object.entries(specs)) {
|
|
530
|
+
// Full structural validation owns malformed specs. Keep this boundary
|
|
531
|
+
// total so validateTaskflow() reports SchemaErrors instead of throwing
|
|
532
|
+
// while it performs the optional invocation-value pass.
|
|
533
|
+
if (typeof spec !== "object" || spec === null || Array.isArray(spec))
|
|
534
|
+
continue;
|
|
535
|
+
const typedSpec = spec;
|
|
536
|
+
if (!(name in args)) {
|
|
537
|
+
if (typedSpec.type !== undefined && typedSpec.required === true && typedSpec.default === undefined)
|
|
538
|
+
errors.push(`Missing required argument '${name}'`);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (typedSpec.type !== undefined)
|
|
542
|
+
errors.push(...typedArgValueErrors(name, typedSpec, args[name], "invocation"));
|
|
543
|
+
}
|
|
544
|
+
return errors;
|
|
545
|
+
}
|
|
373
546
|
export function validateTaskflow(def, opts = {}) {
|
|
374
547
|
const errors = [];
|
|
375
548
|
const warnings = [];
|
|
@@ -397,6 +570,52 @@ export function validateTaskflow(def, opts = {}) {
|
|
|
397
570
|
errors.push("Taskflow must have at least one phase");
|
|
398
571
|
return { ok: false, errors, warnings };
|
|
399
572
|
}
|
|
573
|
+
// Typed invocation args are the trust boundary for resource selectors. Legacy
|
|
574
|
+
// specs remain valid for every flow version, but can never feed a cwd bridge.
|
|
575
|
+
// `version` is informational metadata, not a schema-version selector.
|
|
576
|
+
const argSpecs = (typeof flow.args === "object" && flow.args !== null && !Array.isArray(flow.args)
|
|
577
|
+
? flow.args
|
|
578
|
+
: {});
|
|
579
|
+
for (const [name, spec] of Object.entries(argSpecs)) {
|
|
580
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec))
|
|
581
|
+
continue;
|
|
582
|
+
if (spec.type === "number" && spec.minimum !== undefined && spec.maximum !== undefined && spec.minimum > spec.maximum) {
|
|
583
|
+
errors.push(`Argument '${name}': minimum must be <= maximum`);
|
|
584
|
+
}
|
|
585
|
+
if (spec.type === "enum" && Array.isArray(spec.values) && new Set(spec.values.map((v) => `${typeof v}:${String(v)}`)).size !== spec.values.length) {
|
|
586
|
+
errors.push(`Argument '${name}': enum values must be unique`);
|
|
587
|
+
}
|
|
588
|
+
if (spec.default !== undefined && spec.type !== undefined) {
|
|
589
|
+
errors.push(...typedArgValueErrors(name, spec, spec.default, "default"));
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (opts.args !== undefined) {
|
|
593
|
+
errors.push(...validateInvocationArgs(flow, opts.args));
|
|
594
|
+
for (const [name, spec] of Object.entries(argSpecs)) {
|
|
595
|
+
if (!spec || typeof spec !== "object" || Array.isArray(spec))
|
|
596
|
+
continue;
|
|
597
|
+
if (spec.type === undefined && spec.required === true && spec.default === undefined && !(name in opts.args)) {
|
|
598
|
+
warnings.push(`Legacy untyped argument '${name}' is marked required but remains advisory; add an explicit type to enforce it`);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
for (const name of Object.keys(opts.args)) {
|
|
602
|
+
if (name in argSpecs)
|
|
603
|
+
continue;
|
|
604
|
+
if (strict)
|
|
605
|
+
warnings.push(`Invocation argument '${name}' is undeclared`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Flow-level idleTimeout: positive must be >= 1000; 0 is allowed (disables the
|
|
609
|
+
// watchdog) but every agent-running phase must then declare a finite wall
|
|
610
|
+
// `timeout` so the run can never hang forever (critical invariant #5).
|
|
611
|
+
if (flow.idleTimeout !== undefined) {
|
|
612
|
+
if (typeof flow.idleTimeout !== "number" || !Number.isFinite(flow.idleTimeout) || flow.idleTimeout < 0) {
|
|
613
|
+
errors.push(`Flow 'idleTimeout' must be a non-negative finite number (ms), got ${typeof flow.idleTimeout === "number" ? flow.idleTimeout : typeof flow.idleTimeout}`);
|
|
614
|
+
}
|
|
615
|
+
else if (flow.idleTimeout > 0 && flow.idleTimeout < 1000) {
|
|
616
|
+
errors.push(`Flow 'idleTimeout' must be 0 (disable) or >= 1000 ms, got ${flow.idleTimeout}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
400
619
|
// Hardening for runtime-generated (untrusted) sub-flows: bound breadth and
|
|
401
620
|
// contain filesystem access. These do NOT apply to authored/saved flows.
|
|
402
621
|
if (opts.dynamic) {
|
|
@@ -406,10 +625,12 @@ export function validateTaskflow(def, opts = {}) {
|
|
|
406
625
|
if (typeof flow.concurrency === "number" && flow.concurrency > MAX_DYNAMIC_CONCURRENCY) {
|
|
407
626
|
errors.push(`Dynamic sub-flow concurrency too high (${flow.concurrency}, max ${MAX_DYNAMIC_CONCURRENCY})`);
|
|
408
627
|
}
|
|
409
|
-
const root = opts.cwd ? path.resolve(opts.cwd) : undefined;
|
|
410
628
|
for (const p of flow.phases) {
|
|
411
629
|
if (!p || typeof p !== "object")
|
|
412
630
|
continue;
|
|
631
|
+
if (Array.isArray(p.context) && p.context.length > 0) {
|
|
632
|
+
errors.push(`Dynamic sub-flow phase '${p.id}': context file pre-reads are not allowed in generated flows`);
|
|
633
|
+
}
|
|
413
634
|
// A generated phase may not execute shell commands. `script` runs an
|
|
414
635
|
// arbitrary command — a strictly larger capability than the reserved
|
|
415
636
|
// cwd keywords blocked below — so an LLM-authored plan (flow{def} /
|
|
@@ -444,20 +665,18 @@ export function validateTaskflow(def, opts = {}) {
|
|
|
444
665
|
if (typeof p.concurrency === "number" && p.concurrency > MAX_DYNAMIC_CONCURRENCY) {
|
|
445
666
|
errors.push(`Dynamic sub-flow phase '${p.id}': concurrency too high (${p.concurrency}, max ${MAX_DYNAMIC_CONCURRENCY})`);
|
|
446
667
|
}
|
|
447
|
-
//
|
|
448
|
-
//
|
|
449
|
-
//
|
|
450
|
-
// worktrees that mutate the repo. Only author-written flows may.
|
|
668
|
+
// W1a/FileBroker is not available yet, so a generated phase may not
|
|
669
|
+
// choose any cwd. Lexical containment is insufficient because symlinks
|
|
670
|
+
// and time-of-check/time-of-use races can cross the invocation boundary.
|
|
451
671
|
if (typeof p.cwd === "string") {
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
errors.push(`Dynamic sub-flow phase '${p.id}': cwd '${p.cwd}' escapes the run directory`);
|
|
672
|
+
errors.push(`Dynamic sub-flow phase '${p.id}': cwd selection is not allowed in generated flows`);
|
|
673
|
+
}
|
|
674
|
+
if (Array.isArray(p.branches)) {
|
|
675
|
+
p.branches.forEach((branch, i) => {
|
|
676
|
+
if (branch && typeof branch === "object" && typeof branch.cwd === "string") {
|
|
677
|
+
errors.push(`Dynamic sub-flow phase '${p.id}': branches[${i}].cwd selection is not allowed in generated flows`);
|
|
459
678
|
}
|
|
460
|
-
}
|
|
679
|
+
});
|
|
461
680
|
}
|
|
462
681
|
}
|
|
463
682
|
}
|
|
@@ -516,8 +735,26 @@ export function validateTaskflow(def, opts = {}) {
|
|
|
516
735
|
!THINKING_LEVELS.includes(p.thinking)) {
|
|
517
736
|
errors.push(`Phase '${p.id}': 'thinking' must be one of ${THINKING_LEVELS.join(", ")}; got '${p.thinking}'`);
|
|
518
737
|
}
|
|
519
|
-
if (typeof p.cwd === "string" &&
|
|
520
|
-
|
|
738
|
+
if (typeof p.cwd === "string" && hasCwdPlaceholder(p.cwd)) {
|
|
739
|
+
const argName = cwdArgName(p.cwd);
|
|
740
|
+
if (!argName) {
|
|
741
|
+
errors.push(`Phase '${p.id}': dynamic 'cwd' must be exactly one whole {args.X} placeholder; concatenation, multiple placeholders, and {steps.*} are forbidden.`);
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
const spec = argSpecs[argName];
|
|
745
|
+
if (!spec) {
|
|
746
|
+
errors.push(`Phase '${p.id}': cwd references undeclared argument '${argName}'`);
|
|
747
|
+
}
|
|
748
|
+
else if (spec.type !== "relative-path") {
|
|
749
|
+
errors.push(`Phase '${p.id}': cwd argument '${argName}' must be declared with type 'relative-path'`);
|
|
750
|
+
}
|
|
751
|
+
if (p.cache?.scope === "cross-run") {
|
|
752
|
+
errors.push(`Phase '${p.id}': cwd selected by an argument cannot use cache.scope 'cross-run' until workspace state restoration is available.`);
|
|
753
|
+
}
|
|
754
|
+
if (p.retry && typeof p.retry.max === "number" && p.retry.max > 0) {
|
|
755
|
+
errors.push(`Phase '${p.id}': cwd selected by an argument cannot use retry.max > 0 because a failed resolve-only write has an unknown filesystem outcome and must be reconciled before another attempt.`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
521
758
|
}
|
|
522
759
|
// dependsOn / from entries are string phase-id refs that flow into the graph
|
|
523
760
|
// helpers and nodeId(); a non-string entry would crash the renderer.
|
|
@@ -532,11 +769,35 @@ export function validateTaskflow(def, opts = {}) {
|
|
|
532
769
|
// Branch entries become competitors at runtime (b.task is interpolated); a
|
|
533
770
|
// non-object / non-string-task entry would crash the runtime, so reject it.
|
|
534
771
|
if (Array.isArray(p.branches)) {
|
|
772
|
+
const branchPhaseType = (p.type ?? "agent");
|
|
535
773
|
p.branches.forEach((b, i) => {
|
|
536
774
|
if (!b || typeof b !== "object" || Array.isArray(b))
|
|
537
775
|
errors.push(`Phase '${p.id}': branches[${i}] must be an object with a 'task', got ${b === null ? "null" : typeof b}`);
|
|
538
776
|
else if (typeof b.task !== "string")
|
|
539
777
|
errors.push(`Phase '${p.id}': branches[${i}].task must be a string`);
|
|
778
|
+
else {
|
|
779
|
+
// Per-branch cwd: a literal path is honored per-branch; a reserved
|
|
780
|
+
// workspace keyword ('temp'/'dedicated'/'worktree') is NOT supported
|
|
781
|
+
// per-branch (the workspace lifecycle is per-phase — a branch cannot
|
|
782
|
+
// safely allocate/teardown its own workspace). Reject that shape
|
|
783
|
+
// precisely rather than silently using the wrong cwd. Use the
|
|
784
|
+
// phase-level `cwd` for workspace isolation.
|
|
785
|
+
const bcwd = b.cwd;
|
|
786
|
+
if (typeof bcwd === "string") {
|
|
787
|
+
if (branchPhaseType !== "parallel") {
|
|
788
|
+
errors.push(`Phase '${p.id}' (${branchPhaseType}): branches[${i}].cwd is only supported for parallel phases`);
|
|
789
|
+
}
|
|
790
|
+
else if (hasCwdPlaceholder(bcwd)) {
|
|
791
|
+
errors.push(`Phase '${p.id}': branches[${i}].cwd does not support interpolation placeholders (${bcwd}). Use a literal path.`);
|
|
792
|
+
}
|
|
793
|
+
else if (WORKSPACE_KEYWORDS.includes(bcwd)) {
|
|
794
|
+
errors.push(`Phase '${p.id}': branches[${i}].cwd '${bcwd}' is a reserved workspace keyword not supported per-branch (the workspace lifecycle is per-phase). Use the phase-level 'cwd' for workspace isolation.`);
|
|
795
|
+
}
|
|
796
|
+
else if (typeof p.cwd === "string" && (WORKSPACE_KEYWORDS.includes(p.cwd) || hasCwdPlaceholder(p.cwd))) {
|
|
797
|
+
errors.push(`Phase '${p.id}': branches[${i}].cwd cannot override phase cwd '${p.cwd}' because that phase uses a managed workspace/cwd binding. Put the literal cwd on the phase or remove the branch override.`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
540
801
|
});
|
|
541
802
|
}
|
|
542
803
|
// tools entries are matched against a Set by the adapters (t => set.has(t));
|
|
@@ -590,7 +851,9 @@ export function validateTaskflow(def, opts = {}) {
|
|
|
590
851
|
warnings.push(`Phase '${p.id}' (gate): both 'eval' and 'score' are set — eval runs first (all-pass skips the gate entirely), then score. This is valid but usually one of the two suffices.`);
|
|
591
852
|
}
|
|
592
853
|
// Judge agent naming convention (mirrors the phase-agent check below).
|
|
593
|
-
const judgeAgent = scoreVal
|
|
854
|
+
const judgeAgent = scoreVal !== null && typeof scoreVal === "object"
|
|
855
|
+
? scoreVal.judge?.agent
|
|
856
|
+
: undefined;
|
|
594
857
|
if (typeof judgeAgent === "string" && judgeAgent.includes("_")) {
|
|
595
858
|
errors.push(`Phase '${p.id}': score.judge.agent '${judgeAgent}' uses underscores — use hyphens`);
|
|
596
859
|
}
|
|
@@ -672,6 +935,33 @@ export function validateTaskflow(def, opts = {}) {
|
|
|
672
935
|
errors.push(`Phase '${p.id}' (reduce) requires 'from'`);
|
|
673
936
|
if (!p.task)
|
|
674
937
|
errors.push(`Phase '${p.id}' (reduce) requires 'task'`);
|
|
938
|
+
// reduceStrategy / batchSize (reduce-only).
|
|
939
|
+
const strat = p.reduceStrategy;
|
|
940
|
+
if (strat !== undefined && strat !== "tree" && strat !== "one-shot") {
|
|
941
|
+
errors.push(`Phase '${p.id}' (reduce): reduceStrategy must be 'tree' or 'one-shot', got '${String(strat)}'`);
|
|
942
|
+
}
|
|
943
|
+
const bs = p.batchSize;
|
|
944
|
+
if (bs !== undefined) {
|
|
945
|
+
if (typeof bs !== "number" || !Number.isFinite(bs) || bs < 2 || !Number.isInteger(bs)) {
|
|
946
|
+
errors.push(`Phase '${p.id}' (reduce): batchSize must be an integer >= 2`);
|
|
947
|
+
}
|
|
948
|
+
else if (strat !== "tree") {
|
|
949
|
+
warnings.push(`Phase '${p.id}' (reduce): batchSize is only used with reduceStrategy 'tree' — ignored for one-shot`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (strat === "tree") {
|
|
953
|
+
const batchSize = typeof bs === "number" && Number.isInteger(bs) && bs >= 2 ? bs : 2;
|
|
954
|
+
let remaining = asArray(p.from).length;
|
|
955
|
+
let maxCalls = 0;
|
|
956
|
+
while (remaining > 1 && maxCalls <= TREE_REDUCE_HARD_MAX_CALLS) {
|
|
957
|
+
const callsThisRound = Math.ceil(remaining / batchSize);
|
|
958
|
+
maxCalls += callsThisRound;
|
|
959
|
+
remaining = callsThisRound;
|
|
960
|
+
}
|
|
961
|
+
if (maxCalls > TREE_REDUCE_HARD_MAX_CALLS) {
|
|
962
|
+
errors.push(`Phase '${p.id}' (reduce): tree strategy exceeds hard cap ${TREE_REDUCE_HARD_MAX_CALLS} subagent calls; increase batchSize or split the reduction`);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
675
965
|
}
|
|
676
966
|
if (type === "flow") {
|
|
677
967
|
const hasUse = typeof p.use === "string" && p.use.length > 0;
|
|
@@ -743,6 +1033,32 @@ export function validateTaskflow(def, opts = {}) {
|
|
|
743
1033
|
errors.push(`Phase '${p.id}' (${type}): 'timeout' must be a number >= 1000 ms`);
|
|
744
1034
|
}
|
|
745
1035
|
}
|
|
1036
|
+
// idleTimeout validation (agent-running phases that can use the watchdog).
|
|
1037
|
+
// The host default (300000ms) applies when neither phase nor flow sets it.
|
|
1038
|
+
const AGENT_RUNNING_FOR_IDLE = new Set(AGENT_RUNNING_PHASE_TYPES);
|
|
1039
|
+
if (AGENT_RUNNING_FOR_IDLE.has(type)) {
|
|
1040
|
+
const phaseIdle = p.idleTimeout;
|
|
1041
|
+
if (phaseIdle !== undefined) {
|
|
1042
|
+
if (typeof phaseIdle !== "number" || !Number.isFinite(phaseIdle) || phaseIdle < 0) {
|
|
1043
|
+
errors.push(`Phase '${p.id}': 'idleTimeout' must be a non-negative finite number (ms), got ${typeof phaseIdle === "number" ? phaseIdle : typeof phaseIdle}`);
|
|
1044
|
+
}
|
|
1045
|
+
else if (phaseIdle > 0 && phaseIdle < 1000) {
|
|
1046
|
+
errors.push(`Phase '${p.id}': 'idleTimeout' must be 0 (disable) or >= 1000 ms, got ${phaseIdle}`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
// Effective idle watchdog = phase (wins) → flow → host default (undefined).
|
|
1050
|
+
const effectiveIdle = (typeof phaseIdle === "number" ? phaseIdle : flow.idleTimeout);
|
|
1051
|
+
if (effectiveIdle === 0) {
|
|
1052
|
+
// Disabling the watchdog requires a finite wall timeout so the phase can
|
|
1053
|
+
// never hang forever (critical invariant #5: never hang forever).
|
|
1054
|
+
if (typeof p.timeout !== "number" || !Number.isFinite(p.timeout) || p.timeout < 1000) {
|
|
1055
|
+
errors.push(`Phase '${p.id}': idleTimeout:0 disables the watchdog — a finite wall 'timeout' (>= 1000 ms) is required so this phase cannot hang forever. Add a 'timeout' or use a positive 'idleTimeout'.`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
else if (p.idleTimeout !== undefined) {
|
|
1060
|
+
warnings.push(`Phase '${p.id}' (${type}): 'idleTimeout' only applies to agent-running phases (${AGENT_RUNNING_PHASE_TYPES.join("/")}) — ignored here`);
|
|
1061
|
+
}
|
|
746
1062
|
if (p.retry) {
|
|
747
1063
|
if (typeof p.retry.max !== "number" || p.retry.max < 0) {
|
|
748
1064
|
errors.push(`Phase '${p.id}': retry.max must be a number >= 0`);
|
|
@@ -972,6 +1288,7 @@ export function collectRefs(phase) {
|
|
|
972
1288
|
scan(phase.over);
|
|
973
1289
|
scan(phase.when);
|
|
974
1290
|
scan(phase.until);
|
|
1291
|
+
scan(phase.cwd);
|
|
975
1292
|
// Script phases: the array form of `run` supports {steps.X}/{args.X}
|
|
976
1293
|
// interpolation (the string form does NOT — it's a raw shell command,
|
|
977
1294
|
// validation rejects placeholders in it), and `input` (stdin) does too.
|