@krapht/ork 1.0.0-alpha.15 → 1.0.0-alpha.17

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 (50) hide show
  1. package/README.md +10 -4
  2. package/dist/{__build_info__-Bsb1kjgF.mjs → __build_info__-C1Nm0hlJ.mjs} +25 -6
  3. package/dist/apply-Mb0fNpCX.mjs +17 -0
  4. package/dist/{attach-CPespxlQ.mjs → attach-EP374oMP.mjs} +5 -5
  5. package/dist/{attach-BHO3X3cf.mjs → attach-RQx__RzH.mjs} +46 -8
  6. package/dist/bin.mjs +238 -369
  7. package/dist/brief-Bs3cMQU0.mjs +100 -0
  8. package/dist/{builtin-files-2bHf76x-.mjs → builtin-files-yYaNTQRU.mjs} +1 -1
  9. package/dist/command-tree-BkSYWmYJ.mjs +63 -0
  10. package/dist/{completion-CQ2UYKuJ.mjs → completion-DIXWlnAl.mjs} +87 -6
  11. package/dist/{context-BNqDw9z1.mjs → context-Dq-VM_sq.mjs} +3 -3
  12. package/dist/{find-BYLlszqj.mjs → find-k489laBa.mjs} +2 -2
  13. package/dist/flags-B4ZZWspN.mjs +125 -0
  14. package/dist/{follow-ups-B4GJ3Vzw.mjs → follow-ups-DD8NTPe6.mjs} +59 -24
  15. package/dist/{format-C8MsFj8_.mjs → format-DQc9A_ei.mjs} +17 -4
  16. package/dist/{help-groups-CQ98mLjF.mjs → help-groups-hB6kY34d.mjs} +1 -63
  17. package/dist/in-a-run-D_9GlAlS.mjs +27 -0
  18. package/dist/{install-DWtXTq4y.mjs → install-PVlV2QPq.mjs} +1 -1
  19. package/dist/{limits-B1qzS4cX.mjs → limits-CHqBO7wb.mjs} +1 -1
  20. package/dist/{mcp-BOrVkyhN.mjs → mcp-D38Ajb0b.mjs} +6 -6
  21. package/dist/{once-C1fL3GVo.mjs → once-DuUzpzL2.mjs} +3 -3
  22. package/dist/parse-BzWpMN-a.mjs +107 -0
  23. package/dist/{pause-DOMK-R8V.mjs → pause-Bx3ePGrn.mjs} +3 -3
  24. package/dist/{post-BzA6Qobq.mjs → post-2CaJz_mV.mjs} +5 -3
  25. package/dist/print-n2qetUHc.mjs +19 -0
  26. package/dist/project-records-Cune1l6I.mjs +230 -0
  27. package/dist/{relay-OWuu4fib.mjs → relay-D7RN4m68.mjs} +1 -1
  28. package/dist/{run-CiPsW9Kf.mjs → run-BfxdsQJk.mjs} +1 -1
  29. package/dist/{run--4-1xAF2.mjs → run-OOWQIKMJ.mjs} +33 -20
  30. package/dist/{send-CnNNW-ot.mjs → send-CKth_vgn.mjs} +2 -2
  31. package/dist/{src-B9zWIVqo.mjs → src-BHod38UQ.mjs} +104 -72
  32. package/dist/{src-Dr_fTSu-.mjs → src-DLsFsmOD.mjs} +790 -502
  33. package/dist/{src-DQKz2eOH.mjs → src-DOjTY2ET.mjs} +238 -79
  34. package/dist/{src-DcrhgipG.mjs → src-DmmrX2_C.mjs} +162 -58
  35. package/dist/{store-oKJ-cCf3.mjs → store-DACH768X.mjs} +1 -1
  36. package/dist/ui/assets/app-9Q-62Ywz.css +1 -0
  37. package/dist/ui/assets/app-CN3acQ-1.js +111 -0
  38. package/dist/ui/assets/{index-BtwkftKe.js → index-B2uGxA4W.js} +2 -2
  39. package/dist/ui/index.html +1 -1
  40. package/dist/{ui-CzKlsCnm.mjs → ui-CJ-BX12n.mjs} +3 -3
  41. package/dist/{usage-DHGNk2JL.mjs → usage-DxZ6JGd4.mjs} +62 -107
  42. package/dist/{validate-Bs_gg22g.mjs → validate-BS0nCccW.mjs} +35 -8
  43. package/dist/{watch-CCCfw3n7.mjs → watch-Dp7IB4FR.mjs} +5 -5
  44. package/dist/{workflows-CW28ZU-z.mjs → workflows-Djo1edlK.mjs} +2 -2
  45. package/dist/{worktrees-CyE8aF6j.mjs → worktrees-Drof_Nyx.mjs} +49 -20
  46. package/package.json +8 -4
  47. package/dist/brief-za0ozN_E.mjs +0 -58
  48. package/dist/in-a-run-Bp0k0J09.mjs +0 -24
  49. package/dist/ui/assets/app-Bdc6OBeZ.css +0 -1
  50. package/dist/ui/assets/app-Drp8Dm9y.js +0 -111
@@ -66,7 +66,10 @@ const experimentsOn = ({ env = process.env, project, where, registry = EXPERIMEN
66
66
  /** Unknown names as errors, for the checks every workflow goes through. */
67
67
  const experimentDiagnostics = (options) => {
68
68
  const state = experimentsOn(options);
69
- return state.ok ? [] : state.unknown.map(({ message }) => error("experiment.unknown", message, []));
69
+ if (state.ok) return [];
70
+ const at = options?.at;
71
+ const inProject = options?.project?.experimental ?? [];
72
+ return state.unknown.map(({ name, source, message }) => source === "project" && at !== void 0 ? Object.assign(error("experiment.unknown", `names an unknown experiment \`${name}\`; known: ${knownExperiments(options?.registry)}`, []), at(["experimental", inProject.indexOf(name)])) : error("experiment.unknown", message, []));
70
73
  };
71
74
  // ../integrations/src/integration.ts
72
75
  /** Declares an integration with its option types checked. */
@@ -563,9 +566,11 @@ const IsolationSchema = z.enum([
563
566
  "shared",
564
567
  "readonly"
565
568
  ]);
566
- const BudgetSchema = z.number().positive();
569
+ const MONEY_MESSAGE = "must be a number above 0";
570
+ const TOKENS_MESSAGE = "must be a whole number above 0";
571
+ const BudgetSchema = z.number(MONEY_MESSAGE).positive(MONEY_MESSAGE);
567
572
  /** A cap in tokens: input, output and cache-write; cache reads don't count. */
568
- const TokenBudgetSchema = z.number().int().positive();
573
+ const TokenBudgetSchema = z.number(TOKENS_MESSAGE).int(TOKENS_MESSAGE).positive(TOKENS_MESSAGE);
569
574
  /** Caps across runs (RFC-007), in Claude Code's estimated dollars. */
570
575
  const QuotasSchema = z.strictObject({
571
576
  dayUsd: BudgetSchema.optional().describe("Most to spend in a calendar day."),
@@ -573,7 +578,7 @@ const QuotasSchema = z.strictObject({
573
578
  weekUsd: BudgetSchema.optional().describe("Most to spend in a calendar week, from Monday."),
574
579
  monthUsd: BudgetSchema.optional().describe("Most to spend in a calendar month.")
575
580
  });
576
- const UNITS = {
581
+ const UNITS$1 = {
577
582
  s: 1e3,
578
583
  m: 6e4,
579
584
  h: 36e5
@@ -583,7 +588,7 @@ const timeoutMs = (timeout) => {
583
588
  const match = /^(\d+)([smh])$/u.exec(timeout ?? "");
584
589
  if (match === null) return void 0;
585
590
  const [, amount = "0", unit = "s"] = match;
586
- return Number(amount) * (UNITS[unit] ?? 1e3);
591
+ return Number(amount) * (UNITS$1[unit] ?? 1e3);
587
592
  };
588
593
  /** The most a timer can hold; a longer one fires at once. */
589
594
  const LONGEST_MS = 2 ** 31 - 1;
@@ -1282,6 +1287,8 @@ const newerVersion = (issues, raw) => {
1282
1287
  * @file Workflow and project YAML: parsing with source positions for
1283
1288
  * diagnostics, and the workflow's byte-stable canonical serializer.
1284
1289
  */
1290
+ /** The message, led by `file:line:col: ` when it is in another file. */
1291
+ const diagnosticText = (d) => d.file === void 0 ? d.message : `${d.file}:${d.line ?? 1}:${d.column ?? 1}: ${d.message}`;
1285
1292
  const SCHEMA_URL = "https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json";
1286
1293
  const createLocate = (doc, counter) => (path) => {
1287
1294
  for (let depth = path.length; depth >= 0; depth -= 1) {
@@ -3133,6 +3140,12 @@ const BUILTIN_AGENTS = {
3133
3140
  * reached through several folders counts once, under its real path. Reads,
3134
3141
  * never writes.
3135
3142
  */
3143
+ /** Where an agent was found: lowest to highest precedence. */
3144
+ const AGENT_SOURCES = [
3145
+ "builtin",
3146
+ "user",
3147
+ "project"
3148
+ ];
3136
3149
  const hashOf = (...parts) => createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 12);
3137
3150
  const isDir = (target) => {
3138
3151
  try {
@@ -4959,6 +4972,8 @@ const RETRY = "This may pass: `ork resume` tries again.";
4959
4972
  const usd$1 = (n) => `$${Number.isInteger(n) ? String(n) : n.toFixed(2)}`;
4960
4973
  const paren = (value) => value === void 0 ? "" : ` (${value})`;
4961
4974
  const colon = (reason) => reason === void 0 ? "" : `: ${reason}`;
4975
+ /** A cap as `used N of M`, or `M` alone when the spend is not known. */
4976
+ const usedOf = (cap, used) => used === void 0 ? cap : `used ${used} of ${cap}`;
4962
4977
  const info = (label, actor, message, hint, flags = {}) => ({
4963
4978
  label,
4964
4979
  message,
@@ -4975,7 +4990,7 @@ const END_KINDS = {
4975
4990
  no_report: info("No report", "agent", () => "ended without handing in a report", "Check that its brief says what to report; its session is in the Logs panel."),
4976
4991
  invalid_report: info("Invalid report", "agent", (f) => `handed in a report that breaks its declared output${colon(f.reason)}`, "Check that its brief asks for every field its `output.schema` declares."),
4977
4992
  turn_limit: info("Turn limit", "workflow", (f) => `reached its turn limit${paren(f.maxTurns === void 0 ? void 0 : String(f.maxTurns))}`, "Raise its `maxTurns`, or give it a smaller task."),
4978
- budget_limit: info("Budget limit", "workflow", (f) => `reached its budget${paren([...f.budgetUsd === void 0 ? [] : [usd$1(f.budgetUsd)], ...f.budgetTokens === void 0 ? [] : [`${String(f.budgetTokens)} tokens`]].join(", ") || void 0)}`, "Raise `budgetUsd` or `budgetTokens` on the node, or the same in the workflow's `limits`, then `ork resume`."),
4993
+ budget_limit: info("Budget limit", "workflow", (f) => `reached its budget${paren([...f.budgetUsd === void 0 ? [] : [usedOf(usd$1(f.budgetUsd), f.usedUsd === void 0 ? void 0 : usd$1(f.usedUsd))], ...f.budgetTokens === void 0 ? [] : [`${usedOf(String(f.budgetTokens), f.usedTokens === void 0 ? void 0 : String(f.usedTokens))} tokens`]].join(", ") || void 0)}`, "Raise `budgetUsd` or `budgetTokens` on the node, or the same in the workflow's `limits`, then `ork resume`."),
4979
4994
  timed_out: info("Timed out", "workflow", (f) => `reached its timeout${paren(f.timeout)}`, "Raise its `timeout`, or give it a smaller task."),
4980
4995
  context_too_long: info("Context too long", "workflow", () => "outgrew its model's context", "Give it a smaller task or less context, or a model with a larger context."),
4981
4996
  plan_limit: info("Plan limit", "account", (f) => `ran into a usage limit${paren(f.window?.replaceAll("_", " "))}`, `Wait until the limit resets. ${RETRY}`, {
@@ -5386,18 +5401,25 @@ const loadProject = (projectRoot, file = path.join(projectRoot, PROJECT_FILE)) =
5386
5401
  if (!existsSync(file)) return { diagnostics: [] };
5387
5402
  const parsed = parseProject(readFileSync(file, "utf8"));
5388
5403
  const label = shownPath(file, projectRoot);
5389
- if (parsed.ok) return {
5390
- project: parsed.project,
5391
- diagnostics: [],
5392
- where: (at) => {
5393
- const position = parsed.locate(at);
5394
- return position === null ? label : `${label}:${position.line}:${position.column}`;
5395
- }
5396
- };
5397
- return { diagnostics: parsed.diagnostics.map((d) => {
5398
- const at = d.line === void 0 ? "" : `:${d.line}:${d.column ?? 1}`;
5399
- return error(d.code, `${label}${at}: ${d.message}`, []);
5400
- }) };
5404
+ if (parsed.ok) {
5405
+ const at = (key) => ({
5406
+ file: label,
5407
+ ...parsed.locate(key)
5408
+ });
5409
+ return {
5410
+ project: parsed.project,
5411
+ diagnostics: [],
5412
+ where: (key) => {
5413
+ const position = parsed.locate(key);
5414
+ return position === null ? label : `${label}:${position.line}:${position.column}`;
5415
+ },
5416
+ at
5417
+ };
5418
+ }
5419
+ return { diagnostics: parsed.diagnostics.map((d) => Object.assign(error(d.code, d.message, []), { file: label }, d.line === void 0 ? {} : {
5420
+ line: d.line,
5421
+ column: d.column ?? 1
5422
+ })) };
5401
5423
  };
5402
5424
  /** Where the user's own settings live. */
5403
5425
  const USER_SETTINGS_FILE = path.join(".ork", "settings.yaml");
@@ -5813,6 +5835,7 @@ const runTree = (events) => {
5813
5835
  }
5814
5836
  node.state = "running";
5815
5837
  node.runs += 1;
5838
+ if (event.description !== void 0) node.description = event.description;
5816
5839
  } else if (event.type === "node.held") {
5817
5840
  const node = nodes.get(event.id);
5818
5841
  if (node !== void 0) node.held = true;
@@ -6426,6 +6449,7 @@ const followUpProblem = (input) => {
6426
6449
  "after-deploy"
6427
6450
  ].includes(input.due) && !date) return "`due` is now, after-merge, after-deploy or a YYYY-MM-DD date";
6428
6451
  if (input.kind === "message" && input.draft === void 0) return "a message needs its `draft` (to, channel, text), which the maintainer sends";
6452
+ if (input.steps?.some((step) => step.text.trim() === "") === true) return "each step needs its text";
6429
6453
  return null;
6430
6454
  };
6431
6455
  /** The project a `project:<name>` follow-up is for, or `undefined`. */
@@ -6757,6 +6781,19 @@ port: (node, cwd) => {
6757
6781
  return result;
6758
6782
  } };
6759
6783
  } });
6784
+ // ../core/src/follow-ups/types.ts
6785
+ const FOLLOW_UP_KINDS = [
6786
+ "change",
6787
+ "message",
6788
+ "task",
6789
+ "check"
6790
+ ];
6791
+ const FOLLOW_UP_STATUSES = [
6792
+ "waiting",
6793
+ "due",
6794
+ "done",
6795
+ "dropped"
6796
+ ];
6760
6797
  // ../core/src/project/mcp-file.ts
6761
6798
  /**
6762
6799
  * @file The project's `.mcp.json`, the file Claude Code reads too, so a server
@@ -7006,7 +7043,17 @@ const itemNodeOf = (node) => {
7006
7043
  const { forEach, ...item } = node;
7007
7044
  return item;
7008
7045
  };
7046
+ /**
7047
+ * What any node reports when its `timeout` expires; known on every type, since
7048
+ * the timeout may come from `defaults` or the project.
7049
+ */
7050
+ const TIMED_OUT_FIELD = "timedOut";
7051
+ /** The fields a node reports, or null when they can't be known ahead. */
7009
7052
  const outputFieldsOf = (node) => {
7053
+ const fields = typeFieldsOf(node);
7054
+ return fields === null ? null : [...fields, TIMED_OUT_FIELD];
7055
+ };
7056
+ const typeFieldsOf = (node) => {
7010
7057
  if (forEachOf$1(node) !== void 0) return GATHERED_FIELDS;
7011
7058
  if (isToolNode(node)) return [
7012
7059
  ...TOOL_OUTPUT_FIELDS,
@@ -7984,6 +8031,21 @@ const checkItemSettings = (workflow, edges) => {
7984
8031
  const nodes = Object.entries(workflow.nodes).flatMap(([id, node]) => isClaudeNode(node) ? FIELDS$1.flatMap((field) => checkNodeField(workflow, says, id, field)) : []);
7985
8032
  return [...defaults, ...nodes];
7986
8033
  };
8034
+ // ../core/src/graph/leader-budget.ts
8035
+ /**
8036
+ * @file The `budget.leader` warning: a leader with nodes to delegate to and no
8037
+ * cap of its own under a run budget. Its session sets aside what is left of
8038
+ * the run's budget in that unit, and a leader's reserve is never waited on
8039
+ * (ADR-077), so the nodes it owns are refused. A compiler warning, like
8040
+ * `loop.uncapped`, on the effective workflow: it never stops a run.
8041
+ */
8042
+ const UNITS = ["budgetUsd", "budgetTokens"];
8043
+ /** One warning per leader and run budget its own cap leaves open. */
8044
+ const leaderBudgetWarnings = (workflow) => Object.entries(workflow.nodes).flatMap(([id, node]) => {
8045
+ if (node.type !== "agent.leader" || !isClaudeNode(node)) return [];
8046
+ if (!Object.values(workflow.nodes).some((child) => child.parent === id && (child.type === "agent.leader" || child.type === "agent.worker"))) return [];
8047
+ return UNITS.filter((unit) => workflow.limits?.[unit] !== void 0 && (node[unit] ?? workflow.defaults?.[unit]) === void 0).map((unit) => warning("budget.leader", `leader \`${id}\` has no ${unit} of its own, so its session sets aside all that is left of limits.${unit} and the nodes it owns are refused; give it a ${unit}, or set one in defaults`, ["nodes", id]));
8048
+ });
7987
8049
  // ../core/src/graph/script.ts
7988
8050
  /**
7989
8051
  * @file Checks on the programs a workflow declares (RFC-006): a tool node's and
@@ -8062,7 +8124,11 @@ const modelDriverProblems = (id, node, workflow) => {
8062
8124
  };
8063
8125
  /** Per-type checks that need the rest of the graph. */
8064
8126
  const checkNodes = (workflow, edges) => {
8065
- const diagnostics = [...checkScripts(workflow), ...checkItemSettings(workflow, edges)];
8127
+ const diagnostics = [
8128
+ ...checkScripts(workflow),
8129
+ ...checkItemSettings(workflow, edges),
8130
+ ...leaderBudgetWarnings(workflow)
8131
+ ];
8066
8132
  for (const [id, node] of Object.entries(workflow.nodes)) {
8067
8133
  const path = ["nodes", id];
8068
8134
  const incoming = edges.filter((e) => e.to === id);
@@ -8702,73 +8768,599 @@ const gitOk = async (cwd, args, env = {}) => {
8702
8768
  };
8703
8769
  /** Output lines without the trailing empty one. */
8704
8770
  const lines = (text) => text.split("\n").filter((line) => line !== "");
8705
- // ../core/src/machine/priority.ts
8771
+ /** Gitignored files a session needs as much in a worktree as in the clone. */
8772
+ const LOCAL_AGENT_FILES = ["CLAUDE.local.md", ".claude/settings.local.json"];
8773
+ /** Where the project's instructions, skills and agents live. */
8774
+ const AGENT_PATHSPECS = [
8775
+ ":(glob)**/CLAUDE.md",
8776
+ ":(glob)**/AGENTS.md",
8777
+ ".claude",
8778
+ ".agents"
8779
+ ];
8780
+ /** Symlinked agent folders (e.g. `.claude/skills -> ../.ai/skills`), as targets. */
8781
+ const symlinkTargets = (root) => {
8782
+ const targets = [];
8783
+ for (const dir of [".claude", ".agents"]) {
8784
+ const base = path.join(root, dir);
8785
+ if (!existsSync(base) || !lstatSync(base).isDirectory()) continue;
8786
+ for (const name of [
8787
+ "skills",
8788
+ "agents",
8789
+ "commands",
8790
+ "rules"
8791
+ ]) {
8792
+ const link = path.join(base, name);
8793
+ if (!existsSync(link) || !lstatSync(link).isSymbolicLink()) continue;
8794
+ const target = path.resolve(base, readlinkSync(link));
8795
+ const relative = path.relative(root, target);
8796
+ if (!relative.startsWith("..") && !path.isAbsolute(relative)) targets.push(relative);
8797
+ }
8798
+ }
8799
+ return [...new Set(targets)];
8800
+ };
8801
+ const statusPaths = async (root, pathspecs) => {
8802
+ const out = await gitOk(root, [
8803
+ "status",
8804
+ "--porcelain=v1",
8805
+ "--untracked-files=all",
8806
+ "--",
8807
+ ...pathspecs
8808
+ ]);
8809
+ return lines(out).map((line) => line.slice(3));
8810
+ };
8811
+ /** The project's git root, head, and what a worktree would be missing. */
8812
+ const inspectProject = async (cwd) => {
8813
+ const top = await git(cwd, ["rev-parse", "--show-toplevel"]);
8814
+ if (top.code !== 0) return null;
8815
+ const root = top.stdout.trim();
8816
+ const head = await git(root, ["rev-parse", "HEAD"]);
8817
+ if (head.code !== 0) return null;
8818
+ const agent = await statusPaths(root, [...AGENT_PATHSPECS, ...symlinkTargets(root)]);
8819
+ const all = await statusPaths(root, ["."]);
8820
+ return {
8821
+ root,
8822
+ head: head.stdout.trim(),
8823
+ dirtyAgentFiles: agent,
8824
+ dirtyOther: all.length - agent.length
8825
+ };
8826
+ };
8706
8827
  /**
8707
- * @file Starting a child at the project's priority (`priority:`): `low`, the
8708
- * default, starts it below Ork's own (under `nice`; on macOS also utility QoS
8709
- * through `taskpolicy`), so the user's own apps come first; `normal` leaves
8710
- * it. Only children: the `ork` process itself stays at normal priority.
8828
+ * The gitignored files to copy into a new worktree: every local agent file, and
8829
+ * what the repo's `.worktreeinclude` lists (Claude Code's convention). Only
8830
+ * files that are also gitignored qualify, so tracked files are never copied.
8711
8831
  */
8712
- /** MacOS's tool for a program's QoS. */
8713
- const TASKPOLICY = "/usr/sbin/taskpolicy";
8714
- /** The POSIX tool that starts a program at a lower priority. */
8715
- const NICE = "/usr/bin/nice";
8716
- /** How far `nice` lowers a `low` child: the same as `PRIORITY_BELOW_NORMAL`. */
8717
- const NICE_STEP = String(constants$1.priority.PRIORITY_BELOW_NORMAL);
8832
+ const localFiles = async (root) => {
8833
+ const include = path.join(root, ".worktreeinclude");
8834
+ const candidates = lines(await gitOk(root, [
8835
+ "ls-files",
8836
+ "--others",
8837
+ "--ignored",
8838
+ ...LOCAL_AGENT_FILES.map((f) => `--exclude=${f}`),
8839
+ ...existsSync(include) ? [`--exclude-from=${include}`] : []
8840
+ ]));
8841
+ if (candidates.length === 0) return [];
8842
+ const ignored = await git(root, [
8843
+ "check-ignore",
8844
+ "--",
8845
+ ...candidates
8846
+ ]);
8847
+ return lines(ignored.stdout);
8848
+ };
8849
+ const copyInto = (from, to, files) => {
8850
+ const copied = [];
8851
+ for (const file of files) {
8852
+ const target = path.join(to, file);
8853
+ if (existsSync(target) || !existsSync(path.join(from, file))) continue;
8854
+ mkdirSync(path.dirname(target), { recursive: true });
8855
+ cpSync(path.join(from, file), target, { verbatimSymlinks: true });
8856
+ copied.push(file);
8857
+ }
8858
+ return copied;
8859
+ };
8718
8860
  /**
8719
- * Whether `bin` is a program `spawn` would find, so a wrapper can't hide its
8720
- * absence.
8861
+ * Creates a worktree at `path` from `base`, with the local files: on a new
8862
+ * `branch`, or detached when there is none (`commit: never`). An existing
8863
+ * branch is checked out again only when this run created it (`known`): a
8864
+ * resumed run's branch outlives its removed worktree. Any other existing branch
8865
+ * stops it: Ork never takes over a branch it did not create.
8721
8866
  */
8722
- const programFound = (bin, env) => bin.includes("/") ? existsSync(bin) : (env["PATH"] ?? "").split(path.delimiter).some((dir) => dir !== "" && existsSync(path.join(dir, bin)));
8867
+ const createWorktree = async (options) => {
8868
+ const { root, branch, base } = options;
8869
+ const target = options.path;
8870
+ mkdirSync(path.dirname(target), { recursive: true });
8871
+ if (branch === void 0) {
8872
+ await gitOk(root, [
8873
+ "worktree",
8874
+ "add",
8875
+ "--detach",
8876
+ target,
8877
+ base
8878
+ ]);
8879
+ return {
8880
+ path: target,
8881
+ copied: copyInto(root, target, await localFiles(root)),
8882
+ reused: false
8883
+ };
8884
+ }
8885
+ const exists = (await git(root, [
8886
+ "rev-parse",
8887
+ "--verify",
8888
+ "--quiet",
8889
+ `refs/heads/${branch}`
8890
+ ])).code === 0;
8891
+ if (exists && options.known !== true) throw new Error(`branch ${branch} already exists, and this run did not create it; Ork never takes over a branch it did not create`);
8892
+ await gitOk(root, exists ? [
8893
+ "worktree",
8894
+ "add",
8895
+ target,
8896
+ branch
8897
+ ] : [
8898
+ "worktree",
8899
+ "add",
8900
+ "-b",
8901
+ branch,
8902
+ target,
8903
+ base
8904
+ ]);
8905
+ return {
8906
+ path: target,
8907
+ branch,
8908
+ copied: copyInto(root, target, await localFiles(root)),
8909
+ reused: exists
8910
+ };
8911
+ };
8912
+ /** Copies files that exist in `from` and are missing in the worktree. */
8913
+ const carryFiles = (from, worktree, files) => copyInto(from, worktree, files.filter((f) => !path.isAbsolute(f) && !f.split(/[\\/]/u).includes("..")));
8914
+ /** What git keeps in a worktree's git dir while a rebase, merge or pick runs. */
8915
+ const UNDERWAY = [
8916
+ "rebase-merge",
8917
+ "rebase-apply",
8918
+ "MERGE_HEAD",
8919
+ "CHERRY_PICK_HEAD",
8920
+ "REVERT_HEAD"
8921
+ ];
8922
+ /** Whether a rebase, merge, cherry-pick or revert is underway in `worktree`. */
8923
+ const gitOperationUnderway = async (worktree) => {
8924
+ const paths = await gitOk(worktree, ["rev-parse", ...UNDERWAY.flatMap((name) => ["--git-path", name])]);
8925
+ return lines(paths).some((file) => existsSync(path.resolve(worktree, file)));
8926
+ };
8723
8927
  /**
8724
- * What to spawn for `bin args` at `priority`. For `low`, when the program is
8725
- * there: under `nice`, so it and what it starts run lower from the first
8726
- * instruction, and on macOS under utility QoS as well; else the program itself,
8727
- * which {@link lowerChild} lowers once it has started.
8928
+ * Commits what a session left uncommitted. The repo's hooks run; when they
8929
+ * refuse, the error says why and the worktree keeps the changes. A worktree
8930
+ * mid-rebase (or merge, cherry-pick, revert) is left exactly as it is: its
8931
+ * changes are the operation's to finish, as a conflict resolver's are.
8728
8932
  */
8729
- const startAt = (priority, bin, args, env = process.env) => {
8730
- if (priority !== "low" || !programFound(bin, env)) return [bin, [...args]];
8731
- const niced = existsSync(NICE) ? [
8732
- NICE,
8733
- "-n",
8734
- NICE_STEP,
8735
- bin
8736
- ] : [bin];
8737
- const [first = bin, ...rest] = process.platform === "darwin" && existsSync(TASKPOLICY) ? [
8738
- TASKPOLICY,
8739
- "-c",
8740
- "utility",
8741
- ...niced
8742
- ] : niced;
8743
- return [first, [...rest, ...args]];
8933
+ const commitLeftovers = async (worktree, message) => {
8934
+ if (await gitOperationUnderway(worktree)) return false;
8935
+ const status = await gitOk(worktree, ["status", "--porcelain"]);
8936
+ if (lines(status).length === 0) return false;
8937
+ await gitOk(worktree, ["add", "-A"]);
8938
+ await gitOk(worktree, [
8939
+ "commit",
8940
+ "-m",
8941
+ message
8942
+ ]);
8943
+ return true;
8744
8944
  };
8745
8945
  /**
8746
- * Lowers a started child's priority for `low` where {@link startAt} had no
8747
- * `nice` to start it under (Windows), best effort; what it starts later
8748
- * inherits it. Under `nice` it does nothing: `nice` lowers relative to the
8749
- * priority it finds, so lowering here too could land first and the two add up.
8946
+ * Moves a worktree's branch up to `branch` when it is behind and has nothing of
8947
+ * its own, e.g. a child's branch after its owner merged it and moved on.
8750
8948
  */
8751
- const lowerChild = (priority, pid) => {
8752
- if (priority !== "low" || pid === void 0 || existsSync(NICE)) return;
8753
- try {
8754
- setPriority(pid, constants$1.priority.PRIORITY_BELOW_NORMAL);
8755
- } catch {}
8756
- };
8757
- // ../core/src/run/machine.ts
8949
+ const fastForward = async (worktree, branch) => (await git(worktree, [
8950
+ "merge",
8951
+ "--ff-only",
8952
+ "-q",
8953
+ branch
8954
+ ])).code === 0;
8758
8955
  /**
8759
- * @file A node's share of the machine (the `machine` of its context): the
8760
- * project's priority for its commands, and a machine-wide slot around each
8761
- * heavy one. A wait is never silent: `slot.waiting` names who holds the
8762
- * slots, `slot.taken` ends it, and the node's clock (its owners' and the
8763
- * run's too) stands still meanwhile, so a timeout counts from the slot.
8956
+ * Merges `branch` into the checkout at `into` with `--no-ff`; aborts on
8957
+ * conflict.
8764
8958
  */
8765
- /** A wait for a slot, as `ork logs`, `ork tree` and the UI say it. */
8766
- const slotWaitLine = (holders) => holders.length === 0 ? "waiting for a check slot" : `waiting for a check slot, held by ${holders.join(", ")}`;
8767
- const none = () => {};
8768
- /** The port for one activation; none for a run that shares nothing. */
8769
- const machinePort = (options, runId, id, clock) => {
8770
- const { machine } = options;
8771
- if (machine === void 0) return void 0;
8959
+ const mergeBranch = async (into, branch, message) => {
8960
+ const merged = await git(into, [
8961
+ "merge",
8962
+ "--no-ff",
8963
+ "--no-edit",
8964
+ "-m",
8965
+ message,
8966
+ branch
8967
+ ]);
8968
+ if (merged.code === 0) return { ok: true };
8969
+ const conflicts = lines((await git(into, [
8970
+ "diff",
8971
+ "--name-only",
8972
+ "--diff-filter=U"
8973
+ ])).stdout);
8974
+ await git(into, ["merge", "--abort"]);
8975
+ return {
8976
+ ok: false,
8977
+ conflicts,
8978
+ error: (merged.stdout + merged.stderr).trim().split("\n").at(-1) ?? "the merge failed"
8979
+ };
8980
+ };
8981
+ const removeWorktree = async (root, worktree, force = false) => {
8982
+ await gitOk(root, [
8983
+ "worktree",
8984
+ "remove",
8985
+ ...force ? ["--force"] : [],
8986
+ worktree
8987
+ ]);
8988
+ };
8989
+ // ../core/src/worktree/clean.ts
8990
+ /**
8991
+ * @file What `ork worktrees clean` and `ork worktrees remove` do with what
8992
+ * list.ts reads: the plan (what goes, what's kept and why), by one rule for
8993
+ * every caller, and carrying it out. The CLI and the Worktrees panel both
8994
+ * plan here, so neither removes what the other would keep.
8995
+ */
8996
+ /** What names an item for `remove`: its branch, or its worktree's path. */
8997
+ const keyOf = (item) => item.branch ?? item.path ?? `${item.runId}/${item.nodeId}`;
8998
+ /** The reasons no `force` overrides: removing would break something. */
8999
+ const MAIN_CHECKOUT = "checked out in the main checkout";
9000
+ const RUN_GOING = "its run is still going";
9001
+ const NOT_ORKS = "not created by Ork";
9002
+ const kept = (item, skip) => ({
9003
+ item,
9004
+ removeWorktree: false,
9005
+ deleteBranch: false,
9006
+ skip
9007
+ });
9008
+ /** What clean does with one item. */
9009
+ const cleanStep = (item, options) => {
9010
+ if (!item.recorded) return kept(item, NOT_ORKS);
9011
+ if (item.inMainCheckout) return kept(item, MAIN_CHECKOUT);
9012
+ if (item.heldBy !== void 0) return kept(item, `checked out in ${item.heldBy}`);
9013
+ if (options.live?.has(item.runId) === true) return kept(item, RUN_GOING);
9014
+ if (item.dirty && !options.force) return kept(item, "uncommitted changes");
9015
+ if (item.branch === void 0 && item.checkedOut === void 0 && item.ahead > 0 && !options.force) return kept(item, "commits on no branch");
9016
+ const removable = item.path !== null && !item.missing;
9017
+ if (options.branches && item.branch !== void 0 && !item.merged && !options.force) return {
9018
+ item,
9019
+ removeWorktree: removable,
9020
+ deleteBranch: false,
9021
+ skip: "branch not merged"
9022
+ };
9023
+ return {
9024
+ item,
9025
+ removeWorktree: removable,
9026
+ deleteBranch: options.branches && item.branch !== void 0
9027
+ };
9028
+ };
9029
+ /** Whether a step drops a worktree whose folder is gone, by its own path. */
9030
+ const stepPrunes = (step) => step.item.missing && step.item.path !== null && step.skip === void 0;
9031
+ /** Whether a step does anything. */
9032
+ const stepActs = (step) => step.removeWorktree || step.deleteBranch || stepPrunes(step);
9033
+ /**
9034
+ * What `ork worktrees clean` would do. A worktree with uncommitted changes, an
9035
+ * unmerged branch, or a detached worktree holding commits no branch has, is
9036
+ * kept unless `force`; one checked out in the main checkout or in a worktree
9037
+ * Ork didn't create, whose run is still going, or an `ork/` branch no run
9038
+ * recorded creating, is kept even then.
9039
+ */
9040
+ const planClean = (items, filter, options) => items.filter((item) => filter.runId === void 0 || item.runId === filter.runId).filter((item) => filter.key === void 0 || keyOf(item) === filter.key).filter((item) => !filter.merged || item.merged).map((item) => cleanStep(item, options));
9041
+ /**
9042
+ * Carries out one step; what it did goes into `done` as it goes. It touches
9043
+ * only its own item: a missing worktree is dropped by its own path (`git
9044
+ * worktree remove`), never by `git worktree prune`, which would drop every
9045
+ * missing worktree in the repository, the user's own included.
9046
+ */
9047
+ const applyStep = async (root, step, force, done) => {
9048
+ const { item } = step;
9049
+ if (stepPrunes(step) && !step.removeWorktree && item.path !== null) {
9050
+ await gitOk(root, [
9051
+ "worktree",
9052
+ "remove",
9053
+ item.path
9054
+ ]);
9055
+ done.push(`pruned ${item.path}`);
9056
+ }
9057
+ if (step.removeWorktree && item.path !== null) {
9058
+ await removeWorktree(root, item.path, force);
9059
+ done.push(`removed ${item.path}`);
9060
+ }
9061
+ if (step.deleteBranch && item.branch !== void 0) {
9062
+ await gitOk(root, [
9063
+ "branch",
9064
+ force ? "-D" : "-d",
9065
+ item.branch
9066
+ ]);
9067
+ done.push(`deleted ${item.branch}`);
9068
+ }
9069
+ };
9070
+ /**
9071
+ * Carries out a clean plan, step by step. What it did goes into `done` as it
9072
+ * goes, so a caller still has it when git fails halfway.
9073
+ */
9074
+ const applyClean = async (root, steps, force, done = []) => {
9075
+ for (const step of steps) await applyStep(root, step, force, done);
9076
+ return done;
9077
+ };
9078
+ // ../core/src/list-filters/tables.ts
9079
+ /**
9080
+ * @file The field tables of the lists whose rows live in core (RFC-017): `ork
9081
+ * ps`, `feedback list`, `follow-ups list`, `decide --list`, `worktrees list`
9082
+ * and `usage`. Each says which flags filter it and what `--ids` prints; each
9083
+ * list's default (follow-ups due and waiting, feedback without the dismissed,
9084
+ * `ps`'s 20 of this project) stays its command's.
9085
+ */
9086
+ /**
9087
+ * A scope-only field: a registered project's id or root, or `all`; the caller
9088
+ * reads the store for it, and refuses one not registered.
9089
+ */
9090
+ const projectField = () => ({
9091
+ name: "project",
9092
+ description: "A registered project's id or root, or all (default: the one you are in)",
9093
+ type: "text",
9094
+ all: true
9095
+ });
9096
+ /** What `--status` matches on `ork ps`: the stored status, or the live one. */
9097
+ const RUN_LIST_STATUSES = [
9098
+ "running",
9099
+ "paused",
9100
+ "crashed",
9101
+ "succeeded",
9102
+ "failed",
9103
+ "budget_exhausted",
9104
+ "quota_exhausted",
9105
+ "timed_out",
9106
+ "interrupted"
9107
+ ];
9108
+ const runStatusOf = (run) => run.listed ?? run.status;
9109
+ /** `ork ps`. */
9110
+ const runListTable = {
9111
+ fields: [
9112
+ {
9113
+ name: "status",
9114
+ description: `Only runs in this status, or all: ${RUN_LIST_STATUSES.join(", ")}`,
9115
+ type: "enum",
9116
+ values: RUN_LIST_STATUSES,
9117
+ all: true,
9118
+ get: runStatusOf
9119
+ },
9120
+ {
9121
+ name: "workflow",
9122
+ description: "Only this workflow's runs",
9123
+ type: "text",
9124
+ get: (run) => run.workflow
9125
+ },
9126
+ projectField(),
9127
+ {
9128
+ name: "since",
9129
+ description: "Only runs started since an age (24h, 30d, 12w), a day (2026-11-01), or all",
9130
+ type: "since",
9131
+ all: true,
9132
+ get: (run) => run.startedAt
9133
+ },
9134
+ {
9135
+ name: "until",
9136
+ description: "Only runs started on or before this day (2026-11-01)",
9137
+ type: "until",
9138
+ get: (run) => run.startedAt
9139
+ }
9140
+ ],
9141
+ id: (run) => run.id
9142
+ };
9143
+ /** A feedback item's statuses, in triage's order. */
9144
+ const FEEDBACK_STATUSES = [
9145
+ "new",
9146
+ "triaged",
9147
+ "accepted",
9148
+ "dismissed",
9149
+ "published",
9150
+ "resolved"
9151
+ ];
9152
+ const feedbackStatusOf = (item) => item.status;
9153
+ /** `ork feedback list`. */
9154
+ const feedbackListTable = {
9155
+ fields: [
9156
+ {
9157
+ name: "status",
9158
+ description: `Only items in this status, or all (dismissed too): ${FEEDBACK_STATUSES.join(", ")}`,
9159
+ type: "enum",
9160
+ values: FEEDBACK_STATUSES,
9161
+ all: true,
9162
+ get: feedbackStatusOf
9163
+ },
9164
+ {
9165
+ name: "kind",
9166
+ description: `Only items of this kind: ${FEEDBACK_KINDS.join(", ")}`,
9167
+ type: "enum",
9168
+ values: FEEDBACK_KINDS,
9169
+ get: (item) => item.kind
9170
+ },
9171
+ {
9172
+ name: "severity",
9173
+ description: `Only items of this severity: ${FEEDBACK_SEVERITIES.join(", ")}`,
9174
+ type: "enum",
9175
+ values: FEEDBACK_SEVERITIES,
9176
+ get: (item) => item.severity
9177
+ },
9178
+ {
9179
+ name: "workflow",
9180
+ description: "Only items this workflow's runs reported",
9181
+ type: "text",
9182
+ get: (item) => item.workflows
9183
+ },
9184
+ {
9185
+ name: "run",
9186
+ description: "Only items this run reported",
9187
+ type: "text",
9188
+ get: (item) => item.runs
9189
+ }
9190
+ ],
9191
+ id: (item) => item.id
9192
+ };
9193
+ /**
9194
+ * What `--due` matches: when a follow-up comes due; `date` is any with a
9195
+ * `YYYY-MM-DD` date.
9196
+ */
9197
+ const FOLLOW_UP_DUES = [
9198
+ "now",
9199
+ "after-merge",
9200
+ "after-deploy",
9201
+ "date"
9202
+ ];
9203
+ /** `ork follow-ups list`. */
9204
+ const followUpListTable = {
9205
+ fields: [
9206
+ {
9207
+ name: "status",
9208
+ description: `Only follow-ups in this status, or all (default: due and waiting): ${FOLLOW_UP_STATUSES.join(", ")}`,
9209
+ type: "enum",
9210
+ values: FOLLOW_UP_STATUSES,
9211
+ all: true,
9212
+ get: (f) => f.status
9213
+ },
9214
+ {
9215
+ name: "for",
9216
+ description: "Only those for agent, me, project:<name> or person:<name>",
9217
+ type: "for",
9218
+ get: (f) => f.for
9219
+ },
9220
+ {
9221
+ name: "kind",
9222
+ description: `Only follow-ups of this kind: ${FOLLOW_UP_KINDS.join(", ")}`,
9223
+ type: "enum",
9224
+ values: FOLLOW_UP_KINDS,
9225
+ get: (f) => f.kind
9226
+ },
9227
+ {
9228
+ name: "due",
9229
+ description: `Only those due this way: ${FOLLOW_UP_DUES.join(", ")} (any dated one)`,
9230
+ type: "enum",
9231
+ values: FOLLOW_UP_DUES,
9232
+ get: (f) => /^\d{4}-\d{2}-\d{2}$/u.test(f.due) ? "date" : f.due
9233
+ },
9234
+ projectField()
9235
+ ],
9236
+ id: (f) => f.id
9237
+ };
9238
+ /** `ork decide --list`: every field scopes the store read. */
9239
+ const decisionListTable = {
9240
+ fields: [
9241
+ {
9242
+ name: "workflow",
9243
+ description: "Only the decisions of this workflow's runs",
9244
+ type: "text"
9245
+ },
9246
+ {
9247
+ name: "run",
9248
+ description: "Only this run's decisions (a run id or a prefix of one)",
9249
+ type: "text"
9250
+ },
9251
+ projectField()
9252
+ ],
9253
+ id: (decision) => decision.id
9254
+ };
9255
+ /** `ork worktrees list`; the id is what `ork worktrees remove` takes. */
9256
+ const worktreeListTable = {
9257
+ fields: [{
9258
+ name: "run",
9259
+ description: "Only this run's worktrees",
9260
+ type: "text",
9261
+ get: (item) => item.runId
9262
+ }, projectField()],
9263
+ id: keyOf
9264
+ };
9265
+ /**
9266
+ * `ork usage`: parse only, its values go straight into `store.usage`'s
9267
+ * `UsageFilter`, which filters; its rows have no id.
9268
+ */
9269
+ const usageListTable = { fields: [
9270
+ {
9271
+ name: "workflow",
9272
+ description: "Only this workflow",
9273
+ type: "text"
9274
+ },
9275
+ projectField(),
9276
+ {
9277
+ name: "since",
9278
+ description: "Whole days counting today (30d, 12w), exact hours (24h), a day (2026-11-01), or all",
9279
+ type: "since",
9280
+ all: true
9281
+ },
9282
+ {
9283
+ name: "until",
9284
+ description: "The last day to include",
9285
+ type: "until"
9286
+ }
9287
+ ] };
9288
+ /** Every core list's table, by the command that lists it. */
9289
+ const LIST_TABLES = {
9290
+ ps: runListTable,
9291
+ "feedback list": feedbackListTable,
9292
+ "follow-ups list": followUpListTable,
9293
+ "decide --list": decisionListTable,
9294
+ "worktrees list": worktreeListTable,
9295
+ usage: usageListTable
9296
+ };
9297
+ // ../core/src/machine/priority.ts
9298
+ /**
9299
+ * @file Starting a child at the project's priority (`priority:`): `low`, the
9300
+ * default, starts it below Ork's own (under `nice`; on macOS also utility QoS
9301
+ * through `taskpolicy`), so the user's own apps come first; `normal` leaves
9302
+ * it. Only children: the `ork` process itself stays at normal priority.
9303
+ */
9304
+ /** MacOS's tool for a program's QoS. */
9305
+ const TASKPOLICY = "/usr/sbin/taskpolicy";
9306
+ /** The POSIX tool that starts a program at a lower priority. */
9307
+ const NICE = "/usr/bin/nice";
9308
+ /** How far `nice` lowers a `low` child: the same as `PRIORITY_BELOW_NORMAL`. */
9309
+ const NICE_STEP = String(constants$1.priority.PRIORITY_BELOW_NORMAL);
9310
+ /**
9311
+ * Whether `bin` is a program `spawn` would find, so a wrapper can't hide its
9312
+ * absence.
9313
+ */
9314
+ const programFound = (bin, env) => bin.includes("/") ? existsSync(bin) : (env["PATH"] ?? "").split(path.delimiter).some((dir) => dir !== "" && existsSync(path.join(dir, bin)));
9315
+ /**
9316
+ * What to spawn for `bin args` at `priority`. For `low`, when the program is
9317
+ * there: under `nice`, so it and what it starts run lower from the first
9318
+ * instruction, and on macOS under utility QoS as well; else the program itself,
9319
+ * which {@link lowerChild} lowers once it has started.
9320
+ */
9321
+ const startAt = (priority, bin, args, env = process.env) => {
9322
+ if (priority !== "low" || !programFound(bin, env)) return [bin, [...args]];
9323
+ const niced = existsSync(NICE) ? [
9324
+ NICE,
9325
+ "-n",
9326
+ NICE_STEP,
9327
+ bin
9328
+ ] : [bin];
9329
+ const [first = bin, ...rest] = process.platform === "darwin" && existsSync(TASKPOLICY) ? [
9330
+ TASKPOLICY,
9331
+ "-c",
9332
+ "utility",
9333
+ ...niced
9334
+ ] : niced;
9335
+ return [first, [...rest, ...args]];
9336
+ };
9337
+ /**
9338
+ * Lowers a started child's priority for `low` where {@link startAt} had no
9339
+ * `nice` to start it under (Windows), best effort; what it starts later
9340
+ * inherits it. Under `nice` it does nothing: `nice` lowers relative to the
9341
+ * priority it finds, so lowering here too could land first and the two add up.
9342
+ */
9343
+ const lowerChild = (priority, pid) => {
9344
+ if (priority !== "low" || pid === void 0 || existsSync(NICE)) return;
9345
+ try {
9346
+ setPriority(pid, constants$1.priority.PRIORITY_BELOW_NORMAL);
9347
+ } catch {}
9348
+ };
9349
+ // ../core/src/run/machine.ts
9350
+ /**
9351
+ * @file A node's share of the machine (the `machine` of its context): the
9352
+ * project's priority for its commands, and a machine-wide slot around each
9353
+ * heavy one. A wait is never silent: `slot.waiting` names who holds the
9354
+ * slots, `slot.taken` ends it, and the node's clock (its owners' and the
9355
+ * run's too) stands still meanwhile, so a timeout counts from the slot.
9356
+ */
9357
+ /** A wait for a slot, as `ork logs`, `ork tree` and the UI say it. */
9358
+ const slotWaitLine = (holders) => holders.length === 0 ? "waiting for a check slot" : `waiting for a check slot, held by ${holders.join(", ")}`;
9359
+ const none = () => {};
9360
+ /** The port for one activation; none for a run that shares nothing. */
9361
+ const machinePort = (options, runId, id, clock) => {
9362
+ const { machine } = options;
9363
+ if (machine === void 0) return void 0;
8772
9364
  const { rules, ledger } = machine;
8773
9365
  return {
8774
9366
  priority: rules.priority,
@@ -9815,6 +10407,17 @@ const reportOf = (child, result) => {
9815
10407
  if (result.error !== void 0) report.error = result.error;
9816
10408
  return report;
9817
10409
  };
10410
+ /** A job with the task's description clipped once; blank is none. */
10411
+ const jobOf = (id, task, template) => {
10412
+ const job = {
10413
+ id,
10414
+ brief: task.brief
10415
+ };
10416
+ if (template !== void 0) job.template = template;
10417
+ const description = task.description?.replaceAll(/[\s\p{Cc}]+/gu, " ").trim().slice(0, 80);
10418
+ if (description !== void 0 && description !== "") job.description = description;
10419
+ return job;
10420
+ };
9818
10421
  const createOwnership = (hooks) => {
9819
10422
  const { graph, leader } = hooks;
9820
10423
  const owner = graph.nodes.get(leader);
@@ -9848,11 +10451,10 @@ const createOwnership = (hooks) => {
9848
10451
  return {
9849
10452
  reports: await Promise.all(jobs.map(async (job) => {
9850
10453
  if (!("brief" in job)) return job;
9851
- const child = { id: job.id };
9852
- if (job.template !== void 0) child.template = job.template;
10454
+ const { brief, ...child } = job;
9853
10455
  const result = await hooks.runChild(child, {
9854
10456
  from: leader,
9855
- text: job.brief
10457
+ text: brief
9856
10458
  });
9857
10459
  return reportOf(job.id, result);
9858
10460
  })),
@@ -9871,10 +10473,7 @@ const createOwnership = (hooks) => {
9871
10473
  }
9872
10474
  if (seen.has(task.child) || hooks.isRunning(task.child)) return rejected(task.child, templates.includes(task.child) && maxWorkers > 0 ? "already running; use spawn_worker for another instance" : "already running");
9873
10475
  seen.add(task.child);
9874
- return {
9875
- id: task.child,
9876
- brief: task.brief
9877
- };
10476
+ return jobOf(task.child, task);
9878
10477
  });
9879
10478
  return runJobs(jobs);
9880
10479
  };
@@ -9893,11 +10492,7 @@ const createOwnership = (hooks) => {
9893
10492
  if (spawned.length >= maxWorkers) return rejected(task.template, `no workers left to spawn (maxWorkers: ${String(maxWorkers)})`);
9894
10493
  const id = nextInstance(task.template);
9895
10494
  spawned.push(id);
9896
- return {
9897
- id,
9898
- template: task.template,
9899
- brief: task.brief
9900
- };
10495
+ return jobOf(id, task, task.template);
9901
10496
  });
9902
10497
  return runJobs(jobs);
9903
10498
  };
@@ -10350,6 +10945,7 @@ const startedEvent = (activation, nodeType, round, agent) => {
10350
10945
  round
10351
10946
  };
10352
10947
  if (activation.brief !== void 0) event.owner = activation.brief.from;
10948
+ if (activation.description !== void 0) event.description = activation.description;
10353
10949
  if (agent !== void 0) event.agent = agent;
10354
10950
  const { item } = activation;
10355
10951
  if (item !== void 0) event.item = {
@@ -11802,7 +12398,7 @@ const startRun = (options) => {
11802
12398
  if (stopped !== null) return;
11803
12399
  stopped = status;
11804
12400
  reasons.push(reason);
11805
- queue.length = 0;
12401
+ for (const { id } of queue.splice(0)) summary(id);
11806
12402
  };
11807
12403
  const enqueue = (id, input) => {
11808
12404
  if (stopped !== null) return;
@@ -12888,7 +13484,8 @@ const checkWorkflowText = (text, { place, workflowDir, registered, home }) => {
12888
13484
  ...loaded.diagnostics,
12889
13485
  ...experimentDiagnostics({
12890
13486
  project: loaded.project,
12891
- where: loaded.where
13487
+ where: loaded.where,
13488
+ at: loaded.at
12892
13489
  }),
12893
13490
  ...compiled.diagnostics,
12894
13491
  ...checkCallTargets(parsed.workflow, place, registered),
@@ -12897,7 +13494,7 @@ const checkWorkflowText = (text, { place, workflowDir, registered, home }) => {
12897
13494
  projectRoot
12898
13495
  })
12899
13496
  ]) {
12900
- const position = parsed.locate(diagnostic.path);
13497
+ const position = diagnostic.file === void 0 ? parsed.locate(diagnostic.path) : null;
12901
13498
  diagnostics.push(position === null ? diagnostic : {
12902
13499
  ...diagnostic,
12903
13500
  ...position
@@ -13505,7 +14102,7 @@ const explainWorkflowText = (text, file, nodeId, where) => {
13505
14102
  const first = diagnostics.find((d) => d.severity === "error");
13506
14103
  return {
13507
14104
  ok: false,
13508
- error: `The workflow has errors, so it can't be planned${first === void 0 ? "." : `: ${first.message}`}`
14105
+ error: `The workflow has errors, so it can't be planned${first === void 0 ? "." : `: ${diagnosticText(first)}`}`
13509
14106
  };
13510
14107
  }
13511
14108
  const result = explainSession(workflow, graph, nodeId, {
@@ -15379,333 +15976,115 @@ const parseInputPairs = (pairs) => {
15379
15976
  // ../core/src/run/replay.ts
15380
15977
  const NO_FAN_OUTS = {
15381
15978
  lists: /* @__PURE__ */ new Map(),
15382
- items: /* @__PURE__ */ new Map()
15383
- };
15384
- /** Whether an activation's input carries a result this run made anew. */
15385
- const carries = (fresh, input) => {
15386
- if (input === void 0) return false;
15387
- if (fresh.has(input)) return true;
15388
- const results = input["results"];
15389
- if (results === null || typeof results !== "object") return false;
15390
- return Object.values(results).some((value) => value !== null && typeof value === "object" && fresh.has(value));
15391
- };
15392
- /**
15393
- * Hands each node's recorded results back in order, until the node runs anew
15394
- * once (it failed or never finished last time, or its input is new); from then
15395
- * on it always runs. A node cut off mid-session continues that session, once.
15396
- */
15397
- const createReplay = (history, cutOff = /* @__PURE__ */ new Map(), mail, earlier, fanOuts = NO_FAN_OUTS) => {
15398
- const sessions = new Map(cutOff);
15399
- const used = /* @__PURE__ */ new Map();
15400
- const anew = /* @__PURE__ */ new Set();
15401
- const fresh = /* @__PURE__ */ new WeakSet();
15402
- /** Fanned nodes whose list was resolved anew: their items can't be replayed. */
15403
- const renewed = /* @__PURE__ */ new Set();
15404
- const takeItem = (activation, of) => {
15405
- const recorded = fanOuts.items.get(activation.id)?.get(activation.round ?? 0);
15406
- if (recorded !== void 0 && !renewed.has(of) && !carries(fresh, activation.input)) return recorded;
15407
- anew.add(activation.id);
15408
- return null;
15409
- };
15410
- return {
15411
- take: (activation) => {
15412
- const { id } = activation;
15413
- if (activation.brief !== void 0 || anew.has(id)) return null;
15414
- if (activation.item !== void 0) return takeItem(activation, activation.item.of);
15415
- const index = used.get(id) ?? 0;
15416
- const recorded = history.get(id)?.[index];
15417
- if (recorded === void 0 || carries(fresh, activation.input)) {
15418
- anew.add(id);
15419
- return null;
15420
- }
15421
- used.set(id, index + 1);
15422
- return recorded;
15423
- },
15424
- ran: (result) => {
15425
- fresh.add(result.output);
15426
- },
15427
- cutOff: (activation) => {
15428
- const session = activation.brief === void 0 && (activation.item === void 0 || !renewed.has(activation.item.of)) ? sessions.get(activation.id) : void 0;
15429
- sessions.delete(activation.id);
15430
- return session;
15431
- },
15432
- listed: (id, round, source) => {
15433
- const list = fanOuts.lists.get(listKey(id, round));
15434
- const ranAnew = typeof source === "object" && source !== null && fresh.has(source);
15435
- if (list !== void 0 && !ranAnew) return list;
15436
- renewed.add(id);
15437
- return null;
15438
- },
15439
- previousSession: ({ id, item, round = 0 }) => item === void 0 || renewed.has(item.of) ? void 0 : [...fanOuts.items.get(id) ?? []].findLast(([before]) => before < round)?.[1].sessionId,
15440
- lastOutput: (id) => history.get(id)?.at(-1)?.output,
15441
- kept: () => [...history.keys(), ...fanOuts.items.keys()],
15442
- ...mail === void 0 ? {} : { mail },
15443
- ...earlier === void 0 ? {} : { earlier }
15444
- };
15445
- };
15446
- /**
15447
- * The run's `HumanChannel`, answering questions about loops out of rounds as
15448
- * they were answered last time, in order; anything else goes to `human`.
15449
- */
15450
- const replayLoopAnswers = (human, answers) => {
15451
- const left = [...answers];
15452
- return {
15453
- permit: (request) => human.permit(request),
15454
- ...human.settled === void 0 ? {} : { settled: human.settled },
15455
- ask: (question) => {
15456
- const index = left.findIndex((a) => a.question === question.question);
15457
- const [given] = index === -1 ? [] : left.splice(index, 1);
15458
- return given === void 0 ? human.ask(question) : Promise.resolve(given.answer);
15459
- }
15460
- };
15461
- };
15462
- // ../core/src/store/drafts.ts
15463
- const rowOf = (row) => {
15464
- const baseHash = text(row, "base_hash");
15465
- return {
15466
- project: text(row, "project"),
15467
- workflow: text(row, "workflow"),
15468
- text: text(row, "text"),
15469
- baseHash: baseHash === "" ? null : baseHash,
15470
- at: text(row, "at")
15471
- };
15472
- };
15473
- const draftQueries = (db) => {
15474
- const put = db.prepare("INSERT OR REPLACE INTO workflow_drafts (project, workflow, text, base_hash, at) VALUES (?, ?, ?, ?, ?)");
15475
- const one = db.prepare("SELECT project, workflow, text, base_hash, at FROM workflow_drafts WHERE project = ? AND workflow = ?");
15476
- const all = db.prepare("SELECT project, workflow, text, base_hash, at FROM workflow_drafts WHERE project = ? ORDER BY workflow");
15477
- const drop = db.prepare("DELETE FROM workflow_drafts WHERE project = ? AND workflow = ?");
15478
- return {
15479
- putDraft: (draft) => {
15480
- put.run(draft.project, draft.workflow, draft.text, draft.baseHash, draft.at);
15481
- },
15482
- draft: (project, workflow) => {
15483
- const row = one.get(project, workflow);
15484
- return row === void 0 ? void 0 : rowOf(row);
15485
- },
15486
- drafts: (project) => all.all(project).map((row) => rowOf(row)),
15487
- dropDraft: (project, workflow) => {
15488
- drop.run(project, workflow);
15489
- }
15490
- };
15491
- };
15492
- /** Gitignored files a session needs as much in a worktree as in the clone. */
15493
- const LOCAL_AGENT_FILES = ["CLAUDE.local.md", ".claude/settings.local.json"];
15494
- /** Where the project's instructions, skills and agents live. */
15495
- const AGENT_PATHSPECS = [
15496
- ":(glob)**/CLAUDE.md",
15497
- ":(glob)**/AGENTS.md",
15498
- ".claude",
15499
- ".agents"
15500
- ];
15501
- /** Symlinked agent folders (e.g. `.claude/skills -> ../.ai/skills`), as targets. */
15502
- const symlinkTargets = (root) => {
15503
- const targets = [];
15504
- for (const dir of [".claude", ".agents"]) {
15505
- const base = path.join(root, dir);
15506
- if (!existsSync(base) || !lstatSync(base).isDirectory()) continue;
15507
- for (const name of [
15508
- "skills",
15509
- "agents",
15510
- "commands",
15511
- "rules"
15512
- ]) {
15513
- const link = path.join(base, name);
15514
- if (!existsSync(link) || !lstatSync(link).isSymbolicLink()) continue;
15515
- const target = path.resolve(base, readlinkSync(link));
15516
- const relative = path.relative(root, target);
15517
- if (!relative.startsWith("..") && !path.isAbsolute(relative)) targets.push(relative);
15518
- }
15519
- }
15520
- return [...new Set(targets)];
15521
- };
15522
- const statusPaths = async (root, pathspecs) => {
15523
- const out = await gitOk(root, [
15524
- "status",
15525
- "--porcelain=v1",
15526
- "--untracked-files=all",
15527
- "--",
15528
- ...pathspecs
15529
- ]);
15530
- return lines(out).map((line) => line.slice(3));
15531
- };
15532
- /** The project's git root, head, and what a worktree would be missing. */
15533
- const inspectProject = async (cwd) => {
15534
- const top = await git(cwd, ["rev-parse", "--show-toplevel"]);
15535
- if (top.code !== 0) return null;
15536
- const root = top.stdout.trim();
15537
- const head = await git(root, ["rev-parse", "HEAD"]);
15538
- if (head.code !== 0) return null;
15539
- const agent = await statusPaths(root, [...AGENT_PATHSPECS, ...symlinkTargets(root)]);
15540
- const all = await statusPaths(root, ["."]);
15541
- return {
15542
- root,
15543
- head: head.stdout.trim(),
15544
- dirtyAgentFiles: agent,
15545
- dirtyOther: all.length - agent.length
15546
- };
15547
- };
15548
- /**
15549
- * The gitignored files to copy into a new worktree: every local agent file, and
15550
- * what the repo's `.worktreeinclude` lists (Claude Code's convention). Only
15551
- * files that are also gitignored qualify, so tracked files are never copied.
15552
- */
15553
- const localFiles = async (root) => {
15554
- const include = path.join(root, ".worktreeinclude");
15555
- const candidates = lines(await gitOk(root, [
15556
- "ls-files",
15557
- "--others",
15558
- "--ignored",
15559
- ...LOCAL_AGENT_FILES.map((f) => `--exclude=${f}`),
15560
- ...existsSync(include) ? [`--exclude-from=${include}`] : []
15561
- ]));
15562
- if (candidates.length === 0) return [];
15563
- const ignored = await git(root, [
15564
- "check-ignore",
15565
- "--",
15566
- ...candidates
15567
- ]);
15568
- return lines(ignored.stdout);
15569
- };
15570
- const copyInto = (from, to, files) => {
15571
- const copied = [];
15572
- for (const file of files) {
15573
- const target = path.join(to, file);
15574
- if (existsSync(target) || !existsSync(path.join(from, file))) continue;
15575
- mkdirSync(path.dirname(target), { recursive: true });
15576
- cpSync(path.join(from, file), target, { verbatimSymlinks: true });
15577
- copied.push(file);
15578
- }
15579
- return copied;
15580
- };
15581
- /**
15582
- * Creates a worktree at `path` from `base`, with the local files: on a new
15583
- * `branch`, or detached when there is none (`commit: never`). An existing
15584
- * branch is checked out again only when this run created it (`known`): a
15585
- * resumed run's branch outlives its removed worktree. Any other existing branch
15586
- * stops it: Ork never takes over a branch it did not create.
15587
- */
15588
- const createWorktree = async (options) => {
15589
- const { root, branch, base } = options;
15590
- const target = options.path;
15591
- mkdirSync(path.dirname(target), { recursive: true });
15592
- if (branch === void 0) {
15593
- await gitOk(root, [
15594
- "worktree",
15595
- "add",
15596
- "--detach",
15597
- target,
15598
- base
15599
- ]);
15600
- return {
15601
- path: target,
15602
- copied: copyInto(root, target, await localFiles(root)),
15603
- reused: false
15604
- };
15605
- }
15606
- const exists = (await git(root, [
15607
- "rev-parse",
15608
- "--verify",
15609
- "--quiet",
15610
- `refs/heads/${branch}`
15611
- ])).code === 0;
15612
- if (exists && options.known !== true) throw new Error(`branch ${branch} already exists, and this run did not create it; Ork never takes over a branch it did not create`);
15613
- await gitOk(root, exists ? [
15614
- "worktree",
15615
- "add",
15616
- target,
15617
- branch
15618
- ] : [
15619
- "worktree",
15620
- "add",
15621
- "-b",
15622
- branch,
15623
- target,
15624
- base
15625
- ]);
15626
- return {
15627
- path: target,
15628
- branch,
15629
- copied: copyInto(root, target, await localFiles(root)),
15630
- reused: exists
15631
- };
15979
+ items: /* @__PURE__ */ new Map()
15632
15980
  };
15633
- /** Copies files that exist in `from` and are missing in the worktree. */
15634
- const carryFiles = (from, worktree, files) => copyInto(from, worktree, files.filter((f) => !path.isAbsolute(f) && !f.split(/[\\/]/u).includes("..")));
15635
- /** What git keeps in a worktree's git dir while a rebase, merge or pick runs. */
15636
- const UNDERWAY = [
15637
- "rebase-merge",
15638
- "rebase-apply",
15639
- "MERGE_HEAD",
15640
- "CHERRY_PICK_HEAD",
15641
- "REVERT_HEAD"
15642
- ];
15643
- /** Whether a rebase, merge, cherry-pick or revert is underway in `worktree`. */
15644
- const gitOperationUnderway = async (worktree) => {
15645
- const paths = await gitOk(worktree, ["rev-parse", ...UNDERWAY.flatMap((name) => ["--git-path", name])]);
15646
- return lines(paths).some((file) => existsSync(path.resolve(worktree, file)));
15981
+ /** Whether an activation's input carries a result this run made anew. */
15982
+ const carries = (fresh, input) => {
15983
+ if (input === void 0) return false;
15984
+ if (fresh.has(input)) return true;
15985
+ const results = input["results"];
15986
+ if (results === null || typeof results !== "object") return false;
15987
+ return Object.values(results).some((value) => value !== null && typeof value === "object" && fresh.has(value));
15647
15988
  };
15648
15989
  /**
15649
- * Commits what a session left uncommitted. The repo's hooks run; when they
15650
- * refuse, the error says why and the worktree keeps the changes. A worktree
15651
- * mid-rebase (or merge, cherry-pick, revert) is left exactly as it is: its
15652
- * changes are the operation's to finish, as a conflict resolver's are.
15990
+ * Hands each node's recorded results back in order, until the node runs anew
15991
+ * once (it failed or never finished last time, or its input is new); from then
15992
+ * on it always runs. A node cut off mid-session continues that session, once.
15653
15993
  */
15654
- const commitLeftovers = async (worktree, message) => {
15655
- if (await gitOperationUnderway(worktree)) return false;
15656
- const status = await gitOk(worktree, ["status", "--porcelain"]);
15657
- if (lines(status).length === 0) return false;
15658
- await gitOk(worktree, ["add", "-A"]);
15659
- await gitOk(worktree, [
15660
- "commit",
15661
- "-m",
15662
- message
15663
- ]);
15664
- return true;
15994
+ const createReplay = (history, cutOff = /* @__PURE__ */ new Map(), mail, earlier, fanOuts = NO_FAN_OUTS) => {
15995
+ const sessions = new Map(cutOff);
15996
+ const used = /* @__PURE__ */ new Map();
15997
+ const anew = /* @__PURE__ */ new Set();
15998
+ const fresh = /* @__PURE__ */ new WeakSet();
15999
+ /** Fanned nodes whose list was resolved anew: their items can't be replayed. */
16000
+ const renewed = /* @__PURE__ */ new Set();
16001
+ const takeItem = (activation, of) => {
16002
+ const recorded = fanOuts.items.get(activation.id)?.get(activation.round ?? 0);
16003
+ if (recorded !== void 0 && !renewed.has(of) && !carries(fresh, activation.input)) return recorded;
16004
+ anew.add(activation.id);
16005
+ return null;
16006
+ };
16007
+ return {
16008
+ take: (activation) => {
16009
+ const { id } = activation;
16010
+ if (activation.brief !== void 0 || anew.has(id)) return null;
16011
+ if (activation.item !== void 0) return takeItem(activation, activation.item.of);
16012
+ const index = used.get(id) ?? 0;
16013
+ const recorded = history.get(id)?.[index];
16014
+ if (recorded === void 0 || carries(fresh, activation.input)) {
16015
+ anew.add(id);
16016
+ return null;
16017
+ }
16018
+ used.set(id, index + 1);
16019
+ return recorded;
16020
+ },
16021
+ ran: (result) => {
16022
+ fresh.add(result.output);
16023
+ },
16024
+ cutOff: (activation) => {
16025
+ const session = activation.brief === void 0 && (activation.item === void 0 || !renewed.has(activation.item.of)) ? sessions.get(activation.id) : void 0;
16026
+ sessions.delete(activation.id);
16027
+ return session;
16028
+ },
16029
+ listed: (id, round, source) => {
16030
+ const list = fanOuts.lists.get(listKey(id, round));
16031
+ const ranAnew = typeof source === "object" && source !== null && fresh.has(source);
16032
+ if (list !== void 0 && !ranAnew) return list;
16033
+ renewed.add(id);
16034
+ return null;
16035
+ },
16036
+ previousSession: ({ id, item, round = 0 }) => item === void 0 || renewed.has(item.of) ? void 0 : [...fanOuts.items.get(id) ?? []].findLast(([before]) => before < round)?.[1].sessionId,
16037
+ lastOutput: (id) => history.get(id)?.at(-1)?.output,
16038
+ kept: () => [...history.keys(), ...fanOuts.items.keys()],
16039
+ ...mail === void 0 ? {} : { mail },
16040
+ ...earlier === void 0 ? {} : { earlier }
16041
+ };
15665
16042
  };
15666
16043
  /**
15667
- * Moves a worktree's branch up to `branch` when it is behind and has nothing of
15668
- * its own, e.g. a child's branch after its owner merged it and moved on.
15669
- */
15670
- const fastForward = async (worktree, branch) => (await git(worktree, [
15671
- "merge",
15672
- "--ff-only",
15673
- "-q",
15674
- branch
15675
- ])).code === 0;
15676
- /**
15677
- * Merges `branch` into the checkout at `into` with `--no-ff`; aborts on
15678
- * conflict.
16044
+ * The run's `HumanChannel`, answering questions about loops out of rounds as
16045
+ * they were answered last time, in order; anything else goes to `human`.
15679
16046
  */
15680
- const mergeBranch = async (into, branch, message) => {
15681
- const merged = await git(into, [
15682
- "merge",
15683
- "--no-ff",
15684
- "--no-edit",
15685
- "-m",
15686
- message,
15687
- branch
15688
- ]);
15689
- if (merged.code === 0) return { ok: true };
15690
- const conflicts = lines((await git(into, [
15691
- "diff",
15692
- "--name-only",
15693
- "--diff-filter=U"
15694
- ])).stdout);
15695
- await git(into, ["merge", "--abort"]);
16047
+ const replayLoopAnswers = (human, answers) => {
16048
+ const left = [...answers];
15696
16049
  return {
15697
- ok: false,
15698
- conflicts,
15699
- error: (merged.stdout + merged.stderr).trim().split("\n").at(-1) ?? "the merge failed"
16050
+ permit: (request) => human.permit(request),
16051
+ ...human.settled === void 0 ? {} : { settled: human.settled },
16052
+ ask: (question) => {
16053
+ const index = left.findIndex((a) => a.question === question.question);
16054
+ const [given] = index === -1 ? [] : left.splice(index, 1);
16055
+ return given === void 0 ? human.ask(question) : Promise.resolve(given.answer);
16056
+ }
15700
16057
  };
15701
16058
  };
15702
- const removeWorktree = async (root, worktree, force = false) => {
15703
- await gitOk(root, [
15704
- "worktree",
15705
- "remove",
15706
- ...force ? ["--force"] : [],
15707
- worktree
15708
- ]);
16059
+ // ../core/src/store/drafts.ts
16060
+ const rowOf = (row) => {
16061
+ const baseHash = text(row, "base_hash");
16062
+ return {
16063
+ project: text(row, "project"),
16064
+ workflow: text(row, "workflow"),
16065
+ text: text(row, "text"),
16066
+ baseHash: baseHash === "" ? null : baseHash,
16067
+ at: text(row, "at")
16068
+ };
16069
+ };
16070
+ const draftQueries = (db) => {
16071
+ const put = db.prepare("INSERT OR REPLACE INTO workflow_drafts (project, workflow, text, base_hash, at) VALUES (?, ?, ?, ?, ?)");
16072
+ const one = db.prepare("SELECT project, workflow, text, base_hash, at FROM workflow_drafts WHERE project = ? AND workflow = ?");
16073
+ const all = db.prepare("SELECT project, workflow, text, base_hash, at FROM workflow_drafts WHERE project = ? ORDER BY workflow");
16074
+ const drop = db.prepare("DELETE FROM workflow_drafts WHERE project = ? AND workflow = ?");
16075
+ return {
16076
+ putDraft: (draft) => {
16077
+ put.run(draft.project, draft.workflow, draft.text, draft.baseHash, draft.at);
16078
+ },
16079
+ draft: (project, workflow) => {
16080
+ const row = one.get(project, workflow);
16081
+ return row === void 0 ? void 0 : rowOf(row);
16082
+ },
16083
+ drafts: (project) => all.all(project).map((row) => rowOf(row)),
16084
+ dropDraft: (project, workflow) => {
16085
+ drop.run(project, workflow);
16086
+ }
16087
+ };
15709
16088
  };
15710
16089
  // ../core/src/worktree/list.ts
15711
16090
  /**
@@ -16275,9 +16654,7 @@ const planLimitQueries = (db) => {
16275
16654
  const all = db.prepare("SELECT name, status, resets_at, at FROM plan_limits ORDER BY name");
16276
16655
  const clear = db.prepare("DELETE FROM plan_limits");
16277
16656
  return {
16278
- clearPlanLimits: () => {
16279
- clear.run();
16280
- },
16657
+ clearPlanLimits: () => Number(clear.run().changes),
16281
16658
  recordPlanLimit: ({ window, status, resetsAt }, at) => {
16282
16659
  put.run(window, status, resetsAt ?? null, at);
16283
16660
  },
@@ -17690,7 +18067,7 @@ const capsHere = (home, place) => {
17690
18067
  try {
17691
18068
  const rules = place === void 0 ? void 0 : loadProject(place.root, placeFiles(place).rules);
17692
18069
  return {
17693
- problems: [...user.diagnostics, ...rules?.diagnostics ?? []].map((found) => found.message),
18070
+ problems: [...user.diagnostics, ...rules?.diagnostics ?? []].map((found) => diagnosticText(found)),
17694
18071
  caps: capsOf(user.settings?.quotas, rules?.project?.quotas, place?.root)
17695
18072
  };
17696
18073
  } catch (error) {
@@ -17843,95 +18220,6 @@ const usageReport = (rows, cpu = []) => {
17843
18220
  hints: hintsOf(totals, groups.node)
17844
18221
  };
17845
18222
  };
17846
- // ../core/src/worktree/clean.ts
17847
- /**
17848
- * @file What `ork worktrees clean` and `ork worktrees remove` do with what
17849
- * list.ts reads: the plan (what goes, what's kept and why), by one rule for
17850
- * every caller, and carrying it out. The CLI and the Worktrees panel both
17851
- * plan here, so neither removes what the other would keep.
17852
- */
17853
- /** What names an item for `remove`: its branch, or its worktree's path. */
17854
- const keyOf = (item) => item.branch ?? item.path ?? `${item.runId}/${item.nodeId}`;
17855
- /** The reasons no `force` overrides: removing would break something. */
17856
- const MAIN_CHECKOUT = "checked out in the main checkout";
17857
- const RUN_GOING = "its run is still going";
17858
- const NOT_ORKS = "not created by Ork";
17859
- const kept = (item, skip) => ({
17860
- item,
17861
- removeWorktree: false,
17862
- deleteBranch: false,
17863
- skip
17864
- });
17865
- /** What clean does with one item. */
17866
- const cleanStep = (item, options) => {
17867
- if (!item.recorded) return kept(item, NOT_ORKS);
17868
- if (item.inMainCheckout) return kept(item, MAIN_CHECKOUT);
17869
- if (item.heldBy !== void 0) return kept(item, `checked out in ${item.heldBy}`);
17870
- if (options.live?.has(item.runId) === true) return kept(item, RUN_GOING);
17871
- if (item.dirty && !options.force) return kept(item, "uncommitted changes");
17872
- if (item.branch === void 0 && item.checkedOut === void 0 && item.ahead > 0 && !options.force) return kept(item, "commits on no branch");
17873
- const removable = item.path !== null && !item.missing;
17874
- if (options.branches && item.branch !== void 0 && !item.merged && !options.force) return {
17875
- item,
17876
- removeWorktree: removable,
17877
- deleteBranch: false,
17878
- skip: "branch not merged"
17879
- };
17880
- return {
17881
- item,
17882
- removeWorktree: removable,
17883
- deleteBranch: options.branches && item.branch !== void 0
17884
- };
17885
- };
17886
- /** Whether a step drops a worktree whose folder is gone, by its own path. */
17887
- const stepPrunes = (step) => step.item.missing && step.item.path !== null && step.skip === void 0;
17888
- /** Whether a step does anything. */
17889
- const stepActs = (step) => step.removeWorktree || step.deleteBranch || stepPrunes(step);
17890
- /**
17891
- * What `ork worktrees clean` would do. A worktree with uncommitted changes, an
17892
- * unmerged branch, or a detached worktree holding commits no branch has, is
17893
- * kept unless `force`; one checked out in the main checkout or in a worktree
17894
- * Ork didn't create, whose run is still going, or an `ork/` branch no run
17895
- * recorded creating, is kept even then.
17896
- */
17897
- const planClean = (items, filter, options) => items.filter((item) => filter.runId === void 0 || item.runId === filter.runId).filter((item) => filter.key === void 0 || keyOf(item) === filter.key).filter((item) => !filter.merged || item.merged).map((item) => cleanStep(item, options));
17898
- /**
17899
- * Carries out one step; what it did goes into `done` as it goes. It touches
17900
- * only its own item: a missing worktree is dropped by its own path (`git
17901
- * worktree remove`), never by `git worktree prune`, which would drop every
17902
- * missing worktree in the repository, the user's own included.
17903
- */
17904
- const applyStep = async (root, step, force, done) => {
17905
- const { item } = step;
17906
- if (stepPrunes(step) && !step.removeWorktree && item.path !== null) {
17907
- await gitOk(root, [
17908
- "worktree",
17909
- "remove",
17910
- item.path
17911
- ]);
17912
- done.push(`pruned ${item.path}`);
17913
- }
17914
- if (step.removeWorktree && item.path !== null) {
17915
- await removeWorktree(root, item.path, force);
17916
- done.push(`removed ${item.path}`);
17917
- }
17918
- if (step.deleteBranch && item.branch !== void 0) {
17919
- await gitOk(root, [
17920
- "branch",
17921
- force ? "-D" : "-d",
17922
- item.branch
17923
- ]);
17924
- done.push(`deleted ${item.branch}`);
17925
- }
17926
- };
17927
- /**
17928
- * Carries out a clean plan, step by step. What it did goes into `done` as it
17929
- * goes, so a caller still has it when git fails halfway.
17930
- */
17931
- const applyClean = async (root, steps, force, done = []) => {
17932
- for (const step of steps) await applyStep(root, step, force, done);
17933
- return done;
17934
- };
17935
18223
  // ../core/src/worktree/names.ts
17936
18224
  /**
17937
18225
  * @file The names Ork gives a node's worktree and branch, from the project's
@@ -18731,4 +19019,4 @@ const createWorkspace = (options) => {
18731
19019
  close
18732
19020
  };
18733
19021
  };
18734
- export { resolveInputs as $, createFeedback as $n, CheckReportSchema as $r, sessionDriver as $t, isNewer as A, stillTheSameProcess as An, shownPath as Ar, walkSummary as At, watchingPid as B, outcomeEnd as Bn, WorkflowEditSchema as Br, stoppedEnd as Bt, capsHere as C, liveStatus as Cn, hasInRepoFiles as Cr, explainWorkflowText as Ct, runningScript as D, slotWaitsOf as Dn, placeFor as Dr, reportSchemaFor as Dt, runningRunIds as E, runStatus as En, placeFiles as Er, parseReport as Et, triggerStanding as F, gitRulesOf as Fn, estimateTokens as Fr, runScope as Ft, applyPrune as G, FEEDBACK_SEVERITIES as Gn, beforeOf as Gr, programFound as Gt, isNodeEvent as H, END_KINDS as Hn, newWorkflowText as Hr, slotClock as Ht, declaredTriggers as I, loadProject as In, guardedBase as Ir, calledWorkflows as It, listOrkWorktrees as J, planPost as Jn, parseWorkflow as Jr, gitOk as Jt, planPrune as K, FEEDBACK_TARGETS as Kn, programsRunBy as Kr, startAt as Kt, readProjectTriggers as L, machineRulesOf as Ln, plainPathInside as Lr, lockWaitLine as Lt, lateTriggers as M, nestItems as Mn, inside as Mr, MESSAGE_LIMIT as Mt, triggersReport as N, countedTokens as Nn, projectRootOf as Nr, userMessageText as Nt, detectInstaller as O, templateOf$1 as On, projectsHome as Or, checkWorkflowText as Ot, createWatcher as P, PROJECT_FILE as Pn, LESSON_CAP_TOKENS as Pr, heldTimeout as Pt, parseInputPairs as Q, redact as Qn, WorkflowSchema as Qr, namingNoDriver as Qt, workflowTextHash as R, createSignals as Rn, writePlainFile as Rr, endedWith as Rt, usageReport as S, knownExperiments as Si, tellLiveRun as Sn, loadAgents as Sr, usd as St, releaseNotes as T, pausedOf as Tn, newProjectId as Tr, planNodeSession as Tt, openStore as U, endFor as Un, readDocument as Ur, slotWaitLine as Ut, activeTree as V, END_DETAIL_LIMIT as Vn, applyEdits as Vr, declaredProblem as Vt, storePath as W, FEEDBACK_KINDS as Wn, renameNodeTypes as Wr, lowerChild as Wt, createReplay as X, sendPost as Xn, markValue as Xr, envReadsOf as Xt, inspectProject as Y, runGh as Yn, serializeWorkflow as Yr, clearGitLocalEnv as Yt, replayLoopAnswers as Z, publishPost as Zn, EdgeInputSchema as Zr, modelDriver as Zt, keyOf as _, AGENT_DRIVERS as _i, linkNodes as _n, toolLinks as _r, followEvents as _t, planRemove as a, DriverSchema as ai, createFollowUps as an, renderTemplate as ar, scriptRefusal as at, stepPrunes as b, EXPERIMENTS as bi, requestPause as bn, deleteAgent as br, limitsReport as bt, repoRoot as c, timeoutMs as ci, recordPublished as cn, socketPath as cr, followRunTree as ct, planLine as d, integrationFor as di, deliverFollowUp as dn, maskReplacer as dr, createCalls as dt, expandEdge as ei, nodeSays as en, fingerprint as er, budgetWarnings as et, removeRefusal as f, PREFERRED_DRIVER as fi, followUpProblem as fn, maskSecrets as fr, adoptedCalls as ft, cleanStep as g, ORK_LAYOUT as gi, endLinksOf as gn, linkTools as gr, detachRun as gt, applyClean as h, AGENT_TOOL_LAYOUTS as hi, addressOf as hn, planMoveInto as hr, DETACHED_ENV as ht, lostWork as i, isToolNode as ii, serversIn as in, parseTemplate as ir, TOO_LONG as it, parseVersion as j, isItem as jn, workflowDirOf as jr, startRun as jt, compareVersions as k, processAlive as kn, runDirOf as kr, walkChecks as kt, labelOf as l, INTEGRATIONS as li, startFollowUpChecks as ln, socketRoom as lr, answerDecision as lt, resolveBase as m, readyDriver as mi, DEFAULT_FOLLOW_UPS_RULE as mn, applySteps as mr, resumable as mt, resumedOptions as n, NodeSchema as ni, MCP_FILE as nn, targetOf as nr, sessionRefusals as nt, projectHead as o, NodeIdSchema as oi, checkFollowUpsDue as on, readCondition as or, sendFromScript as ot, stateParts as p, driverChecks as pi, liveSessionOf as pn, planAgentMigration as pr, detachResume as pt, createdWorktrees as q, DISCUSSIONS as qn, renderJsonSchema as qr, git as qt, applyRemove as r, isClaudeNode as ri, mcpNotText as rn, evaluate as rr, driverQueues as rt, readWorktrees as s, retiredDriverHint as si, markDue as sn, serveRun as sr, createRelay as st, createWorkspace as t, settingsOf as ti, declaredOutputOf as tn, normalizeTitle as tr, driverRefusals as tt, mergeStateOf as u, TOOL_PREFIX as ui, takeFollowUps as un, MASK as ur, pendingDecisions as ut, planClean as v, DRIVER_IDS as vi, unlinkNodes as vn, toolsInUse as vr, NOT_STARTED as vt, planUpgrade as w, lockWaitsOf as wn, inRepoPlace as wr, explainSession as wt, USAGE_DIMENSIONS as x, experimentsOn as xi, requestStop as xn, catalogAgent as xr, planReason as xt, stepActs as y, firstLine as yi, requestContinue as yn, createAgent as yr, limitsNow as yt, takeWatchLock as z, endOf as zn, skillsIn as zr, failedWith as zt };
19022
+ export { sendFromScript as $, FEEDBACK_TARGETS as $n, beforeOf as $r, git as $t, declaredTriggers as A, knownExperiments as Ai, liveStatus as An, loadAgents as Ar, endedWith as At, planPrune as B, countedTokens as Bn, inside as Br, decisionListTable as Bt, compareVersions as C, AGENT_TOOL_LAYOUTS as Ci, endLinksOf as Cn, linkTools as Cr, startRun as Ct, triggersReport as D, firstLine as Di, requestPause as Dn, deleteAgent as Dr, runScope as Dt, lateTriggers as E, DRIVER_IDS as Ei, requestContinue as En, createAgent as Er, heldTimeout as Et, activeTree as F, templateOf$1 as Fn, placeFor as Fr, slotWaitLine as Ft, parseInputPairs as G, createSignals as Gn, plainPathInside as Gr, worktreeListTable as Gt, listOrkWorktrees as H, gitRulesOf as Hn, LESSON_CAP_TOKENS as Hr, followUpListTable as Ht, isNodeEvent as I, processAlive as In, projectsHome as Ir, lowerChild as It, driverRefusals as J, END_DETAIL_LIMIT as Jn, WorkflowEditSchema as Jr, keyOf as Jt, resolveInputs as K, endOf as Kn, writePlainFile as Kr, applyClean as Kt, openStore as L, stillTheSameProcess as Ln, runDirOf as Lr, programFound as Lt, workflowTextHash as M, pausedOf as Mn, inRepoPlace as Mr, stoppedEnd as Mt, takeWatchLock as N, runStatus as Nn, newProjectId as Nr, declaredProblem as Nt, createWatcher as O, EXPERIMENTS as Oi, requestStop as On, AGENT_SOURCES as Or, calledWorkflows as Ot, watchingPid as P, slotWaitsOf as Pn, placeFiles as Pr, slotClock as Pt, scriptRefusal as Q, FEEDBACK_SEVERITIES as Qn, renameNodeTypes as Qr, inspectProject as Qt, storePath as R, isItem as Rn, shownPath as Rr, startAt as Rt, detectInstaller as S, readyDriver as Si, addressOf as Sn, planMoveInto as Sr, walkSummary as St, parseVersion as T, AGENT_DRIVERS as Ti, unlinkNodes as Tn, toolsInUse as Tr, userMessageText as Tt, createReplay as U, loadProject as Un, estimateTokens as Ur, runListTable as Ut, createdWorktrees as V, PROJECT_FILE as Vn, projectRootOf as Vr, feedbackListTable as Vt, replayLoopAnswers as W, machineRulesOf as Wn, guardedBase as Wr, usageListTable as Wt, driverQueues as X, endFor as Xn, newWorkflowText as Xr, stepActs as Xt, sessionRefusals as Y, END_KINDS as Yn, applyEdits as Yr, planClean as Yt, TOO_LONG as Z, FEEDBACK_KINDS as Zn, readDocument as Zr, stepPrunes as Zt, capsHere as _, INTEGRATIONS as _i, takeFollowUps as _n, MASK as _r, planNodeSession as _t, planRemove as a, EdgeInputSchema as ai, sessionDriver as an, redact as ar, adoptedCalls as at, runningRunIds as b, PREFERRED_DRIVER as bi, liveSessionOf as bn, planAgentMigration as br, checkWorkflowText as bt, repoRoot as c, expandEdge as ci, MCP_FILE as cn, normalizeTitle as cr, DETACHED_ENV as ct, planLine as d, isClaudeNode as di, FOLLOW_UP_KINDS as dn, parseTemplate as dr, NOT_STARTED as dt, programsRunBy as ei, gitOk as en, DISCUSSIONS as er, createRelay as et, removeRefusal as f, isToolNode as fi, createFollowUps as fn, renderTemplate as fr, limitsNow as ft, usageReport as g, timeoutMs as gi, startFollowUpChecks as gn, socketRoom as gr, explainSession as gt, USAGE_DIMENSIONS as h, retiredDriverHint as hi, recordPublished as hn, socketPath as hr, explainWorkflowText as ht, lostWork as i, markValue as ii, namingNoDriver as in, publishPost as ir, createCalls as it, readProjectTriggers as j, lockWaitsOf as jn, hasInRepoFiles as jr, failedWith as jt, triggerStanding as k, experimentsOn as ki, tellLiveRun as kn, catalogAgent as kr, lockWaitLine as kt, labelOf as l, settingsOf as li, mcpNotText as ln, targetOf as lr, detachRun as lt, resolveBase as m, NodeIdSchema as mi, markDue as mn, serveRun as mr, planReason as mt, resumedOptions as n, parseWorkflow as ni, envReadsOf as nn, runGh as nr, answerDecision as nt, projectHead as o, WorkflowSchema as oi, nodeSays as on, createFeedback as or, detachResume as ot, stateParts as p, DriverSchema as pi, checkFollowUpsDue as pn, readCondition as pr, limitsReport as pt, budgetWarnings as q, outcomeEnd as qn, skillsIn as qr, cleanStep as qt, applyRemove as r, serializeWorkflow as ri, modelDriver as rn, sendPost as rr, pendingDecisions as rt, readWorktrees as s, CheckReportSchema as si, declaredOutputOf as sn, fingerprint as sr, resumable as st, createWorkspace as t, renderJsonSchema as ti, clearGitLocalEnv as tn, planPost as tr, followRunTree as tt, mergeStateOf as u, NodeSchema as ui, serversIn as un, evaluate as ur, followEvents as ut, planUpgrade as v, TOOL_PREFIX as vi, deliverFollowUp as vn, maskReplacer as vr, parseReport as vt, isNewer as w, ORK_LAYOUT as wi, linkNodes as wn, toolLinks as wr, MESSAGE_LIMIT as wt, runningScript as x, driverChecks as xi, DEFAULT_FOLLOW_UPS_RULE as xn, applySteps as xr, walkChecks as xt, releaseNotes as y, integrationFor as yi, followUpProblem as yn, maskSecrets as yr, reportSchemaFor as yt, applyPrune as z, nestItems as zn, workflowDirOf as zr, LIST_TABLES as zt };