@krapht/ork 1.0.0-alpha.6 → 1.0.0-alpha.8

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 (32) hide show
  1. package/dist/{__build_info__-CxH89h1r.mjs → __build_info__-B83zdwIv.mjs} +72 -3
  2. package/dist/bin.mjs +150 -45
  3. package/dist/{clean-BbrkMQQi.mjs → clean-NtapkN2x.mjs} +1 -1
  4. package/dist/{completion-BeS2NCgO.mjs → completion-DAQMrf1u.mjs} +5 -5
  5. package/dist/{context-CHV6aTs7.mjs → context-C5wQNjp9.mjs} +3 -3
  6. package/dist/{decide-Cnt7o3ya.mjs → decide-BaEOo7sO.mjs} +1 -1
  7. package/dist/{find-Dy2H5IvT.mjs → find-DDAkEoda.mjs} +1 -1
  8. package/dist/{format-OL0E1AjA.mjs → format-sGEQmQid.mjs} +14 -4
  9. package/dist/{in-a-run-emTc-kZI.mjs → in-a-run-DENitTUe.mjs} +1 -1
  10. package/dist/{limits-TxTnWxix.mjs → limits-DezRXN0Z.mjs} +1 -1
  11. package/dist/{list-P5CfcoWx.mjs → list-CZTh_wdE.mjs} +1 -1
  12. package/dist/{mcp-qUwFT9yi.mjs → mcp-B2DW26HO.mjs} +6 -6
  13. package/dist/{output-CjOPL8W0.mjs → output-D1wMNIly.mjs} +11 -2
  14. package/dist/{pause-CBSAxtTp.mjs → pause-HuQhHyET.mjs} +4 -4
  15. package/dist/{relay-UgK1Cp9a.mjs → relay-DCGGYSdz.mjs} +2 -2
  16. package/dist/{remove-ccycC3vI.mjs → remove-0L02cre2.mjs} +3 -3
  17. package/dist/{send-B39-Xeo7.mjs → send-C8pQfhED.mjs} +3 -3
  18. package/dist/{src-5-sCA7cY.mjs → src-BNhCiDQw.mjs} +5 -3
  19. package/dist/{src-8nec4uoe.mjs → src-COHxAj7X.mjs} +40 -23
  20. package/dist/{src-jyCJE1kd.mjs → src-DCCbIABO.mjs} +9 -78
  21. package/dist/{src-DG4dyBqm.mjs → src-DosUvy4B.mjs} +3946 -3217
  22. package/dist/ui/assets/app-D14lfYns.css +1 -0
  23. package/dist/ui/assets/{app-C1DAgdU2.js → app-DBYZFrdE.js} +19 -19
  24. package/dist/ui/assets/{index-BZKXPhht.js → index-DXoKpYjA.js} +2 -2
  25. package/dist/ui/index.html +1 -1
  26. package/dist/{ui-DmJWLgxL.mjs → ui-Cdb05xJw.mjs} +4 -4
  27. package/dist/{usage-Bh1lVioA.mjs → usage-jYI2QHt7.mjs} +5 -5
  28. package/dist/{watch-C_vcsn_g.mjs → watch-D4lFXnHT.mjs} +6 -6
  29. package/dist/{workflows-lpexsAYt.mjs → workflows-BF9fO-ER.mjs} +3 -3
  30. package/dist/{worktrees-D4bVVKfY.mjs → worktrees-0iUiA6eD.mjs} +4 -4
  31. package/package.json +1 -1
  32. package/dist/ui/assets/app-Dt_tjFj6.css +0 -1
@@ -1,4 +1,4 @@
1
- import { Cn as placeFiles, Fn as writePlainFile, Ht as FEEDBACK_SEVERITIES, Jt as publishPost, Mn as estimateTokens, Nn as guardedBase, Pn as plainPathInside, Qt as targetOf, Vt as FEEDBACK_KINDS, Xt as fingerprint, cn as maskSecrets, g as parseVersion, h as isNewer, jn as LESSON_CAP_TOKENS, kn as inside, m as compareVersions, wn as placeFor } from "./src-DG4dyBqm.mjs";
1
+ import { Ct as runTree, En as placeFor, Fn as guardedBase, In as plainPathInside, Ln as writePlainFile, Nn as LESSON_CAP_TOKENS, Pn as estimateTokens, Qt as fingerprint, Tn as placeFiles, Ut as FEEDBACK_KINDS, Wt as FEEDBACK_SEVERITIES, Xt as publishPost, en as targetOf, g as parseVersion, h as isNewer, jn as inside, m as compareVersions, un as maskSecrets } from "./src-DosUvy4B.mjs";
2
2
  import path from "node:path";
3
3
  import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
@@ -410,6 +410,75 @@ const nodeSession = (events, node) => {
410
410
  outputs
411
411
  };
412
412
  };
413
+ // ../core/src/store/activity.ts
414
+ const LONGEST_LINE = 120;
415
+ /**
416
+ * The fields of a tool's input that say what it's working on, best first. Only
417
+ * these are shown, so what else a tool was given stays in the logs.
418
+ */
419
+ const TARGETS = [
420
+ "file_path",
421
+ "notebook_path",
422
+ "path",
423
+ "url",
424
+ "pattern",
425
+ "query",
426
+ "command"
427
+ ];
428
+ const oneLine = (text) => {
429
+ const line = text.replaceAll(/\s+/gu, " ").trim();
430
+ return line.length > LONGEST_LINE ? `${line.slice(0, 119)}…` : line;
431
+ };
432
+ const toolLine = ({ name, input }) => {
433
+ const target = TARGETS.map((field) => input[field]).find((value) => typeof value === "string" && value.trim() !== "");
434
+ if (typeof target !== "string") return name;
435
+ return oneLine(target.startsWith(`${name} `) ? target : `${name} ${target}`);
436
+ };
437
+ /** What a finished node said, or why it failed. */
438
+ const finishedNow = (ok, error, summary) => {
439
+ if (!ok) return error === void 0 ? void 0 : oneLine(error);
440
+ return typeof summary === "string" && summary.trim() !== "" ? oneLine(summary) : void 0;
441
+ };
442
+ /** Each node's activity, by id; a node that never started has none. */
443
+ const runActivity = (stored) => {
444
+ const nodes = /* @__PURE__ */ new Map();
445
+ const of = (id) => {
446
+ const found = nodes.get(id);
447
+ if (found !== void 0) return found;
448
+ const fresh = {};
449
+ nodes.set(id, fresh);
450
+ return fresh;
451
+ };
452
+ for (const { at, event } of stored) if (event.type === "node.started") {
453
+ const node = of(event.id);
454
+ node.startedAt ??= at;
455
+ delete node.finishedAt;
456
+ delete node.now;
457
+ } else if (event.type === "node.event") {
458
+ if (event.event.type === "tool.called") of(event.id).now = toolLine(event.event);
459
+ } else if (event.type === "decision.opened") of(event.node).now = oneLine(`Asked: ${event.question}`);
460
+ else if (event.type === "decision.answered") of(event.node).now = oneLine(`Answered: ${event.answer}`);
461
+ else if (event.type === "decision.withdrawn") delete of(event.node).now;
462
+ else if (event.type === "node.finished") {
463
+ const node = of(event.id);
464
+ node.finishedAt = at;
465
+ const now = finishedNow(event.ok, event.error, event.output["summary"]);
466
+ if (now === void 0) delete node.now;
467
+ else node.now = now;
468
+ }
469
+ return nodes;
470
+ };
471
+ /** The ownership tree, with what each node is doing and when it ran. */
472
+ const activeTree = (stored) => {
473
+ const activity = runActivity(stored);
474
+ const dress = (node) => {
475
+ Object.assign(node, activity.get(node.id));
476
+ for (const child of node.children) dress(child);
477
+ };
478
+ const roots = runTree(stored.map(({ event }) => event));
479
+ for (const root of roots) dress(root);
480
+ return roots;
481
+ };
413
482
  /** The dist-tags every install follows, toward and after 1.0.0. */
414
483
  const CHANNEL_TAGS = [
415
484
  "latest",
@@ -611,5 +680,5 @@ const registryShown = (registry) => {
611
680
  * fails when it is stale.
612
681
  */
613
682
  const ORK_NAME = "@krapht/ork";
614
- const ORK_VERSION = "1.0.0-alpha.6";
615
- export { feedbackActions as C, placeOf as S, previewPost as _, registryShown as a, acceptItem as b, checkDue as c, pendingNotice as d, readUpdateCache as f, nodeSession as g, writeUpdateCache as h, registryOf as i, fetchLatest as l, updateCachePath as m, ORK_VERSION as n, updateSwitchOff as o, refreshUpdateCache as p, isSwitchedOn as r, runInstall as s, ORK_NAME as t, newerVersion as u, checkNote as v, openFixRun as w, lessonPlaceOf as x, recordNote as y };
683
+ const ORK_VERSION = "1.0.0-alpha.8";
684
+ export { placeOf as C, lessonPlaceOf as S, openFixRun as T, nodeSession as _, registryShown as a, recordNote as b, checkDue as c, pendingNotice as d, readUpdateCache as f, activeTree as g, writeUpdateCache as h, registryOf as i, fetchLatest as l, updateCachePath as m, ORK_VERSION as n, updateSwitchOff as o, refreshUpdateCache as p, isSwitchedOn as r, runInstall as s, ORK_NAME as t, newerVersion as u, previewPost as v, feedbackActions as w, acceptItem as x, checkNote as y };
package/dist/bin.mjs CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { An as projectRootOf, Cn as placeFiles, D as inspectProject, Dn as shownPath, Dt as userMessageText, En as runDirOf, Ft as PROJECT_FILE, Gt as planPost, H as tellNode, Ht as FEEDBACK_SEVERITIES, I as createCalls, It as gitRulesOf, J as tellLiveRun, Kn as WorkflowSchema, L as addressOf, Lt as loadProject, M as clearGitLocalEnv, N as parseInputPairs, P as resolveInputs, Qn as isClaudeNode, R as endLink, Sn as newProjectId, Tn as projectsHome, Tt as startRun, Un as serializeWorkflow, Ut as FEEDBACK_TARGETS, V as peerOf, Vt as FEEDBACK_KINDS, W as flattenTree, Xt as fingerprint, Yn as settingsOf, Yt as createFeedback, Zn as beforeOf, Zt as normalizeTitle, _ as lateTriggers, _n as catalogAgent, _t as runStatus, an as socketRoom, bn as hasInRepoFiles, cr as CLAUDE_DRIVERS, d as runningRunIds, dn as planMoveInto, dr as experimentsOn, dt as namingNoDriver, et as adoptedCalls, f as runningScript, fn as linkTools, fr as knownExperiments, ft as sessionDriver, gn as deleteAgent, h as isNewer, hn as createAgent, ht as liveStatus, i as NOT_STARTED, in as socketPath, it as replayLoopAnswers, kn as inside, l as planUpgrade, ln as planAgentMigration, lr as firstLine, mn as toolsInUse, n as resolveBase, nt as resumable, on as wakeRun, or as AGENT_TOOL_LAYOUTS, ot as detachRun, pn as toolLinks, q as requestStop, qt as sendPost, rn as serveRun, rr as INTEGRATIONS, rt as createReplay, sn as maskReplacer, sr as ORK_LAYOUT, st as followEvents, t as createWorkspace, tt as detachResume, u as releaseNotes, un as applySteps, ur as EXPERIMENTS, ut as planNodeSession, v as triggersReport, vn as loadAgents, vt as runTree, wn as placeFor, wt as walkSummary, xn as inRepoPlace, z as endLinksOf } from "./src-DG4dyBqm.mjs";
3
- import { a as formatRunEvent, i as formatEvent, n as formatRuns, o as createTerminalHuman, r as formatTree, s as createDecisionQueue, t as ago } from "./format-OL0E1AjA.mjs";
4
- import { C as feedbackActions, S as placeOf, _ as previewPost, b as acceptItem, c as checkDue, d as pendingNotice, f as readUpdateCache, g as nodeSession, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, n as ORK_VERSION, o as updateSwitchOff, p as refreshUpdateCache, r as isSwitchedOn, s as runInstall, t as ORK_NAME, u as newerVersion$1, v as checkNote, w as openFixRun, y as recordNote } from "./__build_info__-CxH89h1r.mjs";
5
- import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-Cnt7o3ya.mjs";
6
- import { n as newRunId, t as findRun } from "./find-Dy2H5IvT.mjs";
7
- import { a as write, c as openStore, i as style, l as storePath, n as cost, o as writeErr, s as nodeOf, t as confirm } from "./output-CjOPL8W0.mjs";
8
- import { n as listOrkWorktrees, t as createdWorktrees } from "./list-P5CfcoWx.mjs";
9
- import { a as planClean, t as applyClean } from "./clean-BbrkMQQi.mjs";
2
+ import { $ as adoptedCalls, $n as settingsOf, $t as normalizeTitle, B as peerOf, Cn as inRepoPlace, Ct as runTree, D as inspectProject, Dn as projectsHome, En as placeFor, F as createCalls, Gt as FEEDBACK_TARGETS, I as addressOf, K as requestStop, L as endLink, Lt as PROJECT_FILE, M as clearGitLocalEnv, Mn as projectRootOf, N as parseInputPairs, On as runDirOf, P as resolveInputs, Qt as fingerprint, R as endLinksOf, Rt as gitRulesOf, Sn as hasInRepoFiles, St as runStatus, Tn as placeFiles, U as flattenTree, Un as beforeOf, Ut as FEEDBACK_KINDS, V as tellNode, Wt as FEEDBACK_SEVERITIES, Xn as WorkflowSchema, Yt as sendPost, Zt as createFeedback, _ as lateTriggers, _n as createAgent, an as serveRun, ar as INTEGRATIONS, at as followEvents, bn as loadAgents, bt as liveStatus, cn as wakeRun, cr as AGENT_TOOL_LAYOUTS, ct as planNodeSession, d as runningRunIds, dn as planAgentMigration, dr as firstLine, et as detachResume, f as runningScript, fn as applySteps, fr as EXPERIMENTS, gn as toolsInUse, gt as startRun, h as isNewer, hn as toolLinks, ht as walkSummary, i as NOT_STARTED, it as detachRun, jn as inside, kn as shownPath, kt as nestItems, l as planUpgrade, ln as maskReplacer, lr as ORK_LAYOUT, lt as namingNoDriver, mn as linkTools, mr as knownExperiments, n as resolveBase, nt as listKey, on as socketPath, pn as planMoveInto, pr as experimentsOn, q as tellLiveRun, qn as serializeWorkflow, qt as planPost, sn as socketRoom, t as createWorkspace, tr as isClaudeNode, tt as resumable, u as releaseNotes, ur as CLAUDE_DRIVERS, ut as sessionDriver, v as triggersReport, vn as deleteAgent, vt as userMessageText, wn as newProjectId, wt as templateOf, yn as catalogAgent, zt as loadProject } from "./src-DosUvy4B.mjs";
3
+ import { a as formatRunEvent, i as formatEvent, n as formatRuns, o as createTerminalHuman, r as formatTree, s as createDecisionQueue, t as ago } from "./format-sGEQmQid.mjs";
4
+ import { C as placeOf, T as openFixRun, _ as nodeSession, b as recordNote, c as checkDue, d as pendingNotice, f as readUpdateCache, g as activeTree, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, n as ORK_VERSION, o as updateSwitchOff, p as refreshUpdateCache, r as isSwitchedOn, s as runInstall, t as ORK_NAME, u as newerVersion$1, v as previewPost, w as feedbackActions, x as acceptItem, y as checkNote } from "./__build_info__-B83zdwIv.mjs";
5
+ import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-BaEOo7sO.mjs";
6
+ import { n as newRunId, t as findRun } from "./find-DDAkEoda.mjs";
7
+ import { a as write, c as openStore, i as style, l as storePath, n as cost, o as writeErr, s as isNodeEvent, t as confirm } from "./output-D1wMNIly.mjs";
8
+ import { n as listOrkWorktrees, t as createdWorktrees } from "./list-CZTh_wdE.mjs";
9
+ import { a as planClean, t as applyClean } from "./clean-NtapkN2x.mjs";
10
10
  import { c as subcommandsOf, i as argsOf, n as groupedEntries, o as publicArgs, s as resolved, t as groupOf } from "./help-groups-BwYoAWsh.mjs";
11
- import { _ as namedMcpEntries, a as builtinExecutors, d as maskedEnv, f as maskedNamesOfRun, g as clearOutputFiles, h as sessionEnv, i as onPath, l as clearStateFiles, m as runScrubber, n as missingProgramMessage, r as missingPrograms, t as envProblems, v as resultOf } from "./src-8nec4uoe.mjs";
12
- import { c as registeredProjects, i as refusedBuiltin, n as WORKFLOW_SUFFIX, o as workflowArg, s as projectAt, t as WORKFLOW_DIR } from "./workflows-lpexsAYt.mjs";
13
- import { a as validate, c as warningLines, i as loadWorkflow, o as validateFile, r as formatReport, s as walkLoaded, t as context } from "./context-CHV6aTs7.mjs";
14
- import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-TxTnWxix.mjs";
15
- import { i as decideRefusal, n as chatRefusal } from "./in-a-run-emTc-kZI.mjs";
11
+ import { _ as namedMcpEntries, a as builtinExecutors, d as maskedEnv, f as maskedNamesOfRun, g as clearOutputFiles, h as sessionEnv, i as onPath, l as clearStateFiles, m as runScrubber, n as missingProgramMessage, r as missingPrograms, t as envProblems, v as resultOf } from "./src-COHxAj7X.mjs";
12
+ import { c as registeredProjects, i as refusedBuiltin, n as WORKFLOW_SUFFIX, o as workflowArg, s as projectAt, t as WORKFLOW_DIR } from "./workflows-BF9fO-ER.mjs";
13
+ import { a as validate, c as warningLines, i as loadWorkflow, o as validateFile, r as formatReport, s as walkLoaded, t as context } from "./context-C5wQNjp9.mjs";
14
+ import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-DezRXN0Z.mjs";
15
+ import { i as decideRefusal, n as chatRefusal } from "./in-a-run-DENitTUe.mjs";
16
16
  import path from "node:path";
17
17
  import { stringify } from "yaml";
18
18
  import { z } from "zod";
@@ -421,6 +421,88 @@ const createRelay = (options) => {
421
421
  }
422
422
  };
423
423
  };
424
+ // ../core/src/run/replay.ts
425
+ const NO_FAN_OUTS = {
426
+ lists: /* @__PURE__ */ new Map(),
427
+ items: /* @__PURE__ */ new Map()
428
+ };
429
+ /** Whether an activation's input carries a result this run made anew. */
430
+ const carries = (fresh, input) => {
431
+ if (input === void 0) return false;
432
+ if (fresh.has(input)) return true;
433
+ const results = input["results"];
434
+ if (results === null || typeof results !== "object") return false;
435
+ return Object.values(results).some((value) => value !== null && typeof value === "object" && fresh.has(value));
436
+ };
437
+ /**
438
+ * Hands each node's recorded results back in order, until the node runs anew
439
+ * once (it failed or never finished last time, or its input is new); from then
440
+ * on it always runs. A node cut off mid-session continues that session, once.
441
+ */
442
+ const createReplay = (history, cutOff = /* @__PURE__ */ new Map(), mail, earlier, fanOuts = NO_FAN_OUTS) => {
443
+ const sessions = new Map(cutOff);
444
+ const used = /* @__PURE__ */ new Map();
445
+ const anew = /* @__PURE__ */ new Set();
446
+ const fresh = /* @__PURE__ */ new WeakSet();
447
+ /** Fanned nodes whose list was resolved anew: their items can't be replayed. */
448
+ const renewed = /* @__PURE__ */ new Set();
449
+ const takeItem = (activation, of) => {
450
+ const recorded = fanOuts.items.get(activation.id)?.get(activation.round ?? 0);
451
+ if (recorded !== void 0 && !renewed.has(of) && !carries(fresh, activation.input)) return recorded;
452
+ anew.add(activation.id);
453
+ return null;
454
+ };
455
+ return {
456
+ take: (activation) => {
457
+ const { id } = activation;
458
+ if (activation.brief !== void 0 || anew.has(id)) return null;
459
+ if (activation.item !== void 0) return takeItem(activation, activation.item.of);
460
+ const index = used.get(id) ?? 0;
461
+ const recorded = history.get(id)?.[index];
462
+ if (recorded === void 0 || carries(fresh, activation.input)) {
463
+ anew.add(id);
464
+ return null;
465
+ }
466
+ used.set(id, index + 1);
467
+ return recorded;
468
+ },
469
+ ran: (result) => {
470
+ fresh.add(result.output);
471
+ },
472
+ cutOff: (activation) => {
473
+ const session = activation.brief === void 0 && (activation.item === void 0 || !renewed.has(activation.item.of)) ? sessions.get(activation.id) : void 0;
474
+ sessions.delete(activation.id);
475
+ return session;
476
+ },
477
+ listed: (id, round, source) => {
478
+ const list = fanOuts.lists.get(listKey(id, round));
479
+ const ranAnew = typeof source === "object" && source !== null && fresh.has(source);
480
+ if (list !== void 0 && !ranAnew) return list;
481
+ renewed.add(id);
482
+ return null;
483
+ },
484
+ previousSession: ({ id, item, round = 0 }) => item === void 0 || renewed.has(item.of) ? void 0 : [...fanOuts.items.get(id) ?? []].findLast(([before]) => before < round)?.[1].sessionId,
485
+ kept: () => [...history.keys(), ...fanOuts.items.keys()],
486
+ ...mail === void 0 ? {} : { mail },
487
+ ...earlier === void 0 ? {} : { earlier }
488
+ };
489
+ };
490
+ /**
491
+ * The run's `HumanChannel`, answering questions about loops out of rounds as
492
+ * they were answered last time, in order; anything else goes to `human`.
493
+ */
494
+ const replayLoopAnswers = (human, answers) => {
495
+ const left = [...answers];
496
+ return {
497
+ permit: (request) => human.permit(request),
498
+ ...human.settled === void 0 ? {} : { settled: human.settled },
499
+ ask: (question) => {
500
+ const index = left.findIndex((a) => a.question === question.question);
501
+ const [given] = index === -1 ? [] : left.splice(index, 1);
502
+ return given === void 0 ? human.ask(question) : Promise.resolve(given.answer);
503
+ }
504
+ };
505
+ };
424
506
  // ../core/src/store/recorder.ts
425
507
  /**
426
508
  * @file Records a run as it happens: each event appended to the store and
@@ -831,12 +913,11 @@ const runReadyDriver = async (workflow, options) => {
831
913
  * where it ran, as the run's sessions were set up (ADR-065), and the exchange
832
914
  * is added to the run's record.
833
915
  */
834
- /** The declared node a spawned worker (`<template>-<n>`) is an instance of. */
835
- const templateOf = (nodes, id) => {
836
- if (id in nodes) return void 0;
837
- const template = id.replace(/-\d+$/u, "");
838
- return template in nodes ? template : void 0;
839
- };
916
+ /**
917
+ * The declared node a spawned worker or an item of a fanned node is an instance
918
+ * of.
919
+ */
920
+ const instanceOf = (nodes, id) => id in nodes ? void 0 : templateOf(id, new Set(Object.keys(nodes)));
840
921
  /** The run's sessions loaded the machine's Claude setup, so this one does. */
841
922
  const machineClaudeOf = (events) => events.some(({ event }) => event.type === "run.started" && event.settings?.machineClaude === true) ? { machineClaude: true } : {};
842
923
  /**
@@ -857,7 +938,7 @@ const resumeChat = async (options) => {
857
938
  const notice = chatNotice(store, place, options.home);
858
939
  if (notice !== null) options.out(notice);
859
940
  const runDir = runDirOf(place, run.id);
860
- const template = templateOf(workflow.nodes, node);
941
+ const template = instanceOf(workflow.nodes, node);
861
942
  const plan = planNodeSession(workflow, node, {
862
943
  cwd,
863
944
  inputs: {},
@@ -924,7 +1005,7 @@ const createLogPrinter = (options) => {
924
1005
  return (stored) => {
925
1006
  const { event } = stored;
926
1007
  if (event.type === "run.started") width = Math.max(...event.nodes.map((n) => n.id.length), 1);
927
- if (options.node !== void 0 && nodeOf(event) !== options.node) return [];
1008
+ if (options.node !== void 0 && !isNodeEvent(event, options.node)) return [];
928
1009
  if (options.json) return [JSON.stringify(stored)];
929
1010
  return formatRunEvent(event, width, tools);
930
1011
  };
@@ -947,6 +1028,31 @@ const followRun = async (store, run, out, options) => {
947
1028
  ...options.signal === void 0 ? {} : { signal: options.signal }
948
1029
  }) === "crashed") out(style("red", `run ${run.id} stopped without finishing (its process is gone)`));
949
1030
  };
1031
+ // src/run/node-notes.ts
1032
+ /**
1033
+ * @file What a run of one node (`ork run --node`) leaves out, said before its
1034
+ * session starts: the messages, steps and `forEach` it does not run. A node
1035
+ * with `forEach` (RFC-013) runs once with no item when it has `items`, and is
1036
+ * refused when it has `from`, as the node that reports its list is not run.
1037
+ */
1038
+ /** A warning line, as the run prints it. */
1039
+ const noteLines = (lines) => lines.map((line) => `${style(["yellow", "bold"], "warning")} ${line}`);
1040
+ /** Why a node can't run alone, or what a run of it leaves out. */
1041
+ const oneNodeNotes = (workflow, id) => {
1042
+ const node = workflow.nodes[id];
1043
+ if (node === void 0) return { warnings: [] };
1044
+ const each = "forEach" in node ? node.forEach : void 0;
1045
+ if (each !== void 0 && "from" in each) return {
1046
+ error: `\`${id}\` runs once for each item of ${each.from}, a list another node reports, and a run of one node does not run that node; run the whole workflow, or give it \`items\` of its own`,
1047
+ warnings: []
1048
+ };
1049
+ const steps = [...beforeOf(node, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in node && node.verify !== void 0 ? ["verify"] : []];
1050
+ return { warnings: [
1051
+ ..."notify" in node && node.notify ? [`${id}'s notify: does nothing here: a run of one node has no message bus.`] : [],
1052
+ ...each === void 0 ? [] : [`${id}'s forEach: does not fan out in a run of one node: it runs once, with no item.`],
1053
+ ...steps.map((step) => `${id}'s ${step}: does not run in a run of one node.`)
1054
+ ] };
1055
+ };
950
1056
  // src/run/origin.ts
951
1057
  /**
952
1058
  * @file Where a run's feedback comes from, beyond what its events say: Ork's
@@ -987,6 +1093,8 @@ const runNode = async (options) => {
987
1093
  if (!pairs.ok) return failed([`--input: ${pairs.error}`]);
988
1094
  const inputs = resolveInputs(workflow, pairs.raw);
989
1095
  if (!inputs.ok) return failed(inputs.errors);
1096
+ const notes = oneNodeNotes(workflow, options.nodeId);
1097
+ if (notes.error !== void 0) return failed([notes.error]);
990
1098
  const limits = runLimits(options.store, place, options.home);
991
1099
  const refused = limitsRefusal(limits, workflow);
992
1100
  if (refused.length > 0) return failed(refused);
@@ -1017,11 +1125,7 @@ const runNode = async (options) => {
1017
1125
  if (ready?.line !== void 0) options.err(ready.line);
1018
1126
  const driver = named ?? ready?.driver;
1019
1127
  if (driver === void 0) return failed(["No Claude driver is ready here."]);
1020
- const planned = workflow.nodes[options.nodeId];
1021
- if (planned !== void 0 && "notify" in planned && planned.notify) options.err(`${style(["yellow", "bold"], "warning")} ${options.nodeId}'s notify: does nothing here: a run of one node has no message bus.`);
1022
- const skipped = planned === void 0 ? [] : [...beforeOf(planned, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in planned && planned.verify !== void 0 ? ["verify"] : []];
1023
- for (const step of skipped) options.err(`${style(["yellow", "bold"], "warning")} ${options.nodeId}'s ${step}: does not run in a run of one node.`);
1024
- for (const warning of plan.warnings) options.err(`${style(["yellow", "bold"], "warning")} ${warning}`);
1128
+ for (const line of noteLines([...notes.warnings, ...plan.warnings])) options.err(line);
1025
1129
  const id = options.nodeId;
1026
1130
  const recorder = createRecorder(options.store, {
1027
1131
  id: runId,
@@ -1459,7 +1563,7 @@ const runWorkflowFile = async (options) => {
1459
1563
  if (options.tools !== void 0) executorOptions.tools = options.tools;
1460
1564
  const script = process.argv[1];
1461
1565
  if (script !== void 0 && onPath(script)) executorOptions.orkBin = script;
1462
- const replay = options.resume === void 0 ? void 0 : createReplay(options.resume.history, options.resume.cutOff, options.resume.mail, options.resume.earlier);
1566
+ const replay = options.resume === void 0 ? void 0 : createReplay(options.resume.history, options.resume.cutOff, options.resume.mail, options.resume.earlier, options.resume.fanOuts);
1463
1567
  if (replay !== void 0) onEvent({
1464
1568
  type: "run.resumed",
1465
1569
  kept: replay.kept()
@@ -1530,7 +1634,7 @@ const flagValues = (rawArgs, flag) => {
1530
1634
  };
1531
1635
  const isDriver = (value) => CLAUDE_DRIVERS.some((d) => d.id === value);
1532
1636
  /** Loads the drivers only when a run starts, so other commands stay fast. */
1533
- const loadRuntime = async (driver) => (await import("./src-5-sCA7cY.mjs")).createRuntime(driver, sessionEnv(process.env));
1637
+ const loadRuntime = async (driver) => (await import("./src-BNhCiDQw.mjs")).createRuntime(driver, sessionEnv(process.env));
1534
1638
  /**
1535
1639
  * Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
1536
1640
  * so that the run ends tidily and leaves no result unscrubbed.
@@ -2217,7 +2321,7 @@ const ORK_SKILLS = {
2217
2321
  principles: "---\nname: principles\ndescription: Use when deciding how to split work into an Ork workflow - which steps are programs, which are Claude sessions, where a person decides - so it costs less than doing it by hand and can't go wrong quietly.\n---\n\n# How Ork splits work\n\nOrk exists to do long jobs for less than doing them by hand, and safely. Four\nrules decide the shape of a workflow.\n\n## 1. A program for what a program can do\n\nA model turn is for judgment. Anything a program can decide, it should: run the\ntests, read a pull request's state, count what changed, wait for CI.\n\n- **A tool node** (`tool.shell`, `tool.git`, `tool.gh`, `tool.aws`) or an\n **`mcp.call`** does it with no session. With `output: { format: json }`, its\n answer routes the run: `when: \"json.state == 'OPEN'\"`.\n- **`verify:`** on a session runs a check before its report counts, so a session\n can't claim success past failing tests. **`before:`** hands it what a program\n found, so it doesn't spend turns finding it.\n- **`watch:`** waits on CI or a review with a program that looks every few\n seconds; hours of waiting cost nothing.\n- **`notify:`** lets a program tell a running session what it found, with no\n model turn to carry the news.\n\n## 2. Spend where it pays\n\n- A small model and low effort for a narrow task (`model: haiku`,\n `effort: low`); the large ones where thinking pays.\n- `budgetUsd` on each session and on the run (`limits.budgetUsd`). A loop of\n sessions without a budget or `limits.timeout` gets a warning, for good reason.\n- Give a session what it needs in its `prompt` and `before:`, rather than\n letting it search; and `isolation: readonly` for a reviewer that only reads.\n\n## 3. Make a mistake impossible before checking for it\n\n- Declare what a node says: `output.schema` lists on tool JSON and Claude\n reports (`verdict: [approve, escalate]`). Conditions then compare against a\n closed list, and a run fails a value off it rather than matching nothing.\n- A verdict is a declared field, never words to search a summary for.\n- Give each condition its opposite, and each failure a handler (`when: \"!ok\"`).\n- Check every draft with Ork's `check` tool until it's clean.\n\n## 4. A person decides what can't be undone\n\nMerging, deploying, publishing, spending a lot: put a `human.gate` before it,\nwith the safe option `recommended`. For a run nobody watches, give it a\n`timeout` and the safe `default`. You never answer a gate yourself.\n\n## An example\n\nA review that a program checks first, and a person signs off:\n\n```yaml\nschemaVersion: 1\nname: careful-review\ndescription: Lint with a program, review with a small model, and ask before merging.\ninputs:\n pr:\n type: string\n required: true\nlimits:\n budgetUsd: 3\nnodes:\n lint:\n type: tool.shell\n command: pnpm lint\n allow: [pnpm lint]\n review:\n type: claude.worker\n prompt: \"Review pull request {{inputs.pr}}. The linter passed.\"\n model: haiku\n effort: low\n isolation: readonly\n budgetUsd: 1\n output:\n schema:\n verdict: [approve, escalate]\n ask:\n type: human.gate\n question: \"Merge pull request {{inputs.pr}}? The review says {{nodes.review.output.verdict}}.\"\n options: [merge, wait]\n recommended: wait\n merge:\n type: tool.gh\n command: pr merge {{inputs.pr}} --squash\n allow: [pr merge]\nedges:\n - lint -> review\n - review -> ask\n - from: ask\n to: merge\n when: \"choice == 'merge'\"\n```\n\n`lint` costs nothing, `review` is a small model with a cap, and nothing merges\nwithout a person.\n",
2218
2322
  run: "---\nname: run\ndescription: Use when starting, following or explaining an Ork run - what it's doing, why it failed, what it cost, and what waits on the person - and when the person asks about their Ork runs.\n---\n\n# Running and following Ork workflows\n\nOrk runs workflows on the person's machine with their accounts, so a run spends\ntheir money and can push, merge or deploy. Start one only when they ask, and\nnever answer a decision for them.\n\n## Starting\n\n1. Find it: Ork's `workflows` tool gives each workflow's name and inputs.\n2. Check it first with `check` (`workflow: <name>`). Don't start one with an\n error.\n3. Start it with `start_run`, giving every required input. Claude Code asks the\n person first, unless they've allowed it. It answers with the run's id; the run goes on in the\n background, whatever happens to this conversation.\n\nWithout Ork's tools: `ork run <workflow> --detach --input name=value`.\n\n## Following\n\n- `run` gives a run's status, what it waits on, its cost so far, and each\n node's state. `runs` lists the newest runs.\n- `run_events` gives its newest events; `node` narrows them, and `last` or\n `after` widens them. A `node.finished` event with `ok: false` says why a node\n failed.\n- `explain_context` says what a node's session was given: its agent,\n instructions, skills and files. Use it when a node didn't know something.\n- `usage` says what runs spent, by workflow and model, and how close the limits\n are.\n\nSay what you found plainly: which node, what it said, and what it cost. Quote a\nnode's own words rather than retelling them.\n\n## What only the person does\n\n- **Decisions.** `decisions` lists the questions waiting on them: a gate, a loop\n that ran out of rounds, a permission. Tell them what each asks, the options,\n and the recommended one, and that they answer in `ork ui` or with\n `ork decide` in their own terminal. Don't answer one: never run\n `ork decide --id … --choose …`; with Ork's plugin, a hook stops it.\n- **Stopping, pausing, resuming.** Tell them the command: `ork stop <run>`,\n `ork pause <run>`, `ork continue <run>`, `ork resume <run>`. A node can be\n stopped alone with `--node <id>`.\n\n## Without Ork's tools\n\n| Want | Command |\n| ---------------------- | ------------------------------ |\n| Runs, newest first | `ork ps` |\n| A run's transcript | `ork logs <run>` (`--follow`) |\n| Its nodes and cost | `ork tree <run>` |\n| What runs spent | `ork usage` |\n| What waits on a person | `ork decide --list` |\n| A node's context | `ork context explain <wf> <n>` |\n",
2219
2323
  setup: "---\nname: setup\ndescription: Use when the person asks you to set up Ork, install it, or get started with it in a repository - checking it's installed, setting up the project, making Ork's tools available to you, and a first workflow.\n---\n\n# Setting up Ork\n\nOrk runs workflows on the person's machine with their own accounts. Setting it\nup changes their machine and their repository, so say what each step does and\nask before it. Work through these in order, and stop where they say no.\n\n## 1. Check\n\n```sh\nork --version\nork doctor\n```\n\n- **No `ork`:** it needs Node 24.20 or later (`node --version`). The install is\n `npm install --global @krapht/ork`; show it and run it only when they say\n yes. The guide:\n https://github.com/mimshins/ork/blob/main/docs/getting-started/installation.md\n- **Read `ork doctor` to them.** It needs git, a git repository with a commit,\n and one Claude driver. Don't run `git init` or a commit without asking.\n- **A driver not ready:** they log in themselves, with `claude` (the CLI\n driver), or by setting `ANTHROPIC_API_KEY` or a cloud provider in their own\n shell (the SDK driver). Never ask for a key, never take one pasted to you, and\n never write one into a file or a command. `gh` and `aws` are optional, and\n their logins are their own too (`gh auth login`, `aws sso login`).\n\n## 2. The project\n\n- **`ork init`** keeps Ork's files in the repository, in `.ork/`, committed with\n the code. It adds `.ork/runs/` to `.gitignore`, links `.claude/` and similar\n folders to `.ork/`, and writes a starter workflow, `hello`.\n- **`ork register`** keeps them beside it, in `~/.ork/projects/<id>/`, and\n writes nothing into the repository. For a repository whose process isn't\n theirs to change, or to try Ork first.\n\nExplain both and ask which. Skip the question when the repository decides it: a\n`.ork/` folder means `ork init` has run (`ork projects` lists what's\nregistered). Neither overwrites a file.\n\n## 3. Make Ork available to you\n\nIf Ork's tools (`workflows`, `check`) already answer you, skip this. Otherwise,\nby where you run, and asking before each change to a config:\n\n- **Claude Code:** Ork's plugin brings the skills and the tools. The person\n types these themselves:\n\n ```\n /plugin marketplace add mimshins/ork\n /plugin install ork@ork\n ```\n\n Or, in a project set up with `ork init`, the skills in the repository for\n everyone who clones it, then the tools:\n\n ```sh\n ork init --skills\n claude mcp add ork -- ork mcp\n ```\n\n New tools start when they restart Claude Code or reconnect `ork` in `/mcp`;\n so does the plugin's, once `ork` is installed.\n\n- **Another MCP client:** add a server named `ork` with command `ork` and args\n `[\"mcp\"]` to its MCP config, where its own docs say. Its tools find the\n project by its id, from `ork projects`.\n- **A shell only:** use the CLI. `ork --help` and each command's `--help` say\n what it takes.\n\n## 4. A first workflow\n\nOffer to write one for a task they name. Follow `/ork:write`, check each draft\n(the `check` tool, or `ork validate <file>`) until it has no error, then save\nit. Don't start it: a run spends their money. Start it only when they say so,\nin the background with its inputs (`start_run`, or\n`ork run <name> --detach --input name=value`), and follow it with `/ork:run`.\n\n## 5. Show them `ork ui`\n\nTell them to run `ork ui` in their own terminal: their runs, graphs and logs in\nthe browser, and where they answer what a run asks.\n\n## Never\n\n- Ask for, store or pass on a password, key or token.\n- Answer a run's decision: they answer in `ork ui` or with `ork decide` in\n their own terminal.\n- Install, edit a config, or start a run without a yes.\n",
2220
- write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## Loops\n\nEvery cycle needs an edge with `maxRounds`. Give the way back a condition and\nthe way out its opposite, as `!ok` and `ok` above; a plain edge beside a loop\nfires every round. When a loop runs out, Ork asks a person what to do.\n\n## What `check` refuses, and why\n\n- **A value the source can't have** (`when.value`): `status == 'failed'` (it's\n `fail`), a gate's `choice == 'aprove'` for `approve`, a value outside a\n declared list. The message names the values it can have.\n- **A loop no condition ends**, a plain edge beside a loop, a loop of sessions\n with no `limits.budgetUsd` or `limits.timeout`: warnings, each worth fixing.\n- **`no way the walk tried reaches x`**: Ork tried every way through, and none\n got to `x`. A join waiting on a branch a first-match router never takes is\n the usual cause.\n"
2324
+ write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\nA worker, tool node, `mcp.call` or `workflow.call` can run once per entry of a\nlist: see [One node per item](#one-node-per-item).\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## One node per item\n\n`forEach` runs a worker, a tool node, an `mcp.call` or a `workflow.call` once\nper entry of a list, with no model turn to fan out or gather. Not a leader, a\ngate, a subagent or a control node.\n\n```yaml\nplan:\n type: claude.worker\n prompt: \"Split this into at most 6 independent tasks: {{inputs.goal}}\"\n isolation: readonly\n output:\n schema:\n tasks:\n list: { title: string, detail: string } # declare the list and its shape\nbuild:\n type: claude.worker\n prompt: \"Task {{index}}: {{item.title}}. {{item.detail}}\"\n isolation: worktree\n forEach:\n from: \"{{nodes.plan.output.tasks}}\" # exactly one path to a declared list\n maxItems: 6 # required with `from`: the cap on sessions started\n maxParallel: 3\n```\n\n- `forEach: { items: [a, b] }` takes a list written in the file instead.\n- `{{item}}` and `{{index}}` work only in a node with `forEach`, in `prompt`,\n `command`, `arguments`, `inputs`, `env` and `ref`. A program also gets\n `ORK_ITEM` and `ORK_INDEX`.\n- The next node reads `items`, `results` (each item's output, `summary`,\n `branch`), `statuses` and `count`: `{{nodes.build.output.results}}`,\n `when: \"all(statuses, 'pass')\"`. Items follow no edges; the node does.\n- `onFailure: finish` (default) runs every item; `stop` starts no more after a\n failure. A list over `maxItems` makes Ork ask a person.\n- Declare the source's `list`, or `check` warns `forEach.undeclared`. Fix\n `forEach.not-list`, `forEach.order` and `item.field` errors; they name the\n field or node.\n\n## Loops\n\nEvery cycle needs an edge with `maxRounds`. Give the way back a condition and\nthe way out its opposite, as `!ok` and `ok` above; a plain edge beside a loop\nfires every round. When a loop runs out, Ork asks a person what to do.\n\n## What `check` refuses, and why\n\n- **A value the source can't have** (`when.value`): `status == 'failed'` (it's\n `fail`), a gate's `choice == 'aprove'` for `approve`, a value outside a\n declared list. The message names the values it can have.\n- **A loop no condition ends**, a plain edge beside a loop, a loop of sessions\n with no `limits.budgetUsd` or `limits.timeout`: warnings, each worth fixing.\n- **`no way the walk tried reaches x`**: Ork tried every way through, and none\n got to `x`. A join waiting on a branch a first-match router never takes is\n the usual cause.\n"
2221
2325
  };
2222
2326
  // src/project/init-skills.ts
2223
2327
  /**
@@ -4117,8 +4221,9 @@ const tree = defineCommand({
4117
4221
  return;
4118
4222
  }
4119
4223
  const { run } = found;
4120
- const events = store.events(run.id).map((s) => s.event);
4121
- const roots = runTree(events);
4224
+ const stored = store.events(run.id);
4225
+ const events = stored.map((s) => s.event);
4226
+ const roots = nestItems(activeTree(stored));
4122
4227
  if (args.json) {
4123
4228
  write(JSON.stringify(roots, void 0, 2));
4124
4229
  return;
@@ -4445,27 +4550,27 @@ const main = defineCommand({
4445
4550
  ps,
4446
4551
  logs,
4447
4552
  tree,
4448
- usage: () => import("./usage-Bh1lVioA.mjs").then((module) => module.usage),
4449
- prune: () => import("./usage-Bh1lVioA.mjs").then((module) => module.prune),
4553
+ usage: () => import("./usage-jYI2QHt7.mjs").then((module) => module.usage),
4554
+ prune: () => import("./usage-jYI2QHt7.mjs").then((module) => module.prune),
4450
4555
  decide,
4451
- relay: () => import("./relay-UgK1Cp9a.mjs").then((module) => module.relay),
4452
- pause: () => import("./pause-CBSAxtTp.mjs").then((module) => module.pause),
4453
- continue: () => import("./pause-CBSAxtTp.mjs").then((module) => module.continueRun),
4556
+ relay: () => import("./relay-DCGGYSdz.mjs").then((module) => module.relay),
4557
+ pause: () => import("./pause-HuQhHyET.mjs").then((module) => module.pause),
4558
+ continue: () => import("./pause-HuQhHyET.mjs").then((module) => module.continueRun),
4454
4559
  stop,
4455
4560
  chat,
4456
- send: () => import("./send-B39-Xeo7.mjs").then((module) => module.send),
4561
+ send: () => import("./send-C8pQfhED.mjs").then((module) => module.send),
4457
4562
  resume,
4458
4563
  feedback,
4459
4564
  context,
4460
- worktrees: () => import("./worktrees-D4bVVKfY.mjs").then((module) => module.worktrees),
4461
- wt: () => import("./worktrees-D4bVVKfY.mjs").then((module) => module.worktrees),
4462
- watch: () => import("./watch-C_vcsn_g.mjs").then((module) => module.watch),
4463
- mcp: () => import("./mcp-qUwFT9yi.mjs").then((module) => module.mcp),
4464
- ui: () => import("./ui-DmJWLgxL.mjs").then((module) => module.ui),
4565
+ worktrees: () => import("./worktrees-0iUiA6eD.mjs").then((module) => module.worktrees),
4566
+ wt: () => import("./worktrees-0iUiA6eD.mjs").then((module) => module.worktrees),
4567
+ watch: () => import("./watch-D4lFXnHT.mjs").then((module) => module.watch),
4568
+ mcp: () => import("./mcp-B2DW26HO.mjs").then((module) => module.mcp),
4569
+ ui: () => import("./ui-Cdb05xJw.mjs").then((module) => module.ui),
4465
4570
  doctor,
4466
4571
  upgrade,
4467
- completion: () => import("./completion-BeS2NCgO.mjs").then((module) => module.completion),
4468
- __complete: () => import("./completion-BeS2NCgO.mjs").then((module) => module.completeCommand(main))
4572
+ completion: () => import("./completion-DAQMrf1u.mjs").then((module) => module.completion),
4573
+ __complete: () => import("./completion-DAQMrf1u.mjs").then((module) => module.completeCommand(main))
4469
4574
  }
4470
4575
  });
4471
4576
  // src/bin.ts
@@ -4483,7 +4588,7 @@ const main = defineCommand({
4483
4588
  clearGitLocalEnv();
4484
4589
  const argv = process.argv.slice(2);
4485
4590
  if (argv[0] === "__complete") {
4486
- const { printCompletions } = await import("./completion-BeS2NCgO.mjs");
4591
+ const { printCompletions } = await import("./completion-DAQMrf1u.mjs");
4487
4592
  await printCompletions(main, argv.slice(1));
4488
4593
  } else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
4489
4594
  else {
@@ -1,4 +1,4 @@
1
- import { A as gitOk, O as removeWorktree } from "./src-DG4dyBqm.mjs";
1
+ import { A as gitOk, O as removeWorktree } from "./src-DosUvy4B.mjs";
2
2
  // ../core/src/worktree/clean.ts
3
3
  /**
4
4
  * @file What `ork worktrees clean` and `ork worktrees remove` do with what
@@ -1,9 +1,9 @@
1
- import { Cn as placeFiles, cr as CLAUDE_DRIVERS, ht as liveStatus, tr as NodeIdSchema, wn as placeFor } from "./src-DG4dyBqm.mjs";
2
- import { a as builtinWorkflowNames, n as pendingDecisions, o as builtinWorkflowText, s as isBuiltinArg } from "./decide-Cnt7o3ya.mjs";
3
- import { t as findRun } from "./find-Dy2H5IvT.mjs";
4
- import { c as openStore, l as storePath, o as writeErr } from "./output-CjOPL8W0.mjs";
1
+ import { En as placeFor, Tn as placeFiles, bt as liveStatus, rr as NodeIdSchema, ur as CLAUDE_DRIVERS } from "./src-DosUvy4B.mjs";
2
+ import { a as builtinWorkflowNames, n as pendingDecisions, o as builtinWorkflowText, s as isBuiltinArg } from "./decide-BaEOo7sO.mjs";
3
+ import { t as findRun } from "./find-DDAkEoda.mjs";
4
+ import { c as openStore, l as storePath, o as writeErr } from "./output-D1wMNIly.mjs";
5
5
  import { a as findEntry, c as subcommandsOf, d as SHELL_SCRIPTS, l as COMPLETE_COMMAND, n as groupedEntries, r as argInfoOf, t as groupOf, u as SHELLS } from "./help-groups-BwYoAWsh.mjs";
6
- import { a as resolveWorkflowArg, n as WORKFLOW_SUFFIX } from "./workflows-lpexsAYt.mjs";
6
+ import { a as resolveWorkflowArg, n as WORKFLOW_SUFFIX } from "./workflows-BF9fO-ER.mjs";
7
7
  import path from "node:path";
8
8
  import { parse } from "yaml";
9
9
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
@@ -1,6 +1,6 @@
1
- import { C as workflowTextHash, Cn as placeFiles, Ct as walkChecks, On as workflowDirOf, S as readProjectTriggers, St as checkWorkflowText, jn as LESSON_CAP_TOKENS, lt as explainSession, wn as placeFor, wt as walkSummary } from "./src-DG4dyBqm.mjs";
2
- import { a as write, i as style, r as plain } from "./output-CjOPL8W0.mjs";
3
- import { c as registeredProjects, i as refusedBuiltin, o as workflowArg, r as findWorkflowFiles, s as projectAt } from "./workflows-lpexsAYt.mjs";
1
+ import { An as workflowDirOf, C as workflowTextHash, En as placeFor, Nn as LESSON_CAP_TOKENS, S as readProjectTriggers, Tn as placeFiles, ht as walkSummary, mt as walkChecks, pt as checkWorkflowText, st as explainSession } from "./src-DosUvy4B.mjs";
2
+ import { a as write, i as style, r as plain } from "./output-D1wMNIly.mjs";
3
+ import { c as registeredProjects, i as refusedBuiltin, o as workflowArg, r as findWorkflowFiles, s as projectAt } from "./workflows-BF9fO-ER.mjs";
4
4
  import path from "node:path";
5
5
  import { readFileSync } from "node:fs";
6
6
  import { defineCommand } from "citty";
@@ -1,4 +1,4 @@
1
- import { Cn as placeFiles, Fn as writePlainFile, Nn as guardedBase, Pn as plainPathInside, ht as liveStatus, in as socketPath, on as wakeRun } from "./src-DG4dyBqm.mjs";
1
+ import { Fn as guardedBase, In as plainPathInside, Ln as writePlainFile, Tn as placeFiles, bt as liveStatus, cn as wakeRun, on as socketPath } from "./src-DosUvy4B.mjs";
2
2
  import path from "node:path";
3
3
  import { existsSync, readFileSync } from "node:fs";
4
4
  import { createHash } from "node:crypto";
@@ -1,4 +1,4 @@
1
- import { wn as placeFor } from "./src-DG4dyBqm.mjs";
1
+ import { En as placeFor } from "./src-DosUvy4B.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  // ../core/src/remote/find.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
- import { cn as maskSecrets, ht as liveStatus } from "./src-DG4dyBqm.mjs";
2
- import { i as style, n as cost } from "./output-CjOPL8W0.mjs";
1
+ import { Ot as isItem, bt as liveStatus, un as maskSecrets } from "./src-DosUvy4B.mjs";
2
+ import { i as style, n as cost } from "./output-D1wMNIly.mjs";
3
3
  import { createInterface } from "node:readline/promises";
4
4
  // ../core/src/control/queue.ts
5
5
  const PERMISSION_OPTIONS = [{
@@ -436,6 +436,7 @@ const nodeVerdict = (output) => {
436
436
  if (typeof count === "number") return `${String(count)} ran`;
437
437
  return "done";
438
438
  };
439
+ const items = (count) => `${String(count)} ${count === 1 ? "item" : "items"}`;
439
440
  /** Transcript lines for one run event, prefixed with the node they are from. */
440
441
  const formatRunEvent = (event, width, tools) => {
441
442
  const tag = (id) => style("cyan", `${id.padEnd(width)} │`);
@@ -492,6 +493,14 @@ const formatRunEvent = (event, width, tools) => {
492
493
  return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
493
494
  }
494
495
  if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
496
+ if (event.type === "each.listed") {
497
+ const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
498
+ return [`${tag(event.node)} ${style("dim", `${items(event.count)}${round}`)}`];
499
+ }
500
+ if (event.type === "each.exceeded") {
501
+ const over = `${items(event.count)}, over maxItems ${String(event.maxItems)}`;
502
+ return [`${tag(event.node)} ${style("yellow", `! ${over}`)}`];
503
+ }
495
504
  if (event.type === "run.finished") return formatRunResult(event.result);
496
505
  if (event.type === "run.resumed") {
497
506
  const [one] = event.kept;
@@ -609,7 +618,7 @@ const stateText = (node) => {
609
618
  return node.state;
610
619
  };
611
620
  /** The ownership tree, one line per node, children under their owner. */
612
- const formatTree = (roots) => {
621
+ const formatTree = (roots, now = Date.now()) => {
613
622
  const lines = [];
614
623
  const walk = (node, lead, branch) => {
615
624
  lines.push({
@@ -627,7 +636,8 @@ const formatTree = (roots) => {
627
636
  return lines.map(({ label: text, node }) => {
628
637
  const usage = node.inputTokens + node.outputTokens === 0 ? "" : style("dim", ` ${count(node.inputTokens)} in / ${count(node.outputTokens)} out · ${cost(node.costUsd)}`);
629
638
  const state = node.held === true ? `${style("yellow", "‖")} paused` : `${STATE_MARK[node.state]} ${stateText(node)}`;
630
- return `${text.padEnd(label)} ${style("dim", node.type.padEnd(type))} ${state}${usage}`;
639
+ const spent = isItem(node) && node.startedAt !== void 0 ? style("dim", ` ${duration((node.finishedAt === void 0 ? now : Date.parse(node.finishedAt)) - Date.parse(node.startedAt))}`) : "";
640
+ return `${text.padEnd(label)} ${style("dim", node.type.padEnd(type))} ${state}${spent}${usage}`;
631
641
  });
632
642
  };
633
643
  export { formatRunEvent as a, formatEvent as i, formatRuns as n, createTerminalHuman as o, formatTree as r, createDecisionQueue as s, ago as t };
@@ -1,4 +1,4 @@
1
- import { p as runMarkerIn } from "./src-8nec4uoe.mjs";
1
+ import { p as runMarkerIn } from "./src-COHxAj7X.mjs";
2
2
  // src/commands/in-a-run.ts
3
3
  /**
4
4
  * @file The commands that act as the user refuse a program or an agent in a run
@@ -1,4 +1,4 @@
1
- import { a as limitsNow, i as NOT_STARTED, o as limitsReport, r as capsHere } from "./src-DG4dyBqm.mjs";
1
+ import { a as limitsNow, i as NOT_STARTED, o as limitsReport, r as capsHere } from "./src-DosUvy4B.mjs";
2
2
  // src/run/limits.ts
3
3
  /**
4
4
  * @file The limits across runs that apply here (RFC-007): the user's settings
@@ -1,4 +1,4 @@
1
- import { A as gitOk, j as lines, k as git } from "./src-DG4dyBqm.mjs";
1
+ import { A as gitOk, j as lines, k as git } from "./src-DosUvy4B.mjs";
2
2
  import { existsSync } from "node:fs";
3
3
  // ../core/src/worktree/list.ts
4
4
  /**
@@ -1,8 +1,8 @@
1
- import { h as isNewer } from "./src-DG4dyBqm.mjs";
2
- import { n as ORK_VERSION } from "./__build_info__-CxH89h1r.mjs";
3
- import { a as openMcp, c as refused, o as tool, s as answer } from "./src-jyCJE1kd.mjs";
4
- import { p as runMarkerIn } from "./src-8nec4uoe.mjs";
5
- import { n as explainNode } from "./context-CHV6aTs7.mjs";
1
+ import { h as isNewer } from "./src-DosUvy4B.mjs";
2
+ import { n as ORK_VERSION } from "./__build_info__-B83zdwIv.mjs";
3
+ import { p as runMarkerIn } from "./src-COHxAj7X.mjs";
4
+ import { n as explainNode } from "./context-C5wQNjp9.mjs";
5
+ import { a as openMcp, c as refused, o as tool, s as answer } from "./src-DCCbIABO.mjs";
6
6
  import { z } from "zod";
7
7
  import { defineCommand } from "citty";
8
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -43,7 +43,7 @@ const orkMcpServer = (options = {}) => {
43
43
  ...options.script === void 0 ? {} : { script: options.script }
44
44
  });
45
45
  const all = [...opened.tools, explainTool(opened, options.home)];
46
- const tools = options.expects !== void 0 && isNewer(options.expects, "1.0.0-alpha.6") ? tooOld(all, options.expects) : all;
46
+ const tools = options.expects !== void 0 && isNewer(options.expects, "1.0.0-alpha.8") ? tooOld(all, options.expects) : all;
47
47
  const server = new McpServer({
48
48
  name: "ork",
49
49
  version: ORK_VERSION