@krapht/ork 0.4.0 → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -12
- package/dist/{__build_info__-CtSVVuZE.mjs → __build_info__-D9lSlMFy.mjs} +36 -112
- package/dist/bin.mjs +688 -306
- package/dist/{clean-Dcr807be.mjs → clean-CA_Oi2Wl.mjs} +1 -1
- package/dist/completion-DBGfL-gN.mjs +405 -0
- package/dist/{context-C_bksb0C.mjs → context-6x3kXREO.mjs} +11 -88
- package/dist/decide-CoIJh34e.mjs +113 -0
- package/dist/{find-DptmbhGd.mjs → find-rBBk7Jui.mjs} +1 -1
- package/dist/{format-J-a21Yvn.mjs → format-zMmhqyLE.mjs} +71 -37
- package/dist/help-groups-BwYoAWsh.mjs +354 -0
- package/dist/{in-a-run-D1Qc5Mit.mjs → in-a-run-DdAakpRR.mjs} +1 -1
- package/dist/{limits-BDZCmGEu.mjs → limits-BPM9wUlV.mjs} +1 -1
- package/dist/{list-ZvY2OcLq.mjs → list-Bv5Lxu7z.mjs} +1 -1
- package/dist/{mcp-CHVKKg3B.mjs → mcp-DM30MFBw.mjs} +7 -7
- package/dist/{output-CXSJO00X.mjs → output-TV_j7Ni0.mjs} +67 -17
- package/dist/{pause-BPSmg4yz.mjs → pause-C7lJkSma.mjs} +4 -4
- package/dist/{relay-Do-aNbSD.mjs → relay-BipJYszF.mjs} +2 -2
- package/dist/{remove-AwaMMaHf.mjs → remove-BDs9isB6.mjs} +3 -3
- package/dist/{send-DhGBkowy.mjs → send-nbkjf3sc.mjs} +3 -3
- package/dist/{src-CpDKIulI.mjs → src-B0TT7DBi.mjs} +28 -15
- package/dist/{src-BHnxpJyH.mjs → src-B9v0lDc6.mjs} +58 -10
- package/dist/{src-UWkk1sl9.mjs → src-BxUJPGa5.mjs} +252 -18
- package/dist/{src-CGqMati3.mjs → src-hrj2h0IZ.mjs} +640 -220
- package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
- package/dist/ui/assets/app-BNT0SuIz.css +1 -0
- package/dist/ui/assets/app-CmkynZyW.js +67 -0
- package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
- package/dist/ui/assets/{index-MQNNgsyT.js → index-CPx74lFf.js} +2 -2
- package/dist/ui/index.html +2 -3
- package/dist/{ui-DhutUMgQ.mjs → ui-B98FSboa.mjs} +5 -5
- package/dist/{usage-CT7Z-yjr.mjs → usage-EW7DI6Z7.mjs} +6 -6
- package/dist/{watch-0Qr2UfCQ.mjs → watch-BAkSGaOp.mjs} +7 -7
- package/dist/workflows-vsEohqa7.mjs +88 -0
- package/dist/{worktrees-DHl80k96.mjs → worktrees-Ahj9rdWK.mjs} +5 -5
- package/package.json +3 -3
- package/dist/ui/assets/app-BgVDC_5l.js +0 -67
- package/dist/ui/assets/app-DVf68fj5.css +0 -1
|
@@ -85,7 +85,7 @@ const sdk = {
|
|
|
85
85
|
name,
|
|
86
86
|
status: "warn",
|
|
87
87
|
detail: "no API key or cloud provider",
|
|
88
|
-
hint: "Set ANTHROPIC_API_KEY (or CLAUDE_CODE_USE_BEDROCK=1 / CLAUDE_CODE_USE_VERTEX=1) to use the SDK driver."
|
|
88
|
+
hint: "Set ANTHROPIC_API_KEY (or CLAUDE_CODE_USE_BEDROCK=1 / CLAUDE_CODE_USE_VERTEX=1 / CLAUDE_CODE_USE_FOUNDRY=1) to use the SDK driver."
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
91
|
};
|
|
@@ -107,7 +107,10 @@ const olderThan = (text, min) => {
|
|
|
107
107
|
}
|
|
108
108
|
return false;
|
|
109
109
|
};
|
|
110
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* The registry of Claude drivers, in the order `ork doctor` lists them. None is
|
|
112
|
+
* a default: a run takes the one ready here (`ready.ts`).
|
|
113
|
+
*/
|
|
111
114
|
const CLAUDE_DRIVERS = [sdk, {
|
|
112
115
|
id: "cli",
|
|
113
116
|
label: "your own `claude` CLI, logged in by you",
|
|
@@ -552,7 +555,8 @@ const ClaudeLeaderNodeSchema = z.strictObject({
|
|
|
552
555
|
...claudeFields,
|
|
553
556
|
...notifyFields,
|
|
554
557
|
...stepFields,
|
|
555
|
-
maxWorkers: z.number().int().nonnegative().optional().describe("Cap on dynamically spawned workers.")
|
|
558
|
+
maxWorkers: z.number().int().nonnegative().optional().describe("Cap on dynamically spawned workers."),
|
|
559
|
+
questions: z.enum(["owner", "user"]).optional().describe("Who a child's ask_user goes to first: `owner` asks a fork of this leader's session, which answers when what it was given settles it or passes it on; `user` (the default) asks the person.")
|
|
556
560
|
});
|
|
557
561
|
const ClaudeWorkerNodeSchema = z.strictObject({
|
|
558
562
|
type: z.literal("claude.worker"),
|
|
@@ -897,6 +901,28 @@ const UserSettingsSchema = z.strictObject({
|
|
|
897
901
|
schemaVersion: z.literal(1),
|
|
898
902
|
quotas: QuotasSchema.optional().describe("Most to spend across every project on this machine.")
|
|
899
903
|
});
|
|
904
|
+
const CheckSchema = z.discriminatedUnion("type", [McpCallNodeSchema, ...TOOL_NODE_SCHEMAS]);
|
|
905
|
+
const TriggerSchema = z.strictObject({
|
|
906
|
+
every: everySchema("1m").describe("How long to wait between looks, e.g. 5m. At least 1m."),
|
|
907
|
+
check: CheckSchema.optional().describe("What to look at; it reports { \"events\": [{ \"key\": \"...\", \"inputs\": { ... } }] }. Without one, the trigger starts a run every time."),
|
|
908
|
+
run: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u, "the name of a workflow in this project, not a path or a flag").describe("The workflow to start, by its name in this project."),
|
|
909
|
+
inputs: z.record(z.string(), z.string()).optional().describe("Inputs every run gets. They win over what a check reports, and aren't marked untrusted."),
|
|
910
|
+
maxRunsPerDay: z.number().int().positive().optional().describe("Most runs to start in a calendar day. Default: 10."),
|
|
911
|
+
concurrency: z.number().int().positive().optional().describe("How many of its runs may be going at once. A new key waits for the next look. Default: 1.")
|
|
912
|
+
});
|
|
913
|
+
const TriggersFileSchema = z.strictObject({
|
|
914
|
+
schemaVersion: z.literal(1),
|
|
915
|
+
triggers: z.record(NodeIdSchema, TriggerSchema)
|
|
916
|
+
});
|
|
917
|
+
/** What a check reports: each thing it found, and the run's inputs for it. */
|
|
918
|
+
const CheckReportSchema = z.object({ events: z.array(z.object({
|
|
919
|
+
key: z.string().min(1),
|
|
920
|
+
inputs: z.record(z.string(), z.union([
|
|
921
|
+
z.string(),
|
|
922
|
+
z.number(),
|
|
923
|
+
z.boolean()
|
|
924
|
+
])).optional()
|
|
925
|
+
})) });
|
|
900
926
|
const NodeListSchema = z.union([NodeIdSchema, z.array(NodeIdSchema).min(1)]);
|
|
901
927
|
/**
|
|
902
928
|
* Either the shorthand `"a -> b"`, `"a -> b, c"`, `"a, b -> c"`, or the object
|
|
@@ -970,28 +996,20 @@ const markValue = (value) => {
|
|
|
970
996
|
const pad = line.startsWith("`") || line.endsWith("`") ? " " : "";
|
|
971
997
|
return `${fence}${pad}${line}${pad}${fence}`;
|
|
972
998
|
};
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
})
|
|
986
|
-
|
|
987
|
-
const CheckReportSchema = z.object({ events: z.array(z.object({
|
|
988
|
-
key: z.string().min(1),
|
|
989
|
-
inputs: z.record(z.string(), z.union([
|
|
990
|
-
z.string(),
|
|
991
|
-
z.number(),
|
|
992
|
-
z.boolean()
|
|
993
|
-
])).optional()
|
|
994
|
-
})) });
|
|
999
|
+
// ../schema/src/newer-version.ts
|
|
1000
|
+
/**
|
|
1001
|
+
* A file with a higher `schemaVersion` than this Ork reads was made by a newer
|
|
1002
|
+
* Ork: the message saying so, instead of `expected 1` and whatever else the
|
|
1003
|
+
* newer format holds. `null` when that is not why the file failed.
|
|
1004
|
+
*/
|
|
1005
|
+
const newerVersion = (issues, raw) => {
|
|
1006
|
+
const written = raw !== null && typeof raw === "object" ? Reflect.get(raw, "schemaVersion") : void 0;
|
|
1007
|
+
const issue = issues.find((found) => found.code === "invalid_value" && found.path.length === 1 && found.path[0] === "schemaVersion");
|
|
1008
|
+
const [read] = issue?.code === "invalid_value" ? issue.values : [];
|
|
1009
|
+
if (typeof written !== "number" || typeof read !== "number") return null;
|
|
1010
|
+
if (written <= read) return null;
|
|
1011
|
+
return `this file was made by a newer Ork (\`schemaVersion: ${String(written)}\`; this one reads ${String(read)}). Upgrade with \`ork upgrade\`.`;
|
|
1012
|
+
};
|
|
995
1013
|
// ../schema/src/yaml.ts
|
|
996
1014
|
/**
|
|
997
1015
|
* @file Workflow and project YAML: parsing with source positions for
|
|
@@ -1080,19 +1098,32 @@ const parseWith = (text, schema) => {
|
|
|
1080
1098
|
};
|
|
1081
1099
|
}
|
|
1082
1100
|
const result = schema.safeParse(raw);
|
|
1083
|
-
if (!result.success)
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1101
|
+
if (!result.success) {
|
|
1102
|
+
const newer = newerVersion(result.error.issues, raw);
|
|
1103
|
+
if (newer !== null) return {
|
|
1104
|
+
ok: false,
|
|
1105
|
+
locate,
|
|
1106
|
+
diagnostics: [withPosition({
|
|
1089
1107
|
severity: "error",
|
|
1090
|
-
code:
|
|
1091
|
-
message:
|
|
1092
|
-
path:
|
|
1093
|
-
}, locate)
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1108
|
+
code: "schema.newer_version",
|
|
1109
|
+
message: newer,
|
|
1110
|
+
path: ["schemaVersion"]
|
|
1111
|
+
}, locate)]
|
|
1112
|
+
};
|
|
1113
|
+
return {
|
|
1114
|
+
ok: false,
|
|
1115
|
+
locate,
|
|
1116
|
+
diagnostics: result.error.issues.map((issue) => {
|
|
1117
|
+
const described = describeIssue(issue, raw, toPath(issue.path));
|
|
1118
|
+
return withPosition({
|
|
1119
|
+
severity: "error",
|
|
1120
|
+
code: `schema.${issue.code}`,
|
|
1121
|
+
message: described.message,
|
|
1122
|
+
path: described.path
|
|
1123
|
+
}, locate);
|
|
1124
|
+
})
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1096
1127
|
return {
|
|
1097
1128
|
ok: true,
|
|
1098
1129
|
data: result.data,
|
|
@@ -1171,8 +1202,8 @@ const serializeWorkflow = (workflow) => {
|
|
|
1171
1202
|
};
|
|
1172
1203
|
// ../schema/src/json-schema.ts
|
|
1173
1204
|
/**
|
|
1174
|
-
* @file Renders the published JSON Schemas for workflow files, agent sidecars
|
|
1175
|
-
*
|
|
1205
|
+
* @file Renders the published JSON Schemas for workflow files, agent sidecars,
|
|
1206
|
+
* project rules, user settings and triggers from the Zod models.
|
|
1176
1207
|
*/
|
|
1177
1208
|
/** The published JSON Schema for workflow files, as formatted JSON text. */
|
|
1178
1209
|
const renderJsonSchema = () => {
|
|
@@ -3529,6 +3560,27 @@ const writePlugin = (stack, dir, description) => {
|
|
|
3529
3560
|
*/
|
|
3530
3561
|
/** Where a run's socket lives; `home` is `~` (tests pass a folder). */
|
|
3531
3562
|
const socketPath = (runId, home = homedir()) => path.join(home, ".ork", "sockets", `${runId}.sock`);
|
|
3563
|
+
/** The longest run id Ork makes: a run of one node's, `node-` and 8 more. */
|
|
3564
|
+
const LONGEST_RUN_ID = "node-00000000";
|
|
3565
|
+
/**
|
|
3566
|
+
* The bytes a Unix socket's path may take, its ending zero left out: 104 on
|
|
3567
|
+
* macOS and the BSDs, 108 on Linux. `undefined` where Ork's sockets are not
|
|
3568
|
+
* Unix sockets.
|
|
3569
|
+
*/
|
|
3570
|
+
const SOCKET_PATH_MAX = {
|
|
3571
|
+
darwin: 103,
|
|
3572
|
+
freebsd: 103,
|
|
3573
|
+
openbsd: 103,
|
|
3574
|
+
linux: 107
|
|
3575
|
+
};
|
|
3576
|
+
/**
|
|
3577
|
+
* Whether every run's socket fits under this `~`. One that doesn't can't
|
|
3578
|
+
* listen, and the run looks at the store every second instead (L38).
|
|
3579
|
+
*/
|
|
3580
|
+
const socketRoom = (home = homedir(), platform = process.platform) => ({
|
|
3581
|
+
bytes: Buffer.byteLength(socketPath(LONGEST_RUN_ID, home)),
|
|
3582
|
+
max: SOCKET_PATH_MAX[platform]
|
|
3583
|
+
});
|
|
3532
3584
|
/** For errors another handler already settles. */
|
|
3533
3585
|
const ignore$1 = () => {};
|
|
3534
3586
|
const messageOf$1 = (line) => {
|
|
@@ -4287,9 +4339,10 @@ const publishPost = (item, records, options) => {
|
|
|
4287
4339
|
/**
|
|
4288
4340
|
* @file Posting feedback about Ork to its GitHub Discussions (ADR-059) with the
|
|
4289
4341
|
* person's own `gh`, never a token of Ork's: the "Agent feedback" category of
|
|
4290
|
-
* `mimshins/ork`, found by its slug. The category's
|
|
4291
|
-
*
|
|
4292
|
-
* marker gets a "+1" comment, and
|
|
4342
|
+
* `mimshins/ork`, found by its slug. The category's threads are read first,
|
|
4343
|
+
* most recently updated first, a page at a time up to `THREADS_SEARCHED`; one
|
|
4344
|
+
* whose body carries the item's fingerprint marker gets a "+1" comment, and
|
|
4345
|
+
* otherwise a new thread starts.
|
|
4293
4346
|
*/
|
|
4294
4347
|
/** Where feedback about Ork goes. */
|
|
4295
4348
|
const DISCUSSIONS = {
|
|
@@ -4297,6 +4350,14 @@ const DISCUSSIONS = {
|
|
|
4297
4350
|
name: "ork",
|
|
4298
4351
|
category: "agent-feedback"
|
|
4299
4352
|
};
|
|
4353
|
+
/** How many threads one read asks for: GitHub's most. */
|
|
4354
|
+
const THREADS_PAGE = 100;
|
|
4355
|
+
/**
|
|
4356
|
+
* How many of the category's threads are searched for the marker at most, a
|
|
4357
|
+
* page at a time until it is found: a duplicate thread is public, and ten reads
|
|
4358
|
+
* cover the category for a long while.
|
|
4359
|
+
*/
|
|
4360
|
+
const THREADS_SEARCHED = 1e3;
|
|
4300
4361
|
/** The real `gh`, on `PATH`. */
|
|
4301
4362
|
const runGh = (args) => new Promise((resolve) => {
|
|
4302
4363
|
execFile("gh", [...args], { maxBuffer: 16777216 }, (error, stdout, stderr) => {
|
|
@@ -4333,9 +4394,36 @@ const graphql = async (gh, query, vars) => {
|
|
|
4333
4394
|
};
|
|
4334
4395
|
const text$1 = (value) => typeof value === "string" ? value : "";
|
|
4335
4396
|
const PLACE = `query($owner:String!,$name:String!,$slug:String!){repository(owner:$owner,name:$name){id discussionCategory(slug:$slug){id}}}`;
|
|
4336
|
-
const THREADS = `query($owner:String!,$name:String!,$cat:ID
|
|
4397
|
+
const THREADS = `query($owner:String!,$name:String!,$cat:ID!,$after:String){repository(owner:$owner,name:$name){discussions(categoryId:$cat,first:${String(THREADS_PAGE)},after:$after,orderBy:{field:UPDATED_AT,direction:DESC}){nodes{id url body} pageInfo{hasNextPage endCursor}}}}`;
|
|
4337
4398
|
const CREATE = `mutation($repo:ID!,$cat:ID!,$title:String!,$body:String!){createDiscussion(input:{repositoryId:$repo,categoryId:$cat,title:$title,body:$body}){discussion{url}}}`;
|
|
4338
4399
|
const COMMENT = `mutation($id:ID!,$body:String!){addDiscussionComment(input:{discussionId:$id,body:$body}){comment{url}}}`;
|
|
4400
|
+
/**
|
|
4401
|
+
* The category's thread whose body carries `marker`, read a page at a time,
|
|
4402
|
+
* most recently updated first, until it is found, the threads end, or
|
|
4403
|
+
* `THREADS_SEARCHED` have been read.
|
|
4404
|
+
*/
|
|
4405
|
+
const findThread = async (gh, vars, marker) => {
|
|
4406
|
+
let after;
|
|
4407
|
+
for (let read = 0; read < THREADS_SEARCHED; read += THREADS_PAGE) {
|
|
4408
|
+
const page = await graphql(gh, THREADS, {
|
|
4409
|
+
...vars,
|
|
4410
|
+
...after === void 0 ? {} : { after }
|
|
4411
|
+
});
|
|
4412
|
+
if (!page.ok) return page;
|
|
4413
|
+
const discussions = field(field(page.data, "repository"), "discussions");
|
|
4414
|
+
const nodes = field(discussions, "nodes");
|
|
4415
|
+
const thread = (Array.isArray(nodes) ? nodes : []).find((node) => text$1(field(node, "body")).includes(marker));
|
|
4416
|
+
if (thread !== void 0) return {
|
|
4417
|
+
ok: true,
|
|
4418
|
+
thread
|
|
4419
|
+
};
|
|
4420
|
+
const info = field(discussions, "pageInfo");
|
|
4421
|
+
const cursor = text$1(field(info, "endCursor"));
|
|
4422
|
+
if (field(info, "hasNextPage") !== true || cursor === "") break;
|
|
4423
|
+
after = cursor;
|
|
4424
|
+
}
|
|
4425
|
+
return { ok: true };
|
|
4426
|
+
};
|
|
4339
4427
|
/** Finds where `post` goes, reading only: its thread, or the category. */
|
|
4340
4428
|
const planPost = async (post, gh = runGh) => {
|
|
4341
4429
|
const where = {
|
|
@@ -4354,14 +4442,12 @@ const planPost = async (post, gh = runGh) => {
|
|
|
4354
4442
|
ok: false,
|
|
4355
4443
|
error: `No \`${DISCUSSIONS.category}\` category in the Discussions of \`${DISCUSSIONS.owner}/${DISCUSSIONS.name}\`.`
|
|
4356
4444
|
};
|
|
4357
|
-
const
|
|
4445
|
+
const found = await findThread(gh, {
|
|
4358
4446
|
...where,
|
|
4359
4447
|
cat
|
|
4360
|
-
});
|
|
4361
|
-
if (!
|
|
4362
|
-
const
|
|
4363
|
-
const marker = fingerprintMarker(post.fingerprint);
|
|
4364
|
-
const thread = (Array.isArray(nodes) ? nodes : []).find((node) => text$1(field(node, "body")).includes(marker));
|
|
4448
|
+
}, fingerprintMarker(post.fingerprint));
|
|
4449
|
+
if (!found.ok) return found;
|
|
4450
|
+
const { thread } = found;
|
|
4365
4451
|
return thread === void 0 ? {
|
|
4366
4452
|
ok: true,
|
|
4367
4453
|
plan: {
|
|
@@ -4648,7 +4734,11 @@ const OUTPUT_FIELDS = {
|
|
|
4648
4734
|
"count"
|
|
4649
4735
|
],
|
|
4650
4736
|
"control.router": null,
|
|
4651
|
-
"human.gate": [
|
|
4737
|
+
"human.gate": [
|
|
4738
|
+
"choice",
|
|
4739
|
+
"answeredBy",
|
|
4740
|
+
"note"
|
|
4741
|
+
],
|
|
4652
4742
|
"workflow.call": null,
|
|
4653
4743
|
"mcp.call": ["text", "json"]
|
|
4654
4744
|
};
|
|
@@ -4754,6 +4844,9 @@ const ownSays = (node) => {
|
|
|
4754
4844
|
field: "choice",
|
|
4755
4845
|
kind: "values",
|
|
4756
4846
|
values: optionIds(node)
|
|
4847
|
+
}, {
|
|
4848
|
+
field: "note",
|
|
4849
|
+
kind: "text"
|
|
4757
4850
|
}],
|
|
4758
4851
|
canFail: false,
|
|
4759
4852
|
canDeclare: false
|
|
@@ -5403,6 +5496,8 @@ const envOutsideEnv = (segments, path) => error("template.env", `\`${describePat
|
|
|
5403
5496
|
* - The tree has no cycles;
|
|
5404
5497
|
* - A node that sets `isolation: continue` neither owns nor is owned: it works in
|
|
5405
5498
|
* the worktree its input came from, outside the tree's merges.
|
|
5499
|
+
* - A leader's `questions:` is about its children's questions, so a leader with
|
|
5500
|
+
* no `claude.leader` or `claude.worker` child can't set it.
|
|
5406
5501
|
*/
|
|
5407
5502
|
const checkOwnership = (workflow) => {
|
|
5408
5503
|
const diagnostics = [];
|
|
@@ -5430,6 +5525,14 @@ const checkOwnership = (workflow) => {
|
|
|
5430
5525
|
"isolation"
|
|
5431
5526
|
]));
|
|
5432
5527
|
}
|
|
5528
|
+
for (const [id, node] of Object.entries(nodes)) {
|
|
5529
|
+
if (node.type !== "claude.leader" || node.questions === void 0) continue;
|
|
5530
|
+
if (!Object.values(nodes).some((other) => other.parent === id && (other.type === "claude.leader" || other.type === "claude.worker"))) diagnostics.push(error("questions.no-children", `\`${id}\` sets \`questions\`, but owns no \`claude.leader\` or \`claude.worker\` whose questions it could take`, [
|
|
5531
|
+
"nodes",
|
|
5532
|
+
id,
|
|
5533
|
+
"questions"
|
|
5534
|
+
]));
|
|
5535
|
+
}
|
|
5433
5536
|
const reported = /* @__PURE__ */ new Set();
|
|
5434
5537
|
for (const start of Object.keys(nodes)) {
|
|
5435
5538
|
const seen = [];
|
|
@@ -5904,7 +6007,15 @@ const checkCallTargets = (workflow, own, registered) => Object.entries(workflow.
|
|
|
5904
6007
|
*/
|
|
5905
6008
|
/** A `maxRounds` edge. Removing them leaves a DAG (the compiler checks it). */
|
|
5906
6009
|
const isLoopEdge = (edge) => edge.maxRounds !== void 0;
|
|
5907
|
-
const
|
|
6010
|
+
const keys = /* @__PURE__ */ new WeakMap();
|
|
6011
|
+
const edgeKey = (edge) => {
|
|
6012
|
+
let key = keys.get(edge);
|
|
6013
|
+
if (key === void 0) {
|
|
6014
|
+
key = `${String(edge.source)}:${edge.from}->${edge.to}`;
|
|
6015
|
+
keys.set(edge, key);
|
|
6016
|
+
}
|
|
6017
|
+
return key;
|
|
6018
|
+
};
|
|
5908
6019
|
/**
|
|
5909
6020
|
* Decides every outgoing edge of a finished node. An edge with `when:` is taken
|
|
5910
6021
|
* when its condition holds for the node's output; one without is taken when the
|
|
@@ -5942,21 +6053,58 @@ const joinOutput = (edges, resolved) => {
|
|
|
5942
6053
|
count: ran.length
|
|
5943
6054
|
};
|
|
5944
6055
|
};
|
|
6056
|
+
/** How a reason for a run that was left waiting starts: a bug in Ork. */
|
|
6057
|
+
const STUCK_REASON = "ork engine error:";
|
|
6058
|
+
/**
|
|
6059
|
+
* Why a run that was left waiting fails: a node neither ran nor was skipped
|
|
6060
|
+
* while an edge could still reach it. The scheduler resolves every edge whose
|
|
6061
|
+
* source can't run any more, so only a bug in Ork leaves one.
|
|
6062
|
+
*/
|
|
6063
|
+
const stuckReason = (waiting) => `${STUCK_REASON} ${waiting.map(({ id, waitingOn }) => `${id} was still waiting on ${waitingOn.join(", ")}`).join("; ")} with nothing left to run; this is a bug in Ork, not in the workflow`;
|
|
6064
|
+
/** Each node's place along the non-loop edges, sources first. */
|
|
6065
|
+
const orderOf = (graph) => {
|
|
6066
|
+
const order = /* @__PURE__ */ new Map();
|
|
6067
|
+
const left = /* @__PURE__ */ new Map();
|
|
6068
|
+
for (const [id, compiled] of graph.nodes) left.set(id, compiled.incoming.filter((e) => !isLoopEdge(e)).length);
|
|
6069
|
+
const ready = [...left].filter(([, count]) => count === 0).map(([id]) => id);
|
|
6070
|
+
for (let id = ready.shift(); id !== void 0; id = ready.shift()) {
|
|
6071
|
+
order.set(id, order.size);
|
|
6072
|
+
for (const edge of graph.nodes.get(id)?.outgoing ?? []) {
|
|
6073
|
+
if (isLoopEdge(edge)) continue;
|
|
6074
|
+
const count = (left.get(edge.to) ?? 0) - 1;
|
|
6075
|
+
left.set(edge.to, count);
|
|
6076
|
+
if (count === 0) ready.push(edge.to);
|
|
6077
|
+
}
|
|
6078
|
+
}
|
|
6079
|
+
return order;
|
|
6080
|
+
};
|
|
5945
6081
|
/**
|
|
5946
|
-
* Tracks, per target, which incoming non-loop edges have resolved
|
|
5947
|
-
*
|
|
6082
|
+
* Tracks, per target, which incoming non-loop edges have resolved in its
|
|
6083
|
+
* current round: since every one last resolved.
|
|
5948
6084
|
*
|
|
5949
|
-
* - A node runs
|
|
6085
|
+
* - A node runs each time an incoming edge is taken, and is skipped once every
|
|
5950
6086
|
* incoming non-loop edge has resolved as not taken (dead-path elimination),
|
|
5951
6087
|
* so nothing downstream waits on a branch that will never run.
|
|
5952
6088
|
* - A `control.join` waits: `all` until every input resolved, `any` for the first
|
|
5953
6089
|
* that ran, `n` for `n` that ran. A join none of whose required inputs ran is
|
|
5954
6090
|
* skipped.
|
|
6091
|
+
* - An edge whose source can't run any more counts as not taken (`stranded`): in
|
|
6092
|
+
* a later round, a branch the loop doesn't go back to never runs again.
|
|
5955
6093
|
*/
|
|
5956
6094
|
const createInbox = (graph) => {
|
|
5957
6095
|
const resolved = /* @__PURE__ */ new Map();
|
|
6096
|
+
/** Targets that ran, or joins that fired, in their current round. */
|
|
5958
6097
|
const fired = /* @__PURE__ */ new Set();
|
|
5959
|
-
|
|
6098
|
+
let order;
|
|
6099
|
+
const forwards = /* @__PURE__ */ new Map();
|
|
6100
|
+
const forwardIn = (id) => {
|
|
6101
|
+
let edges = forwards.get(id);
|
|
6102
|
+
if (edges === void 0) {
|
|
6103
|
+
edges = (graph.nodes.get(id)?.incoming ?? []).filter((e) => !isLoopEdge(e));
|
|
6104
|
+
forwards.set(id, edges);
|
|
6105
|
+
}
|
|
6106
|
+
return edges;
|
|
6107
|
+
};
|
|
5960
6108
|
const reset = (id) => {
|
|
5961
6109
|
resolved.delete(id);
|
|
5962
6110
|
fired.delete(id);
|
|
@@ -5994,30 +6142,69 @@ const createInbox = (graph) => {
|
|
|
5994
6142
|
/** Records that `edge` resolved, and says what happens at its target. */
|
|
5995
6143
|
const arrive = (edge, taken, output) => {
|
|
5996
6144
|
const id = edge.to;
|
|
5997
|
-
const isJoin = graph.nodes.get(id)?.node.type === "control.join";
|
|
5998
|
-
if (!isJoin && taken) {
|
|
5999
|
-
reset(id);
|
|
6000
|
-
return {
|
|
6001
|
-
kind: "activate",
|
|
6002
|
-
input: output
|
|
6003
|
-
};
|
|
6004
|
-
}
|
|
6005
6145
|
const seen = resolved.get(id) ?? /* @__PURE__ */ new Map();
|
|
6006
6146
|
resolved.set(id, seen);
|
|
6007
6147
|
seen.set(edgeKey(edge), {
|
|
6008
6148
|
taken,
|
|
6009
6149
|
output
|
|
6010
6150
|
});
|
|
6011
|
-
if (
|
|
6012
|
-
const
|
|
6013
|
-
const
|
|
6014
|
-
if (
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6151
|
+
if (graph.nodes.get(id)?.node.type === "control.join") return arriveAtJoin(id, seen);
|
|
6152
|
+
const complete = forwardIn(id).every((e) => seen.has(edgeKey(e)));
|
|
6153
|
+
const ran = taken || fired.has(id);
|
|
6154
|
+
if (complete) reset(id);
|
|
6155
|
+
else if (taken) fired.add(id);
|
|
6156
|
+
if (taken) return {
|
|
6157
|
+
kind: "activate",
|
|
6158
|
+
input: output
|
|
6159
|
+
};
|
|
6160
|
+
return complete && !ran ? { kind: "skip" } : { kind: "wait" };
|
|
6161
|
+
};
|
|
6162
|
+
/** The edges a target still waits on. */
|
|
6163
|
+
const missingOf = (id, seen) => forwardIn(id).filter((e) => !seen.has(edgeKey(e)));
|
|
6164
|
+
/**
|
|
6165
|
+
* The first target, in graph order, whose every edge still to resolve comes
|
|
6166
|
+
* from a node that can't run any more: neither `active` (queued or running)
|
|
6167
|
+
* nor after one along the non-loop edges. A loop's edge starts a new round,
|
|
6168
|
+
* so it doesn't count. Its edges count as not taken; the caller delivers
|
|
6169
|
+
* them, and asks again, as that can start something.
|
|
6170
|
+
*/
|
|
6171
|
+
const stranded = (active) => {
|
|
6172
|
+
const candidates = [...resolved].map(([id, seen]) => ({
|
|
6173
|
+
id,
|
|
6174
|
+
missing: missingOf(id, seen)
|
|
6175
|
+
})).filter(({ missing }) => !missing.some((e) => active(e.from)));
|
|
6176
|
+
if (candidates.length === 0) return [];
|
|
6177
|
+
/** Nodes known to be unable to run, in this call. */
|
|
6178
|
+
const dead = /* @__PURE__ */ new Set();
|
|
6179
|
+
const canRun = (from) => {
|
|
6180
|
+
const visited = /* @__PURE__ */ new Set([from]);
|
|
6181
|
+
const stack = [from];
|
|
6182
|
+
for (let id = stack.pop(); id !== void 0; id = stack.pop()) {
|
|
6183
|
+
if (active(id)) return true;
|
|
6184
|
+
for (const edge of forwardIn(id)) {
|
|
6185
|
+
if (visited.has(edge.from) || dead.has(edge.from)) continue;
|
|
6186
|
+
visited.add(edge.from);
|
|
6187
|
+
stack.push(edge.from);
|
|
6188
|
+
}
|
|
6189
|
+
}
|
|
6190
|
+
for (const id of visited) dead.add(id);
|
|
6191
|
+
return false;
|
|
6192
|
+
};
|
|
6193
|
+
order ??= orderOf(graph);
|
|
6194
|
+
const known = order;
|
|
6195
|
+
const place = (id) => known.get(id) ?? Infinity;
|
|
6196
|
+
return candidates.filter(({ missing }) => !missing.some((e) => canRun(e.from))).toSorted((a, b) => place(a.id) - place(b.id))[0]?.missing ?? [];
|
|
6197
|
+
};
|
|
6198
|
+
/** Targets an edge reached that still wait on others. */
|
|
6199
|
+
const waiting = () => [...resolved].map(([id, seen]) => ({
|
|
6200
|
+
id,
|
|
6201
|
+
waitingOn: [...new Set(missingOf(id, seen).map((e) => e.from))]
|
|
6202
|
+
}));
|
|
6203
|
+
return {
|
|
6204
|
+
arrive,
|
|
6205
|
+
stranded,
|
|
6206
|
+
waiting
|
|
6019
6207
|
};
|
|
6020
|
-
return { arrive };
|
|
6021
6208
|
};
|
|
6022
6209
|
// ../core/src/run/escalation.ts
|
|
6023
6210
|
const LOOP_CHOICES = {
|
|
@@ -6202,6 +6389,125 @@ const createOwnership = (hooks) => {
|
|
|
6202
6389
|
spawn
|
|
6203
6390
|
};
|
|
6204
6391
|
};
|
|
6392
|
+
// ../core/src/run/scope.ts
|
|
6393
|
+
/**
|
|
6394
|
+
* @file A run's id and folder, and the scope templates and `when:` conditions
|
|
6395
|
+
* read during it.
|
|
6396
|
+
*/
|
|
6397
|
+
/** The run's id (given, or a new one) and its `.ork/runs/<run>` folder. */
|
|
6398
|
+
const runPlace = (options) => {
|
|
6399
|
+
const runId = options.runId ?? randomUUID().slice(0, 8);
|
|
6400
|
+
return {
|
|
6401
|
+
runId,
|
|
6402
|
+
runDir: options.runDir ?? path.join(options.cwd, ".ork", "runs", runId)
|
|
6403
|
+
};
|
|
6404
|
+
};
|
|
6405
|
+
/**
|
|
6406
|
+
* The folders every node of a run reads from: the workflow's, the project's,
|
|
6407
|
+
* Ork's.
|
|
6408
|
+
*/
|
|
6409
|
+
const foldersOf = (options) => ({
|
|
6410
|
+
workflowDir: options.workflowDir ?? options.cwd,
|
|
6411
|
+
projectRoot: options.projectRoot ?? options.cwd,
|
|
6412
|
+
...options.orkDir === void 0 ? {} : { orkDir: options.orkDir }
|
|
6413
|
+
});
|
|
6414
|
+
/** What templates and `when:` read during a run. */
|
|
6415
|
+
const runScope = (inputs, outputs, runId) => {
|
|
6416
|
+
const scope = {
|
|
6417
|
+
inputs,
|
|
6418
|
+
nodes: Object.fromEntries(Object.entries(outputs).map(([id, output]) => [id, { output }]))
|
|
6419
|
+
};
|
|
6420
|
+
if (runId !== void 0) scope.run = { id: runId };
|
|
6421
|
+
return scope;
|
|
6422
|
+
};
|
|
6423
|
+
// ../core/src/run/context.ts
|
|
6424
|
+
/**
|
|
6425
|
+
* @file One activation's execute context: what the scheduler hands a node's
|
|
6426
|
+
* executor. Its inputs and the outputs so far, where it works, the budget
|
|
6427
|
+
* left, the human, its hold and its own stop signal (ADR-041), its ports on
|
|
6428
|
+
* the bus, the session it resumes, and, for a leader, its ownership of its
|
|
6429
|
+
* children.
|
|
6430
|
+
*/
|
|
6431
|
+
const nodeContext = (deps, activation, cwd, round) => {
|
|
6432
|
+
const { options, runId } = deps;
|
|
6433
|
+
const { graph } = options;
|
|
6434
|
+
const { id, template } = activation;
|
|
6435
|
+
const context = {
|
|
6436
|
+
runId,
|
|
6437
|
+
id,
|
|
6438
|
+
workflow: graph.workflow,
|
|
6439
|
+
inputs: options.inputs,
|
|
6440
|
+
outputs: deps.outputs,
|
|
6441
|
+
input: activation.input,
|
|
6442
|
+
round,
|
|
6443
|
+
cwd,
|
|
6444
|
+
runDir: deps.runDir,
|
|
6445
|
+
...foldersOf(options),
|
|
6446
|
+
budgetLeftUsd: deps.spend.left(),
|
|
6447
|
+
budgetLeftNow: deps.spend.budgetLeft,
|
|
6448
|
+
quota: deps.spend.quota,
|
|
6449
|
+
ownSpend: (sessionId, total) => deps.spend.ownOf(id, sessionId, total),
|
|
6450
|
+
human: deps.human,
|
|
6451
|
+
hold: deps.holds.forNode(id, template),
|
|
6452
|
+
...deps.ports.forNode(id, template, cwd),
|
|
6453
|
+
...options.calls === void 0 ? {} : { calls: options.calls },
|
|
6454
|
+
emit: (event) => {
|
|
6455
|
+
deps.ports.owners.seen(id, event, cwd);
|
|
6456
|
+
options.onEvent({
|
|
6457
|
+
type: "node.event",
|
|
6458
|
+
id,
|
|
6459
|
+
event
|
|
6460
|
+
});
|
|
6461
|
+
},
|
|
6462
|
+
signal: deps.stops.signalFor(id, template)
|
|
6463
|
+
};
|
|
6464
|
+
const owner = ownerPort(deps, id, template);
|
|
6465
|
+
if (owner !== void 0) context.owner = owner;
|
|
6466
|
+
if (template !== void 0) context.template = template;
|
|
6467
|
+
if (activation.brief !== void 0) context.brief = activation.brief;
|
|
6468
|
+
const resume = options.replay?.cutOff?.(activation) ?? resumeOf(deps.nodeOf(activation), deps.sessionIds.get(id));
|
|
6469
|
+
if (resume !== void 0) context.resume = resume;
|
|
6470
|
+
if (deps.nodeOf(activation)?.type === "claude.leader") context.ownership = createOwnership({
|
|
6471
|
+
graph,
|
|
6472
|
+
leader: id,
|
|
6473
|
+
runChild: deps.runChild,
|
|
6474
|
+
isRunning: (other) => deps.running.has(other),
|
|
6475
|
+
checkpoint: () => options.workspace?.checkpoint(id) ?? Promise.resolve(null),
|
|
6476
|
+
lend: deps.slots.release,
|
|
6477
|
+
reclaim: deps.slots.take
|
|
6478
|
+
});
|
|
6479
|
+
return context;
|
|
6480
|
+
};
|
|
6481
|
+
/** Where `id`'s questions go first, if its leader takes them (ADR-063). */
|
|
6482
|
+
const ownerPort = (deps, id, template) => deps.ports.owners.portFor(id, template, {
|
|
6483
|
+
budgetLeftUsd: deps.spend.left,
|
|
6484
|
+
charge: (owner, sessionId, usage) => {
|
|
6485
|
+
const own = deps.spend.count(owner, {
|
|
6486
|
+
ok: true,
|
|
6487
|
+
output: {},
|
|
6488
|
+
sessionId,
|
|
6489
|
+
usage
|
|
6490
|
+
}, false);
|
|
6491
|
+
deps.summary(owner).costUsd += own.usage?.costUsd ?? 0;
|
|
6492
|
+
},
|
|
6493
|
+
emit: (owner, event) => {
|
|
6494
|
+
deps.options.onEvent({
|
|
6495
|
+
type: "node.event",
|
|
6496
|
+
id: owner,
|
|
6497
|
+
event
|
|
6498
|
+
});
|
|
6499
|
+
}
|
|
6500
|
+
});
|
|
6501
|
+
/**
|
|
6502
|
+
* The session a node that runs again continues, when it resumes (ADR-033): by
|
|
6503
|
+
* `session: resume`, the default for a leader; a worker starts fresh unless it
|
|
6504
|
+
* says `resume`.
|
|
6505
|
+
*/
|
|
6506
|
+
const resumeOf = (node, sessionId) => {
|
|
6507
|
+
if (node === void 0 || !isClaudeNode(node)) return void 0;
|
|
6508
|
+
if (node.type === "claude.subagent") return void 0;
|
|
6509
|
+
return (node.session ?? (node.type === "claude.leader" ? "resume" : "fresh")) === "resume" ? sessionId : void 0;
|
|
6510
|
+
};
|
|
6205
6511
|
// ../core/src/usage/spend.ts
|
|
6206
6512
|
/** What a report would add to the session's earlier ones, counting nothing. */
|
|
6207
6513
|
const unreported = (reported, key, total) => {
|
|
@@ -6224,11 +6530,13 @@ const ownSpend = (reported, key, total) => {
|
|
|
6224
6530
|
* nodes recorded. A session's report counts once its node finished, or when
|
|
6225
6531
|
* `ork chat` made it outside any node: a session cut off before its node
|
|
6226
6532
|
* finished has recorded nothing, so its next total counts the part that was cut
|
|
6227
|
-
* off.
|
|
6533
|
+
* off. A leader's fork is never resumed, so its report counts when it comes,
|
|
6534
|
+
* whether or not its leader finished.
|
|
6228
6535
|
*/
|
|
6229
6536
|
const earlierSpend = (events) => {
|
|
6230
6537
|
const reported = /* @__PURE__ */ new Map();
|
|
6231
6538
|
const open = /* @__PURE__ */ new Map();
|
|
6539
|
+
const forks = /* @__PURE__ */ new Map();
|
|
6232
6540
|
let total = 0;
|
|
6233
6541
|
for (const event of events) if (event.type === "node.started") open.set(event.id, null);
|
|
6234
6542
|
else if (event.type === "node.finished") {
|
|
@@ -6239,7 +6547,11 @@ const earlierSpend = (events) => {
|
|
|
6239
6547
|
} else if (event.type === "node.event" && event.event.type === "session.finished") {
|
|
6240
6548
|
const { sessionId, usage } = event.event.outcome;
|
|
6241
6549
|
const key = `${event.id}/${sessionId}`;
|
|
6242
|
-
if (
|
|
6550
|
+
if (event.event.fork === true) {
|
|
6551
|
+
const own = ownSpend(reported, key, usage.costUsd);
|
|
6552
|
+
total += own;
|
|
6553
|
+
forks.set(event.id, (forks.get(event.id) ?? 0) + own);
|
|
6554
|
+
} else if (open.has(event.id)) open.set(event.id, {
|
|
6243
6555
|
key,
|
|
6244
6556
|
cost: usage.costUsd
|
|
6245
6557
|
});
|
|
@@ -6247,12 +6559,13 @@ const earlierSpend = (events) => {
|
|
|
6247
6559
|
}
|
|
6248
6560
|
return {
|
|
6249
6561
|
total,
|
|
6250
|
-
reported
|
|
6562
|
+
reported,
|
|
6563
|
+
forks
|
|
6251
6564
|
};
|
|
6252
6565
|
};
|
|
6253
6566
|
// ../core/src/run/contacts.ts
|
|
6254
|
-
const SESSION_TYPES$
|
|
6255
|
-
const isSession = (node) => SESSION_TYPES$
|
|
6567
|
+
const SESSION_TYPES$4 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
|
|
6568
|
+
const isSession = (node) => SESSION_TYPES$4.has(node?.type ?? "");
|
|
6256
6569
|
const isTool = (node) => node?.type.startsWith("tool.") ?? false;
|
|
6257
6570
|
const channelPeersOf = (id, channels) => channels.flatMap(([a, b]) => {
|
|
6258
6571
|
if (a === id) return [b];
|
|
@@ -6285,12 +6598,6 @@ const scriptContactsOf = (id, nodes, channels) => {
|
|
|
6285
6598
|
/** Whether a program in a tool node may write to the session `id`. */
|
|
6286
6599
|
const hearsFromScripts = (id, nodes, channels) => nodes.some((node) => isTool(node) && scriptContactsOf(node.id, nodes, channels).includes(id));
|
|
6287
6600
|
// ../core/src/run/results.ts
|
|
6288
|
-
/**
|
|
6289
|
-
* @file Small helpers for the scheduler's results: pass-through and failure
|
|
6290
|
-
* results, run-summary entries, what a run has spent, `node.finished` events,
|
|
6291
|
-
* the declared nodes `run.started` lists, which session a re-run node
|
|
6292
|
-
* resumes, and closing the workspace at the end.
|
|
6293
|
-
*/
|
|
6294
6601
|
/** Control nodes pass their input through; a join's input is its output. */
|
|
6295
6602
|
const passThrough = (input) => ({
|
|
6296
6603
|
output: input ?? {},
|
|
@@ -6416,6 +6723,16 @@ const summaryOf = (summaries, id) => {
|
|
|
6416
6723
|
summaries.set(id, created);
|
|
6417
6724
|
return created;
|
|
6418
6725
|
};
|
|
6726
|
+
/** A run's summary; a resumed run's leaders keep their forks' spend (ADR-063). */
|
|
6727
|
+
const createSummaries = (earlier) => {
|
|
6728
|
+
const summaries = /* @__PURE__ */ new Map();
|
|
6729
|
+
const summary = (id) => summaryOf(summaries, id);
|
|
6730
|
+
for (const [id, costUsd] of earlier?.forks ?? []) summary(id).costUsd += costUsd;
|
|
6731
|
+
return {
|
|
6732
|
+
summaries,
|
|
6733
|
+
summary
|
|
6734
|
+
};
|
|
6735
|
+
};
|
|
6419
6736
|
/** The tighter of the workflow's budget and what a caller left it, if any. */
|
|
6420
6737
|
const runBudget = (graph, settings) => {
|
|
6421
6738
|
const limits = [graph.workflow.limits?.budgetUsd, settings?.budgetCap].filter((limit) => limit !== void 0);
|
|
@@ -6439,7 +6756,19 @@ const runStartedEvent = (graph, runId, options) => ({
|
|
|
6439
6756
|
...options.settings === void 0 ? {} : { settings: options.settings },
|
|
6440
6757
|
...runBudget(graph, options.settings)
|
|
6441
6758
|
});
|
|
6442
|
-
/** The
|
|
6759
|
+
/** The unread mail of nodes that never ran; a node that ran saw its notice. */
|
|
6760
|
+
const unreadOf = (nodes, unread) => {
|
|
6761
|
+
const ran = new Set(nodes.filter((node) => node.runs > 0).map((node) => node.id));
|
|
6762
|
+
const left = [...unread ?? []].filter(([node]) => !ran.has(node)).map(([node, messages]) => ({
|
|
6763
|
+
node,
|
|
6764
|
+
messages
|
|
6765
|
+
}));
|
|
6766
|
+
return left.length === 0 ? {} : { unread: left };
|
|
6767
|
+
};
|
|
6768
|
+
/**
|
|
6769
|
+
* The run's result: failed when any reason was given, unless it stopped. A
|
|
6770
|
+
* skipped node or a handled failure leaves it `succeeded`, and is counted.
|
|
6771
|
+
*/
|
|
6443
6772
|
const runResult = (run) => ({
|
|
6444
6773
|
runId: run.runId,
|
|
6445
6774
|
status: run.stopped ?? (run.reasons.length > 0 ? "failed" : "succeeded"),
|
|
@@ -6447,7 +6776,10 @@ const runResult = (run) => ({
|
|
|
6447
6776
|
nodes: run.nodes,
|
|
6448
6777
|
reasons: run.reasons,
|
|
6449
6778
|
worktrees: run.worktrees,
|
|
6450
|
-
...run.outputs === void 0 ? {} : { outputs: run.outputs }
|
|
6779
|
+
...run.outputs === void 0 ? {} : { outputs: run.outputs },
|
|
6780
|
+
skipped: run.nodes.filter((node) => node.runs === 0).length,
|
|
6781
|
+
failuresHandled: run.failuresHandled,
|
|
6782
|
+
...unreadOf(run.nodes, run.unread)
|
|
6451
6783
|
});
|
|
6452
6784
|
/** A workflow's `outputs`, rendered as the run ends. */
|
|
6453
6785
|
const renderOutputs = (outputs, scope) => outputs === void 0 ? void 0 : Object.fromEntries(Object.entries(outputs).map(([name, text]) => {
|
|
@@ -6459,95 +6791,6 @@ const closeWorkspace = (workspace, reasons) => (workspace?.close() ?? Promise.re
|
|
|
6459
6791
|
reasons.push(`cleaning up worktrees: ${String(error)}`);
|
|
6460
6792
|
return [];
|
|
6461
6793
|
});
|
|
6462
|
-
/**
|
|
6463
|
-
* The session a node that runs again continues, when it resumes (ADR-033): by
|
|
6464
|
-
* `session: resume`, the default for a leader; a worker starts fresh unless it
|
|
6465
|
-
* says `resume`.
|
|
6466
|
-
*/
|
|
6467
|
-
const resumeOf = (node, sessionId) => {
|
|
6468
|
-
if (node === void 0 || !isClaudeNode(node)) return void 0;
|
|
6469
|
-
if (node.type === "claude.subagent") return void 0;
|
|
6470
|
-
return (node.session ?? (node.type === "claude.leader" ? "resume" : "fresh")) === "resume" ? sessionId : void 0;
|
|
6471
|
-
};
|
|
6472
|
-
// ../core/src/run/scope.ts
|
|
6473
|
-
/**
|
|
6474
|
-
* @file A run's id and folder, and the scope templates and `when:` conditions
|
|
6475
|
-
* read during it.
|
|
6476
|
-
*/
|
|
6477
|
-
/** The run's id (given, or a new one) and its `.ork/runs/<run>` folder. */
|
|
6478
|
-
const runPlace = (options) => {
|
|
6479
|
-
const runId = options.runId ?? randomUUID().slice(0, 8);
|
|
6480
|
-
return {
|
|
6481
|
-
runId,
|
|
6482
|
-
runDir: options.runDir ?? path.join(options.cwd, ".ork", "runs", runId)
|
|
6483
|
-
};
|
|
6484
|
-
};
|
|
6485
|
-
/**
|
|
6486
|
-
* The folders every node of a run reads from: the workflow's, the project's,
|
|
6487
|
-
* Ork's.
|
|
6488
|
-
*/
|
|
6489
|
-
const foldersOf = (options) => ({
|
|
6490
|
-
workflowDir: options.workflowDir ?? options.cwd,
|
|
6491
|
-
projectRoot: options.projectRoot ?? options.cwd,
|
|
6492
|
-
...options.orkDir === void 0 ? {} : { orkDir: options.orkDir }
|
|
6493
|
-
});
|
|
6494
|
-
/** What templates and `when:` read during a run. */
|
|
6495
|
-
const runScope = (inputs, outputs, runId) => {
|
|
6496
|
-
const scope = {
|
|
6497
|
-
inputs,
|
|
6498
|
-
nodes: Object.fromEntries(Object.entries(outputs).map(([id, output]) => [id, { output }]))
|
|
6499
|
-
};
|
|
6500
|
-
if (runId !== void 0) scope.run = { id: runId };
|
|
6501
|
-
return scope;
|
|
6502
|
-
};
|
|
6503
|
-
// ../core/src/run/context.ts
|
|
6504
|
-
const nodeContext = (deps, activation, cwd, round) => {
|
|
6505
|
-
const { options, runId } = deps;
|
|
6506
|
-
const { graph } = options;
|
|
6507
|
-
const { id, template } = activation;
|
|
6508
|
-
const context = {
|
|
6509
|
-
runId,
|
|
6510
|
-
id,
|
|
6511
|
-
workflow: graph.workflow,
|
|
6512
|
-
inputs: options.inputs,
|
|
6513
|
-
outputs: deps.outputs,
|
|
6514
|
-
input: activation.input,
|
|
6515
|
-
round,
|
|
6516
|
-
cwd,
|
|
6517
|
-
runDir: deps.runDir,
|
|
6518
|
-
...foldersOf(options),
|
|
6519
|
-
budgetLeftUsd: deps.spend.left(),
|
|
6520
|
-
budgetLeftNow: deps.spend.budgetLeft,
|
|
6521
|
-
quota: deps.spend.quota,
|
|
6522
|
-
ownSpend: (sessionId, total) => deps.spend.ownOf(id, sessionId, total),
|
|
6523
|
-
human: deps.human,
|
|
6524
|
-
hold: deps.holds.forNode(id, template),
|
|
6525
|
-
...deps.ports.forNode(id, template, cwd),
|
|
6526
|
-
...options.calls === void 0 ? {} : { calls: options.calls },
|
|
6527
|
-
emit: (event) => {
|
|
6528
|
-
options.onEvent({
|
|
6529
|
-
type: "node.event",
|
|
6530
|
-
id,
|
|
6531
|
-
event
|
|
6532
|
-
});
|
|
6533
|
-
},
|
|
6534
|
-
signal: deps.stops.signalFor(id, template)
|
|
6535
|
-
};
|
|
6536
|
-
if (template !== void 0) context.template = template;
|
|
6537
|
-
if (activation.brief !== void 0) context.brief = activation.brief;
|
|
6538
|
-
const resume = options.replay?.cutOff?.(activation) ?? resumeOf(deps.nodeOf(activation), deps.sessionIds.get(id));
|
|
6539
|
-
if (resume !== void 0) context.resume = resume;
|
|
6540
|
-
if (deps.nodeOf(activation)?.type === "claude.leader") context.ownership = createOwnership({
|
|
6541
|
-
graph,
|
|
6542
|
-
leader: id,
|
|
6543
|
-
runChild: deps.runChild,
|
|
6544
|
-
isRunning: (other) => deps.running.has(other),
|
|
6545
|
-
checkpoint: () => options.workspace?.checkpoint(id) ?? Promise.resolve(null),
|
|
6546
|
-
lend: deps.slots.release,
|
|
6547
|
-
reclaim: deps.slots.take
|
|
6548
|
-
});
|
|
6549
|
-
return context;
|
|
6550
|
-
};
|
|
6551
6794
|
// ../core/src/run/holds.ts
|
|
6552
6795
|
const createHolds = (options) => {
|
|
6553
6796
|
let paused = false;
|
|
@@ -6778,7 +7021,8 @@ const createClock = (options) => {
|
|
|
6778
7021
|
/** The same channel, noting who waits on the user for how long. */
|
|
6779
7022
|
human: (channel) => ({
|
|
6780
7023
|
permit: (request) => asking(request.nodeId, () => channel.permit(request)),
|
|
6781
|
-
ask: (question) => asking(question.nodeId, () => channel.ask(question))
|
|
7024
|
+
ask: (question) => asking(question.nodeId, () => channel.ask(question)),
|
|
7025
|
+
...channel.settled === void 0 ? {} : { settled: channel.settled }
|
|
6782
7026
|
}),
|
|
6783
7027
|
/** Stands still while the node, or a node it owns, waits on the user. */
|
|
6784
7028
|
forNode: (id) => ({
|
|
@@ -7128,7 +7372,7 @@ const createOwnerQuestions = ({ ownerOf, deliverTo, onEvent }) => {
|
|
|
7128
7372
|
deliver(ownerQuestionText(id, node, question));
|
|
7129
7373
|
return promise;
|
|
7130
7374
|
},
|
|
7131
|
-
answer: (id, question, choice) => {
|
|
7375
|
+
answer: (id, question, choice, note) => {
|
|
7132
7376
|
const asked = open.get(question);
|
|
7133
7377
|
if (asked?.owner !== id) return {
|
|
7134
7378
|
ok: false,
|
|
@@ -7139,11 +7383,13 @@ const createOwnerQuestions = ({ ownerOf, deliverTo, onEvent }) => {
|
|
|
7139
7383
|
error: `\`${choice}\` is not an option: ${asked.options.map((o) => o.id).join(", ")}`
|
|
7140
7384
|
};
|
|
7141
7385
|
open.delete(question);
|
|
7386
|
+
const said = note?.trim() ?? "";
|
|
7142
7387
|
onEvent({
|
|
7143
7388
|
type: "owner.answered",
|
|
7144
7389
|
node: id,
|
|
7145
7390
|
question,
|
|
7146
|
-
answer: choice
|
|
7391
|
+
answer: choice,
|
|
7392
|
+
...said === "" ? {} : { note: said }
|
|
7147
7393
|
});
|
|
7148
7394
|
asked.resolve(choice);
|
|
7149
7395
|
return { ok: true };
|
|
@@ -7164,7 +7410,7 @@ const createOwnerQuestions = ({ ownerOf, deliverTo, onEvent }) => {
|
|
|
7164
7410
|
};
|
|
7165
7411
|
};
|
|
7166
7412
|
// ../core/src/run/bus.ts
|
|
7167
|
-
const SESSION_TYPES$
|
|
7413
|
+
const SESSION_TYPES$3 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
|
|
7168
7414
|
const createBus = ({ graph, onEvent, mail, relay, slots }) => {
|
|
7169
7415
|
/** Spawned instance -> the declared node it is an instance of. */
|
|
7170
7416
|
const templates = /* @__PURE__ */ new Map();
|
|
@@ -7183,7 +7429,7 @@ const createBus = ({ graph, onEvent, mail, relay, slots }) => {
|
|
|
7183
7429
|
/** Sessions in `wait`: called when a message arrives or the session ends. */
|
|
7184
7430
|
const waiters = /* @__PURE__ */ new Map();
|
|
7185
7431
|
const declared = (id) => graph.nodes.get(templates.get(id) ?? id);
|
|
7186
|
-
const hasSession = (id) => SESSION_TYPES$
|
|
7432
|
+
const hasSession = (id) => SESSION_TYPES$3.has(graph.nodes.get(id)?.node.type ?? "");
|
|
7187
7433
|
const inbox = (id) => {
|
|
7188
7434
|
const existing = inboxes.get(id);
|
|
7189
7435
|
if (existing !== void 0) return existing;
|
|
@@ -7288,7 +7534,7 @@ const createBus = ({ graph, onEvent, mail, relay, slots }) => {
|
|
|
7288
7534
|
});
|
|
7289
7535
|
return unread;
|
|
7290
7536
|
},
|
|
7291
|
-
answer: (question, choice) => questions.answer(id, question, choice),
|
|
7537
|
+
answer: (question, choice, note) => questions.answer(id, question, choice, note),
|
|
7292
7538
|
connect: (deliver) => {
|
|
7293
7539
|
live.set(id, deliver);
|
|
7294
7540
|
for (const body of waiting.get(id) ?? []) deliver(userMessageText(body));
|
|
@@ -7353,6 +7599,11 @@ const createBus = ({ graph, onEvent, mail, relay, slots }) => {
|
|
|
7353
7599
|
via: "rule"
|
|
7354
7600
|
});
|
|
7355
7601
|
land(to);
|
|
7602
|
+
},
|
|
7603
|
+
unread: () => {
|
|
7604
|
+
const counts = /* @__PURE__ */ new Map();
|
|
7605
|
+
for (const [id, unread] of [...inboxes, ...waiting]) counts.set(id, (counts.get(id) ?? 0) + unread.length);
|
|
7606
|
+
return new Map([...counts].filter(([, count]) => count > 0));
|
|
7356
7607
|
}
|
|
7357
7608
|
};
|
|
7358
7609
|
};
|
|
@@ -7365,7 +7616,7 @@ const createBus = ({ graph, onEvent, mail, relay, slots }) => {
|
|
|
7365
7616
|
* notice when it is released; a node's claims are released when it finishes.
|
|
7366
7617
|
* Claims are advisory: they steer agents, they do not lock files.
|
|
7367
7618
|
*/
|
|
7368
|
-
const SESSION_TYPES$
|
|
7619
|
+
const SESSION_TYPES$2 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
|
|
7369
7620
|
const WHOLE = ".";
|
|
7370
7621
|
/**
|
|
7371
7622
|
* A resource as the run compares it: a path from the project root (an absolute
|
|
@@ -7390,7 +7641,7 @@ const writersOf = (graph) => {
|
|
|
7390
7641
|
const isolation = graph.workflow.defaults?.isolation ?? "worktree";
|
|
7391
7642
|
const writers = /* @__PURE__ */ new Set();
|
|
7392
7643
|
for (const [id, { node }] of graph.nodes) {
|
|
7393
|
-
if (!SESSION_TYPES$
|
|
7644
|
+
if (!SESSION_TYPES$2.has(node.type)) continue;
|
|
7394
7645
|
if ("isolation" in node && (node.isolation ?? isolation) === "readonly") continue;
|
|
7395
7646
|
writers.add(id);
|
|
7396
7647
|
}
|
|
@@ -7485,6 +7736,55 @@ const createClaims = ({ graph, onEvent, notify }) => {
|
|
|
7485
7736
|
releaseAll
|
|
7486
7737
|
};
|
|
7487
7738
|
};
|
|
7739
|
+
// ../core/src/run/owners.ts
|
|
7740
|
+
/** The node types whose sessions can ask (`ask_user`). */
|
|
7741
|
+
const ASKING = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
|
|
7742
|
+
const createOwners = (graph) => {
|
|
7743
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
7744
|
+
/** The leader that takes `id`'s questions first, if one does. */
|
|
7745
|
+
const ownerOf = (id, template) => {
|
|
7746
|
+
const node = graph.nodes.get(template ?? id)?.node;
|
|
7747
|
+
if (node?.parent === void 0 || !ASKING.has(node.type)) return void 0;
|
|
7748
|
+
const owner = graph.nodes.get(node.parent)?.node;
|
|
7749
|
+
return owner?.type === "claude.leader" && owner.questions === "owner" ? node.parent : void 0;
|
|
7750
|
+
};
|
|
7751
|
+
return {
|
|
7752
|
+
/** Notes `id`'s own sessions as they start and end; forks aren't its own. */
|
|
7753
|
+
seen: (id, event, cwd) => {
|
|
7754
|
+
if (event.type === "session.started" && event.fork !== true) sessions.set(id, {
|
|
7755
|
+
sessionId: event.sessionId,
|
|
7756
|
+
cwd: event.cwd ?? cwd,
|
|
7757
|
+
model: event.model,
|
|
7758
|
+
...event.driver === void 0 ? {} : { driver: event.driver }
|
|
7759
|
+
});
|
|
7760
|
+
const last = sessions.get(id);
|
|
7761
|
+
if (event.type === "session.finished" && event.fork !== true && last !== void 0 && event.outcome.sessionId !== "") sessions.set(id, {
|
|
7762
|
+
...last,
|
|
7763
|
+
sessionId: event.outcome.sessionId
|
|
7764
|
+
});
|
|
7765
|
+
},
|
|
7766
|
+
/**
|
|
7767
|
+
* The port for `id`'s questions, or `undefined` when they go straight to
|
|
7768
|
+
* the person. `charge` counts a fork's spend; `emit` records an event as a
|
|
7769
|
+
* node's.
|
|
7770
|
+
*/
|
|
7771
|
+
portFor: (id, template, hooks) => {
|
|
7772
|
+
const owner = ownerOf(id, template);
|
|
7773
|
+
if (owner === void 0) return void 0;
|
|
7774
|
+
return {
|
|
7775
|
+
id: owner,
|
|
7776
|
+
session: () => sessions.get(owner),
|
|
7777
|
+
budgetLeftUsd: hooks.budgetLeftUsd,
|
|
7778
|
+
charge: (sessionId, usage) => {
|
|
7779
|
+
hooks.charge(owner, sessionId, usage);
|
|
7780
|
+
},
|
|
7781
|
+
emit: (event) => {
|
|
7782
|
+
hooks.emit(owner, event);
|
|
7783
|
+
}
|
|
7784
|
+
};
|
|
7785
|
+
}
|
|
7786
|
+
};
|
|
7787
|
+
};
|
|
7488
7788
|
// ../core/src/run/rules.ts
|
|
7489
7789
|
/**
|
|
7490
7790
|
* @file Firing a finished node's `notify:` rules (RFC-005). A rule sends
|
|
@@ -7570,8 +7870,17 @@ const createRunPorts = ({ options, slots, scope, stopping }) => {
|
|
|
7570
7870
|
onEvent
|
|
7571
7871
|
});
|
|
7572
7872
|
const send = bus.rule;
|
|
7873
|
+
const rules = createRules({
|
|
7874
|
+
graph,
|
|
7875
|
+
send,
|
|
7876
|
+
scope,
|
|
7877
|
+
stopping,
|
|
7878
|
+
scrub
|
|
7879
|
+
});
|
|
7573
7880
|
return {
|
|
7574
7881
|
bus,
|
|
7882
|
+
/** Each node's latest session, and whose questions go to a leader first. */
|
|
7883
|
+
owners: createOwners(graph),
|
|
7575
7884
|
/** One activation's ports; `template` is a spawned worker's node. */
|
|
7576
7885
|
forNode: (id, template, cwd) => ({
|
|
7577
7886
|
mailbox: bus.mailbox(id, template),
|
|
@@ -7579,13 +7888,7 @@ const createRunPorts = ({ options, slots, scope, stopping }) => {
|
|
|
7579
7888
|
feedback: feedback.port(id, template)
|
|
7580
7889
|
}),
|
|
7581
7890
|
/** Sends what a node's fresh result declares (`notify:`). */
|
|
7582
|
-
fireRules:
|
|
7583
|
-
graph,
|
|
7584
|
-
send,
|
|
7585
|
-
scope,
|
|
7586
|
-
stopping,
|
|
7587
|
-
scrub
|
|
7588
|
-
}).fire,
|
|
7891
|
+
fireRules: rules.fire,
|
|
7589
7892
|
/** The activation of `id` ended: its claims are released. */
|
|
7590
7893
|
finished: (id) => {
|
|
7591
7894
|
claims.releaseAll(id);
|
|
@@ -7596,6 +7899,11 @@ const createRunPorts = ({ options, slots, scope, stopping }) => {
|
|
|
7596
7899
|
const createRouter = (options) => {
|
|
7597
7900
|
const { graph, onEvent } = options;
|
|
7598
7901
|
const inbox = createInbox(graph);
|
|
7902
|
+
/** Failed results with an edge taken: the run went on (G8, L42). */
|
|
7903
|
+
let handled = 0;
|
|
7904
|
+
/** How many activations of each node are queued, running or routing. */
|
|
7905
|
+
const active = /* @__PURE__ */ new Map();
|
|
7906
|
+
const isActive = (id) => active.has(id);
|
|
7599
7907
|
const skip = (id) => {
|
|
7600
7908
|
options.skipped(id);
|
|
7601
7909
|
onEvent({
|
|
@@ -7625,9 +7933,51 @@ const createRouter = (options) => {
|
|
|
7625
7933
|
if (taken) anyTaken = true;
|
|
7626
7934
|
}
|
|
7627
7935
|
}
|
|
7628
|
-
if (
|
|
7936
|
+
if (result.ok) return;
|
|
7937
|
+
if (anyTaken) handled += 1;
|
|
7938
|
+
else options.failed(`${id}: ${result.error ?? "failed"}`);
|
|
7939
|
+
};
|
|
7940
|
+
/** An activation of `id` is queued; it is active until `done`. */
|
|
7941
|
+
const queued = (id) => {
|
|
7942
|
+
active.set(id, (active.get(id) ?? 0) + 1);
|
|
7943
|
+
};
|
|
7944
|
+
/** An activation of `id` has run and routed, or never will. */
|
|
7945
|
+
const done = (id) => {
|
|
7946
|
+
const count = (active.get(id) ?? 0) - 1;
|
|
7947
|
+
if (count > 0) active.set(id, count);
|
|
7948
|
+
else active.delete(id);
|
|
7949
|
+
};
|
|
7950
|
+
/**
|
|
7951
|
+
* Resolves as not taken each edge whose source can't run any more, given what
|
|
7952
|
+
* is active (queued, running or routing), so a node fed only by branches that
|
|
7953
|
+
* didn't run in this round is skipped, and a join waiting on one fires. One
|
|
7954
|
+
* target at a time, in graph order, as each can start something.
|
|
7955
|
+
*/
|
|
7956
|
+
const resolveStranded = () => {
|
|
7957
|
+
for (let edges = inbox.stranded(isActive); edges.length > 0 && !options.stopped(); edges = inbox.stranded(isActive)) for (const edge of edges) deliver(edge, false, {});
|
|
7958
|
+
};
|
|
7959
|
+
/**
|
|
7960
|
+
* Routes a finished activation unless the run has stopped, then resolves as
|
|
7961
|
+
* not taken what can't run any more in this round.
|
|
7962
|
+
*/
|
|
7963
|
+
const finished = async (id, result) => {
|
|
7964
|
+
if (!options.stopped()) await route(id, result);
|
|
7965
|
+
done(id);
|
|
7966
|
+
if (!options.stopped()) resolveStranded();
|
|
7967
|
+
};
|
|
7968
|
+
/** Why the run fails if a node still waits with nothing left to run. */
|
|
7969
|
+
const stuck = () => {
|
|
7970
|
+
const waiting = inbox.waiting();
|
|
7971
|
+
return waiting.length === 0 ? [] : [stuckReason(waiting)];
|
|
7972
|
+
};
|
|
7973
|
+
return {
|
|
7974
|
+
route,
|
|
7975
|
+
finished,
|
|
7976
|
+
queued,
|
|
7977
|
+
done,
|
|
7978
|
+
stuck,
|
|
7979
|
+
handled: () => handled
|
|
7629
7980
|
};
|
|
7630
|
-
return { route };
|
|
7631
7981
|
};
|
|
7632
7982
|
// ../core/src/run/slots.ts
|
|
7633
7983
|
/**
|
|
@@ -7676,7 +8026,7 @@ const startRun = (options) => {
|
|
|
7676
8026
|
const sessionIds = /* @__PURE__ */ new Map();
|
|
7677
8027
|
const spend = spendOf(limits.budgetUsd, options);
|
|
7678
8028
|
const running = /* @__PURE__ */ new Set();
|
|
7679
|
-
const summaries
|
|
8029
|
+
const { summaries, summary } = createSummaries(options.replay?.earlier);
|
|
7680
8030
|
const queue = [];
|
|
7681
8031
|
const reasons = [];
|
|
7682
8032
|
const stops = createStops(abort.signal);
|
|
@@ -7688,7 +8038,6 @@ const startRun = (options) => {
|
|
|
7688
8038
|
pump();
|
|
7689
8039
|
});
|
|
7690
8040
|
const scope = () => runScope(options.inputs, outputs, runId);
|
|
7691
|
-
const summary = (id) => summaryOf(summaries, id);
|
|
7692
8041
|
const nodeOf = (activation) => graph.nodes.get(activation.template ?? activation.id)?.node;
|
|
7693
8042
|
const stop = (status, reason) => {
|
|
7694
8043
|
if (stopped !== null) return;
|
|
@@ -7697,7 +8046,9 @@ const startRun = (options) => {
|
|
|
7697
8046
|
queue.length = 0;
|
|
7698
8047
|
};
|
|
7699
8048
|
const enqueue = (id, input) => {
|
|
7700
|
-
if (stopped
|
|
8049
|
+
if (stopped !== null) return;
|
|
8050
|
+
router.queued(id);
|
|
8051
|
+
queue.push({
|
|
7701
8052
|
id,
|
|
7702
8053
|
input
|
|
7703
8054
|
});
|
|
@@ -7738,12 +8089,8 @@ const startRun = (options) => {
|
|
|
7738
8089
|
activate: enqueue,
|
|
7739
8090
|
takeLoop: loops.take,
|
|
7740
8091
|
stopped: () => stopped !== null,
|
|
7741
|
-
skipped: (id) =>
|
|
7742
|
-
|
|
7743
|
-
},
|
|
7744
|
-
failed: (reason) => {
|
|
7745
|
-
reasons.push(reason);
|
|
7746
|
-
}
|
|
8092
|
+
skipped: (id) => void summary(id),
|
|
8093
|
+
failed: (reason) => void reasons.push(reason)
|
|
7747
8094
|
});
|
|
7748
8095
|
const cancelTimeout = watchRunTimeout({
|
|
7749
8096
|
timeout: limits.timeout,
|
|
@@ -7764,15 +8111,16 @@ const startRun = (options) => {
|
|
|
7764
8111
|
cancelTimeout();
|
|
7765
8112
|
const worktrees = await closeWorkspace(options.workspace, reasons);
|
|
7766
8113
|
await options.beforeFinish?.();
|
|
7767
|
-
const nodes = [...summaries.values()];
|
|
7768
8114
|
const result = runResult({
|
|
7769
8115
|
runId,
|
|
7770
8116
|
stopped,
|
|
7771
8117
|
costUsd: spend.total(),
|
|
7772
|
-
nodes,
|
|
8118
|
+
nodes: [...summaries.values()],
|
|
7773
8119
|
reasons,
|
|
7774
8120
|
worktrees,
|
|
7775
|
-
outputs: renderOutputs(graph.workflow.outputs, scope())
|
|
8121
|
+
outputs: renderOutputs(graph.workflow.outputs, scope()),
|
|
8122
|
+
failuresHandled: router.handled(),
|
|
8123
|
+
unread: ports.bus.unread()
|
|
7776
8124
|
});
|
|
7777
8125
|
onEvent({
|
|
7778
8126
|
type: "run.finished",
|
|
@@ -7782,6 +8130,7 @@ const startRun = (options) => {
|
|
|
7782
8130
|
};
|
|
7783
8131
|
const settle = () => {
|
|
7784
8132
|
if (settled || busy > 0 || queue.length > 0) return;
|
|
8133
|
+
if (stopped === null) reasons.push(...router.stuck());
|
|
7785
8134
|
settled = true;
|
|
7786
8135
|
close();
|
|
7787
8136
|
};
|
|
@@ -7823,7 +8172,8 @@ const startRun = (options) => {
|
|
|
7823
8172
|
slots,
|
|
7824
8173
|
nodeOf,
|
|
7825
8174
|
runChild,
|
|
7826
|
-
spend
|
|
8175
|
+
spend,
|
|
8176
|
+
summary
|
|
7827
8177
|
},
|
|
7828
8178
|
clock,
|
|
7829
8179
|
graceMs,
|
|
@@ -7859,7 +8209,7 @@ const startRun = (options) => {
|
|
|
7859
8209
|
const run = async (activation, isSession) => {
|
|
7860
8210
|
const result = await perform(activation);
|
|
7861
8211
|
if (isSession) slots.release();
|
|
7862
|
-
|
|
8212
|
+
await router.finished(activation.id, result);
|
|
7863
8213
|
busy -= 1;
|
|
7864
8214
|
pump();
|
|
7865
8215
|
};
|
|
@@ -7889,10 +8239,7 @@ const startRun = (options) => {
|
|
|
7889
8239
|
settle();
|
|
7890
8240
|
};
|
|
7891
8241
|
onEvent(runStartedEvent(graph, runId, options));
|
|
7892
|
-
for (const id of graph.entries)
|
|
7893
|
-
id,
|
|
7894
|
-
input: void 0
|
|
7895
|
-
});
|
|
8242
|
+
for (const id of graph.entries) enqueue(id);
|
|
7896
8243
|
queueMicrotask(pump);
|
|
7897
8244
|
return {
|
|
7898
8245
|
runId,
|
|
@@ -8051,6 +8398,8 @@ const walkFindings = (graph, tally) => {
|
|
|
8051
8398
|
const onlyRounds = graph.edges.filter((edge) => edge.maxRounds !== void 0 && behind(graph, [edge.to]).has(edge.from) && tally.entered.has(linkOf(edge)) && !tally.left.has(linkOf(edge)) && !anyBehind(graph, edge.from, hidden) && !said.has(edge.source)).map((edge) => warning("walk.only-rounds", `every way the walk tried through the loop \`${linkOf(edge)}\` ran out of rounds`, ["edges", edge.source]));
|
|
8052
8399
|
return [...neverRuns, ...onlyRounds];
|
|
8053
8400
|
};
|
|
8401
|
+
/** Whether a run ended with a node still waiting, which only a bug causes. */
|
|
8402
|
+
const leftWaiting = (result) => result.reasons.some((reason) => reason.startsWith(STUCK_REASON));
|
|
8054
8403
|
/** A path that never settled, which only a bug in the scheduler can cause. */
|
|
8055
8404
|
const unfinished = (choices) => warning("walk.unfinished", `the walk got stuck on one way through this workflow${choices.length === 0 ? "" : ` (${choices.map((choice) => `\`${choice}\``).join(", ")})`}. This is a bug in Ork, not in the workflow`, []);
|
|
8056
8405
|
// ../core/src/run/walk-reads.ts
|
|
@@ -8620,7 +8969,8 @@ const walkWorkflow = async (graph, options = {}) => {
|
|
|
8620
8969
|
if (result === null) {
|
|
8621
8970
|
handle.interrupt();
|
|
8622
8971
|
stuck = path.said;
|
|
8623
|
-
} else if (
|
|
8972
|
+
} else if (leftWaiting(result)) stuck = path.said;
|
|
8973
|
+
else if (path.overran()) overran = true;
|
|
8624
8974
|
else {
|
|
8625
8975
|
counts.paths += 1;
|
|
8626
8976
|
const succeeded = result.status === "succeeded";
|
|
@@ -9153,6 +9503,20 @@ const subagentsOf = (workflow, owner, inputs, outputs = {}) => {
|
|
|
9153
9503
|
});
|
|
9154
9504
|
return entries.length === 0 ? void 0 : Object.fromEntries(entries);
|
|
9155
9505
|
};
|
|
9506
|
+
// ../core/src/run/driver.ts
|
|
9507
|
+
/** The node types that start a session of their own, and so need a driver. */
|
|
9508
|
+
const SESSION_TYPES$1 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
|
|
9509
|
+
/** The driver a workflow names for a node, if it names one. */
|
|
9510
|
+
const namedDriver = (node, workflow) => ("driver" in node ? node.driver : void 0) ?? workflow.defaults?.driver;
|
|
9511
|
+
/** The session nodes that name no driver, which take the one ready here. */
|
|
9512
|
+
const namingNoDriver = (workflow) => Object.entries(workflow.nodes).filter(([, node]) => SESSION_TYPES$1.has(node.type) && namedDriver(node, workflow) === void 0).map(([id]) => id);
|
|
9513
|
+
/**
|
|
9514
|
+
* The driver a session runs on: `override` (`--driver`), else what the workflow
|
|
9515
|
+
* names, else `ready`, the driver the run found ready when it started (and
|
|
9516
|
+
* recorded, for a resume). `undefined` only when none applies, which a run
|
|
9517
|
+
* refuses before it starts.
|
|
9518
|
+
*/
|
|
9519
|
+
const sessionDriver = (choice) => choice.override ?? choice.named ?? choice.ready;
|
|
9156
9520
|
// ../core/src/run/harness.ts
|
|
9157
9521
|
const CLAIMS_LINE = "- Other nodes may change this project at the same time. Before you edit a file or folder, `claim` it (a path from the project root); if someone holds it, work on something else or ask them. `release` what you are done with; Ork releases the rest when you finish.";
|
|
9158
9522
|
const HANDOFF = {
|
|
@@ -9328,6 +9692,7 @@ const planNodeSession = (workflow, id, options) => {
|
|
|
9328
9692
|
permissionMode: "default",
|
|
9329
9693
|
reportSchema: reportSchemaFor(node)
|
|
9330
9694
|
};
|
|
9695
|
+
if (node.type === "claude.leader") spec.promptCache = "1h";
|
|
9331
9696
|
const agents = subagentsOf(workflow, declared, options.inputs, options.outputs);
|
|
9332
9697
|
if (agents !== void 0) {
|
|
9333
9698
|
spec.agents = agents;
|
|
@@ -9348,7 +9713,7 @@ const planNodeSession = (workflow, id, options) => {
|
|
|
9348
9713
|
return {
|
|
9349
9714
|
ok: true,
|
|
9350
9715
|
spec,
|
|
9351
|
-
driver: node
|
|
9716
|
+
driver: namedDriver(node, workflow),
|
|
9352
9717
|
warnings,
|
|
9353
9718
|
context
|
|
9354
9719
|
};
|
|
@@ -9846,7 +10211,7 @@ const resumeState = (events) => {
|
|
|
9846
10211
|
if (event.type === "node.event") {
|
|
9847
10212
|
const inner = event.event;
|
|
9848
10213
|
const current = open.get(event.id);
|
|
9849
|
-
if (current === void 0) continue;
|
|
10214
|
+
if (current === void 0 || inner.fork === true) continue;
|
|
9850
10215
|
if (inner.type === "session.started") current.sessionId = inner.sessionId;
|
|
9851
10216
|
if (inner.type === "session.finished" && inner.outcome.sessionId) current.sessionId = inner.outcome.sessionId;
|
|
9852
10217
|
continue;
|
|
@@ -9990,6 +10355,7 @@ const replayLoopAnswers = (human, answers) => {
|
|
|
9990
10355
|
const left = [...answers];
|
|
9991
10356
|
return {
|
|
9992
10357
|
permit: (request) => human.permit(request),
|
|
10358
|
+
...human.settled === void 0 ? {} : { settled: human.settled },
|
|
9993
10359
|
ask: (question) => {
|
|
9994
10360
|
const index = left.findIndex((a) => a.question === question.question);
|
|
9995
10361
|
const [given] = index === -1 ? [] : left.splice(index, 1);
|
|
@@ -10079,7 +10445,7 @@ const adoptedCalls = (store, runId) => {
|
|
|
10079
10445
|
return adopted;
|
|
10080
10446
|
};
|
|
10081
10447
|
/** What the called run's own `limits.budgetUsd` is capped to, and so on. */
|
|
10082
|
-
const argsOf = (file, id, caller, request) => [
|
|
10448
|
+
const argsOf = (file, id, caller, request, machineClaude) => [
|
|
10083
10449
|
"run",
|
|
10084
10450
|
file,
|
|
10085
10451
|
"--run-id",
|
|
@@ -10088,7 +10454,8 @@ const argsOf = (file, id, caller, request) => [
|
|
|
10088
10454
|
caller,
|
|
10089
10455
|
...Object.entries(request.inputs).flatMap(([name, value]) => ["--input", `${name}=${value}`]),
|
|
10090
10456
|
...request.budgetLeftUsd === void 0 ? [] : ["--budget-cap", String(request.budgetLeftUsd)],
|
|
10091
|
-
...request.ref === void 0 ? [] : ["--base-ref", request.ref]
|
|
10457
|
+
...request.ref === void 0 ? [] : ["--base-ref", request.ref],
|
|
10458
|
+
...machineClaude ? ["--machine-claude"] : []
|
|
10092
10459
|
];
|
|
10093
10460
|
/** Why the target's `calls.accept` shuts this project out, or `null`. */
|
|
10094
10461
|
const notAccepted = (options, request, target) => {
|
|
@@ -10132,7 +10499,7 @@ const startCall = async (options, request) => {
|
|
|
10132
10499
|
const detached = await (options.detach ?? detachRun)({
|
|
10133
10500
|
store,
|
|
10134
10501
|
runId: id,
|
|
10135
|
-
args: argsOf(file, id, `${runId}/${request.callId}`, request),
|
|
10502
|
+
args: argsOf(file, id, `${runId}/${request.callId}`, request, options.machineClaude === true),
|
|
10136
10503
|
cwd: target.root,
|
|
10137
10504
|
runDir: runDirOf(target, id),
|
|
10138
10505
|
env: options.env
|
|
@@ -10396,6 +10763,11 @@ const notRunning = (run) => {
|
|
|
10396
10763
|
const status = liveStatus(run);
|
|
10397
10764
|
return status === "running" ? null : `Run ${run.id} is not running (${status}).`;
|
|
10398
10765
|
};
|
|
10766
|
+
/**
|
|
10767
|
+
* A run of one node (`ork run <wf> --node n`) has no scheduler to hold or to
|
|
10768
|
+
* fail one node through, so it takes only a stop of the whole run.
|
|
10769
|
+
*/
|
|
10770
|
+
const oneNode = (run, cannot) => run.id.startsWith("node-") ? `Run ${run.id} runs one node, which it cannot ${cannot}; stop the whole run instead: ork stop ${run.id}` : null;
|
|
10399
10771
|
/** Every node of a run's tree, spawned instances included. */
|
|
10400
10772
|
const flattenTree = (nodes) => nodes.flatMap((node) => [node, ...flattenTree(node.children)]);
|
|
10401
10773
|
/** What a run's stored events say about it now. */
|
|
@@ -10421,12 +10793,14 @@ const request = async (store, run, command, refusal, home) => {
|
|
|
10421
10793
|
const stopRefusal = (store, run, node) => {
|
|
10422
10794
|
const refused = notRunning(run);
|
|
10423
10795
|
if (refused !== null || node === void 0) return refused;
|
|
10796
|
+
const single = oneNode(run, "stop by name");
|
|
10797
|
+
if (single !== null) return single;
|
|
10424
10798
|
const named = stateOf(store, run).nodes.filter((n) => n.id === node || n.template === node);
|
|
10425
10799
|
if (named.length === 0) return `${node} is not a node of run ${run.id}.`;
|
|
10426
10800
|
return named.some((n) => n.state === "running") ? null : `${node} is not running in run ${run.id}.`;
|
|
10427
10801
|
};
|
|
10428
10802
|
const pauseRefusal = (store, run, node) => {
|
|
10429
|
-
const refused = notRunning(run);
|
|
10803
|
+
const refused = notRunning(run) ?? oneNode(run, "pause or continue");
|
|
10430
10804
|
if (refused !== null) return refused;
|
|
10431
10805
|
const { paused, nodes } = stateOf(store, run);
|
|
10432
10806
|
if (paused.run) return `Run ${run.id} is already paused.`;
|
|
@@ -10435,7 +10809,7 @@ const pauseRefusal = (store, run, node) => {
|
|
|
10435
10809
|
return paused.nodes.includes(node) ? `${node} is already paused in run ${run.id}.` : null;
|
|
10436
10810
|
};
|
|
10437
10811
|
const continueRefusal = (store, run, node) => {
|
|
10438
|
-
const refused = notRunning(run);
|
|
10812
|
+
const refused = notRunning(run) ?? oneNode(run, "pause or continue");
|
|
10439
10813
|
if (refused !== null) return refused;
|
|
10440
10814
|
const { paused } = stateOf(store, run);
|
|
10441
10815
|
if (node === void 0) return paused.run || paused.nodes.length > 0 ? null : `Run ${run.id} is not paused.`;
|
|
@@ -10937,15 +11311,59 @@ const parseInputPairs = (pairs) => {
|
|
|
10937
11311
|
raw
|
|
10938
11312
|
};
|
|
10939
11313
|
};
|
|
11314
|
+
// ../core/src/worktree/git-env.ts
|
|
11315
|
+
/**
|
|
11316
|
+
* @file Git's local environment variables, the ones a git hook sets (`GIT_DIR`,
|
|
11317
|
+
* `GIT_INDEX_FILE`, …), and an environment without them. Ork finds a project
|
|
11318
|
+
* by its path, never through these, so an `ork` started from a hook clears
|
|
11319
|
+
* them at its entry, and core's git runner leaves them out for callers that
|
|
11320
|
+
* don't start there (specs/worktrees.md).
|
|
11321
|
+
*/
|
|
11322
|
+
/**
|
|
11323
|
+
* What `git rev-parse --local-env-vars` prints (git 2.39.5). A fixed list, so
|
|
11324
|
+
* no git is spawned at every start; a test fails when the installed git names
|
|
11325
|
+
* one that is missing here.
|
|
11326
|
+
*/
|
|
11327
|
+
const GIT_LOCAL_ENV = [
|
|
11328
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
11329
|
+
"GIT_CONFIG",
|
|
11330
|
+
"GIT_CONFIG_PARAMETERS",
|
|
11331
|
+
"GIT_CONFIG_COUNT",
|
|
11332
|
+
"GIT_OBJECT_DIRECTORY",
|
|
11333
|
+
"GIT_DIR",
|
|
11334
|
+
"GIT_WORK_TREE",
|
|
11335
|
+
"GIT_IMPLICIT_WORK_TREE",
|
|
11336
|
+
"GIT_GRAFT_FILE",
|
|
11337
|
+
"GIT_INDEX_FILE",
|
|
11338
|
+
"GIT_NO_REPLACE_OBJECTS",
|
|
11339
|
+
"GIT_REPLACE_REF_BASE",
|
|
11340
|
+
"GIT_PREFIX",
|
|
11341
|
+
"GIT_INTERNAL_SUPER_PREFIX",
|
|
11342
|
+
"GIT_SHALLOW_FILE",
|
|
11343
|
+
"GIT_COMMON_DIR"
|
|
11344
|
+
];
|
|
11345
|
+
/** A copy of `env` without git's local variables. */
|
|
11346
|
+
const withoutGitLocalEnv = (env) => {
|
|
11347
|
+
const clean = { ...env };
|
|
11348
|
+
for (const name of GIT_LOCAL_ENV) delete clean[name];
|
|
11349
|
+
return clean;
|
|
11350
|
+
};
|
|
11351
|
+
/** Removes git's local variables from `env` in place (the CLI's entry). */
|
|
11352
|
+
const clearGitLocalEnv = (env = process.env) => {
|
|
11353
|
+
for (const name of GIT_LOCAL_ENV) delete env[name];
|
|
11354
|
+
};
|
|
10940
11355
|
// ../core/src/worktree/git.ts
|
|
10941
11356
|
/**
|
|
10942
11357
|
* @file A minimal `git` runner for the worktree manager: arguments without a
|
|
10943
|
-
* shell, results that never throw
|
|
11358
|
+
* shell, results that never throw, and none of git's local variables (a
|
|
11359
|
+
* hook's `GIT_DIR` or `GIT_INDEX_FILE`), so it acts on the repository at
|
|
11360
|
+
* `cwd` (specs/worktrees.md).
|
|
10944
11361
|
*/
|
|
10945
11362
|
/** Runs `git` with arguments (no shell) in `cwd`. Never throws. */
|
|
10946
11363
|
const git = (cwd, args) => new Promise((resolve) => {
|
|
10947
11364
|
execFile("git", args, {
|
|
10948
11365
|
cwd,
|
|
11366
|
+
env: withoutGitLocalEnv(process.env),
|
|
10949
11367
|
maxBuffer: 33554432,
|
|
10950
11368
|
encoding: "utf8"
|
|
10951
11369
|
}, (error, stdout, stderr) => {
|
|
@@ -11706,6 +12124,8 @@ const isNewer = (candidate, current) => {
|
|
|
11706
12124
|
};
|
|
11707
12125
|
// ../core/src/update/installers.ts
|
|
11708
12126
|
const SOURCE = "/packages/cli/";
|
|
12127
|
+
/** Pnpm's global virtual store, where pnpm 11 keeps global packages. */
|
|
12128
|
+
const PNPM_STORE_LINKS = /\/store\/v\d+\/links\//u;
|
|
11709
12129
|
const has = (...parts) => (script) => parts.some((part) => script.includes(part));
|
|
11710
12130
|
const INSTALLERS = [
|
|
11711
12131
|
{
|
|
@@ -11738,7 +12158,7 @@ const INSTALLERS = [
|
|
|
11738
12158
|
{
|
|
11739
12159
|
id: "pnpm",
|
|
11740
12160
|
label: "pnpm (global)",
|
|
11741
|
-
owns: has("/pnpm/global/", "/.pnpm-global/"),
|
|
12161
|
+
owns: (script) => has("/pnpm/global/", "/.pnpm-global/")(script) || PNPM_STORE_LINKS.test(script),
|
|
11742
12162
|
command: (spec) => [
|
|
11743
12163
|
"pnpm",
|
|
11744
12164
|
"add",
|
|
@@ -12502,4 +12922,4 @@ const createWorkspace = (options) => {
|
|
|
12502
12922
|
close
|
|
12503
12923
|
};
|
|
12504
12924
|
};
|
|
12505
|
-
export {
|
|
12925
|
+
export { text as $, timeoutMs as $n, parseTemplate as $t, gitOk as A, guardedBase as An, runScope as At, linkNodes as B, serializeWorkflow as Bn, FEEDBACK_TARGETS as Bt, workflowTextHash as C, runDirOf as Cn, walkChecks as Ct, inspectProject as D, projectRootOf as Dn, userMessageText as Dt, BRANCH_PREFIX as E, inside as En, MESSAGE_LIMIT as Et, declaredProblem as F, applyEdits as Fn, PROJECT_FILE as Ft, requestContinue as G, expandEdge as Gn, publishPost as Gt, tellNode as H, EdgeInputSchema as Hn, planPost as Ht, createCalls as I, newWorkflowText as In, gitRulesOf as It, tellLiveRun as J, beforeOf as Jn, normalizeTitle as Jt, requestPause as K, settingsOf as Kn, createFeedback as Kt, addressOf as L, readDocument as Ln, loadProject as Lt, clearGitLocalEnv as M, writePlainFile as Mn, envReadsOf as Mt, parseInputPairs as N, skillsIn as Nn, nodeSays as Nt, removeWorktree as O, LESSON_CAP_TOKENS as On, heldTimeout as Ot, resolveInputs as P, WorkflowEditSchema as Pn, declaredOutputOf as Pt, inTransaction as Q, NodeIdSchema as Qn, evaluate as Qt, endLink as R, renderJsonSchema as Rn, FEEDBACK_KINDS as Rt, readProjectTriggers as S, projectsHome as Sn, checkWorkflowText as St, watchingPid as T, workflowDirOf as Tn, startRun as Tt, unlinkNodes as U, WorkflowSchema as Un, runGh as Ut, peerOf as V, markValue as Vn, DISCUSSIONS as Vt, flattenTree as W, CheckReportSchema as Wn, sendPost as Wt, usageQueries as X, isToolNode as Xn, maskReplacer as Xt, backfillUsage as Y, isClaudeNode as Yn, targetOf as Yt, eventOf as Z, programsRunBy as Zn, maskSecrets as Zt, lateTriggers as _, hasInRepoFiles as _n, runStatus as _t, limitsNow as a, wakeRun as an, CLAUDE_DRIVERS as ar, DETACHED_ENV as at, triggerStanding as b, placeFiles as bn, processAlive as bt, usd as c, planMoveInto as cn, explainWorkflowText as ct, runningRunIds as d, toolsInUse as dn, namingNoDriver as dt, renderTemplate as en, INTEGRATIONS as er, adoptedCalls as et, runningScript as f, createAgent as fn, sessionDriver as ft, parseVersion as g, PROJECT_MODES as gn, pausedOf as gt, isNewer as h, loadAgents as hn, liveStatus as ht, NOT_STARTED as i, socketRoom as in, ORK_LAYOUT as ir, replayLoopAnswers as it, lines as j, plainPathInside as jn, calledWorkflows as jt, git as k, estimateTokens as kn, scriptContactsOf as kt, planUpgrade as l, linkTools as ln, explainSession as lt, compareVersions as m, catalogAgent as mn, reportSchemaFor as mt, resolveBase as n, serveRun as nn, integrationFor as nr, resumable as nt, limitsReport as o, planAgentMigration as on, firstLine as or, detachRun as ot, detectInstaller as p, deleteAgent as pn, parseReport as pt, requestStop as q, NodeSchema as qn, fingerprint as qt, capsHere as r, socketPath as rn, AGENT_TOOL_LAYOUTS as rr, createReplay as rt, planReason as s, applySteps as sn, followEvents as st, createWorkspace as t, readCondition as tn, TOOL_PREFIX as tr, detachResume as tt, releaseNotes as u, toolLinks as un, planNodeSession as ut, triggersReport as v, inRepoPlace as vn, runTree as vt, takeWatchLock as w, shownPath as wn, walkSummary as wt, declaredTriggers as x, placeFor as xn, stillTheSameProcess as xt, createWatcher as y, newProjectId as yn, ownProcessStart as yt, endLinksOf as z, parseWorkflow as zn, FEEDBACK_SEVERITIES as zt };
|