taskflow-core 0.2.1 → 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.
Files changed (67) hide show
  1. package/README.md +3 -3
  2. package/dist/build-info.d.ts +48 -0
  3. package/dist/build-info.d.ts.map +1 -0
  4. package/dist/build-info.js +112 -0
  5. package/dist/build-info.js.map +1 -0
  6. package/dist/build-info.json +4 -0
  7. package/dist/exec/driver.d.ts +5 -0
  8. package/dist/exec/driver.d.ts.map +1 -1
  9. package/dist/exec/driver.js +25 -16
  10. package/dist/exec/driver.js.map +1 -1
  11. package/dist/exec/kernel-policy.d.ts.map +1 -1
  12. package/dist/exec/kernel-policy.js +10 -0
  13. package/dist/exec/kernel-policy.js.map +1 -1
  14. package/dist/exec/step-kinds.d.ts +14 -1
  15. package/dist/exec/step-kinds.d.ts.map +1 -1
  16. package/dist/exec/step-kinds.js +98 -3
  17. package/dist/exec/step-kinds.js.map +1 -1
  18. package/dist/exec/step.d.ts +21 -0
  19. package/dist/exec/step.d.ts.map +1 -1
  20. package/dist/exec/step.js +22 -5
  21. package/dist/exec/step.js.map +1 -1
  22. package/dist/final-output.d.ts +53 -0
  23. package/dist/final-output.d.ts.map +1 -0
  24. package/dist/final-output.js +65 -0
  25. package/dist/final-output.js.map +1 -0
  26. package/dist/flowir/canonical-hash.d.ts.map +1 -1
  27. package/dist/flowir/canonical-hash.js +2 -0
  28. package/dist/flowir/canonical-hash.js.map +1 -1
  29. package/dist/flowir/compile.d.ts.map +1 -1
  30. package/dist/flowir/compile.js +4 -0
  31. package/dist/flowir/compile.js.map +1 -1
  32. package/dist/flowir/meta.d.ts +3 -0
  33. package/dist/flowir/meta.d.ts.map +1 -1
  34. package/dist/flowir/schema.d.ts +3 -0
  35. package/dist/flowir/schema.d.ts.map +1 -1
  36. package/dist/flowir/schema.js.map +1 -1
  37. package/dist/flowir/translate.d.ts.map +1 -1
  38. package/dist/flowir/translate.js +4 -0
  39. package/dist/flowir/translate.js.map +1 -1
  40. package/dist/index.d.ts +3 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +3 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/resume.d.ts +76 -0
  45. package/dist/resume.d.ts.map +1 -0
  46. package/dist/resume.js +190 -0
  47. package/dist/resume.js.map +1 -0
  48. package/dist/runtime/phases/parallel.d.ts +3 -0
  49. package/dist/runtime/phases/parallel.d.ts.map +1 -1
  50. package/dist/runtime/phases/parallel.js.map +1 -1
  51. package/dist/runtime/phases/reduce.d.ts +47 -0
  52. package/dist/runtime/phases/reduce.d.ts.map +1 -0
  53. package/dist/runtime/phases/reduce.js +195 -0
  54. package/dist/runtime/phases/reduce.js.map +1 -0
  55. package/dist/runtime.d.ts +28 -1
  56. package/dist/runtime.d.ts.map +1 -1
  57. package/dist/runtime.js +392 -59
  58. package/dist/runtime.js.map +1 -1
  59. package/dist/schema.d.ts +67 -0
  60. package/dist/schema.d.ts.map +1 -1
  61. package/dist/schema.js +177 -1
  62. package/dist/schema.js.map +1 -1
  63. package/dist/store.d.ts +49 -0
  64. package/dist/store.d.ts.map +1 -1
  65. package/dist/store.js +5 -1
  66. package/dist/store.js.map +1 -1
  67. package/package.json +2 -2
package/dist/schema.d.ts CHANGED
@@ -5,6 +5,9 @@
5
5
  * to a subagent (an isolated `pi` process). Phases form a DAG via `dependsOn`.
6
6
  */
7
7
  import { Type, type Static } from "typebox";
8
+ /** Hard cap on subagent calls made by one tree-reduce phase. This bounds
9
+ * author mistakes even when no run budget was declared. */
10
+ export declare const TREE_REDUCE_HARD_MAX_CALLS = 256;
8
11
  /** Closed set of native phase kinds — single source of truth for DSL + FlowIR. */
9
12
  export declare const PHASE_TYPES: readonly ["agent", "parallel", "map", "gate", "reduce", "approval", "flow", "loop", "tournament", "script",
10
13
  /** First completed branch wins (Horizon B). */
@@ -12,6 +15,9 @@ export declare const PHASE_TYPES: readonly ["agent", "parallel", "map", "gate",
12
15
  /** Dynamic sub-DAG: nested sub-flow or graft-promote into parent (Horizon B). */
13
16
  "expand"];
14
17
  export type PhaseType = (typeof PHASE_TYPES)[number];
18
+ /** Phase kinds that execute one or more subagents and therefore require an
19
+ * effective idle watchdog (or a finite wall timeout when the watchdog is off). */
20
+ export declare const AGENT_RUNNING_PHASE_TYPES: readonly ["agent", "gate", "reduce", "map", "parallel", "loop", "tournament", "race"];
15
21
  /** Loop iteration bounds. Authors may lower the max; the hard cap is a runaway guard. */
16
22
  export declare const LOOP_DEFAULT_MAX_ITERATIONS = 10;
17
23
  export declare const LOOP_HARD_MAX_ITERATIONS = 100;
@@ -40,6 +46,7 @@ export type CacheScope = (typeof CACHE_SCOPES)[number];
40
46
  declare const ParallelTaskSchema: Type.TObject<{
41
47
  task: Type.TString;
42
48
  agent: Type.TOptional<Type.TString>;
49
+ cwd: Type.TOptional<Type.TString>;
43
50
  }>;
44
51
  /** Declarative retry policy for a phase's subagent call(s). */
45
52
  declare const RetrySchema: Type.TObject<{
@@ -72,6 +79,7 @@ declare const PhaseSchema: Type.TObject<{
72
79
  branches: Type.TOptional<Type.TArray<Type.TObject<{
73
80
  task: Type.TString;
74
81
  agent: Type.TOptional<Type.TString>;
82
+ cwd: Type.TOptional<Type.TString>;
75
83
  }>>>;
76
84
  /**
77
85
  * [race] When true (default), abort in-flight loser branches after the first branch
@@ -80,6 +88,21 @@ declare const PhaseSchema: Type.TObject<{
80
88
  */
81
89
  cancelLosers: Type.TOptional<Type.TBoolean>;
82
90
  from: Type.TOptional<Type.TArray<Type.TString>>;
91
+ /**
92
+ * [reduce] How the aggregated `from[]` inputs are reduced. `'one-shot'`
93
+ * (default) feeds all inputs to a single reducer call as `{previous.output}`.
94
+ * `'tree'` batches the inputs (see `batchSize`) and runs intermediate
95
+ * reducer calls over each batch, then reduces the round outputs until one
96
+ * remains — useful when the aggregated input would exceed a single prompt.
97
+ * Tree reduction uses the SAME agent/model/options/timeout/idleTimeout as
98
+ * the phase and forces the imperative runtime (the event kernel falls back).
99
+ * The corrected `{previous.output}` aggregation (all `from[]` sources) always
100
+ * applies regardless of strategy.
101
+ */
102
+ reduceStrategy: Type.TOptional<Type.TUnsafe<"one-shot" | "tree">>;
103
+ /** [reduce] With `reduceStrategy:'tree'`, the max number of aggregated inputs
104
+ * fed to each intermediate reducer call (>= 2). Ignored for one-shot. */
105
+ batchSize: Type.TOptional<Type.TInteger>;
83
106
  use: Type.TOptional<Type.TString>;
84
107
  /** [expand] Fragment source — string interpolation or inline Taskflow / phases (same as flow.def). */
85
108
  def: Type.TOptional<Type.TUnknown>;
@@ -91,6 +114,14 @@ declare const PhaseSchema: Type.TObject<{
91
114
  run: Type.TOptional<Type.TUnion<[Type.TString, Type.TArray<Type.TString>]>>;
92
115
  input: Type.TOptional<Type.TString>;
93
116
  timeout: Type.TOptional<Type.TNumber>;
117
+ /**
118
+ * Idle watchdog override (ms) for agent-running phases. A positive value (>= 1000)
119
+ * replaces the host default (300000ms): if a subagent produces no output for this
120
+ * long it is killed as stalled. `0` DISABLES the watchdog — but then a finite
121
+ * wall `timeout` (>= 1000) is REQUIRED on this phase so it can never hang forever.
122
+ * Overrides the flow-level `idleTimeout`. Absent → flow-level or host default.
123
+ */
124
+ idleTimeout: Type.TOptional<Type.TNumber>;
94
125
  until: Type.TOptional<Type.TString>;
95
126
  maxIterations: Type.TOptional<Type.TNumber>;
96
127
  convergence: Type.TOptional<Type.TBoolean>;
@@ -210,6 +241,14 @@ export declare const TaskflowSchema: Type.TObject<{
210
241
  strictInterpolation: Type.TOptional<Type.TBoolean>;
211
242
  contextSharing: Type.TOptional<Type.TBoolean>;
212
243
  incremental: Type.TOptional<Type.TBoolean>;
244
+ /**
245
+ * Flow-level idle watchdog (ms) for all agent-running phases that don't set
246
+ * their own `idleTimeout`. Positive (>= 1000) overrides the host default
247
+ * (300000ms); `0` disables the watchdog for those phases — but then every
248
+ * agent-running phase MUST declare a finite wall `timeout` (>= 1000) so the
249
+ * flow can never hang forever. A per-phase `idleTimeout` overrides this.
250
+ */
251
+ idleTimeout: Type.TOptional<Type.TNumber>;
213
252
  phases: Type.TArray<Type.TObject<{
214
253
  id: Type.TString;
215
254
  type: Type.TOptional<Type.TUnsafe<"agent" | "approval" | "expand" | "flow" | "gate" | "loop" | "map" | "parallel" | "race" | "reduce" | "script" | "tournament">>;
@@ -220,6 +259,7 @@ export declare const TaskflowSchema: Type.TObject<{
220
259
  branches: Type.TOptional<Type.TArray<Type.TObject<{
221
260
  task: Type.TString;
222
261
  agent: Type.TOptional<Type.TString>;
262
+ cwd: Type.TOptional<Type.TString>;
223
263
  }>>>;
224
264
  /**
225
265
  * [race] When true (default), abort in-flight loser branches after the first branch
@@ -228,6 +268,21 @@ export declare const TaskflowSchema: Type.TObject<{
228
268
  */
229
269
  cancelLosers: Type.TOptional<Type.TBoolean>;
230
270
  from: Type.TOptional<Type.TArray<Type.TString>>;
271
+ /**
272
+ * [reduce] How the aggregated `from[]` inputs are reduced. `'one-shot'`
273
+ * (default) feeds all inputs to a single reducer call as `{previous.output}`.
274
+ * `'tree'` batches the inputs (see `batchSize`) and runs intermediate
275
+ * reducer calls over each batch, then reduces the round outputs until one
276
+ * remains — useful when the aggregated input would exceed a single prompt.
277
+ * Tree reduction uses the SAME agent/model/options/timeout/idleTimeout as
278
+ * the phase and forces the imperative runtime (the event kernel falls back).
279
+ * The corrected `{previous.output}` aggregation (all `from[]` sources) always
280
+ * applies regardless of strategy.
281
+ */
282
+ reduceStrategy: Type.TOptional<Type.TUnsafe<"one-shot" | "tree">>;
283
+ /** [reduce] With `reduceStrategy:'tree'`, the max number of aggregated inputs
284
+ * fed to each intermediate reducer call (>= 2). Ignored for one-shot. */
285
+ batchSize: Type.TOptional<Type.TInteger>;
231
286
  use: Type.TOptional<Type.TString>;
232
287
  /** [expand] Fragment source — string interpolation or inline Taskflow / phases (same as flow.def). */
233
288
  def: Type.TOptional<Type.TUnknown>;
@@ -239,6 +294,14 @@ export declare const TaskflowSchema: Type.TObject<{
239
294
  run: Type.TOptional<Type.TUnion<[Type.TString, Type.TArray<Type.TString>]>>;
240
295
  input: Type.TOptional<Type.TString>;
241
296
  timeout: Type.TOptional<Type.TNumber>;
297
+ /**
298
+ * Idle watchdog override (ms) for agent-running phases. A positive value (>= 1000)
299
+ * replaces the host default (300000ms): if a subagent produces no output for this
300
+ * long it is killed as stalled. `0` DISABLES the watchdog — but then a finite
301
+ * wall `timeout` (>= 1000) is REQUIRED on this phase so it can never hang forever.
302
+ * Overrides the flow-level `idleTimeout`. Absent → flow-level or host default.
303
+ */
304
+ idleTimeout: Type.TOptional<Type.TNumber>;
242
305
  until: Type.TOptional<Type.TString>;
243
306
  maxIterations: Type.TOptional<Type.TNumber>;
244
307
  convergence: Type.TOptional<Type.TBoolean>;
@@ -292,6 +355,10 @@ export interface ShorthandStep {
292
355
  context?: string[];
293
356
  /** Max characters per context file (pass-through to Phase.contextLimit). */
294
357
  contextLimit?: number;
358
+ /** Working directory for this step's subagent (pass-through to Phase.cwd).
359
+ * A literal path or a reserved workspace keyword ('temp'/'dedicated'/
360
+ * 'worktree'). A top-level `cwd` is the default; a step cwd overrides it. */
361
+ cwd?: string;
295
362
  }
296
363
  /** True when `def` is a shorthand spec (no `phases`, but a task/tasks/chain field). */
297
364
  export declare function isShorthand(def: unknown): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"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"}
package/dist/schema.js CHANGED
@@ -11,9 +11,13 @@ import { scorerShapeErrors } from "./scorers.js";
11
11
  import { Type } from "typebox";
12
12
  import { Errors as SchemaErrors } from "typebox/value";
13
13
  import { cwdArgName, hasCwdPlaceholder, normalizeRelativePath } from "./cwd-bridge.js";
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;
@@ -61,6 +77,9 @@ const CACHE_CROSS_RUN_BLOCKED_TYPES = ["gate", "approval", "loop", "tournament",
61
77
  const ParallelTaskSchema = Type.Object({
62
78
  task: Type.String({ description: "Task for this parallel branch (supports interpolation)" }),
63
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
+ })),
64
83
  }, { additionalProperties: false });
65
84
  /** Declarative retry policy for a phase's subagent call(s). */
66
85
  const RetrySchema = Type.Object({
@@ -110,6 +129,24 @@ const PhaseSchema = Type.Object({
110
129
  cancelLosers: Type.Optional(Type.Boolean({ default: true })),
111
130
  // reduce
112
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." })),
113
150
  // sub-workflow (flow) + expand fragment
114
151
  use: Type.Optional(Type.String({ description: "[flow] Name of a saved taskflow to run as this phase" })),
115
152
  /** [expand] Fragment source — string interpolation or inline Taskflow / phases (same as flow.def). */
@@ -137,6 +174,16 @@ const PhaseSchema = Type.Object({
137
174
  timeout: Type.Optional(Type.Number({
138
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.",
139
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
+ })),
140
187
  // loop-until-done
141
188
  until: Type.Optional(Type.String({
142
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).",
@@ -279,6 +326,16 @@ export const TaskflowSchema = Type.Object({
279
326
  incremental: Type.Optional(Type.Boolean({
280
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.",
281
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
+ })),
282
339
  phases: Type.Array(PhaseSchema, { minItems: 1, description: "Ordered phase definitions (DAG via dependsOn)" }),
283
340
  }, { additionalProperties: false });
284
341
  /** True when `def` is a shorthand spec (no `phases`, but a task/tasks/chain field). */
@@ -310,6 +367,8 @@ function readStep(s) {
310
367
  step.context = ctx;
311
368
  if (typeof o.contextLimit === "number")
312
369
  step.contextLimit = o.contextLimit;
370
+ if (typeof o.cwd === "string")
371
+ step.cwd = o.cwd;
313
372
  return step;
314
373
  }
315
374
  return { task: "" };
@@ -336,6 +395,13 @@ export function desugar(def) {
336
395
  meta.budget = d.budget;
337
396
  if (typeof d.strictInterpolation === "boolean")
338
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;
339
405
  const nameOf = (fallback) => (typeof d.name === "string" && d.name.trim() ? d.name.trim() : fallback);
340
406
  // chain → sequential agent phases
341
407
  if (Array.isArray(d.chain) && d.chain.length > 0) {
@@ -353,6 +419,9 @@ export function desugar(def) {
353
419
  phase.context = s.context;
354
420
  if (s.contextLimit !== undefined)
355
421
  phase.contextLimit = s.contextLimit;
422
+ const stepCwd = s.cwd ?? topCwd;
423
+ if (stepCwd)
424
+ phase.cwd = stepCwd;
356
425
  if (i > 0)
357
426
  phase.dependsOn = [`step${i}`];
358
427
  if (i === steps.length - 1)
@@ -366,8 +435,18 @@ export function desugar(def) {
366
435
  // per branch): spec-level context plus the union of step-level contexts.
367
436
  if (Array.isArray(d.tasks) && d.tasks.length > 0) {
368
437
  const steps = d.tasks.map(readStep);
369
- const branches = steps.map((s) => (s.agent ? { task: s.task, agent: s.agent } : { task: s.task }));
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
+ });
370
447
  const phase = { id: "parallel", type: "parallel", branches, final: true };
448
+ if (topCwd)
449
+ phase.cwd = topCwd;
371
450
  const shared = [...(readContextList(d.context) ?? []), ...steps.flatMap((s) => s.context ?? [])];
372
451
  if (shared.length)
373
452
  phase.context = Array.from(new Set(shared));
@@ -389,6 +468,8 @@ export function desugar(def) {
389
468
  phase.context = ctx;
390
469
  if (typeof d.contextLimit === "number")
391
470
  phase.contextLimit = d.contextLimit;
471
+ if (topCwd)
472
+ phase.cwd = topCwd;
392
473
  return { name: nameOf("task"), ...meta, phases: [phase] };
393
474
  }
394
475
  throw new Error("Shorthand spec needs one of: 'task' (single), 'tasks' (parallel), or 'chain' (sequential)");
@@ -524,6 +605,17 @@ export function validateTaskflow(def, opts = {}) {
524
605
  warnings.push(`Invocation argument '${name}' is undeclared`);
525
606
  }
526
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
+ }
527
619
  // Hardening for runtime-generated (untrusted) sub-flows: bound breadth and
528
620
  // contain filesystem access. These do NOT apply to authored/saved flows.
529
621
  if (opts.dynamic) {
@@ -579,6 +671,13 @@ export function validateTaskflow(def, opts = {}) {
579
671
  if (typeof p.cwd === "string") {
580
672
  errors.push(`Dynamic sub-flow phase '${p.id}': cwd selection is not allowed in generated flows`);
581
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`);
678
+ }
679
+ });
680
+ }
582
681
  }
583
682
  }
584
683
  const ids = new Set();
@@ -670,11 +769,35 @@ export function validateTaskflow(def, opts = {}) {
670
769
  // Branch entries become competitors at runtime (b.task is interpolated); a
671
770
  // non-object / non-string-task entry would crash the runtime, so reject it.
672
771
  if (Array.isArray(p.branches)) {
772
+ const branchPhaseType = (p.type ?? "agent");
673
773
  p.branches.forEach((b, i) => {
674
774
  if (!b || typeof b !== "object" || Array.isArray(b))
675
775
  errors.push(`Phase '${p.id}': branches[${i}] must be an object with a 'task', got ${b === null ? "null" : typeof b}`);
676
776
  else if (typeof b.task !== "string")
677
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
+ }
678
801
  });
679
802
  }
680
803
  // tools entries are matched against a Set by the adapters (t => set.has(t));
@@ -812,6 +935,33 @@ export function validateTaskflow(def, opts = {}) {
812
935
  errors.push(`Phase '${p.id}' (reduce) requires 'from'`);
813
936
  if (!p.task)
814
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
+ }
815
965
  }
816
966
  if (type === "flow") {
817
967
  const hasUse = typeof p.use === "string" && p.use.length > 0;
@@ -883,6 +1033,32 @@ export function validateTaskflow(def, opts = {}) {
883
1033
  errors.push(`Phase '${p.id}' (${type}): 'timeout' must be a number >= 1000 ms`);
884
1034
  }
885
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
+ }
886
1062
  if (p.retry) {
887
1063
  if (typeof p.retry.max !== "number" || p.retry.max < 0) {
888
1064
  errors.push(`Phase '${p.id}': retry.max must be a number >= 0`);