@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.
Files changed (37) hide show
  1. package/README.md +16 -12
  2. package/dist/{__build_info__-CtSVVuZE.mjs → __build_info__-D9lSlMFy.mjs} +36 -112
  3. package/dist/bin.mjs +688 -306
  4. package/dist/{clean-Dcr807be.mjs → clean-CA_Oi2Wl.mjs} +1 -1
  5. package/dist/completion-DBGfL-gN.mjs +405 -0
  6. package/dist/{context-C_bksb0C.mjs → context-6x3kXREO.mjs} +11 -88
  7. package/dist/decide-CoIJh34e.mjs +113 -0
  8. package/dist/{find-DptmbhGd.mjs → find-rBBk7Jui.mjs} +1 -1
  9. package/dist/{format-J-a21Yvn.mjs → format-zMmhqyLE.mjs} +71 -37
  10. package/dist/help-groups-BwYoAWsh.mjs +354 -0
  11. package/dist/{in-a-run-D1Qc5Mit.mjs → in-a-run-DdAakpRR.mjs} +1 -1
  12. package/dist/{limits-BDZCmGEu.mjs → limits-BPM9wUlV.mjs} +1 -1
  13. package/dist/{list-ZvY2OcLq.mjs → list-Bv5Lxu7z.mjs} +1 -1
  14. package/dist/{mcp-CHVKKg3B.mjs → mcp-DM30MFBw.mjs} +7 -7
  15. package/dist/{output-CXSJO00X.mjs → output-TV_j7Ni0.mjs} +67 -17
  16. package/dist/{pause-BPSmg4yz.mjs → pause-C7lJkSma.mjs} +4 -4
  17. package/dist/{relay-Do-aNbSD.mjs → relay-BipJYszF.mjs} +2 -2
  18. package/dist/{remove-AwaMMaHf.mjs → remove-BDs9isB6.mjs} +3 -3
  19. package/dist/{send-DhGBkowy.mjs → send-nbkjf3sc.mjs} +3 -3
  20. package/dist/{src-CpDKIulI.mjs → src-B0TT7DBi.mjs} +28 -15
  21. package/dist/{src-BHnxpJyH.mjs → src-B9v0lDc6.mjs} +58 -10
  22. package/dist/{src-UWkk1sl9.mjs → src-BxUJPGa5.mjs} +252 -18
  23. package/dist/{src-CGqMati3.mjs → src-hrj2h0IZ.mjs} +640 -220
  24. package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
  25. package/dist/ui/assets/app-BNT0SuIz.css +1 -0
  26. package/dist/ui/assets/app-CmkynZyW.js +67 -0
  27. package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
  28. package/dist/ui/assets/{index-MQNNgsyT.js → index-CPx74lFf.js} +2 -2
  29. package/dist/ui/index.html +2 -3
  30. package/dist/{ui-DhutUMgQ.mjs → ui-B98FSboa.mjs} +5 -5
  31. package/dist/{usage-CT7Z-yjr.mjs → usage-EW7DI6Z7.mjs} +6 -6
  32. package/dist/{watch-0Qr2UfCQ.mjs → watch-BAkSGaOp.mjs} +7 -7
  33. package/dist/workflows-vsEohqa7.mjs +88 -0
  34. package/dist/{worktrees-DHl80k96.mjs → worktrees-Ahj9rdWK.mjs} +5 -5
  35. package/package.json +3 -3
  36. package/dist/ui/assets/app-BgVDC_5l.js +0 -67
  37. 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
- /** The registry of Claude drivers; the first is the default. */
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
- const CheckSchema = z.discriminatedUnion("type", [McpCallNodeSchema, ...TOOL_NODE_SCHEMAS]);
974
- const TriggerSchema = z.strictObject({
975
- every: everySchema("1m").describe("How long to wait between looks, e.g. 5m. At least 1m."),
976
- check: CheckSchema.optional().describe("What to look at; it reports { \"events\": [{ \"key\": \"...\", \"inputs\": { ... } }] }. Without one, the trigger starts a run every time."),
977
- 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."),
978
- 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."),
979
- maxRunsPerDay: z.number().int().positive().optional().describe("Most runs to start in a calendar day. Default: 10."),
980
- 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.")
981
- });
982
- const TriggersFileSchema = z.strictObject({
983
- schemaVersion: z.literal(1),
984
- triggers: z.record(NodeIdSchema, TriggerSchema)
985
- });
986
- /** What a check reports: each thing it found, and the run's inputs for it. */
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) return {
1084
- ok: false,
1085
- locate,
1086
- diagnostics: result.error.issues.map((issue) => {
1087
- const described = describeIssue(issue, raw, toPath(issue.path));
1088
- return withPosition({
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: `schema.${issue.code}`,
1091
- message: described.message,
1092
- path: described.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
- * and project rules from the Zod models.
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 100 most recently updated
4291
- * threads are read first; one whose body carries the item's fingerprint
4292
- * marker gets a "+1" comment, and otherwise a new thread starts.
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!){repository(owner:$owner,name:$name){discussions(categoryId:$cat,first:${String(100)},orderBy:{field:UPDATED_AT,direction:DESC}){nodes{id url body}}}}`;
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 threads = await graphql(gh, THREADS, {
4445
+ const found = await findThread(gh, {
4358
4446
  ...where,
4359
4447
  cat
4360
- });
4361
- if (!threads.ok) return threads;
4362
- const nodes = field(field(field(threads.data, "repository"), "discussions"), "nodes");
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": ["choice", "answeredBy"],
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 edgeKey = (edge) => `${String(edge.source)}:${edge.from}->${edge.to}`;
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 since the
5947
- * target last ran or was skipped.
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 when any incoming edge is taken, and is skipped once every
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
- const forwardIn = (id) => (graph.nodes.get(id)?.incoming ?? []).filter((e) => !isLoopEdge(e));
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 (isJoin) return arriveAtJoin(id, seen);
6012
- const inputs = forwardIn(id);
6013
- const allDead = inputs.every((e) => seen.get(edgeKey(e))?.taken === false);
6014
- if (inputs.length > 0 && allDead) {
6015
- reset(id);
6016
- return { kind: "skip" };
6017
- }
6018
- return { kind: "wait" };
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 (open.has(event.id)) open.set(event.id, {
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$3 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
6255
- const isSession = (node) => SESSION_TYPES$3.has(node?.type ?? "");
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 run's result: failed when any reason was given, unless it stopped. */
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$2 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
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$2.has(graph.nodes.get(id)?.node.type ?? "");
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$1 = /* @__PURE__ */ new Set(["claude.leader", "claude.worker"]);
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$1.has(node.type)) continue;
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: createRules({
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 (!result.ok && !anyTaken) options.failed(`${id}: ${result.error ?? "failed"}`);
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 = /* @__PURE__ */ new Map();
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 === null) queue.push({
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
- summary(id);
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
- if (stopped === null) await router.route(activation.id, result);
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) queue.push({
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 (path.overran()) overran = true;
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.driver ?? defaults.driver ?? "sdk",
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 { resumable as $, planMoveInto as $t, parseInputPairs as A, parseWorkflow as An, FEEDBACK_KINDS as At, unlinkNodes as B, isClaudeNode as Bn, normalizeTitle as Bt, watchingPid as C, writePlainFile as Cn, runScope as Ct, git as D, newWorkflowText as Dn, nodeSays as Dt, removeWorktree as E, applyEdits as En, envReadsOf as Et, endLink as F, WorkflowSchema as Fn, runGh as Ft, tellLiveRun as G, TOOL_PREFIX as Gn, parseTemplate as Gt, requestContinue as H, programsRunBy as Hn, maskReplacer as Ht, endLinksOf as I, expandEdge as In, sendPost as It, eventOf as J, ORK_LAYOUT as Jn, serveRun as Jt, backfillUsage as K, integrationFor as Kn, renderTemplate as Kt, linkNodes as L, settingsOf as Ln, publishPost as Lt, declaredProblem as M, CheckReportSchema as Mn, FEEDBACK_TARGETS as Mt, createCalls as N, markValue as Nn, DISCUSSIONS as Nt, gitOk as O, readDocument as On, declaredOutputOf as Ot, addressOf as P, EdgeInputSchema as Pn, planPost as Pt, detachResume as Q, applySteps as Qt, peerOf as R, NodeSchema as Rn, createFeedback as Rt, takeWatchLock as S, plainPathInside as Sn, heldTimeout as St, inspectProject as T, WorkflowEditSchema as Tn, calledWorkflows as Tt, requestPause as U, timeoutMs as Un, maskSecrets as Ut, flattenTree as V, isToolNode as Vn, targetOf as Vt, requestStop as W, INTEGRATIONS as Wn, evaluate as Wt, text as X, firstLine as Xn, wakeRun as Xt, inTransaction as Y, CLAUDE_DRIVERS as Yn, socketPath as Yt, adoptedCalls as Z, planAgentMigration as Zt, createWatcher as _, inside as _n, walkChecks as _t, limitsNow as a, catalogAgent as an, explainWorkflowText as at, readProjectTriggers as b, estimateTokens as bn, MESSAGE_LIMIT as bt, usd as c, hasInRepoFiles as cn, parseReport as ct, runningRunIds as d, placeFiles as dn, runStatus as dt, linkTools as en, createReplay as et, runningScript as f, placeFor as fn, runTree as ft, triggersReport as g, workflowDirOf as gn, checkWorkflowText as gt, lateTriggers as h, shownPath as hn, stillTheSameProcess as ht, NOT_STARTED as i, deleteAgent as in, followEvents as it, resolveInputs as j, serializeWorkflow as jn, FEEDBACK_SEVERITIES as jt, lines as k, renderJsonSchema as kn, gitRulesOf as kt, planUpgrade as l, inRepoPlace as ln, liveStatus as lt, isNewer as m, runDirOf as mn, processAlive as mt, resolveBase as n, toolsInUse as nn, DETACHED_ENV as nt, limitsReport as o, loadAgents as on, explainSession as ot, detectInstaller as p, projectsHome as pn, ownProcessStart as pt, usageQueries as q, AGENT_TOOL_LAYOUTS as qn, readCondition as qt, capsHere as r, createAgent as rn, detachRun as rt, planReason as s, PROJECT_MODES as sn, planNodeSession as st, createWorkspace as t, toolLinks as tn, replayLoopAnswers as tt, releaseNotes as u, newProjectId as un, pausedOf as ut, triggerStanding as v, projectRootOf as vn, walkSummary as vt, BRANCH_PREFIX as w, skillsIn as wn, scriptContactsOf as wt, workflowTextHash as x, guardedBase as xn, userMessageText as xt, declaredTriggers as y, LESSON_CAP_TOKENS as yn, startRun as yt, tellNode as z, beforeOf as zn, fingerprint as zt };
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 };