@smithers-orchestrator/graph 0.24.0 → 0.25.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smithers-orchestrator/graph",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Framework-neutral Smithers workflow graph model and extraction helpers",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -22,7 +22,7 @@
22
22
  "dependencies": {
23
23
  "drizzle-orm": "^0.45.2",
24
24
  "zod": "^4.3.6",
25
- "@smithers-orchestrator/errors": "0.24.0"
25
+ "@smithers-orchestrator/errors": "0.25.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/bun": "latest",
@@ -0,0 +1 @@
1
+ export type { TaskAspects } from "./types";
@@ -3,15 +3,16 @@
3
3
  /** @typedef {import("../HostText.ts").HostText} HostText */
4
4
  // @smithers-type-exports-end
5
5
 
6
- import { resolveStableId } from "@smithers-orchestrator/graph/utils/tree-ids";
6
+ import { resolveStableId } from "../utils/tree-ids.js";
7
7
  import { getTableName } from "drizzle-orm";
8
- import { DEFAULT_MERGE_QUEUE_CONCURRENCY, WORKTREE_EMPTY_PATH_ERROR, } from "@smithers-orchestrator/graph/constants";
8
+ import { DEFAULT_MERGE_QUEUE_CONCURRENCY, WORKTREE_EMPTY_PATH_ERROR, } from "../constants.js";
9
9
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
10
- import { resolveWorktreePath } from "@smithers-orchestrator/graph/worktree-path";
10
+ import { resolveWorktreePath } from "../worktree-path.js";
11
11
 
12
12
  /** @typedef {import("../ExtractOptions.ts").ExtractOptions} ExtractOptions */
13
13
  /** @typedef {import("../ExtractResult.ts").ExtractResult} ExtractResult */
14
14
  /** @typedef {import("../HostNode.ts").HostNode} HostNode */
15
+ /** @typedef {import("../TaskDescriptor.ts").TaskDescriptor} TaskDescriptor */
15
16
  /** @typedef {import("../XmlNode.ts").XmlNode} XmlNode */
16
17
 
17
18
  // TODO(migration): Delegate extractFromHost to
@@ -48,13 +49,13 @@ const DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS = envHeartbeatTimeoutMs();
48
49
  const DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS = envHeartbeatTimeoutMs();
49
50
  /**
50
51
  * @param {unknown} value
51
- * @returns {boolean}
52
+ * @returns {value is any}
52
53
  */
53
54
  function isDrizzleTable(value) {
54
55
  if (!value || typeof value !== "object")
55
56
  return false;
56
57
  try {
57
- const name = getTableName(value);
58
+ const name = getTableName(/** @type {any} */ (value));
58
59
  return typeof name === "string" && name.length > 0;
59
60
  }
60
61
  catch {
@@ -63,7 +64,7 @@ function isDrizzleTable(value) {
63
64
  }
64
65
  /**
65
66
  * @param {unknown} value
66
- * @returns {boolean}
67
+ * @returns {value is import("zod").ZodObject<any>}
67
68
  */
68
69
  function isZodObject(value) {
69
70
  return Boolean(value && typeof value === "object" && "shape" in value);
@@ -83,6 +84,15 @@ function parseHeartbeatTimeoutMs(raw) {
83
84
  }
84
85
  return Math.floor(candidate);
85
86
  }
87
+ /**
88
+ * @param {unknown} value
89
+ * @returns {Record<string, unknown>}
90
+ */
91
+ function recordOrEmpty(value) {
92
+ return value && typeof value === "object" && !Array.isArray(value)
93
+ ? /** @type {Record<string, unknown>} */ (value)
94
+ : {};
95
+ }
86
96
  /**
87
97
  * @param {HostNode} node
88
98
  * @returns {XmlNode}
@@ -92,7 +102,7 @@ function toXmlNode(node) {
92
102
  return { kind: "text", text: node.text };
93
103
  }
94
104
  const element = {
95
- kind: "element",
105
+ kind: /** @type {"element"} */ ("element"),
96
106
  tag: node.tag,
97
107
  props: node.props ?? {},
98
108
  children: node.children.map(toXmlNode),
@@ -112,11 +122,13 @@ function getRalphIteration(opts, id) {
112
122
  if (map instanceof Map) {
113
123
  return map.get(id) ?? fallback;
114
124
  }
115
- const value = map[id];
125
+ const value = /** @type {Record<string, number>} */ (map)[id];
116
126
  return typeof value === "number" ? value : fallback;
117
127
  }
118
128
  /**
119
129
  * @param {Record<string, unknown>} raw
130
+ * @param {boolean} [isAgent]
131
+ * @returns {{ retries: number; retryPolicy: import("../RetryPolicy.ts").RetryPolicy | undefined }}
120
132
  */
121
133
  function resolveRetryConfig(raw, isAgent = false) {
122
134
  const noRetry = Boolean(raw.noRetry);
@@ -139,7 +151,7 @@ function resolveRetryConfig(raw, isAgent = false) {
139
151
  const retryPolicy = hasExplicitRetryPolicy
140
152
  ? /** @type {import("../RetryPolicy.ts").RetryPolicy} */ (raw.retryPolicy)
141
153
  : retries > 0
142
- ? { backoff: "exponential", initialDelayMs: 1000 }
154
+ ? /** @type {import("../RetryPolicy.ts").RetryPolicy} */ ({ backoff: "exponential", initialDelayMs: 1000 })
143
155
  : undefined;
144
156
  return { retries, retryPolicy };
145
157
  }
@@ -152,7 +164,9 @@ export function extractFromHost(root, opts) {
152
164
  if (!root) {
153
165
  return { xml: null, tasks: [], mountedTaskIds: [] };
154
166
  }
167
+ /** @type {TaskDescriptor[]} */
155
168
  const tasks = [];
169
+ /** @type {string[]} */
156
170
  const mountedTaskIds = [];
157
171
  const seen = new Set();
158
172
  const seenRalph = new Set();
@@ -251,7 +265,10 @@ export function extractFromHost(root, opts) {
251
265
  if (!pathVal) {
252
266
  throw new SmithersError("WORKTREE_EMPTY_PATH", WORKTREE_EMPTY_PATH_ERROR);
253
267
  }
254
- const normPath = resolveWorktreePath(pathVal, { baseRootDir: opts?.baseRootDir });
268
+ const normPath = resolveWorktreePath(pathVal, {
269
+ baseRootDir: opts?.baseRootDir,
270
+ workflowPath: opts?.workflowPath,
271
+ });
255
272
  const branch = node.rawProps?.branch ? String(node.rawProps.branch) : undefined;
256
273
  const baseBranch = node.rawProps?.baseBranch ? String(node.rawProps.baseBranch) : undefined;
257
274
  nextWorktreeStack = [...worktreeStack, { id, path: normPath, branch, baseBranch }];
@@ -276,7 +293,7 @@ export function extractFromHost(root, opts) {
276
293
  }
277
294
  const outputTable = isDrizzleTable(outputRaw) ? outputRaw : null;
278
295
  const outputTableName = outputTable
279
- ? getTableName(outputTable)
296
+ ? getTableName(/** @type {any} */ (outputTable))
280
297
  : typeof outputRaw === "string"
281
298
  ? outputRaw
282
299
  : "";
@@ -285,7 +302,9 @@ export function extractFromHost(root, opts) {
285
302
  const timeoutMs = typeof raw.timeoutMs === "number" ? raw.timeoutMs : null;
286
303
  const heartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
287
304
  const continueOnFail = Boolean(raw.continueOnFail);
288
- const cachePolicy = raw.cache && typeof raw.cache === "object" ? raw.cache : undefined;
305
+ const cachePolicy = raw.cache && typeof raw.cache === "object"
306
+ ? /** @type {import("../CachePolicy.ts").CachePolicy<unknown>} */ (raw.cache)
307
+ : undefined;
289
308
  const dependsOn = Array.isArray(raw.dependsOn)
290
309
  ? raw.dependsOn.filter((v) => typeof v === "string")
291
310
  : undefined;
@@ -340,9 +359,9 @@ export function extractFromHost(root, opts) {
340
359
  }
341
360
  return result.output;
342
361
  },
343
- label: raw.label,
362
+ label: typeof raw.label === "string" ? raw.label : undefined,
344
363
  meta: {
345
- ...raw.meta,
364
+ ...recordOrEmpty(raw.meta),
346
365
  __subflow: true,
347
366
  __subflowMode: mode,
348
367
  __subflowInput: raw.__smithersSubflowInput,
@@ -374,7 +393,7 @@ export function extractFromHost(root, opts) {
374
393
  }
375
394
  const outputTable = isDrizzleTable(outputRaw) ? outputRaw : null;
376
395
  const outputTableName = outputTable
377
- ? getTableName(outputTable)
396
+ ? getTableName(/** @type {any} */ (outputTable))
378
397
  : typeof outputRaw === "string"
379
398
  ? outputRaw
380
399
  : "";
@@ -383,7 +402,9 @@ export function extractFromHost(root, opts) {
383
402
  const timeoutMs = typeof raw.timeoutMs === "number" ? raw.timeoutMs : null;
384
403
  const heartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw) ?? DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS;
385
404
  const continueOnFail = Boolean(raw.continueOnFail);
386
- const cachePolicy = raw.cache && typeof raw.cache === "object" ? raw.cache : undefined;
405
+ const cachePolicy = raw.cache && typeof raw.cache === "object"
406
+ ? /** @type {import("../CachePolicy.ts").CachePolicy<unknown>} */ (raw.cache)
407
+ : undefined;
387
408
  const dependsOn = Array.isArray(raw.dependsOn)
388
409
  ? raw.dependsOn.filter((v) => typeof v === "string")
389
410
  : undefined;
@@ -468,9 +489,9 @@ export function extractFromHost(root, opts) {
468
489
  },
469
490
  });
470
491
  },
471
- label: raw.label,
492
+ label: typeof raw.label === "string" ? raw.label : undefined,
472
493
  meta: {
473
- ...raw.meta,
494
+ ...recordOrEmpty(raw.meta),
474
495
  __sandbox: true,
475
496
  __sandboxRuntime: runtime,
476
497
  __sandboxProvider: provider,
@@ -520,12 +541,12 @@ export function extractFromHost(root, opts) {
520
541
  }
521
542
  const outputTable = isDrizzleTable(outputRaw) ? outputRaw : null;
522
543
  const outputTableName = outputTable
523
- ? getTableName(outputTable)
544
+ ? getTableName(/** @type {any} */ (outputTable))
524
545
  : typeof outputRaw === "string"
525
546
  ? outputRaw
526
547
  : "";
527
548
  const outputRef = !outputTable && isZodObject(outputRaw) ? outputRaw : undefined;
528
- const outputSchema = raw.outputSchema ?? outputRef;
549
+ const outputSchema = isZodObject(raw.outputSchema) ? raw.outputSchema : outputRef;
529
550
  const waitAsync = Boolean(raw.waitAsync);
530
551
  const timeoutMs = typeof raw.timeoutMs === "number" ? raw.timeoutMs : null;
531
552
  const heartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
@@ -564,9 +585,9 @@ export function extractFromHost(root, opts) {
564
585
  prompt: undefined,
565
586
  staticPayload: undefined,
566
587
  computeFn: undefined,
567
- label: raw.label,
588
+ label: typeof raw.label === "string" ? raw.label : undefined,
568
589
  meta: {
569
- ...raw.meta,
590
+ ...recordOrEmpty(raw.meta),
570
591
  __waitForEvent: true,
571
592
  __eventName: raw.__smithersEventName ?? raw.event,
572
593
  __correlationId: raw.__smithersCorrelationId ?? raw.correlationId,
@@ -646,9 +667,9 @@ export function extractFromHost(root, opts) {
646
667
  prompt: undefined,
647
668
  staticPayload: undefined,
648
669
  computeFn: undefined,
649
- label: raw.label ?? `timer:${nodeId}`,
670
+ label: typeof raw.label === "string" ? raw.label : `timer:${nodeId}`,
650
671
  meta: {
651
- ...raw.meta,
672
+ ...recordOrEmpty(raw.meta),
652
673
  __timer: true,
653
674
  __timerType: hasDuration ? "duration" : "absolute",
654
675
  ...(hasDuration ? { __timerDuration: duration } : {}),
@@ -698,19 +719,21 @@ export function extractFromHost(root, opts) {
698
719
  }
699
720
  const outputTable = isDrizzleTable(outputRaw) ? outputRaw : null;
700
721
  const outputTableName = outputTable
701
- ? getTableName(outputTable)
722
+ ? getTableName(/** @type {any} */ (outputTable))
702
723
  : typeof outputRaw === "string"
703
724
  ? outputRaw
704
725
  : "";
705
726
  const outputRef = !outputTable && isZodObject(outputRaw) ? outputRaw : undefined;
706
- const outputSchema = raw.outputSchema ?? outputRef;
727
+ const outputSchema = isZodObject(raw.outputSchema) ? raw.outputSchema : outputRef;
707
728
  const needsApproval = Boolean(raw.needsApproval);
708
729
  const waitAsync = Boolean(raw.waitAsync);
730
+ /** @type {TaskDescriptor["approvalMode"]} */
709
731
  const approvalMode = raw.approvalMode === "decision" ||
710
732
  raw.approvalMode === "select" ||
711
733
  raw.approvalMode === "rank"
712
734
  ? raw.approvalMode
713
735
  : "gate";
736
+ /** @type {TaskDescriptor["approvalOnDeny"]} */
714
737
  const approvalOnDeny = raw.approvalOnDeny === "continue" ||
715
738
  raw.approvalOnDeny === "skip" ||
716
739
  raw.approvalOnDeny === "fail"
@@ -735,31 +758,36 @@ export function extractFromHost(root, opts) {
735
758
  const approvalAllowedUsers = Array.isArray(raw.approvalAllowedUsers)
736
759
  ? raw.approvalAllowedUsers.filter((value) => typeof value === "string")
737
760
  : undefined;
738
- const approvalAutoApprove = raw.approvalAutoApprove && typeof raw.approvalAutoApprove === "object" && !Array.isArray(raw.approvalAutoApprove)
761
+ const approvalAutoApproveRaw = raw.approvalAutoApprove && typeof raw.approvalAutoApprove === "object" && !Array.isArray(raw.approvalAutoApprove)
762
+ ? /** @type {Record<string, unknown>} */ (raw.approvalAutoApprove)
763
+ : undefined;
764
+ const approvalAutoApprove = approvalAutoApproveRaw
739
765
  ? {
740
- ...(typeof raw.approvalAutoApprove.after === "number"
741
- ? { after: raw.approvalAutoApprove.after }
766
+ ...(typeof approvalAutoApproveRaw.after === "number"
767
+ ? { after: approvalAutoApproveRaw.after }
742
768
  : {}),
743
- ...(typeof raw.approvalAutoApprove.audit === "boolean"
744
- ? { audit: raw.approvalAutoApprove.audit }
769
+ ...(typeof approvalAutoApproveRaw.audit === "boolean"
770
+ ? { audit: approvalAutoApproveRaw.audit }
745
771
  : {}),
746
- ...(typeof raw.approvalAutoApprove.conditionMet === "boolean"
747
- ? { conditionMet: raw.approvalAutoApprove.conditionMet }
772
+ ...(typeof approvalAutoApproveRaw.conditionMet === "boolean"
773
+ ? { conditionMet: approvalAutoApproveRaw.conditionMet }
748
774
  : {}),
749
- ...(typeof raw.approvalAutoApprove.revertOnMet === "boolean"
750
- ? { revertOnMet: raw.approvalAutoApprove.revertOnMet }
775
+ ...(typeof approvalAutoApproveRaw.revertOnMet === "boolean"
776
+ ? { revertOnMet: approvalAutoApproveRaw.revertOnMet }
751
777
  : {}),
752
778
  }
753
779
  : undefined;
754
780
  const skipIf = Boolean(raw.skipIf);
755
- const agent = raw.agent;
781
+ const agent = /** @type {TaskDescriptor["agent"]} */ (raw.agent);
756
782
  const kind = raw.__smithersKind;
757
783
  const isAgent = kind === "agent" || Boolean(agent);
758
784
  const { retries, retryPolicy } = resolveRetryConfig(raw, isAgent);
759
785
  const timeoutMs = typeof raw.timeoutMs === "number" ? raw.timeoutMs : null;
760
786
  const parsedHeartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
761
787
  const continueOnFail = Boolean(raw.continueOnFail);
762
- const cachePolicy = raw.cache && typeof raw.cache === "object" ? raw.cache : undefined;
788
+ const cachePolicy = raw.cache && typeof raw.cache === "object"
789
+ ? /** @type {import("../CachePolicy.ts").CachePolicy<unknown>} */ (raw.cache)
790
+ : undefined;
763
791
  const heartbeatTimeoutMs = parsedHeartbeatTimeoutMs ??
764
792
  (isAgent ? DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS : null);
765
793
  const prompt = isAgent ? String(raw.children ?? "") : undefined;
@@ -767,8 +795,8 @@ export function extractFromHost(root, opts) {
767
795
  throw new SmithersError("MDX_PRELOAD_INACTIVE", `Task "${raw.id ?? nodeId}" prompt resolved to [object Object] — MDX preload is likely not active.\n` +
768
796
  `Check that bunfig.toml has a top-level preload (not under [run]) and mdxPlugin() is registered.`);
769
797
  }
770
- const isCompute = kind === "compute" && typeof raw.__smithersComputeFn === "function";
771
- const computeFn = isCompute ? raw.__smithersComputeFn : undefined;
798
+ const isCompute = (kind === "compute" || kind === "human") && typeof raw.__smithersComputeFn === "function";
799
+ const computeFn = isCompute ? /** @type {() => unknown} */ (raw.__smithersComputeFn) : undefined;
772
800
  const staticPayload = isAgent || isCompute
773
801
  ? undefined
774
802
  : (raw.__smithersPayload ?? raw.__payload ?? raw.children);
@@ -811,17 +839,23 @@ export function extractFromHost(root, opts) {
811
839
  continueOnFail,
812
840
  cachePolicy,
813
841
  hijack: Boolean(raw.hijack),
814
- onHijackExit: raw.onHijackExit === "reopen" ? "reopen" : "complete",
842
+ onHijackExit: /** @type {"reopen" | "complete"} */ (raw.onHijackExit === "reopen" ? "reopen" : "complete"),
815
843
  agent,
816
844
  prompt,
817
845
  staticPayload,
818
846
  computeFn,
819
- label: raw.label,
820
- meta: raw.meta,
821
- scorers: raw.scorers,
847
+ label: typeof raw.label === "string" ? raw.label : undefined,
848
+ meta: raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
849
+ ? /** @type {Record<string, unknown>} */ (raw.meta)
850
+ : undefined,
851
+ scorers: raw.scorers && typeof raw.scorers === "object" && !Array.isArray(raw.scorers)
852
+ ? /** @type {import("../ScorersMap.ts").ScorersMap} */ (raw.scorers)
853
+ : undefined,
822
854
  parallelGroupId: parallelGroup?.id,
823
855
  parallelMaxConcurrency: parallelGroup?.max,
824
- memoryConfig: raw.memory ?? undefined,
856
+ memoryConfig: raw.memory && typeof raw.memory === "object" && !Array.isArray(raw.memory)
857
+ ? /** @type {TaskDescriptor["memoryConfig"]} */ (raw.memory)
858
+ : undefined,
825
859
  };
826
860
  // Worktree path is captured in typed fields (worktreeId/worktreePath) and
827
861
  // consumed by the engine; avoid attaching untyped ad-hoc properties.
package/src/extract.js CHANGED
@@ -2,14 +2,12 @@ import { getTableName } from "drizzle-orm";
2
2
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
3
3
  import { validateForkSources } from "./validateForkSources.js";
4
4
  import { resolveWorktreePath } from "./worktree-path.js";
5
+ import { DEFAULT_MERGE_QUEUE_CONCURRENCY, WORKTREE_EMPTY_PATH_ERROR } from "./constants.js";
5
6
  /** @typedef {import("./TaskDescriptor.ts").TaskDescriptor} TaskDescriptor */
6
7
  /** @typedef {import("./XmlNode.ts").XmlNode} XmlNode */
7
8
  /** @typedef {import("./ExtractOptions.ts").ExtractOptions} ExtractOptions */
8
9
  /** @typedef {import("./HostNode.ts").HostNode} HostNode */
9
10
  /** @typedef {import("./WorkflowGraph.ts").WorkflowGraph} WorkflowGraph */
10
-
11
- const DEFAULT_MERGE_QUEUE_CONCURRENCY = 1;
12
- const WORKTREE_EMPTY_PATH_ERROR = "<Worktree> requires a non-empty path prop";
13
11
  // Default per-task heartbeat timeout. 10 min is the floor for agent-backed
14
12
  // tasks: LLM CLIs (claude, codex, gemini, kimi) can sit silent for several
15
13
  // minutes during long deliberation or large-context reads. The previous
@@ -60,13 +58,13 @@ function isZodObject(value) {
60
58
  }
61
59
  /**
62
60
  * @param {unknown} value
63
- * @returns {boolean}
61
+ * @returns {value is any}
64
62
  */
65
63
  function isDrizzleTable(value) {
66
64
  if (!value || typeof value !== "object")
67
65
  return false;
68
66
  try {
69
- const name = getTableName(value);
67
+ const name = getTableName(/** @type {any} */ (value));
70
68
  return typeof name === "string" && name.length > 0;
71
69
  }
72
70
  catch {
@@ -89,7 +87,7 @@ function resolveOutput(raw) {
89
87
  }
90
88
  const outputTable = isDrizzleTable(outputRaw) ? outputRaw : null;
91
89
  const outputTableName = outputTable
92
- ? getTableName(outputTable)
90
+ ? getTableName(/** @type {any} */ (outputTable))
93
91
  : typeof outputRaw === "string"
94
92
  ? outputRaw
95
93
  : "";
@@ -119,6 +117,8 @@ function parseHeartbeatTimeoutMs(raw) {
119
117
  }
120
118
  /**
121
119
  * @param {Record<string, unknown>} raw
120
+ * @param {boolean} [isAgent]
121
+ * @returns {{ retries: number; retryPolicy: import("./RetryPolicy.ts").RetryPolicy | undefined }}
122
122
  */
123
123
  function resolveRetryConfig(raw, isAgent = false) {
124
124
  const noRetry = Boolean(raw.noRetry);
@@ -135,12 +135,12 @@ function resolveRetryConfig(raw, isAgent = false) {
135
135
  : defaultNoRetryForContinueOnFail
136
136
  ? (isAgent ? 1 : 0)
137
137
  : hasExplicitRetries
138
- ? raw.retries
138
+ ? /** @type {number} */ (raw.retries)
139
139
  : Infinity;
140
140
  const retryPolicy = hasExplicitRetryPolicy
141
- ? raw.retryPolicy
141
+ ? /** @type {import("./RetryPolicy.ts").RetryPolicy} */ (raw.retryPolicy)
142
142
  : retries > 0
143
- ? { backoff: "exponential", initialDelayMs: 1000 }
143
+ ? /** @type {import("./RetryPolicy.ts").RetryPolicy} */ ({ backoff: "exponential", initialDelayMs: 1000 })
144
144
  : undefined;
145
145
  return { retries, retryPolicy };
146
146
  }
@@ -153,7 +153,7 @@ function toXmlNode(node) {
153
153
  return { kind: "text", text: node.text };
154
154
  }
155
155
  const element = {
156
- kind: "element",
156
+ kind: /** @type {"element"} */ ("element"),
157
157
  tag: node.tag,
158
158
  props: node.props ?? {},
159
159
  children: node.children.map(toXmlNode),
@@ -173,7 +173,7 @@ function getRalphIteration(opts, id) {
173
173
  if (map instanceof Map) {
174
174
  return map.get(id) ?? fallback;
175
175
  }
176
- const value = map[id];
176
+ const value = /** @type {Record<string, number>} */ (map)[id];
177
177
  return typeof value === "number" ? value : fallback;
178
178
  }
179
179
  /**
@@ -236,7 +236,7 @@ function approvalAutoApprove(value) {
236
236
  if (!value || typeof value !== "object" || Array.isArray(value)) {
237
237
  return undefined;
238
238
  }
239
- const raw = value;
239
+ const raw = /** @type {Record<string, unknown>} */ (value);
240
240
  return {
241
241
  ...(typeof raw.after === "number" ? { after: raw.after } : {}),
242
242
  ...(typeof raw.audit === "boolean" ? { audit: raw.audit } : {}),
@@ -248,6 +248,49 @@ function approvalAutoApprove(value) {
248
248
  : {}),
249
249
  };
250
250
  }
251
+ /**
252
+ * Normalize the `__aspects` element prop attached by `<Task>` into the
253
+ * `TaskAspects` budget metadata the engine enforces. Only the budget configs
254
+ * are kept; the render-time accumulator and tracking flags are dropped (the
255
+ * engine keeps its own durable per-run accumulator and budgets enforce
256
+ * regardless of tracking).
257
+ *
258
+ * @param {unknown} value
259
+ * @returns {import("./types").TaskAspects | undefined}
260
+ */
261
+ function aspects(value) {
262
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
263
+ return undefined;
264
+ }
265
+ const raw = /** @type {Record<string, unknown>} */ (value);
266
+ /** @type {import("./types").TaskAspects} */
267
+ const out = {};
268
+ const token = raw.tokenBudget;
269
+ if (token && typeof token === "object" && !Array.isArray(token) &&
270
+ typeof (/** @type {Record<string, unknown>} */ (token).max) === "number") {
271
+ const t = /** @type {Record<string, unknown>} */ (token);
272
+ out.tokenBudget = {
273
+ max: /** @type {number} */ (t.max),
274
+ ...(typeof t.perTask === "number" ? { perTask: t.perTask } : {}),
275
+ ...(t.onExceeded === "warn" || t.onExceeded === "skip-remaining" || t.onExceeded === "fail"
276
+ ? { onExceeded: /** @type {"fail" | "warn" | "skip-remaining"} */ (t.onExceeded) }
277
+ : {}),
278
+ };
279
+ }
280
+ const latency = raw.latencySlo;
281
+ if (latency && typeof latency === "object" && !Array.isArray(latency) &&
282
+ typeof (/** @type {Record<string, unknown>} */ (latency).maxMs) === "number") {
283
+ const l = /** @type {Record<string, unknown>} */ (latency);
284
+ out.latencySlo = {
285
+ maxMs: /** @type {number} */ (l.maxMs),
286
+ ...(typeof l.perTask === "number" ? { perTask: l.perTask } : {}),
287
+ ...(l.onExceeded === "warn" || l.onExceeded === "fail"
288
+ ? { onExceeded: /** @type {"fail" | "warn"} */ (l.onExceeded) }
289
+ : {}),
290
+ };
291
+ }
292
+ return out.tokenBudget || out.latencySlo ? out : undefined;
293
+ }
251
294
  /**
252
295
  * @param {"parallel" | "merge-queue"} tag
253
296
  * @param {Record<string, unknown>} raw
@@ -300,7 +343,9 @@ export function extractGraph(root, opts) {
300
343
  if (!root) {
301
344
  return { xml: null, tasks: [], mountedTaskIds: [] };
302
345
  }
346
+ /** @type {TaskDescriptor[]} */
303
347
  const tasks = [];
348
+ /** @type {string[]} */
304
349
  const mountedTaskIds = [];
305
350
  const seen = new Set();
306
351
  const seenRalph = new Set();
@@ -408,7 +453,7 @@ export function extractGraph(root, opts) {
408
453
  heartbeatTimeoutMs: parseHeartbeatTimeoutMs(raw),
409
454
  continueOnFail: Boolean(raw.continueOnFail),
410
455
  cachePolicy: raw.cache && typeof raw.cache === "object"
411
- ? raw.cache
456
+ ? /** @type {import("./CachePolicy.ts").CachePolicy<unknown>} */ (raw.cache)
412
457
  : undefined,
413
458
  label: typeof raw.label === "string" ? raw.label : undefined,
414
459
  meta: {
@@ -441,7 +486,7 @@ export function extractGraph(root, opts) {
441
486
  heartbeatTimeoutMs: parseHeartbeatTimeoutMs(raw) ?? DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS,
442
487
  continueOnFail: Boolean(raw.continueOnFail),
443
488
  cachePolicy: raw.cache && typeof raw.cache === "object"
444
- ? raw.cache
489
+ ? /** @type {import("./CachePolicy.ts").CachePolicy<unknown>} */ (raw.cache)
445
490
  : undefined,
446
491
  label: typeof raw.label === "string" ? raw.label : undefined,
447
492
  meta: {
@@ -579,7 +624,7 @@ export function extractGraph(root, opts) {
579
624
  const kind = raw.__smithersKind;
580
625
  const isAgent = kind === "agent" || Boolean(raw.agent);
581
626
  const { retries, retryPolicy } = resolveRetryConfig(raw, isAgent);
582
- const isCompute = kind === "compute" && typeof raw.__smithersComputeFn === "function";
627
+ const isCompute = (kind === "compute" || kind === "human") && typeof raw.__smithersComputeFn === "function";
583
628
  const parsedHeartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
584
629
  const heartbeatTimeoutMs = parsedHeartbeatTimeoutMs ??
585
630
  (isAgent ? DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS : null);
@@ -606,28 +651,31 @@ export function extractGraph(root, opts) {
606
651
  heartbeatTimeoutMs,
607
652
  continueOnFail: Boolean(raw.continueOnFail),
608
653
  cachePolicy: raw.cache && typeof raw.cache === "object"
609
- ? raw.cache
654
+ ? /** @type {import("./CachePolicy.ts").CachePolicy<unknown>} */ (raw.cache)
610
655
  : undefined,
611
656
  hijack: Boolean(raw.hijack),
612
657
  onHijackExit: raw.onHijackExit === "reopen" ? "reopen" : "complete",
613
- agent: raw.agent,
658
+ agent: /** @type {TaskDescriptor["agent"]} */ (raw.agent),
614
659
  prompt,
615
660
  staticPayload: isAgent || isCompute
616
661
  ? undefined
617
662
  : (raw.__smithersPayload ?? raw.__payload ?? raw.children),
618
663
  computeFn: isCompute
619
- ? raw.__smithersComputeFn
664
+ ? /** @type {() => unknown} */ (raw.__smithersComputeFn)
620
665
  : undefined,
621
666
  label: typeof raw.label === "string" ? raw.label : undefined,
622
667
  meta: raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
623
- ? raw.meta
668
+ ? /** @type {Record<string, unknown>} */ (raw.meta)
624
669
  : undefined,
625
670
  scorers: raw.scorers && typeof raw.scorers === "object" && !Array.isArray(raw.scorers)
626
- ? raw.scorers
671
+ ? /** @type {import("./ScorersMap.ts").ScorersMap} */ (raw.scorers)
627
672
  : undefined,
673
+ groundTruth: raw.groundTruth,
674
+ context: raw.context,
628
675
  memoryConfig: raw.memory && typeof raw.memory === "object" && !Array.isArray(raw.memory)
629
676
  ? raw.memory
630
677
  : undefined,
678
+ aspects: aspects(raw.__aspects),
631
679
  });
632
680
  }
633
681
  let elementIndex = 0;
package/src/index.d.ts CHANGED
@@ -109,6 +109,18 @@ type ApprovalOption$1 = {
109
109
  summary?: string;
110
110
  metadata?: Record<string, unknown>;
111
111
  };
112
+ type TaskAspects$1 = {
113
+ tokenBudget?: {
114
+ max: number;
115
+ perTask?: number;
116
+ onExceeded?: "fail" | "warn" | "skip-remaining";
117
+ };
118
+ latencySlo?: {
119
+ maxMs: number;
120
+ perTask?: number;
121
+ onExceeded?: "fail" | "warn";
122
+ };
123
+ };
112
124
  type TaskDescriptor$1 = {
113
125
  nodeId: string;
114
126
  ordinal: number;
@@ -116,6 +128,8 @@ type TaskDescriptor$1 = {
116
128
  ralphId?: string;
117
129
  dependsOn?: string[];
118
130
  needs?: Record<string, string>;
131
+ /** Logical id of the task whose final agent session this task forks. Gates execution and seeds the session. */
132
+ forkSource?: string;
119
133
  worktreeId?: string;
120
134
  worktreePath?: string;
121
135
  worktreeBranch?: string;
@@ -155,7 +169,10 @@ type TaskDescriptor$1 = {
155
169
  label?: string;
156
170
  meta?: Record<string, unknown>;
157
171
  scorers?: ScorersMap$1;
172
+ groundTruth?: unknown;
173
+ context?: unknown;
158
174
  memoryConfig?: TaskMemoryConfig$1;
175
+ aspects?: TaskAspects$1;
159
176
  };
160
177
  type WorkflowGraph$2 = {
161
178
  readonly xml: XmlNode$1 | null;
@@ -191,13 +208,18 @@ declare function extractGraph(root: HostNode$1 | null, opts?: ExtractOptions$1):
191
208
  declare function extractFromHost(root: HostNode$1 | null, opts?: ExtractOptions$1): WorkflowGraph$1;
192
209
  /**
193
210
  * Resolve a <Worktree path> prop exactly the way graph extraction resolves it.
211
+ * Relative paths are resolved against the launch root (`--root`, the nearest
212
+ * `.smithers` anchor, or the operator cwd), never `dirname(workflowPath)`.
213
+ * `workflowPath` is threaded through graph/engine rendering for workflow
214
+ * identity and diagnostics only; it is not a worktree path resolution base.
194
215
  *
195
216
  * @param {unknown} path
196
- * @param {{ baseRootDir?: string }} [opts]
217
+ * @param {{ baseRootDir?: string; workflowPath?: string | null }} [opts]
197
218
  * @returns {string}
198
219
  */
199
220
  declare function resolveWorktreePath(path: unknown, opts?: {
200
221
  baseRootDir?: string;
222
+ workflowPath?: string | null;
201
223
  }): string;
202
224
  type ExtractOptions$1 = ExtractOptions$2;
203
225
  type HostNode$1 = HostNode$2;
@@ -222,6 +244,7 @@ type ScorerBinding = ScorerBinding$1;
222
244
  type ScorerFn = ScorerFn$1;
223
245
  type ScorerInput = ScorerInput$1;
224
246
  type ScorersMap = ScorersMap$1;
247
+ type TaskAspects = TaskAspects$1;
225
248
  type TaskDescriptor = TaskDescriptor$1;
226
249
  type TaskMemoryConfig = TaskMemoryConfig$1;
227
250
  type WorkflowGraph = WorkflowGraph$2;
@@ -229,4 +252,4 @@ type XmlElement = XmlElement$1;
229
252
  type XmlNode = XmlNode$1;
230
253
  type XmlText = XmlText$1;
231
254
 
232
- export { type AgentLike, type ApprovalOption, type CachePolicy, type ExtractGraph, type ExtractOptions, type GraphSnapshot, type HostElement, type HostNode, type HostText, type MemoryNamespace, type MemoryNamespaceKind, type RetryPolicy, type SamplingConfig, type ScoreResult, type Scorer, type ScorerBinding, type ScorerFn, type ScorerInput, type ScorersMap, type TaskDescriptor, type TaskMemoryConfig, type WorkflowGraph, type XmlElement, type XmlNode, type XmlText, extractFromHost, extractGraph, resolveWorktreePath };
255
+ export { type AgentLike, type ApprovalOption, type CachePolicy, type ExtractGraph, type ExtractOptions, type GraphSnapshot, type HostElement, type HostNode, type HostText, type MemoryNamespace, type MemoryNamespaceKind, type RetryPolicy, type SamplingConfig, type ScoreResult, type Scorer, type ScorerBinding, type ScorerFn, type ScorerInput, type ScorersMap, type TaskAspects, type TaskDescriptor, type TaskMemoryConfig, type WorkflowGraph, type XmlElement, type XmlNode, type XmlText, extractFromHost, extractGraph, resolveWorktreePath };
package/src/index.js CHANGED
@@ -22,6 +22,7 @@
22
22
  /** @typedef {import("./ScorerInput.ts").ScorerInput} ScorerInput */
23
23
  /** @typedef {import("./ScorersMap.ts").ScorersMap} ScorersMap */
24
24
  /** @typedef {import("./TaskDescriptor.ts").TaskDescriptor} TaskDescriptor */
25
+ /** @typedef {import("./TaskAspects.ts").TaskAspects} TaskAspects */
25
26
  /** @typedef {import("./TaskMemoryConfig.ts").TaskMemoryConfig} TaskMemoryConfig */
26
27
  /** @typedef {import("./WorkflowGraph.ts").WorkflowGraph} WorkflowGraph */
27
28
  /** @typedef {import("./XmlElement.ts").XmlElement} XmlElement */
package/src/types.ts CHANGED
@@ -123,6 +123,26 @@ export type ApprovalOption = {
123
123
  metadata?: Record<string, unknown>;
124
124
  };
125
125
 
126
+ /**
127
+ * Resolved `<Aspects>` budget configuration that applies to a task, extracted
128
+ * from the `__aspects` element prop. The engine reads this at task-dispatch
129
+ * time to enforce per-run token and latency budgets. The render-time
130
+ * accumulator carried alongside the budgets in the component tree is dropped
131
+ * here; the engine keeps its own durable accumulator.
132
+ */
133
+ export type TaskAspects = {
134
+ tokenBudget?: {
135
+ max: number;
136
+ perTask?: number;
137
+ onExceeded?: "fail" | "warn" | "skip-remaining";
138
+ };
139
+ latencySlo?: {
140
+ maxMs: number;
141
+ perTask?: number;
142
+ onExceeded?: "fail" | "warn";
143
+ };
144
+ };
145
+
126
146
  export type TaskDescriptor = {
127
147
  nodeId: string;
128
148
  ordinal: number;
@@ -171,8 +191,12 @@ export type TaskDescriptor = {
171
191
  label?: string;
172
192
  meta?: Record<string, unknown>;
173
193
  scorers?: ScorersMap;
194
+ groundTruth?: unknown;
195
+ context?: unknown;
174
196
 
175
197
  memoryConfig?: TaskMemoryConfig;
198
+ /** Resolved `<Aspects>` budget configuration enforced by the engine at dispatch. */
199
+ aspects?: TaskAspects;
176
200
  };
177
201
 
178
202
  export type WorkflowGraph = {
@@ -44,6 +44,10 @@ export function validateForkSources(tasks) {
44
44
  // fork edge. Used only for cycle detection.
45
45
  /** @type {Map<string, Set<string>>} */
46
46
  const adjacency = new Map();
47
+ /**
48
+ * @param {string} from
49
+ * @param {string} to
50
+ */
47
51
  const addEdge = (from, to) => {
48
52
  const cleanedFrom = logicalId(from);
49
53
  const cleanedTo = logicalId(to);
@@ -1,12 +1,16 @@
1
- import { isAbsolute, resolve } from "node:path";
1
+ import { dirname, isAbsolute, resolve } from "node:path";
2
2
  import { SmithersError } from "@smithers-orchestrator/errors/SmithersError";
3
3
  import { WORKTREE_EMPTY_PATH_ERROR } from "./constants.js";
4
4
 
5
5
  /**
6
6
  * Resolve a <Worktree path> prop exactly the way graph extraction resolves it.
7
+ * Relative paths are resolved against the launch root (`--root`, the nearest
8
+ * `.smithers` anchor, or the operator cwd), never `dirname(workflowPath)`.
9
+ * `workflowPath` is threaded through graph/engine rendering for workflow
10
+ * identity and diagnostics only; it is not a worktree path resolution base.
7
11
  *
8
12
  * @param {unknown} path
9
- * @param {{ baseRootDir?: string }} [opts]
13
+ * @param {{ baseRootDir?: string; workflowPath?: string | null }} [opts]
10
14
  * @returns {string}
11
15
  */
12
16
  export function resolveWorktreePath(path, opts) {
@@ -20,5 +24,43 @@ export function resolveWorktreePath(path, opts) {
20
24
  const base = typeof opts?.baseRootDir === "string" && opts.baseRootDir.length > 0
21
25
  ? opts.baseRootDir
22
26
  : process.cwd();
23
- return resolve(base, pathVal);
27
+ const resolvedPath = resolve(base, pathVal);
28
+ warnRelativeWorktreePathOnce(pathVal, base, resolvedPath, opts?.workflowPath);
29
+ return resolvedPath;
30
+ }
31
+
32
+ let didWarnRelativeWorktreePath = false;
33
+
34
+ /**
35
+ * Reset process-local relative worktree warning state for deterministic tests.
36
+ *
37
+ * @returns {void}
38
+ */
39
+ export function resetRelativeWorktreePathWarningForTest() {
40
+ didWarnRelativeWorktreePath = false;
41
+ }
42
+
43
+ /**
44
+ * @param {string} pathVal
45
+ * @param {string} base
46
+ * @param {string} resolvedPath
47
+ * @param {string | null | undefined} workflowPath
48
+ * @returns {void}
49
+ */
50
+ function warnRelativeWorktreePathOnce(pathVal, base, resolvedPath, workflowPath) {
51
+ if (didWarnRelativeWorktreePath) {
52
+ return;
53
+ }
54
+ didWarnRelativeWorktreePath = true;
55
+ const workflowDir = typeof workflowPath === "string" && workflowPath.length > 0
56
+ ? dirname(workflowPath)
57
+ : undefined;
58
+ console.warn("relative <Worktree path> resolved against launch root, not the workflow file directory", {
59
+ code: "WORKTREE_RELATIVE_PATH_BASE_ROOT",
60
+ path: pathVal,
61
+ base,
62
+ resolvedPath,
63
+ workflowPath,
64
+ workflowDir,
65
+ });
24
66
  }