@krapht/ork 0.1.0 → 0.2.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.
package/README.md CHANGED
@@ -3,10 +3,12 @@
3
3
  The `ork` command: orchestrate Claude agents on your machine with hierarchical,
4
4
  serializable workflows.
5
5
 
6
- > **Pre-alpha.** `ork init`, `ork validate`, `ork doctor` and `ork run` (each
7
- > writing node in its own git worktree) work today; delegation and the context
8
- > compiler are being built. See the
9
- > [project README](https://github.com/mimshins/ork#readme).
6
+ > **Pre-alpha.** The workflow format, `ork run` with delegation and worktrees,
7
+ > the context compiler, agents, run logs, decisions from any terminal, messages
8
+ > between nodes, chat, resume, claims and local feedback work today; a web UI
9
+ > comes next. Expect changes between 0.x releases. See the
10
+ > [project README](https://github.com/mimshins/ork#readme) and the
11
+ > [guides](https://github.com/mimshins/ork/tree/main/docs#readme).
10
12
 
11
13
  ```sh
12
14
  npm install --global @krapht/ork
@@ -19,12 +21,14 @@ ork run hello --input task="add a README"
19
21
  ork worktrees # the worktrees and branches runs left; `clean` tidies up
20
22
  ork run hello --node lead --input task="add a README" --in-place
21
23
  # run one Claude node on its own
24
+ ork upgrade # a newer version? upgrade the way you installed Ork
22
25
  ```
23
26
 
24
27
  `--driver sdk` (the default) needs `ANTHROPIC_API_KEY` or a cloud provider;
25
28
  `--driver cli` uses your own logged-in `claude`.
26
29
 
27
30
  Ork never stores or asks for credentials: it uses the tools you already have,
28
- signed in with your own accounts.
31
+ signed in with your own accounts. Once a day it asks the npm registry whether a
32
+ newer version is out (`ORK_NO_UPDATE_CHECK=1` turns that off).
29
33
 
30
34
  Requires Node.js 24.20 or later. MIT licensed.
package/dist/bin.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { $ as parseWorkflow, A as planAgentMigration, B as PROJECT_MODES, C as fingerprint, D as parseTemplate, E as maskSecrets, F as toolsInUse, G as placeFor, H as inRepoPlace, I as createAgent, J as shownPath, K as projectsHome, L as deleteAgent, M as planMoveInto, N as linkTools, O as renderTemplate, P as toolLinks, R as catalogAgent, S as createFeedback, T as maskReplacer, U as newProjectId, V as hasInRepoFiles, W as placeFiles, X as inside, Y as workflowDirOf, Z as projectRootOf, _ as gitRulesOf, a as removeWorktree, at as INTEGRATIONS, b as FEEDBACK_SEVERITIES, c as lines, ct as AGENT_TOOL_LAYOUTS, dt as firstLine$1, et as serializeWorkflow, f as userMessageText, g as compileWorkflow, h as resolvePractices, i as inspectProject, it as isToolNode, j as applySteps, k as contextDiagnostics, l as startRun, lt as ORK_LAYOUT, m as practiceInstructions, n as resolveBase, nt as settingsOf, o as git, ot as TOOL_PREFIX, p as runScope, q as runDirOf, rt as isClaudeNode, s as gitOk, st as integrationFor, t as createWorkspace, tt as WorkflowSchema, u as planNodeSession, ut as CLAUDE_DRIVERS, v as loadProject, w as targetOf, x as FEEDBACK_TARGETS, y as FEEDBACK_KINDS, z as loadAgents } from "./src-CqNlvqV_.mjs";
2
+ import { A as contextDiagnostics, B as loadAgents, C as createFeedback, D as maskSecrets, E as maskReplacer, F as toolLinks, G as placeFiles, H as hasInRepoFiles, I as toolsInUse, J as runDirOf, K as placeFor, L as createAgent, M as applySteps, N as planMoveInto, O as parseTemplate, P as linkTools, Q as projectRootOf, R as deleteAgent, S as FEEDBACK_TARGETS, T as targetOf, U as inRepoPlace, V as PROJECT_MODES, W as newProjectId, X as workflowDirOf, Y as shownPath, Z as inside, _ as compileWorkflow, a as removeWorktree, at as isToolNode, b as FEEDBACK_KINDS, c as lines, ct as integrationFor, d as planNodeSession, dt as CLAUDE_DRIVERS, et as parseWorkflow, ft as firstLine$1, g as resolvePractices, h as practiceInstructions, i as inspectProject, it as isClaudeNode, j as planAgentMigration, k as renderTemplate, l as detectInstaller, lt as AGENT_TOOL_LAYOUTS, m as runScope, n as resolveBase, nt as WorkflowSchema, o as git, ot as INTEGRATIONS, p as userMessageText, q as projectsHome, rt as settingsOf, s as gitOk, st as TOOL_PREFIX, t as createWorkspace, tt as serializeWorkflow, u as startRun, ut as ORK_LAYOUT, v as gitRulesOf, w as fingerprint, x as FEEDBACK_SEVERITIES, y as loadProject, z as catalogAgent } from "./src-CJ_zF0Zj.mjs";
3
3
  import { defineCommand, runMain } from "citty";
4
4
  import { createInterface } from "node:readline/promises";
5
5
  import path from "node:path";
6
6
  import { stringify } from "yaml";
7
- import { accessSync, appendFileSync, constants, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
7
+ import { accessSync, appendFileSync, constants, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { createHash, randomUUID } from "node:crypto";
9
9
  import { homedir } from "node:os";
10
10
  import { createConnection, createServer } from "node:net";
@@ -13,8 +13,6 @@ import { DatabaseSync } from "node:sqlite";
13
13
  import { execFile, spawn } from "node:child_process";
14
14
  import { styleText } from "node:util";
15
15
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
16
- // src/__build_info__.ts
17
- const ORK_VERSION = "0.1.0";
18
16
  // ../core/src/control/queue.ts
19
17
  const PERMISSION_OPTIONS = [{
20
18
  id: "allow",
@@ -881,7 +879,7 @@ const MIGRATIONS = [
881
879
  );`
882
880
  ];
883
881
  /** A text column, or `""` when it is null. */
884
- const text = (row, key) => {
882
+ const text$1 = (row, key) => {
885
883
  const value = row?.[key];
886
884
  return typeof value === "string" ? value : "";
887
885
  };
@@ -914,23 +912,23 @@ const kindOf = (value) => value === "permission" ? "permission" : "question";
914
912
  const statusOf$1 = (value) => value === "answered" || value === "withdrawn" ? value : "open";
915
913
  const decisionOf = (row) => {
916
914
  const record = {
917
- id: text(row, "id"),
918
- runId: text(row, "run_id"),
919
- node: text(row, "node"),
920
- kind: kindOf(text(row, "kind")),
921
- question: text(row, "question"),
922
- options: choicesOf(text(row, "options") || "[]"),
923
- openedAt: text(row, "opened_at"),
924
- status: statusOf$1(text(row, "status"))
915
+ id: text$1(row, "id"),
916
+ runId: text$1(row, "run_id"),
917
+ node: text$1(row, "node"),
918
+ kind: kindOf(text$1(row, "kind")),
919
+ question: text$1(row, "question"),
920
+ options: choicesOf(text$1(row, "options") || "[]"),
921
+ openedAt: text$1(row, "opened_at"),
922
+ status: statusOf$1(text$1(row, "status"))
925
923
  };
926
924
  const optional = {
927
- recommended: text(row, "recommended"),
928
- context: text(row, "context"),
929
- answer: text(row, "answer"),
930
- answeredAt: text(row, "answered_at")
925
+ recommended: text$1(row, "recommended"),
926
+ context: text$1(row, "context"),
927
+ answer: text$1(row, "answer"),
928
+ answeredAt: text$1(row, "answered_at")
931
929
  };
932
930
  for (const [key, value] of Object.entries(optional)) if (value !== "") Object.assign(record, { [key]: value });
933
- const by = text(row, "answered_by");
931
+ const by = text$1(row, "answered_by");
934
932
  if (by === "terminal" || by === "decide") record.answeredBy = by;
935
933
  return record;
936
934
  };
@@ -982,12 +980,12 @@ const decisionQueries = (db) => {
982
980
  const at = (/* @__PURE__ */ new Date()).toISOString();
983
981
  return pending.all(runId).flatMap((row) => {
984
982
  handled.run(at, Number(row["id"]));
985
- const kind = text(row, "kind");
983
+ const kind = text$1(row, "kind");
986
984
  if (kind === "stop") return [{ kind }];
987
985
  if (kind === "message") return [{
988
986
  kind,
989
- node: text(row, "node"),
990
- body: text(row, "body")
987
+ node: text$1(row, "node"),
988
+ body: text$1(row, "body")
991
989
  }];
992
990
  return [];
993
991
  });
@@ -1012,7 +1010,7 @@ const SOURCES = [
1012
1010
  const oneOf$1 = (values, value, fallback) => values.find((v) => v === value) ?? fallback;
1013
1011
  /** A JSON array column of strings, without nulls. */
1014
1012
  const strings = (row, key) => {
1015
- const parsed = JSON.parse(text(row, key) || "[]");
1013
+ const parsed = JSON.parse(text$1(row, key) || "[]");
1016
1014
  return Array.isArray(parsed) ? parsed.filter((v) => typeof v === "string" && v !== "") : [];
1017
1015
  };
1018
1016
  const ORIGIN_COLUMNS = [
@@ -1026,46 +1024,46 @@ const ORIGIN_COLUMNS = [
1026
1024
  const recordOf$2 = (row) => {
1027
1025
  const origin = {};
1028
1026
  for (const [key, column] of ORIGIN_COLUMNS) {
1029
- const value = text(row, column);
1027
+ const value = text$1(row, column);
1030
1028
  if (value !== "") origin[key] = value;
1031
1029
  }
1032
1030
  const feedback = {
1033
- source: oneOf$1(SOURCES, text(row, "source"), "system"),
1034
- kind: oneOf$1(FEEDBACK_KINDS, text(row, "kind"), "idea"),
1035
- target: oneOf$1(FEEDBACK_TARGETS, text(row, "target"), "workflow"),
1036
- severity: oneOf$1(FEEDBACK_SEVERITIES, text(row, "severity"), "medium"),
1037
- title: text(row, "title"),
1038
- body: text(row, "body"),
1031
+ source: oneOf$1(SOURCES, text$1(row, "source"), "system"),
1032
+ kind: oneOf$1(FEEDBACK_KINDS, text$1(row, "kind"), "idea"),
1033
+ target: oneOf$1(FEEDBACK_TARGETS, text$1(row, "target"), "workflow"),
1034
+ severity: oneOf$1(FEEDBACK_SEVERITIES, text$1(row, "severity"), "medium"),
1035
+ title: text$1(row, "title"),
1036
+ body: text$1(row, "body"),
1039
1037
  evidence: strings(row, "evidence"),
1040
- fingerprint: text(row, "fingerprint"),
1038
+ fingerprint: text$1(row, "fingerprint"),
1041
1039
  origin
1042
1040
  };
1043
- const code = text(row, "code");
1041
+ const code = text$1(row, "code");
1044
1042
  if (code !== "") feedback.code = code;
1045
1043
  const record = {
1046
1044
  id: Number(row?.["id"]),
1047
- runId: text(row, "run_id"),
1045
+ runId: text$1(row, "run_id"),
1048
1046
  seq: Number(row?.["seq"]),
1049
- at: text(row, "at"),
1047
+ at: text$1(row, "at"),
1050
1048
  feedback
1051
1049
  };
1052
- const node = text(row, "node");
1050
+ const node = text$1(row, "node");
1053
1051
  if (node !== "") record.node = node;
1054
1052
  return record;
1055
1053
  };
1056
1054
  const itemOf = (row) => {
1057
1055
  const severities = strings(row, "severities");
1058
1056
  return {
1059
- id: text(row, "fingerprint"),
1060
- status: oneOf$1(STATUSES$1, text(row, "status"), "new"),
1061
- title: text(row, "title"),
1062
- target: oneOf$1(FEEDBACK_TARGETS, text(row, "target"), "workflow"),
1063
- kind: oneOf$1(FEEDBACK_KINDS, text(row, "kind"), "idea"),
1057
+ id: text$1(row, "fingerprint"),
1058
+ status: oneOf$1(STATUSES$1, text$1(row, "status"), "new"),
1059
+ title: text$1(row, "title"),
1060
+ target: oneOf$1(FEEDBACK_TARGETS, text$1(row, "target"), "workflow"),
1061
+ kind: oneOf$1(FEEDBACK_KINDS, text$1(row, "kind"), "idea"),
1064
1062
  severity: FEEDBACK_SEVERITIES.toReversed().find((s) => severities.includes(s)) ?? "medium",
1065
1063
  sources: strings(row, "sources").flatMap((s) => SOURCES.filter((known) => known === s)),
1066
1064
  count: Number(row?.["count"]),
1067
- firstSeen: text(row, "first_seen"),
1068
- lastSeen: text(row, "last_seen"),
1065
+ firstSeen: text$1(row, "first_seen"),
1066
+ lastSeen: text$1(row, "last_seen"),
1069
1067
  runs: strings(row, "runs"),
1070
1068
  workflows: strings(row, "workflows"),
1071
1069
  models: strings(row, "models")
@@ -1113,11 +1111,11 @@ const feedbackQueries = (db) => {
1113
1111
  * (`ork migrate`) with its runs' workflow files.
1114
1112
  */
1115
1113
  const recordOf$1 = (row) => ({
1116
- id: text(row, "id"),
1117
- root: text(row, "root"),
1118
- mode: PROJECT_MODES.find((m) => m === text(row, "mode")) ?? "in-repo",
1119
- dir: text(row, "dir"),
1120
- registeredAt: text(row, "registered_at")
1114
+ id: text$1(row, "id"),
1115
+ root: text$1(row, "root"),
1116
+ mode: PROJECT_MODES.find((m) => m === text$1(row, "mode")) ?? "in-repo",
1117
+ dir: text$1(row, "dir"),
1118
+ registeredAt: text$1(row, "registered_at")
1121
1119
  });
1122
1120
  const projectQueries = (db) => {
1123
1121
  const all = db.prepare("SELECT * FROM projects ORDER BY id");
@@ -1185,16 +1183,16 @@ const STATUSES = [
1185
1183
  const statusOf = (value) => STATUSES.find((s) => s === value) ?? "failed";
1186
1184
  const recordOf = (row) => {
1187
1185
  const record = {
1188
- id: text(row, "id"),
1189
- workflow: text(row, "workflow"),
1190
- file: text(row, "file"),
1191
- project: text(row, "project"),
1192
- status: statusOf(text(row, "status")),
1186
+ id: text$1(row, "id"),
1187
+ workflow: text$1(row, "workflow"),
1188
+ file: text$1(row, "file"),
1189
+ project: text$1(row, "project"),
1190
+ status: statusOf(text$1(row, "status")),
1193
1191
  pid: Number(row?.["pid"]),
1194
- startedAt: text(row, "started_at"),
1192
+ startedAt: text$1(row, "started_at"),
1195
1193
  costUsd: Number(row?.["cost_usd"])
1196
1194
  };
1197
- const finished = text(row, "finished_at");
1195
+ const finished = text$1(row, "finished_at");
1198
1196
  if (finished !== "") record.finishedAt = finished;
1199
1197
  return record;
1200
1198
  };
@@ -1270,8 +1268,8 @@ const openStore = (file = storePath()) => {
1270
1268
  events: (runId, after = 0) => eventsAfter.all(runId, after).map((r) => ({
1271
1269
  runId,
1272
1270
  seq: Number(r["seq"]),
1273
- at: text(r, "at"),
1274
- event: eventOf(text(r, "data"))
1271
+ at: text$1(r, "at"),
1272
+ event: eventOf(text$1(r, "data"))
1275
1273
  })),
1276
1274
  close: () => {
1277
1275
  db.close();
@@ -1348,6 +1346,144 @@ const alive = (pid) => {
1348
1346
  };
1349
1347
  /** A run's status, with `crashed` for a `running` run whose process is gone. */
1350
1348
  const liveStatus = (run) => run.status === "running" && !alive(run.pid) ? "crashed" : run.status;
1349
+ // ../core/src/update/version.ts
1350
+ const VERSION = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/u;
1351
+ /** `1.2.3`, `v1.2.3-beta.1` and the like; `null` for anything else. */
1352
+ const parseVersion = (text) => {
1353
+ const match = VERSION.exec(text.trim());
1354
+ if (match === null) return null;
1355
+ const [, major = "0", minor = "0", patch = "0", pre] = match;
1356
+ return {
1357
+ core: [
1358
+ Number(major),
1359
+ Number(minor),
1360
+ Number(patch)
1361
+ ],
1362
+ pre: pre === void 0 ? [] : pre.split(".")
1363
+ };
1364
+ };
1365
+ const NUMERIC = /^\d+$/u;
1366
+ /** One pre-release identifier against another: numbers sort below words. */
1367
+ const compareIdentifier = (a, b) => {
1368
+ const an = NUMERIC.test(a);
1369
+ const bn = NUMERIC.test(b);
1370
+ if (an && bn) return Number(a) - Number(b);
1371
+ if (an !== bn) return an ? -1 : 1;
1372
+ return a < b ? -1 : Number(a > b);
1373
+ };
1374
+ const comparePre = (a, b) => {
1375
+ if (a.length === 0 || b.length === 0) return b.length - a.length;
1376
+ for (let i = 0; i < Math.min(a.length, b.length); i += 1) {
1377
+ const order = compareIdentifier(a[i] ?? "", b[i] ?? "");
1378
+ if (order !== 0) return order;
1379
+ }
1380
+ return a.length - b.length;
1381
+ };
1382
+ /** Semver precedence: negative when `a` is older, 0 when equal. */
1383
+ const compareVersions = (a, b) => {
1384
+ for (let i = 0; i < 3; i += 1) {
1385
+ const order = (a.core[i] ?? 0) - (b.core[i] ?? 0);
1386
+ if (order !== 0) return order;
1387
+ }
1388
+ return comparePre(a.pre, b.pre);
1389
+ };
1390
+ /** Whether `candidate` is newer than `current`; false when either is invalid. */
1391
+ const isNewer = (candidate, current) => {
1392
+ const a = parseVersion(candidate);
1393
+ const b = parseVersion(current);
1394
+ return a !== null && b !== null && compareVersions(a, b) > 0;
1395
+ };
1396
+ const updateCachePath = (home = homedir()) => path.join(home, ".ork", "update-check.json");
1397
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1398
+ const text = (record, key) => {
1399
+ const value = record[key];
1400
+ return typeof value === "string" ? value : void 0;
1401
+ };
1402
+ const notifiedOf = (value) => {
1403
+ if (!isRecord(value)) return void 0;
1404
+ const version = text(value, "version");
1405
+ const at = text(value, "at");
1406
+ return version === void 0 || at === void 0 ? void 0 : {
1407
+ version,
1408
+ at
1409
+ };
1410
+ };
1411
+ /** The cache, or `null` when there is none or it cannot be read. */
1412
+ const readUpdateCache = (file) => {
1413
+ let parsed;
1414
+ try {
1415
+ parsed = JSON.parse(readFileSync(file, "utf8"));
1416
+ } catch {
1417
+ return null;
1418
+ }
1419
+ if (!isRecord(parsed)) return null;
1420
+ const checkedAt = text(parsed, "checkedAt");
1421
+ if (checkedAt === void 0) return null;
1422
+ const latest = text(parsed, "latest");
1423
+ const notified = notifiedOf(parsed["notified"]);
1424
+ return {
1425
+ checkedAt,
1426
+ ...latest === void 0 ? {} : { latest },
1427
+ ...notified === void 0 ? {} : { notified }
1428
+ };
1429
+ };
1430
+ /** Writes the cache whole (a temporary file, then a rename). */
1431
+ const writeUpdateCache = (file, cache) => {
1432
+ mkdirSync(path.dirname(file), { recursive: true });
1433
+ const temporary = `${file}.${process.pid}.tmp`;
1434
+ writeFileSync(temporary, `${JSON.stringify(cache, null, 2)}\n`);
1435
+ renameSync(temporary, file);
1436
+ };
1437
+ const age = (iso, now) => {
1438
+ const time = Date.parse(iso);
1439
+ return Number.isNaN(time) ? Infinity : now.getTime() - time;
1440
+ };
1441
+ /** Whether it is time to ask the registry again. */
1442
+ const checkDue = (cache, now) => cache === null || age(cache.checkedAt, now) >= 864e5;
1443
+ /** The package's `latest` version on the registry, or `null` on any failure. */
1444
+ const fetchLatest = async (name, options = {}) => {
1445
+ const registry = (options.registry ?? "https://registry.npmjs.org").replace(/\/+$/u, "");
1446
+ const get = options.fetch ?? globalThis.fetch;
1447
+ try {
1448
+ const response = await get(`${registry}/-/package/${name}/dist-tags`, {
1449
+ headers: { accept: "application/json" },
1450
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5e3)
1451
+ });
1452
+ if (!response.ok) return null;
1453
+ const tags = await response.json();
1454
+ return (isRecord(tags) ? text(tags, "latest") : void 0) ?? null;
1455
+ } catch {
1456
+ return null;
1457
+ }
1458
+ };
1459
+ /**
1460
+ * Asks the registry and records the answer. A failed check is recorded too, so
1461
+ * an offline machine is not asked again until the interval passes; the last
1462
+ * known `latest` is kept.
1463
+ */
1464
+ const refreshUpdateCache = async (file, name, now, options = {}) => {
1465
+ const before = readUpdateCache(file);
1466
+ const latest = await fetchLatest(name, options) ?? before?.latest;
1467
+ const cache = {
1468
+ ...before,
1469
+ checkedAt: now.toISOString(),
1470
+ ...latest === void 0 ? {} : { latest }
1471
+ };
1472
+ writeUpdateCache(file, cache);
1473
+ return cache;
1474
+ };
1475
+ /** The newer version the cache knows of, if any. */
1476
+ const newerVersion = (cache, current) => cache?.latest !== void 0 && isNewer(cache.latest, current) ? cache.latest : null;
1477
+ /**
1478
+ * The version to tell the user about now: newer than theirs, and not already
1479
+ * told today. `null` when there is nothing to say.
1480
+ */
1481
+ const pendingNotice = (cache, current, now) => {
1482
+ const newer = newerVersion(cache, current);
1483
+ if (newer === null) return null;
1484
+ const told = cache?.notified;
1485
+ return told?.version === newer && age(told.at, now) < 864e5 ? null : newer;
1486
+ };
1351
1487
  // ../core/src/worktree/list.ts
1352
1488
  /**
1353
1489
  * @file What `ork worktrees` shows and cleans: every worktree and branch Ork's
@@ -2817,6 +2953,15 @@ const missingPrograms = (workflow, installed = (program) => onPath(program)) =>
2817
2953
  }
2818
2954
  return [...missing.values()];
2819
2955
  };
2956
+ // src/__build_info__.ts
2957
+ /**
2958
+ * @file The published package's name and version, bound to
2959
+ * `packages/cli/package.json`. Do not edit by hand: `pnpm changesets:version`
2960
+ * and `pnpm build` rewrite it (`tools/release/build-info.ts`), and a test
2961
+ * fails when it is stale.
2962
+ */
2963
+ const ORK_NAME = "@krapht/ork";
2964
+ const ORK_VERSION = "0.2.0";
2820
2965
  // src/run/origin.ts
2821
2966
  /**
2822
2967
  * @file Where a run's feedback comes from, beyond what its events say: Ork's
@@ -3190,7 +3335,7 @@ const flagValues = (rawArgs, flag) => {
3190
3335
  };
3191
3336
  const isDriver = (value) => CLAUDE_DRIVERS.some((d) => d.id === value);
3192
3337
  /** Loads the drivers only when a run starts, so other commands stay fast. */
3193
- const loadRuntime = async (driver) => (await import("./src-CDkj_-3F.mjs")).createRuntime(driver, process.env);
3338
+ const loadRuntime = async (driver) => (await import("./src-DAsOYtoR.mjs")).createRuntime(driver, process.env);
3194
3339
  /** Ctrl-C asks the run to stop. */
3195
3340
  const onInterrupt = (stop) => {
3196
3341
  process.once("SIGINT", () => {
@@ -3942,6 +4087,428 @@ const resume = defineCommand({
3942
4087
  }
3943
4088
  }
3944
4089
  });
4090
+ // src/update/notify.ts
4091
+ /**
4092
+ * @file The new-version notice (ADR-038). At most once a day a command starts a
4093
+ * detached copy of `ork` that asks the npm registry for the latest version
4094
+ * and records it in `~/.ork/update-check.json`, so no command waits on the
4095
+ * network; a later command, when a newer version is known, ends with one line
4096
+ * on stderr (once a day). Off in CI, without a terminal, with `--json`, and
4097
+ * with `ORK_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1`.
4098
+ */
4099
+ /** Set in the background copy of `ork` that only runs the check. */
4100
+ const UPDATE_CHILD_ENV = "ORK_UPDATE_CHECK_CHILD";
4101
+ /** A registry to ask instead of npm's, e.g. a company mirror. */
4102
+ const UPDATE_REGISTRY_ENV = "ORK_UPDATE_REGISTRY";
4103
+ const on = (value) => value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
4104
+ /** Why the automatic check is off for this command, or `null` when it is on. */
4105
+ const updateCheckOff = (env, argv, terminal) => {
4106
+ if (on(env["ORK_NO_UPDATE_CHECK"]) || on(env["NO_UPDATE_NOTIFIER"])) return "turned off";
4107
+ if (on(env["CI"])) return "in CI";
4108
+ if (on(env["ORK_DETACHED"]) || !terminal) return "no terminal";
4109
+ if (argv.includes("--json")) return "JSON output";
4110
+ if (argv[0] === "upgrade") return "ork upgrade checks for itself";
4111
+ return null;
4112
+ };
4113
+ const registryOf = (env) => {
4114
+ const registry = env[UPDATE_REGISTRY_ENV];
4115
+ return registry === void 0 || registry === "" ? void 0 : registry;
4116
+ };
4117
+ /** The notice's text. */
4118
+ const updateNotice = (latest, current) => `Ork ${latest} is out (you have ${current}). Upgrade with: ork upgrade`;
4119
+ /**
4120
+ * Starts the background check when one is due. Records the attempt first, so
4121
+ * commands started in the next moments do not start one too.
4122
+ */
4123
+ const startUpdateCheck = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
4124
+ try {
4125
+ const cache = readUpdateCache(file);
4126
+ if (!checkDue(cache, now)) return;
4127
+ writeUpdateCache(file, {
4128
+ ...cache,
4129
+ checkedAt: now.toISOString()
4130
+ });
4131
+ const child = spawn(process.execPath, [...process.execArgv, process.argv[1] ?? ""], {
4132
+ detached: true,
4133
+ stdio: "ignore",
4134
+ env: {
4135
+ ...process.env,
4136
+ [UPDATE_CHILD_ENV]: "1"
4137
+ }
4138
+ });
4139
+ child.on("error", () => {});
4140
+ child.unref();
4141
+ } catch {}
4142
+ };
4143
+ /** The background copy's whole job: ask the registry, record the answer. */
4144
+ const runUpdateChild = async () => {
4145
+ const registry = registryOf(process.env);
4146
+ await refreshUpdateCache(updateCachePath(), ORK_NAME, /* @__PURE__ */ new Date(), registry === void 0 ? {} : { registry });
4147
+ };
4148
+ /** Prints the notice when a newer version is known and not told today. */
4149
+ const showUpdateNotice = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
4150
+ try {
4151
+ const cache = readUpdateCache(file);
4152
+ const latest = pendingNotice(cache, ORK_VERSION, now);
4153
+ if (cache === null || latest === null) return;
4154
+ const text = updateNotice(latest, ORK_VERSION);
4155
+ process.stderr.write(`\n${styleText("yellow", text, { stream: process.stderr })}\n`);
4156
+ writeUpdateCache(file, {
4157
+ ...cache,
4158
+ notified: {
4159
+ version: latest,
4160
+ at: now.toISOString()
4161
+ }
4162
+ });
4163
+ } catch {}
4164
+ };
4165
+ // src/update/upgrade.ts
4166
+ /**
4167
+ * @file What `ork upgrade` does (ADR-038): compares the registry's latest
4168
+ * version with this one, works out how Ork was installed, and gives the
4169
+ * command to run (a global npm, pnpm, yarn, bun or Volta install) or advice
4170
+ * (a clone, `npx`); runs the command when the user agrees. Also the version
4171
+ * line of `ork doctor`, read from the cache without asking the network.
4172
+ */
4173
+ const REPOSITORY = "https://github.com/mimshins/ork";
4174
+ /** The GitHub release page of a version, with its changes. */
4175
+ const releaseNotes = (name, version) => `${REPOSITORY}/releases/tag/${encodeURIComponent(`${name}@${version}`)}`;
4176
+ /** The running script's real path, which tells how Ork was installed. */
4177
+ const runningScript = () => {
4178
+ const script = process.argv[1] ?? "";
4179
+ try {
4180
+ return realpathSync(script);
4181
+ } catch {
4182
+ return script;
4183
+ }
4184
+ };
4185
+ const planUpgrade = (options) => {
4186
+ const { latest, name } = options;
4187
+ if (latest === null) return { kind: "unreachable" };
4188
+ if (!isNewer(latest, options.current)) return { kind: "latest" };
4189
+ const spec = `${name}@${latest}`;
4190
+ const installer = detectInstaller(options.script, options.installers);
4191
+ if (installer?.command !== void 0) return {
4192
+ kind: "command",
4193
+ latest,
4194
+ installer,
4195
+ argv: installer.command(spec)
4196
+ };
4197
+ return {
4198
+ kind: "advice",
4199
+ latest,
4200
+ text: installer?.advice?.(spec, options.script.replaceAll("\\", "/")) ?? `Ork could not tell how it was installed. With npm: npm install --global ${spec}`
4201
+ };
4202
+ };
4203
+ /** Runs the installer's command on this terminal; resolves its exit code. */
4204
+ const runInstall = (argv) => new Promise((resolve) => {
4205
+ const [program = "", ...args] = argv;
4206
+ const child = spawn(program, args, { stdio: "inherit" });
4207
+ child.on("error", (error) => {
4208
+ process.stderr.write(`Could not run ${program}: ${error.message}\n`);
4209
+ resolve(127);
4210
+ });
4211
+ child.on("close", (code) => {
4212
+ resolve(code ?? 1);
4213
+ });
4214
+ });
4215
+ /** `ork doctor`'s line for Ork itself, from the last check. */
4216
+ const versionCheck = (current, cache, checksOff) => {
4217
+ const newer = newerVersion(cache, current);
4218
+ if (newer !== null) return {
4219
+ name: "ork",
4220
+ status: "warn",
4221
+ detail: `${current}; ${newer} is out`,
4222
+ hint: "Run ork upgrade."
4223
+ };
4224
+ return {
4225
+ name: "ork",
4226
+ status: "ok",
4227
+ detail: `${current} (${cache?.latest === void 0 ? checksOff ? "update check off" : "not checked for updates yet" : `the latest as of ${cache.checkedAt.slice(0, 10)}`})`
4228
+ };
4229
+ };
4230
+ // src/doctor/checks.ts
4231
+ /**
4232
+ * @file The checks behind `ork doctor`: Ork's own version against the last
4233
+ * update check, and each tool's presence and login status, through an
4234
+ * injectable `exec` so tests need no real tools. Never prints a token or an
4235
+ * account.
4236
+ */
4237
+ const TIMEOUT_MS = 15e3;
4238
+ const exec = (program, args) => new Promise((resolve) => {
4239
+ execFile(program, args, {
4240
+ timeout: TIMEOUT_MS,
4241
+ encoding: "utf8"
4242
+ }, (error, stdout) => {
4243
+ resolve({
4244
+ ok: error === null,
4245
+ stdout
4246
+ });
4247
+ });
4248
+ });
4249
+ /** Ork's own version, against the last update check (no network). */
4250
+ const orkCheck = () => versionCheck(ORK_VERSION, readUpdateCache(updateCachePath()), updateCheckOff(process.env, [], true) !== null);
4251
+ const MIN_NODE = [24, 20];
4252
+ const checkNode = (version) => {
4253
+ const [major = 0, minor = 0] = version.split(".").map(Number);
4254
+ const [minMajor, minMinor] = MIN_NODE;
4255
+ return major > minMajor || major === minMajor && minor >= minMinor ? {
4256
+ name: "node",
4257
+ status: "ok",
4258
+ detail: `v${version}`
4259
+ } : {
4260
+ name: "node",
4261
+ status: "fail",
4262
+ detail: `v${version}`,
4263
+ hint: `Ork needs Node ${MIN_NODE.join(".")} or later.`
4264
+ };
4265
+ };
4266
+ const checkGit = async (run) => {
4267
+ const version = await run("git", ["--version"]);
4268
+ if (!version.ok) return [{
4269
+ name: "git",
4270
+ status: "fail",
4271
+ detail: "not found",
4272
+ hint: "Install git; Ork gives each writing node its own worktree."
4273
+ }];
4274
+ const inside = await run("git", ["rev-parse", "--is-inside-work-tree"]);
4275
+ return [{
4276
+ name: "git",
4277
+ status: "ok",
4278
+ detail: firstLine$1(version.stdout)
4279
+ }, inside.ok ? {
4280
+ name: "git repo",
4281
+ status: "ok",
4282
+ detail: "inside a work tree"
4283
+ } : {
4284
+ name: "git repo",
4285
+ status: "warn",
4286
+ detail: "not a git repository",
4287
+ hint: "Run Ork inside a git repository so nodes can use worktrees."
4288
+ }];
4289
+ };
4290
+ /**
4291
+ * Runs every check: Node and git, then every registered Claude driver and
4292
+ * integration (ADR-022). Node, git, and at least one driver are required;
4293
+ * integrations are optional.
4294
+ */
4295
+ const runChecks = async (run, env, nodeVersion) => {
4296
+ const [git, drivers, tools] = await Promise.all([
4297
+ checkGit(run),
4298
+ Promise.all(CLAUDE_DRIVERS.map((d) => d.doctor(run, env))),
4299
+ Promise.all(INTEGRATIONS.flatMap((i) => i.doctor === void 0 ? [] : [i.doctor(run, env)]))
4300
+ ]);
4301
+ const results = [
4302
+ checkNode(nodeVersion),
4303
+ ...git,
4304
+ ...drivers,
4305
+ ...tools
4306
+ ];
4307
+ return {
4308
+ results,
4309
+ ok: drivers.some((d) => d.status === "ok") && !results.some((r) => r.status === "fail")
4310
+ };
4311
+ };
4312
+ // src/project/link.ts
4313
+ /**
4314
+ * @file What `ork link` and `ork doctor` say about a project's tool folders:
4315
+ * one line per folder (linked, created, kept as a real folder, pointing
4316
+ * elsewhere, broken), and one doctor check per tool the project uses, only
4317
+ * once the project has `.ork/`.
4318
+ */
4319
+ const DESCRIBE = {
4320
+ linked: (link) => `${link.path} → ${link.origin}`,
4321
+ missing: (link) => `${link.path} → ${link.origin}`,
4322
+ broken: (link) => `${link.path} → ${link.target ?? "?"}, which does not exist`,
4323
+ elsewhere: (link) => `${link.path} → ${link.target ?? "?"}, not ${link.origin}`,
4324
+ folder: (link) => `${link.path} is a real folder, left as it is`
4325
+ };
4326
+ const describe = (link) => DESCRIBE[link.state](link);
4327
+ const LABEL = {
4328
+ linked: style("dim", "ok "),
4329
+ missing: style("yellow", "missing"),
4330
+ broken: style("red", "broken "),
4331
+ elsewhere: style("yellow", "kept "),
4332
+ folder: style("yellow", "kept ")
4333
+ };
4334
+ /** One line per folder; `created` marks the links this call made. */
4335
+ const formatLinks = (links, created = []) => links.map((link) => {
4336
+ return `${created.includes(link.path) ? style("green", "linked ") : LABEL[link.state]} ${describe(link)}`;
4337
+ });
4338
+ const checkOf = (tool, links) => {
4339
+ const name = `${AGENT_TOOL_LAYOUTS.find((l) => l.id === tool)?.name ?? tool} links`;
4340
+ const detail = links.map((link) => describe(link)).join("; ");
4341
+ if (links.every((l) => l.state === "linked")) return {
4342
+ name,
4343
+ status: "ok",
4344
+ detail: links.map((l) => l.path).join(", ")
4345
+ };
4346
+ if (links.some((l) => l.state === "broken" || l.state === "missing")) return {
4347
+ name,
4348
+ status: "warn",
4349
+ detail,
4350
+ hint: `Run ork link ${tool} to link what is missing; a broken link needs fixing by hand.`
4351
+ };
4352
+ return {
4353
+ name,
4354
+ status: "skip",
4355
+ detail
4356
+ };
4357
+ };
4358
+ /**
4359
+ * One check per tool the project uses, once it has `.ork/`: are its folders
4360
+ * linked to `.ork/agents` and `.ork/skills`?
4361
+ */
4362
+ const linkChecks = (projectRoot) => {
4363
+ const ork = path.dirname(ORK_LAYOUT.agents);
4364
+ if (!existsSync(path.join(projectRoot, ork))) return [];
4365
+ const links = toolLinks(projectRoot, toolsInUse(projectRoot));
4366
+ return Object.entries(Object.groupBy(links, (link) => link.tool)).map(([tool, group = []]) => checkOf(tool, group));
4367
+ };
4368
+ // src/commands/install.ts
4369
+ /**
4370
+ * @file This installation of Ork: its name and version for the root command;
4371
+ * `ork doctor`, which prints Ork's version against the last update check and
4372
+ * the environment checks (Node, git, the user's own claude, gh and aws
4373
+ * logins, the project's links) and fails when no Claude driver can run; and
4374
+ * `ork upgrade`, which asks the npm registry for the latest version and, when
4375
+ * it is newer, upgrades Ork the way it was installed after the user agrees
4376
+ * (`--yes` skips the question; `--check` only reports). A clone or `npx` gets
4377
+ * advice instead. Ork never upgrades itself unasked, and stores no
4378
+ * credentials (ADR-038).
4379
+ */
4380
+ /** The root command's name, version and description. */
4381
+ const ORK_META = {
4382
+ name: "ork",
4383
+ version: ORK_VERSION,
4384
+ description: "Orchestrate Claude agents on your machine"
4385
+ };
4386
+ const MARK = {
4387
+ ok: style("green", "✓"),
4388
+ warn: style("yellow", "!"),
4389
+ fail: style("red", "✗"),
4390
+ skip: style("dim", "-")
4391
+ };
4392
+ const formatCheck = (result) => {
4393
+ const line = `${MARK[result.status]} ${result.name.padEnd(20)} ${result.detail}`;
4394
+ return result.hint === void 0 ? [line] : [line, ` ${style("dim", result.hint)}`];
4395
+ };
4396
+ const doctor = defineCommand({
4397
+ meta: {
4398
+ name: "doctor",
4399
+ description: "Check Node, git, and your own claude, gh, and aws logins (Ork stores no credentials)"
4400
+ },
4401
+ run: async () => {
4402
+ const { results, ok } = await runChecks(exec, process.env, process.versions.node);
4403
+ const place = projectAt(process.cwd());
4404
+ for (const result of [
4405
+ orkCheck(),
4406
+ ...results,
4407
+ ...place.mode === "attached" ? [] : linkChecks(place.root)
4408
+ ]) for (const line of formatCheck(result)) write(line);
4409
+ if (!ok) {
4410
+ write(`\n${style("red", "Not ready:")} Ork needs Node, git, and at least one Claude driver.`);
4411
+ process.exitCode = 1;
4412
+ }
4413
+ }
4414
+ });
4415
+ /** The ids of runs still running anywhere on this machine. */
4416
+ const runningRuns = () => {
4417
+ const store = openStore();
4418
+ try {
4419
+ return store.runs({ limit: 500 }).filter((run) => liveStatus(run) === "running").map((run) => run.id.slice(0, 8));
4420
+ } finally {
4421
+ store.close();
4422
+ }
4423
+ };
4424
+ /** Records what the registry said, so the notice and doctor agree with it. */
4425
+ const remember = (latest, notified) => {
4426
+ try {
4427
+ const file = updateCachePath();
4428
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4429
+ writeUpdateCache(file, {
4430
+ ...readUpdateCache(file),
4431
+ checkedAt: now,
4432
+ latest,
4433
+ ...notified ? { notified: {
4434
+ version: latest,
4435
+ at: now
4436
+ } } : {}
4437
+ });
4438
+ } catch {}
4439
+ };
4440
+ /** Whether to go ahead: `--yes`, or the user says so on a terminal. */
4441
+ const agreed = (yes, command) => {
4442
+ if (yes) return Promise.resolve(true);
4443
+ if (!process.stdin.isTTY) {
4444
+ write(`Run it yourself: ${command}\nor run ork upgrade --yes.`);
4445
+ return Promise.resolve(false);
4446
+ }
4447
+ return confirm(`Run ${style("bold", command)}? [y/N] `);
4448
+ };
4449
+ const upgrade = defineCommand({
4450
+ meta: {
4451
+ name: "upgrade",
4452
+ description: "Upgrade Ork to the latest version, the way it was installed (asks first)"
4453
+ },
4454
+ args: {
4455
+ check: {
4456
+ type: "boolean",
4457
+ description: "Only say whether a newer version is out",
4458
+ default: false
4459
+ },
4460
+ yes: {
4461
+ type: "boolean",
4462
+ description: "Run the upgrade without asking",
4463
+ default: false
4464
+ }
4465
+ },
4466
+ run: async ({ args }) => {
4467
+ const registry = registryOf(process.env);
4468
+ const latest = await fetchLatest(ORK_NAME, {
4469
+ ...registry === void 0 ? {} : { registry },
4470
+ timeoutMs: 15e3
4471
+ });
4472
+ const plan = planUpgrade({
4473
+ name: ORK_NAME,
4474
+ current: ORK_VERSION,
4475
+ latest,
4476
+ script: runningScript()
4477
+ });
4478
+ if (plan.kind === "unreachable") {
4479
+ writeErr("Could not reach the npm registry to look for a new version.");
4480
+ process.exitCode = 1;
4481
+ return;
4482
+ }
4483
+ if (latest !== null) remember(latest, plan.kind !== "latest");
4484
+ if (plan.kind === "latest") {
4485
+ write(`${style("green", "✓")} Ork ${ORK_VERSION} is the latest version.`);
4486
+ return;
4487
+ }
4488
+ write(`Ork ${style("bold", plan.latest)} is out (you have ${ORK_VERSION}).\nWhat changed: ${releaseNotes(ORK_NAME, plan.latest)}`);
4489
+ if (args.check) return;
4490
+ if (plan.kind === "advice") {
4491
+ write(plan.text);
4492
+ process.exitCode = 1;
4493
+ return;
4494
+ }
4495
+ const running = runningRuns();
4496
+ if (running.length > 0) write(`${style("yellow", "!")} Running now: ${running.join(", ")}. Their next sessions would start from the new install; upgrading after they finish is safer.`);
4497
+ const command = plan.argv.join(" ");
4498
+ write(`Installed with ${plan.installer.label}.`);
4499
+ if (!await agreed(args.yes, command)) {
4500
+ process.exitCode = 1;
4501
+ return;
4502
+ }
4503
+ const code = await runInstall(plan.argv);
4504
+ if (code !== 0) {
4505
+ writeErr(`${command} failed (exit ${code}); the upgrade did not finish.`);
4506
+ process.exitCode = code;
4507
+ return;
4508
+ }
4509
+ write(`${style("green", "✓")} Upgraded Ork to ${plan.latest}.`);
4510
+ }
4511
+ });
3945
4512
  // src/project/draft.ts
3946
4513
  /**
3947
4514
  * @file `ork migrate --from-agents` (ADR-036): drafts a workflow from a
@@ -4370,62 +4937,6 @@ const initRecorded = (store, cwd, options = {}) => {
4370
4937
  const recorded = recordInRepo(store, cwd);
4371
4938
  return recorded.ok ? initProject(cwd, options) : { error: recorded.error };
4372
4939
  };
4373
- // src/project/link.ts
4374
- /**
4375
- * @file What `ork link` and `ork doctor` say about a project's tool folders:
4376
- * one line per folder (linked, created, kept as a real folder, pointing
4377
- * elsewhere, broken), and one doctor check per tool the project uses, only
4378
- * once the project has `.ork/`.
4379
- */
4380
- const DESCRIBE = {
4381
- linked: (link) => `${link.path} → ${link.origin}`,
4382
- missing: (link) => `${link.path} → ${link.origin}`,
4383
- broken: (link) => `${link.path} → ${link.target ?? "?"}, which does not exist`,
4384
- elsewhere: (link) => `${link.path} → ${link.target ?? "?"}, not ${link.origin}`,
4385
- folder: (link) => `${link.path} is a real folder, left as it is`
4386
- };
4387
- const describe = (link) => DESCRIBE[link.state](link);
4388
- const LABEL = {
4389
- linked: style("dim", "ok "),
4390
- missing: style("yellow", "missing"),
4391
- broken: style("red", "broken "),
4392
- elsewhere: style("yellow", "kept "),
4393
- folder: style("yellow", "kept ")
4394
- };
4395
- /** One line per folder; `created` marks the links this call made. */
4396
- const formatLinks = (links, created = []) => links.map((link) => {
4397
- return `${created.includes(link.path) ? style("green", "linked ") : LABEL[link.state]} ${describe(link)}`;
4398
- });
4399
- const checkOf = (tool, links) => {
4400
- const name = `${AGENT_TOOL_LAYOUTS.find((l) => l.id === tool)?.name ?? tool} links`;
4401
- const detail = links.map((link) => describe(link)).join("; ");
4402
- if (links.every((l) => l.state === "linked")) return {
4403
- name,
4404
- status: "ok",
4405
- detail: links.map((l) => l.path).join(", ")
4406
- };
4407
- if (links.some((l) => l.state === "broken" || l.state === "missing")) return {
4408
- name,
4409
- status: "warn",
4410
- detail,
4411
- hint: `Run ork link ${tool} to link what is missing; a broken link needs fixing by hand.`
4412
- };
4413
- return {
4414
- name,
4415
- status: "skip",
4416
- detail
4417
- };
4418
- };
4419
- /**
4420
- * One check per tool the project uses, once it has `.ork/`: are its folders
4421
- * linked to `.ork/agents` and `.ork/skills`?
4422
- */
4423
- const linkChecks = (projectRoot) => {
4424
- const ork = path.dirname(ORK_LAYOUT.agents);
4425
- if (!existsSync(path.join(projectRoot, ork))) return [];
4426
- const links = toolLinks(projectRoot, toolsInUse(projectRoot));
4427
- return Object.entries(Object.groupBy(links, (link) => link.tool)).map(([tool, group = []]) => checkOf(tool, group));
4428
- };
4429
4940
  // src/project/migrate.ts
4430
4941
  /**
4431
4942
  * @file `ork migrate` (ADR-036): moves an attached project's folder into its
@@ -5357,94 +5868,13 @@ const addAgent = (options) => {
5357
5868
  if (options.model !== void 0) agent.model = options.model;
5358
5869
  return createAgent(options.name, agent, options);
5359
5870
  };
5360
- // src/doctor/checks.ts
5361
- /**
5362
- * @file The checks behind `ork doctor`: each tool's presence and login status,
5363
- * through an injectable `exec` so tests need no real tools. Never prints a
5364
- * token or an account.
5365
- */
5366
- const TIMEOUT_MS = 15e3;
5367
- const exec = (program, args) => new Promise((resolve) => {
5368
- execFile(program, args, {
5369
- timeout: TIMEOUT_MS,
5370
- encoding: "utf8"
5371
- }, (error, stdout) => {
5372
- resolve({
5373
- ok: error === null,
5374
- stdout
5375
- });
5376
- });
5377
- });
5378
- const MIN_NODE = [24, 20];
5379
- const checkNode = (version) => {
5380
- const [major = 0, minor = 0] = version.split(".").map(Number);
5381
- const [minMajor, minMinor] = MIN_NODE;
5382
- return major > minMajor || major === minMajor && minor >= minMinor ? {
5383
- name: "node",
5384
- status: "ok",
5385
- detail: `v${version}`
5386
- } : {
5387
- name: "node",
5388
- status: "fail",
5389
- detail: `v${version}`,
5390
- hint: `Ork needs Node ${MIN_NODE.join(".")} or later.`
5391
- };
5392
- };
5393
- const checkGit = async (run) => {
5394
- const version = await run("git", ["--version"]);
5395
- if (!version.ok) return [{
5396
- name: "git",
5397
- status: "fail",
5398
- detail: "not found",
5399
- hint: "Install git; Ork gives each writing node its own worktree."
5400
- }];
5401
- const inside = await run("git", ["rev-parse", "--is-inside-work-tree"]);
5402
- return [{
5403
- name: "git",
5404
- status: "ok",
5405
- detail: firstLine$1(version.stdout)
5406
- }, inside.ok ? {
5407
- name: "git repo",
5408
- status: "ok",
5409
- detail: "inside a work tree"
5410
- } : {
5411
- name: "git repo",
5412
- status: "warn",
5413
- detail: "not a git repository",
5414
- hint: "Run Ork inside a git repository so nodes can use worktrees."
5415
- }];
5416
- };
5417
- /**
5418
- * Runs every check: Node and git, then every registered Claude driver and
5419
- * integration (ADR-022). Node, git, and at least one driver are required;
5420
- * integrations are optional.
5421
- */
5422
- const runChecks = async (run, env, nodeVersion) => {
5423
- const [git, drivers, tools] = await Promise.all([
5424
- checkGit(run),
5425
- Promise.all(CLAUDE_DRIVERS.map((d) => d.doctor(run, env))),
5426
- Promise.all(INTEGRATIONS.flatMap((i) => i.doctor === void 0 ? [] : [i.doctor(run, env)]))
5427
- ]);
5428
- const results = [
5429
- checkNode(nodeVersion),
5430
- ...git,
5431
- ...drivers,
5432
- ...tools
5433
- ];
5434
- return {
5435
- results,
5436
- ok: drivers.some((d) => d.status === "ok") && !results.some((r) => r.status === "fail")
5437
- };
5438
- };
5439
5871
  // src/commands/setup.ts
5440
5872
  /**
5441
5873
  * @file Setting up: `ork init` scaffolds `.ork/` in a project, records it, and
5442
5874
  * links the tool folders it uses; `ork link` points tools' agents and skills
5443
5875
  * folders at `.ork/` (never in an attached project); `ork agents` lists,
5444
- * shows, adds and removes the agents nodes can take; `ork doctor` prints the
5445
- * environment checks (Node, git, the user's own claude, gh and aws logins,
5446
- * and the project's links) and fails when no Claude driver can run. Ork
5447
- * stores no credentials.
5876
+ * shows, adds and removes the agents nodes can take. `ork doctor` and `ork
5877
+ * upgrade` live in `install.ts`.
5448
5878
  */
5449
5879
  const init = defineCommand({
5450
5880
  meta: {
@@ -5512,31 +5942,6 @@ const link = defineCommand({
5512
5942
  for (const line of formatLinks(result.links, result.created)) write(line);
5513
5943
  }
5514
5944
  });
5515
- const MARK = {
5516
- ok: style("green", "✓"),
5517
- warn: style("yellow", "!"),
5518
- fail: style("red", "✗"),
5519
- skip: style("dim", "-")
5520
- };
5521
- const formatCheck = (result) => {
5522
- const line = `${MARK[result.status]} ${result.name.padEnd(20)} ${result.detail}`;
5523
- return result.hint === void 0 ? [line] : [line, ` ${style("dim", result.hint)}`];
5524
- };
5525
- const doctor = defineCommand({
5526
- meta: {
5527
- name: "doctor",
5528
- description: "Check Node, git, and your own claude, gh, and aws logins (Ork stores no credentials)"
5529
- },
5530
- run: async () => {
5531
- const { results, ok } = await runChecks(exec, process.env, process.versions.node);
5532
- const place = projectAt(process.cwd());
5533
- for (const result of [...results, ...place.mode === "attached" ? [] : linkChecks(place.root)]) for (const line of formatCheck(result)) write(line);
5534
- if (!ok) {
5535
- write(`\n${style("red", "Not ready:")} Ork needs Node, git, and at least one Claude driver.`);
5536
- process.exitCode = 1;
5537
- }
5538
- }
5539
- });
5540
5945
  const json = {
5541
5946
  type: "boolean",
5542
5947
  description: "Print JSON",
@@ -5673,11 +6078,7 @@ const agents = defineCommand({
5673
6078
  // src/main.ts
5674
6079
  /** @file The root `ork` command and its subcommands. */
5675
6080
  const main = defineCommand({
5676
- meta: {
5677
- name: "ork",
5678
- version: ORK_VERSION,
5679
- description: "Orchestrate Claude agents on your machine"
5680
- },
6081
+ meta: ORK_META,
5681
6082
  subCommands: {
5682
6083
  init,
5683
6084
  register,
@@ -5698,13 +6099,26 @@ const main = defineCommand({
5698
6099
  context,
5699
6100
  worktrees,
5700
6101
  wt: worktrees,
5701
- doctor
6102
+ doctor,
6103
+ upgrade
5702
6104
  }
5703
6105
  });
5704
6106
  // src/bin.ts
5705
6107
  /**
5706
6108
  * @file The `ork` executable: runs the CLI's root command. The published
5707
- * package's `bin` points at its bundle.
6109
+ * package's `bin` points at its bundle. When the new-version check is on, it
6110
+ * starts the daily background check and ends with the notice (ADR-038); the
6111
+ * background copy it starts only runs the check.
5708
6112
  */
5709
- await runMain(main);
6113
+ if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
6114
+ else {
6115
+ const argv = process.argv.slice(2);
6116
+ if (updateCheckOff(process.env, argv, process.stderr.isTTY) === null) {
6117
+ startUpdateCheck();
6118
+ process.once("exit", () => {
6119
+ showUpdateNotice();
6120
+ });
6121
+ }
6122
+ await runMain(main);
6123
+ }
5710
6124
  export {};
@@ -4581,6 +4581,82 @@ const startRun = (options) => {
4581
4581
  tell: ports.bus.tell
4582
4582
  };
4583
4583
  };
4584
+ // ../core/src/update/installers.ts
4585
+ const SOURCE = "/packages/cli/";
4586
+ const has = (...parts) => (script) => parts.some((part) => script.includes(part));
4587
+ const INSTALLERS = [
4588
+ {
4589
+ id: "source",
4590
+ label: "a clone of the repository",
4591
+ owns: (script) => script.includes(SOURCE) && !script.includes("/node_modules/"),
4592
+ advice: (_spec, script) => `In ${script.slice(0, script.indexOf(SOURCE))}: git pull, then pnpm install and pnpm build.`
4593
+ },
4594
+ {
4595
+ id: "npx",
4596
+ label: "npx",
4597
+ owns: has("/_npx/"),
4598
+ advice: (spec) => `npx keeps its own copy: run npx ${spec} …, or install it with npm install --global ${spec}.`
4599
+ },
4600
+ {
4601
+ id: "pnpm",
4602
+ label: "pnpm (global)",
4603
+ owns: has("/pnpm/global/", "/.pnpm-global/"),
4604
+ command: (spec) => [
4605
+ "pnpm",
4606
+ "add",
4607
+ "--global",
4608
+ spec
4609
+ ]
4610
+ },
4611
+ {
4612
+ id: "yarn",
4613
+ label: "yarn (global)",
4614
+ owns: has("/yarn/global/"),
4615
+ command: (spec) => [
4616
+ "yarn",
4617
+ "global",
4618
+ "add",
4619
+ spec
4620
+ ]
4621
+ },
4622
+ {
4623
+ id: "bun",
4624
+ label: "bun (global)",
4625
+ owns: has("/.bun/install/global/"),
4626
+ command: (spec) => [
4627
+ "bun",
4628
+ "add",
4629
+ "--global",
4630
+ spec
4631
+ ]
4632
+ },
4633
+ {
4634
+ id: "volta",
4635
+ label: "Volta",
4636
+ owns: has("/.volta/tools/"),
4637
+ command: (spec) => [
4638
+ "volta",
4639
+ "install",
4640
+ spec
4641
+ ]
4642
+ },
4643
+ {
4644
+ id: "npm",
4645
+ label: "npm (global)",
4646
+ owns: has("/node_modules/"),
4647
+ command: (spec) => [
4648
+ "npm",
4649
+ "install",
4650
+ "--global",
4651
+ spec
4652
+ ]
4653
+ }
4654
+ ];
4655
+ /** The installer of the running script, or `undefined` when none fits. */
4656
+ const detectInstaller = (script, installers = INSTALLERS) => {
4657
+ const normal = script.replaceAll("\\", "/");
4658
+ return installers.find((i) => i.owns(normal));
4659
+ };
4584
4660
  // ../core/src/worktree/git.ts
4585
4661
  /**
4586
4662
  * @file A minimal `git` runner for the worktree manager: arguments without a
@@ -5354,4 +5430,4 @@ const createWorkspace = (options) => {
5354
5430
  close
5355
5431
  };
5356
5432
  };
5357
- export { parseWorkflow as $, planAgentMigration as A, PROJECT_MODES as B, fingerprint as C, parseTemplate as D, maskSecrets as E, toolsInUse as F, placeFor as G, inRepoPlace as H, createAgent as I, shownPath as J, projectsHome as K, deleteAgent as L, planMoveInto as M, linkTools as N, renderTemplate as O, toolLinks as P, SKILL_FILE as Q, catalogAgent as R, createFeedback as S, maskReplacer as T, newProjectId as U, hasInRepoFiles as V, placeFiles as W, inside as X, workflowDirOf as Y, projectRootOf as Z, gitRulesOf as _, removeWorktree as a, INTEGRATIONS as at, FEEDBACK_SEVERITIES as b, lines as c, AGENT_TOOL_LAYOUTS as ct, parseReport as d, firstLine as dt, serializeWorkflow as et, userMessageText as f, compileWorkflow as g, resolvePractices as h, inspectProject as i, isToolNode as it, applySteps as j, contextDiagnostics as k, startRun as l, ORK_LAYOUT as lt, practiceInstructions as m, resolveBase as n, settingsOf as nt, git as o, TOOL_PREFIX as ot, runScope as p, runDirOf as q, BRANCH_PREFIX as r, isClaudeNode as rt, gitOk as s, integrationFor as st, createWorkspace as t, WorkflowSchema as tt, planNodeSession as u, CLAUDE_DRIVERS as ut, loadProject as v, targetOf as w, FEEDBACK_TARGETS as x, FEEDBACK_KINDS as y, loadAgents as z };
5433
+ export { SKILL_FILE as $, contextDiagnostics as A, loadAgents as B, createFeedback as C, maskSecrets as D, maskReplacer as E, toolLinks as F, placeFiles as G, hasInRepoFiles as H, toolsInUse as I, runDirOf as J, placeFor as K, createAgent as L, applySteps as M, planMoveInto as N, parseTemplate as O, linkTools as P, projectRootOf as Q, deleteAgent as R, FEEDBACK_TARGETS as S, targetOf as T, inRepoPlace as U, PROJECT_MODES as V, newProjectId as W, workflowDirOf as X, shownPath as Y, inside as Z, compileWorkflow as _, removeWorktree as a, isToolNode as at, FEEDBACK_KINDS as b, lines as c, integrationFor as ct, planNodeSession as d, CLAUDE_DRIVERS as dt, parseWorkflow as et, parseReport as f, firstLine as ft, resolvePractices as g, practiceInstructions as h, inspectProject as i, isClaudeNode as it, planAgentMigration as j, renderTemplate as k, detectInstaller as l, AGENT_TOOL_LAYOUTS as lt, runScope as m, resolveBase as n, WorkflowSchema as nt, git as o, INTEGRATIONS as ot, userMessageText as p, projectsHome as q, BRANCH_PREFIX as r, settingsOf as rt, gitOk as s, TOOL_PREFIX as st, createWorkspace as t, serializeWorkflow as tt, startRun as u, ORK_LAYOUT as ut, gitRulesOf as v, fingerprint as w, FEEDBACK_SEVERITIES as x, loadProject as y, catalogAgent as z };
@@ -1,4 +1,4 @@
1
- import { b as FEEDBACK_SEVERITIES, d as parseReport, x as FEEDBACK_TARGETS, y as FEEDBACK_KINDS } from "./src-CqNlvqV_.mjs";
1
+ import { S as FEEDBACK_TARGETS, b as FEEDBACK_KINDS, f as parseReport, x as FEEDBACK_SEVERITIES } from "./src-CJ_zF0Zj.mjs";
2
2
  import { z } from "zod";
3
3
  import { randomBytes, timingSafeEqual } from "node:crypto";
4
4
  import { createInterface } from "node:readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krapht/ork",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Orchestrate Claude agents on your machine: hierarchical, serializable workflows with a CLI and a localhost UI.",
5
5
  "keywords": [
6
6
  "claude",