@krapht/ork 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/__build_info__-CtSVVuZE.mjs +691 -0
  2. package/dist/bin.mjs +826 -526
  3. package/dist/clean-Dcr807be.mjs +88 -0
  4. package/dist/{context-BFGC_C02.mjs → context-C_bksb0C.mjs} +45 -129
  5. package/dist/{find-BcvhT6UN.mjs → find-DptmbhGd.mjs} +1 -1
  6. package/dist/{format-DqpObyMl.mjs → format-J-a21Yvn.mjs} +3 -163
  7. package/dist/{in-a-run-BbLhZOFW.mjs → in-a-run-D1Qc5Mit.mjs} +1 -1
  8. package/dist/{limits-BKYAmqCR.mjs → limits-BDZCmGEu.mjs} +1 -1
  9. package/dist/list-ZvY2OcLq.mjs +110 -0
  10. package/dist/{mcp-BWsdQyri.mjs → mcp-CHVKKg3B.mjs} +6 -6
  11. package/dist/{output-Q2EdeVUt.mjs → output-CXSJO00X.mjs} +68 -24
  12. package/dist/{pause-DKACDfV1.mjs → pause-BPSmg4yz.mjs} +4 -4
  13. package/dist/{relay-CbK-Wme9.mjs → relay-Do-aNbSD.mjs} +2 -2
  14. package/dist/remove-AwaMMaHf.mjs +194 -0
  15. package/dist/{send-C6yr04Fe.mjs → send-DhGBkowy.mjs} +3 -3
  16. package/dist/{src-CZOVF2yU.mjs → src-BHnxpJyH.mjs} +1 -1
  17. package/dist/{src-Cz1yOUTg.mjs → src-CGqMati3.mjs} +1411 -747
  18. package/dist/{src-D5ruZFgD.mjs → src-CpDKIulI.mjs} +1200 -55
  19. package/dist/{src-DtN1k-B3.mjs → src-UWkk1sl9.mjs} +1 -1
  20. package/dist/ui/assets/app-BgVDC_5l.js +67 -0
  21. package/dist/ui/assets/{app-C722xNZ3.css → app-DVf68fj5.css} +1 -1
  22. package/dist/ui/assets/{index-BoG6O-Lp.js → index-MQNNgsyT.js} +2 -2
  23. package/dist/ui/index.html +1 -1
  24. package/dist/ui-DhutUMgQ.mjs +472 -0
  25. package/dist/{usage-BIjf9Xdt.mjs → usage-CT7Z-yjr.mjs} +5 -4
  26. package/dist/{watch-B8o4Szd_.mjs → watch-0Qr2UfCQ.mjs} +6 -6
  27. package/dist/worktrees-DHl80k96.mjs +212 -0
  28. package/package.json +4 -4
  29. package/dist/__build_info__-CXXMLvjt.mjs +0 -104
  30. package/dist/ui/assets/app-BlDdP396.js +0 -61
  31. package/dist/ui-CZS1XKla.mjs +0 -160
package/dist/bin.mjs CHANGED
@@ -1,22 +1,25 @@
1
1
  #!/usr/bin/env node
2
- import { $ as replayLoopAnswers, $t as projectsHome, Bt as linkTools, Cn as beforeOf, Ct as FEEDBACK_SEVERITIES, Dt as targetOf, E as planNodeSession, Et as fingerprint, Ft as socketPath, Gt as catalogAgent, H as requestStop, Ht as toolsInUse, I as peerOf, It as wakeRun, Jt as hasInRepoFiles, Kt as loadAgents, L as tellNode, Lt as planAgentMigration, M as addressOf, Mn as ORK_LAYOUT, N as endLink, Nn as CLAUDE_DRIVERS, O as parseInputPairs, On as INTEGRATIONS, Ot as maskReplacer, P as endLinksOf, Pn as firstLine, Pt as serveRun, Q as createReplay, Qt as placeFor, Rt as applySteps, St as FEEDBACK_KINDS, Tt as createFeedback, U as tellLiveRun, Ut as createAgent, Vt as toolLinks, Wt as deleteAgent, X as detachResume, Xt as newProjectId, Y as adoptedCalls, Yt as inRepoPlace, Z as resumable, Zt as placeFiles, b as inspectProject, d as triggersReport, dt as startRun, en as runDirOf, hn as serializeWorkflow, i as NOT_STARTED, in as projectRootOf, j as createCalls, jn as AGENT_TOOL_LAYOUTS, k as resolveInputs, kt as maskSecrets, l as detectInstaller, n as resolveBase, nt as followEvents, ot as runStatus, pt as userMessageText, rn as inside, rt as liveStatus, st as runTree, t as createWorkspace, tn as shownPath, tt as detachRun, u as lateTriggers, ut as walkSummary, wn as isClaudeNode, xn as settingsOf, xt as gitRulesOf, yn as WorkflowSchema, z as flattenTree, zt as planMoveInto } from "./src-Cz1yOUTg.mjs";
3
- import { a as pendingDecisions, i as answerDecision, n as ORK_VERSION, r as nodeSession, t as ORK_NAME } from "./__build_info__-CXXMLvjt.mjs";
4
- import { n as newRunId, t as findRun } from "./find-BcvhT6UN.mjs";
5
- 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-Q2EdeVUt.mjs";
6
- import { a as formatRunEvent, c as createdWorktrees, i as formatEvent, l as listOrkWorktrees, n as formatRuns, o as createTerminalHuman, r as formatTree, s as applyClean, t as ago, u as planClean } from "./format-DqpObyMl.mjs";
7
- import { a as validate, c as warningLines, d as workflowArg, f as projectAt, i as loadWorkflow, l as WORKFLOW_DIR, o as validateFile, p as isNewer, r as formatReport, s as walkLoaded, t as context, u as WORKFLOW_SUFFIX } from "./context-BFGC_C02.mjs";
8
- import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-BKYAmqCR.mjs";
9
- import { _ as resultOf, 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 } from "./src-DtN1k-B3.mjs";
10
- import { i as decideRefusal, n as chatRefusal } from "./in-a-run-BbLhZOFW.mjs";
2
+ import { $ as resumable, $t as planMoveInto, A as parseInputPairs, At as FEEDBACK_KINDS, Bn as isClaudeNode, Bt as normalizeTitle, F as endLink, Fn as WorkflowSchema, G as tellLiveRun, Ht as maskReplacer, I as endLinksOf, It as sendPost, Jn as ORK_LAYOUT, Jt as serveRun, Ln as settingsOf, Mt as FEEDBACK_TARGETS, N as createCalls, P as addressOf, Pt as planPost, Q as detachResume, Qt as applySteps, R as peerOf, Rt as createFeedback, T as inspectProject, V as flattenTree, W as requestStop, Wn as INTEGRATIONS, Xn as firstLine, Xt as wakeRun, Yn as CLAUDE_DRIVERS, Yt as socketPath, Z as adoptedCalls, Zt as planAgentMigration, _n as inside, an as catalogAgent, cn as hasInRepoFiles, d as runningRunIds, dn as placeFiles, dt as runStatus, en as linkTools, et as createReplay, f as runningScript, fn as placeFor, ft as runTree, g as triggersReport, h as lateTriggers, hn as shownPath, i as NOT_STARTED, in as deleteAgent, it as followEvents, j as resolveInputs, jn as serializeWorkflow, jt as FEEDBACK_SEVERITIES, kt as gitRulesOf, l as planUpgrade, ln as inRepoPlace, lt as liveStatus, m as isNewer, mn as runDirOf, n as resolveBase, nn as toolsInUse, on as loadAgents, pn as projectsHome, qn as AGENT_TOOL_LAYOUTS, rn as createAgent, rt as detachRun, st as planNodeSession, t as createWorkspace, tn as toolLinks, tt as replayLoopAnswers, u as releaseNotes, un as newProjectId, vn as projectRootOf, vt as walkSummary, xt as userMessageText, yt as startRun, z as tellNode, zn as beforeOf, zt as fingerprint } from "./src-CGqMati3.mjs";
3
+ import { C as checkNote, D as placeOf, O as feedbackActions, S as previewPost, T as acceptItem, _ as answerDecision, b as builtinWorkflowFile, c as checkDue, d as pendingNotice, f as readUpdateCache, g as nodeSession, h as writeUpdateCache, i as registryOf, k as openFixRun, 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, v as pendingDecisions, w as recordNote, y as builtinProblem } from "./__build_info__-CtSVVuZE.mjs";
4
+ import { n as newRunId, t as findRun } from "./find-DptmbhGd.mjs";
5
+ 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-CXSJO00X.mjs";
6
+ import { n as listOrkWorktrees, t as createdWorktrees } from "./list-ZvY2OcLq.mjs";
7
+ import { a as planClean, t as applyClean } from "./clean-Dcr807be.mjs";
8
+ import { a as formatRunEvent, i as formatEvent, n as formatRuns, o as createTerminalHuman, r as formatTree, t as ago } from "./format-J-a21Yvn.mjs";
9
+ import { a as validate, c as warningLines, d as refusedBuiltin, f as workflowArg, i as loadWorkflow, l as WORKFLOW_DIR, m as registeredProjects, o as validateFile, p as projectAt, r as formatReport, s as walkLoaded, t as context, u as WORKFLOW_SUFFIX } from "./context-C_bksb0C.mjs";
10
+ import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-BDZCmGEu.mjs";
11
+ import { _ as resultOf, 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 } from "./src-UWkk1sl9.mjs";
12
+ import { i as decideRefusal, n as chatRefusal } from "./in-a-run-D1Qc5Mit.mjs";
11
13
  import { defineCommand, runMain } from "citty";
12
14
  import { createInterface } from "node:readline/promises";
13
15
  import path from "node:path";
14
16
  import { stringify } from "yaml";
15
- import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
17
+ import { z } from "zod";
18
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
16
19
  import { createHash } from "node:crypto";
17
20
  import { homedir } from "node:os";
18
- import { setTimeout } from "node:timers/promises";
19
21
  import { execFile, spawn } from "node:child_process";
22
+ import { setTimeout } from "node:timers/promises";
20
23
  import { styleText } from "node:util";
21
24
  // ../core/src/control/queue.ts
22
25
  const PERMISSION_OPTIONS = [{
@@ -208,41 +211,6 @@ const startControl = async (options) => {
208
211
  }
209
212
  };
210
213
  };
211
- // ../core/src/feedback/human.ts
212
- const RATINGS = ["good", "bad"];
213
- const MAX_TITLE = 120;
214
- /** The run event a note becomes; its title is the note's first line. */
215
- const humanNoteEvent = (note) => {
216
- const text = maskSecrets(note.text.trim());
217
- const [first = ""] = text.split("\n");
218
- const title = first.slice(0, MAX_TITLE);
219
- const kind = note.kind ?? "friction";
220
- const target = targetOf({
221
- kind,
222
- title,
223
- ...note.target === void 0 ? {} : { target: note.target }
224
- });
225
- const feedback = {
226
- source: "human",
227
- kind,
228
- target,
229
- severity: note.severity ?? (note.rating === "bad" ? "high" : "medium"),
230
- title,
231
- body: text,
232
- evidence: note.rating === void 0 ? [] : [`rating: ${note.rating}`],
233
- fingerprint: fingerprint({
234
- target,
235
- kind,
236
- title
237
- }),
238
- origin: { workflow: note.workflow }
239
- };
240
- return {
241
- type: "feedback.recorded",
242
- ...note.node === void 0 ? {} : { node: note.node },
243
- feedback
244
- };
245
- };
246
214
  // ../core/src/feedback/signals.ts
247
215
  const ORK_TOOL = "mcp__ork__";
248
216
  /**
@@ -402,6 +370,47 @@ const createSignals = (options = {}) => {
402
370
  return signal === null ? [event] : [event, ...record(...signal)];
403
371
  };
404
372
  };
373
+ /** A title's words, as fingerprints compare them. */
374
+ const titleWords = (title) => new Set(normalizeTitle(title).split(" ").filter((word) => word !== ""));
375
+ /** The share of two titles' words they have in common (Jaccard). */
376
+ const titleOverlap = (a, b) => {
377
+ const left = titleWords(a);
378
+ const right = titleWords(b);
379
+ const union = /* @__PURE__ */ new Set([...left, ...right]);
380
+ if (union.size === 0) return 0;
381
+ return [...left].filter((word) => right.has(word)).length / union.size;
382
+ };
383
+ const unsettled = (item) => item.status === "new" || item.status === "triaged";
384
+ const firstSeen = (a, b) => a.firstSeen.localeCompare(b.firstSeen) || a.id.localeCompare(b.id);
385
+ /**
386
+ * One kind of item's groups, oldest first: an item joins the first group whose
387
+ * first-seen item it is near enough, so every member is within the rule of the
388
+ * item the others go under, never only of one another.
389
+ */
390
+ const groupsOf = (items) => {
391
+ const groups = [];
392
+ for (const item of items.toSorted(firstSeen)) {
393
+ const group = groups.find(([root]) => root !== void 0 && titleOverlap(root.title, item.title) >= .8);
394
+ if (group === void 0) groups.push([item]);
395
+ else group.push(item);
396
+ }
397
+ return groups;
398
+ };
399
+ /** The merges triage would make among `items`, oldest group first. */
400
+ const proposeMerges = (items) => {
401
+ const byKind = /* @__PURE__ */ new Map();
402
+ for (const item of items.filter((i) => unsettled(i))) {
403
+ const key = `${item.target}/${item.kind}`;
404
+ byKind.set(key, [...byKind.get(key) ?? [], item]);
405
+ }
406
+ return [...byKind.values()].flatMap((sameKind) => groupsOf(sameKind)).filter((group) => group.length > 1 && group.some((item) => item.status === "new")).map((group) => {
407
+ const [into, ...from] = group;
408
+ return into === void 0 ? void 0 : {
409
+ into,
410
+ from
411
+ };
412
+ }).filter((merge) => merge !== void 0).toSorted((a, b) => firstSeen(a.into, b.into));
413
+ };
405
414
  // ../core/src/remote/relay.ts
406
415
  /**
407
416
  * @file A run's side of the relay (RFC-003): its nodes' links, read from the
@@ -526,97 +535,6 @@ const createRecorder = (store, run, runDir) => {
526
535
  store.begin(run);
527
536
  return appendRecorder(store, run.id, runDir);
528
537
  };
529
- const updateCachePath = (home = homedir()) => path.join(home, ".ork", "update-check.json");
530
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
531
- const text = (record, key) => {
532
- const value = record[key];
533
- return typeof value === "string" ? value : void 0;
534
- };
535
- const notifiedOf = (value) => {
536
- if (!isRecord(value)) return void 0;
537
- const version = text(value, "version");
538
- const at = text(value, "at");
539
- return version === void 0 || at === void 0 ? void 0 : {
540
- version,
541
- at
542
- };
543
- };
544
- /** The cache, or `null` when there is none or it cannot be read. */
545
- const readUpdateCache = (file) => {
546
- let parsed;
547
- try {
548
- parsed = JSON.parse(readFileSync(file, "utf8"));
549
- } catch {
550
- return null;
551
- }
552
- if (!isRecord(parsed)) return null;
553
- const checkedAt = text(parsed, "checkedAt");
554
- if (checkedAt === void 0) return null;
555
- const latest = text(parsed, "latest");
556
- const notified = notifiedOf(parsed["notified"]);
557
- return {
558
- checkedAt,
559
- ...latest === void 0 ? {} : { latest },
560
- ...notified === void 0 ? {} : { notified }
561
- };
562
- };
563
- /** Writes the cache whole (a temporary file, then a rename). */
564
- const writeUpdateCache = (file, cache) => {
565
- mkdirSync(path.dirname(file), { recursive: true });
566
- const temporary = `${file}.${process.pid}.tmp`;
567
- writeFileSync(temporary, `${JSON.stringify(cache, null, 2)}\n`);
568
- renameSync(temporary, file);
569
- };
570
- const age = (iso, now) => {
571
- const time = Date.parse(iso);
572
- return Number.isNaN(time) ? Infinity : now.getTime() - time;
573
- };
574
- /** Whether it is time to ask the registry again. */
575
- const checkDue = (cache, now) => cache === null || age(cache.checkedAt, now) >= 864e5;
576
- /** The package's `latest` version on the registry, or `null` on any failure. */
577
- const fetchLatest = async (name, options = {}) => {
578
- const registry = (options.registry ?? "https://registry.npmjs.org").replace(/\/+$/u, "");
579
- const get = options.fetch ?? globalThis.fetch;
580
- try {
581
- const response = await get(`${registry}/-/package/${name}/dist-tags`, {
582
- headers: { accept: "application/json" },
583
- signal: AbortSignal.timeout(options.timeoutMs ?? 5e3)
584
- });
585
- if (!response.ok) return null;
586
- const tags = await response.json();
587
- return (isRecord(tags) ? text(tags, "latest") : void 0) ?? null;
588
- } catch {
589
- return null;
590
- }
591
- };
592
- /**
593
- * Asks the registry and records the answer. A failed check is recorded too, so
594
- * an offline machine is not asked again until the interval passes; the last
595
- * known `latest` is kept.
596
- */
597
- const refreshUpdateCache = async (file, name, now, options = {}) => {
598
- const before = readUpdateCache(file);
599
- const latest = await fetchLatest(name, options) ?? before?.latest;
600
- const cache = {
601
- ...before,
602
- checkedAt: now.toISOString(),
603
- ...latest === void 0 ? {} : { latest }
604
- };
605
- writeUpdateCache(file, cache);
606
- return cache;
607
- };
608
- /** The newer version the cache knows of, if any. */
609
- const newerVersion = (cache, current) => cache?.latest !== void 0 && isNewer(cache.latest, current) ? cache.latest : null;
610
- /**
611
- * The version to tell the user about now: newer than theirs, and not already
612
- * told today. `null` when there is nothing to say.
613
- */
614
- const pendingNotice = (cache, current, now) => {
615
- const newer = newerVersion(cache, current);
616
- if (newer === null) return null;
617
- const told = cache?.notified;
618
- return told?.version === newer && age(told.at, now) < 864e5 ? null : newer;
619
- };
620
538
  // src/runs/chat.ts
621
539
  /**
622
540
  * @file `ork chat`'s two paths. A node of a live run gets the message through
@@ -1273,7 +1191,7 @@ const flagValues = (rawArgs, flag) => {
1273
1191
  };
1274
1192
  const isDriver = (value) => CLAUDE_DRIVERS.some((d) => d.id === value);
1275
1193
  /** Loads the drivers only when a run starts, so other commands stay fast. */
1276
- const loadRuntime = async (driver) => (await import("./src-CZOVF2yU.mjs")).createRuntime(driver, sessionEnv(process.env));
1194
+ const loadRuntime = async (driver) => (await import("./src-BHnxpJyH.mjs")).createRuntime(driver, sessionEnv(process.env));
1277
1195
  /**
1278
1196
  * Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
1279
1197
  * so that the run ends tidily and leaves no result unscrubbed.
@@ -1304,7 +1222,7 @@ const run = defineCommand({
1304
1222
  args: {
1305
1223
  workflow: {
1306
1224
  type: "positional",
1307
- description: "A workflow file, or the name of one in the project's workflows (.ork/workflows/)",
1225
+ description: "A workflow file, the name of one in the project's workflows (.ork/workflows/), or one of Ork's own (ork:feedback-apply)",
1308
1226
  required: true
1309
1227
  },
1310
1228
  node: {
@@ -1375,6 +1293,7 @@ const run = defineCommand({
1375
1293
  return;
1376
1294
  }
1377
1295
  const cwd = process.cwd();
1296
+ if (refusedBuiltin([args.workflow], cwd)) return;
1378
1297
  const target = workflowArg(args.workflow, cwd);
1379
1298
  const store = openStore();
1380
1299
  if (args.detach) {
@@ -1591,18 +1510,44 @@ const chat = defineCommand({
1591
1510
  }
1592
1511
  }
1593
1512
  });
1594
- // src/commands/control.ts
1513
+ // src/commands/decide.ts
1595
1514
  /**
1596
- * @file `ork decide`, `ork stop` and `ork resume`: answer a live run's open
1597
- * decisions and stop a live run, or one of its nodes (ADR-041), from any
1598
- * terminal (both write to the store, then wake the run on its socket;
1599
- * specs/messaging.md), and continue a stopped or crashed run with its
1600
- * recorded state (ADR-026).
1515
+ * @file `ork decide`: answer a live run's open decisions from any terminal (it
1516
+ * writes to the store, then wakes the run on its socket; specs/messaging.md).
1517
+ * With `--id` and `--choose`, `--note` and `--rating` also note how the
1518
+ * decision's node went, as the run's feedback (specs/feedback.md).
1601
1519
  */
1602
- const failed = (message) => {
1520
+ const failed$2 = (message) => {
1603
1521
  writeErr(message);
1604
1522
  process.exitCode = 1;
1605
1523
  };
1524
+ /**
1525
+ * Notes the answered decision's node, when there's a note or a rating; a rating
1526
+ * alone is its note's words.
1527
+ */
1528
+ const noteDecision = (store, id, note, rating) => {
1529
+ if (note === void 0 && rating === void 0) return void 0;
1530
+ const decision = store.decision(id);
1531
+ const [run] = decision === void 0 ? [] : store.lookup(decision.runId);
1532
+ if (decision === void 0 || run === void 0) return {
1533
+ ok: false,
1534
+ error: `No decision ${id} to note.`
1535
+ };
1536
+ return recordNote(store, run, {
1537
+ text: note ?? `Rated ${rating ?? ""} at ${decision.question}`,
1538
+ node: decision.node,
1539
+ rating
1540
+ }, "--");
1541
+ };
1542
+ /** Why a note given with an answer can't be recorded, before answering. */
1543
+ const noteProblem = (note, rating) => {
1544
+ if (note === void 0 && rating === void 0) return void 0;
1545
+ const checked = checkNote({
1546
+ text: note ?? "rated",
1547
+ rating
1548
+ }, "--");
1549
+ return checked.ok ? void 0 : checked.error;
1550
+ };
1606
1551
  /** One line per open decision, for `--list`. */
1607
1552
  const formatDecisions = (decisions) => decisions.length === 0 ? ["No open decisions."] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)} ${d.question} ${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
1608
1553
  const decide = defineCommand({
@@ -1638,12 +1583,20 @@ const decide = defineCommand({
1638
1583
  choose: {
1639
1584
  type: "string",
1640
1585
  description: "…with this option"
1586
+ },
1587
+ note: {
1588
+ type: "string",
1589
+ description: "…and note how its node went; the note joins the run's feedback"
1590
+ },
1591
+ rating: {
1592
+ type: "string",
1593
+ description: "…rating it good or bad"
1641
1594
  }
1642
1595
  },
1643
1596
  run: async ({ args }) => {
1644
1597
  const refusal = args.id !== void 0 || args.choose !== void 0 || process.stdin.isTTY && !args.list && !args.json ? decideRefusal() : null;
1645
1598
  if (refusal !== null) {
1646
- failed(refusal);
1599
+ failed$2(refusal);
1647
1600
  return;
1648
1601
  }
1649
1602
  const store = openStore();
@@ -1655,19 +1608,35 @@ const decide = defineCommand({
1655
1608
  try {
1656
1609
  if (args.id !== void 0 || args.choose !== void 0) {
1657
1610
  if (args.id === void 0 || args.choose === void 0) {
1658
- failed("--id and --choose go together.");
1611
+ failed$2("--id and --choose go together.");
1612
+ return;
1613
+ }
1614
+ const problem = noteProblem(args.note, args.rating);
1615
+ if (problem !== void 0) {
1616
+ failed$2(problem);
1659
1617
  return;
1660
1618
  }
1661
1619
  const answered = await answerDecision(store, args.id, args.choose);
1662
- if (answered.ok) write(`${style("green", "✓")} ${args.id}: ${args.choose}`);
1663
- else failed(answered.error);
1620
+ if (!answered.ok) {
1621
+ failed$2(answered.error);
1622
+ return;
1623
+ }
1624
+ write(`${style("green", "✓")} ${args.id}: ${args.choose}`);
1625
+ const noted = noteDecision(store, args.id, args.note, args.rating);
1626
+ if (noted === void 0) return;
1627
+ if (noted.ok) write(`${style("green", "✓")} noted in item ${noted.item}`);
1628
+ else failed$2(noted.error);
1629
+ return;
1630
+ }
1631
+ if (args.note !== void 0 || args.rating !== void 0) {
1632
+ failed$2("--note and --rating go with --id and --choose.");
1664
1633
  return;
1665
1634
  }
1666
1635
  const filter = {};
1667
1636
  if (args.run !== void 0 && args.run !== "") {
1668
1637
  const found = findRun(store, args.run, process.cwd());
1669
1638
  if (!found.ok) {
1670
- failed(found.error);
1639
+ failed$2(found.error);
1671
1640
  return;
1672
1641
  }
1673
1642
  filter.runId = found.run.id;
@@ -1700,6 +1669,17 @@ const decide = defineCommand({
1700
1669
  }
1701
1670
  }
1702
1671
  });
1672
+ // src/commands/control.ts
1673
+ /**
1674
+ * @file `ork stop` and `ork resume`: stop a live run, or one of its nodes
1675
+ * (ADR-041), from any terminal (it writes to the store, then wakes the run on
1676
+ * its socket; specs/messaging.md), and continue a stopped or crashed run with
1677
+ * its recorded state (ADR-026). `ork decide` is in `decide.ts`.
1678
+ */
1679
+ const failed$1 = (message) => {
1680
+ writeErr(message);
1681
+ process.exitCode = 1;
1682
+ };
1703
1683
  const stop = defineCommand({
1704
1684
  meta: {
1705
1685
  name: "stop",
@@ -1721,13 +1701,13 @@ const stop = defineCommand({
1721
1701
  try {
1722
1702
  const found = findRun(store, args.run, process.cwd());
1723
1703
  if (!found.ok) {
1724
- failed(found.error);
1704
+ failed$1(found.error);
1725
1705
  return;
1726
1706
  }
1727
1707
  const { run } = found;
1728
1708
  const asked = await requestStop(store, run, void 0, args.node);
1729
1709
  if (!asked.ok) {
1730
- failed(asked.error);
1710
+ failed$1(asked.error);
1731
1711
  return;
1732
1712
  }
1733
1713
  if (args.node !== void 0) {
@@ -1790,7 +1770,7 @@ const resume = defineCommand({
1790
1770
  },
1791
1771
  run: async ({ args, rawArgs }) => {
1792
1772
  if (args.driver !== void 0 && !isDriver(args.driver)) {
1793
- failed(`--driver must be sdk or cli, got \`${args.driver}\``);
1773
+ failed$1(`--driver must be sdk or cli, got \`${args.driver}\``);
1794
1774
  return;
1795
1775
  }
1796
1776
  const store = openStore();
@@ -1802,14 +1782,14 @@ const resume = defineCommand({
1802
1782
  ...args.driver === void 0 ? {} : { driver: args.driver }
1803
1783
  }) : found;
1804
1784
  if (!found.ok || !ready.ok) {
1805
- failed(ready.ok ? "" : ready.error);
1785
+ failed$1(ready.ok ? "" : ready.error);
1806
1786
  return;
1807
1787
  }
1808
1788
  if (args.detach) {
1809
1789
  const flags = rawArgs.filter((a) => a !== "--detach" && a !== args.run);
1810
1790
  const detached = await detachResume(store, found.run, flags);
1811
1791
  if (detached.ok) write(`▸ run ${found.run.id} resumed in the background`);
1812
- else failed(`${detached.error}\n(output: ${detached.log})`);
1792
+ else failed$1(`${detached.error}\n(output: ${detached.log})`);
1813
1793
  return;
1814
1794
  }
1815
1795
  const { exitCode } = await runWorkflowFile({
@@ -1852,22 +1832,20 @@ const resume = defineCommand({
1852
1832
  */
1853
1833
  /** Set in the background copy of `ork` that only runs the check. */
1854
1834
  const UPDATE_CHILD_ENV = "ORK_UPDATE_CHECK_CHILD";
1855
- /** A registry to ask instead of npm's, e.g. a company mirror. */
1856
- const UPDATE_REGISTRY_ENV = "ORK_UPDATE_REGISTRY";
1857
- const on = (value) => value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
1858
- /** Why the automatic check is off for this command, or `null` when it is on. */
1835
+ /**
1836
+ * Why the automatic check is off for this command, or `null` when it is on: the
1837
+ * environment's switches (core's, which `ork ui` reads too), then where a
1838
+ * notice on stderr would go.
1839
+ */
1859
1840
  const updateCheckOff = (env, argv, terminal) => {
1860
- if (on(env["ORK_NO_UPDATE_CHECK"]) || on(env["NO_UPDATE_NOTIFIER"])) return "turned off";
1861
- if (on(env["CI"])) return "in CI";
1862
- if (on(env["ORK_DETACHED"]) || !terminal) return "no terminal";
1841
+ const switched = updateSwitchOff(env);
1842
+ if (switched !== null) return switched === "CI" ? "in CI" : "turned off";
1843
+ if (isSwitchedOn(env["ORK_DETACHED"]) || !terminal) return "no terminal";
1863
1844
  if (argv.includes("--json")) return "JSON output";
1864
1845
  if (argv[0] === "upgrade") return "ork upgrade checks for itself";
1846
+ if (argv[0] === "ui" && argv.includes("--serve")) return "ork ui's server checks for itself";
1865
1847
  return null;
1866
1848
  };
1867
- const registryOf = (env) => {
1868
- const registry = env[UPDATE_REGISTRY_ENV];
1869
- return registry === void 0 || registry === "" ? void 0 : registry;
1870
- };
1871
1849
  /** The notice's text. */
1872
1850
  const updateNotice = (latest, current) => `Ork ${latest} is out (you have ${current}). Upgrade with: ork upgrade`;
1873
1851
  /**
@@ -1918,54 +1896,11 @@ const showUpdateNotice = (file = updateCachePath(), now = /* @__PURE__ */ new Da
1918
1896
  };
1919
1897
  // src/update/upgrade.ts
1920
1898
  /**
1921
- * @file What `ork upgrade` does (ADR-038): compares the registry's latest
1922
- * version with this one, works out how Ork was installed, and gives the
1923
- * command to run (a global npm, pnpm, yarn, bun or Volta install) or advice
1924
- * (a clone, `npx`); runs the command when the user agrees. Also the version
1925
- * line of `ork doctor`, read from the cache without asking the network.
1926
- */
1927
- const REPOSITORY = "https://github.com/mimshins/ork";
1928
- /** The GitHub release page of a version, with its changes. */
1929
- const releaseNotes = (name, version) => `${REPOSITORY}/releases/tag/${encodeURIComponent(`${name}@${version}`)}`;
1930
- /** The running script's real path, which tells how Ork was installed. */
1931
- const runningScript = () => {
1932
- const script = process.argv[1] ?? "";
1933
- try {
1934
- return realpathSync(script);
1935
- } catch {
1936
- return script;
1937
- }
1938
- };
1939
- const planUpgrade = (options) => {
1940
- const { latest, name } = options;
1941
- if (latest === null) return { kind: "unreachable" };
1942
- if (!isNewer(latest, options.current)) return { kind: "latest" };
1943
- const spec = `${name}@${latest}`;
1944
- const installer = detectInstaller(options.script, options.installers);
1945
- if (installer?.command !== void 0) return {
1946
- kind: "command",
1947
- latest,
1948
- installer,
1949
- argv: installer.command(spec)
1950
- };
1951
- return {
1952
- kind: "advice",
1953
- latest,
1954
- text: installer?.advice?.(spec, options.script.replaceAll("\\", "/")) ?? `Ork could not tell how it was installed. With npm: npm install --global ${spec}`
1955
- };
1956
- };
1957
- /** Runs the installer's command on this terminal; resolves its exit code. */
1958
- const runInstall = (argv) => new Promise((resolve) => {
1959
- const [program = "", ...args] = argv;
1960
- const child = spawn(program, args, { stdio: "inherit" });
1961
- child.on("error", (error) => {
1962
- process.stderr.write(`Could not run ${program}: ${error.message}\n`);
1963
- resolve(127);
1964
- });
1965
- child.on("close", (code) => {
1966
- resolve(code ?? 1);
1967
- });
1968
- });
1899
+ * @file `ork doctor`'s line for Ork itself (ADR-038): the running version
1900
+ * against the last update check, read from the record without asking the
1901
+ * network. What `ork upgrade` does is core's (`planUpgrade`, `runInstall`),
1902
+ * shared with the server behind `ork ui`.
1903
+ */
1969
1904
  /** `ork doctor`'s line for Ork itself, from the last check. */
1970
1905
  const versionCheck = (current, cache, checksOff) => {
1971
1906
  const newer = newerVersion(cache, current);
@@ -2301,7 +2236,7 @@ const doctor = defineCommand({
2301
2236
  const runningRuns = () => {
2302
2237
  const store = openStore();
2303
2238
  try {
2304
- return store.runs({ limit: 500 }).filter((run) => liveStatus(run) === "running").map((run) => run.id.slice(0, 8));
2239
+ return runningRunIds(store).map((id) => id.slice(0, 8));
2305
2240
  } finally {
2306
2241
  store.close();
2307
2242
  }
@@ -2914,33 +2849,6 @@ const migrateHere = async (cwd, options) => {
2914
2849
  store.close();
2915
2850
  }
2916
2851
  };
2917
- // src/project/worktrees.ts
2918
- /** One line of state: where it is and what clean would care about. */
2919
- const worktreeState = (item) => {
2920
- const parts = [];
2921
- if (item.path === null) parts.push("no worktree");
2922
- else if (item.missing) parts.push("missing (prunable)");
2923
- if (item.dirty) parts.push(style("yellow", "uncommitted changes"));
2924
- const commits = item.merged ? style("green", "merged") : `${String(item.ahead)} commit(s) not merged`;
2925
- if (item.checkedOut !== void 0) parts.push(`on the workflow's branch ${item.checkedOut}`, commits);
2926
- else if (item.branch === void 0) parts.push(item.ahead > 0 ? style("yellow", `detached, ${String(item.ahead)} commit(s) on no branch`) : "detached, nothing committed");
2927
- else parts.push(commits);
2928
- if (item.inMainCheckout) parts.push("checked out here");
2929
- return parts.join(" · ");
2930
- };
2931
- /** A worktree's name: its branch, or its run and node when created detached. */
2932
- const labelOf = (item) => item.branch ?? `${item.runId}/${item.nodeId} (detached)`;
2933
- const formatWorktrees = (items) => {
2934
- if (items.length === 0) return ["No Ork worktrees or branches."];
2935
- const width = Math.max(...items.map((i) => labelOf(i).length));
2936
- return items.flatMap((item) => [`${style("bold", labelOf(item).padEnd(width))} ${worktreeState(item)}`, ...item.path === null ? [] : [` ${style("dim", item.path)}`]]);
2937
- };
2938
- const formatPlan = (steps) => steps.map((step) => {
2939
- const actions = [...step.removeWorktree ? ["remove worktree"] : [], ...step.deleteBranch ? ["delete branch"] : []];
2940
- const what = actions.length > 0 ? actions.join(" + ") : "keep";
2941
- const why = step.skip === void 0 ? "" : style("dim", ` (${step.skip})`);
2942
- return ` ${labelOf(step.item)}: ${what}${why}`;
2943
- });
2944
2852
  // src/commands/project-records.ts
2945
2853
  /**
2946
2854
  * @file `ork projects`' subcommands on one project's record (ADR-035): `forget`
@@ -3006,124 +2914,8 @@ const move = defineCommand({
3006
2914
  * follows its moved clone (ADR-035); none writes into a repository. `ork
3007
2915
  * migrate` moves an attached project into its repository, brings its agents
3008
2916
  * and skills under `.ork/`, or drafts a workflow from its agents (ADR-036),
3009
- * and commits nothing. `ork worktrees` lists the worktrees and branches runs
3010
- * left behind (those the runs recorded creating, whatever the project's git
3011
- * rules named them, and any `ork/` branch), with their state read from git,
3012
- * and cleans them up safely after showing the plan and asking.
3013
- */
3014
- const projectRoot = async () => (await inspectProject(process.cwd()))?.root ?? null;
3015
- /** What Ork created in the repo at `root`, from its runs' records and git. */
3016
- const itemsIn = async (root) => {
3017
- const store = openStore();
3018
- try {
3019
- return await listOrkWorktrees(root, createdWorktrees(store, root));
3020
- } finally {
3021
- store.close();
3022
- }
3023
- };
3024
- const notGit = () => {
3025
- process.stderr.write("Not in a git repository with a commit.\n");
3026
- process.exitCode = 1;
3027
- };
3028
- const show = async (json) => {
3029
- const root = await projectRoot();
3030
- if (root === null) {
3031
- notGit();
3032
- return;
3033
- }
3034
- const items = await itemsIn(root);
3035
- if (json) write(JSON.stringify(items, void 0, 2));
3036
- else for (const line of formatWorktrees(items)) write(line);
3037
- };
3038
- const list = defineCommand({
3039
- meta: {
3040
- name: "list",
3041
- description: "List Ork's worktrees and branches (the default)"
3042
- },
3043
- args: { json: {
3044
- type: "boolean",
3045
- description: "Print JSON",
3046
- default: false
3047
- } },
3048
- run: ({ args }) => show(args.json)
3049
- });
3050
- const clean = defineCommand({
3051
- meta: {
3052
- name: "clean",
3053
- description: "Remove Ork's worktrees (merged ones by default); never one with uncommitted changes unless --force"
3054
- },
3055
- args: {
3056
- run: {
3057
- type: "string",
3058
- description: "Only this run's worktrees"
3059
- },
3060
- all: {
3061
- type: "boolean",
3062
- description: "Include worktrees whose branch is not merged",
3063
- default: false
3064
- },
3065
- branches: {
3066
- type: "boolean",
3067
- description: "Also delete the branches",
3068
- default: false
3069
- },
3070
- force: {
3071
- type: "boolean",
3072
- description: "Remove uncommitted changes and unmerged branches too",
3073
- default: false
3074
- },
3075
- yes: {
3076
- type: "boolean",
3077
- description: "Do not ask",
3078
- default: false
3079
- }
3080
- },
3081
- run: async ({ args }) => {
3082
- const root = await projectRoot();
3083
- if (root === null) {
3084
- notGit();
3085
- return;
3086
- }
3087
- const filter = { merged: !args.all };
3088
- if (args.run !== void 0) filter.runId = args.run;
3089
- const steps = planClean(await itemsIn(root), filter, {
3090
- branches: args.branches,
3091
- force: args.force
3092
- });
3093
- const acting = steps.filter((s) => s.removeWorktree || s.deleteBranch);
3094
- if (steps.length === 0 || acting.length === 0) {
3095
- for (const line of formatPlan(steps)) write(line);
3096
- write("Nothing to clean.");
3097
- return;
3098
- }
3099
- write("Plan:");
3100
- for (const line of formatPlan(steps)) write(line);
3101
- if (!args.yes) {
3102
- if (!process.stdin.isTTY) {
3103
- process.stderr.write("Pass --yes to clean without a terminal.\n");
3104
- process.exitCode = 1;
3105
- return;
3106
- }
3107
- if (!await confirm("Go ahead? [y/N] ")) return;
3108
- }
3109
- for (const line of await applyClean(root, acting, args.force)) write(`${style("green", "✓")} ${line}`);
3110
- }
3111
- });
3112
- const worktrees = defineCommand({
3113
- meta: {
3114
- name: "worktrees",
3115
- description: "Track and clean up the git worktrees Ork creates for runs"
3116
- },
3117
- subCommands: {
3118
- list,
3119
- clean
3120
- },
3121
- run: async ({ rawArgs }) => {
3122
- const [first] = rawArgs;
3123
- if (first === "list" || first === "clean") return;
3124
- await show(rawArgs.includes("--json"));
3125
- }
3126
- });
2917
+ * and commits nothing. `ork worktrees` is worktrees.ts.
2918
+ */
3127
2919
  const json$2 = {
3128
2920
  type: "boolean",
3129
2921
  description: "Print JSON",
@@ -3220,6 +3012,20 @@ const migrate = defineCommand({
3220
3012
  }, (file) => formatReport(validateFile(file, process.cwd())));
3221
3013
  }
3222
3014
  });
3015
+ // src/store.ts
3016
+ /**
3017
+ * @file Opening the user's store (`~/.ork/state.db`) for the length of one
3018
+ * command, and closing it after, whatever happens.
3019
+ */
3020
+ /** Runs `body` with the user's store open, and closes it after. */
3021
+ const withStore = async (body) => {
3022
+ const store = openStore();
3023
+ try {
3024
+ await body(store);
3025
+ } finally {
3026
+ store.close();
3027
+ }
3028
+ };
3223
3029
  // src/runs/feedback.ts
3224
3030
  /**
3225
3031
  * @file `ork feedback`'s helpers (specs/feedback.md): finding an item by a
@@ -3269,6 +3075,7 @@ const formatFeedbackItem = (item, records, now = Date.now()) => {
3269
3075
  ];
3270
3076
  if (item.workflows.length > 0) lines.push(`Workflows: ${item.workflows.join(", ")}`);
3271
3077
  if (item.models.length > 0) lines.push(`Models: ${item.models.join(", ")}`);
3078
+ if (item.merged.length > 0) lines.push(`Merged into it: ${item.merged.map((id) => id.slice(0, 12)).join(", ")} (ork feedback split ${item.id.slice(0, 12)} takes them apart)`);
3272
3079
  for (const record of records) {
3273
3080
  const { feedback } = record;
3274
3081
  const where = record.node === void 0 ? "" : ` · ${record.node}`;
@@ -3279,181 +3086,533 @@ const formatFeedbackItem = (item, records, now = Date.now()) => {
3279
3086
  }
3280
3087
  return lines;
3281
3088
  };
3282
- const oneOf = (name, value, allowed) => {
3283
- if (value === void 0) return { ok: true };
3284
- const found = allowed.find((a) => a === value);
3285
- return found === void 0 ? {
3286
- ok: false,
3287
- error: `--${name} is one of ${allowed.join(", ")}`
3288
- } : {
3289
- ok: true,
3290
- value: found
3291
- };
3292
- };
3293
3089
  /**
3294
3090
  * `ork feedback note`: records a person's note on a run, or one of its nodes,
3295
3091
  * as that run's feedback; returns the item it joins, or why it could not.
3296
3092
  */
3297
3093
  const noteRun = (store, cwd, args) => {
3298
- if (args.text.trim() === "") return {
3094
+ const found = findRun(store, args.run, cwd);
3095
+ if (!found.ok) return found;
3096
+ return recordNote(store, found.run, args, "--");
3097
+ };
3098
+ // src/runs/apply.ts
3099
+ /**
3100
+ * @file `ork feedback apply` (ADR-059): runs Ork's own `ork:feedback-apply` in
3101
+ * the project an item came from, with the item as its untrusted input. Its
3102
+ * worker proposes a fix on a branch and its gate asks to keep or discard it;
3103
+ * Ork never merges. Kept, the item is `resolved`; discarded, the run's
3104
+ * worktree and branch go and the item stays `accepted`.
3105
+ */
3106
+ /** The most of an item's records the worker reads, newest first. */
3107
+ const RECORDS = 10;
3108
+ /** The most of a record's body the worker reads. */
3109
+ const BODY_CHARS = 1500;
3110
+ const BUILTIN = "ork:feedback-apply";
3111
+ /** An item and its newest records, as the worker reads them. */
3112
+ const aboutItem = (item, records) => {
3113
+ const newest = records.toReversed().slice(0, RECORDS);
3114
+ const lines = [`Item ${item.id.slice(0, 12)}: ${item.title}`, `About: ${item.target} (${item.kind}), severity ${item.severity}; seen ${String(item.count)} time(s) in ${String(item.runs.length)} run(s).`];
3115
+ if (item.workflows.length > 0) lines.push(`Workflows: ${item.workflows.join(", ")}`);
3116
+ for (const record of newest) {
3117
+ const { feedback } = record;
3118
+ const where = record.node === void 0 ? "" : `, node ${record.node}`;
3119
+ lines.push("", `Record from run ${record.runId}${where} (${feedback.source}):`);
3120
+ if (feedback.title !== item.title) lines.push(feedback.title);
3121
+ if (feedback.body !== "") lines.push(feedback.body.slice(0, BODY_CHARS));
3122
+ if (feedback.evidence.length > 0) lines.push(`Evidence: ${feedback.evidence.join(", ")}`);
3123
+ }
3124
+ if (records.length > newest.length) lines.push("", `(${String(records.length - newest.length)} older record(s) left out.)`);
3125
+ return lines.join("\n");
3126
+ };
3127
+ const text = (value) => typeof value === "string" ? value : "";
3128
+ /** What a run of `ork:feedback-apply` ended with. */
3129
+ const outcomeOf = (result) => {
3130
+ const node = (id) => result?.nodes.find((n) => n.id === id);
3131
+ const fix = node("fix");
3132
+ const choice = text(node("keep")?.output?.["choice"]);
3133
+ const branch = text(fix?.output?.["branch"]);
3134
+ const summary = text(fix?.output?.["summary"]);
3135
+ if (choice === "keep") return {
3136
+ kind: "kept",
3137
+ branch,
3138
+ summary
3139
+ };
3140
+ if (choice === "discard") return {
3141
+ kind: "discarded",
3142
+ branch
3143
+ };
3144
+ if (fix?.ok === false && result?.status === "failed") return {
3145
+ kind: "nothing",
3146
+ summary: summary || (fix.error ?? "")
3147
+ };
3148
+ return {
3149
+ kind: "unfinished",
3150
+ reasons: result?.reasons ?? []
3151
+ };
3152
+ };
3153
+ /**
3154
+ * Removes what a run left in the repository at `root`: its worktrees and
3155
+ * branches.
3156
+ */
3157
+ const discardRun = async (store, root, runId) => {
3158
+ const items = await listOrkWorktrees(root, createdWorktrees(store, root));
3159
+ const steps = planClean(items, {
3160
+ merged: false,
3161
+ runId
3162
+ }, {
3163
+ branches: true,
3164
+ force: true
3165
+ });
3166
+ return applyClean(root, steps.filter((s) => s.removeWorktree || s.deleteBranch), true);
3167
+ };
3168
+ /** How many commits the run's worktrees hold that the current branch hasn't. */
3169
+ const commitsAhead = async (store, root, runId) => (await listOrkWorktrees(root, createdWorktrees(store, root))).filter((item) => item.runId === runId).reduce((sum, item) => sum + item.ahead, 0);
3170
+ /** What to say, and the item's status, after the run ends as `outcome`. */
3171
+ const settle = async (options, root, runId, outcome) => {
3172
+ const { store, item } = options;
3173
+ const short = item.id.slice(0, 12);
3174
+ switch (outcome.kind) {
3175
+ case "kept":
3176
+ if (await (options.ahead ?? commitsAhead)(store, root, runId) === 0) {
3177
+ const done = await discardRun(store, root, runId);
3178
+ return [`the fix's branch holds no commits, so there's nothing to keep; ${short} stays accepted`, ...done];
3179
+ }
3180
+ store.setFeedbackStatus(item.id, "resolved");
3181
+ return [`kept the fix on ${outcome.branch}; ${short} is resolved`, "Review and merge it like any other change; Ork never merges it."];
3182
+ case "discarded": {
3183
+ const done = await discardRun(store, root, runId);
3184
+ return [`discarded the fix; ${short} stays accepted`, ...done];
3185
+ }
3186
+ case "nothing": return [`no fix proposed; ${short} stays accepted`, ...outcome.summary === "" ? [] : [outcome.summary]];
3187
+ case "unfinished": return [`run ${runId} didn't finish, so ${short} stays as it was`, ...outcome.reasons];
3188
+ default: return outcome;
3189
+ }
3190
+ };
3191
+ /** Proposes a fix for an item, in the project it came from. */
3192
+ const applyItem = async (options) => {
3193
+ const { store, item } = options;
3194
+ const short = item.id.slice(0, 12);
3195
+ const allowed = feedbackActions(item, openFixRun(store, item)).apply;
3196
+ if (!allowed.ok) return {
3299
3197
  ok: false,
3300
- error: "The note is empty."
3198
+ error: allowed.error
3301
3199
  };
3302
- const found = findRun(store, args.run, cwd);
3200
+ const found = placeOf(store, item, void 0, options.registered);
3303
3201
  if (!found.ok) return found;
3304
- const { run } = found;
3305
- const ran = store.events(run.id).some(({ event }) => "id" in event && event.id === args.node);
3306
- if (args.node !== void 0 && !ran) return {
3202
+ const { place, project } = found;
3203
+ const problem = builtinProblem(place, BUILTIN);
3204
+ const file = builtinWorkflowFile(place, BUILTIN);
3205
+ if (problem !== void 0 || file === void 0) return {
3307
3206
  ok: false,
3308
- error: `No node ${args.node} ran in run ${run.id}.`
3309
- };
3310
- const kind = oneOf("kind", args.kind, FEEDBACK_KINDS);
3311
- const severity = oneOf("severity", args.severity, FEEDBACK_SEVERITIES);
3312
- const rating = oneOf("rating", args.rating, RATINGS);
3313
- for (const checked of [
3314
- kind,
3315
- severity,
3316
- rating
3317
- ]) if (!checked.ok) return checked;
3318
- const event = humanNoteEvent({
3319
- text: args.text,
3320
- workflow: run.workflow,
3321
- ...args.node === void 0 ? {} : { node: args.node },
3322
- ...kind.ok && kind.value !== void 0 ? { kind: kind.value } : {},
3323
- ...severity.ok && severity.value !== void 0 ? { severity: severity.value } : {},
3324
- ...rating.ok && rating.value !== void 0 ? { rating: rating.value } : {}
3207
+ error: problem ?? `${BUILTIN} couldn't be written.`
3208
+ };
3209
+ const records = store.feedbackRecords(item.id).filter((r) => store.lookup(r.runId)[0]?.project === project);
3210
+ options.out(`▸ proposing a fix for ${short} in ${place.root}`);
3211
+ const { result } = await runWorkflowFile({
3212
+ ...options,
3213
+ file,
3214
+ cwd: place.root,
3215
+ inputs: [`item=${short}`, `about=${aboutItem(item, records)}`],
3216
+ untrusted: ["about"],
3217
+ inPlace: false,
3218
+ json: false,
3219
+ onRun: (handle) => {
3220
+ store.setFeedbackStatus(item.id, "accepted");
3221
+ options.onRun?.(handle);
3222
+ }
3325
3223
  });
3326
- store.append(run.id, event);
3224
+ if (result === null) return {
3225
+ ok: false,
3226
+ error: `${BUILTIN} didn't start.`
3227
+ };
3228
+ const outcome = outcomeOf(result);
3327
3229
  return {
3328
3230
  ok: true,
3329
- item: event.type === "feedback.recorded" ? event.feedback.fingerprint : ""
3231
+ outcome,
3232
+ lines: await settle(options, place.root, result.runId, outcome)
3330
3233
  };
3331
3234
  };
3332
- // src/commands/runs.ts
3235
+ // src/runs/triage.ts
3333
3236
  /**
3334
- * @file `ork ps`, `ork logs`, `ork tree` and `ork feedback`: read what every
3335
- * project's runs recorded in `~/.ork/state.db` (specs/event-store.md,
3336
- * specs/feedback.md).
3237
+ * @file `ork feedback triage` (ADR-059): the merges Ork's rules propose among
3238
+ * unsettled items, each made only when the person says yes, then every new
3239
+ * item marked `triaged`, so a declined merge isn't asked again until a new
3240
+ * item joins it. `ork feedback split` takes a merge apart.
3337
3241
  */
3338
- /** Runs `body` with the user's store open, and closes it after. */
3339
- const withStore = async (body) => {
3340
- const store = openStore();
3341
- try {
3342
- await body(store);
3343
- } finally {
3344
- store.close();
3345
- }
3242
+ const lineOf = (item) => `${item.id.slice(0, 12)} ${`${String(item.count)}×`.padStart(4)} ${item.title}`;
3243
+ /** A proposed merge as the person reads it before saying yes. */
3244
+ const formatMerge = (merge) => [`Merge into ${lineOf(merge.into)}`, ...merge.from.map((item) => ` ${lineOf(item)}`)];
3245
+ /**
3246
+ * Proposes each merge, makes those `ask` says yes to, and marks every item that
3247
+ * was new `triaged`.
3248
+ */
3249
+ const triage$1 = async (store, ask) => {
3250
+ const items = store.feedbackItems();
3251
+ const fresh = items.filter((item) => item.status === "new");
3252
+ let merged = 0;
3253
+ let declined = 0;
3254
+ for (const merge of proposeMerges(items)) if (await ask(merge)) merged += store.mergeFeedback(merge.into.id, merge.from.map((item) => item.id));
3255
+ else declined += 1;
3256
+ for (const item of fresh) store.setFeedbackStatus(item.id, "triaged");
3257
+ return {
3258
+ merged,
3259
+ declined,
3260
+ triaged: fresh.length
3261
+ };
3346
3262
  };
3347
- const ps = defineCommand({
3348
- meta: {
3349
- name: "ps",
3350
- description: "List this project's runs, newest first"
3351
- },
3352
- args: {
3353
- all: {
3354
- type: "boolean",
3355
- description: "Every project's runs",
3356
- default: false
3357
- },
3358
- limit: {
3359
- type: "string",
3360
- description: "How many (default 20)"
3361
- },
3362
- json: {
3363
- type: "boolean",
3364
- description: "Print JSON",
3365
- default: false
3366
- }
3367
- },
3368
- run: ({ args }) => withStore((store) => {
3369
- const limit = Number(args.limit ?? 20);
3370
- const runs = store.runs({
3371
- limit: Number.isInteger(limit) && limit > 0 ? limit : 20,
3372
- ...args.all ? {} : { project: placeFor(process.cwd(), store.projects()).root }
3373
- });
3374
- const statusOf = (run) => runStatus(run, run.status === "running" ? store.events(run.id).map((s) => s.event) : []);
3375
- if (args.json) write(JSON.stringify(runs, void 0, 2));
3376
- else for (const line of formatRuns(runs, args.all, Date.now(), statusOf)) write(line);
3377
- })
3378
- });
3379
- const runArg = {
3263
+ /** What triage did, in a line. */
3264
+ const triageSummary = ({ merged, declined, triaged }) => {
3265
+ return [
3266
+ merged === 0 ? "nothing merged" : `merged ${String(merged)} item(s)`,
3267
+ ...declined === 0 ? [] : [`${String(declined)} merge(s) declined`],
3268
+ `${String(triaged)} new item(s) triaged`
3269
+ ].join("; ");
3270
+ };
3271
+ // src/commands/feedback-loop.ts
3272
+ /**
3273
+ * @file The `ork feedback` subcommands that close the loop (ADR-059): `apply`
3274
+ * runs Ork's own workflow to propose a fix for an item, `triage` merges items
3275
+ * about the same problem by rules after asking, and `split` takes a merge
3276
+ * apart.
3277
+ */
3278
+ const itemArg$1 = {
3380
3279
  type: "positional",
3381
- description: "A run id or a prefix of one (default: this project's newest)",
3382
- required: false
3280
+ description: "A feedback item's id, or a prefix of one",
3281
+ required: true
3383
3282
  };
3384
- const logs = defineCommand({
3283
+ const apply = defineCommand({
3385
3284
  meta: {
3386
- name: "logs",
3387
- description: "Print a run's transcript"
3285
+ name: "apply",
3286
+ description: "Have Ork propose a fix for an item on a branch, then keep or discard it"
3388
3287
  },
3389
3288
  args: {
3390
- run: runArg,
3391
- node: {
3289
+ item: itemArg$1,
3290
+ driver: {
3392
3291
  type: "string",
3393
- description: "Only this node's events"
3394
- },
3395
- follow: {
3396
- type: "boolean",
3397
- alias: "f",
3398
- description: "Keep printing a live run's events until it ends",
3399
- default: false
3292
+ description: "sdk (API key or cloud provider) or cli (your claude login)"
3400
3293
  },
3401
- json: {
3402
- type: "boolean",
3403
- description: "Print the stored events as JSON lines",
3404
- default: false
3294
+ "run-id": {
3295
+ type: "string",
3296
+ description: "Internal: the id `ork ui` picked for the run"
3297
+ }
3298
+ },
3299
+ run: async ({ args }) => {
3300
+ if (args.driver !== void 0 && !isDriver(args.driver)) {
3301
+ writeErr(`--driver must be sdk or cli, got \`${args.driver}\``);
3302
+ process.exitCode = 1;
3303
+ return;
3304
+ }
3305
+ const driver = args.driver;
3306
+ const store = openStore();
3307
+ const human = terminalHuman();
3308
+ try {
3309
+ const found = findFeedbackItem(store, args.item);
3310
+ if (!found.ok) {
3311
+ writeErr(found.error);
3312
+ process.exitCode = 1;
3313
+ return;
3314
+ }
3315
+ const applied = await applyItem({
3316
+ store,
3317
+ item: found.item,
3318
+ registered: registeredProjects(),
3319
+ runtime: loadRuntime,
3320
+ ...human === void 0 ? {} : { human },
3321
+ ...driver === void 0 ? {} : { driver },
3322
+ ...args["run-id"] === void 0 ? {} : { runId: args["run-id"] },
3323
+ out: write,
3324
+ err: writeErr,
3325
+ onRun: (handle) => {
3326
+ onInterrupt(handle.interrupt);
3327
+ }
3328
+ });
3329
+ if (!applied.ok) {
3330
+ writeErr(applied.error);
3331
+ process.exitCode = 1;
3332
+ return;
3333
+ }
3334
+ const [first, ...rest] = applied.lines;
3335
+ const mark = applied.outcome.kind === "kept" ? style("green", "✓") : style("yellow", "·");
3336
+ write(`${mark} ${first ?? ""}`);
3337
+ for (const line of rest) write(` ${line}`);
3338
+ if (applied.outcome.kind === "unfinished") process.exitCode = 1;
3339
+ } finally {
3340
+ human?.close();
3341
+ store.close();
3405
3342
  }
3343
+ }
3344
+ });
3345
+ const triage = defineCommand({
3346
+ meta: {
3347
+ name: "triage",
3348
+ description: "Merge new items about the same problem as others, asking about each"
3406
3349
  },
3350
+ args: { yes: {
3351
+ type: "boolean",
3352
+ description: "Make every merge without asking",
3353
+ default: false
3354
+ } },
3407
3355
  run: ({ args }) => withStore(async (store) => {
3408
- const found = findRun(store, args.run, process.cwd());
3356
+ if (!args.yes && !process.stdin.isTTY) {
3357
+ writeErr("Pass --yes to triage without a terminal.");
3358
+ process.exitCode = 1;
3359
+ return;
3360
+ }
3361
+ const done = await triage$1(store, async (merge) => {
3362
+ for (const line of formatMerge(merge)) write(line);
3363
+ return args.yes || await confirm("Merge them? [y/N] ");
3364
+ });
3365
+ write(`${style("green", "✓")} ${triageSummary(done)}`);
3366
+ })
3367
+ });
3368
+ const split = defineCommand({
3369
+ meta: {
3370
+ name: "split",
3371
+ description: "Take apart the items triage merged into one"
3372
+ },
3373
+ args: { item: itemArg$1 },
3374
+ run: ({ args }) => withStore((store) => {
3375
+ const found = findFeedbackItem(store, args.item);
3409
3376
  if (!found.ok) {
3410
3377
  writeErr(found.error);
3411
3378
  process.exitCode = 1;
3412
3379
  return;
3413
3380
  }
3414
- const abort = new AbortController();
3415
- process.once("SIGINT", () => {
3416
- abort.abort();
3381
+ const moved = store.splitFeedback(found.item.id);
3382
+ write(moved === 0 ? `Nothing was merged into ${found.item.id.slice(0, 12)}.` : `${style("green", "✓")} split ${String(moved)} item(s) back out of ${found.item.id.slice(0, 12)}`);
3383
+ })
3384
+ });
3385
+ // src/runs/jsonl.ts
3386
+ /**
3387
+ * @file `ork feedback export` and `import` (specs/feedback.md): feedback
3388
+ * records as JSON lines, one record a line, so a team can move feedback
3389
+ * between machines without GitHub. A line is checked before it's added, and a
3390
+ * record already here (its run and event) is skipped.
3391
+ */
3392
+ const Text = z.string();
3393
+ const LineSchema = z.strictObject({
3394
+ feedbackVersion: z.literal(1),
3395
+ runId: z.string().min(1),
3396
+ seq: z.number().int().nonnegative(),
3397
+ at: z.string().min(1),
3398
+ node: z.string().min(1).optional(),
3399
+ feedback: z.strictObject({
3400
+ source: z.enum([
3401
+ "system",
3402
+ "agent",
3403
+ "human"
3404
+ ]),
3405
+ kind: z.enum(FEEDBACK_KINDS),
3406
+ target: z.enum(FEEDBACK_TARGETS),
3407
+ severity: z.enum(FEEDBACK_SEVERITIES),
3408
+ title: Text.min(1),
3409
+ body: Text,
3410
+ evidence: z.array(Text),
3411
+ code: Text.optional(),
3412
+ fingerprint: z.string().regex(/^[0-9a-f]{8,64}$/u),
3413
+ origin: z.strictObject({
3414
+ workflow: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/u).optional(),
3415
+ workflowHash: Text.optional(),
3416
+ nodeType: Text.optional(),
3417
+ model: Text.optional(),
3418
+ driver: Text.optional(),
3419
+ version: Text.optional()
3420
+ })
3421
+ })
3422
+ });
3423
+ /** Records as JSON lines, with the format's version on each. */
3424
+ const exportLines = (records) => records.map(({ id: _id, ...record }) => JSON.stringify({
3425
+ feedbackVersion: 1,
3426
+ ...record
3427
+ })).map((line) => `${line}\n`).join("");
3428
+ /** The records in a JSONL file's text, and what was wrong with the rest. */
3429
+ const parseLines = (text) => {
3430
+ const records = [];
3431
+ const problems = [];
3432
+ for (const [index, line] of text.split("\n").entries()) {
3433
+ if (line.trim() === "") continue;
3434
+ let value;
3435
+ try {
3436
+ value = JSON.parse(line);
3437
+ } catch {
3438
+ problems.push(`line ${String(index + 1)}: not JSON`);
3439
+ continue;
3440
+ }
3441
+ const parsed = LineSchema.safeParse(value);
3442
+ if (!parsed.success) {
3443
+ const [issue] = parsed.error.issues;
3444
+ problems.push(`line ${String(index + 1)}: ${issue?.path.join(".") ?? ""} ${issue?.message ?? "isn't a feedback record"}`.trim());
3445
+ continue;
3446
+ }
3447
+ const { feedbackVersion: _version, node, feedback, ...rest } = parsed.data;
3448
+ const { code, origin, ...report } = feedback;
3449
+ records.push({
3450
+ ...rest,
3451
+ ...node === void 0 ? {} : { node },
3452
+ feedback: {
3453
+ ...report,
3454
+ ...code === void 0 ? {} : { code },
3455
+ origin: Object.fromEntries(Object.entries(origin).filter(([, v]) => v !== void 0))
3456
+ }
3417
3457
  });
3418
- await followRun(store, found.run, write, {
3419
- follow: args.follow,
3420
- json: args.json,
3421
- signal: abort.signal,
3422
- ...args.node === void 0 ? {} : { node: args.node }
3458
+ }
3459
+ return {
3460
+ records,
3461
+ problems
3462
+ };
3463
+ };
3464
+ // src/runs/publish.ts
3465
+ /** What will be sent, as the person reads it before saying yes. */
3466
+ const formatPreview = (post, plan) => plan.kind === "comment" ? [
3467
+ `A thread for this exists (${plan.url}), so this comment goes on it:`,
3468
+ "",
3469
+ post.comment
3470
+ ] : [
3471
+ "A new thread in the Agent feedback category of mimshins/ork:",
3472
+ "",
3473
+ `Title: ${post.title}`,
3474
+ "",
3475
+ post.body
3476
+ ];
3477
+ // src/commands/feedback-share.ts
3478
+ /**
3479
+ * @file The `ork feedback` subcommands that move feedback off this machine,
3480
+ * only when the person says so (ADR-059): `publish` shares an item about Ork
3481
+ * with its maintainers, through the person's own `gh`, after showing exactly
3482
+ * what will be posted; `export` and `import` move records as JSON lines.
3483
+ */
3484
+ const failed = (message) => {
3485
+ writeErr(message);
3486
+ process.exitCode = 1;
3487
+ };
3488
+ const publish = defineCommand({
3489
+ meta: {
3490
+ name: "publish",
3491
+ description: "Share an item about Ork with its maintainers, after showing exactly what goes"
3492
+ },
3493
+ args: {
3494
+ item: {
3495
+ type: "positional",
3496
+ description: "A feedback item's id, or a prefix of one",
3497
+ required: true
3498
+ },
3499
+ "hide-names": {
3500
+ type: "boolean",
3501
+ description: "Also hide the repositories' and workflows' names",
3502
+ default: false
3503
+ }
3504
+ },
3505
+ run: ({ args }) => withStore(async (store) => {
3506
+ if (!process.stdin.isTTY) {
3507
+ failed("publish shows what it will post and asks you first: run it in a terminal.");
3508
+ return;
3509
+ }
3510
+ const found = findFeedbackItem(store, args.item);
3511
+ if (!found.ok) {
3512
+ failed(found.error);
3513
+ return;
3514
+ }
3515
+ const preview = previewPost(store, found.item, {
3516
+ home: homedir(),
3517
+ registered: registeredProjects(),
3518
+ hideNames: args["hide-names"]
3423
3519
  });
3520
+ if (!preview.ok) {
3521
+ failed(preview.error);
3522
+ return;
3523
+ }
3524
+ const planned = await planPost(preview.post);
3525
+ if (!planned.ok) {
3526
+ failed(planned.error);
3527
+ return;
3528
+ }
3529
+ for (const line of formatPreview(preview.post, planned.plan)) write(line);
3530
+ write("");
3531
+ if (!await confirm("Post it with your own gh login? [y/N] ")) {
3532
+ write("Nothing was sent.");
3533
+ return;
3534
+ }
3535
+ const sent = await sendPost(preview.post, planned.plan);
3536
+ if (!sent.ok) {
3537
+ failed(sent.error);
3538
+ return;
3539
+ }
3540
+ store.setFeedbackStatus(found.item.id, "published");
3541
+ write(`${style("green", "✓")} ${sent.kind === "created" ? "posted" : "added a +1 to"} ${sent.url}`);
3424
3542
  })
3425
3543
  });
3426
- const tree = defineCommand({
3544
+ const exportFeedback = defineCommand({
3427
3545
  meta: {
3428
- name: "tree",
3429
- description: "Show a run's ownership tree with status, tokens and cost"
3546
+ name: "export",
3547
+ description: "Write feedback records as JSON lines, to share them"
3430
3548
  },
3431
3549
  args: {
3432
- run: runArg,
3433
- json: {
3550
+ all: {
3434
3551
  type: "boolean",
3435
- description: "Print JSON",
3552
+ description: "Dismissed items' records too",
3436
3553
  default: false
3554
+ },
3555
+ output: {
3556
+ type: "string",
3557
+ alias: "o",
3558
+ description: "A file to write (default: the terminal)"
3437
3559
  }
3438
3560
  },
3439
3561
  run: ({ args }) => withStore((store) => {
3440
- const found = findRun(store, args.run, process.cwd());
3441
- if (!found.ok) {
3442
- writeErr(found.error);
3443
- process.exitCode = 1;
3562
+ const records = store.exportFeedback({ all: args.all });
3563
+ const text = exportLines(records);
3564
+ if (args.output === void 0) {
3565
+ process.stdout.write(text);
3444
3566
  return;
3445
3567
  }
3446
- const { run } = found;
3447
- const events = store.events(run.id).map((s) => s.event);
3448
- const roots = runTree(events);
3449
- if (args.json) {
3450
- write(JSON.stringify(roots, void 0, 2));
3568
+ writeFileSync(args.output, text);
3569
+ write(`${style("green", "✓")} wrote ${String(records.length)} record(s) to ${args.output}`);
3570
+ })
3571
+ });
3572
+ const importFeedback = defineCommand({
3573
+ meta: {
3574
+ name: "import",
3575
+ description: "Add feedback records from a JSON-lines file"
3576
+ },
3577
+ args: { file: {
3578
+ type: "positional",
3579
+ description: "What `ork feedback export` wrote",
3580
+ required: true
3581
+ } },
3582
+ run: ({ args }) => withStore((store) => {
3583
+ let text;
3584
+ try {
3585
+ text = readFileSync(args.file, "utf8");
3586
+ } catch {
3587
+ failed(`Can't read ${args.file}.`);
3451
3588
  return;
3452
3589
  }
3453
- write(`${style("bold", `run ${run.id}`)} ${style("dim", `of ${run.workflow} · ${runStatus(run, events)} · ${cost(run.costUsd)}`)}`);
3454
- for (const line of formatTree(roots)) write(line);
3590
+ const { records, problems } = parseLines(text);
3591
+ const added = records.filter((r) => store.importFeedback(r)).length;
3592
+ for (const problem of problems) writeErr(problem);
3593
+ write(`${style("green", "✓")} added ${String(added)} record(s); ${String(records.length - added)} were here already${problems.length === 0 ? "" : `; ${String(problems.length)} line(s) left out`}`);
3594
+ if (problems.length > 0) process.exitCode = 1;
3455
3595
  })
3456
3596
  });
3597
+ // src/commands/feedback.ts
3598
+ /**
3599
+ * @file `ork feedback`: the feedback every run recorded, grouped into items
3600
+ * (specs/feedback.md): list and show them, note a run, dismiss and restore an
3601
+ * item, and accept one, as a lesson its workflow reads from then on. What
3602
+ * closes the loop further (apply, triage, split) is in `feedback-loop.ts`,
3603
+ * and what moves feedback off the machine (publish, export, import) in
3604
+ * `feedback-share.ts`.
3605
+ */
3606
+ /** Runs `body` on the item `prefix` names, or says why there's none. */
3607
+ const withItem = (prefix, body) => withStore((store) => {
3608
+ const found = findFeedbackItem(store, prefix);
3609
+ if (!found.ok) {
3610
+ writeErr(found.error);
3611
+ process.exitCode = 1;
3612
+ return;
3613
+ }
3614
+ body(store, found.item);
3615
+ });
3457
3616
  const itemArg = {
3458
3617
  type: "positional",
3459
3618
  description: "A feedback item's id, or a prefix of one",
@@ -3467,7 +3626,7 @@ const json$1 = {
3467
3626
  const feedback = defineCommand({
3468
3627
  meta: {
3469
3628
  name: "feedback",
3470
- description: "What got in the way in your runs, grouped: list, show, note, dismiss, restore"
3629
+ description: "What got in the way in your runs, grouped: list, show, note, triage, accept, apply, publish, export, import"
3471
3630
  },
3472
3631
  subCommands: {
3473
3632
  list: defineCommand({
@@ -3498,22 +3657,16 @@ const feedback = defineCommand({
3498
3657
  item: itemArg,
3499
3658
  json: json$1
3500
3659
  },
3501
- run: ({ args }) => withStore((store) => {
3502
- const found = findFeedbackItem(store, args.item);
3503
- if (!found.ok) {
3504
- writeErr(found.error);
3505
- process.exitCode = 1;
3506
- return;
3507
- }
3508
- const records = store.feedbackRecords(found.item.id);
3660
+ run: ({ args }) => withItem(args.item, (store, item) => {
3661
+ const records = store.feedbackRecords(item.id);
3509
3662
  if (args.json) {
3510
3663
  write(JSON.stringify({
3511
- ...found.item,
3664
+ ...item,
3512
3665
  records
3513
3666
  }, void 0, 2));
3514
3667
  return;
3515
3668
  }
3516
- for (const line of formatFeedbackItem(found.item, records)) write(line);
3669
+ for (const line of formatFeedbackItem(item, records)) write(line);
3517
3670
  })
3518
3671
  }),
3519
3672
  note: defineCommand({
@@ -3564,21 +3717,59 @@ const feedback = defineCommand({
3564
3717
  process.exitCode = 1;
3565
3718
  })
3566
3719
  }),
3720
+ accept: defineCommand({
3721
+ meta: {
3722
+ name: "accept",
3723
+ description: "Accept an item; with --lesson, every later session of its workflow reads it"
3724
+ },
3725
+ args: {
3726
+ item: itemArg,
3727
+ lesson: {
3728
+ type: "boolean",
3729
+ description: "Add it to the workflow's lessons (.ork/lessons/<workflow>.md)",
3730
+ default: false
3731
+ },
3732
+ message: {
3733
+ type: "string",
3734
+ alias: "m",
3735
+ description: "The lesson's words (default: the item's title)"
3736
+ },
3737
+ workflow: {
3738
+ type: "string",
3739
+ description: "Whose lessons, when it was seen in more than one"
3740
+ }
3741
+ },
3742
+ run: ({ args }) => withItem(args.item, (store, item) => {
3743
+ const accepted = acceptItem(store, item, {
3744
+ lesson: args.lesson,
3745
+ text: args.message,
3746
+ workflow: args.workflow
3747
+ }, registeredProjects());
3748
+ if (!accepted.ok) {
3749
+ writeErr(accepted.error);
3750
+ process.exitCode = 1;
3751
+ return;
3752
+ }
3753
+ const [first, ...rest] = accepted.lines;
3754
+ write(`${style("green", "✓")} ${first ?? ""}`);
3755
+ for (const line of rest) write(` ${line}`);
3756
+ })
3757
+ }),
3758
+ apply,
3759
+ triage,
3760
+ split,
3761
+ publish,
3762
+ export: exportFeedback,
3763
+ import: importFeedback,
3567
3764
  dismiss: defineCommand({
3568
3765
  meta: {
3569
3766
  name: "dismiss",
3570
3767
  description: "Dismiss an item: it leaves the list, and stays dismissed"
3571
3768
  },
3572
3769
  args: { item: itemArg },
3573
- run: ({ args }) => withStore((store) => {
3574
- const found = findFeedbackItem(store, args.item);
3575
- if (!found.ok) {
3576
- writeErr(found.error);
3577
- process.exitCode = 1;
3578
- return;
3579
- }
3580
- store.setFeedbackStatus(found.item.id, "dismissed");
3581
- write(`${style("green", "✓")} dismissed ${found.item.id}: ${found.item.title}`);
3770
+ run: ({ args }) => withItem(args.item, (store, item) => {
3771
+ store.setFeedbackStatus(item.id, "dismissed");
3772
+ write(`${style("green", "✓")} dismissed ${item.id}: ${item.title}`);
3582
3773
  })
3583
3774
  }),
3584
3775
  restore: defineCommand({
@@ -3587,19 +3778,128 @@ const feedback = defineCommand({
3587
3778
  description: "Bring a dismissed item back to the list, as new"
3588
3779
  },
3589
3780
  args: { item: itemArg },
3590
- run: ({ args }) => withStore((store) => {
3591
- const found = findFeedbackItem(store, args.item);
3592
- if (!found.ok) {
3593
- writeErr(found.error);
3594
- process.exitCode = 1;
3595
- return;
3596
- }
3597
- store.setFeedbackStatus(found.item.id, "new");
3598
- write(`${style("green", "✓")} restored ${found.item.id}: ${found.item.title}`);
3781
+ run: ({ args }) => withItem(args.item, (store, item) => {
3782
+ store.setFeedbackStatus(item.id, "new");
3783
+ write(`${style("green", "✓")} restored ${item.id}: ${item.title}`);
3599
3784
  })
3600
3785
  })
3601
3786
  }
3602
3787
  });
3788
+ // src/commands/runs.ts
3789
+ /**
3790
+ * @file `ork ps`, `ork logs` and `ork tree`: read what every project's runs
3791
+ * recorded in `~/.ork/state.db` (specs/event-store.md).
3792
+ */
3793
+ const ps = defineCommand({
3794
+ meta: {
3795
+ name: "ps",
3796
+ description: "List this project's runs, newest first"
3797
+ },
3798
+ args: {
3799
+ all: {
3800
+ type: "boolean",
3801
+ description: "Every project's runs",
3802
+ default: false
3803
+ },
3804
+ limit: {
3805
+ type: "string",
3806
+ description: "How many (default 20)"
3807
+ },
3808
+ json: {
3809
+ type: "boolean",
3810
+ description: "Print JSON",
3811
+ default: false
3812
+ }
3813
+ },
3814
+ run: ({ args }) => withStore((store) => {
3815
+ const limit = Number(args.limit ?? 20);
3816
+ const runs = store.runs({
3817
+ limit: Number.isInteger(limit) && limit > 0 ? limit : 20,
3818
+ ...args.all ? {} : { project: placeFor(process.cwd(), store.projects()).root }
3819
+ });
3820
+ const statusOf = (run) => runStatus(run, run.status === "running" ? store.events(run.id).map((s) => s.event) : []);
3821
+ if (args.json) write(JSON.stringify(runs, void 0, 2));
3822
+ else for (const line of formatRuns(runs, args.all, Date.now(), statusOf)) write(line);
3823
+ })
3824
+ });
3825
+ const runArg = {
3826
+ type: "positional",
3827
+ description: "A run id or a prefix of one (default: this project's newest)",
3828
+ required: false
3829
+ };
3830
+ const logs = defineCommand({
3831
+ meta: {
3832
+ name: "logs",
3833
+ description: "Print a run's transcript"
3834
+ },
3835
+ args: {
3836
+ run: runArg,
3837
+ node: {
3838
+ type: "string",
3839
+ description: "Only this node's events"
3840
+ },
3841
+ follow: {
3842
+ type: "boolean",
3843
+ alias: "f",
3844
+ description: "Keep printing a live run's events until it ends",
3845
+ default: false
3846
+ },
3847
+ json: {
3848
+ type: "boolean",
3849
+ description: "Print the stored events as JSON lines",
3850
+ default: false
3851
+ }
3852
+ },
3853
+ run: ({ args }) => withStore(async (store) => {
3854
+ const found = findRun(store, args.run, process.cwd());
3855
+ if (!found.ok) {
3856
+ writeErr(found.error);
3857
+ process.exitCode = 1;
3858
+ return;
3859
+ }
3860
+ const abort = new AbortController();
3861
+ process.once("SIGINT", () => {
3862
+ abort.abort();
3863
+ });
3864
+ await followRun(store, found.run, write, {
3865
+ follow: args.follow,
3866
+ json: args.json,
3867
+ signal: abort.signal,
3868
+ ...args.node === void 0 ? {} : { node: args.node }
3869
+ });
3870
+ })
3871
+ });
3872
+ const tree = defineCommand({
3873
+ meta: {
3874
+ name: "tree",
3875
+ description: "Show a run's ownership tree with status, tokens and cost"
3876
+ },
3877
+ args: {
3878
+ run: runArg,
3879
+ json: {
3880
+ type: "boolean",
3881
+ description: "Print JSON",
3882
+ default: false
3883
+ }
3884
+ },
3885
+ run: ({ args }) => withStore((store) => {
3886
+ const found = findRun(store, args.run, process.cwd());
3887
+ if (!found.ok) {
3888
+ writeErr(found.error);
3889
+ process.exitCode = 1;
3890
+ return;
3891
+ }
3892
+ const { run } = found;
3893
+ const events = store.events(run.id).map((s) => s.event);
3894
+ const roots = runTree(events);
3895
+ if (args.json) {
3896
+ write(JSON.stringify(roots, void 0, 2));
3897
+ return;
3898
+ }
3899
+ write(`${style("bold", `run ${run.id}`)} ${style("dim", `of ${run.workflow} · ${runStatus(run, events)} · ${cost(run.costUsd)}`)}`);
3900
+ for (const line of formatTree(roots)) write(line);
3901
+ })
3902
+ });
3603
3903
  // src/agents/manage.ts
3604
3904
  /**
3605
3905
  * @file What `ork agents` prints and does: the catalog as a table, one agent in
@@ -3904,23 +4204,23 @@ const main = defineCommand({
3904
4204
  ps,
3905
4205
  logs,
3906
4206
  tree,
3907
- usage: () => import("./usage-BIjf9Xdt.mjs").then((module) => module.usage),
3908
- prune: () => import("./usage-BIjf9Xdt.mjs").then((module) => module.prune),
4207
+ usage: () => import("./usage-CT7Z-yjr.mjs").then((module) => module.usage),
4208
+ prune: () => import("./usage-CT7Z-yjr.mjs").then((module) => module.prune),
3909
4209
  decide,
3910
- relay: () => import("./relay-CbK-Wme9.mjs").then((module) => module.relay),
3911
- pause: () => import("./pause-DKACDfV1.mjs").then((module) => module.pause),
3912
- continue: () => import("./pause-DKACDfV1.mjs").then((module) => module.continueRun),
4210
+ relay: () => import("./relay-Do-aNbSD.mjs").then((module) => module.relay),
4211
+ pause: () => import("./pause-BPSmg4yz.mjs").then((module) => module.pause),
4212
+ continue: () => import("./pause-BPSmg4yz.mjs").then((module) => module.continueRun),
3913
4213
  stop,
3914
4214
  chat,
3915
- send: () => import("./send-C6yr04Fe.mjs").then((module) => module.send),
4215
+ send: () => import("./send-DhGBkowy.mjs").then((module) => module.send),
3916
4216
  resume,
3917
4217
  feedback,
3918
4218
  context,
3919
- worktrees,
3920
- wt: worktrees,
3921
- watch: () => import("./watch-B8o4Szd_.mjs").then((module) => module.watch),
3922
- mcp: () => import("./mcp-BWsdQyri.mjs").then((module) => module.mcp),
3923
- ui: () => import("./ui-CZS1XKla.mjs").then((module) => module.ui),
4219
+ worktrees: () => import("./worktrees-DHl80k96.mjs").then((module) => module.worktrees),
4220
+ wt: () => import("./worktrees-DHl80k96.mjs").then((module) => module.worktrees),
4221
+ watch: () => import("./watch-0Qr2UfCQ.mjs").then((module) => module.watch),
4222
+ mcp: () => import("./mcp-CHVKKg3B.mjs").then((module) => module.mcp),
4223
+ ui: () => import("./ui-DhutUMgQ.mjs").then((module) => module.ui),
3924
4224
  doctor,
3925
4225
  upgrade
3926
4226
  }