@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 +2 -2
- package/src/TaskAspects.ts +1 -0
- package/src/dom/extract.js +78 -44
- package/src/extract.js +68 -20
- package/src/index.d.ts +25 -2
- package/src/index.js +1 -0
- package/src/types.ts +24 -0
- package/src/validateForkSources.js +4 -0
- package/src/worktree-path.js +45 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smithers-orchestrator/graph",
|
|
3
|
-
"version": "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.
|
|
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";
|
package/src/dom/extract.js
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
/** @typedef {import("../HostText.ts").HostText} HostText */
|
|
4
4
|
// @smithers-type-exports-end
|
|
5
5
|
|
|
6
|
-
import { resolveStableId } from "
|
|
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 "
|
|
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 "
|
|
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 {
|
|
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 {
|
|
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, {
|
|
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"
|
|
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"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
741
|
-
? { after:
|
|
766
|
+
...(typeof approvalAutoApproveRaw.after === "number"
|
|
767
|
+
? { after: approvalAutoApproveRaw.after }
|
|
742
768
|
: {}),
|
|
743
|
-
...(typeof
|
|
744
|
-
? { audit:
|
|
769
|
+
...(typeof approvalAutoApproveRaw.audit === "boolean"
|
|
770
|
+
? { audit: approvalAutoApproveRaw.audit }
|
|
745
771
|
: {}),
|
|
746
|
-
...(typeof
|
|
747
|
-
? { conditionMet:
|
|
772
|
+
...(typeof approvalAutoApproveRaw.conditionMet === "boolean"
|
|
773
|
+
? { conditionMet: approvalAutoApproveRaw.conditionMet }
|
|
748
774
|
: {}),
|
|
749
|
-
...(typeof
|
|
750
|
-
? { 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"
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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);
|
package/src/worktree-path.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|