@krapht/ork 1.0.0-alpha.11 → 1.0.0-alpha.13
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/dist/{__build_info__-CIchF6nr.mjs → __build_info__-Da73_nBs.mjs} +4 -3
- package/dist/attach-E6NNyA3T.mjs +89 -0
- package/dist/bin.mjs +213 -1230
- package/dist/{decide-B6PnKeqo.mjs → builtin-files-n6o6oWpB.mjs} +3 -51
- package/dist/{chips-BA7SrrH5.mjs → chips-CyeB_rhR.mjs} +1 -1
- package/dist/{clean-Di-KT7Mf.mjs → clean-Bh4KZgzc.mjs} +1 -1
- package/dist/{completion-C5D0sEs5.mjs → completion-SNNNxnv9.mjs} +7 -6
- package/dist/{context-BaNshWr9.mjs → context-DHudLA5e.mjs} +6 -6
- package/dist/decide-DuIHACvH.mjs +50 -0
- package/dist/{find-BicBbGMg.mjs → find-CjsNYAAf.mjs} +1 -1
- package/dist/{format-lWhN-jYR.mjs → format-C60TFpRe.mjs} +5 -85
- package/dist/{help-groups-BwYoAWsh.mjs → help-groups-CyfPWlST.mjs} +8 -0
- package/dist/{in-a-run-CgkVouyG.mjs → in-a-run-BYjTXaST.mjs} +4 -2
- package/dist/{limits-jZQp9ZJC.mjs → limits-C5QKZNLT.mjs} +2 -2
- package/dist/{list-CEaJ07jd.mjs → list-BUb5Uc3H.mjs} +1 -1
- package/dist/{mcp-DYgVA9f5.mjs → mcp-bcctR-wc.mjs} +6 -6
- package/dist/once-DPQu5Q3H.mjs +76 -0
- package/dist/{output-CM95njYt.mjs → output-CnRzrbUM.mjs} +3 -1
- package/dist/{pause-D6PuSlNl.mjs → pause-BPvlDIvz.mjs} +4 -4
- package/dist/{relay-DNx7be9k.mjs → relay-C8W0t_zc.mjs} +2 -2
- package/dist/{remove-CpMb4NDl.mjs → remove-BkFWGVYo.mjs} +3 -3
- package/dist/run-CQIfq918.mjs +192 -0
- package/dist/{send-CLs_fyg7.mjs → send-CaZlGG5a.mjs} +3 -3
- package/dist/{src-D6_xjm-x.mjs → src-BeR6f70A.mjs} +595 -228
- package/dist/{src-B6KvTm7B.mjs → src-CGJGL1sZ.mjs} +7 -6
- package/dist/{src-BnZgZGrf.mjs → src-CqRqsb2t.mjs} +44 -7
- package/dist/{src-9iGkjnsy.mjs → src-D93tHZ5B.mjs} +110 -35
- package/dist/store-CM3cERGl.mjs +1365 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +18 -0
- package/dist/ui/assets/app-DYRC0V4p.js +111 -0
- package/dist/ui/assets/{index-By_8KKG6.js → index-CWwm8HTG.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-CIci4UBW.mjs → ui-D2Ydofq0.mjs} +4 -4
- package/dist/{usage-GceBGMjU.mjs → usage-6WIowblz.mjs} +6 -6
- package/dist/verdict-CIy8tAyX.mjs +84 -0
- package/dist/{watch-BmoybMQg.mjs → watch-uOU_WeBI.mjs} +7 -7
- package/dist/{workflows-DZvW3i1e.mjs → workflows-7tQdMnrA.mjs} +3 -3
- package/dist/{worktrees-Be51E2rB.mjs → worktrees-ZCII298F.mjs} +5 -5
- package/package.json +1 -1
- package/dist/ui/assets/app-CxURph7a.js +0 -75
package/dist/bin.mjs
CHANGED
|
@@ -1,243 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { i as
|
|
5
|
-
import { n as
|
|
6
|
-
import { n as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { n as
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { c as
|
|
15
|
-
import { i as
|
|
16
|
-
import {
|
|
2
|
+
import { A as resolveInputs, An as applySteps, Ar as readyDriver, B as tellNode, Bn as inRepoPlace, Cn as socketPath, Cr as retiredDriverHint, D as inspectProject, Dn as maskReplacer, Et as nestItems, F as addressOf, Fn as createAgent, G as requestStop, Gn as runDirOf, H as flattenTree, Hn as placeFiles, I as endLink, In as deleteAgent, J as detachResume, Jn as inside, Jt as gitRulesOf, K as tellLiveRun, Kn as shownPath, L as endLinksOf, Ln as catalogAgent, Mn as linkTools, Mr as ORK_LAYOUT, Nn as toolLinks, Nr as AGENT_DRIVERS, P as createCalls, Pn as toolsInUse, Q as detachRun, Rn as loadAgents, S as activeTree, Sn as serveRun, St as templateOf, Tn as wakeRun, Un as placeFor, Vn as newProjectId, Vt as clearGitLocalEnv, Wn as projectsHome, Wt as sessionDriver, X as listKey, Xt as machineRulesOf, Y as resumable, Yn as projectRootOf, Yt as loadProject, Z as DETACHED_ENV, _r as settingsOf, _t as liveStatus, a as planUpgrade, bt as runTree, c as runningScript, cn as FEEDBACK_TARGETS, d as isNewer, dr as serializeWorkflow, en as createSignals, et as NOT_STARTED, fn as sendPost, ft as walkSummary, gn as normalizeTitle, ht as userMessageText, jn as planMoveInto, jr as AGENT_TOOL_LAYOUTS, k as parseInputPairs, kn as planAgentMigration, kr as driverChecks, mn as createFeedback, mr as WorkflowSchema, n as resumedOptions, o as releaseNotes, on as FEEDBACK_KINDS, or as renameNodeTypes, pt as startRun, q as adoptedCalls, qt as PROJECT_FILE, r as resolveBase, s as runningRunIds, sn as FEEDBACK_SEVERITIES, sr as beforeOf, st as planNodeSession, t as createWorkspace, tn as endOf, un as planPost, w as openStore, yr as isClaudeNode, yt as runStatus, z as peerOf, zn as hasInRepoFiles } from "./src-BeR6f70A.mjs";
|
|
3
|
+
import { a as unknownNode, c as callsShown, d as createDecisionQueue, i as followRun, l as createTerminalHuman, n as attachRun, o as formatEvent, r as askStored, s as formatRunEvent, t as withStore$1, u as readText } from "./store-CM3cERGl.mjs";
|
|
4
|
+
import { C as feedbackActions, S as placeOf, _ as previewPost, b as acceptItem, f as readUpdateCache, g as nodeSession, 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 checkNote, w as openFixRun, y as recordNote } from "./__build_info__-Da73_nBs.mjs";
|
|
5
|
+
import { n as builtinWorkflowFile, t as builtinProblem } from "./builtin-files-n6o6oWpB.mjs";
|
|
6
|
+
import { n as pendingDecisions, t as answerDecision } from "./decide-DuIHACvH.mjs";
|
|
7
|
+
import { n as newRunId, t as findRun } from "./find-CjsNYAAf.mjs";
|
|
8
|
+
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-BUb5Uc3H.mjs";
|
|
9
|
+
import { a as planClean, t as applyClean } from "./clean-Bh4KZgzc.mjs";
|
|
10
|
+
import { c as subcommandsOf, i as argsOf, n as groupedEntries, o as publicArgs, s as resolved, t as groupOf } from "./help-groups-CyfPWlST.mjs";
|
|
11
|
+
import { C as hang, S as cut, T as wrap, a as style, b as takePlainFlag, f as bold, i as plain, n as cost, o as write, p as paint, r as detachHint, s as writeErr, t as confirm, v as canKey, w as widthOf, x as termOf } from "./output-CnRzrbUM.mjs";
|
|
12
|
+
import { n as chip, r as nodeChip, t as CHIP_LABELS } from "./chips-CyeB_rhR.mjs";
|
|
13
|
+
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-D93tHZ5B.mjs";
|
|
14
|
+
import { c as registeredProjects, i as refusedBuiltin, n as WORKFLOW_SUFFIX, o as workflowArg, s as projectAt, t as WORKFLOW_DIR } from "./workflows-7tQdMnrA.mjs";
|
|
15
|
+
import { C as startUpdateCheck, S as showUpdateNotice, _ as runChecks, a as validateFile, c as warningLines, d as runReadyDriver, f as unfitLines, g as orkCheck, h as headWarning, i as validate, l as checksHere, m as experimentsCheck, o as walkLoaded, p as exec, r as loadWorkflow, s as formatReport, t as context, u as chooseReadyDriver, v as socketCheck, w as updateCheckOff, x as runUpdateChild, y as triggerChecks } from "./context-DHudLA5e.mjs";
|
|
16
|
+
import { a as createView, o as styler } from "./verdict-CIy8tAyX.mjs";
|
|
17
|
+
import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-C5QKZNLT.mjs";
|
|
18
|
+
import { a as decideRefusal, r as chatRefusal } from "./in-a-run-BYjTXaST.mjs";
|
|
19
|
+
import { i as listedStatus, n as formatRuns, r as formatTree, t as ago } from "./format-C60TFpRe.mjs";
|
|
17
20
|
import path from "node:path";
|
|
18
21
|
import { stringify } from "yaml";
|
|
19
22
|
import { z } from "zod";
|
|
20
23
|
import { appendFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
21
24
|
import { createHash, randomUUID } from "node:crypto";
|
|
22
25
|
import { homedir } from "node:os";
|
|
23
|
-
import { setTimeout } from "node:timers/promises";
|
|
24
|
-
import { defineCommand, renderUsage, runMain } from "citty";
|
|
25
|
-
import { createInterface } from "node:readline/promises";
|
|
26
|
-
// ../core/src/control/queue.ts
|
|
27
|
-
const PERMISSION_OPTIONS = [{
|
|
28
|
-
id: "allow",
|
|
29
|
-
label: "Allow"
|
|
30
|
-
}, {
|
|
31
|
-
id: "deny",
|
|
32
|
-
label: "Deny"
|
|
33
|
-
}];
|
|
34
|
-
const DENIED = "The user did not allow this tool.";
|
|
35
|
-
/** The deny message a session gets, with the person's words when given. */
|
|
36
|
-
const deniedMessage = (note) => note === void 0 ? DENIED : `${DENIED} They said: ${note}`;
|
|
37
|
-
/** The input fields that say what a tool will do, the first one present. */
|
|
38
|
-
const TOOL_INPUT_KEYS = [
|
|
39
|
-
"command",
|
|
40
|
-
"file_path",
|
|
41
|
-
"path",
|
|
42
|
-
"url",
|
|
43
|
-
"pattern"
|
|
44
|
-
];
|
|
45
|
-
/** A tool's input for the decision's context: as written, cut at `max`. */
|
|
46
|
-
const inputContext = (input, max = 4e3) => {
|
|
47
|
-
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
48
|
-
const text = key === void 0 ? JSON.stringify(input, void 0, 2) : String(input[key]);
|
|
49
|
-
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
50
|
-
};
|
|
51
|
-
const createDecisionQueue = (options) => {
|
|
52
|
-
const { store, runId, emit } = options;
|
|
53
|
-
const earlier = store.decisions({ runId });
|
|
54
|
-
let count = Math.max(0, ...earlier.map(({ id }) => Number(id.slice(runId.length + 1)) || 0));
|
|
55
|
-
for (const { id, node, status } of earlier) {
|
|
56
|
-
if (status !== "open") continue;
|
|
57
|
-
store.withdrawDecision(id);
|
|
58
|
-
emit({
|
|
59
|
-
type: "decision.withdrawn",
|
|
60
|
-
node,
|
|
61
|
-
decision: id
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
/** Opens a decision and settles it with the first answer. */
|
|
65
|
-
const decide = (open, local, withdrawn) => {
|
|
66
|
-
count += 1;
|
|
67
|
-
const id = `${runId}-${String(count)}`;
|
|
68
|
-
const record = store.openDecision({
|
|
69
|
-
...open,
|
|
70
|
-
id,
|
|
71
|
-
runId
|
|
72
|
-
});
|
|
73
|
-
const event = {
|
|
74
|
-
type: "decision.opened",
|
|
75
|
-
node: record.node,
|
|
76
|
-
decision: id,
|
|
77
|
-
kind: record.kind,
|
|
78
|
-
question: record.question,
|
|
79
|
-
options: record.options
|
|
80
|
-
};
|
|
81
|
-
if (record.recommended !== void 0) event.recommended = record.recommended;
|
|
82
|
-
if (record.owner !== void 0) event.owner = record.owner;
|
|
83
|
-
emit(event);
|
|
84
|
-
const { promise, resolve, reject } = Promise.withResolvers();
|
|
85
|
-
const settled = new AbortController();
|
|
86
|
-
/** `note` is the stored one: masked, and absent when blank. */
|
|
87
|
-
const finish = (answer, by, note) => {
|
|
88
|
-
if (settled.signal.aborted) return;
|
|
89
|
-
settled.abort();
|
|
90
|
-
const answered = {
|
|
91
|
-
type: "decision.answered",
|
|
92
|
-
node: open.node,
|
|
93
|
-
decision: id,
|
|
94
|
-
answer,
|
|
95
|
-
by
|
|
96
|
-
};
|
|
97
|
-
if (note !== void 0) answered.note = note;
|
|
98
|
-
emit(answered);
|
|
99
|
-
resolve(note === void 0 ? {
|
|
100
|
-
answer,
|
|
101
|
-
by
|
|
102
|
-
} : {
|
|
103
|
-
answer,
|
|
104
|
-
by,
|
|
105
|
-
note
|
|
106
|
-
});
|
|
107
|
-
};
|
|
108
|
-
const withdraw = () => {
|
|
109
|
-
if (settled.signal.aborted) return;
|
|
110
|
-
settled.abort();
|
|
111
|
-
store.withdrawDecision(id);
|
|
112
|
-
emit({
|
|
113
|
-
type: "decision.withdrawn",
|
|
114
|
-
node: open.node,
|
|
115
|
-
decision: id
|
|
116
|
-
});
|
|
117
|
-
reject(/* @__PURE__ */ new Error("the question was withdrawn"));
|
|
118
|
-
};
|
|
119
|
-
if (withdrawn?.aborted === true) withdraw();
|
|
120
|
-
withdrawn?.addEventListener("abort", withdraw, { once: true });
|
|
121
|
-
const fromStore = async () => {
|
|
122
|
-
try {
|
|
123
|
-
const closed = await options.waitFor(id, settled.signal);
|
|
124
|
-
if (closed.status === "answered" && closed.answer !== void 0) finish(closed.answer, closed.answeredBy ?? "decide", closed.note);
|
|
125
|
-
} catch {}
|
|
126
|
-
};
|
|
127
|
-
const fromTerminal = async (ask) => {
|
|
128
|
-
try {
|
|
129
|
-
const { answer, note } = await ask(settled.signal);
|
|
130
|
-
if (store.answerDecision(id, answer, "terminal", note)) finish(answer, "terminal", store.decision(id)?.note);
|
|
131
|
-
} catch {}
|
|
132
|
-
};
|
|
133
|
-
fromStore();
|
|
134
|
-
if (local !== void 0) fromTerminal(local);
|
|
135
|
-
return promise;
|
|
136
|
-
};
|
|
137
|
-
return {
|
|
138
|
-
ask: (question) => {
|
|
139
|
-
const open = {
|
|
140
|
-
node: question.nodeId,
|
|
141
|
-
kind: "question",
|
|
142
|
-
question: question.question,
|
|
143
|
-
options: question.options
|
|
144
|
-
};
|
|
145
|
-
if (question.recommended !== void 0) open.recommended = question.recommended;
|
|
146
|
-
if (question.context !== void 0) open.context = question.context;
|
|
147
|
-
if (question.document !== void 0) open.document = question.document;
|
|
148
|
-
if (question.owner !== void 0) open.owner = question.owner;
|
|
149
|
-
const { local } = options;
|
|
150
|
-
return decide(open, local === void 0 ? void 0 : (signal) => {
|
|
151
|
-
let note;
|
|
152
|
-
return local.ask({
|
|
153
|
-
...question,
|
|
154
|
-
question: maskSecrets(question.question),
|
|
155
|
-
...question.context === void 0 ? {} : { context: maskSecrets(question.context) },
|
|
156
|
-
...question.document === void 0 ? {} : { document: maskSecrets(question.document) },
|
|
157
|
-
signal,
|
|
158
|
-
onNote: (given) => {
|
|
159
|
-
note = given;
|
|
160
|
-
}
|
|
161
|
-
}).then((answer) => note === void 0 ? { answer } : {
|
|
162
|
-
answer,
|
|
163
|
-
note
|
|
164
|
-
});
|
|
165
|
-
}, question.signal).then(({ answer, note }) => {
|
|
166
|
-
if (note !== void 0) question.onNote?.(note);
|
|
167
|
-
return answer;
|
|
168
|
-
});
|
|
169
|
-
},
|
|
170
|
-
settled: (question) => {
|
|
171
|
-
count += 1;
|
|
172
|
-
const id = `${runId}-${String(count)}`;
|
|
173
|
-
const { owner } = question;
|
|
174
|
-
const record = store.openDecision({
|
|
175
|
-
id,
|
|
176
|
-
runId,
|
|
177
|
-
node: question.nodeId,
|
|
178
|
-
kind: "question",
|
|
179
|
-
question: question.question,
|
|
180
|
-
options: question.options,
|
|
181
|
-
owner,
|
|
182
|
-
...question.recommended === void 0 ? {} : { recommended: question.recommended },
|
|
183
|
-
...question.context === void 0 ? {} : { context: question.context }
|
|
184
|
-
});
|
|
185
|
-
emit({
|
|
186
|
-
type: "decision.opened",
|
|
187
|
-
node: record.node,
|
|
188
|
-
decision: id,
|
|
189
|
-
kind: "question",
|
|
190
|
-
question: record.question,
|
|
191
|
-
options: record.options,
|
|
192
|
-
owner,
|
|
193
|
-
...record.recommended === void 0 ? {} : { recommended: record.recommended }
|
|
194
|
-
});
|
|
195
|
-
store.answerDecision(id, question.answer, "owner", question.note);
|
|
196
|
-
const note = store.decision(id)?.note;
|
|
197
|
-
emit({
|
|
198
|
-
type: "decision.answered",
|
|
199
|
-
node: record.node,
|
|
200
|
-
decision: id,
|
|
201
|
-
answer: question.answer,
|
|
202
|
-
by: "owner",
|
|
203
|
-
owner,
|
|
204
|
-
...note === void 0 ? {} : { note }
|
|
205
|
-
});
|
|
206
|
-
},
|
|
207
|
-
permit: async (request) => {
|
|
208
|
-
const { local } = options;
|
|
209
|
-
let localMessage = DENIED;
|
|
210
|
-
const { answer, note } = await decide({
|
|
211
|
-
node: request.nodeId,
|
|
212
|
-
kind: "permission",
|
|
213
|
-
question: `Allow ${request.toolName}?`,
|
|
214
|
-
options: PERMISSION_OPTIONS,
|
|
215
|
-
context: inputContext(request.input)
|
|
216
|
-
}, local === void 0 ? void 0 : (signal) => {
|
|
217
|
-
let said;
|
|
218
|
-
return local.permit({
|
|
219
|
-
...request,
|
|
220
|
-
signal,
|
|
221
|
-
onNote: (given) => {
|
|
222
|
-
said = given;
|
|
223
|
-
}
|
|
224
|
-
}).then((result) => {
|
|
225
|
-
if (!result.allow) localMessage = result.message;
|
|
226
|
-
const choice = result.allow ? "allow" : "deny";
|
|
227
|
-
return said === void 0 ? { answer: choice } : {
|
|
228
|
-
answer: choice,
|
|
229
|
-
note: said
|
|
230
|
-
};
|
|
231
|
-
});
|
|
232
|
-
}, request.signal);
|
|
233
|
-
if (note !== void 0) request.onNote?.(note);
|
|
234
|
-
return answer === "allow" ? { allow: true } : {
|
|
235
|
-
allow: false,
|
|
236
|
-
message: note === void 0 ? localMessage : deniedMessage(note)
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
};
|
|
26
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
27
|
+
import { defineCommand, renderUsage, runCommand, runMain } from "citty";
|
|
241
28
|
// ../core/src/control/control.ts
|
|
242
29
|
const startControl = async (options) => {
|
|
243
30
|
const { store, runId } = options;
|
|
@@ -462,7 +249,7 @@ const fileSlotLedger = (options) => {
|
|
|
462
249
|
onWait?.(holders());
|
|
463
250
|
}
|
|
464
251
|
try {
|
|
465
|
-
await setTimeout(options.pollMs ?? 500, void 0, { signal });
|
|
252
|
+
await setTimeout$1(options.pollMs ?? 500, void 0, { signal });
|
|
466
253
|
} catch {
|
|
467
254
|
return null;
|
|
468
255
|
}
|
|
@@ -759,862 +546,6 @@ const renderHelp = async (cmd, parent) => {
|
|
|
759
546
|
const showHelp = async (cmd, parent) => {
|
|
760
547
|
process.stdout.write(`${await renderHelp(cmd, parent)}\n\n`);
|
|
761
548
|
};
|
|
762
|
-
// src/human-card.ts
|
|
763
|
-
/** Lines of context a card shows before `d` (or `--verbose`) shows the rest. */
|
|
764
|
-
const CONTEXT_LINES = 6;
|
|
765
|
-
/** Text from outside with its escapes gone and its line breaks kept. */
|
|
766
|
-
const clean = (text) => text.split("\n").map((line) => plain(line)).join("\n");
|
|
767
|
-
/** The card above the options, in the rich look or the plain lines. */
|
|
768
|
-
const cardLines = (t, q, place, verbose, document) => {
|
|
769
|
-
const context = q.context === void 0 ? void 0 : clean(q.context);
|
|
770
|
-
if (!t.rich) return {
|
|
771
|
-
lines: [
|
|
772
|
-
...place === void 0 ? [] : [`run ${place.run} · decision ${place.decision}`],
|
|
773
|
-
...document === void 0 ? [] : [document],
|
|
774
|
-
`? ${plain(q.nodeId)} asks: ${clean(q.question)}`,
|
|
775
|
-
...context === void 0 ? [] : [` ${context.replaceAll("\n", "\n ")}`]
|
|
776
|
-
],
|
|
777
|
-
moreContext: false
|
|
778
|
-
};
|
|
779
|
-
const shown = context === void 0 ? [] : wrap(context, t.width - 3);
|
|
780
|
-
const cutShort = !verbose && shown.length > CONTEXT_LINES;
|
|
781
|
-
const body = cutShort ? shown.slice(0, CONTEXT_LINES) : shown;
|
|
782
|
-
const where = place === void 0 ? "" : ` ${paint(t, "muted", `run ${place.run} · ${String(place.index)} of ${String(place.total)}`)}`;
|
|
783
|
-
return {
|
|
784
|
-
lines: [
|
|
785
|
-
"",
|
|
786
|
-
` ${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, plain(q.nodeId))}${where}`,
|
|
787
|
-
...wrap(clean(q.question), t.width - 1).map((l) => ` ${bold(t, l)}`),
|
|
788
|
-
...body.map((l) => paint(t, "muted", ` ${l}`)),
|
|
789
|
-
...cutShort ? [paint(t, "muted", ` … ${String(shown.length - CONTEXT_LINES)} more lines (d)`)] : []
|
|
790
|
-
],
|
|
791
|
-
moreContext: cutShort
|
|
792
|
-
};
|
|
793
|
-
};
|
|
794
|
-
/** The line above a permission prompt: who wants to run what. */
|
|
795
|
-
const permitLine = (t, nodeId, preview) => {
|
|
796
|
-
const node = plain(nodeId);
|
|
797
|
-
const what = plain(preview);
|
|
798
|
-
if (!t.rich) return `? ${node} wants to run ${what}`;
|
|
799
|
-
const room = Math.max(20, t.width - node.length - 24);
|
|
800
|
-
return ` ${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, node)} wants to run ${cut(what, room)}`;
|
|
801
|
-
};
|
|
802
|
-
/**
|
|
803
|
-
* The line a card collapses to once answered: `✓ 509da722-1 · add-invalid-node`
|
|
804
|
-
* (the decision, or the node when there is none, and the choice), or a note
|
|
805
|
-
* that it was left for later.
|
|
806
|
-
*/
|
|
807
|
-
const doneLine = (t, place, nodeId, answer) => {
|
|
808
|
-
const who = place?.decision ?? plain(nodeId);
|
|
809
|
-
if (answer.kind === "later") return ` ${paint(t, "muted", "·")} ${cut(`${who} left for later`, t.width - 3)}`;
|
|
810
|
-
const said = answer.note === void 0 ? "" : ` · said: ${plain(answer.note)}`;
|
|
811
|
-
return ` ${paint(t, "ok", "✓")} ${cut(`${who} · ${answer.id}${said}`, t.width - 3)}`;
|
|
812
|
-
};
|
|
813
|
-
// src/term/text.ts
|
|
814
|
-
/**
|
|
815
|
-
* @file The text prompt: one line of text, edited in place on a terminal
|
|
816
|
-
* (arrows, Home/End, Backspace, Delete, Ctrl-U to clear), collapsing to one
|
|
817
|
-
* line when answered; Enter sends, Esc (or Ctrl-D on an empty line) leaves
|
|
818
|
-
* it. Plain mode asks `message: ` and reads a line, as the prompts always
|
|
819
|
-
* did. `editLine` is the editing itself, which the select prompt reuses for
|
|
820
|
-
* its note.
|
|
821
|
-
*/
|
|
822
|
-
const at = (next, to) => ({
|
|
823
|
-
value: next.join(""),
|
|
824
|
-
pos: to
|
|
825
|
-
});
|
|
826
|
-
/** `state` after `key`; keys that do not edit leave it as it is. */
|
|
827
|
-
const editLine = (state, key) => {
|
|
828
|
-
const { value, pos } = state;
|
|
829
|
-
const chars = Array.from(value);
|
|
830
|
-
if (key.name === "char") return at(chars.toSpliced(pos, 0, key.text), pos + 1);
|
|
831
|
-
if (key.name === "backspace") return pos === 0 ? state : at(chars.toSpliced(pos - 1, 1), pos - 1);
|
|
832
|
-
if (key.name === "delete") return at(chars.toSpliced(pos, 1), pos);
|
|
833
|
-
if (key.name === "left") return {
|
|
834
|
-
value,
|
|
835
|
-
pos: Math.max(0, pos - 1)
|
|
836
|
-
};
|
|
837
|
-
if (key.name === "right") return {
|
|
838
|
-
value,
|
|
839
|
-
pos: Math.min(chars.length, pos + 1)
|
|
840
|
-
};
|
|
841
|
-
if (key.name === "home" || key.name === "ctrl-a") return {
|
|
842
|
-
value,
|
|
843
|
-
pos: 0
|
|
844
|
-
};
|
|
845
|
-
if (key.name === "end" || key.name === "ctrl-e") return {
|
|
846
|
-
value,
|
|
847
|
-
pos: chars.length
|
|
848
|
-
};
|
|
849
|
-
if (key.name === "ctrl-u") return at(chars.slice(pos), 0);
|
|
850
|
-
return state;
|
|
851
|
-
};
|
|
852
|
-
/**
|
|
853
|
-
* The line to draw for `state` after `lead`, within `width`, and the column of
|
|
854
|
-
* the cursor in it. A value too long for the room scrolls to keep the cursor in
|
|
855
|
-
* view.
|
|
856
|
-
*/
|
|
857
|
-
const showLine = (lead, state, width, leadWidth) => {
|
|
858
|
-
const chars = Array.from(state.value);
|
|
859
|
-
const room = Math.max(1, width - leadWidth - 1);
|
|
860
|
-
const start = Math.max(0, state.pos - room + 1);
|
|
861
|
-
return {
|
|
862
|
-
line: `${lead}${chars.slice(start, start + room).join("")}`,
|
|
863
|
-
column: leadWidth + state.pos - start
|
|
864
|
-
};
|
|
865
|
-
};
|
|
866
|
-
const defaultDone$1 = (io, message) => (value) => {
|
|
867
|
-
const term = termOf(io.output, io.env);
|
|
868
|
-
const mark = paint(term, "ok", "✓");
|
|
869
|
-
return cut(` ${mark} ${message} · ${value === void 0 || value === "" ? "skipped" : value}`, term.width);
|
|
870
|
-
};
|
|
871
|
-
/** Asks for one line of text. */
|
|
872
|
-
const readText = async (io, options) => {
|
|
873
|
-
const { message, signal } = options;
|
|
874
|
-
if (signal?.aborted === true) throw withdrawn(signal);
|
|
875
|
-
if (!canKey(io.input, io.output, io.env)) {
|
|
876
|
-
const answer = await readLine(io, ` ${message}: `, signal);
|
|
877
|
-
return answer === void 0 ? { kind: "cancelled" } : {
|
|
878
|
-
kind: "text",
|
|
879
|
-
value: answer.trim()
|
|
880
|
-
};
|
|
881
|
-
}
|
|
882
|
-
const done = options.done ?? defaultDone$1(io, message);
|
|
883
|
-
const term = () => termOf(io.output, io.env);
|
|
884
|
-
const region = createRegion(io.output, term);
|
|
885
|
-
const initial = options.initial ?? "";
|
|
886
|
-
return new Promise((resolve, reject) => {
|
|
887
|
-
let state = {
|
|
888
|
-
value: initial,
|
|
889
|
-
pos: Array.from(initial).length
|
|
890
|
-
};
|
|
891
|
-
const lead = ` ${paint(term(), "accent", "?")} ${bold(term(), message)} › `;
|
|
892
|
-
const leadWidth = Array.from(` ? ${message} › `).length;
|
|
893
|
-
const render = () => {
|
|
894
|
-
const { line, column } = showLine(lead, state, term().width, leadWidth);
|
|
895
|
-
region.draw([line], column);
|
|
896
|
-
};
|
|
897
|
-
const finish = (result, line) => {
|
|
898
|
-
stop();
|
|
899
|
-
io.output.off("resize", render);
|
|
900
|
-
signal?.removeEventListener("abort", onAbort);
|
|
901
|
-
region.finish(line);
|
|
902
|
-
resolve(result);
|
|
903
|
-
};
|
|
904
|
-
const onAbort = () => {
|
|
905
|
-
stop();
|
|
906
|
-
io.output.off("resize", render);
|
|
907
|
-
region.finish();
|
|
908
|
-
reject(withdrawn(signal));
|
|
909
|
-
};
|
|
910
|
-
const stop = readKeys(io.input, (key) => {
|
|
911
|
-
if (key.name === "enter") {
|
|
912
|
-
const value = state.value.trim();
|
|
913
|
-
finish({
|
|
914
|
-
kind: "text",
|
|
915
|
-
value
|
|
916
|
-
}, done(value));
|
|
917
|
-
} else if (key.name === "escape" || key.name === "ctrl-d" && state.value === "") finish({ kind: "cancelled" }, done());
|
|
918
|
-
else if (key.name === "ctrl-c") {
|
|
919
|
-
finish({ kind: "interrupted" });
|
|
920
|
-
interrupt(io);
|
|
921
|
-
} else {
|
|
922
|
-
state = editLine(state, key);
|
|
923
|
-
render();
|
|
924
|
-
}
|
|
925
|
-
});
|
|
926
|
-
io.output.on("resize", render);
|
|
927
|
-
signal?.addEventListener("abort", onAbort, { once: true });
|
|
928
|
-
render();
|
|
929
|
-
});
|
|
930
|
-
};
|
|
931
|
-
// src/term/select.ts
|
|
932
|
-
/**
|
|
933
|
-
* @file The select prompt: a list of options moved with ↑/↓ (or `j`/`k`),
|
|
934
|
-
* Home/End and `1`–`9`, chosen with Enter. `n` adds a note (one line, edited
|
|
935
|
-
* in place), `d` shows or hides the details (a document), `q` or Esc leaves
|
|
936
|
-
* the question for later, Ctrl-C stops. It redraws only its own lines,
|
|
937
|
-
* follows a resize, and collapses to one line once answered. Plain mode
|
|
938
|
-
* prints the options as `1) id label - description` and asks `Choose [n]: `,
|
|
939
|
-
* as `ork decide` always did; there a note is asked for after the choice when
|
|
940
|
-
* the prompt takes one.
|
|
941
|
-
*/
|
|
942
|
-
const NOTE_PROMPT = "Note for the run (optional, Enter to skip)";
|
|
943
|
-
/** The option an answer names: its number, or its id; empty is `recommended`. */
|
|
944
|
-
const pickOption = (answer, options, recommended) => {
|
|
945
|
-
const value = answer.trim();
|
|
946
|
-
if (value === "") return recommended;
|
|
947
|
-
const index = Number(value);
|
|
948
|
-
if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
|
|
949
|
-
return options.find((o) => o.id === value)?.id;
|
|
950
|
-
};
|
|
951
|
-
const label = (option) => option.label === void 0 ? option.id : `${option.id} ${option.label}`;
|
|
952
|
-
const defaultDone = (options) => (result, term) => {
|
|
953
|
-
if (result.kind === "later") return ` ${paint(term, "muted", "·")} left for later`;
|
|
954
|
-
const option = options.find((o) => o.id === result.id);
|
|
955
|
-
return cut(` ${paint(term, "ok", "✓")} ${option === void 0 ? result.id : label(option)}`, term.width);
|
|
956
|
-
};
|
|
957
|
-
/** The numbered option lines of a plain prompt: ` 1) id label - description`. */
|
|
958
|
-
const plainOptions = (options, recommended) => options.map((option, i) => {
|
|
959
|
-
const description = option.description === void 0 ? "" : ` - ${option.description}`;
|
|
960
|
-
const mark = option.id === recommended ? " (recommended)" : "";
|
|
961
|
-
const name = option.label === void 0 ? "" : ` ${option.label}`;
|
|
962
|
-
return ` ${String(i + 1)}) ${option.id}${name}${description}${mark}`;
|
|
963
|
-
});
|
|
964
|
-
/** The plain prompt: numbered options, then `Choose [n]: ` until one fits. */
|
|
965
|
-
const selectPlain = (io, o) => {
|
|
966
|
-
const term = termOf(io.output, io.env);
|
|
967
|
-
const say = (text) => {
|
|
968
|
-
io.output.write(`${text}\n`);
|
|
969
|
-
};
|
|
970
|
-
for (const line of o.header?.(term) ?? []) say(line);
|
|
971
|
-
for (const line of plainOptions(o.options, o.recommended)) say(line);
|
|
972
|
-
for (const line of o.footer ?? []) say(line);
|
|
973
|
-
const at = o.options.findIndex((opt) => opt.id === o.recommended);
|
|
974
|
-
const hint = at === -1 ? "" : ` [${String(at + 1)}]`;
|
|
975
|
-
const ask = async () => {
|
|
976
|
-
const answer = await readLine(io, ` Choose${hint}: `, o.signal);
|
|
977
|
-
if (answer === void 0) throw new Error("input ended");
|
|
978
|
-
const id = pickOption(answer, o.options, o.recommended);
|
|
979
|
-
if (id === void 0) {
|
|
980
|
-
if (o.details !== void 0 && answer.trim() === "d") say(o.details);
|
|
981
|
-
else say(" Type an option's number or id.");
|
|
982
|
-
return ask();
|
|
983
|
-
}
|
|
984
|
-
if (o.note !== true) return {
|
|
985
|
-
kind: "chosen",
|
|
986
|
-
id
|
|
987
|
-
};
|
|
988
|
-
const noted = await readText(io, {
|
|
989
|
-
message: o.notePrompt ?? NOTE_PROMPT,
|
|
990
|
-
...o.signal === void 0 ? {} : { signal: o.signal }
|
|
991
|
-
});
|
|
992
|
-
return noted.kind === "text" && noted.value !== "" ? {
|
|
993
|
-
kind: "chosen",
|
|
994
|
-
id,
|
|
995
|
-
note: noted.value
|
|
996
|
-
} : {
|
|
997
|
-
kind: "chosen",
|
|
998
|
-
id
|
|
999
|
-
};
|
|
1000
|
-
};
|
|
1001
|
-
return ask();
|
|
1002
|
-
};
|
|
1003
|
-
/** Asks for one of `options`. */
|
|
1004
|
-
const select = (io, o) => {
|
|
1005
|
-
if (o.signal?.aborted === true) return Promise.reject(withdrawn(o.signal));
|
|
1006
|
-
if (!canKey(io.input, io.output, io.env)) return selectPlain(io, o);
|
|
1007
|
-
const term = () => termOf(io.output, io.env);
|
|
1008
|
-
const region = createRegion(io.output, term);
|
|
1009
|
-
const done = o.done ?? defaultDone(o.options);
|
|
1010
|
-
return new Promise((resolve, reject) => {
|
|
1011
|
-
const start = o.options.findIndex((opt) => opt.id === o.recommended);
|
|
1012
|
-
let cursor = Math.max(0, start);
|
|
1013
|
-
let showDetails = false;
|
|
1014
|
-
let note;
|
|
1015
|
-
let editing;
|
|
1016
|
-
const optionLines = (t) => o.options.flatMap((option, i) => {
|
|
1017
|
-
const selected = i === cursor;
|
|
1018
|
-
const number = i < 9 ? `${String(i + 1)}.` : " ";
|
|
1019
|
-
const mark = option.id === o.recommended ? " (recommended)" : "";
|
|
1020
|
-
const head = cut(`${number} ${label(option)}`, t.width - 4 - mark.length);
|
|
1021
|
-
const text = selected ? apply$1(t, ["bold", "cyan"], head) : head;
|
|
1022
|
-
const lines = [` ${selected ? paint(t, "accent", "❯") : " "} ${text}${mark === "" ? "" : paint(t, "ok", mark)}`];
|
|
1023
|
-
for (const line of wrap(option.description ?? "", t.width - 7)) if (line !== "") lines.push(paint(t, "muted", ` ${line}`));
|
|
1024
|
-
return lines;
|
|
1025
|
-
});
|
|
1026
|
-
const keyLine = (t) => {
|
|
1027
|
-
const keys = editing === void 0 ? [
|
|
1028
|
-
"↑/↓ move",
|
|
1029
|
-
"Enter choose",
|
|
1030
|
-
...o.note === true ? ["n note"] : [],
|
|
1031
|
-
...o.details === void 0 ? [] : [`d ${showDetails ? "hide" : "show"} details`],
|
|
1032
|
-
...o.later === true ? ["q later"] : []
|
|
1033
|
-
] : ["Enter save", "Esc cancel"];
|
|
1034
|
-
return paint(t, "muted", cut(` ${keys.join(" · ")}`, t.width));
|
|
1035
|
-
};
|
|
1036
|
-
const render = () => {
|
|
1037
|
-
const t = term();
|
|
1038
|
-
const head = o.header?.(t) ?? [];
|
|
1039
|
-
const body = optionLines(t);
|
|
1040
|
-
const noteLine = note === void 0 ? [] : [paint(t, "muted", cut(` note: ${note}`, t.width))];
|
|
1041
|
-
const above = [
|
|
1042
|
-
...head,
|
|
1043
|
-
...body,
|
|
1044
|
-
...noteLine
|
|
1045
|
-
];
|
|
1046
|
-
const room = Math.max(3, t.rows - above.length - 3);
|
|
1047
|
-
const all = showDetails && o.details !== void 0 ? wrap(o.details.trimEnd(), t.width - 3) : [];
|
|
1048
|
-
const detail = (all.length > room ? [...all.slice(0, room - 1), `… ${String(all.length - room + 1)} more lines`] : all).map((line) => paint(t, "muted", ` ${line}`));
|
|
1049
|
-
const lines = [
|
|
1050
|
-
...above,
|
|
1051
|
-
...detail,
|
|
1052
|
-
keyLine(t)
|
|
1053
|
-
];
|
|
1054
|
-
if (editing === void 0) {
|
|
1055
|
-
region.draw(lines);
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
const lead = ` ${paint(t, "accent", "✎")} Note › `;
|
|
1059
|
-
const { line, column } = showLine(lead, editing, t.width, 10);
|
|
1060
|
-
region.draw([...lines, line], column);
|
|
1061
|
-
};
|
|
1062
|
-
const finish = (result) => {
|
|
1063
|
-
stop();
|
|
1064
|
-
cleanup();
|
|
1065
|
-
region.finish(done(result, term()));
|
|
1066
|
-
resolve(result);
|
|
1067
|
-
};
|
|
1068
|
-
const cleanup = () => {
|
|
1069
|
-
io.output.off("resize", render);
|
|
1070
|
-
o.signal?.removeEventListener("abort", onAbort);
|
|
1071
|
-
};
|
|
1072
|
-
const onAbort = () => {
|
|
1073
|
-
stop();
|
|
1074
|
-
cleanup();
|
|
1075
|
-
region.finish();
|
|
1076
|
-
reject(withdrawn(o.signal));
|
|
1077
|
-
};
|
|
1078
|
-
const edit = (key) => {
|
|
1079
|
-
if (editing === void 0) return;
|
|
1080
|
-
if (key.name === "enter") {
|
|
1081
|
-
const value = editing.value.trim();
|
|
1082
|
-
note = value === "" ? void 0 : value;
|
|
1083
|
-
editing = void 0;
|
|
1084
|
-
} else if (key.name === "escape") editing = void 0;
|
|
1085
|
-
else editing = editLine(editing, key);
|
|
1086
|
-
render();
|
|
1087
|
-
};
|
|
1088
|
-
const move = (to) => {
|
|
1089
|
-
cursor = Math.min(o.options.length - 1, Math.max(0, to));
|
|
1090
|
-
render();
|
|
1091
|
-
};
|
|
1092
|
-
const stop = readKeys(io.input, (key) => {
|
|
1093
|
-
if (key.name === "ctrl-c") {
|
|
1094
|
-
stop();
|
|
1095
|
-
cleanup();
|
|
1096
|
-
region.finish();
|
|
1097
|
-
resolve({ kind: "interrupted" });
|
|
1098
|
-
interrupt(io);
|
|
1099
|
-
} else if (editing !== void 0) edit(key);
|
|
1100
|
-
else if (key.name === "enter") {
|
|
1101
|
-
const id = o.options[cursor]?.id;
|
|
1102
|
-
if (id === void 0) return;
|
|
1103
|
-
finish(note === void 0 ? {
|
|
1104
|
-
kind: "chosen",
|
|
1105
|
-
id
|
|
1106
|
-
} : {
|
|
1107
|
-
kind: "chosen",
|
|
1108
|
-
id,
|
|
1109
|
-
note
|
|
1110
|
-
});
|
|
1111
|
-
} else if (key.name === "up") move(cursor - 1);
|
|
1112
|
-
else if (key.name === "down") move(cursor + 1);
|
|
1113
|
-
else if (key.name === "home") move(0);
|
|
1114
|
-
else if (key.name === "end") move(o.options.length - 1);
|
|
1115
|
-
else if (key.name === "escape" && o.later === true) finish({ kind: "later" });
|
|
1116
|
-
else if (key.name === "char") {
|
|
1117
|
-
const c = key.text;
|
|
1118
|
-
if (c === "k") move(cursor - 1);
|
|
1119
|
-
else if (c === "j") move(cursor + 1);
|
|
1120
|
-
else if (c >= "1" && c <= "9") {
|
|
1121
|
-
if (Number(c) <= o.options.length) move(Number(c) - 1);
|
|
1122
|
-
} else if (c === "n" && o.note === true) {
|
|
1123
|
-
const value = note ?? "";
|
|
1124
|
-
editing = {
|
|
1125
|
-
value,
|
|
1126
|
-
pos: Array.from(value).length
|
|
1127
|
-
};
|
|
1128
|
-
render();
|
|
1129
|
-
} else if (c === "d" && o.details !== void 0) {
|
|
1130
|
-
showDetails = !showDetails;
|
|
1131
|
-
render();
|
|
1132
|
-
} else if (c === "q" && o.later === true) finish({ kind: "later" });
|
|
1133
|
-
}
|
|
1134
|
-
});
|
|
1135
|
-
io.output.on("resize", render);
|
|
1136
|
-
o.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1137
|
-
render();
|
|
1138
|
-
});
|
|
1139
|
-
};
|
|
1140
|
-
// src/human.ts
|
|
1141
|
-
/**
|
|
1142
|
-
* @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
|
|
1143
|
-
* permission prompts on the terminal, one at a time, and denies or defers
|
|
1144
|
-
* when no one is there. It draws through the terminal kit (`term/`): a card
|
|
1145
|
-
* with an ASK chip and a list moved with the arrow keys, a yes-or-no for a
|
|
1146
|
-
* permission, and one optional line for a note after a choice. Plain (not a
|
|
1147
|
-
* terminal, `NO_COLOR`, `--plain`) it keeps the numbered prompts. In a run it
|
|
1148
|
-
* is the decision queue's local answerer, racing `ork decide`.
|
|
1149
|
-
*/
|
|
1150
|
-
const PREVIEW = 160;
|
|
1151
|
-
/** A document longer than this is offered (`d`) rather than printed. */
|
|
1152
|
-
const SHORT_DOCUMENT_LINES = 40;
|
|
1153
|
-
const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
|
|
1154
|
-
const NOTE_MESSAGE = "Note for the run (optional, Enter to skip)";
|
|
1155
|
-
const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
|
|
1156
|
-
/** One line that shows what a tool is about to do. */
|
|
1157
|
-
const previewInput = (toolName, input) => {
|
|
1158
|
-
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
1159
|
-
const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
|
|
1160
|
-
return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
|
|
1161
|
-
};
|
|
1162
|
-
/** Never answers, until `signal` withdraws it: what a stopped prompt leaves. */
|
|
1163
|
-
const parked = (signal) => new Promise((_, reject) => {
|
|
1164
|
-
signal?.addEventListener("abort", () => {
|
|
1165
|
-
reject(withdrawn(signal));
|
|
1166
|
-
});
|
|
1167
|
-
});
|
|
1168
|
-
/**
|
|
1169
|
-
* Answers permission prompts and `ask_user` questions on the terminal, one at a
|
|
1170
|
-
* time. Without a terminal it denies tools and leaves questions to the node. A
|
|
1171
|
-
* withdrawn prompt (answered elsewhere) rejects, which frees the turn.
|
|
1172
|
-
*/
|
|
1173
|
-
const createTerminalHuman = (io) => {
|
|
1174
|
-
let rl;
|
|
1175
|
-
let closed;
|
|
1176
|
-
let turn = Promise.resolve();
|
|
1177
|
-
const say = (text) => {
|
|
1178
|
-
io.output.write(`${text}\n`);
|
|
1179
|
-
};
|
|
1180
|
-
/** The one `readline` the plain prompts share; a rich prompt never opens it. */
|
|
1181
|
-
const readLine = (text, signal) => {
|
|
1182
|
-
if (rl === void 0) {
|
|
1183
|
-
const opened = createInterface({
|
|
1184
|
-
input: io.input,
|
|
1185
|
-
output: io.output,
|
|
1186
|
-
...plainEnv(io.env) ? { terminal: false } : {}
|
|
1187
|
-
});
|
|
1188
|
-
if (io.interrupt !== void 0) opened.on("SIGINT", io.interrupt);
|
|
1189
|
-
closed = new Promise((resolve) => {
|
|
1190
|
-
opened.once("close", () => {
|
|
1191
|
-
resolve(void 0);
|
|
1192
|
-
});
|
|
1193
|
-
});
|
|
1194
|
-
rl = opened;
|
|
1195
|
-
}
|
|
1196
|
-
const answer = signal === void 0 ? rl.question(text) : rl.question(text, { signal });
|
|
1197
|
-
return closed === void 0 ? answer : Promise.race([answer, closed]);
|
|
1198
|
-
};
|
|
1199
|
-
const kit = {
|
|
1200
|
-
input: io.input,
|
|
1201
|
-
output: io.output,
|
|
1202
|
-
readLine,
|
|
1203
|
-
...io.env === void 0 ? {} : { env: io.env },
|
|
1204
|
-
...io.interrupt === void 0 ? {} : { interrupt: io.interrupt }
|
|
1205
|
-
};
|
|
1206
|
-
const term = () => termOf(io.output, io.env);
|
|
1207
|
-
/** The optional line after a choice, when the asker takes a note. */
|
|
1208
|
-
const takeNote = async (onNote, signal) => {
|
|
1209
|
-
if (onNote === void 0) return;
|
|
1210
|
-
const noted = await readText(kit, {
|
|
1211
|
-
message: NOTE_MESSAGE,
|
|
1212
|
-
...signal === void 0 ? {} : { signal }
|
|
1213
|
-
});
|
|
1214
|
-
if (noted.kind === "interrupted") await parked(signal);
|
|
1215
|
-
if (noted.kind === "text" && noted.value !== "") onNote(noted.value);
|
|
1216
|
-
};
|
|
1217
|
-
const oneAtATime = (work) => {
|
|
1218
|
-
const result = turn.then(work);
|
|
1219
|
-
turn = result.catch(() => null);
|
|
1220
|
-
return result;
|
|
1221
|
-
};
|
|
1222
|
-
const asking = (q, place) => oneAtATime(async () => {
|
|
1223
|
-
const { signal } = q;
|
|
1224
|
-
if (signal?.aborted === true) throw new Error("the question was withdrawn");
|
|
1225
|
-
const t = term();
|
|
1226
|
-
const documentLines = q.document?.trimEnd().split("\n").length ?? 0;
|
|
1227
|
-
const document = q.document === void 0 ? void 0 : clean(q.document);
|
|
1228
|
-
const printed = document !== void 0 && documentLines <= SHORT_DOCUMENT_LINES;
|
|
1229
|
-
const shownDocument = t.rich || !printed ? void 0 : document;
|
|
1230
|
-
const header = (tt) => cardLines(tt, q, place, io.verbose === true, shownDocument).lines;
|
|
1231
|
-
const options = q.options.map((o) => ({
|
|
1232
|
-
id: o.id,
|
|
1233
|
-
...o.label === void 0 ? {} : { label: plain(o.label) },
|
|
1234
|
-
...o.description === void 0 ? {} : { description: plain(o.description) }
|
|
1235
|
-
}));
|
|
1236
|
-
const { moreContext } = cardLines(t, q, place, io.verbose === true);
|
|
1237
|
-
const details = t.rich ? [moreContext ? clean(q.context ?? "") : void 0, document].filter((part) => part !== void 0).join("\n\n") || void 0 : printed ? void 0 : document;
|
|
1238
|
-
if (!io.interactive) {
|
|
1239
|
-
for (const line of header(t)) say(line);
|
|
1240
|
-
for (const line of plainOptions(options, q.recommended)) say(line);
|
|
1241
|
-
const answer = q.recommended ?? NO_ONE_TO_ASK;
|
|
1242
|
-
say(paint(t, "muted", ` no terminal; answered ${answer}`));
|
|
1243
|
-
return answer;
|
|
1244
|
-
}
|
|
1245
|
-
const result = await select(kit, {
|
|
1246
|
-
header,
|
|
1247
|
-
options,
|
|
1248
|
-
...q.recommended === void 0 ? {} : { recommended: q.recommended },
|
|
1249
|
-
...details === void 0 ? {} : { details },
|
|
1250
|
-
...!t.rich && details !== void 0 ? { footer: [` Document: ${String(documentLines)} lines. Type d to read it.`] } : {},
|
|
1251
|
-
note: q.onNote !== void 0,
|
|
1252
|
-
notePrompt: NOTE_MESSAGE,
|
|
1253
|
-
later: place !== void 0,
|
|
1254
|
-
done: (answer, tt) => doneLine(tt, place, q.nodeId, answer),
|
|
1255
|
-
...signal === void 0 ? {} : { signal }
|
|
1256
|
-
});
|
|
1257
|
-
if (result.kind === "interrupted") return parked(signal);
|
|
1258
|
-
if (result.kind === "later") return null;
|
|
1259
|
-
if (result.note !== void 0) q.onNote?.(result.note);
|
|
1260
|
-
return result.id;
|
|
1261
|
-
});
|
|
1262
|
-
return {
|
|
1263
|
-
permit: ({ nodeId, toolName, input, signal, onNote }) => oneAtATime(async () => {
|
|
1264
|
-
if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
|
|
1265
|
-
const t = term();
|
|
1266
|
-
if (t.rich) say("");
|
|
1267
|
-
say(permitLine(t, nodeId, previewInput(toolName, input)));
|
|
1268
|
-
if (!io.interactive) {
|
|
1269
|
-
say(paint(t, "muted", ` denied: ${NO_ONE_TO_PERMIT}`));
|
|
1270
|
-
return {
|
|
1271
|
-
allow: false,
|
|
1272
|
-
message: NO_ONE_TO_PERMIT
|
|
1273
|
-
};
|
|
1274
|
-
}
|
|
1275
|
-
const allowed = await confirmOrStop(kit, {
|
|
1276
|
-
question: "Allow?",
|
|
1277
|
-
...signal === void 0 ? {} : { signal }
|
|
1278
|
-
});
|
|
1279
|
-
if (allowed === null) return parked(signal);
|
|
1280
|
-
if (allowed) return { allow: true };
|
|
1281
|
-
await takeNote(onNote, signal);
|
|
1282
|
-
return {
|
|
1283
|
-
allow: false,
|
|
1284
|
-
message: "The user did not allow this tool."
|
|
1285
|
-
};
|
|
1286
|
-
}),
|
|
1287
|
-
ask: async (q) => await asking(q) ?? NO_ONE_TO_ASK,
|
|
1288
|
-
askAt: (q, place) => asking(q, place),
|
|
1289
|
-
close: () => {
|
|
1290
|
-
rl?.close();
|
|
1291
|
-
}
|
|
1292
|
-
};
|
|
1293
|
-
};
|
|
1294
|
-
// src/run/format-end.ts
|
|
1295
|
-
/**
|
|
1296
|
-
* @file How a node's end reads on the terminal (specs/runtime/session-ends.md):
|
|
1297
|
-
* its kind's label and Ork's own message, a note for a kind that may pass,
|
|
1298
|
-
* the setup hint for an account or provider end, and the scrubbed raw detail
|
|
1299
|
-
* only when asked for. Never the provider's own words first.
|
|
1300
|
-
*/
|
|
1301
|
-
/** What a transient kind says in place of its hint on the log line. */
|
|
1302
|
-
const MAY_PASS = "(may pass; ork resume retries)";
|
|
1303
|
-
/** `Turn limit: reached its turn limit (8)`, with a note when it may pass. */
|
|
1304
|
-
const endHeadline = (end) => {
|
|
1305
|
-
const { label, transient } = END_KINDS[end.kind];
|
|
1306
|
-
return `${label}: ${end.message}${transient ? ` ${MAY_PASS}` : ""}`;
|
|
1307
|
-
};
|
|
1308
|
-
/**
|
|
1309
|
-
* The lines under a headline: the hint for an account or provider end (what to
|
|
1310
|
-
* set up) or whenever `full` (`--verbose`, one node's log), and the detail only
|
|
1311
|
-
* when `full`. A transient kind's hint is its retry wording, which the headline
|
|
1312
|
-
* already says, so it is not printed twice.
|
|
1313
|
-
*/
|
|
1314
|
-
const endLines = (end, full, term = createView().term) => {
|
|
1315
|
-
const style = styler(term);
|
|
1316
|
-
const { actor, hint, transient } = END_KINDS[end.kind];
|
|
1317
|
-
const setup = actor === "account" || actor === "provider";
|
|
1318
|
-
const lines = [];
|
|
1319
|
-
if (hint !== "" && !transient && (full || setup)) lines.push(` ${style("dim", hint)}`);
|
|
1320
|
-
if (full && end.detail !== void 0) lines.push(style("dim", " details:"), ...end.detail.split("\n").map((line) => style("dim", ` ${line}`)));
|
|
1321
|
-
return lines;
|
|
1322
|
-
};
|
|
1323
|
-
/**
|
|
1324
|
-
* A finished node's line under `tag`: its verdict, or for a failure its kind
|
|
1325
|
-
* and message (and the lines under it). The agent's own summary stays for a
|
|
1326
|
-
* `fail` it reported, and an old run's text no rule reads stays as stored.
|
|
1327
|
-
*/
|
|
1328
|
-
const formatFinished = (event, tag, verbose, term = createView().term, rest = tag) => {
|
|
1329
|
-
const style = styler(term);
|
|
1330
|
-
const conflict = hasConflict(event.output);
|
|
1331
|
-
const mark = event.ok ? style(conflict ? "yellow" : "green", conflict ? "!" : "✓") : style("red", "✗");
|
|
1332
|
-
const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
|
|
1333
|
-
const end = endOf(event);
|
|
1334
|
-
if (!(!event.ok && end.kind !== "reported_failure" && (event.end !== void 0 || end.kind !== "unknown"))) return [`${tag} ${mark} ${nodeVerdict(event.output, event.ok, event.error)}${spent}`];
|
|
1335
|
-
return [`${tag} ${mark} ${endHeadline(end)}${spent}`, ...endLines(end, verbose, term).map((line) => `${rest} ${line}`)];
|
|
1336
|
-
};
|
|
1337
|
-
// src/run/format-outcome.ts
|
|
1338
|
-
/**
|
|
1339
|
-
* @file How a finished session reads on the terminal: its report or its end,
|
|
1340
|
-
* the branch, artifacts and fields, and the usage line.
|
|
1341
|
-
*/
|
|
1342
|
-
const STATUS_MARK = {
|
|
1343
|
-
pass: {
|
|
1344
|
-
mark: "✓",
|
|
1345
|
-
color: "green"
|
|
1346
|
-
},
|
|
1347
|
-
done: {
|
|
1348
|
-
mark: "✓",
|
|
1349
|
-
color: "green"
|
|
1350
|
-
},
|
|
1351
|
-
changes_requested: {
|
|
1352
|
-
mark: "!",
|
|
1353
|
-
color: "yellow"
|
|
1354
|
-
},
|
|
1355
|
-
fail: {
|
|
1356
|
-
mark: "✗",
|
|
1357
|
-
color: "red"
|
|
1358
|
-
}
|
|
1359
|
-
};
|
|
1360
|
-
const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
|
|
1361
|
-
/** The usage line: cost, tokens, turns and time. */
|
|
1362
|
-
const formatUsage = ({ usage }, term = createView().term) => styler(term)("dim", [
|
|
1363
|
-
cost(usage.costUsd),
|
|
1364
|
-
`${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
|
|
1365
|
-
`${String(usage.turns)} turns`,
|
|
1366
|
-
seconds(usage.durationMs)
|
|
1367
|
-
].join(" · "));
|
|
1368
|
-
/** How a finished session reads on the terminal. */
|
|
1369
|
-
const formatOutcome = (nodeId, outcome, verbose = false, term = createView().term) => {
|
|
1370
|
-
const style = styler(term);
|
|
1371
|
-
const { report } = outcome;
|
|
1372
|
-
if (report === null) {
|
|
1373
|
-
const end = outcomeEnd(outcome);
|
|
1374
|
-
return [
|
|
1375
|
-
`${style("red", "✗")} ${style("bold", nodeId)} ${endHeadline(end)}`,
|
|
1376
|
-
...endLines(end, verbose, term),
|
|
1377
|
-
` ${formatUsage(outcome, term)}`
|
|
1378
|
-
];
|
|
1379
|
-
}
|
|
1380
|
-
const shown = STATUS_MARK[report.status];
|
|
1381
|
-
const lines = [`${shown === void 0 ? "-" : style(shown.color, shown.mark)} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
|
|
1382
|
-
if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
|
|
1383
|
-
for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
|
|
1384
|
-
for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
|
|
1385
|
-
lines.push(` ${formatUsage(outcome, term)}`);
|
|
1386
|
-
return lines;
|
|
1387
|
-
};
|
|
1388
|
-
// src/run/format-result.ts
|
|
1389
|
-
/** How a run ended: its mark, and its chip's word and role when rich. */
|
|
1390
|
-
const RUN_END = {
|
|
1391
|
-
succeeded: {
|
|
1392
|
-
mark: "✓",
|
|
1393
|
-
ok: "ok",
|
|
1394
|
-
label: CHIP_LABELS.ok
|
|
1395
|
-
},
|
|
1396
|
-
failed: {
|
|
1397
|
-
mark: "✗",
|
|
1398
|
-
ok: "danger",
|
|
1399
|
-
label: CHIP_LABELS.fail
|
|
1400
|
-
},
|
|
1401
|
-
budget_exhausted: {
|
|
1402
|
-
mark: "!",
|
|
1403
|
-
ok: "warn",
|
|
1404
|
-
label: CHIP_LABELS.wait
|
|
1405
|
-
},
|
|
1406
|
-
quota_exhausted: {
|
|
1407
|
-
mark: "!",
|
|
1408
|
-
ok: "warn",
|
|
1409
|
-
label: CHIP_LABELS.wait
|
|
1410
|
-
},
|
|
1411
|
-
timed_out: {
|
|
1412
|
-
mark: "!",
|
|
1413
|
-
ok: "warn",
|
|
1414
|
-
label: CHIP_LABELS.wait
|
|
1415
|
-
},
|
|
1416
|
-
interrupted: {
|
|
1417
|
-
mark: "!",
|
|
1418
|
-
ok: "warn",
|
|
1419
|
-
label: CHIP_LABELS.wait
|
|
1420
|
-
}
|
|
1421
|
-
};
|
|
1422
|
-
const MARK_COLOR = {
|
|
1423
|
-
ok: "green",
|
|
1424
|
-
danger: "red",
|
|
1425
|
-
warn: "yellow"
|
|
1426
|
-
};
|
|
1427
|
-
/** `1 failure handled`, `2 failures handled`. */
|
|
1428
|
-
const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" : "failures"} handled`;
|
|
1429
|
-
/**
|
|
1430
|
-
* The run summary: status, cost, what ran, what was skipped and which failures
|
|
1431
|
-
* an edge handled, branches to look at, why, and mail to nodes that never ran.
|
|
1432
|
-
*/
|
|
1433
|
-
const formatRunResult = (result, term = createView().term) => {
|
|
1434
|
-
const style = styler(term);
|
|
1435
|
-
const end = RUN_END[result.status];
|
|
1436
|
-
const mark = term.rich ? chip(term, end.label, end.ok) : style(MARK_COLOR[end.ok], end.mark);
|
|
1437
|
-
const ran = result.nodes.filter((n) => n.runs > 0).length;
|
|
1438
|
-
const skipped = result.skipped ?? result.nodes.length - ran;
|
|
1439
|
-
const handled = result.failuresHandled ?? 0;
|
|
1440
|
-
const lines = ["", `${mark} run ${result.runId} ${result.status.replace("_", " ")} ${style("dim", `· ${cost(result.costUsd)} · ${String(ran)} ran, ${String(skipped)} skipped${handled === 0 ? "" : `, ${failuresHandled(handled)}`}`)}`];
|
|
1441
|
-
for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
|
|
1442
|
-
for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
|
|
1443
|
-
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`);
|
|
1444
|
-
return lines;
|
|
1445
|
-
};
|
|
1446
|
-
// src/run/format-tool.ts
|
|
1447
|
-
/**
|
|
1448
|
-
* @file How a session's own words and tool calls read in the transcript: the
|
|
1449
|
-
* agent's text (indented plain, wrapped rich), and a tool call as `Tool first
|
|
1450
|
-
* line` cut to the screen with `+N lines`, a path inside the node's worktree
|
|
1451
|
-
* relative to it, or whole with `--verbose`. Untrusted text goes through
|
|
1452
|
-
* `plain()` first.
|
|
1453
|
-
*/
|
|
1454
|
-
const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${plain(line)}`);
|
|
1455
|
-
/** Agent text under a 2-column indent, its long lines wrapped to `room`. */
|
|
1456
|
-
const wrapped = (text, room) => {
|
|
1457
|
-
const width = Math.max(1, room - 2);
|
|
1458
|
-
return text.trimEnd().split("\n").flatMap((line) => {
|
|
1459
|
-
const safe = plain(line).trimEnd();
|
|
1460
|
-
return widthOf(safe) <= width ? [safe] : wrap(safe, width);
|
|
1461
|
-
}).map((line) => ` ${line}`);
|
|
1462
|
-
};
|
|
1463
|
-
/** A tool's input as the call says it: the key that names its target, whole. */
|
|
1464
|
-
const toolText = (input, root) => {
|
|
1465
|
-
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
1466
|
-
const text = key === void 0 ? JSON.stringify(input) : String(input[key]);
|
|
1467
|
-
if (root === void 0) return text;
|
|
1468
|
-
const base = root.replace(/\/+$/u, "");
|
|
1469
|
-
return text === base ? "." : text.replaceAll(`${base}/`, "");
|
|
1470
|
-
};
|
|
1471
|
-
/**
|
|
1472
|
-
* A tool call: rich, `Tool first line` cut to `room` with `+N lines` when it
|
|
1473
|
-
* had more; plain, today's one dim line. `verbose` shows the call whole.
|
|
1474
|
-
*/
|
|
1475
|
-
const toolLines = (name, input, view, room, verbose, node) => {
|
|
1476
|
-
const { term } = view;
|
|
1477
|
-
const style = styler(term);
|
|
1478
|
-
if (!term.rich && !verbose) return [style("dim", ` · ${plain(previewInput(name, input))}`)];
|
|
1479
|
-
const label = plain(name);
|
|
1480
|
-
const [first = "", ...rest] = toolText(input, view.roots.get(node)).trimEnd().split("\n").map((line) => plain(line));
|
|
1481
|
-
const lead = term.rich ? " " : " · ";
|
|
1482
|
-
const sep = term.rich ? " " : " ";
|
|
1483
|
-
const headWidth = widthOf(lead) + widthOf(label) + sep.length;
|
|
1484
|
-
const styledHead = `${style("dim", lead)}${style("bold", label)}${style("dim", sep)}`;
|
|
1485
|
-
if (verbose) {
|
|
1486
|
-
const pad = " ".repeat(headWidth);
|
|
1487
|
-
return [`${styledHead}${style("dim", first)}`, ...rest.map((line) => style("dim", `${pad}${line}`))];
|
|
1488
|
-
}
|
|
1489
|
-
const more = rest.length === 0 ? "" : ` +${String(rest.length)} ${rest.length === 1 ? "line" : "lines"}`;
|
|
1490
|
-
return [`${styledHead}${style("dim", cut(first.trim(), Math.max(1, room - headWidth - more.length)))}${style("dim", more)}`];
|
|
1491
|
-
};
|
|
1492
|
-
// src/run/format.ts
|
|
1493
|
-
/**
|
|
1494
|
-
* @file How runs read on the terminal: session events and run events with node
|
|
1495
|
-
* chips or prefixes; the node result is `format-outcome.ts`'s, a tool call
|
|
1496
|
-
* `format-tool.ts`'s and the run summary `format-result.ts`'s.
|
|
1497
|
-
*/
|
|
1498
|
-
/** A line's 2-column indent, after any style codes that open it. */
|
|
1499
|
-
const INDENT = /^((?:\u001B\[[0-9;]*m)*) {2}/u;
|
|
1500
|
-
/** Who answered a decision, as the log says it. */
|
|
1501
|
-
const ANSWERED_BY = {
|
|
1502
|
-
terminal: "terminal",
|
|
1503
|
-
decide: "ork decide",
|
|
1504
|
-
ui: "ork ui",
|
|
1505
|
-
owner: "its owner"
|
|
1506
|
-
};
|
|
1507
|
-
/** The transcript lines for one event; tool names come from `tools`. */
|
|
1508
|
-
const formatEvent = (nodeId, event, tools, verbose = false, view = createView(), room = view.term.width - 1) => {
|
|
1509
|
-
const { term } = view;
|
|
1510
|
-
const style = styler(term);
|
|
1511
|
-
if (event.type === "session.started") {
|
|
1512
|
-
const fork = event.fork === true ? ", a fork to answer a question" : "";
|
|
1513
|
-
return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId}${fork})`)];
|
|
1514
|
-
}
|
|
1515
|
-
if (event.type === "assistant.text") return term.rich ? wrapped(event.text, room) : indent(event.text);
|
|
1516
|
-
if (event.type === "session.waiting") return indent(event.reason);
|
|
1517
|
-
if (event.type === "tool.called") {
|
|
1518
|
-
tools.set(event.id, event.name);
|
|
1519
|
-
return toolLines(event.name, event.input, view, room, verbose, nodeId);
|
|
1520
|
-
}
|
|
1521
|
-
if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
|
|
1522
|
-
if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
|
|
1523
|
-
return formatOutcome(nodeId, event.outcome, verbose, term);
|
|
1524
|
-
};
|
|
1525
|
-
const items = (count) => `${String(count)} ${count === 1 ? "item" : "items"}`;
|
|
1526
|
-
/** Transcript lines for one run event, prefixed with the node they are from. */
|
|
1527
|
-
const formatRunEvent = (event, width, tools, terminal = false, verbose = false, view = createView()) => {
|
|
1528
|
-
const { term } = view;
|
|
1529
|
-
const style = styler(term);
|
|
1530
|
-
const tag = (id) => term.rich ? `${nodeChip(term, id)}${" ".repeat(Math.max(0, width - id.length))}` : style("cyan", `${id.padEnd(width)} │`);
|
|
1531
|
-
const blank = (id) => term.rich ? " ".repeat(widthOf(tag(id))) : tag(id);
|
|
1532
|
-
const under = (id, lines) => lines.map((line, i) => `${i === 0 ? tag(id) : blank(id)} ${line}`);
|
|
1533
|
-
if (event.type === "run.started") {
|
|
1534
|
-
const starts = `starts at ${event.entries.join(", ")}`;
|
|
1535
|
-
return [
|
|
1536
|
-
term.rich ? `${chip(term, CHIP_LABELS.run, "accent")} run ${style("bold", event.runId)} of ${event.workflow} ${style("dim", starts)}` : style("dim", `▸ run ${event.runId} of ${event.workflow} (${starts})`),
|
|
1537
|
-
...event.settings?.trigger === void 0 ? [] : [style("dim", ` started by trigger ${event.settings.trigger}; untrusted inputs: ${(event.settings.untrusted ?? []).join(", ") || "none"}`)],
|
|
1538
|
-
...term.rich ? [""] : []
|
|
1539
|
-
];
|
|
1540
|
-
}
|
|
1541
|
-
if (event.type === "node.started") {
|
|
1542
|
-
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
1543
|
-
const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
|
|
1544
|
-
return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
|
|
1545
|
-
}
|
|
1546
|
-
if (event.type === "node.event") {
|
|
1547
|
-
const inner = event.event;
|
|
1548
|
-
if (inner.type === "session.finished") return [];
|
|
1549
|
-
const fork = inner.fork === true ? style("dim", "fork · ") : "";
|
|
1550
|
-
if (inner.type === "session.started") {
|
|
1551
|
-
const why = inner.fork === true ? ", to answer a question" : "";
|
|
1552
|
-
return [`${tag(event.id)} ${fork}${style("dim", `${inner.model}, session ${inner.sessionId}${why}`)}`];
|
|
1553
|
-
}
|
|
1554
|
-
const room = term.width - widthOf(tag(event.id)) - 1 + 2 - (inner.fork === true ? 7 : 0);
|
|
1555
|
-
return under(event.id, formatEvent(event.id, event.event, tools, verbose, view, room).map((line) => `${fork}${line.replace(INDENT, "$1")}`));
|
|
1556
|
-
}
|
|
1557
|
-
if (event.type === "node.finished") return formatFinished(event, tag(event.id), verbose, term, blank(event.id));
|
|
1558
|
-
if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
|
|
1559
|
-
if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
|
|
1560
|
-
if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
|
|
1561
|
-
if (event.type === "watch.changed") return [`${tag(event.id)} ${style("dim", `look ${String(event.looks)}:`)} ${event.said}`];
|
|
1562
|
-
if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
|
|
1563
|
-
if (event.type === "edge.taken") return [];
|
|
1564
|
-
if (event.type === "worktree") {
|
|
1565
|
-
if (event.action !== "removed") view.roots.set(event.id, event.path);
|
|
1566
|
-
const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
|
|
1567
|
-
const what = event.action === "removed" && event.branch !== void 0 ? `worktree of ${event.branch} (branch kept)` : `${event.branch ?? event.path}${detail}`;
|
|
1568
|
-
const line = `${event.action} ${what}`;
|
|
1569
|
-
return [`${tag(event.id)} ${event.action === "conflict" ? style("yellow", `! ${line}`) : style("dim", `⎇ ${line}`)}`];
|
|
1570
|
-
}
|
|
1571
|
-
if (event.type === "decision.opened") {
|
|
1572
|
-
const what = event.kind === "permission" ? "asks to" : "asks:";
|
|
1573
|
-
const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
|
|
1574
|
-
const where = event.owner === void 0 ? `decision ${event.decision}; ${terminal ? "answer here or with ork decide" : "ork decide"}` : `decision ${event.decision}, via ${event.owner}`;
|
|
1575
|
-
return [`${tag(event.node)} ${term.rich ? chip(term, CHIP_LABELS.ask, "accent") : style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
|
|
1576
|
-
}
|
|
1577
|
-
if (event.type === "decision.answered") {
|
|
1578
|
-
const by = event.by === "owner" && event.owner !== void 0 ? event.owner : ANSWERED_BY[event.by];
|
|
1579
|
-
const said = event.note === void 0 ? "" : ` · said: ${event.note.replaceAll(/\s+/gu, " ")}`;
|
|
1580
|
-
return [`${tag(event.node)} ${style("dim", `→ ${event.answer} (${by})${said}`)}`];
|
|
1581
|
-
}
|
|
1582
|
-
if (event.type === "decision.withdrawn") return [`${tag(event.node)} ${style("dim", "→ question withdrawn")}`];
|
|
1583
|
-
if (event.type === "run.command") {
|
|
1584
|
-
const node = event.node === void 0 ? "" : ` --node ${event.node}`;
|
|
1585
|
-
return [style(["yellow", "bold"], `! ork ${event.command}${node} received`)];
|
|
1586
|
-
}
|
|
1587
|
-
if (event.type === "node.held") {
|
|
1588
|
-
const at = event.at === "tool" ? "its tool call waits for ork continue" : "it starts after ork continue";
|
|
1589
|
-
return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
|
|
1590
|
-
}
|
|
1591
|
-
if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
|
|
1592
|
-
if (event.type === "slot.waiting") return [`${tag(event.id)} ${style("yellow", `‖ ${slotWaitLine(event.holders)}`)}`];
|
|
1593
|
-
if (event.type === "slot.taken") return [`${tag(event.id)} ${style("dim", "▸ got a check slot")}`];
|
|
1594
|
-
if (event.type === "each.listed") {
|
|
1595
|
-
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
1596
|
-
return [`${tag(event.node)} ${style("dim", `${items(event.count)}${round}`)}`];
|
|
1597
|
-
}
|
|
1598
|
-
if (event.type === "each.exceeded") {
|
|
1599
|
-
const over = `${items(event.count)}, over maxItems ${String(event.maxItems)}`;
|
|
1600
|
-
return [`${tag(event.node)} ${style("yellow", `! ${over}`)}`];
|
|
1601
|
-
}
|
|
1602
|
-
if (event.type === "run.finished") return formatRunResult(event.result, term);
|
|
1603
|
-
if (event.type === "run.resumed") {
|
|
1604
|
-
const [one] = event.kept;
|
|
1605
|
-
return ["", style("dim", `▸ resumed: ${one === void 0 ? "every node runs again" : event.kept.length === 1 ? `${one} keeps its result` : `${event.kept.join(", ")} keep their results`}`)];
|
|
1606
|
-
}
|
|
1607
|
-
if (event.type === "feedback.recorded") {
|
|
1608
|
-
const { target, kind, title } = event.feedback;
|
|
1609
|
-
const line = style("dim", `✎ feedback (${target}, ${kind}): ${title}`);
|
|
1610
|
-
return [event.node === void 0 ? line : `${tag(event.node)} ${line}`];
|
|
1611
|
-
}
|
|
1612
|
-
if (event.type === "user.message") {
|
|
1613
|
-
const note = event.delivered === "queued" ? " (for its next session)" : "";
|
|
1614
|
-
return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
|
|
1615
|
-
}
|
|
1616
|
-
return under(event.node, formatBusEvent(event, term));
|
|
1617
|
-
};
|
|
1618
549
|
// src/runs/chat.ts
|
|
1619
550
|
/**
|
|
1620
551
|
* @file `ork chat`'s two paths. A node of a live run gets the message through
|
|
@@ -1664,6 +595,8 @@ const resumeChat = async (options) => {
|
|
|
1664
595
|
...template === void 0 ? {} : { template }
|
|
1665
596
|
});
|
|
1666
597
|
if (!plan.ok) return plan.error;
|
|
598
|
+
const declared = workflow.nodes[template ?? node];
|
|
599
|
+
const itemModel = declared !== void 0 && isClaudeNode(declared) && declared.model?.includes("{{") === true ? session.model : void 0;
|
|
1667
600
|
const servers = namedMcpEntries(run.project, plan.mcp);
|
|
1668
601
|
if (!servers.ok) return servers.error;
|
|
1669
602
|
let driver = options.driver ?? session.driver ?? plan.driver;
|
|
@@ -1681,6 +614,7 @@ const resumeChat = async (options) => {
|
|
|
1681
614
|
});
|
|
1682
615
|
const started = (await options.runtime(driver, placeFiles(place).runs)).start({
|
|
1683
616
|
...plan.spec,
|
|
617
|
+
...itemModel === void 0 ? {} : { model: itemModel },
|
|
1684
618
|
...servers.value,
|
|
1685
619
|
resume: session.sessionId,
|
|
1686
620
|
prompt: options.body,
|
|
@@ -1703,53 +637,6 @@ const resumeChat = async (options) => {
|
|
|
1703
637
|
}
|
|
1704
638
|
return failure;
|
|
1705
639
|
};
|
|
1706
|
-
// src/runs/logs.ts
|
|
1707
|
-
/**
|
|
1708
|
-
* @file `ork logs`: replays a stored run's events as the transcript `ork run`
|
|
1709
|
-
* printed, for the whole run or one node, and tails a live run until it
|
|
1710
|
-
* ends.
|
|
1711
|
-
*/
|
|
1712
|
-
/** Turns stored events into lines, keeping the state a transcript needs. */
|
|
1713
|
-
const createLogPrinter = (options) => {
|
|
1714
|
-
const tools = /* @__PURE__ */ new Map();
|
|
1715
|
-
const view = createView();
|
|
1716
|
-
let width = 8;
|
|
1717
|
-
return (stored) => {
|
|
1718
|
-
const { event } = stored;
|
|
1719
|
-
if (event.type === "run.started") width = Math.max(...event.nodes.map((n) => n.id.length), 1);
|
|
1720
|
-
if (options.node !== void 0 && !isNodeEvent(event, options.node)) return [];
|
|
1721
|
-
if (options.json) return [JSON.stringify(stored)];
|
|
1722
|
-
return formatRunEvent(event, width, tools, false, options.verbose === true || options.node !== void 0, view);
|
|
1723
|
-
};
|
|
1724
|
-
};
|
|
1725
|
-
/**
|
|
1726
|
-
* Why `--node` names nothing in this run, or `null`: no event names it and the
|
|
1727
|
-
* run didn't declare it. A live run may still start it, so that is not
|
|
1728
|
-
* refused.
|
|
1729
|
-
*/
|
|
1730
|
-
const unknownNode = (stored, node, live) => {
|
|
1731
|
-
const named = stored.some(({ event }) => isNodeEvent(event, node));
|
|
1732
|
-
const declared = stored.some(({ event }) => event.type === "run.started" && event.nodes.some((n) => n.id === node));
|
|
1733
|
-
return named || declared || live ? null : `Run has no node ${node}.`;
|
|
1734
|
-
};
|
|
1735
|
-
/**
|
|
1736
|
-
* Prints a run's events; with `follow`, then each new one as it is stored:
|
|
1737
|
-
* pushed over the run's socket, or polled from the store when there is none.
|
|
1738
|
-
* Returns when `run.finished` is stored, the run's process is gone, or `signal`
|
|
1739
|
-
* aborts.
|
|
1740
|
-
*/
|
|
1741
|
-
const followRun = async (store, run, out, options) => {
|
|
1742
|
-
const print = createLogPrinter(options);
|
|
1743
|
-
if (await followEvents(store, run.id, (stored) => {
|
|
1744
|
-
for (const line of print(stored)) out(line);
|
|
1745
|
-
}, {
|
|
1746
|
-
follow: options.follow,
|
|
1747
|
-
...options.after === void 0 ? {} : { after: options.after },
|
|
1748
|
-
...options.intervalMs === void 0 ? {} : { intervalMs: options.intervalMs },
|
|
1749
|
-
...options.home === void 0 ? {} : { home: options.home },
|
|
1750
|
-
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
1751
|
-
}) === "crashed") out(style("red", `run ${run.id} stopped without finishing (its process is gone)`));
|
|
1752
|
-
};
|
|
1753
640
|
// src/run/node-notes.ts
|
|
1754
641
|
/**
|
|
1755
642
|
* @file What a run of one node (`ork run --node`) leaves out, said before its
|
|
@@ -1870,7 +757,8 @@ const runNode = async (options) => {
|
|
|
1870
757
|
if (!servers.ok) return failed([servers.error]);
|
|
1871
758
|
const named = sessionDriver({
|
|
1872
759
|
override: options.driver,
|
|
1873
|
-
named: plan.driver
|
|
760
|
+
named: plan.driver,
|
|
761
|
+
pinned: plan.pinned
|
|
1874
762
|
});
|
|
1875
763
|
const ready = named === void 0 ? chooseReadyDriver([options.nodeId], true, await (options.driverChecks ?? checksHere)()) : null;
|
|
1876
764
|
if (ready?.ok === false) return failed(ready.lines);
|
|
@@ -1923,7 +811,7 @@ const runNode = async (options) => {
|
|
|
1923
811
|
if (event.type === "feedback.recorded" && !options.json) for (const line of formatRunEvent(event, id.length, /* @__PURE__ */ new Map(), false, options.verbose, view)) options.out(line);
|
|
1924
812
|
}
|
|
1925
813
|
};
|
|
1926
|
-
const nodeType = workflow.nodes[id]?.type ?? "
|
|
814
|
+
const nodeType = workflow.nodes[id]?.type ?? "agent.worker";
|
|
1927
815
|
record({
|
|
1928
816
|
type: "run.started",
|
|
1929
817
|
runId,
|
|
@@ -1988,6 +876,59 @@ const runNode = async (options) => {
|
|
|
1988
876
|
};
|
|
1989
877
|
};
|
|
1990
878
|
/** Every value of a repeatable flag: `--input a=1 --input=b=2`. */
|
|
879
|
+
// src/run/show-calls.ts
|
|
880
|
+
/**
|
|
881
|
+
* @file A foreground caller's terminal shows the runs it calls (ADR-081): each
|
|
882
|
+
* `call.started` attaches to the called run under its call node, at any
|
|
883
|
+
* depth, and asks its decisions when a person is at the terminal. Once the
|
|
884
|
+
* caller has ended, following stops for a called run that still runs; one the
|
|
885
|
+
* caller stopped gets a moment to end first.
|
|
886
|
+
*/
|
|
887
|
+
/** How long a called run the caller stopped is followed while it stops. */
|
|
888
|
+
const STOP_GRACE_MS = 5e3;
|
|
889
|
+
const running = (store, id) => {
|
|
890
|
+
const [record] = store.lookup(id);
|
|
891
|
+
return record !== void 0 && liveStatus(record) === "running";
|
|
892
|
+
};
|
|
893
|
+
const showCalls = (options) => {
|
|
894
|
+
const { store, showCalls: show } = options;
|
|
895
|
+
const shown = [];
|
|
896
|
+
return {
|
|
897
|
+
/** Attaches to the run a `call.started` of the caller names. */
|
|
898
|
+
onEvent: (event) => {
|
|
899
|
+
if (event.type !== "call.started" || show === void 0 || options.json) return;
|
|
900
|
+
const stop = new AbortController();
|
|
901
|
+
const done = attachRun(store, event.run, options.out, {
|
|
902
|
+
basePath: [event.node],
|
|
903
|
+
signal: stop.signal,
|
|
904
|
+
...show.ask === void 0 ? { hintNoTerminal: true } : { human: show.ask },
|
|
905
|
+
...options.home === void 0 ? {} : { home: options.home }
|
|
906
|
+
}).catch((error) => {
|
|
907
|
+
options.err(`run ${event.run} could not be shown here: ${error instanceof Error ? error.message : String(error)}`);
|
|
908
|
+
});
|
|
909
|
+
shown.push({
|
|
910
|
+
run: event.run,
|
|
911
|
+
stop,
|
|
912
|
+
done
|
|
913
|
+
});
|
|
914
|
+
},
|
|
915
|
+
/**
|
|
916
|
+
* The caller has ended `status`: stops following the called runs still
|
|
917
|
+
* running, at once, or after a moment when the caller stopped them, and
|
|
918
|
+
* names those that still run.
|
|
919
|
+
*/
|
|
920
|
+
end: async (status) => {
|
|
921
|
+
const stopped = status === "interrupted" || status === "timed_out";
|
|
922
|
+
const timers = shown.filter(({ run }) => running(store, run)).map(({ stop }) => setTimeout(() => {
|
|
923
|
+
stop.abort();
|
|
924
|
+
}, stopped ? STOP_GRACE_MS : 0));
|
|
925
|
+
await Promise.all(shown.map(({ done }) => done));
|
|
926
|
+
for (const timer of timers) clearTimeout(timer);
|
|
927
|
+
const say = styler(createView().term);
|
|
928
|
+
for (const { run } of shown.filter(({ run: id }) => running(store, id))) options.out(say("dim", `↳ run ${run} still running · ork attach ${run}`));
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
};
|
|
1991
932
|
// src/run/calls.ts
|
|
1992
933
|
/**
|
|
1993
934
|
* @file What a run needs to reach other runs: its port for `workflow.call`
|
|
@@ -2005,6 +946,7 @@ const callPorts = (options) => {
|
|
|
2005
946
|
const trigger = options.trigger ?? options.resume?.settings.trigger;
|
|
2006
947
|
const untrusted = options.untrusted ?? options.resume?.settings.untrusted;
|
|
2007
948
|
const machineClaude = options.machineClaude ?? options.resume?.settings.machineClaude ?? false;
|
|
949
|
+
const shown = showCalls(options);
|
|
2008
950
|
const own = graph.workflow.limits?.budgetUsd;
|
|
2009
951
|
const budgetUsd = cap === void 0 ? own : Math.min(cap, own ?? Number.POSITIVE_INFINITY);
|
|
2010
952
|
return {
|
|
@@ -2022,11 +964,15 @@ const callPorts = (options) => {
|
|
|
2022
964
|
store,
|
|
2023
965
|
runId,
|
|
2024
966
|
place: options.place,
|
|
2025
|
-
emit:
|
|
967
|
+
emit: (event) => {
|
|
968
|
+
options.emit(event);
|
|
969
|
+
shown.onEvent(event);
|
|
970
|
+
},
|
|
2026
971
|
home,
|
|
2027
972
|
adopt: adoptedCalls(store, runId),
|
|
2028
973
|
newId: () => newRunId(store),
|
|
2029
974
|
machineClaude,
|
|
975
|
+
detach: options.detach,
|
|
2030
976
|
env: maskedEnv(maskedNamesOfRun(graph.workflow, options.place, store.projects())),
|
|
2031
977
|
resumeSession: (run, node, body, signal) => {
|
|
2032
978
|
const { blocked, leftUsd } = runLimits(store, options.place, home).now();
|
|
@@ -2044,6 +990,7 @@ const callPorts = (options) => {
|
|
|
2044
990
|
}) : Promise.resolve(`${NOT_STARTED}${blocked}.`);
|
|
2045
991
|
}
|
|
2046
992
|
}),
|
|
993
|
+
shown,
|
|
2047
994
|
...linksOff ? {} : { relay: createRelay({
|
|
2048
995
|
store,
|
|
2049
996
|
runId,
|
|
@@ -2126,7 +1073,7 @@ const dirtyAgentMessage = (files) => [
|
|
|
2126
1073
|
"Commit or stash them, or pass --allow-dirty to run with the committed versions."
|
|
2127
1074
|
];
|
|
2128
1075
|
/** Claude nodes that get their own worktree. */
|
|
2129
|
-
const worktreeNodes = (workflow) => Object.entries(workflow.nodes).filter(([, node]) => isClaudeNode(node) && node.type !== "
|
|
1076
|
+
const worktreeNodes = (workflow) => Object.entries(workflow.nodes).filter(([, node]) => isClaudeNode(node) && node.type !== "agent.subagent" && (node.isolation ?? workflow.defaults?.isolation ?? "worktree") === "worktree").map(([id]) => id);
|
|
2130
1077
|
/** Checks the project for a run's worktrees, and resolves `git.base`. */
|
|
2131
1078
|
const prepareCheckout = async (workflow, options, inputs) => {
|
|
2132
1079
|
const isolated = worktreeNodes(workflow);
|
|
@@ -2297,10 +1244,7 @@ const runWorkflowFile = async (options) => {
|
|
|
2297
1244
|
head: options.baseRef ?? options.resume?.settings.baseRef ?? base,
|
|
2298
1245
|
inputs: inputs.inputs,
|
|
2299
1246
|
onEvent,
|
|
2300
|
-
...options.resume
|
|
2301
|
-
adopt: options.resume.worktrees,
|
|
2302
|
-
known: options.resume.branches
|
|
2303
|
-
}
|
|
1247
|
+
...resumedOptions(options.resume)
|
|
2304
1248
|
});
|
|
2305
1249
|
const maskedNames = maskedNamesOfRun(workflow, place, projects);
|
|
2306
1250
|
const executorOptions = {
|
|
@@ -2364,6 +1308,7 @@ const runWorkflowFile = async (options) => {
|
|
|
2364
1308
|
started.resolve(handle);
|
|
2365
1309
|
options.onRun?.(handle);
|
|
2366
1310
|
const result = await handle.done;
|
|
1311
|
+
await ports.shown.end(result.status);
|
|
2367
1312
|
clearOutputFiles(runDir, options.err);
|
|
2368
1313
|
if (result.status === "succeeded") clearStateFiles(runDir, options.err);
|
|
2369
1314
|
await endLinksOf(options.store, runId, result.status, options.home);
|
|
@@ -2392,7 +1337,7 @@ const isDriver = (value) => AGENT_DRIVERS.some((d) => d.id === value);
|
|
|
2392
1337
|
/** Why `--driver` refuses a value: a retired id names its replacement. */
|
|
2393
1338
|
const badDriver = (value) => `--${retiredDriverHint(value) ?? `driver must be one of ${AGENT_DRIVERS.map((d) => d.id).join(", ")}, got \`${value}\``}`;
|
|
2394
1339
|
/** Loads the drivers only when a run starts, so other commands stay fast. */
|
|
2395
|
-
const loadRuntime = async (driver, runs) => (await import("./src-
|
|
1340
|
+
const loadRuntime = async (driver, runs) => (await import("./src-CqRqsb2t.mjs")).createRuntime(driver, sessionEnv(process.env), runs);
|
|
2396
1341
|
/**
|
|
2397
1342
|
* Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
|
|
2398
1343
|
* so that the run ends tidily and leaves no result unscrubbed.
|
|
@@ -2412,7 +1357,7 @@ const onInterrupt = (stop) => {
|
|
|
2412
1357
|
/** The terminal that answers a run's questions; none in a detached run. */
|
|
2413
1358
|
const terminalHuman = () => {
|
|
2414
1359
|
const detached = process.env[DETACHED_ENV] === "1";
|
|
2415
|
-
if (detached) writeErr(style("dim", "detached run: no terminal asks; answer with ork decide"));
|
|
1360
|
+
if (detached) writeErr(style("dim", "detached run: no terminal asks; answer with ork attach or ork decide"));
|
|
2416
1361
|
return detached ? void 0 : createTerminalHuman({
|
|
2417
1362
|
input: process.stdin,
|
|
2418
1363
|
output: process.stderr,
|
|
@@ -2532,7 +1477,7 @@ const run = defineCommand({
|
|
|
2532
1477
|
});
|
|
2533
1478
|
if (detached.ok) {
|
|
2534
1479
|
write(`▸ run ${detached.runId} started in the background`);
|
|
2535
|
-
write(style("dim", `
|
|
1480
|
+
write(style("dim", ` ${detachHint(detached.runId)}`));
|
|
2536
1481
|
} else {
|
|
2537
1482
|
writeErr(detached.error);
|
|
2538
1483
|
writeErr(style("dim", `(output: ${detached.log})`));
|
|
@@ -2572,6 +1517,7 @@ const run = defineCommand({
|
|
|
2572
1517
|
trigger: args.trigger,
|
|
2573
1518
|
untrusted: flagValues(rawArgs, "--untrusted")
|
|
2574
1519
|
},
|
|
1520
|
+
...human === void 0 ? {} : { showCalls: callsShown(human, process.stdin.isTTY) },
|
|
2575
1521
|
onRun: (handle) => {
|
|
2576
1522
|
onInterrupt(handle.interrupt);
|
|
2577
1523
|
}
|
|
@@ -2935,28 +1881,17 @@ const decide = defineCommand({
|
|
|
2935
1881
|
}
|
|
2936
1882
|
const collapses = canKey(process.stdin, process.stdout);
|
|
2937
1883
|
for (const [index, d] of open.entries()) {
|
|
2938
|
-
const
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
let note;
|
|
2947
|
-
question.onNote = (given) => {
|
|
2948
|
-
note = given;
|
|
2949
|
-
};
|
|
2950
|
-
const choice = await human.askAt(question, {
|
|
2951
|
-
run: d.runId,
|
|
2952
|
-
decision: d.id,
|
|
2953
|
-
index: index + 1,
|
|
2954
|
-
total: open.length
|
|
1884
|
+
const asked = await askStored(store, human, d, {
|
|
1885
|
+
by: "decide",
|
|
1886
|
+
place: {
|
|
1887
|
+
run: d.runId,
|
|
1888
|
+
decision: d.id,
|
|
1889
|
+
index: index + 1,
|
|
1890
|
+
total: open.length
|
|
1891
|
+
}
|
|
2955
1892
|
});
|
|
2956
|
-
if (
|
|
2957
|
-
|
|
2958
|
-
if (!answered.ok) write(`${style("yellow", "!")} ${answered.error}\n`);
|
|
2959
|
-
else if (!collapses) write(`${style("green", "✓")} ${d.id}: ${choice}${saidLine(store, d.id)}\n`);
|
|
1893
|
+
if (asked.kind === "refused") write(`${style("yellow", "!")} ${asked.message}\n`);
|
|
1894
|
+
else if (asked.kind === "answered" && !collapses) write(`${style("green", "✓")} ${d.id}: ${asked.choice}${saidLine(store, d.id)}\n`);
|
|
2960
1895
|
}
|
|
2961
1896
|
} finally {
|
|
2962
1897
|
human?.close();
|
|
@@ -3015,7 +1950,7 @@ const stop = defineCommand({
|
|
|
3015
1950
|
write(`${style("green", "✓")} run ${run.id} ${liveStatus(now)}`);
|
|
3016
1951
|
return;
|
|
3017
1952
|
}
|
|
3018
|
-
await setTimeout(100);
|
|
1953
|
+
await setTimeout$1(100);
|
|
3019
1954
|
}
|
|
3020
1955
|
write(`Asked run ${run.id} to stop; it is still finishing its running nodes (see ork logs ${run.id} -f).`);
|
|
3021
1956
|
} finally {
|
|
@@ -3168,7 +2103,7 @@ const formatCheck = (result, term = termOf(process.stdout)) => {
|
|
|
3168
2103
|
* folder. Says nothing when there were none. `bin` is the `cmd` program.
|
|
3169
2104
|
*/
|
|
3170
2105
|
const leaseChecks = async (place, env = process.env, bin = "cmd") => {
|
|
3171
|
-
const { cleanStaleLeases, leaseLedger } = await import("./src-
|
|
2106
|
+
const { cleanStaleLeases, leaseLedger } = await import("./src-CqRqsb2t.mjs");
|
|
3172
2107
|
const { cleaned, failed } = await cleanStaleLeases({
|
|
3173
2108
|
bin,
|
|
3174
2109
|
ledger: leaseLedger(placeFiles(place).runs),
|
|
@@ -3194,10 +2129,10 @@ const leaseChecks = async (place, env = process.env, bin = "cmd") => {
|
|
|
3194
2129
|
* test fails when it is stale.
|
|
3195
2130
|
*/
|
|
3196
2131
|
const ORK_SKILLS = {
|
|
3197
|
-
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:
|
|
2132
|
+
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: claude-code.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: agent.worker\n prompt: \"Review pull request {{inputs.pr}}. The linter passed.\"\n model: claude-code.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",
|
|
3198
2133
|
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",
|
|
3199
2134
|
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",
|
|
3200
|
-
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:
|
|
2135
|
+
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: claude-code.sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: agent.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| `agent.worker` | An agent session that does one task and reports |\n| `agent.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `agent.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 `model` may start with its driver (`claude-code.opus`,\n`open-code.anthropic/claude-sonnet-5-5`); the node then runs on that tool, and\nneeds no `driver`. A file with the old `claude.worker`, `claude.leader` or\n`claude.subagent` types needs `ork migrate`.\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- **An agent 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 an agent 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: agent.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: agent.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`, and `{{item.<field>}}` in a\n agent node's `model` and `effort`. A program also gets `ORK_ITEM` and\n `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"
|
|
3201
2136
|
};
|
|
3202
2137
|
// src/project/init-skills.ts
|
|
3203
2138
|
/**
|
|
@@ -3510,7 +2445,7 @@ const draftWorkflow = (options) => {
|
|
|
3510
2445
|
for (const [index, agent] of agents.entries()) {
|
|
3511
2446
|
const before = agents[index - 1];
|
|
3512
2447
|
nodes[agent] = {
|
|
3513
|
-
type: "
|
|
2448
|
+
type: "agent.worker",
|
|
3514
2449
|
agent,
|
|
3515
2450
|
isolation: before === void 0 ? "worktree" : "continue",
|
|
3516
2451
|
prompt: before === void 0 ? "{{inputs.task}}" : `{{inputs.task}}\n\n${before} reported: {{nodes.${before}.output.summary}}`
|
|
@@ -3526,7 +2461,7 @@ const draftWorkflow = (options) => {
|
|
|
3526
2461
|
recommended: "review"
|
|
3527
2462
|
};
|
|
3528
2463
|
nodes[reviewer] = {
|
|
3529
|
-
type: "
|
|
2464
|
+
type: "agent.worker",
|
|
3530
2465
|
agent: reviewer,
|
|
3531
2466
|
isolation: "readonly",
|
|
3532
2467
|
session: "resume",
|
|
@@ -3860,13 +2795,13 @@ const starterWorkflow = () => ({
|
|
|
3860
2795
|
limits: { budgetUsd: 5 },
|
|
3861
2796
|
nodes: {
|
|
3862
2797
|
lead: {
|
|
3863
|
-
type: "
|
|
2798
|
+
type: "agent.leader",
|
|
3864
2799
|
prompt: "Plan this task and delegate it to your worker: {{inputs.task}}",
|
|
3865
2800
|
model: "opus",
|
|
3866
2801
|
maxWorkers: 2
|
|
3867
2802
|
},
|
|
3868
2803
|
worker: {
|
|
3869
|
-
type: "
|
|
2804
|
+
type: "agent.worker",
|
|
3870
2805
|
parent: "lead",
|
|
3871
2806
|
prompt: "Do the part of the task your owner gives you, then report."
|
|
3872
2807
|
},
|
|
@@ -3959,11 +2894,58 @@ const initRecorded = (store, cwd, options = {}) => {
|
|
|
3959
2894
|
const recorded = recordInRepo(store, cwd);
|
|
3960
2895
|
return recorded.ok ? initProject(cwd, options) : { error: recorded.error };
|
|
3961
2896
|
};
|
|
2897
|
+
// src/project/migrate-format.ts
|
|
2898
|
+
/**
|
|
2899
|
+
* @file The workflow-format part of `ork migrate` (RFC-015): finds the
|
|
2900
|
+
* project's workflow files that name a renamed node type, plans their rewrite
|
|
2901
|
+
* in the YAML itself (comments and layout kept), and writes it. A file that
|
|
2902
|
+
* cannot be rewritten is named and left untouched.
|
|
2903
|
+
*/
|
|
2904
|
+
/** The project's workflow files (`*.ork.yaml` in its workflows folder). */
|
|
2905
|
+
const workflowFiles = (place) => {
|
|
2906
|
+
const dir = placeFiles(place).workflows;
|
|
2907
|
+
try {
|
|
2908
|
+
return readdirSync(dir).filter((name) => name.endsWith(WORKFLOW_SUFFIX)).toSorted().map((name) => path.join(dir, name));
|
|
2909
|
+
} catch {
|
|
2910
|
+
return [];
|
|
2911
|
+
}
|
|
2912
|
+
};
|
|
2913
|
+
/** Which workflow files name a renamed type, and what each becomes. */
|
|
2914
|
+
const planFormat = (place) => {
|
|
2915
|
+
const plan = {
|
|
2916
|
+
changes: [],
|
|
2917
|
+
skipped: []
|
|
2918
|
+
};
|
|
2919
|
+
for (const file of workflowFiles(place)) {
|
|
2920
|
+
const result = renameNodeTypes(readFileSync(file, "utf8"));
|
|
2921
|
+
if (result === null) continue;
|
|
2922
|
+
if (result.ok) plan.changes.push({
|
|
2923
|
+
file,
|
|
2924
|
+
text: result.text,
|
|
2925
|
+
renamed: result.renamed
|
|
2926
|
+
});
|
|
2927
|
+
else plan.skipped.push({
|
|
2928
|
+
file,
|
|
2929
|
+
reason: result.reason
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
return plan;
|
|
2933
|
+
};
|
|
2934
|
+
/** One line per file: what changes, or why it is left as it is. */
|
|
2935
|
+
const formatFormatPlan = (plan, place) => [...plan.changes.map(({ file, renamed }) => {
|
|
2936
|
+
const what = Object.entries(renamed).map(([from, count]) => `${from} ×${String(count)}`).join(", ");
|
|
2937
|
+
return ` rename ${shownPath(file, place.root)}: ${what}`;
|
|
2938
|
+
}), ...plan.skipped.map(({ file, reason }) => ` skip ${shownPath(file, place.root)}: ${reason}; left as it is`)];
|
|
2939
|
+
/** Writes the rewritten files. */
|
|
2940
|
+
const applyFormat = (plan) => {
|
|
2941
|
+
for (const { file, text } of plan.changes) writeFileSync(file, text);
|
|
2942
|
+
};
|
|
3962
2943
|
// src/project/migrate.ts
|
|
3963
2944
|
/**
|
|
3964
2945
|
* @file `ork migrate` (ADR-036): moves an attached project's folder into its
|
|
3965
|
-
* repository's `.ork/`,
|
|
3966
|
-
* skills under `.ork/` and points the tools' folders at them
|
|
2946
|
+
* repository's `.ork/`, with `--agents` brings the project's agents and
|
|
2947
|
+
* skills under `.ork/` and points the tools' folders at them, and rewrites
|
|
2948
|
+
* workflow files that name a renamed node type (RFC-015). It plans first,
|
|
3967
2949
|
* refusing on any clash or a live run, and commits nothing: the changes are
|
|
3968
2950
|
* left for the user to review and commit.
|
|
3969
2951
|
*/
|
|
@@ -4028,8 +3010,7 @@ const fail = (message) => {
|
|
|
4028
3010
|
process.exitCode = 1;
|
|
4029
3011
|
};
|
|
4030
3012
|
/** `ork migrate` in `cwd`: shows the plan, asks, and applies it. */
|
|
4031
|
-
const migrateHere = async (cwd, options) => {
|
|
4032
|
-
const store = openStore();
|
|
3013
|
+
const migrateHere = async (cwd, options, store = openStore()) => {
|
|
4033
3014
|
try {
|
|
4034
3015
|
const plan = planMigration(placeFor(cwd, store.projects()), options);
|
|
4035
3016
|
const problem = refusal(plan, store);
|
|
@@ -4037,14 +3018,18 @@ const migrateHere = async (cwd, options) => {
|
|
|
4037
3018
|
fail(problem);
|
|
4038
3019
|
return;
|
|
4039
3020
|
}
|
|
4040
|
-
|
|
3021
|
+
const format = planFormat(plan.place);
|
|
3022
|
+
const nothing = plan.steps.length === 0 && format.changes.length === 0;
|
|
3023
|
+
write(nothing ? "Nothing to migrate." : "Plan:");
|
|
3024
|
+
for (const line of formatFormatPlan(format, plan.place)) write(line);
|
|
4041
3025
|
for (const line of formatMigration(plan)) write(line);
|
|
4042
|
-
if (
|
|
3026
|
+
if (nothing || options.dryRun) return;
|
|
4043
3027
|
if (!options.yes && !process.stdin.isTTY) {
|
|
4044
3028
|
fail("Pass --yes to migrate without a terminal.");
|
|
4045
3029
|
return;
|
|
4046
3030
|
}
|
|
4047
3031
|
if (!options.yes && !await confirm("Go ahead?")) return;
|
|
3032
|
+
applyFormat(format);
|
|
4048
3033
|
applyMigration(plan, store);
|
|
4049
3034
|
write(`${style("green", "✓")} migrated. ${afterMigration(plan)}`);
|
|
4050
3035
|
} finally {
|
|
@@ -4058,7 +3043,7 @@ const migrateHere = async (cwd, options) => {
|
|
|
4058
3043
|
* place after the clone was moved or renamed, and its runs follow.
|
|
4059
3044
|
*/
|
|
4060
3045
|
/** Runs `use` with the store open, and closes it. */
|
|
4061
|
-
const withStore
|
|
3046
|
+
const withStore = (use) => {
|
|
4062
3047
|
const store = openStore();
|
|
4063
3048
|
try {
|
|
4064
3049
|
return use(store);
|
|
@@ -4078,7 +3063,7 @@ const forget = defineCommand({
|
|
|
4078
3063
|
},
|
|
4079
3064
|
args: { id },
|
|
4080
3065
|
run: ({ args }) => {
|
|
4081
|
-
if (withStore
|
|
3066
|
+
if (withStore((store) => store.forgetProject(args.id))) {
|
|
4082
3067
|
write(`${style("green", "forgot")} ${args.id} (its files are kept)`);
|
|
4083
3068
|
return;
|
|
4084
3069
|
}
|
|
@@ -4100,7 +3085,7 @@ const move = defineCommand({
|
|
|
4100
3085
|
}
|
|
4101
3086
|
},
|
|
4102
3087
|
run: ({ args }) => {
|
|
4103
|
-
const result = withStore
|
|
3088
|
+
const result = withStore((store) => relocateProject(store, args.id, args.path));
|
|
4104
3089
|
if (result.ok) {
|
|
4105
3090
|
write(`${style("green", "moved")} ${args.id} → ${result.project.root}`);
|
|
4106
3091
|
return;
|
|
@@ -4141,7 +3126,7 @@ const register = defineCommand({
|
|
|
4141
3126
|
json: json$2
|
|
4142
3127
|
},
|
|
4143
3128
|
run: ({ args }) => {
|
|
4144
|
-
const result = withStore
|
|
3129
|
+
const result = withStore((store) => registerProject(store, args.path ?? process.cwd(), args.id === void 0 ? {} : { id: args.id }));
|
|
4145
3130
|
if (!result.ok) {
|
|
4146
3131
|
writeErr(result.error);
|
|
4147
3132
|
process.exitCode = 1;
|
|
@@ -4161,7 +3146,7 @@ const projects = defineCommand({
|
|
|
4161
3146
|
},
|
|
4162
3147
|
run: ({ args, rawArgs }) => {
|
|
4163
3148
|
if (rawArgs[0] === "forget" || rawArgs[0] === "move") return;
|
|
4164
|
-
const all = withStore
|
|
3149
|
+
const all = withStore((store) => store.projects());
|
|
4165
3150
|
if (args.json) write(JSON.stringify(all, void 0, 2));
|
|
4166
3151
|
else if (all.length === 0) write("No projects registered yet: ork init (in-repo) or ork register (attached).");
|
|
4167
3152
|
else for (const line of formatProjects(all)) write(line);
|
|
@@ -4170,7 +3155,7 @@ const projects = defineCommand({
|
|
|
4170
3155
|
const migrate = defineCommand({
|
|
4171
3156
|
meta: {
|
|
4172
3157
|
name: "migrate",
|
|
4173
|
-
description: "Move an attached project into its repository's .ork/ (--agents: also its agents and skills); --from-agents drafts a workflow. Commits nothing"
|
|
3158
|
+
description: "Move an attached project into its repository's .ork/ (--agents: also its agents and skills) and rewrites workflows that name renamed node types; --from-agents drafts a workflow. Commits nothing"
|
|
4174
3159
|
},
|
|
4175
3160
|
args: {
|
|
4176
3161
|
agents: {
|
|
@@ -4214,20 +3199,6 @@ const migrate = defineCommand({
|
|
|
4214
3199
|
}, (file) => formatReport(validateFile(file, process.cwd())));
|
|
4215
3200
|
}
|
|
4216
3201
|
});
|
|
4217
|
-
// src/store.ts
|
|
4218
|
-
/**
|
|
4219
|
-
* @file Opening the user's store (`~/.ork/state.db`) for the length of one
|
|
4220
|
-
* command, and closing it after, whatever happens.
|
|
4221
|
-
*/
|
|
4222
|
-
/** Runs `body` with the user's store open, and closes it after. */
|
|
4223
|
-
const withStore = async (body) => {
|
|
4224
|
-
const store = openStore();
|
|
4225
|
-
try {
|
|
4226
|
-
await body(store);
|
|
4227
|
-
} finally {
|
|
4228
|
-
store.close();
|
|
4229
|
-
}
|
|
4230
|
-
};
|
|
4231
3202
|
// src/runs/feedback.ts
|
|
4232
3203
|
/**
|
|
4233
3204
|
* @file `ork feedback`'s helpers (specs/feedback.md): finding an item by a
|
|
@@ -4558,7 +3529,7 @@ const triage = defineCommand({
|
|
|
4558
3529
|
description: "Make every merge without asking",
|
|
4559
3530
|
default: false
|
|
4560
3531
|
} },
|
|
4561
|
-
run: ({ args }) => withStore(async (store) => {
|
|
3532
|
+
run: ({ args }) => withStore$1(async (store) => {
|
|
4562
3533
|
if (!args.yes && !process.stdin.isTTY) {
|
|
4563
3534
|
writeErr("Pass --yes to triage without a terminal.");
|
|
4564
3535
|
process.exitCode = 1;
|
|
@@ -4577,7 +3548,7 @@ const split = defineCommand({
|
|
|
4577
3548
|
description: "Take apart the items triage merged into one"
|
|
4578
3549
|
},
|
|
4579
3550
|
args: { item: itemArg$1 },
|
|
4580
|
-
run: ({ args }) => withStore((store) => {
|
|
3551
|
+
run: ({ args }) => withStore$1((store) => {
|
|
4581
3552
|
const found = findFeedbackItem(store, args.item);
|
|
4582
3553
|
if (!found.ok) {
|
|
4583
3554
|
writeErr(found.error);
|
|
@@ -4719,7 +3690,7 @@ const publish = defineCommand({
|
|
|
4719
3690
|
default: false
|
|
4720
3691
|
}
|
|
4721
3692
|
},
|
|
4722
|
-
run: ({ args }) => withStore(async (store) => {
|
|
3693
|
+
run: ({ args }) => withStore$1(async (store) => {
|
|
4723
3694
|
if (!process.stdin.isTTY) {
|
|
4724
3695
|
failed("publish shows what it will post and asks you first: run it in a terminal.");
|
|
4725
3696
|
return;
|
|
@@ -4775,7 +3746,7 @@ const exportFeedback = defineCommand({
|
|
|
4775
3746
|
description: "A file to write (default: the terminal)"
|
|
4776
3747
|
}
|
|
4777
3748
|
},
|
|
4778
|
-
run: ({ args }) => withStore((store) => {
|
|
3749
|
+
run: ({ args }) => withStore$1((store) => {
|
|
4779
3750
|
const records = store.exportFeedback({ all: args.all });
|
|
4780
3751
|
const text = exportLines(records);
|
|
4781
3752
|
if (args.output === void 0) {
|
|
@@ -4796,7 +3767,7 @@ const importFeedback = defineCommand({
|
|
|
4796
3767
|
description: "What `ork feedback export` wrote",
|
|
4797
3768
|
required: true
|
|
4798
3769
|
} },
|
|
4799
|
-
run: ({ args }) => withStore((store) => {
|
|
3770
|
+
run: ({ args }) => withStore$1((store) => {
|
|
4800
3771
|
let text;
|
|
4801
3772
|
try {
|
|
4802
3773
|
text = readFileSync(args.file, "utf8");
|
|
@@ -4821,7 +3792,7 @@ const importFeedback = defineCommand({
|
|
|
4821
3792
|
* `feedback-share.ts`.
|
|
4822
3793
|
*/
|
|
4823
3794
|
/** Runs `body` on the item `prefix` names, or says why there's none. */
|
|
4824
|
-
const withItem = (prefix, body) => withStore((store) => {
|
|
3795
|
+
const withItem = (prefix, body) => withStore$1((store) => {
|
|
4825
3796
|
const found = findFeedbackItem(store, prefix);
|
|
4826
3797
|
if (!found.ok) {
|
|
4827
3798
|
writeErr(found.error);
|
|
@@ -4859,7 +3830,7 @@ const feedback = defineCommand({
|
|
|
4859
3830
|
},
|
|
4860
3831
|
json: json$1
|
|
4861
3832
|
},
|
|
4862
|
-
run: ({ args }) => withStore((store) => {
|
|
3833
|
+
run: ({ args }) => withStore$1((store) => {
|
|
4863
3834
|
const items = store.feedbackItems({ all: args.all });
|
|
4864
3835
|
if (args.json) write(JSON.stringify(items, void 0, 2));
|
|
4865
3836
|
else for (const line of formatFeedbackItems(items)) write(line);
|
|
@@ -4921,7 +3892,7 @@ const feedback = defineCommand({
|
|
|
4921
3892
|
description: "good or bad"
|
|
4922
3893
|
}
|
|
4923
3894
|
},
|
|
4924
|
-
run: ({ args }) => withStore((store) => {
|
|
3895
|
+
run: ({ args }) => withStore$1((store) => {
|
|
4925
3896
|
const noted = noteRun(store, process.cwd(), {
|
|
4926
3897
|
...args,
|
|
4927
3898
|
text: args.message
|
|
@@ -5028,7 +3999,7 @@ const ps = defineCommand({
|
|
|
5028
3999
|
default: false
|
|
5029
4000
|
}
|
|
5030
4001
|
},
|
|
5031
|
-
run: ({ args }) => withStore((store) => {
|
|
4002
|
+
run: ({ args }) => withStore$1((store) => {
|
|
5032
4003
|
const limit = Number(args.limit ?? 20);
|
|
5033
4004
|
const runs = store.runs({
|
|
5034
4005
|
limit: Number.isInteger(limit) && limit > 0 ? limit : 20,
|
|
@@ -5072,7 +4043,7 @@ const logs = defineCommand({
|
|
|
5072
4043
|
default: false
|
|
5073
4044
|
}
|
|
5074
4045
|
},
|
|
5075
|
-
run: ({ args }) => withStore(async (store) => {
|
|
4046
|
+
run: ({ args }) => withStore$1(async (store) => {
|
|
5076
4047
|
const found = findRun(store, args.run, process.cwd());
|
|
5077
4048
|
if (!found.ok) {
|
|
5078
4049
|
writeErr(found.error);
|
|
@@ -5111,7 +4082,7 @@ const tree = defineCommand({
|
|
|
5111
4082
|
default: false
|
|
5112
4083
|
}
|
|
5113
4084
|
},
|
|
5114
|
-
run: ({ args }) => withStore((store) => {
|
|
4085
|
+
run: ({ args }) => withStore$1((store) => {
|
|
5115
4086
|
const found = findRun(store, args.run, process.cwd());
|
|
5116
4087
|
if (!found.ok) {
|
|
5117
4088
|
writeErr(found.error);
|
|
@@ -5449,28 +4420,30 @@ const main = defineCommand({
|
|
|
5449
4420
|
run,
|
|
5450
4421
|
ps,
|
|
5451
4422
|
logs,
|
|
4423
|
+
attach: () => import("./attach-E6NNyA3T.mjs").then((module) => module.attach),
|
|
5452
4424
|
tree,
|
|
5453
|
-
usage: () => import("./usage-
|
|
5454
|
-
prune: () => import("./usage-
|
|
4425
|
+
usage: () => import("./usage-6WIowblz.mjs").then((module) => module.usage),
|
|
4426
|
+
prune: () => import("./usage-6WIowblz.mjs").then((module) => module.prune),
|
|
5455
4427
|
decide,
|
|
5456
|
-
relay: () => import("./relay-
|
|
5457
|
-
pause: () => import("./pause-
|
|
5458
|
-
continue: () => import("./pause-
|
|
4428
|
+
relay: () => import("./relay-C8W0t_zc.mjs").then((module) => module.relay),
|
|
4429
|
+
pause: () => import("./pause-BPvlDIvz.mjs").then((module) => module.pause),
|
|
4430
|
+
continue: () => import("./pause-BPvlDIvz.mjs").then((module) => module.continueRun),
|
|
5459
4431
|
stop,
|
|
5460
4432
|
chat,
|
|
5461
|
-
|
|
4433
|
+
once: () => import("./once-DPQu5Q3H.mjs").then((module) => module.once),
|
|
4434
|
+
send: () => import("./send-CaZlGG5a.mjs").then((module) => module.send),
|
|
5462
4435
|
resume,
|
|
5463
4436
|
feedback,
|
|
5464
4437
|
context,
|
|
5465
|
-
worktrees: () => import("./worktrees-
|
|
5466
|
-
wt: () => import("./worktrees-
|
|
5467
|
-
watch: () => import("./watch-
|
|
5468
|
-
mcp: () => import("./mcp-
|
|
5469
|
-
ui: () => import("./ui-
|
|
4438
|
+
worktrees: () => import("./worktrees-ZCII298F.mjs").then((module) => module.worktrees),
|
|
4439
|
+
wt: () => import("./worktrees-ZCII298F.mjs").then((module) => module.worktrees),
|
|
4440
|
+
watch: () => import("./watch-uOU_WeBI.mjs").then((module) => module.watch),
|
|
4441
|
+
mcp: () => import("./mcp-bcctR-wc.mjs").then((module) => module.mcp),
|
|
4442
|
+
ui: () => import("./ui-D2Ydofq0.mjs").then((module) => module.ui),
|
|
5470
4443
|
doctor,
|
|
5471
4444
|
upgrade,
|
|
5472
|
-
completion: () => import("./completion-
|
|
5473
|
-
__complete: () => import("./completion-
|
|
4445
|
+
completion: () => import("./completion-SNNNxnv9.mjs").then((module) => module.completion),
|
|
4446
|
+
__complete: () => import("./completion-SNNNxnv9.mjs").then((module) => module.completeCommand(main))
|
|
5474
4447
|
}
|
|
5475
4448
|
});
|
|
5476
4449
|
// src/bin.ts
|
|
@@ -5484,12 +4457,13 @@ const main = defineCommand({
|
|
|
5484
4457
|
* flag is taken out of the arguments and set as `ORK_PLAIN=1`. `--help`
|
|
5485
4458
|
* leaves out the flags only Ork passes to itself. `ork __complete`, which tab
|
|
5486
4459
|
* completion runs on every Tab, goes first: no update check, and no citty, so
|
|
5487
|
-
* a typed `--help` is a word to complete.
|
|
4460
|
+
* a typed `--help` is a word to complete. `ork once -- <command>` skips
|
|
4461
|
+
* citty's help scan, so a `--help` or `-h` of the command is the command's.
|
|
5488
4462
|
*/
|
|
5489
4463
|
clearGitLocalEnv();
|
|
5490
4464
|
const argv = takePlainFlag(process.argv.slice(2));
|
|
5491
4465
|
if (argv[0] === "__complete") {
|
|
5492
|
-
const { printCompletions } = await import("./completion-
|
|
4466
|
+
const { printCompletions } = await import("./completion-SNNNxnv9.mjs");
|
|
5493
4467
|
await printCompletions(main, argv.slice(1));
|
|
5494
4468
|
} else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
|
|
5495
4469
|
else {
|
|
@@ -5499,7 +4473,16 @@ else {
|
|
|
5499
4473
|
showUpdateNotice();
|
|
5500
4474
|
});
|
|
5501
4475
|
}
|
|
5502
|
-
|
|
4476
|
+
const dash = argv.indexOf("--");
|
|
4477
|
+
const askedHelp = argv.slice(1, dash === -1 ? 0 : dash).some((arg) => arg === "--help" || arg === "-h");
|
|
4478
|
+
if (argv[0] === "once" && dash !== -1 && !askedHelp) try {
|
|
4479
|
+
const { once } = await import("./once-DPQu5Q3H.mjs");
|
|
4480
|
+
await runCommand(once, { rawArgs: argv.slice(1) });
|
|
4481
|
+
} catch (error) {
|
|
4482
|
+
console.error(error);
|
|
4483
|
+
process.exitCode = 1;
|
|
4484
|
+
}
|
|
4485
|
+
else await runMain(main, {
|
|
5503
4486
|
rawArgs: argv,
|
|
5504
4487
|
showUsage: showHelp
|
|
5505
4488
|
});
|