@smithers-orchestrator/graph 0.24.2 → 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.2",
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.2"
25
+ "@smithers-orchestrator/errors": "0.25.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/bun": "latest",
@@ -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 } : {}),
@@ -343,7 +343,9 @@ export function extractGraph(root, opts) {
343
343
  if (!root) {
344
344
  return { xml: null, tasks: [], mountedTaskIds: [] };
345
345
  }
346
+ /** @type {TaskDescriptor[]} */
346
347
  const tasks = [];
348
+ /** @type {string[]} */
347
349
  const mountedTaskIds = [];
348
350
  const seen = new Set();
349
351
  const seenRalph = new Set();
@@ -451,7 +453,7 @@ export function extractGraph(root, opts) {
451
453
  heartbeatTimeoutMs: parseHeartbeatTimeoutMs(raw),
452
454
  continueOnFail: Boolean(raw.continueOnFail),
453
455
  cachePolicy: raw.cache && typeof raw.cache === "object"
454
- ? raw.cache
456
+ ? /** @type {import("./CachePolicy.ts").CachePolicy<unknown>} */ (raw.cache)
455
457
  : undefined,
456
458
  label: typeof raw.label === "string" ? raw.label : undefined,
457
459
  meta: {
@@ -484,7 +486,7 @@ export function extractGraph(root, opts) {
484
486
  heartbeatTimeoutMs: parseHeartbeatTimeoutMs(raw) ?? DEFAULT_SANDBOX_TASK_HEARTBEAT_TIMEOUT_MS,
485
487
  continueOnFail: Boolean(raw.continueOnFail),
486
488
  cachePolicy: raw.cache && typeof raw.cache === "object"
487
- ? raw.cache
489
+ ? /** @type {import("./CachePolicy.ts").CachePolicy<unknown>} */ (raw.cache)
488
490
  : undefined,
489
491
  label: typeof raw.label === "string" ? raw.label : undefined,
490
492
  meta: {
@@ -622,7 +624,7 @@ export function extractGraph(root, opts) {
622
624
  const kind = raw.__smithersKind;
623
625
  const isAgent = kind === "agent" || Boolean(raw.agent);
624
626
  const { retries, retryPolicy } = resolveRetryConfig(raw, isAgent);
625
- const isCompute = kind === "compute" && typeof raw.__smithersComputeFn === "function";
627
+ const isCompute = (kind === "compute" || kind === "human") && typeof raw.__smithersComputeFn === "function";
626
628
  const parsedHeartbeatTimeoutMs = parseHeartbeatTimeoutMs(raw);
627
629
  const heartbeatTimeoutMs = parsedHeartbeatTimeoutMs ??
628
630
  (isAgent ? DEFAULT_LOCAL_TASK_HEARTBEAT_TIMEOUT_MS : null);
@@ -649,25 +651,27 @@ export function extractGraph(root, opts) {
649
651
  heartbeatTimeoutMs,
650
652
  continueOnFail: Boolean(raw.continueOnFail),
651
653
  cachePolicy: raw.cache && typeof raw.cache === "object"
652
- ? raw.cache
654
+ ? /** @type {import("./CachePolicy.ts").CachePolicy<unknown>} */ (raw.cache)
653
655
  : undefined,
654
656
  hijack: Boolean(raw.hijack),
655
657
  onHijackExit: raw.onHijackExit === "reopen" ? "reopen" : "complete",
656
- agent: raw.agent,
658
+ agent: /** @type {TaskDescriptor["agent"]} */ (raw.agent),
657
659
  prompt,
658
660
  staticPayload: isAgent || isCompute
659
661
  ? undefined
660
662
  : (raw.__smithersPayload ?? raw.__payload ?? raw.children),
661
663
  computeFn: isCompute
662
- ? raw.__smithersComputeFn
664
+ ? /** @type {() => unknown} */ (raw.__smithersComputeFn)
663
665
  : undefined,
664
666
  label: typeof raw.label === "string" ? raw.label : undefined,
665
667
  meta: raw.meta && typeof raw.meta === "object" && !Array.isArray(raw.meta)
666
- ? raw.meta
668
+ ? /** @type {Record<string, unknown>} */ (raw.meta)
667
669
  : undefined,
668
670
  scorers: raw.scorers && typeof raw.scorers === "object" && !Array.isArray(raw.scorers)
669
- ? raw.scorers
671
+ ? /** @type {import("./ScorersMap.ts").ScorersMap} */ (raw.scorers)
670
672
  : undefined,
673
+ groundTruth: raw.groundTruth,
674
+ context: raw.context,
671
675
  memoryConfig: raw.memory && typeof raw.memory === "object" && !Array.isArray(raw.memory)
672
676
  ? raw.memory
673
677
  : undefined,
package/src/index.d.ts CHANGED
@@ -128,6 +128,8 @@ type TaskDescriptor$1 = {
128
128
  ralphId?: string;
129
129
  dependsOn?: string[];
130
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;
131
133
  worktreeId?: string;
132
134
  worktreePath?: string;
133
135
  worktreeBranch?: string;
@@ -167,6 +169,8 @@ type TaskDescriptor$1 = {
167
169
  label?: string;
168
170
  meta?: Record<string, unknown>;
169
171
  scorers?: ScorersMap$1;
172
+ groundTruth?: unknown;
173
+ context?: unknown;
170
174
  memoryConfig?: TaskMemoryConfig$1;
171
175
  aspects?: TaskAspects$1;
172
176
  };
@@ -204,13 +208,18 @@ declare function extractGraph(root: HostNode$1 | null, opts?: ExtractOptions$1):
204
208
  declare function extractFromHost(root: HostNode$1 | null, opts?: ExtractOptions$1): WorkflowGraph$1;
205
209
  /**
206
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.
207
215
  *
208
216
  * @param {unknown} path
209
- * @param {{ baseRootDir?: string }} [opts]
217
+ * @param {{ baseRootDir?: string; workflowPath?: string | null }} [opts]
210
218
  * @returns {string}
211
219
  */
212
220
  declare function resolveWorktreePath(path: unknown, opts?: {
213
221
  baseRootDir?: string;
222
+ workflowPath?: string | null;
214
223
  }): string;
215
224
  type ExtractOptions$1 = ExtractOptions$2;
216
225
  type HostNode$1 = HostNode$2;
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
@@ -191,6 +191,8 @@ export type TaskDescriptor = {
191
191
  label?: string;
192
192
  meta?: Record<string, unknown>;
193
193
  scorers?: ScorersMap;
194
+ groundTruth?: unknown;
195
+ context?: unknown;
194
196
 
195
197
  memoryConfig?: TaskMemoryConfig;
196
198
  /** Resolved `<Aspects>` budget configuration enforced by the engine at dispatch. */
@@ -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
  }