@krapht/ork 1.0.0-alpha.1 → 1.0.0-alpha.10
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 +3 -4
- package/dist/{__build_info__-D9lSlMFy.mjs → __build_info__-DFF18K8y.mjs} +72 -3
- package/dist/bin.mjs +579 -379
- package/dist/{clean-CA_Oi2Wl.mjs → clean-DVaEH9e7.mjs} +5 -2
- package/dist/{completion-DBGfL-gN.mjs → completion-Dv5kf598.mjs} +6 -6
- package/dist/context-CEaKOVmG.mjs +683 -0
- package/dist/{decide-CoIJh34e.mjs → decide-3l9q8UtX.mjs} +3 -3
- package/dist/{find-rBBk7Jui.mjs → find-DEjjOlTG.mjs} +1 -1
- package/dist/format-Cb5_m-Qd.mjs +191 -0
- package/dist/{in-a-run-DdAakpRR.mjs → in-a-run-BS9Nc-JW.mjs} +1 -1
- package/dist/{limits-BPM9wUlV.mjs → limits-BSwEjQST.mjs} +1 -1
- package/dist/{list-Bv5Lxu7z.mjs → list-Qxr6rwsY.mjs} +11 -4
- package/dist/{mcp-DM30MFBw.mjs → mcp-CslnG5wf.mjs} +6 -6
- package/dist/output-Bqljtmp_.mjs +40 -0
- package/dist/{pause-C7lJkSma.mjs → pause-D7OVB5Ac.mjs} +4 -4
- package/dist/{relay-BipJYszF.mjs → relay-EceRIebO.mjs} +2 -2
- package/dist/{remove-BDs9isB6.mjs → remove-0IQZVWmI.mjs} +14 -7
- package/dist/{send-nbkjf3sc.mjs → send-C0uwfUYi.mjs} +3 -3
- package/dist/{src-hrj2h0IZ.mjs → src-Df6KOIDC.mjs} +5678 -3245
- package/dist/{src-BxUJPGa5.mjs → src-cmmXKHn1.mjs} +265 -203
- package/dist/{src-B0TT7DBi.mjs → src-i5k2Y-kf.mjs} +35 -86
- package/dist/src-oaJYIddp.mjs +2255 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
- package/dist/ui/assets/app-C8uhHVYp.css +1 -0
- package/dist/ui/assets/app-qs8Lofv6.js +75 -0
- package/dist/ui/assets/{index-CPx74lFf.js → index-DV1ZaOk-.js} +3 -3
- package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/{ui-B98FSboa.mjs → ui-CBkHJdqk.mjs} +4 -4
- package/dist/{usage-EW7DI6Z7.mjs → usage-Cd8pEYyK.mjs} +5 -5
- package/dist/{watch-BAkSGaOp.mjs → watch-DW5R2OPO.mjs} +6 -6
- package/dist/{workflows-vsEohqa7.mjs → workflows-BT1gDRIV.mjs} +4 -3
- package/dist/{worktrees-Ahj9rdWK.mjs → worktrees-g1MQMGie.mjs} +78 -19
- package/package.json +2 -4
- package/dist/context-6x3kXREO.mjs +0 -274
- package/dist/format-zMmhqyLE.mjs +0 -427
- package/dist/output-TV_j7Ni0.mjs +0 -950
- package/dist/src-B9v0lDc6.mjs +0 -1204
- package/dist/ui/assets/app-BNT0SuIz.css +0 -1
- package/dist/ui/assets/app-CmkynZyW.js +0 -67
- package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -1,58 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { C as
|
|
4
|
-
import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-
|
|
5
|
-
import { n as newRunId, t as findRun } from "./find-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { a as planClean, t as applyClean } from "./clean-CA_Oi2Wl.mjs";
|
|
2
|
+
import { $ as adoptedCalls, A as inspectProject, An as inside, B as createCalls, Cn as newProjectId, Ct as runTree, D as openStore, Dn as runDirOf, E as isNodeEvent, En as projectsHome, F as clearGitLocalEnv, G as peerOf, H as endLink, Hn as beforeOf, Ht as FEEDBACK_KINDS, I as parseInputPairs, It as PROJECT_FILE, J as flattenTree, Jt as sendPost, K as tellNode, Kn as serializeWorkflow, Kt as planPost, L as resolveInputs, Lt as gitRulesOf, On as shownPath, Ot as nestItems, Q as tellLiveRun, Qn as settingsOf, Qt as normalizeTitle, Rt as loadProject, Sn as inRepoPlace, St as runStatus, Tn as placeFor, U as endLinksOf, Ut as FEEDBACK_SEVERITIES, V as addressOf, Wt as FEEDBACK_TARGETS, Xt as createFeedback, Yn as WorkflowSchema, Z as requestStop, Zt as fingerprint, _n as createAgent, an as socketPath, at as followEvents, bn as loadAgents, bt as liveStatus, ct as planNodeSession, d as runningRunIds, dn as planAgentMigration, dr as readyDriver, er as isClaudeNode, et as detachResume, f as runningScript, fn as applySteps, fr as AGENT_TOOL_LAYOUTS, gn as toolsInUse, gt as startRun, h as isNewer, hn as toolLinks, ht as walkSummary, i as NOT_STARTED, in as serveRun, ir as retiredDriverHint, it as detachRun, jn as projectRootOf, l as planUpgrade, ln as maskReplacer, mn as linkTools, mr as AGENT_DRIVERS, n as resolveBase, nt as listKey, pn as planMoveInto, pr as ORK_LAYOUT, rt as DETACHED_ENV, sn as wakeRun, t as createWorkspace, tt as resumable, u as releaseNotes, un as maskSecrets, ur as driverChecks, ut as sessionDriver, vn as deleteAgent, vt as userMessageText, wn as placeFiles, wt as templateOf, xn as hasInRepoFiles, yn as catalogAgent } from "./src-Df6KOIDC.mjs";
|
|
3
|
+
import { C as placeOf, T as openFixRun, _ as nodeSession, b as recordNote, f as readUpdateCache, g as activeTree, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, n as ORK_VERSION, s as runInstall, t as ORK_NAME, v as previewPost, w as feedbackActions, x as acceptItem, y as checkNote } from "./__build_info__-DFF18K8y.mjs";
|
|
4
|
+
import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-3l9q8UtX.mjs";
|
|
5
|
+
import { n as newRunId, t as findRun } from "./find-DEjjOlTG.mjs";
|
|
6
|
+
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-Qxr6rwsY.mjs";
|
|
7
|
+
import { a as planClean, t as applyClean } from "./clean-DVaEH9e7.mjs";
|
|
9
8
|
import { c as subcommandsOf, i as argsOf, n as groupedEntries, o as publicArgs, s as resolved, t as groupOf } from "./help-groups-BwYoAWsh.mjs";
|
|
10
|
-
import { a as
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { i as
|
|
14
|
-
import {
|
|
15
|
-
import { i as
|
|
9
|
+
import { a as write, i as style, n as cost, o as writeErr, t as confirm } from "./output-Bqljtmp_.mjs";
|
|
10
|
+
import { _ as namedMcpEntries, a as builtinExecutors, d as maskedEnv, f as maskedNamesOfRun, g as clearOutputFiles, h as sessionEnv, i as onPath, l as clearStateFiles, m as runScrubber, n as missingProgramMessage, r as missingPrograms, t as envProblems, v as resultOf } from "./src-cmmXKHn1.mjs";
|
|
11
|
+
import { c as registeredProjects, i as refusedBuiltin, n as WORKFLOW_SUFFIX, o as workflowArg, s as projectAt, t as WORKFLOW_DIR } from "./workflows-BT1gDRIV.mjs";
|
|
12
|
+
import { C as startUpdateCheck, S as showUpdateNotice, _ as runChecks, a as validate, c as warningLines, d as runReadyDriver, f as unfitLines, g as orkCheck, h as headWarning, i as loadWorkflow, l as checksHere, m as experimentsCheck, o as validateFile, p as exec, r as formatReport, s as walkLoaded, t as context, u as chooseReadyDriver, v as socketCheck, w as updateCheckOff, x as runUpdateChild, y as triggerChecks } from "./context-CEaKOVmG.mjs";
|
|
13
|
+
import { a as nodeVerdict, i as hasConflict, n as formatRuns, o as firstLine, r as formatTree, s as formatBusEvent, t as ago } from "./format-Cb5_m-Qd.mjs";
|
|
14
|
+
import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-BSwEjQST.mjs";
|
|
15
|
+
import { i as decideRefusal, n as chatRefusal } from "./in-a-run-BS9Nc-JW.mjs";
|
|
16
16
|
import path from "node:path";
|
|
17
17
|
import { stringify } from "yaml";
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
|
-
import { execFile, spawn } from "node:child_process";
|
|
23
22
|
import { setTimeout } from "node:timers/promises";
|
|
24
23
|
import { defineCommand, renderUsage, runMain } from "citty";
|
|
25
24
|
import { createInterface } from "node:readline/promises";
|
|
26
|
-
import { styleText } from "node:util";
|
|
27
|
-
// ../integrations/src/ready.ts
|
|
28
|
-
/**
|
|
29
|
-
* @file Which Claude driver is ready on this machine: each driver's own check
|
|
30
|
-
* (what `ork doctor` shows), and the one a run takes for the nodes that name
|
|
31
|
-
* no driver, so a committed workflow never carries one person's login.
|
|
32
|
-
*/
|
|
33
|
-
/** Each registered Claude driver's own check, in registry order. */
|
|
34
|
-
const driverChecks = (run, env) => Promise.all(CLAUDE_DRIVERS.map(async (driver) => ({
|
|
35
|
-
id: driver.id,
|
|
36
|
-
result: await driver.doctor(run, env)
|
|
37
|
-
})));
|
|
38
|
-
/**
|
|
39
|
-
* The driver ready here: the one whose check passes; with more than one,
|
|
40
|
-
* {@link PREFERRED_DRIVER}; with none, each driver's hint.
|
|
41
|
-
*/
|
|
42
|
-
const readyDriver = (checks) => {
|
|
43
|
-
const ready = checks.filter(({ result }) => result.status === "ok");
|
|
44
|
-
const pick = ready.find(({ id }) => id === "cli") ?? ready.at(0);
|
|
45
|
-
if (pick === void 0) return {
|
|
46
|
-
ok: false,
|
|
47
|
-
hints: checks.flatMap(({ result }) => result.hint === void 0 ? [] : [result.hint])
|
|
48
|
-
};
|
|
49
|
-
return {
|
|
50
|
-
ok: true,
|
|
51
|
-
driver: pick.id,
|
|
52
|
-
detail: pick.result.detail,
|
|
53
|
-
ready: ready.length
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
25
|
// ../core/src/control/queue.ts
|
|
57
26
|
const PERMISSION_OPTIONS = [{
|
|
58
27
|
id: "allow",
|
|
@@ -64,14 +33,33 @@ const PERMISSION_OPTIONS = [{
|
|
|
64
33
|
const DENIED = "The user did not allow this tool.";
|
|
65
34
|
/** The deny message a session gets, with the person's words when given. */
|
|
66
35
|
const deniedMessage = (note) => note === void 0 ? DENIED : `${DENIED} They said: ${note}`;
|
|
67
|
-
/**
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
36
|
+
/** The input fields that say what a tool will do, the first one present. */
|
|
37
|
+
const TOOL_INPUT_KEYS = [
|
|
38
|
+
"command",
|
|
39
|
+
"file_path",
|
|
40
|
+
"path",
|
|
41
|
+
"url",
|
|
42
|
+
"pattern"
|
|
43
|
+
];
|
|
44
|
+
/** A tool's input for the decision's context: as written, cut at `max`. */
|
|
45
|
+
const inputContext = (input, max = 4e3) => {
|
|
46
|
+
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
47
|
+
const text = key === void 0 ? JSON.stringify(input, void 0, 2) : String(input[key]);
|
|
48
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
71
49
|
};
|
|
72
50
|
const createDecisionQueue = (options) => {
|
|
73
51
|
const { store, runId, emit } = options;
|
|
74
|
-
|
|
52
|
+
const earlier = store.decisions({ runId });
|
|
53
|
+
let count = Math.max(0, ...earlier.map(({ id }) => Number(id.slice(runId.length + 1)) || 0));
|
|
54
|
+
for (const { id, node, status } of earlier) {
|
|
55
|
+
if (status !== "open") continue;
|
|
56
|
+
store.withdrawDecision(id);
|
|
57
|
+
emit({
|
|
58
|
+
type: "decision.withdrawn",
|
|
59
|
+
node,
|
|
60
|
+
decision: id
|
|
61
|
+
});
|
|
62
|
+
}
|
|
75
63
|
/** Opens a decision and settles it with the first answer. */
|
|
76
64
|
const decide = (open, local, withdrawn) => {
|
|
77
65
|
count += 1;
|
|
@@ -155,12 +143,16 @@ const createDecisionQueue = (options) => {
|
|
|
155
143
|
};
|
|
156
144
|
if (question.recommended !== void 0) open.recommended = question.recommended;
|
|
157
145
|
if (question.context !== void 0) open.context = question.context;
|
|
146
|
+
if (question.document !== void 0) open.document = question.document;
|
|
158
147
|
if (question.owner !== void 0) open.owner = question.owner;
|
|
159
148
|
const { local } = options;
|
|
160
149
|
return decide(open, local === void 0 ? void 0 : (signal) => {
|
|
161
150
|
let note;
|
|
162
151
|
return local.ask({
|
|
163
152
|
...question,
|
|
153
|
+
question: maskSecrets(question.question),
|
|
154
|
+
...question.context === void 0 ? {} : { context: maskSecrets(question.context) },
|
|
155
|
+
...question.document === void 0 ? {} : { document: maskSecrets(question.document) },
|
|
164
156
|
signal,
|
|
165
157
|
onNote: (given) => {
|
|
166
158
|
note = given;
|
|
@@ -219,7 +211,7 @@ const createDecisionQueue = (options) => {
|
|
|
219
211
|
kind: "permission",
|
|
220
212
|
question: `Allow ${request.toolName}?`,
|
|
221
213
|
options: PERMISSION_OPTIONS,
|
|
222
|
-
context:
|
|
214
|
+
context: inputContext(request.input)
|
|
223
215
|
}, local === void 0 ? void 0 : (signal) => {
|
|
224
216
|
let said;
|
|
225
217
|
return local.permit({
|
|
@@ -236,7 +228,7 @@ const createDecisionQueue = (options) => {
|
|
|
236
228
|
note: said
|
|
237
229
|
};
|
|
238
230
|
});
|
|
239
|
-
});
|
|
231
|
+
}, request.signal);
|
|
240
232
|
if (note !== void 0) request.onNote?.(note);
|
|
241
233
|
return answer === "allow" ? { allow: true } : {
|
|
242
234
|
allow: false,
|
|
@@ -613,6 +605,89 @@ const createRelay = (options) => {
|
|
|
613
605
|
}
|
|
614
606
|
};
|
|
615
607
|
};
|
|
608
|
+
// ../core/src/run/replay.ts
|
|
609
|
+
const NO_FAN_OUTS = {
|
|
610
|
+
lists: /* @__PURE__ */ new Map(),
|
|
611
|
+
items: /* @__PURE__ */ new Map()
|
|
612
|
+
};
|
|
613
|
+
/** Whether an activation's input carries a result this run made anew. */
|
|
614
|
+
const carries = (fresh, input) => {
|
|
615
|
+
if (input === void 0) return false;
|
|
616
|
+
if (fresh.has(input)) return true;
|
|
617
|
+
const results = input["results"];
|
|
618
|
+
if (results === null || typeof results !== "object") return false;
|
|
619
|
+
return Object.values(results).some((value) => value !== null && typeof value === "object" && fresh.has(value));
|
|
620
|
+
};
|
|
621
|
+
/**
|
|
622
|
+
* Hands each node's recorded results back in order, until the node runs anew
|
|
623
|
+
* once (it failed or never finished last time, or its input is new); from then
|
|
624
|
+
* on it always runs. A node cut off mid-session continues that session, once.
|
|
625
|
+
*/
|
|
626
|
+
const createReplay = (history, cutOff = /* @__PURE__ */ new Map(), mail, earlier, fanOuts = NO_FAN_OUTS) => {
|
|
627
|
+
const sessions = new Map(cutOff);
|
|
628
|
+
const used = /* @__PURE__ */ new Map();
|
|
629
|
+
const anew = /* @__PURE__ */ new Set();
|
|
630
|
+
const fresh = /* @__PURE__ */ new WeakSet();
|
|
631
|
+
/** Fanned nodes whose list was resolved anew: their items can't be replayed. */
|
|
632
|
+
const renewed = /* @__PURE__ */ new Set();
|
|
633
|
+
const takeItem = (activation, of) => {
|
|
634
|
+
const recorded = fanOuts.items.get(activation.id)?.get(activation.round ?? 0);
|
|
635
|
+
if (recorded !== void 0 && !renewed.has(of) && !carries(fresh, activation.input)) return recorded;
|
|
636
|
+
anew.add(activation.id);
|
|
637
|
+
return null;
|
|
638
|
+
};
|
|
639
|
+
return {
|
|
640
|
+
take: (activation) => {
|
|
641
|
+
const { id } = activation;
|
|
642
|
+
if (activation.brief !== void 0 || anew.has(id)) return null;
|
|
643
|
+
if (activation.item !== void 0) return takeItem(activation, activation.item.of);
|
|
644
|
+
const index = used.get(id) ?? 0;
|
|
645
|
+
const recorded = history.get(id)?.[index];
|
|
646
|
+
if (recorded === void 0 || carries(fresh, activation.input)) {
|
|
647
|
+
anew.add(id);
|
|
648
|
+
return null;
|
|
649
|
+
}
|
|
650
|
+
used.set(id, index + 1);
|
|
651
|
+
return recorded;
|
|
652
|
+
},
|
|
653
|
+
ran: (result) => {
|
|
654
|
+
fresh.add(result.output);
|
|
655
|
+
},
|
|
656
|
+
cutOff: (activation) => {
|
|
657
|
+
const session = activation.brief === void 0 && (activation.item === void 0 || !renewed.has(activation.item.of)) ? sessions.get(activation.id) : void 0;
|
|
658
|
+
sessions.delete(activation.id);
|
|
659
|
+
return session;
|
|
660
|
+
},
|
|
661
|
+
listed: (id, round, source) => {
|
|
662
|
+
const list = fanOuts.lists.get(listKey(id, round));
|
|
663
|
+
const ranAnew = typeof source === "object" && source !== null && fresh.has(source);
|
|
664
|
+
if (list !== void 0 && !ranAnew) return list;
|
|
665
|
+
renewed.add(id);
|
|
666
|
+
return null;
|
|
667
|
+
},
|
|
668
|
+
previousSession: ({ id, item, round = 0 }) => item === void 0 || renewed.has(item.of) ? void 0 : [...fanOuts.items.get(id) ?? []].findLast(([before]) => before < round)?.[1].sessionId,
|
|
669
|
+
lastOutput: (id) => history.get(id)?.at(-1)?.output,
|
|
670
|
+
kept: () => [...history.keys(), ...fanOuts.items.keys()],
|
|
671
|
+
...mail === void 0 ? {} : { mail },
|
|
672
|
+
...earlier === void 0 ? {} : { earlier }
|
|
673
|
+
};
|
|
674
|
+
};
|
|
675
|
+
/**
|
|
676
|
+
* The run's `HumanChannel`, answering questions about loops out of rounds as
|
|
677
|
+
* they were answered last time, in order; anything else goes to `human`.
|
|
678
|
+
*/
|
|
679
|
+
const replayLoopAnswers = (human, answers) => {
|
|
680
|
+
const left = [...answers];
|
|
681
|
+
return {
|
|
682
|
+
permit: (request) => human.permit(request),
|
|
683
|
+
...human.settled === void 0 ? {} : { settled: human.settled },
|
|
684
|
+
ask: (question) => {
|
|
685
|
+
const index = left.findIndex((a) => a.question === question.question);
|
|
686
|
+
const [given] = index === -1 ? [] : left.splice(index, 1);
|
|
687
|
+
return given === void 0 ? human.ask(question) : Promise.resolve(given.answer);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
};
|
|
616
691
|
// ../core/src/store/recorder.ts
|
|
617
692
|
/**
|
|
618
693
|
* @file Records a run as it happens: each event appended to the store and
|
|
@@ -699,291 +774,285 @@ const renderHelp = async (cmd, parent) => {
|
|
|
699
774
|
const showHelp = async (cmd, parent) => {
|
|
700
775
|
process.stdout.write(`${await renderHelp(cmd, parent)}\n\n`);
|
|
701
776
|
};
|
|
702
|
-
// src/
|
|
703
|
-
/**
|
|
704
|
-
* @file The new-version notice (ADR-038). At most once a day a command starts a
|
|
705
|
-
* detached copy of `ork` that asks the npm registry for the latest version
|
|
706
|
-
* and records it in `~/.ork/update-check.json`, so no command waits on the
|
|
707
|
-
* network; a later command, when a newer version is known, ends with one line
|
|
708
|
-
* on stderr (once a day). Off in CI, without a terminal, with `--json`, and
|
|
709
|
-
* with `ORK_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1`.
|
|
710
|
-
*/
|
|
711
|
-
/** Set in the background copy of `ork` that only runs the check. */
|
|
712
|
-
const UPDATE_CHILD_ENV = "ORK_UPDATE_CHECK_CHILD";
|
|
713
|
-
/**
|
|
714
|
-
* Why the automatic check is off for this command, or `null` when it is on: the
|
|
715
|
-
* environment's switches (core's, which `ork ui` reads too), then where a
|
|
716
|
-
* notice on stderr would go.
|
|
717
|
-
*/
|
|
718
|
-
const updateCheckOff = (env, argv, terminal) => {
|
|
719
|
-
const switched = updateSwitchOff(env);
|
|
720
|
-
if (switched !== null) return switched === "CI" ? "in CI" : "turned off";
|
|
721
|
-
if (isSwitchedOn(env["ORK_DETACHED"]) || !terminal) return "no terminal";
|
|
722
|
-
if (argv.includes("--json")) return "JSON output";
|
|
723
|
-
if (argv[0] === "upgrade") return "ork upgrade checks for itself";
|
|
724
|
-
if (argv[0] === "ui" && argv.includes("--serve")) return "ork ui's server checks for itself";
|
|
725
|
-
return null;
|
|
726
|
-
};
|
|
727
|
-
/** The notice's text. */
|
|
728
|
-
const updateNotice = (latest, current) => `Ork ${latest} is out (you have ${current}). Upgrade with: ork upgrade`;
|
|
777
|
+
// src/human.ts
|
|
729
778
|
/**
|
|
730
|
-
*
|
|
731
|
-
*
|
|
779
|
+
* @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
|
|
780
|
+
* permission prompts on the terminal, one at a time, and denies or defers
|
|
781
|
+
* when no one is there. After a choice it offers one optional line for a
|
|
782
|
+
* note, which Enter skips. In a run it is the decision queue's local
|
|
783
|
+
* answerer, racing `ork decide`.
|
|
732
784
|
*/
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
env: {
|
|
745
|
-
...process.env,
|
|
746
|
-
[UPDATE_CHILD_ENV]: "1"
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
child.on("error", () => {});
|
|
750
|
-
child.unref();
|
|
751
|
-
} catch {}
|
|
785
|
+
const PREVIEW = 160;
|
|
786
|
+
/** A document longer than this is offered (`d`) rather than printed. */
|
|
787
|
+
const SHORT_DOCUMENT_LINES = 40;
|
|
788
|
+
const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
|
|
789
|
+
const NOTE_PROMPT = " Note for the run (optional, Enter to skip): ";
|
|
790
|
+
const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
|
|
791
|
+
/** One line that shows what a tool is about to do. */
|
|
792
|
+
const previewInput = (toolName, input) => {
|
|
793
|
+
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
794
|
+
const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
|
|
795
|
+
return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
|
|
752
796
|
};
|
|
753
|
-
/** The
|
|
754
|
-
const
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
797
|
+
/** The option an answer names: its number, or its id. */
|
|
798
|
+
const pickOption = (answer, options, recommended) => {
|
|
799
|
+
const value = answer.trim();
|
|
800
|
+
if (value === "") return recommended;
|
|
801
|
+
const index = Number(value);
|
|
802
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
|
|
803
|
+
return options.find((o) => o.id === value)?.id;
|
|
760
804
|
};
|
|
761
|
-
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
if (cache === null || latest === null) return;
|
|
767
|
-
const text = updateNotice(latest, ORK_VERSION);
|
|
768
|
-
process.stderr.write(`\n${styleText("yellow", text, { stream: process.stderr })}\n`);
|
|
769
|
-
writeUpdateCache(file, {
|
|
770
|
-
...cache,
|
|
771
|
-
notified: {
|
|
772
|
-
version: latest,
|
|
773
|
-
at: now.toISOString()
|
|
774
|
-
}
|
|
775
|
-
});
|
|
776
|
-
} catch {}
|
|
805
|
+
const optionLine = (option, index, recommended) => {
|
|
806
|
+
const label = option.label === void 0 ? "" : ` ${option.label}`;
|
|
807
|
+
const description = option.description === void 0 ? "" : style("dim", ` - ${option.description}`);
|
|
808
|
+
const mark = option.id === recommended ? style("green", " (recommended)") : "";
|
|
809
|
+
return ` ${String(index + 1)}) ${style("bold", option.id)}${label}${description}${mark}`;
|
|
777
810
|
};
|
|
778
|
-
// src/update/upgrade.ts
|
|
779
811
|
/**
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
* shared with the server behind `ork ui`.
|
|
812
|
+
* Answers permission prompts and `ask_user` questions on the terminal, one at a
|
|
813
|
+
* time. Without a terminal it denies tools and leaves questions to the node. A
|
|
814
|
+
* withdrawn prompt (answered elsewhere) rejects, which frees the turn.
|
|
784
815
|
*/
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
status: "warn",
|
|
791
|
-
detail: `${current}; ${newer} is out`,
|
|
792
|
-
hint: "Run ork upgrade."
|
|
816
|
+
const createTerminalHuman = (io) => {
|
|
817
|
+
let rl;
|
|
818
|
+
let turn = Promise.resolve();
|
|
819
|
+
const say = (text) => {
|
|
820
|
+
io.output.write(`${text}\n`);
|
|
793
821
|
};
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
822
|
+
const prompt = (text, signal) => {
|
|
823
|
+
if (rl === void 0) {
|
|
824
|
+
rl = createInterface({
|
|
825
|
+
input: io.input,
|
|
826
|
+
output: io.output
|
|
827
|
+
});
|
|
828
|
+
if (io.interrupt !== void 0) rl.on("SIGINT", io.interrupt);
|
|
829
|
+
}
|
|
830
|
+
return signal === void 0 ? rl.question(text) : rl.question(text, { signal });
|
|
798
831
|
};
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
* checks, which `ork run` also reads to pick the one ready), and whether a
|
|
805
|
-
* run's socket path fits, through an injectable `exec` so tests need no real
|
|
806
|
-
* tools. Never prints a token or an account.
|
|
807
|
-
*/
|
|
808
|
-
const TIMEOUT_MS = 15e3;
|
|
809
|
-
const exec = (program, args) => new Promise((resolve) => {
|
|
810
|
-
execFile(program, args, {
|
|
811
|
-
timeout: TIMEOUT_MS,
|
|
812
|
-
encoding: "utf8"
|
|
813
|
-
}, (error, stdout) => {
|
|
814
|
-
resolve({
|
|
815
|
-
ok: error === null,
|
|
816
|
-
stdout
|
|
817
|
-
});
|
|
818
|
-
});
|
|
819
|
-
});
|
|
820
|
-
/** Ork's own version, against the last update check (no network). */
|
|
821
|
-
const orkCheck = () => versionCheck(ORK_VERSION, readUpdateCache(updateCachePath()), updateCheckOff(process.env, [], true) !== null);
|
|
822
|
-
const MIN_NODE = [24, 20];
|
|
823
|
-
const checkNode = (version) => {
|
|
824
|
-
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
825
|
-
const [minMajor, minMinor] = MIN_NODE;
|
|
826
|
-
return major > minMajor || major === minMajor && minor >= minMinor ? {
|
|
827
|
-
name: "node",
|
|
828
|
-
status: "ok",
|
|
829
|
-
detail: `v${version}`
|
|
830
|
-
} : {
|
|
831
|
-
name: "node",
|
|
832
|
-
status: "fail",
|
|
833
|
-
detail: `v${version}`,
|
|
834
|
-
hint: `Ork needs Node ${MIN_NODE.join(".")} or later.`
|
|
832
|
+
/** The optional line after a choice, when the asker takes a note. */
|
|
833
|
+
const takeNote = async (onNote, signal) => {
|
|
834
|
+
if (onNote === void 0) return;
|
|
835
|
+
const note = (await prompt(NOTE_PROMPT, signal)).trim();
|
|
836
|
+
if (note !== "") onNote(note);
|
|
835
837
|
};
|
|
836
|
-
|
|
837
|
-
const
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
hint: "Install git; Ork gives each writing node its own worktree."
|
|
844
|
-
}];
|
|
845
|
-
const inside = await run("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
846
|
-
return [{
|
|
847
|
-
name: "git",
|
|
848
|
-
status: "ok",
|
|
849
|
-
detail: firstLine(version.stdout)
|
|
850
|
-
}, inside.ok ? {
|
|
851
|
-
name: "git repo",
|
|
852
|
-
status: "ok",
|
|
853
|
-
detail: "inside a work tree"
|
|
854
|
-
} : {
|
|
855
|
-
name: "git repo",
|
|
856
|
-
status: "warn",
|
|
857
|
-
detail: "not a git repository",
|
|
858
|
-
hint: "Run Ork inside a git repository so nodes can use worktrees."
|
|
859
|
-
}];
|
|
860
|
-
};
|
|
861
|
-
/**
|
|
862
|
-
* Runs every check: Node and git, then every registered Claude driver and
|
|
863
|
-
* integration (ADR-022). Node, git, and at least one driver are required;
|
|
864
|
-
* integrations are optional.
|
|
865
|
-
*/
|
|
866
|
-
const runChecks = async (run, env, nodeVersion) => {
|
|
867
|
-
const [git, checked, tools] = await Promise.all([
|
|
868
|
-
checkGit(run),
|
|
869
|
-
driverChecks(run, env),
|
|
870
|
-
Promise.all(INTEGRATIONS.flatMap((i) => i.doctor === void 0 ? [] : [i.doctor(run, env)]))
|
|
871
|
-
]);
|
|
872
|
-
const drivers = checked.map(({ result }) => result);
|
|
873
|
-
const results = [
|
|
874
|
-
checkNode(nodeVersion),
|
|
875
|
-
...git,
|
|
876
|
-
...drivers,
|
|
877
|
-
...tools
|
|
878
|
-
];
|
|
879
|
-
return {
|
|
880
|
-
results,
|
|
881
|
-
ok: drivers.some((d) => d.status === "ok") && !results.some((r) => r.status === "fail")
|
|
838
|
+
const choose = async (hint, options, recommended, signal, document) => {
|
|
839
|
+
const answer = await prompt(` Choose${hint}: `, signal);
|
|
840
|
+
const picked = pickOption(answer, options, recommended);
|
|
841
|
+
if (picked !== void 0) return picked;
|
|
842
|
+
if (document !== void 0 && answer.trim() === "d") say(document);
|
|
843
|
+
else say(style("dim", " Type an option's number or id."));
|
|
844
|
+
return choose(hint, options, recommended, signal, document);
|
|
882
845
|
};
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
* to the store, a second at a time. `home` is the user's `~`.
|
|
888
|
-
*/
|
|
889
|
-
const socketCheck = (home, platform) => {
|
|
890
|
-
const { bytes, max } = socketRoom(home, platform);
|
|
891
|
-
if (max === void 0 || bytes <= max) return {
|
|
892
|
-
name: "sockets",
|
|
893
|
-
status: "ok",
|
|
894
|
-
detail: max === void 0 ? "not Unix sockets here" : `${String(bytes)} of ${String(max)} bytes`
|
|
846
|
+
const oneAtATime = (work) => {
|
|
847
|
+
const result = turn.then(work);
|
|
848
|
+
turn = result.catch(() => null);
|
|
849
|
+
return result;
|
|
895
850
|
};
|
|
896
851
|
return {
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
852
|
+
permit: ({ nodeId, toolName, input, signal, onNote }) => oneAtATime(async () => {
|
|
853
|
+
if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
|
|
854
|
+
say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} wants to run ${previewInput(toolName, input)}`);
|
|
855
|
+
if (!io.interactive) {
|
|
856
|
+
say(style("dim", ` denied: ${NO_ONE_TO_PERMIT}`));
|
|
857
|
+
return {
|
|
858
|
+
allow: false,
|
|
859
|
+
message: NO_ONE_TO_PERMIT
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
const answer = (await prompt(" Allow? [y/N] ", signal)).trim().toLowerCase();
|
|
863
|
+
if (answer === "y" || answer === "yes") return { allow: true };
|
|
864
|
+
await takeNote(onNote, signal);
|
|
865
|
+
return {
|
|
866
|
+
allow: false,
|
|
867
|
+
message: "The user did not allow this tool."
|
|
868
|
+
};
|
|
869
|
+
}),
|
|
870
|
+
ask: ({ nodeId, question, options, recommended, context, document, signal, onNote }) => oneAtATime(async () => {
|
|
871
|
+
if (signal?.aborted === true) throw new Error("the question was withdrawn");
|
|
872
|
+
const documentLines = document?.trimEnd().split("\n").length ?? 0;
|
|
873
|
+
if (document !== void 0 && documentLines <= SHORT_DOCUMENT_LINES) say(document);
|
|
874
|
+
say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} asks: ${question}`);
|
|
875
|
+
if (context !== void 0) say(style("dim", ` ${context.replaceAll("\n", "\n ")}`));
|
|
876
|
+
for (const [index, option] of options.entries()) say(optionLine(option, index, recommended));
|
|
877
|
+
if (!io.interactive) {
|
|
878
|
+
const answer = recommended ?? NO_ONE_TO_ASK;
|
|
879
|
+
say(style("dim", ` no terminal; answered ${answer}`));
|
|
880
|
+
return answer;
|
|
881
|
+
}
|
|
882
|
+
const fallback = options.findIndex((o) => o.id === recommended);
|
|
883
|
+
const hint = fallback === -1 ? "" : ` [${String(fallback + 1)}]`;
|
|
884
|
+
if (documentLines > SHORT_DOCUMENT_LINES) say(` Document: ${String(documentLines)} lines. Type d to read it.`);
|
|
885
|
+
const picked = await choose(hint, options, recommended, signal, document);
|
|
886
|
+
await takeNote(onNote, signal);
|
|
887
|
+
return picked;
|
|
888
|
+
}),
|
|
889
|
+
close: () => {
|
|
890
|
+
rl?.close();
|
|
891
|
+
}
|
|
901
892
|
};
|
|
902
893
|
};
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
const report = triggersReport(store, store.projects(), home);
|
|
912
|
-
const broken = report.broken.map(({ file, message }) => ({
|
|
913
|
-
name: "triggers",
|
|
914
|
-
status: "warn",
|
|
915
|
-
detail: `${file}: ${message}`,
|
|
916
|
-
hint: "Its triggers are off until it can be read."
|
|
917
|
-
}));
|
|
918
|
-
const live = report.triggers.filter(({ standing }) => standing === "live");
|
|
919
|
-
if (live.length === 0) return broken;
|
|
920
|
-
const late = lateTriggers(report, /* @__PURE__ */ new Date());
|
|
921
|
-
if (late.length === 0) return [...broken, {
|
|
922
|
-
name: "ork watch",
|
|
923
|
-
status: "ok",
|
|
924
|
-
detail: `${String(live.length)} trigger(s) looking`
|
|
925
|
-
}];
|
|
926
|
-
return [...broken, {
|
|
927
|
-
name: "ork watch",
|
|
928
|
-
status: "warn",
|
|
929
|
-
detail: `not looking: ${late.map(({ trigger }) => trigger).join(", ")}`,
|
|
930
|
-
hint: report.pid === void 0 ? "Start it with `ork watch --detach`." : "See `ork watch --status`."
|
|
931
|
-
}];
|
|
932
|
-
} finally {
|
|
933
|
-
store.close();
|
|
934
|
-
}
|
|
894
|
+
// src/run/format-result.ts
|
|
895
|
+
const RUN_MARK = {
|
|
896
|
+
succeeded: style("green", "✓"),
|
|
897
|
+
failed: style("red", "✗"),
|
|
898
|
+
budget_exhausted: style("yellow", "!"),
|
|
899
|
+
quota_exhausted: style("yellow", "!"),
|
|
900
|
+
timed_out: style("yellow", "!"),
|
|
901
|
+
interrupted: style("yellow", "!")
|
|
935
902
|
};
|
|
936
|
-
|
|
903
|
+
/** `1 failure handled`, `2 failures handled`. */
|
|
904
|
+
const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" : "failures"} handled`;
|
|
937
905
|
/**
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
* ready on this machine, said in one line. With none ready, the run stops
|
|
941
|
-
* before any session starts, with the hints `ork doctor` gives. A resumed run
|
|
942
|
-
* keeps the driver it recorded.
|
|
906
|
+
* The run summary: status, cost, what ran, what was skipped and which failures
|
|
907
|
+
* an edge handled, branches to look at, why, and mail to nodes that never ran.
|
|
943
908
|
*/
|
|
944
|
-
|
|
945
|
-
const
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
const [
|
|
949
|
-
|
|
909
|
+
const formatRunResult = (result) => {
|
|
910
|
+
const ran = result.nodes.filter((n) => n.runs > 0).length;
|
|
911
|
+
const skipped = result.skipped ?? result.nodes.length - ran;
|
|
912
|
+
const handled = result.failuresHandled ?? 0;
|
|
913
|
+
const lines = ["", `${RUN_MARK[result.status]} run ${result.runId} ${result.status.replace("_", " ")} ${style("dim", `· ${cost(result.costUsd)} · ${String(ran)} ran, ${String(skipped)} skipped${handled === 0 ? "" : `, ${failuresHandled(handled)}`}`)}`];
|
|
914
|
+
for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
|
|
915
|
+
for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
|
|
916
|
+
for (const { node, messages } of result.unread ?? []) lines.push(` ${style(["yellow", "bold"], "warning")} ${node} never ran, so ${messages === 1 ? "1 message" : `${String(messages)} messages`} to it went unread`);
|
|
917
|
+
return lines;
|
|
950
918
|
};
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
ok: false,
|
|
959
|
-
lines: [
|
|
960
|
-
`No Claude driver is ready here, and ${names.join(", ")} ${names.length === 1 ? "names" : "name"} none.`,
|
|
961
|
-
...ready.hints,
|
|
962
|
-
"See `ork doctor`."
|
|
963
|
-
]
|
|
964
|
-
};
|
|
965
|
-
const why = ready.ready === 1 ? `the one ready here (${ready.detail})` : `more than one is ready, and a run takes ${ready.driver} first`;
|
|
966
|
-
return {
|
|
967
|
-
ok: true,
|
|
968
|
-
driver: ready.driver,
|
|
969
|
-
line: `${subject(names, all)} the ${ready.driver} driver: ${why}.`
|
|
970
|
-
};
|
|
919
|
+
// src/run/format.ts
|
|
920
|
+
/** Who answered a decision, as the log says it. */
|
|
921
|
+
const ANSWERED_BY = {
|
|
922
|
+
terminal: "terminal",
|
|
923
|
+
decide: "ork decide",
|
|
924
|
+
ui: "ork ui",
|
|
925
|
+
owner: "its owner"
|
|
971
926
|
};
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
|
|
927
|
+
const STATUS_MARK = {
|
|
928
|
+
pass: style("green", "✓"),
|
|
929
|
+
done: style("green", "✓"),
|
|
930
|
+
changes_requested: style("yellow", "!"),
|
|
931
|
+
fail: style("red", "✗")
|
|
932
|
+
};
|
|
933
|
+
const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
|
|
934
|
+
const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${line}`);
|
|
935
|
+
/** The usage line: cost, tokens, turns and time. */
|
|
936
|
+
const formatUsage = ({ usage }) => style("dim", [
|
|
937
|
+
cost(usage.costUsd),
|
|
938
|
+
`${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
|
|
939
|
+
`${String(usage.turns)} turns`,
|
|
940
|
+
seconds(usage.durationMs)
|
|
941
|
+
].join(" · "));
|
|
942
|
+
/** How a finished session reads on the terminal. */
|
|
943
|
+
const formatOutcome = (nodeId, outcome) => {
|
|
944
|
+
const { report } = outcome;
|
|
945
|
+
if (report === null) return [
|
|
946
|
+
`${style("red", "✗")} ${style("bold", nodeId)} ended without a report (${outcome.subtype})`,
|
|
947
|
+
...outcome.errors.map((e) => ` ${style("red", e)}`),
|
|
948
|
+
` ${formatUsage(outcome)}`
|
|
949
|
+
];
|
|
950
|
+
const lines = [`${STATUS_MARK[report.status] ?? "-"} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
|
|
951
|
+
if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
|
|
952
|
+
for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
|
|
953
|
+
for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
|
|
954
|
+
lines.push(` ${formatUsage(outcome)}`);
|
|
955
|
+
return lines;
|
|
956
|
+
};
|
|
957
|
+
/** The transcript lines for one event; tool names come from `tools`. */
|
|
958
|
+
const formatEvent = (nodeId, event, tools) => {
|
|
959
|
+
if (event.type === "session.started") {
|
|
960
|
+
const fork = event.fork === true ? ", a fork to answer a question" : "";
|
|
961
|
+
return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId}${fork})`)];
|
|
962
|
+
}
|
|
963
|
+
if (event.type === "assistant.text") return indent(event.text);
|
|
964
|
+
if (event.type === "session.waiting") return indent(event.reason);
|
|
965
|
+
if (event.type === "tool.called") {
|
|
966
|
+
tools.set(event.id, event.name);
|
|
967
|
+
return [style("dim", ` · ${previewInput(event.name, event.input)}`)];
|
|
968
|
+
}
|
|
969
|
+
if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
|
|
970
|
+
if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
|
|
971
|
+
return formatOutcome(nodeId, event.outcome);
|
|
972
|
+
};
|
|
973
|
+
const items = (count) => `${String(count)} ${count === 1 ? "item" : "items"}`;
|
|
974
|
+
/** Transcript lines for one run event, prefixed with the node they are from. */
|
|
975
|
+
const formatRunEvent = (event, width, tools, terminal = false) => {
|
|
976
|
+
const tag = (id) => style("cyan", `${id.padEnd(width)} │`);
|
|
977
|
+
if (event.type === "run.started") return [style("dim", `▸ run ${event.runId} of ${event.workflow} (starts at ${event.entries.join(", ")})`), ...event.settings?.trigger === void 0 ? [] : [style("dim", ` started by trigger ${event.settings.trigger}; untrusted inputs: ${(event.settings.untrusted ?? []).join(", ") || "none"}`)]];
|
|
978
|
+
if (event.type === "node.started") {
|
|
979
|
+
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
980
|
+
const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
|
|
981
|
+
return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
|
|
982
|
+
}
|
|
983
|
+
if (event.type === "node.event") {
|
|
984
|
+
const inner = event.event;
|
|
985
|
+
if (inner.type === "session.finished") return [];
|
|
986
|
+
const fork = inner.fork === true ? style("dim", "fork · ") : "";
|
|
987
|
+
if (inner.type === "session.started") {
|
|
988
|
+
const why = inner.fork === true ? ", to answer a question" : "";
|
|
989
|
+
return [`${tag(event.id)} ${fork}${style("dim", `${inner.model}, session ${inner.sessionId}${why}`)}`];
|
|
990
|
+
}
|
|
991
|
+
return formatEvent(event.id, event.event, tools).map((line) => `${tag(event.id)} ${fork}${line.replace(/^ {2}/u, "")}`);
|
|
992
|
+
}
|
|
993
|
+
if (event.type === "node.finished") {
|
|
994
|
+
const conflict = hasConflict(event.output);
|
|
995
|
+
const mark = event.ok ? style(conflict ? "yellow" : "green", conflict ? "!" : "✓") : style("red", "✗");
|
|
996
|
+
const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
|
|
997
|
+
return [`${tag(event.id)} ${mark} ${nodeVerdict(event.output, event.ok, event.error)}${spent}`];
|
|
998
|
+
}
|
|
999
|
+
if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
|
|
1000
|
+
if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
|
|
1001
|
+
if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
|
|
1002
|
+
if (event.type === "watch.changed") return [`${tag(event.id)} ${style("dim", `look ${String(event.looks)}:`)} ${event.said}`];
|
|
1003
|
+
if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
|
|
1004
|
+
if (event.type === "edge.taken") return [];
|
|
1005
|
+
if (event.type === "worktree") {
|
|
1006
|
+
const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
|
|
1007
|
+
const what = event.action === "removed" && event.branch !== void 0 ? `worktree of ${event.branch} (branch kept)` : `${event.branch ?? event.path}${detail}`;
|
|
1008
|
+
const line = `${event.action} ${what}`;
|
|
1009
|
+
return [`${tag(event.id)} ${event.action === "conflict" ? style("yellow", `! ${line}`) : style("dim", `⎇ ${line}`)}`];
|
|
1010
|
+
}
|
|
1011
|
+
if (event.type === "decision.opened") {
|
|
1012
|
+
const what = event.kind === "permission" ? "asks to" : "asks:";
|
|
1013
|
+
const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
|
|
1014
|
+
const where = event.owner === void 0 ? `decision ${event.decision}; ${terminal ? "answer here or with ork decide" : "ork decide"}` : `decision ${event.decision}, via ${event.owner}`;
|
|
1015
|
+
return [`${tag(event.node)} ${style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
|
|
1016
|
+
}
|
|
1017
|
+
if (event.type === "decision.answered") {
|
|
1018
|
+
const by = event.by === "owner" && event.owner !== void 0 ? event.owner : ANSWERED_BY[event.by];
|
|
1019
|
+
const said = event.note === void 0 ? "" : ` · said: ${event.note.replaceAll(/\s+/gu, " ")}`;
|
|
1020
|
+
return [`${tag(event.node)} ${style("dim", `→ ${event.answer} (${by})${said}`)}`];
|
|
1021
|
+
}
|
|
1022
|
+
if (event.type === "decision.withdrawn") return [`${tag(event.node)} ${style("dim", "→ question withdrawn")}`];
|
|
1023
|
+
if (event.type === "run.command") {
|
|
1024
|
+
const node = event.node === void 0 ? "" : ` --node ${event.node}`;
|
|
1025
|
+
return [style(["yellow", "bold"], `! ork ${event.command}${node} received`)];
|
|
1026
|
+
}
|
|
1027
|
+
if (event.type === "node.held") {
|
|
1028
|
+
const at = event.at === "tool" ? "its tool call waits for ork continue" : "it starts after ork continue";
|
|
1029
|
+
return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
|
|
1030
|
+
}
|
|
1031
|
+
if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
|
|
1032
|
+
if (event.type === "each.listed") {
|
|
1033
|
+
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
1034
|
+
return [`${tag(event.node)} ${style("dim", `${items(event.count)}${round}`)}`];
|
|
1035
|
+
}
|
|
1036
|
+
if (event.type === "each.exceeded") {
|
|
1037
|
+
const over = `${items(event.count)}, over maxItems ${String(event.maxItems)}`;
|
|
1038
|
+
return [`${tag(event.node)} ${style("yellow", `! ${over}`)}`];
|
|
1039
|
+
}
|
|
1040
|
+
if (event.type === "run.finished") return formatRunResult(event.result);
|
|
1041
|
+
if (event.type === "run.resumed") {
|
|
1042
|
+
const [one] = event.kept;
|
|
1043
|
+
const kept = one === void 0 ? "every node runs again" : event.kept.length === 1 ? `${one} keeps its result` : `${event.kept.join(", ")} keep their results`;
|
|
1044
|
+
return ["", style("dim", `▸ resumed: ${kept}`)];
|
|
1045
|
+
}
|
|
1046
|
+
if (event.type === "feedback.recorded") {
|
|
1047
|
+
const { target, kind, title } = event.feedback;
|
|
1048
|
+
const line = style("dim", `✎ feedback (${target}, ${kind}): ${title}`);
|
|
1049
|
+
return [event.node === void 0 ? line : `${tag(event.node)} ${line}`];
|
|
1050
|
+
}
|
|
1051
|
+
if (event.type === "user.message") {
|
|
1052
|
+
const note = event.delivered === "queued" ? " (for its next session)" : "";
|
|
1053
|
+
return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
|
|
1054
|
+
}
|
|
1055
|
+
return formatBusEvent(event).map((line) => `${tag(event.node)} ${line}`);
|
|
987
1056
|
};
|
|
988
1057
|
// src/runs/chat.ts
|
|
989
1058
|
/**
|
|
@@ -994,12 +1063,11 @@ const runReadyDriver = async (workflow, options) => {
|
|
|
994
1063
|
* where it ran, as the run's sessions were set up (ADR-065), and the exchange
|
|
995
1064
|
* is added to the run's record.
|
|
996
1065
|
*/
|
|
997
|
-
/**
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
};
|
|
1066
|
+
/**
|
|
1067
|
+
* The declared node a spawned worker or an item of a fanned node is an instance
|
|
1068
|
+
* of.
|
|
1069
|
+
*/
|
|
1070
|
+
const instanceOf = (nodes, id) => id in nodes ? void 0 : templateOf(id, new Set(Object.keys(nodes)));
|
|
1003
1071
|
/** The run's sessions loaded the machine's Claude setup, so this one does. */
|
|
1004
1072
|
const machineClaudeOf = (events) => events.some(({ event }) => event.type === "run.started" && event.settings?.machineClaude === true) ? { machineClaude: true } : {};
|
|
1005
1073
|
/**
|
|
@@ -1020,7 +1088,7 @@ const resumeChat = async (options) => {
|
|
|
1020
1088
|
const notice = chatNotice(store, place, options.home);
|
|
1021
1089
|
if (notice !== null) options.out(notice);
|
|
1022
1090
|
const runDir = runDirOf(place, run.id);
|
|
1023
|
-
const template =
|
|
1091
|
+
const template = instanceOf(workflow.nodes, node);
|
|
1024
1092
|
const plan = planNodeSession(workflow, node, {
|
|
1025
1093
|
cwd,
|
|
1026
1094
|
inputs: {},
|
|
@@ -1035,10 +1103,12 @@ const resumeChat = async (options) => {
|
|
|
1035
1103
|
...template === void 0 ? {} : { template }
|
|
1036
1104
|
});
|
|
1037
1105
|
if (!plan.ok) return plan.error;
|
|
1106
|
+
const servers = namedMcpEntries(run.project, plan.mcp);
|
|
1107
|
+
if (!servers.ok) return servers.error;
|
|
1038
1108
|
let driver = options.driver ?? session.driver ?? plan.driver;
|
|
1039
1109
|
if (driver === void 0) {
|
|
1040
1110
|
const ready = readyDriver(await (options.driverChecks ?? checksHere)());
|
|
1041
|
-
if (!ready.ok) return ["No
|
|
1111
|
+
if (!ready.ok) return ["No agent driver is ready here.", ...ready.hints].join(" ");
|
|
1042
1112
|
driver = ready.driver;
|
|
1043
1113
|
}
|
|
1044
1114
|
const record = appendRecorder(store, run.id, runDir);
|
|
@@ -1048,8 +1118,9 @@ const resumeChat = async (options) => {
|
|
|
1048
1118
|
body: options.body,
|
|
1049
1119
|
delivered: "resumed"
|
|
1050
1120
|
});
|
|
1051
|
-
const started = (await options.runtime(driver)).start({
|
|
1121
|
+
const started = (await options.runtime(driver, placeFiles(place).runs)).start({
|
|
1052
1122
|
...plan.spec,
|
|
1123
|
+
...servers.value,
|
|
1053
1124
|
resume: session.sessionId,
|
|
1054
1125
|
prompt: options.body,
|
|
1055
1126
|
...machineClaudeOf(events)
|
|
@@ -1084,12 +1155,22 @@ const createLogPrinter = (options) => {
|
|
|
1084
1155
|
return (stored) => {
|
|
1085
1156
|
const { event } = stored;
|
|
1086
1157
|
if (event.type === "run.started") width = Math.max(...event.nodes.map((n) => n.id.length), 1);
|
|
1087
|
-
if (options.node !== void 0 &&
|
|
1158
|
+
if (options.node !== void 0 && !isNodeEvent(event, options.node)) return [];
|
|
1088
1159
|
if (options.json) return [JSON.stringify(stored)];
|
|
1089
1160
|
return formatRunEvent(event, width, tools);
|
|
1090
1161
|
};
|
|
1091
1162
|
};
|
|
1092
1163
|
/**
|
|
1164
|
+
* Why `--node` names nothing in this run, or `null`: no event names it and the
|
|
1165
|
+
* run didn't declare it. A live run may still start it, so that is not
|
|
1166
|
+
* refused.
|
|
1167
|
+
*/
|
|
1168
|
+
const unknownNode = (stored, node, live) => {
|
|
1169
|
+
const named = stored.some(({ event }) => isNodeEvent(event, node));
|
|
1170
|
+
const declared = stored.some(({ event }) => event.type === "run.started" && event.nodes.some((n) => n.id === node));
|
|
1171
|
+
return named || declared || live ? null : `Run has no node ${node}.`;
|
|
1172
|
+
};
|
|
1173
|
+
/**
|
|
1093
1174
|
* Prints a run's events; with `follow`, then each new one as it is stored:
|
|
1094
1175
|
* pushed over the run's socket, or polled from the store when there is none.
|
|
1095
1176
|
* Returns when `run.finished` is stored, the run's process is gone, or `signal`
|
|
@@ -1107,6 +1188,31 @@ const followRun = async (store, run, out, options) => {
|
|
|
1107
1188
|
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
1108
1189
|
}) === "crashed") out(style("red", `run ${run.id} stopped without finishing (its process is gone)`));
|
|
1109
1190
|
};
|
|
1191
|
+
// src/run/node-notes.ts
|
|
1192
|
+
/**
|
|
1193
|
+
* @file What a run of one node (`ork run --node`) leaves out, said before its
|
|
1194
|
+
* session starts: the messages, steps and `forEach` it does not run. A node
|
|
1195
|
+
* with `forEach` (RFC-013) runs once with no item when it has `items`, and is
|
|
1196
|
+
* refused when it has `from`, as the node that reports its list is not run.
|
|
1197
|
+
*/
|
|
1198
|
+
/** A warning line, as the run prints it. */
|
|
1199
|
+
const noteLines = (lines) => lines.map((line) => `${style(["yellow", "bold"], "warning")} ${line}`);
|
|
1200
|
+
/** Why a node can't run alone, or what a run of it leaves out. */
|
|
1201
|
+
const oneNodeNotes = (workflow, id) => {
|
|
1202
|
+
const node = workflow.nodes[id];
|
|
1203
|
+
if (node === void 0) return { warnings: [] };
|
|
1204
|
+
const each = "forEach" in node ? node.forEach : void 0;
|
|
1205
|
+
if (each !== void 0 && "from" in each) return {
|
|
1206
|
+
error: `\`${id}\` runs once for each item of ${each.from}, a list another node reports, and a run of one node does not run that node; run the whole workflow, or give it \`items\` of its own`,
|
|
1207
|
+
warnings: []
|
|
1208
|
+
};
|
|
1209
|
+
const steps = [...beforeOf(node, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in node && node.verify !== void 0 ? ["verify"] : []];
|
|
1210
|
+
return { warnings: [
|
|
1211
|
+
..."notify" in node && node.notify ? [`${id}'s notify: does nothing here: a run of one node has no message bus.`] : [],
|
|
1212
|
+
...each === void 0 ? [] : [`${id}'s forEach: does not fan out in a run of one node: it runs once, with no item.`],
|
|
1213
|
+
...steps.map((step) => `${id}'s ${step}: does not run in a run of one node.`)
|
|
1214
|
+
] };
|
|
1215
|
+
};
|
|
1110
1216
|
// src/run/origin.ts
|
|
1111
1217
|
/**
|
|
1112
1218
|
* @file Where a run's feedback comes from, beyond what its events say: Ork's
|
|
@@ -1114,6 +1220,8 @@ const followRun = async (store, run, out, options) => {
|
|
|
1114
1220
|
* content hash.
|
|
1115
1221
|
*/
|
|
1116
1222
|
/** Ork's version and the workflow's hash, for the run's feedback records. */
|
|
1223
|
+
/** The Ork version a run records in `run.started` and `run.resumed`. */
|
|
1224
|
+
const ORK_RUN_VERSION = ORK_VERSION;
|
|
1117
1225
|
const feedbackOrigin = (workflowFile) => {
|
|
1118
1226
|
const origin = { version: ORK_VERSION };
|
|
1119
1227
|
try {
|
|
@@ -1147,6 +1255,8 @@ const runNode = async (options) => {
|
|
|
1147
1255
|
if (!pairs.ok) return failed([`--input: ${pairs.error}`]);
|
|
1148
1256
|
const inputs = resolveInputs(workflow, pairs.raw);
|
|
1149
1257
|
if (!inputs.ok) return failed(inputs.errors);
|
|
1258
|
+
const notes = oneNodeNotes(workflow, options.nodeId);
|
|
1259
|
+
if (notes.error !== void 0) return failed([notes.error]);
|
|
1150
1260
|
const limits = runLimits(options.store, place, options.home);
|
|
1151
1261
|
const refused = limitsRefusal(limits, workflow);
|
|
1152
1262
|
if (refused.length > 0) return failed(refused);
|
|
@@ -1166,6 +1276,8 @@ const runNode = async (options) => {
|
|
|
1166
1276
|
pluginDir: path.join(runDir, "plugins", options.nodeId)
|
|
1167
1277
|
});
|
|
1168
1278
|
if (!plan.ok) return failed([plan.error]);
|
|
1279
|
+
const servers = namedMcpEntries(projectRoot, plan.mcp);
|
|
1280
|
+
if (!servers.ok) return failed([servers.error]);
|
|
1169
1281
|
const named = sessionDriver({
|
|
1170
1282
|
override: options.driver,
|
|
1171
1283
|
named: plan.driver
|
|
@@ -1174,12 +1286,13 @@ const runNode = async (options) => {
|
|
|
1174
1286
|
if (ready?.ok === false) return failed(ready.lines);
|
|
1175
1287
|
if (ready?.line !== void 0) options.err(ready.line);
|
|
1176
1288
|
const driver = named ?? ready?.driver;
|
|
1177
|
-
if (driver === void 0) return failed(["No
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1289
|
+
if (driver === void 0) return failed(["No agent driver is ready here."]);
|
|
1290
|
+
const unfit = unfitLines(workflow, options.drivers, {
|
|
1291
|
+
override: driver,
|
|
1292
|
+
only: options.nodeId
|
|
1293
|
+
});
|
|
1294
|
+
if (unfit.length > 0) return failed(unfit);
|
|
1295
|
+
for (const line of noteLines([...notes.warnings, ...plan.warnings])) options.err(line);
|
|
1183
1296
|
const id = options.nodeId;
|
|
1184
1297
|
const recorder = createRecorder(options.store, {
|
|
1185
1298
|
id: runId,
|
|
@@ -1237,9 +1350,12 @@ const runNode = async (options) => {
|
|
|
1237
1350
|
nodeType,
|
|
1238
1351
|
round: 1
|
|
1239
1352
|
});
|
|
1240
|
-
const runtime = await options.runtime(driver);
|
|
1353
|
+
const runtime = await options.runtime(driver, placeFiles(place).runs);
|
|
1241
1354
|
if (options.machineClaude === true) plan.spec.machineClaude = true;
|
|
1242
|
-
const session = runtime.start(
|
|
1355
|
+
const session = runtime.start({
|
|
1356
|
+
...plan.spec,
|
|
1357
|
+
...servers.value
|
|
1358
|
+
}, {
|
|
1243
1359
|
human: control.human,
|
|
1244
1360
|
...graph === null ? {} : { feedback: createFeedback({
|
|
1245
1361
|
graph,
|
|
@@ -1498,6 +1614,14 @@ const preflight = async (options, given) => {
|
|
|
1498
1614
|
lines: driver.lines
|
|
1499
1615
|
};
|
|
1500
1616
|
if (driver.line !== void 0) options.err(driver.line);
|
|
1617
|
+
const unfit = unfitLines(given.effective, options.drivers, {
|
|
1618
|
+
override: options.driver,
|
|
1619
|
+
ready: driver.driver ?? options.resume?.settings.readyDriver
|
|
1620
|
+
});
|
|
1621
|
+
if (unfit.length > 0) return {
|
|
1622
|
+
ok: false,
|
|
1623
|
+
lines: unfit
|
|
1624
|
+
};
|
|
1501
1625
|
const checkout = await prepareCheckout(workflow, {
|
|
1502
1626
|
...options,
|
|
1503
1627
|
cwd: place.root
|
|
@@ -1588,7 +1712,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1588
1712
|
for (const event of signals(raw)) {
|
|
1589
1713
|
control.publish(record(event));
|
|
1590
1714
|
if (options.json) options.out(JSON.stringify(event));
|
|
1591
|
-
else for (const line of formatRunEvent(event, width, tools)) options.out(line);
|
|
1715
|
+
else for (const line of formatRunEvent(event, width, tools, options.human !== void 0)) options.out(line);
|
|
1592
1716
|
}
|
|
1593
1717
|
};
|
|
1594
1718
|
const workspace = root === void 0 ? void 0 : createWorkspace({
|
|
@@ -1605,7 +1729,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1605
1729
|
});
|
|
1606
1730
|
const maskedNames = maskedNamesOfRun(workflow, place, projects);
|
|
1607
1731
|
const executorOptions = {
|
|
1608
|
-
runtimeFor: options.runtime,
|
|
1732
|
+
runtimeFor: (driver) => options.runtime(driver, placeFiles(place).runs),
|
|
1609
1733
|
inPlace: options.inPlace,
|
|
1610
1734
|
maskedNames
|
|
1611
1735
|
};
|
|
@@ -1614,10 +1738,11 @@ const runWorkflowFile = async (options) => {
|
|
|
1614
1738
|
if (options.tools !== void 0) executorOptions.tools = options.tools;
|
|
1615
1739
|
const script = process.argv[1];
|
|
1616
1740
|
if (script !== void 0 && onPath(script)) executorOptions.orkBin = script;
|
|
1617
|
-
const replay = options.resume === void 0 ? void 0 : createReplay(options.resume.history, options.resume.cutOff, options.resume.mail, options.resume.earlier);
|
|
1741
|
+
const replay = options.resume === void 0 ? void 0 : createReplay(options.resume.history, options.resume.cutOff, options.resume.mail, options.resume.earlier, options.resume.fanOuts);
|
|
1618
1742
|
if (replay !== void 0) onEvent({
|
|
1619
1743
|
type: "run.resumed",
|
|
1620
|
-
kept: replay.kept()
|
|
1744
|
+
kept: replay.kept(),
|
|
1745
|
+
ork: ORK_RUN_VERSION
|
|
1621
1746
|
});
|
|
1622
1747
|
const ports = callPorts({
|
|
1623
1748
|
...options,
|
|
@@ -1639,6 +1764,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1639
1764
|
executors: builtinExecutors(executorOptions),
|
|
1640
1765
|
human: options.resume === void 0 ? control.human : replayLoopAnswers(control.human, options.resume.loopAnswers),
|
|
1641
1766
|
onEvent,
|
|
1767
|
+
orkVersion: ORK_RUN_VERSION,
|
|
1642
1768
|
settings: {
|
|
1643
1769
|
inPlace: options.inPlace,
|
|
1644
1770
|
...options.driver === void 0 ? {} : { driver: options.driver },
|
|
@@ -1683,9 +1809,11 @@ const flagValues = (rawArgs, flag) => {
|
|
|
1683
1809
|
}
|
|
1684
1810
|
return values;
|
|
1685
1811
|
};
|
|
1686
|
-
const isDriver = (value) =>
|
|
1812
|
+
const isDriver = (value) => AGENT_DRIVERS.some((d) => d.id === value);
|
|
1813
|
+
/** Why `--driver` refuses a value: a retired id names its replacement. */
|
|
1814
|
+
const badDriver = (value) => `--${retiredDriverHint(value) ?? `driver must be one of ${AGENT_DRIVERS.map((d) => d.id).join(", ")}, got \`${value}\``}`;
|
|
1687
1815
|
/** Loads the drivers only when a run starts, so other commands stay fast. */
|
|
1688
|
-
const loadRuntime = async (driver) => (await import("./src-
|
|
1816
|
+
const loadRuntime = async (driver, runs) => (await import("./src-oaJYIddp.mjs")).createRuntime(driver, sessionEnv(process.env), runs);
|
|
1689
1817
|
/**
|
|
1690
1818
|
* Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
|
|
1691
1819
|
* so that the run ends tidily and leaves no result unscrubbed.
|
|
@@ -1703,11 +1831,18 @@ const onInterrupt = (stop) => {
|
|
|
1703
1831
|
process.once("SIGHUP", interrupt);
|
|
1704
1832
|
};
|
|
1705
1833
|
/** The terminal that answers a run's questions; none in a detached run. */
|
|
1706
|
-
const terminalHuman = () =>
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1834
|
+
const terminalHuman = () => {
|
|
1835
|
+
const detached = process.env[DETACHED_ENV] === "1";
|
|
1836
|
+
if (detached) writeErr(style("dim", "detached run: no terminal asks; answer with ork decide"));
|
|
1837
|
+
return detached ? void 0 : createTerminalHuman({
|
|
1838
|
+
input: process.stdin,
|
|
1839
|
+
output: process.stderr,
|
|
1840
|
+
interactive: process.stdin.isTTY,
|
|
1841
|
+
interrupt: () => {
|
|
1842
|
+
process.kill(process.pid, "SIGINT");
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
};
|
|
1711
1846
|
const run = defineCommand({
|
|
1712
1847
|
meta: {
|
|
1713
1848
|
name: "run",
|
|
@@ -1729,7 +1864,7 @@ const run = defineCommand({
|
|
|
1729
1864
|
},
|
|
1730
1865
|
driver: {
|
|
1731
1866
|
type: "string",
|
|
1732
|
-
description: "
|
|
1867
|
+
description: "claude-code (default: the driver ready here)"
|
|
1733
1868
|
},
|
|
1734
1869
|
"in-place": {
|
|
1735
1870
|
type: "boolean",
|
|
@@ -1787,7 +1922,7 @@ const run = defineCommand({
|
|
|
1787
1922
|
},
|
|
1788
1923
|
run: async ({ args, rawArgs }) => {
|
|
1789
1924
|
if (args.driver !== void 0 && !isDriver(args.driver)) {
|
|
1790
|
-
writeErr(
|
|
1925
|
+
writeErr(badDriver(args.driver));
|
|
1791
1926
|
process.exitCode = 1;
|
|
1792
1927
|
return;
|
|
1793
1928
|
}
|
|
@@ -1945,7 +2080,7 @@ const chat = defineCommand({
|
|
|
1945
2080
|
},
|
|
1946
2081
|
driver: {
|
|
1947
2082
|
type: "string",
|
|
1948
|
-
description: "For a finished run:
|
|
2083
|
+
description: "For a finished run: claude-code (default: the one it ran with)"
|
|
1949
2084
|
}
|
|
1950
2085
|
},
|
|
1951
2086
|
run: async ({ args }) => {
|
|
@@ -1971,7 +2106,7 @@ const chat = defineCommand({
|
|
|
1971
2106
|
return;
|
|
1972
2107
|
}
|
|
1973
2108
|
if (args.driver !== void 0 && !isDriver(args.driver)) {
|
|
1974
|
-
writeErr(
|
|
2109
|
+
writeErr(`${badDriver(args.driver)}.`);
|
|
1975
2110
|
process.exitCode = 1;
|
|
1976
2111
|
return;
|
|
1977
2112
|
}
|
|
@@ -2016,7 +2151,8 @@ const chat = defineCommand({
|
|
|
2016
2151
|
* writes to the store, then wakes the run on its socket; specs/messaging.md).
|
|
2017
2152
|
* `--say` gives the answer a note, which goes to the run with it. With `--id`
|
|
2018
2153
|
* and `--choose`, `--note` and `--rating` instead note how the decision's
|
|
2019
|
-
* node went, as the run's feedback (specs/feedback.md).
|
|
2154
|
+
* node went, as the run's feedback (specs/feedback.md). `--document` prints
|
|
2155
|
+
* what a gate gave to read beside its question.
|
|
2020
2156
|
*/
|
|
2021
2157
|
const failed$2 = (message) => {
|
|
2022
2158
|
writeErr(message);
|
|
@@ -2055,7 +2191,7 @@ const saidLine = (store, id) => {
|
|
|
2055
2191
|
return note === void 0 ? "" : style("dim", ` · said: ${note}`);
|
|
2056
2192
|
};
|
|
2057
2193
|
/** One line per open decision, for `--list`. */
|
|
2058
|
-
const formatDecisions = (decisions) => decisions.length === 0 ? ["No open decisions."] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)}${d.owner === void 0 ? "" : style("dim", ` via ${d.owner}`)} ${d.question} ${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
|
|
2194
|
+
const formatDecisions = (decisions, workflow) => decisions.length === 0 ? [workflow === void 0 ? "No open decisions." : `No open decisions in workflow ${workflow}.`] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)}${d.owner === void 0 ? "" : style("dim", ` via ${d.owner}`)} ${d.question} ${d.document === void 0 ? "" : `${style("dim", "+document")} `}${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
|
|
2059
2195
|
const decide = defineCommand({
|
|
2060
2196
|
meta: {
|
|
2061
2197
|
name: "decide",
|
|
@@ -2082,6 +2218,15 @@ const decide = defineCommand({
|
|
|
2082
2218
|
description: "List them as JSON",
|
|
2083
2219
|
default: false
|
|
2084
2220
|
},
|
|
2221
|
+
workflow: {
|
|
2222
|
+
type: "string",
|
|
2223
|
+
description: "Only the decisions of this workflow's runs"
|
|
2224
|
+
},
|
|
2225
|
+
document: {
|
|
2226
|
+
type: "boolean",
|
|
2227
|
+
description: "Print the document of the decision --id names",
|
|
2228
|
+
default: false
|
|
2229
|
+
},
|
|
2085
2230
|
id: {
|
|
2086
2231
|
type: "string",
|
|
2087
2232
|
description: "Answer this decision…"
|
|
@@ -2104,7 +2249,7 @@ const decide = defineCommand({
|
|
|
2104
2249
|
}
|
|
2105
2250
|
},
|
|
2106
2251
|
run: async ({ args }) => {
|
|
2107
|
-
const refusal = args.id !== void 0 || args.choose !== void 0 || process.stdin.isTTY && !args.list && !args.json ? decideRefusal() : null;
|
|
2252
|
+
const refusal = !args.document && (args.id !== void 0 || args.choose !== void 0 || process.stdin.isTTY && !args.list && !args.json) ? decideRefusal() : null;
|
|
2108
2253
|
if (refusal !== null) {
|
|
2109
2254
|
failed$2(refusal);
|
|
2110
2255
|
return;
|
|
@@ -2116,6 +2261,16 @@ const decide = defineCommand({
|
|
|
2116
2261
|
interactive: process.stdin.isTTY
|
|
2117
2262
|
});
|
|
2118
2263
|
try {
|
|
2264
|
+
if (args.document) {
|
|
2265
|
+
if (args.id === void 0 || args.choose !== void 0) {
|
|
2266
|
+
failed$2("--document goes with --id, without --choose.");
|
|
2267
|
+
return;
|
|
2268
|
+
}
|
|
2269
|
+
const { document } = store.decision(args.id) ?? {};
|
|
2270
|
+
if (document === void 0) failed$2(`${args.id} has no document.`);
|
|
2271
|
+
else write(document);
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2119
2274
|
if (args.id !== void 0 || args.choose !== void 0) {
|
|
2120
2275
|
if (args.id === void 0 || args.choose === void 0) {
|
|
2121
2276
|
failed$2("--id and --choose go together.");
|
|
@@ -2147,6 +2302,7 @@ const decide = defineCommand({
|
|
|
2147
2302
|
return;
|
|
2148
2303
|
}
|
|
2149
2304
|
const filter = {};
|
|
2305
|
+
if (args.workflow !== void 0) filter.workflow = args.workflow;
|
|
2150
2306
|
if (args.run !== void 0 && args.run !== "") {
|
|
2151
2307
|
const found = findRun(store, args.run, process.cwd());
|
|
2152
2308
|
if (!found.ok) {
|
|
@@ -2161,7 +2317,7 @@ const decide = defineCommand({
|
|
|
2161
2317
|
return;
|
|
2162
2318
|
}
|
|
2163
2319
|
if (args.list || !process.stdin.isTTY || open.length === 0) {
|
|
2164
|
-
for (const line of formatDecisions(open)) write(line);
|
|
2320
|
+
for (const line of formatDecisions(open, args.workflow)) write(line);
|
|
2165
2321
|
return;
|
|
2166
2322
|
}
|
|
2167
2323
|
for (const d of open) {
|
|
@@ -2173,6 +2329,7 @@ const decide = defineCommand({
|
|
|
2173
2329
|
};
|
|
2174
2330
|
if (d.recommended !== void 0) question.recommended = d.recommended;
|
|
2175
2331
|
if (d.context !== void 0) question.context = d.context;
|
|
2332
|
+
if (d.document !== void 0) question.document = d.document;
|
|
2176
2333
|
let note;
|
|
2177
2334
|
question.onNote = (given) => {
|
|
2178
2335
|
note = given;
|
|
@@ -2264,7 +2421,7 @@ const resume = defineCommand({
|
|
|
2264
2421
|
},
|
|
2265
2422
|
driver: {
|
|
2266
2423
|
type: "string",
|
|
2267
|
-
description: "
|
|
2424
|
+
description: "claude-code (default: the one the run used)"
|
|
2268
2425
|
},
|
|
2269
2426
|
"in-place": {
|
|
2270
2427
|
type: "boolean",
|
|
@@ -2288,7 +2445,7 @@ const resume = defineCommand({
|
|
|
2288
2445
|
},
|
|
2289
2446
|
run: async ({ args, rawArgs }) => {
|
|
2290
2447
|
if (args.driver !== void 0 && !isDriver(args.driver)) {
|
|
2291
|
-
failed$1(
|
|
2448
|
+
failed$1(badDriver(args.driver));
|
|
2292
2449
|
return;
|
|
2293
2450
|
}
|
|
2294
2451
|
const store = openStore();
|
|
@@ -2339,6 +2496,36 @@ const resume = defineCommand({
|
|
|
2339
2496
|
}
|
|
2340
2497
|
}
|
|
2341
2498
|
});
|
|
2499
|
+
// src/doctor/lease-checks.ts
|
|
2500
|
+
/**
|
|
2501
|
+
* @file What `ork doctor` says of Command Code leases (RFC-014): it removes the
|
|
2502
|
+
* `ork` MCP entries a crashed session left in its checkout's local scope, and
|
|
2503
|
+
* says where. Never prints a token.
|
|
2504
|
+
*/
|
|
2505
|
+
/**
|
|
2506
|
+
* Removes the `ork` MCP entries a crashed Command Code session left in its
|
|
2507
|
+
* checkout's local scope (RFC-014), from the leases in the project's runs
|
|
2508
|
+
* folder. Says nothing when there were none. `bin` is the `cmd` program.
|
|
2509
|
+
*/
|
|
2510
|
+
const leaseChecks = async (place, env = process.env, bin = "cmd") => {
|
|
2511
|
+
const { cleanStaleLeases, leaseLedger } = await import("./src-oaJYIddp.mjs");
|
|
2512
|
+
const { cleaned, failed } = await cleanStaleLeases({
|
|
2513
|
+
bin,
|
|
2514
|
+
ledger: leaseLedger(placeFiles(place).runs),
|
|
2515
|
+
env
|
|
2516
|
+
});
|
|
2517
|
+
const name = "command-code";
|
|
2518
|
+
return [...cleaned.length === 0 ? [] : [{
|
|
2519
|
+
name,
|
|
2520
|
+
status: "ok",
|
|
2521
|
+
detail: `removed the \`ork\` MCP entry a stopped session left in ${cleaned.join(", ")}`
|
|
2522
|
+
}], ...failed.length === 0 ? [] : [{
|
|
2523
|
+
name,
|
|
2524
|
+
status: "warn",
|
|
2525
|
+
detail: `could not remove the \`ork\` MCP entry a stopped session left in ${failed.join(", ")}`,
|
|
2526
|
+
hint: "Run `cmd mcp remove --scope local ork` in each of those folders."
|
|
2527
|
+
}]];
|
|
2528
|
+
};
|
|
2342
2529
|
// src/project/ork-skills.ts
|
|
2343
2530
|
/**
|
|
2344
2531
|
* @file The Claude Code plugin's skills (`plugins/ork/skills/`) as text, by
|
|
@@ -2349,8 +2536,8 @@ const resume = defineCommand({
|
|
|
2349
2536
|
const ORK_SKILLS = {
|
|
2350
2537
|
principles: "---\nname: principles\ndescription: Use when deciding how to split work into an Ork workflow - which steps are programs, which are Claude sessions, where a person decides - so it costs less than doing it by hand and can't go wrong quietly.\n---\n\n# How Ork splits work\n\nOrk exists to do long jobs for less than doing them by hand, and safely. Four\nrules decide the shape of a workflow.\n\n## 1. A program for what a program can do\n\nA model turn is for judgment. Anything a program can decide, it should: run the\ntests, read a pull request's state, count what changed, wait for CI.\n\n- **A tool node** (`tool.shell`, `tool.git`, `tool.gh`, `tool.aws`) or an\n **`mcp.call`** does it with no session. With `output: { format: json }`, its\n answer routes the run: `when: \"json.state == 'OPEN'\"`.\n- **`verify:`** on a session runs a check before its report counts, so a session\n can't claim success past failing tests. **`before:`** hands it what a program\n found, so it doesn't spend turns finding it.\n- **`watch:`** waits on CI or a review with a program that looks every few\n seconds; hours of waiting cost nothing.\n- **`notify:`** lets a program tell a running session what it found, with no\n model turn to carry the news.\n\n## 2. Spend where it pays\n\n- A small model and low effort for a narrow task (`model: haiku`,\n `effort: low`); the large ones where thinking pays.\n- `budgetUsd` on each session and on the run (`limits.budgetUsd`). A loop of\n sessions without a budget or `limits.timeout` gets a warning, for good reason.\n- Give a session what it needs in its `prompt` and `before:`, rather than\n letting it search; and `isolation: readonly` for a reviewer that only reads.\n\n## 3. Make a mistake impossible before checking for it\n\n- Declare what a node says: `output.schema` lists on tool JSON and Claude\n reports (`verdict: [approve, escalate]`). Conditions then compare against a\n closed list, and a run fails a value off it rather than matching nothing.\n- A verdict is a declared field, never words to search a summary for.\n- Give each condition its opposite, and each failure a handler (`when: \"!ok\"`).\n- Check every draft with Ork's `check` tool until it's clean.\n\n## 4. A person decides what can't be undone\n\nMerging, deploying, publishing, spending a lot: put a `human.gate` before it,\nwith the safe option `recommended`. For a run nobody watches, give it a\n`timeout` and the safe `default`. You never answer a gate yourself.\n\n## An example\n\nA review that a program checks first, and a person signs off:\n\n```yaml\nschemaVersion: 1\nname: careful-review\ndescription: Lint with a program, review with a small model, and ask before merging.\ninputs:\n pr:\n type: string\n required: true\nlimits:\n budgetUsd: 3\nnodes:\n lint:\n type: tool.shell\n command: pnpm lint\n allow: [pnpm lint]\n review:\n type: claude.worker\n prompt: \"Review pull request {{inputs.pr}}. The linter passed.\"\n model: haiku\n effort: low\n isolation: readonly\n budgetUsd: 1\n output:\n schema:\n verdict: [approve, escalate]\n ask:\n type: human.gate\n question: \"Merge pull request {{inputs.pr}}? The review says {{nodes.review.output.verdict}}.\"\n options: [merge, wait]\n recommended: wait\n merge:\n type: tool.gh\n command: pr merge {{inputs.pr}} --squash\n allow: [pr merge]\nedges:\n - lint -> review\n - review -> ask\n - from: ask\n to: merge\n when: \"choice == 'merge'\"\n```\n\n`lint` costs nothing, `review` is a small model with a cap, and nothing merges\nwithout a person.\n",
|
|
2351
2538
|
run: "---\nname: run\ndescription: Use when starting, following or explaining an Ork run - what it's doing, why it failed, what it cost, and what waits on the person - and when the person asks about their Ork runs.\n---\n\n# Running and following Ork workflows\n\nOrk runs workflows on the person's machine with their accounts, so a run spends\ntheir money and can push, merge or deploy. Start one only when they ask, and\nnever answer a decision for them.\n\n## Starting\n\n1. Find it: Ork's `workflows` tool gives each workflow's name and inputs.\n2. Check it first with `check` (`workflow: <name>`). Don't start one with an\n error.\n3. Start it with `start_run`, giving every required input. Claude Code asks the\n person first, unless they've allowed it. It answers with the run's id; the run goes on in the\n background, whatever happens to this conversation.\n\nWithout Ork's tools: `ork run <workflow> --detach --input name=value`.\n\n## Following\n\n- `run` gives a run's status, what it waits on, its cost so far, and each\n node's state. `runs` lists the newest runs.\n- `run_events` gives its newest events; `node` narrows them, and `last` or\n `after` widens them. A `node.finished` event with `ok: false` says why a node\n failed.\n- `explain_context` says what a node's session was given: its agent,\n instructions, skills and files. Use it when a node didn't know something.\n- `usage` says what runs spent, by workflow and model, and how close the limits\n are.\n\nSay what you found plainly: which node, what it said, and what it cost. Quote a\nnode's own words rather than retelling them.\n\n## What only the person does\n\n- **Decisions.** `decisions` lists the questions waiting on them: a gate, a loop\n that ran out of rounds, a permission. Tell them what each asks, the options,\n and the recommended one, and that they answer in `ork ui` or with\n `ork decide` in their own terminal. Don't answer one: never run\n `ork decide --id … --choose …`; with Ork's plugin, a hook stops it.\n- **Stopping, pausing, resuming.** Tell them the command: `ork stop <run>`,\n `ork pause <run>`, `ork continue <run>`, `ork resume <run>`. A node can be\n stopped alone with `--node <id>`.\n\n## Without Ork's tools\n\n| Want | Command |\n| ---------------------- | ------------------------------ |\n| Runs, newest first | `ork ps` |\n| A run's transcript | `ork logs <run>` (`--follow`) |\n| Its nodes and cost | `ork tree <run>` |\n| What runs spent | `ork usage` |\n| What waits on a person | `ork decide --list` |\n| A node's context | `ork context explain <wf> <n>` |\n",
|
|
2352
|
-
setup: "---\nname: setup\ndescription: Use when the person asks you to set up Ork, install it, or get started with it in a repository - checking it's installed, setting up the project, making Ork's tools available to you, and a first workflow.\n---\n\n# Setting up Ork\n\nOrk runs workflows on the person's machine with their own accounts. Setting it\nup changes their machine and their repository, so say what each step does and\nask before it. Work through these in order, and stop where they say no.\n\n## 1. Check\n\n```sh\nork --version\nork doctor\n```\n\n- **No `ork`:** it needs Node 24.20 or later (`node --version`). The install is\n `npm install --global @krapht/ork`; show it and run it only when they say\n yes. The guide:\n https://github.com/mimshins/ork/blob/main/docs/getting-started/installation.md\n- **Read `ork doctor` to them.** It needs git, a git repository with a commit,\n and one
|
|
2353
|
-
write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## Loops\n\nEvery cycle needs an edge with `maxRounds`. Give the way back a condition and\nthe way out its opposite, as `!ok` and `ok` above; a plain edge beside a loop\nfires every round. When a loop runs out, Ork asks a person what to do.\n\n## What `check` refuses, and why\n\n- **A value the source can't have** (`when.value`): `status == 'failed'` (it's\n `fail`), a gate's `choice == 'aprove'` for `approve`, a value outside a\n declared list. The message names the values it can have.\n- **A loop no condition ends**, a plain edge beside a loop, a loop of sessions\n with no `limits.budgetUsd` or `limits.timeout`: warnings, each worth fixing.\n- **`no way the walk tried reaches x`**: Ork tried every way through, and none\n got to `x`. A join waiting on a branch a first-match router never takes is\n the usual cause.\n"
|
|
2539
|
+
setup: "---\nname: setup\ndescription: Use when the person asks you to set up Ork, install it, or get started with it in a repository - checking it's installed, setting up the project, making Ork's tools available to you, and a first workflow.\n---\n\n# Setting up Ork\n\nOrk runs workflows on the person's machine with their own accounts. Setting it\nup changes their machine and their repository, so say what each step does and\nask before it. Work through these in order, and stop where they say no.\n\n## 1. Check\n\n```sh\nork --version\nork doctor\n```\n\n- **No `ork`:** it needs Node 24.20 or later (`node --version`). The install is\n `npm install --global @krapht/ork`; show it and run it only when they say\n yes. The guide:\n https://github.com/mimshins/ork/blob/main/docs/getting-started/installation.md\n- **Read `ork doctor` to them.** It needs git, a git repository with a commit,\n and one agent driver. Don't run `git init` or a commit without asking.\n- **A driver not ready:** they log in themselves, with `claude auth login`, or\n by setting `ANTHROPIC_API_KEY` or a cloud provider in their own shell (either\n makes the claude-code driver ready). Never ask for a key, never take one pasted to you, and\n never write one into a file or a command. `gh` and `aws` are optional, and\n their logins are their own too (`gh auth login`, `aws sso login`).\n\n## 2. The project\n\n- **`ork init`** keeps Ork's files in the repository, in `.ork/`, committed with\n the code. It adds `.ork/runs/` to `.gitignore`, links `.claude/` and similar\n folders to `.ork/`, and writes a starter workflow, `hello`.\n- **`ork register`** keeps them beside it, in `~/.ork/projects/<id>/`, and\n writes nothing into the repository. For a repository whose process isn't\n theirs to change, or to try Ork first.\n\nExplain both and ask which. Skip the question when the repository decides it: a\n`.ork/` folder means `ork init` has run (`ork projects` lists what's\nregistered). Neither overwrites a file.\n\n## 3. Make Ork available to you\n\nIf Ork's tools (`workflows`, `check`) already answer you, skip this. Otherwise,\nby where you run, and asking before each change to a config:\n\n- **Claude Code:** Ork's plugin brings the skills and the tools. The person\n types these themselves:\n\n ```\n /plugin marketplace add mimshins/ork\n /plugin install ork@ork\n ```\n\n Or, in a project set up with `ork init`, the skills in the repository for\n everyone who clones it, then the tools:\n\n ```sh\n ork init --skills\n claude mcp add ork -- ork mcp\n ```\n\n New tools start when they restart Claude Code or reconnect `ork` in `/mcp`;\n so does the plugin's, once `ork` is installed.\n\n- **Another MCP client:** add a server named `ork` with command `ork` and args\n `[\"mcp\"]` to its MCP config, where its own docs say. Its tools find the\n project by its id, from `ork projects`.\n- **A shell only:** use the CLI. `ork --help` and each command's `--help` say\n what it takes.\n\n## 4. A first workflow\n\nOffer to write one for a task they name. Follow `/ork:write`, check each draft\n(the `check` tool, or `ork validate <file>`) until it has no error, then save\nit. Don't start it: a run spends their money. Start it only when they say so,\nin the background with its inputs (`start_run`, or\n`ork run <name> --detach --input name=value`), and follow it with `/ork:run`.\n\n## 5. Show them `ork ui`\n\nTell them to run `ork ui` in their own terminal: their runs, graphs and logs in\nthe browser, and where they answer what a run asks.\n\n## Never\n\n- Ask for, store or pass on a password, key or token.\n- Answer a run's decision: they answer in `ork ui` or with `ork decide` in\n their own terminal.\n- Install, edit a config, or start a run without a yes.\n",
|
|
2540
|
+
write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\nA worker, tool node, `mcp.call` or `workflow.call` can run once per entry of a\nlist: see [One node per item](#one-node-per-item).\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## One node per item\n\n`forEach` runs a worker, a tool node, an `mcp.call` or a `workflow.call` once\nper entry of a list, with no model turn to fan out or gather. Not a leader, a\ngate, a subagent or a control node.\n\n```yaml\nplan:\n type: claude.worker\n prompt: \"Split this into at most 6 independent tasks: {{inputs.goal}}\"\n isolation: readonly\n output:\n schema:\n tasks:\n list: { title: string, detail: string } # declare the list and its shape\nbuild:\n type: claude.worker\n prompt: \"Task {{index}}: {{item.title}}. {{item.detail}}\"\n isolation: worktree\n forEach:\n from: \"{{nodes.plan.output.tasks}}\" # exactly one path to a declared list\n maxItems: 6 # required with `from`: the cap on sessions started\n maxParallel: 3\n```\n\n- `forEach: { items: [a, b] }` takes a list written in the file instead.\n- `{{item}}` and `{{index}}` work only in a node with `forEach`, in `prompt`,\n `command`, `arguments`, `inputs`, `env` and `ref`. A program also gets\n `ORK_ITEM` and `ORK_INDEX`.\n- The next node reads `items`, `results` (each item's output, `summary`,\n `branch`), `statuses` and `count`: `{{nodes.build.output.results}}`,\n `when: \"all(statuses, 'pass')\"`. Items follow no edges; the node does.\n- `onFailure: finish` (default) runs every item; `stop` starts no more after a\n failure. A list over `maxItems` makes Ork ask a person.\n- Declare the source's `list`, or `check` warns `forEach.undeclared`. Fix\n `forEach.not-list`, `forEach.order` and `item.field` errors; they name the\n field or node.\n\n## Loops\n\nEvery cycle needs an edge with `maxRounds`. Give the way back a condition and\nthe way out its opposite, as `!ok` and `ok` above; a plain edge beside a loop\nfires every round. When a loop runs out, Ork asks a person what to do.\n\n## What `check` refuses, and why\n\n- **A value the source can't have** (`when.value`): `status == 'failed'` (it's\n `fail`), a gate's `choice == 'aprove'` for `approve`, a value outside a\n declared list. The message names the values it can have.\n- **A loop no condition ends**, a plain edge beside a loop, a loop of sessions\n with no `limits.budgetUsd` or `limits.timeout`: warnings, each worth fixing.\n- **`no way the walk tried reaches x`**: Ork tried every way through, and none\n got to `x`. A join waiting on a branch a first-match router never takes is\n the usual cause.\n"
|
|
2354
2541
|
};
|
|
2355
2542
|
// src/project/init-skills.ts
|
|
2356
2543
|
/**
|
|
@@ -2498,7 +2685,7 @@ const projectChecks = (root) => [...linkChecks(root), ...skillChecks(root, ORK_V
|
|
|
2498
2685
|
* @file This installation of Ork: its name and version for the root command;
|
|
2499
2686
|
* `ork doctor`, which prints Ork's version against the last update check and
|
|
2500
2687
|
* the environment checks (Node, git, the user's own claude, gh and aws
|
|
2501
|
-
* logins, the project's links) and fails when no
|
|
2688
|
+
* logins, the project's links) and fails when no agent driver can run; and
|
|
2502
2689
|
* `ork upgrade`, which asks the npm registry for the latest version and, when
|
|
2503
2690
|
* it is newer, upgrades Ork the way it was installed after the user agrees
|
|
2504
2691
|
* (`--yes` skips the question; `--check` only reports). A clone or `npx` gets
|
|
@@ -2529,17 +2716,21 @@ const doctor = defineCommand({
|
|
|
2529
2716
|
run: async () => {
|
|
2530
2717
|
const { results, ok } = await runChecks(exec, process.env, process.versions.node);
|
|
2531
2718
|
const place = projectAt(process.cwd());
|
|
2719
|
+
const experiments = experimentsCheck(place);
|
|
2532
2720
|
for (const result of [
|
|
2533
2721
|
orkCheck(),
|
|
2534
2722
|
...results,
|
|
2535
2723
|
...place.mode === "attached" ? [] : projectChecks(place.root),
|
|
2536
2724
|
socketCheck(),
|
|
2537
|
-
...triggerChecks()
|
|
2725
|
+
...triggerChecks(),
|
|
2726
|
+
...await leaseChecks(place),
|
|
2727
|
+
experiments
|
|
2538
2728
|
]) for (const line of formatCheck(result)) write(line);
|
|
2539
2729
|
if (!ok) {
|
|
2540
|
-
write(`\n${style("red", "Not ready:")} Ork needs Node, git, and at least one
|
|
2730
|
+
write(`\n${style("red", "Not ready:")} Ork needs Node, git, and at least one agent driver.`);
|
|
2541
2731
|
process.exitCode = 1;
|
|
2542
2732
|
}
|
|
2733
|
+
if (experiments.status === "fail") process.exitCode = 1;
|
|
2543
2734
|
}
|
|
2544
2735
|
});
|
|
2545
2736
|
/** The ids of runs still running anywhere on this machine. */
|
|
@@ -3034,7 +3225,7 @@ const driverLine = (checks, projectDefault) => {
|
|
|
3034
3225
|
const why = ready.ready === 1 ? ready.detail : `more than one is ready, and ${ready.driver} comes first`;
|
|
3035
3226
|
return `Runs use the driver that's ready when they start: ${ready.driver} here (${why}).`;
|
|
3036
3227
|
}
|
|
3037
|
-
return `Runs use the driver that's ready when they start, and none is ready here yet: ${checks.find(({ id }) => id === "
|
|
3228
|
+
return `Runs use the driver that's ready when they start, and none is ready here yet: ${checks.find(({ id }) => id === "claude-code")?.result.hint ?? "see `ork doctor`."}`;
|
|
3038
3229
|
};
|
|
3039
3230
|
/**
|
|
3040
3231
|
* The line for the project at `root`: `ork doctor`'s driver checks through
|
|
@@ -3635,7 +3826,7 @@ const apply = defineCommand({
|
|
|
3635
3826
|
item: itemArg$1,
|
|
3636
3827
|
driver: {
|
|
3637
3828
|
type: "string",
|
|
3638
|
-
description: "
|
|
3829
|
+
description: "claude-code (default: the driver ready here)"
|
|
3639
3830
|
},
|
|
3640
3831
|
"run-id": {
|
|
3641
3832
|
type: "string",
|
|
@@ -3644,7 +3835,7 @@ const apply = defineCommand({
|
|
|
3644
3835
|
},
|
|
3645
3836
|
run: async ({ args }) => {
|
|
3646
3837
|
if (args.driver !== void 0 && !isDriver(args.driver)) {
|
|
3647
|
-
writeErr(
|
|
3838
|
+
writeErr(badDriver(args.driver));
|
|
3648
3839
|
process.exitCode = 1;
|
|
3649
3840
|
return;
|
|
3650
3841
|
}
|
|
@@ -4214,6 +4405,12 @@ const logs = defineCommand({
|
|
|
4214
4405
|
process.exitCode = 1;
|
|
4215
4406
|
return;
|
|
4216
4407
|
}
|
|
4408
|
+
const problem = args.node === void 0 ? null : unknownNode(store.events(found.run.id), args.node, args.follow && found.run.status === "running");
|
|
4409
|
+
if (problem !== null) {
|
|
4410
|
+
writeErr(problem);
|
|
4411
|
+
process.exitCode = 1;
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4217
4414
|
const abort = new AbortController();
|
|
4218
4415
|
process.once("SIGINT", () => {
|
|
4219
4416
|
abort.abort();
|
|
@@ -4247,8 +4444,9 @@ const tree = defineCommand({
|
|
|
4247
4444
|
return;
|
|
4248
4445
|
}
|
|
4249
4446
|
const { run } = found;
|
|
4250
|
-
const
|
|
4251
|
-
const
|
|
4447
|
+
const stored = store.events(run.id);
|
|
4448
|
+
const events = stored.map((s) => s.event);
|
|
4449
|
+
const roots = nestItems(activeTree(stored));
|
|
4252
4450
|
if (args.json) {
|
|
4253
4451
|
write(JSON.stringify(roots, void 0, 2));
|
|
4254
4452
|
return;
|
|
@@ -4377,6 +4575,8 @@ const init = defineCommand({
|
|
|
4377
4575
|
skills: args.skills,
|
|
4378
4576
|
link: args.link
|
|
4379
4577
|
});
|
|
4578
|
+
const noCommit = await headWarning(exec);
|
|
4579
|
+
if (noCommit !== null) write(style("yellow", `\n! git: ${noCommit}`));
|
|
4380
4580
|
write(`\nNext: edit ${WORKFLOW_DIR}/, then run ${style("bold", "ork validate")}.`);
|
|
4381
4581
|
}
|
|
4382
4582
|
});
|
|
@@ -4575,27 +4775,27 @@ const main = defineCommand({
|
|
|
4575
4775
|
ps,
|
|
4576
4776
|
logs,
|
|
4577
4777
|
tree,
|
|
4578
|
-
usage: () => import("./usage-
|
|
4579
|
-
prune: () => import("./usage-
|
|
4778
|
+
usage: () => import("./usage-Cd8pEYyK.mjs").then((module) => module.usage),
|
|
4779
|
+
prune: () => import("./usage-Cd8pEYyK.mjs").then((module) => module.prune),
|
|
4580
4780
|
decide,
|
|
4581
|
-
relay: () => import("./relay-
|
|
4582
|
-
pause: () => import("./pause-
|
|
4583
|
-
continue: () => import("./pause-
|
|
4781
|
+
relay: () => import("./relay-EceRIebO.mjs").then((module) => module.relay),
|
|
4782
|
+
pause: () => import("./pause-D7OVB5Ac.mjs").then((module) => module.pause),
|
|
4783
|
+
continue: () => import("./pause-D7OVB5Ac.mjs").then((module) => module.continueRun),
|
|
4584
4784
|
stop,
|
|
4585
4785
|
chat,
|
|
4586
|
-
send: () => import("./send-
|
|
4786
|
+
send: () => import("./send-C0uwfUYi.mjs").then((module) => module.send),
|
|
4587
4787
|
resume,
|
|
4588
4788
|
feedback,
|
|
4589
4789
|
context,
|
|
4590
|
-
worktrees: () => import("./worktrees-
|
|
4591
|
-
wt: () => import("./worktrees-
|
|
4592
|
-
watch: () => import("./watch-
|
|
4593
|
-
mcp: () => import("./mcp-
|
|
4594
|
-
ui: () => import("./ui-
|
|
4790
|
+
worktrees: () => import("./worktrees-g1MQMGie.mjs").then((module) => module.worktrees),
|
|
4791
|
+
wt: () => import("./worktrees-g1MQMGie.mjs").then((module) => module.worktrees),
|
|
4792
|
+
watch: () => import("./watch-DW5R2OPO.mjs").then((module) => module.watch),
|
|
4793
|
+
mcp: () => import("./mcp-CslnG5wf.mjs").then((module) => module.mcp),
|
|
4794
|
+
ui: () => import("./ui-CBkHJdqk.mjs").then((module) => module.ui),
|
|
4595
4795
|
doctor,
|
|
4596
4796
|
upgrade,
|
|
4597
|
-
completion: () => import("./completion-
|
|
4598
|
-
__complete: () => import("./completion-
|
|
4797
|
+
completion: () => import("./completion-Dv5kf598.mjs").then((module) => module.completion),
|
|
4798
|
+
__complete: () => import("./completion-Dv5kf598.mjs").then((module) => module.completeCommand(main))
|
|
4599
4799
|
}
|
|
4600
4800
|
});
|
|
4601
4801
|
// src/bin.ts
|
|
@@ -4613,7 +4813,7 @@ const main = defineCommand({
|
|
|
4613
4813
|
clearGitLocalEnv();
|
|
4614
4814
|
const argv = process.argv.slice(2);
|
|
4615
4815
|
if (argv[0] === "__complete") {
|
|
4616
|
-
const { printCompletions } = await import("./completion-
|
|
4816
|
+
const { printCompletions } = await import("./completion-Dv5kf598.mjs");
|
|
4617
4817
|
await printCompletions(main, argv.slice(1));
|
|
4618
4818
|
} else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
|
|
4619
4819
|
else {
|