@krapht/ork 1.0.0-alpha.10 → 1.0.0-alpha.12
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__-DFF18K8y.mjs → __build_info__-BJbE3Zvp.mjs} +4 -72
- package/dist/attach-BmV5ckb9.mjs +89 -0
- package/dist/bin.mjs +467 -864
- package/dist/{decide-3l9q8UtX.mjs → builtin-files-w0YULJ4c.mjs} +2 -50
- package/dist/chips-CyeB_rhR.mjs +43 -0
- package/dist/{clean-DVaEH9e7.mjs → clean-D8GI_2K-.mjs} +1 -1
- package/dist/{completion-Dv5kf598.mjs → completion-CzElB8nS.mjs} +24 -16
- package/dist/{context-CEaKOVmG.mjs → context-BAwgVSX5.mjs} +43 -31
- package/dist/decide-DlbyctOE.mjs +50 -0
- package/dist/{find-DEjjOlTG.mjs → find-p05wlbmu.mjs} +1 -1
- package/dist/format-Dni98cbh.mjs +206 -0
- package/dist/{help-groups-BwYoAWsh.mjs → help-groups-DYP_zMRP.mjs} +4 -0
- package/dist/{in-a-run-BS9Nc-JW.mjs → in-a-run-BI-hLIl_.mjs} +4 -2
- package/dist/{limits-BSwEjQST.mjs → limits-CElmhowV.mjs} +1 -1
- package/dist/{list-Qxr6rwsY.mjs → list-zW2xKuGr.mjs} +1 -1
- package/dist/{mcp-CslnG5wf.mjs → mcp-CM3b65AZ.mjs} +6 -6
- package/dist/output-CnRzrbUM.mjs +423 -0
- package/dist/{pause-D7OVB5Ac.mjs → pause-BuS83WAI.mjs} +4 -4
- package/dist/{relay-EceRIebO.mjs → relay-DAsJA8wF.mjs} +2 -2
- package/dist/{remove-0IQZVWmI.mjs → remove-Dh7D_hVn.mjs} +3 -3
- package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
- package/dist/{send-C0uwfUYi.mjs → send-MFEQZH31.mjs} +3 -3
- package/dist/{src-cmmXKHn1.mjs → src-BfxkJOYm.mjs} +234 -150
- package/dist/{src-Df6KOIDC.mjs → src-CdDTQLdQ.mjs} +2081 -1035
- package/dist/{src-oaJYIddp.mjs → src-D1QedD5y.mjs} +523 -81
- package/dist/{src-i5k2Y-kf.mjs → src-S9yK2OHU.mjs} +79 -11
- package/dist/store-B2OaE4ID.mjs +1356 -0
- package/dist/ui/assets/{app-qs8Lofv6.js → app-CAoZtku5.js} +16 -16
- package/dist/ui/assets/app-QoHVi-mb.css +1 -0
- package/dist/ui/assets/{index-DV1ZaOk-.js → index-BL7Ad0OA.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-CBkHJdqk.mjs → ui-C4Ys26oO.mjs} +4 -4
- package/dist/{usage-Cd8pEYyK.mjs → usage-C2zx-X6q.mjs} +43 -32
- package/dist/verdict-CIy8tAyX.mjs +84 -0
- package/dist/{watch-DW5R2OPO.mjs → watch-DzCeqM05.mjs} +28 -16
- package/dist/{workflows-BT1gDRIV.mjs → workflows-Rr5w90A0.mjs} +3 -3
- package/dist/{worktrees-g1MQMGie.mjs → worktrees-f6SAbum5.mjs} +11 -10
- package/package.json +1 -1
- package/dist/format-Cb5_m-Qd.mjs +0 -191
- package/dist/output-Bqljtmp_.mjs +0 -40
- package/dist/ui/assets/app-C8uhHVYp.css +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -1,242 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
3
|
-
import {
|
|
4
|
-
import { i as
|
|
5
|
-
import { n as
|
|
6
|
-
import { n as
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { a as
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { a as
|
|
14
|
-
import { i as
|
|
15
|
-
import { i as
|
|
2
|
+
import { $ as DETACHED_ENV, $t as endOf, An as linkTools, Ar as AGENT_DRIVERS, At as nestItems, Bn as placeFiles, C as openStore, Cn as wakeRun, Ct as runStatus, Dn as planAgentMigration, Dr as readyDriver, E as inspectProject, Er as driverChecks, Et as templateOf, Fn as catalogAgent, Gt as PROJECT_FILE, H as tellNode, Hn as projectsHome, I as createCalls, In as loadAgents, J as tellLiveRun, Jt as machineRulesOf, Kn as inside, Kt as gitRulesOf, L as addressOf, Ln as hasInRepoFiles, M as parseInputPairs, Mn as toolsInUse, N as resolveInputs, Nn as createAgent, On as applySteps, Or as AGENT_TOOL_LAYOUTS, Pn as deleteAgent, Q as listKey, Qt as createSignals, R as endLink, Rn as inRepoPlace, Tn as maskReplacer, Un as runDirOf, V as peerOf, Vn as placeFor, W as flattenTree, Wn as shownPath, X as detachResume, Y as adoptedCalls, Z as resumable, _t as startRun, a as releaseNotes, an as FEEDBACK_SEVERITIES, bn as serveRun, br as retiredDriverHint, cn as planPost, cr as serializeWorkflow, dr as WorkflowSchema, dt as sessionDriver, et as detachRun, fn as createFeedback, gr as isClaudeNode, gt as walkSummary, i as planUpgrade, in as FEEDBACK_KINDS, ir as beforeOf, j as clearGitLocalEnv, jn as toolLinks, kn as planMoveInto, kr as ORK_LAYOUT, lt as planNodeSession, mn as normalizeTitle, mr as settingsOf, n as resolveBase, nt as NOT_STARTED, o as runningRunIds, on as FEEDBACK_TARGETS, q as requestStop, qn as projectRootOf, qt as loadProject, s as runningScript, t as createWorkspace, u as isNewer, un as sendPost, wt as runTree, x as activeTree, xn as socketPath, xt as liveStatus, yt as userMessageText, z as endLinksOf, zn as newProjectId } from "./src-CdDTQLdQ.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-B2OaE4ID.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__-BJbE3Zvp.mjs";
|
|
5
|
+
import { n as builtinWorkflowFile, t as builtinProblem } from "./builtin-files-w0YULJ4c.mjs";
|
|
6
|
+
import { n as pendingDecisions, t as answerDecision } from "./decide-DlbyctOE.mjs";
|
|
7
|
+
import { n as newRunId, t as findRun } from "./find-p05wlbmu.mjs";
|
|
8
|
+
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-zW2xKuGr.mjs";
|
|
9
|
+
import { a as planClean, t as applyClean } from "./clean-D8GI_2K-.mjs";
|
|
10
|
+
import { c as subcommandsOf, i as argsOf, n as groupedEntries, o as publicArgs, s as resolved, t as groupOf } from "./help-groups-DYP_zMRP.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-BfxkJOYm.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-Rr5w90A0.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-BAwgVSX5.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-CElmhowV.mjs";
|
|
18
|
+
import { a as decideRefusal, r as chatRefusal } from "./in-a-run-BI-hLIl_.mjs";
|
|
19
|
+
import { i as listedStatus, n as formatRuns, r as formatTree, t as ago } from "./format-Dni98cbh.mjs";
|
|
16
20
|
import path from "node:path";
|
|
17
21
|
import { stringify } from "yaml";
|
|
18
22
|
import { z } from "zod";
|
|
19
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
20
|
-
import { createHash } from "node:crypto";
|
|
23
|
+
import { appendFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
24
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
21
25
|
import { homedir } from "node:os";
|
|
22
|
-
import { setTimeout } from "node:timers/promises";
|
|
26
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
23
27
|
import { defineCommand, renderUsage, runMain } from "citty";
|
|
24
|
-
import { createInterface } from "node:readline/promises";
|
|
25
|
-
// ../core/src/control/queue.ts
|
|
26
|
-
const PERMISSION_OPTIONS = [{
|
|
27
|
-
id: "allow",
|
|
28
|
-
label: "Allow"
|
|
29
|
-
}, {
|
|
30
|
-
id: "deny",
|
|
31
|
-
label: "Deny"
|
|
32
|
-
}];
|
|
33
|
-
const DENIED = "The user did not allow this tool.";
|
|
34
|
-
/** The deny message a session gets, with the person's words when given. */
|
|
35
|
-
const deniedMessage = (note) => note === void 0 ? DENIED : `${DENIED} They said: ${note}`;
|
|
36
|
-
/** The input fields that say what a tool will do, the first one present. */
|
|
37
|
-
const TOOL_INPUT_KEYS = [
|
|
38
|
-
"command",
|
|
39
|
-
"file_path",
|
|
40
|
-
"path",
|
|
41
|
-
"url",
|
|
42
|
-
"pattern"
|
|
43
|
-
];
|
|
44
|
-
/** A tool's input for the decision's context: as written, cut at `max`. */
|
|
45
|
-
const inputContext = (input, max = 4e3) => {
|
|
46
|
-
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
47
|
-
const text = key === void 0 ? JSON.stringify(input, void 0, 2) : String(input[key]);
|
|
48
|
-
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
49
|
-
};
|
|
50
|
-
const createDecisionQueue = (options) => {
|
|
51
|
-
const { store, runId, emit } = options;
|
|
52
|
-
const earlier = store.decisions({ runId });
|
|
53
|
-
let count = Math.max(0, ...earlier.map(({ id }) => Number(id.slice(runId.length + 1)) || 0));
|
|
54
|
-
for (const { id, node, status } of earlier) {
|
|
55
|
-
if (status !== "open") continue;
|
|
56
|
-
store.withdrawDecision(id);
|
|
57
|
-
emit({
|
|
58
|
-
type: "decision.withdrawn",
|
|
59
|
-
node,
|
|
60
|
-
decision: id
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
/** Opens a decision and settles it with the first answer. */
|
|
64
|
-
const decide = (open, local, withdrawn) => {
|
|
65
|
-
count += 1;
|
|
66
|
-
const id = `${runId}-${String(count)}`;
|
|
67
|
-
const record = store.openDecision({
|
|
68
|
-
...open,
|
|
69
|
-
id,
|
|
70
|
-
runId
|
|
71
|
-
});
|
|
72
|
-
const event = {
|
|
73
|
-
type: "decision.opened",
|
|
74
|
-
node: record.node,
|
|
75
|
-
decision: id,
|
|
76
|
-
kind: record.kind,
|
|
77
|
-
question: record.question,
|
|
78
|
-
options: record.options
|
|
79
|
-
};
|
|
80
|
-
if (record.recommended !== void 0) event.recommended = record.recommended;
|
|
81
|
-
if (record.owner !== void 0) event.owner = record.owner;
|
|
82
|
-
emit(event);
|
|
83
|
-
const { promise, resolve, reject } = Promise.withResolvers();
|
|
84
|
-
const settled = new AbortController();
|
|
85
|
-
/** `note` is the stored one: masked, and absent when blank. */
|
|
86
|
-
const finish = (answer, by, note) => {
|
|
87
|
-
if (settled.signal.aborted) return;
|
|
88
|
-
settled.abort();
|
|
89
|
-
const answered = {
|
|
90
|
-
type: "decision.answered",
|
|
91
|
-
node: open.node,
|
|
92
|
-
decision: id,
|
|
93
|
-
answer,
|
|
94
|
-
by
|
|
95
|
-
};
|
|
96
|
-
if (note !== void 0) answered.note = note;
|
|
97
|
-
emit(answered);
|
|
98
|
-
resolve(note === void 0 ? {
|
|
99
|
-
answer,
|
|
100
|
-
by
|
|
101
|
-
} : {
|
|
102
|
-
answer,
|
|
103
|
-
by,
|
|
104
|
-
note
|
|
105
|
-
});
|
|
106
|
-
};
|
|
107
|
-
const withdraw = () => {
|
|
108
|
-
if (settled.signal.aborted) return;
|
|
109
|
-
settled.abort();
|
|
110
|
-
store.withdrawDecision(id);
|
|
111
|
-
emit({
|
|
112
|
-
type: "decision.withdrawn",
|
|
113
|
-
node: open.node,
|
|
114
|
-
decision: id
|
|
115
|
-
});
|
|
116
|
-
reject(/* @__PURE__ */ new Error("the question was withdrawn"));
|
|
117
|
-
};
|
|
118
|
-
if (withdrawn?.aborted === true) withdraw();
|
|
119
|
-
withdrawn?.addEventListener("abort", withdraw, { once: true });
|
|
120
|
-
const fromStore = async () => {
|
|
121
|
-
try {
|
|
122
|
-
const closed = await options.waitFor(id, settled.signal);
|
|
123
|
-
if (closed.status === "answered" && closed.answer !== void 0) finish(closed.answer, closed.answeredBy ?? "decide", closed.note);
|
|
124
|
-
} catch {}
|
|
125
|
-
};
|
|
126
|
-
const fromTerminal = async (ask) => {
|
|
127
|
-
try {
|
|
128
|
-
const { answer, note } = await ask(settled.signal);
|
|
129
|
-
if (store.answerDecision(id, answer, "terminal", note)) finish(answer, "terminal", store.decision(id)?.note);
|
|
130
|
-
} catch {}
|
|
131
|
-
};
|
|
132
|
-
fromStore();
|
|
133
|
-
if (local !== void 0) fromTerminal(local);
|
|
134
|
-
return promise;
|
|
135
|
-
};
|
|
136
|
-
return {
|
|
137
|
-
ask: (question) => {
|
|
138
|
-
const open = {
|
|
139
|
-
node: question.nodeId,
|
|
140
|
-
kind: "question",
|
|
141
|
-
question: question.question,
|
|
142
|
-
options: question.options
|
|
143
|
-
};
|
|
144
|
-
if (question.recommended !== void 0) open.recommended = question.recommended;
|
|
145
|
-
if (question.context !== void 0) open.context = question.context;
|
|
146
|
-
if (question.document !== void 0) open.document = question.document;
|
|
147
|
-
if (question.owner !== void 0) open.owner = question.owner;
|
|
148
|
-
const { local } = options;
|
|
149
|
-
return decide(open, local === void 0 ? void 0 : (signal) => {
|
|
150
|
-
let note;
|
|
151
|
-
return local.ask({
|
|
152
|
-
...question,
|
|
153
|
-
question: maskSecrets(question.question),
|
|
154
|
-
...question.context === void 0 ? {} : { context: maskSecrets(question.context) },
|
|
155
|
-
...question.document === void 0 ? {} : { document: maskSecrets(question.document) },
|
|
156
|
-
signal,
|
|
157
|
-
onNote: (given) => {
|
|
158
|
-
note = given;
|
|
159
|
-
}
|
|
160
|
-
}).then((answer) => note === void 0 ? { answer } : {
|
|
161
|
-
answer,
|
|
162
|
-
note
|
|
163
|
-
});
|
|
164
|
-
}, question.signal).then(({ answer, note }) => {
|
|
165
|
-
if (note !== void 0) question.onNote?.(note);
|
|
166
|
-
return answer;
|
|
167
|
-
});
|
|
168
|
-
},
|
|
169
|
-
settled: (question) => {
|
|
170
|
-
count += 1;
|
|
171
|
-
const id = `${runId}-${String(count)}`;
|
|
172
|
-
const { owner } = question;
|
|
173
|
-
const record = store.openDecision({
|
|
174
|
-
id,
|
|
175
|
-
runId,
|
|
176
|
-
node: question.nodeId,
|
|
177
|
-
kind: "question",
|
|
178
|
-
question: question.question,
|
|
179
|
-
options: question.options,
|
|
180
|
-
owner,
|
|
181
|
-
...question.recommended === void 0 ? {} : { recommended: question.recommended },
|
|
182
|
-
...question.context === void 0 ? {} : { context: question.context }
|
|
183
|
-
});
|
|
184
|
-
emit({
|
|
185
|
-
type: "decision.opened",
|
|
186
|
-
node: record.node,
|
|
187
|
-
decision: id,
|
|
188
|
-
kind: "question",
|
|
189
|
-
question: record.question,
|
|
190
|
-
options: record.options,
|
|
191
|
-
owner,
|
|
192
|
-
...record.recommended === void 0 ? {} : { recommended: record.recommended }
|
|
193
|
-
});
|
|
194
|
-
store.answerDecision(id, question.answer, "owner", question.note);
|
|
195
|
-
const note = store.decision(id)?.note;
|
|
196
|
-
emit({
|
|
197
|
-
type: "decision.answered",
|
|
198
|
-
node: record.node,
|
|
199
|
-
decision: id,
|
|
200
|
-
answer: question.answer,
|
|
201
|
-
by: "owner",
|
|
202
|
-
owner,
|
|
203
|
-
...note === void 0 ? {} : { note }
|
|
204
|
-
});
|
|
205
|
-
},
|
|
206
|
-
permit: async (request) => {
|
|
207
|
-
const { local } = options;
|
|
208
|
-
let localMessage = DENIED;
|
|
209
|
-
const { answer, note } = await decide({
|
|
210
|
-
node: request.nodeId,
|
|
211
|
-
kind: "permission",
|
|
212
|
-
question: `Allow ${request.toolName}?`,
|
|
213
|
-
options: PERMISSION_OPTIONS,
|
|
214
|
-
context: inputContext(request.input)
|
|
215
|
-
}, local === void 0 ? void 0 : (signal) => {
|
|
216
|
-
let said;
|
|
217
|
-
return local.permit({
|
|
218
|
-
...request,
|
|
219
|
-
signal,
|
|
220
|
-
onNote: (given) => {
|
|
221
|
-
said = given;
|
|
222
|
-
}
|
|
223
|
-
}).then((result) => {
|
|
224
|
-
if (!result.allow) localMessage = result.message;
|
|
225
|
-
const choice = result.allow ? "allow" : "deny";
|
|
226
|
-
return said === void 0 ? { answer: choice } : {
|
|
227
|
-
answer: choice,
|
|
228
|
-
note: said
|
|
229
|
-
};
|
|
230
|
-
});
|
|
231
|
-
}, request.signal);
|
|
232
|
-
if (note !== void 0) request.onNote?.(note);
|
|
233
|
-
return answer === "allow" ? { allow: true } : {
|
|
234
|
-
allow: false,
|
|
235
|
-
message: note === void 0 ? localMessage : deniedMessage(note)
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
};
|
|
240
28
|
// ../core/src/control/control.ts
|
|
241
29
|
const startControl = async (options) => {
|
|
242
30
|
const { store, runId } = options;
|
|
@@ -306,165 +94,6 @@ const startControl = async (options) => {
|
|
|
306
94
|
}
|
|
307
95
|
};
|
|
308
96
|
};
|
|
309
|
-
// ../core/src/feedback/signals.ts
|
|
310
|
-
const ORK_TOOL = "mcp__ork__";
|
|
311
|
-
/**
|
|
312
|
-
* An observer for one run's events: give it each event, and record what it
|
|
313
|
-
* returns, in order (the event, its origin filled in when it is feedback, then
|
|
314
|
-
* any feedback it signals).
|
|
315
|
-
*/
|
|
316
|
-
const createSignals = (options = {}) => {
|
|
317
|
-
let workflow;
|
|
318
|
-
const nodeTypes = /* @__PURE__ */ new Map();
|
|
319
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
320
|
-
const tools = /* @__PURE__ */ new Map();
|
|
321
|
-
const asked = /* @__PURE__ */ new Map();
|
|
322
|
-
const seen = /* @__PURE__ */ new Set();
|
|
323
|
-
const originOf = (node) => {
|
|
324
|
-
const origin = {};
|
|
325
|
-
if (workflow !== void 0) origin.workflow = workflow;
|
|
326
|
-
if (options.workflowHash !== void 0) origin.workflowHash = options.workflowHash;
|
|
327
|
-
const type = node === void 0 ? void 0 : nodeTypes.get(node);
|
|
328
|
-
if (type !== void 0) origin.nodeType = type;
|
|
329
|
-
const session = node === void 0 ? void 0 : sessions.get(node);
|
|
330
|
-
if (session !== void 0) origin.model = session.model;
|
|
331
|
-
if (session?.driver !== void 0) origin.driver = session.driver;
|
|
332
|
-
if (options.version !== void 0) origin.version = options.version;
|
|
333
|
-
return origin;
|
|
334
|
-
};
|
|
335
|
-
const record = (node, signal) => {
|
|
336
|
-
const print = fingerprint(signal);
|
|
337
|
-
if (seen.has(print)) return [];
|
|
338
|
-
seen.add(print);
|
|
339
|
-
return [{
|
|
340
|
-
type: "feedback.recorded",
|
|
341
|
-
...node === void 0 ? {} : { node },
|
|
342
|
-
feedback: {
|
|
343
|
-
source: "system",
|
|
344
|
-
kind: signal.kind,
|
|
345
|
-
target: signal.target,
|
|
346
|
-
severity: signal.severity,
|
|
347
|
-
title: signal.title,
|
|
348
|
-
body: signal.body,
|
|
349
|
-
evidence: signal.evidence ?? [],
|
|
350
|
-
code: signal.code,
|
|
351
|
-
fingerprint: print,
|
|
352
|
-
origin: originOf(node)
|
|
353
|
-
}
|
|
354
|
-
}];
|
|
355
|
-
};
|
|
356
|
-
const answered = (question, answer) => {
|
|
357
|
-
if (question.kind === "permission") {
|
|
358
|
-
if (answer !== "deny") return null;
|
|
359
|
-
const tool = question.question.replace(/^Allow /u, "").replace(/\?$/u, "");
|
|
360
|
-
return [question.node, {
|
|
361
|
-
code: "permission.denied",
|
|
362
|
-
kind: "friction",
|
|
363
|
-
target: "workflow",
|
|
364
|
-
severity: "low",
|
|
365
|
-
title: `${question.node} was denied ${tool}`,
|
|
366
|
-
body: "Allow it in the node's `tools.allow` if it needs it, or tell it in its prompt not to use it.",
|
|
367
|
-
evidence: [tool]
|
|
368
|
-
}];
|
|
369
|
-
}
|
|
370
|
-
if (question.recommended === void 0 || answer === question.recommended) return null;
|
|
371
|
-
return [question.node, {
|
|
372
|
-
code: "recommendation.overridden",
|
|
373
|
-
kind: "context-gap",
|
|
374
|
-
target: "workflow",
|
|
375
|
-
severity: "low",
|
|
376
|
-
title: `The answer to ${question.node}'s question was not the one it recommended`,
|
|
377
|
-
body: `Asked: ${question.question} Recommended: ${question.recommended}. Answered: ${answer}.`
|
|
378
|
-
}];
|
|
379
|
-
};
|
|
380
|
-
/** The feedback an event signals, if any, and the node it is about. */
|
|
381
|
-
const signalOf = (event) => {
|
|
382
|
-
if (event.type === "loop.exhausted") return [event.from, {
|
|
383
|
-
code: "loop.exhausted",
|
|
384
|
-
kind: "workflow",
|
|
385
|
-
target: "workflow",
|
|
386
|
-
severity: "medium",
|
|
387
|
-
title: `The loop ${event.from} -> ${event.to} ran out of its ${String(event.maxRounds)} rounds`,
|
|
388
|
-
body: "Raise `maxRounds`, or make what the loop's nodes are asked to fix clearer so it settles sooner."
|
|
389
|
-
}];
|
|
390
|
-
if (event.type === "run.finished" && event.result.status === "budget_exhausted") return [void 0, {
|
|
391
|
-
code: "budget.exhausted",
|
|
392
|
-
kind: "workflow",
|
|
393
|
-
target: "workflow",
|
|
394
|
-
severity: "medium",
|
|
395
|
-
title: "The run's budget ran out",
|
|
396
|
-
body: `The run stopped after spending $${event.result.costUsd.toFixed(4)}. Raise \`limits.budgetUsd\`, or give nodes cheaper models or smaller budgets.`
|
|
397
|
-
}];
|
|
398
|
-
if (event.type === "decision.answered") {
|
|
399
|
-
const question = asked.get(event.decision);
|
|
400
|
-
return question === void 0 ? null : answered(question, event.answer);
|
|
401
|
-
}
|
|
402
|
-
if (event.type === "node.event" && event.event.type === "tool.result") {
|
|
403
|
-
const name = tools.get(`${event.id}\0${event.event.id}`);
|
|
404
|
-
if (!event.event.isError || name?.startsWith(ORK_TOOL) !== true) return null;
|
|
405
|
-
const tool = name.slice(10);
|
|
406
|
-
return [event.id, {
|
|
407
|
-
code: "tool.error",
|
|
408
|
-
kind: "bug",
|
|
409
|
-
target: "ork",
|
|
410
|
-
severity: "medium",
|
|
411
|
-
title: `Ork's ${tool} tool returned an error`,
|
|
412
|
-
body: `A call to \`${tool}\` failed; the node's events show the call.`,
|
|
413
|
-
evidence: [name]
|
|
414
|
-
}];
|
|
415
|
-
}
|
|
416
|
-
if (event.type === "node.finished" && nodeTypes.get(event.id) === "human.gate" && event.output["answeredBy"] === "timeout") return [event.id, {
|
|
417
|
-
code: "gate.timeout",
|
|
418
|
-
kind: "workflow",
|
|
419
|
-
target: "workflow",
|
|
420
|
-
severity: "low",
|
|
421
|
-
title: `The gate ${event.id} timed out and took its default`,
|
|
422
|
-
body: "No one answered in time. Lengthen its `timeout`, or keep it if the default was right."
|
|
423
|
-
}];
|
|
424
|
-
return null;
|
|
425
|
-
};
|
|
426
|
-
/** Keeps what later events and origins need. */
|
|
427
|
-
const observe = (event) => {
|
|
428
|
-
if (event.type === "run.started") workflow = event.workflow;
|
|
429
|
-
if (event.type === "node.started") nodeTypes.set(event.id, event.nodeType);
|
|
430
|
-
if (event.type === "decision.opened") {
|
|
431
|
-
const question = {
|
|
432
|
-
node: event.node,
|
|
433
|
-
kind: event.kind,
|
|
434
|
-
question: event.question
|
|
435
|
-
};
|
|
436
|
-
if (event.recommended !== void 0) question.recommended = event.recommended;
|
|
437
|
-
asked.set(event.decision, question);
|
|
438
|
-
}
|
|
439
|
-
if (event.type !== "node.event") return;
|
|
440
|
-
const inner = event.event;
|
|
441
|
-
if (inner.type === "session.started" && inner.fork !== true) sessions.set(event.id, {
|
|
442
|
-
model: inner.model,
|
|
443
|
-
...inner.driver === void 0 ? {} : { driver: inner.driver }
|
|
444
|
-
});
|
|
445
|
-
if (inner.type === "tool.called") tools.set(`${event.id}\0${inner.id}`, inner.name);
|
|
446
|
-
};
|
|
447
|
-
return (event) => {
|
|
448
|
-
observe(event);
|
|
449
|
-
if (event.type === "feedback.recorded") {
|
|
450
|
-
if (seen.has(event.feedback.fingerprint)) return [];
|
|
451
|
-
seen.add(event.feedback.fingerprint);
|
|
452
|
-
const origin = {
|
|
453
|
-
...originOf(event.node),
|
|
454
|
-
...event.feedback.origin
|
|
455
|
-
};
|
|
456
|
-
return [{
|
|
457
|
-
...event,
|
|
458
|
-
feedback: {
|
|
459
|
-
...event.feedback,
|
|
460
|
-
origin
|
|
461
|
-
}
|
|
462
|
-
}];
|
|
463
|
-
}
|
|
464
|
-
const signal = signalOf(event);
|
|
465
|
-
return signal === null ? [event] : [event, ...record(...signal)];
|
|
466
|
-
};
|
|
467
|
-
};
|
|
468
97
|
/** A title's words, as fingerprints compare them. */
|
|
469
98
|
const titleWords = (title) => new Set(normalizeTitle(title).split(" ").filter((word) => word !== ""));
|
|
470
99
|
/** The share of two titles' words they have in common (Jaccard). */
|
|
@@ -506,6 +135,140 @@ const proposeMerges = (items) => {
|
|
|
506
135
|
};
|
|
507
136
|
}).filter((merge) => merge !== void 0).toSorted((a, b) => firstSeen(a.into, b.into));
|
|
508
137
|
};
|
|
138
|
+
/** `ORK_HEAVY_SLOTS` as a count; anything but a positive integer is the default. */
|
|
139
|
+
const heavySlots = (env = process.env) => {
|
|
140
|
+
const count = Number(env["ORK_HEAVY_SLOTS"]);
|
|
141
|
+
return Number.isInteger(count) && count > 0 ? count : 2;
|
|
142
|
+
};
|
|
143
|
+
const codeOf = (error) => error instanceof Error && "code" in error ? error.code : void 0;
|
|
144
|
+
const alive = (pid) => {
|
|
145
|
+
if (pid <= 0) return false;
|
|
146
|
+
try {
|
|
147
|
+
process.kill(pid, 0);
|
|
148
|
+
return true;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
return codeOf(error) === "EPERM";
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const read = (file) => {
|
|
154
|
+
try {
|
|
155
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
156
|
+
if (typeof value !== "object" || value === null || !("id" in value && "run" in value && "node" in value && "pid" in value)) return;
|
|
157
|
+
const { id, run, node, pid } = value;
|
|
158
|
+
return typeof id === "string" && typeof run === "string" && typeof node === "string" && typeof pid === "number" ? {
|
|
159
|
+
id,
|
|
160
|
+
run,
|
|
161
|
+
node,
|
|
162
|
+
pid
|
|
163
|
+
} : void 0;
|
|
164
|
+
} catch {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
const listed = (dir) => {
|
|
169
|
+
try {
|
|
170
|
+
return readdirSync(dir).filter((name) => name.endsWith(".json")).toSorted();
|
|
171
|
+
} catch {
|
|
172
|
+
return [];
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
/** Written whole (a rename), so no reader sees half a record. */
|
|
176
|
+
const writeWhole = (file, record) => {
|
|
177
|
+
const draft = `${file}.${record.id}.tmp`;
|
|
178
|
+
writeFileSync(draft, JSON.stringify(record), { mode: 384 });
|
|
179
|
+
renameSync(draft, file);
|
|
180
|
+
};
|
|
181
|
+
/** Deletes a record if it is still `id`'s. */
|
|
182
|
+
const dropIf = (file, id) => {
|
|
183
|
+
if (read(file)?.id === id) rmSync(file, { force: true });
|
|
184
|
+
};
|
|
185
|
+
let tickets = 0;
|
|
186
|
+
/** A ticket's name: sorted by when it was queued. */
|
|
187
|
+
const ticketName = (id) => `${String(Date.now()).padStart(15, "0")}-${String(process.pid).padStart(8, "0")}-${String(tickets += 1).padStart(6, "0")}-${id}.json`;
|
|
188
|
+
/** The ledger in `dir`, with `slots` slots. */
|
|
189
|
+
const fileSlotLedger = (options) => {
|
|
190
|
+
const { dir, slots } = options;
|
|
191
|
+
const queue = path.join(dir, "queue");
|
|
192
|
+
const slotFile = (index) => path.join(dir, `slot-${String(index)}.json`);
|
|
193
|
+
const records = () => Array.from({ length: slots }, (_, index) => {
|
|
194
|
+
const file = slotFile(index);
|
|
195
|
+
return {
|
|
196
|
+
file,
|
|
197
|
+
held: read(file)
|
|
198
|
+
};
|
|
199
|
+
});
|
|
200
|
+
const holders = () => records().flatMap(({ held }) => held === void 0 ? [] : [held]);
|
|
201
|
+
const sweep = () => {
|
|
202
|
+
const gone = [];
|
|
203
|
+
for (const file of [...records().map((record) => record.file), ...listed(queue).map((name) => path.join(queue, name))]) {
|
|
204
|
+
const held = read(file);
|
|
205
|
+
if (held === void 0 || alive(held.pid)) continue;
|
|
206
|
+
dropIf(file, held.id);
|
|
207
|
+
gone.push(held);
|
|
208
|
+
}
|
|
209
|
+
return gone;
|
|
210
|
+
};
|
|
211
|
+
/** Links `draft` into a free slot; true when it got one. */
|
|
212
|
+
const tryTake = (draft) => {
|
|
213
|
+
for (const { file, held } of records()) {
|
|
214
|
+
if (held !== void 0) continue;
|
|
215
|
+
try {
|
|
216
|
+
linkSync(draft, file);
|
|
217
|
+
return true;
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (codeOf(error) !== "EEXIST") throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
};
|
|
224
|
+
const take = async (holder, { signal, onWait }) => {
|
|
225
|
+
mkdirSync(queue, {
|
|
226
|
+
recursive: true,
|
|
227
|
+
mode: 448
|
|
228
|
+
});
|
|
229
|
+
const record = {
|
|
230
|
+
id: randomUUID(),
|
|
231
|
+
...holder
|
|
232
|
+
};
|
|
233
|
+
const ticket = path.join(queue, ticketName(record.id));
|
|
234
|
+
writeWhole(ticket, record);
|
|
235
|
+
let told = false;
|
|
236
|
+
try {
|
|
237
|
+
for (;;) {
|
|
238
|
+
if (signal.aborted) return null;
|
|
239
|
+
sweep();
|
|
240
|
+
const free = records().filter(({ held }) => held === void 0).length;
|
|
241
|
+
if (listed(queue).indexOf(path.basename(ticket)) < free && tryTake(ticket)) {
|
|
242
|
+
const release = () => {
|
|
243
|
+
for (const { file, held } of records()) if (held?.id === record.id) dropIf(file, record.id);
|
|
244
|
+
};
|
|
245
|
+
return release;
|
|
246
|
+
}
|
|
247
|
+
if (!told) {
|
|
248
|
+
told = true;
|
|
249
|
+
onWait?.(holders());
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
await setTimeout$1(options.pollMs ?? 500, void 0, { signal });
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} finally {
|
|
258
|
+
rmSync(ticket, { force: true });
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
return {
|
|
262
|
+
take,
|
|
263
|
+
holders,
|
|
264
|
+
sweep
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
/** The machine's ledger: `~/.ork/slots`, `ORK_HEAVY_SLOTS` slots. */
|
|
268
|
+
const machineSlotLedger = (options = {}) => fileSlotLedger({
|
|
269
|
+
dir: path.join(options.home ?? homedir(), ".ork", "slots"),
|
|
270
|
+
slots: heavySlots(options.env)
|
|
271
|
+
});
|
|
509
272
|
// ../core/src/remote/relay.ts
|
|
510
273
|
/**
|
|
511
274
|
* @file A run's side of the relay (RFC-003): its nodes' links, read from the
|
|
@@ -716,6 +479,11 @@ const createRecorder = (store, run, runDir) => {
|
|
|
716
479
|
// src/help-root.ts
|
|
717
480
|
/** Where the user docs start. */
|
|
718
481
|
const DOCS_URL = "https://github.com/mimshins/ork/blob/main/docs/README.md";
|
|
482
|
+
/** The flags every command takes, which `bin.ts` handles before citty. */
|
|
483
|
+
const GLOBAL_FLAGS = [{
|
|
484
|
+
name: "--plain",
|
|
485
|
+
description: "Plain output: no color, no redrawn prompts (also ORK_PLAIN=1, NO_COLOR)"
|
|
486
|
+
}];
|
|
719
487
|
const rowOf = (entry) => {
|
|
720
488
|
const short = groupOf(entry.name)?.short ?? entry.description;
|
|
721
489
|
const alias = entry.aliases.length === 0 ? "" : ` (alias: ${entry.aliases.join(", ")})`;
|
|
@@ -743,6 +511,9 @@ const renderRootHelp = async (cmd) => {
|
|
|
743
511
|
...groups.flatMap((group) => groupLines(group, width)),
|
|
744
512
|
`Usage: ${name} <command> [flags]`,
|
|
745
513
|
"",
|
|
514
|
+
"Flags for every command:",
|
|
515
|
+
...GLOBAL_FLAGS.map((f) => ` ${f.name} ${f.description}`),
|
|
516
|
+
"",
|
|
746
517
|
`Use "${name} <command> --help" for more about a command.`,
|
|
747
518
|
`${name} v${meta.version ?? ""}`
|
|
748
519
|
].join("\n");
|
|
@@ -766,294 +537,15 @@ const renderHelp = async (cmd, parent) => {
|
|
|
766
537
|
if (cmd.subCommands !== void 0 && parent === void 0) return renderRootHelp(shown);
|
|
767
538
|
const above = parent?.meta === void 0 ? void 0 : { meta: parent.meta };
|
|
768
539
|
const usage = await renderUsage(shown, above);
|
|
769
|
-
|
|
540
|
+
const globals = `GLOBAL OPTIONS\n\n${GLOBAL_FLAGS.map((f) => ` ${f.name} ${f.description}`).join("\n")}`;
|
|
541
|
+
if (cmd.subCommands === void 0) return `${usage}\n\n${globals}`;
|
|
770
542
|
const choices = await choicesOf(shown);
|
|
771
|
-
return usage.replace(` [OPTIONS] ${choices}`, ` <command> [flags]`).replace(` ${choices}`, ` <command> [flags]`)
|
|
543
|
+
return `${usage.replace(` [OPTIONS] ${choices}`, ` <command> [flags]`).replace(` ${choices}`, ` <command> [flags]`)}\n\n${globals}`;
|
|
772
544
|
};
|
|
773
545
|
/** Prints {@link renderHelp}'s usage, as citty prints its own. */
|
|
774
546
|
const showHelp = async (cmd, parent) => {
|
|
775
547
|
process.stdout.write(`${await renderHelp(cmd, parent)}\n\n`);
|
|
776
548
|
};
|
|
777
|
-
// src/human.ts
|
|
778
|
-
/**
|
|
779
|
-
* @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
|
|
780
|
-
* permission prompts on the terminal, one at a time, and denies or defers
|
|
781
|
-
* when no one is there. After a choice it offers one optional line for a
|
|
782
|
-
* note, which Enter skips. In a run it is the decision queue's local
|
|
783
|
-
* answerer, racing `ork decide`.
|
|
784
|
-
*/
|
|
785
|
-
const PREVIEW = 160;
|
|
786
|
-
/** A document longer than this is offered (`d`) rather than printed. */
|
|
787
|
-
const SHORT_DOCUMENT_LINES = 40;
|
|
788
|
-
const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
|
|
789
|
-
const NOTE_PROMPT = " Note for the run (optional, Enter to skip): ";
|
|
790
|
-
const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
|
|
791
|
-
/** One line that shows what a tool is about to do. */
|
|
792
|
-
const previewInput = (toolName, input) => {
|
|
793
|
-
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
794
|
-
const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
|
|
795
|
-
return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
|
|
796
|
-
};
|
|
797
|
-
/** The option an answer names: its number, or its id. */
|
|
798
|
-
const pickOption = (answer, options, recommended) => {
|
|
799
|
-
const value = answer.trim();
|
|
800
|
-
if (value === "") return recommended;
|
|
801
|
-
const index = Number(value);
|
|
802
|
-
if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
|
|
803
|
-
return options.find((o) => o.id === value)?.id;
|
|
804
|
-
};
|
|
805
|
-
const optionLine = (option, index, recommended) => {
|
|
806
|
-
const label = option.label === void 0 ? "" : ` ${option.label}`;
|
|
807
|
-
const description = option.description === void 0 ? "" : style("dim", ` - ${option.description}`);
|
|
808
|
-
const mark = option.id === recommended ? style("green", " (recommended)") : "";
|
|
809
|
-
return ` ${String(index + 1)}) ${style("bold", option.id)}${label}${description}${mark}`;
|
|
810
|
-
};
|
|
811
|
-
/**
|
|
812
|
-
* Answers permission prompts and `ask_user` questions on the terminal, one at a
|
|
813
|
-
* time. Without a terminal it denies tools and leaves questions to the node. A
|
|
814
|
-
* withdrawn prompt (answered elsewhere) rejects, which frees the turn.
|
|
815
|
-
*/
|
|
816
|
-
const createTerminalHuman = (io) => {
|
|
817
|
-
let rl;
|
|
818
|
-
let turn = Promise.resolve();
|
|
819
|
-
const say = (text) => {
|
|
820
|
-
io.output.write(`${text}\n`);
|
|
821
|
-
};
|
|
822
|
-
const prompt = (text, signal) => {
|
|
823
|
-
if (rl === void 0) {
|
|
824
|
-
rl = createInterface({
|
|
825
|
-
input: io.input,
|
|
826
|
-
output: io.output
|
|
827
|
-
});
|
|
828
|
-
if (io.interrupt !== void 0) rl.on("SIGINT", io.interrupt);
|
|
829
|
-
}
|
|
830
|
-
return signal === void 0 ? rl.question(text) : rl.question(text, { signal });
|
|
831
|
-
};
|
|
832
|
-
/** The optional line after a choice, when the asker takes a note. */
|
|
833
|
-
const takeNote = async (onNote, signal) => {
|
|
834
|
-
if (onNote === void 0) return;
|
|
835
|
-
const note = (await prompt(NOTE_PROMPT, signal)).trim();
|
|
836
|
-
if (note !== "") onNote(note);
|
|
837
|
-
};
|
|
838
|
-
const choose = async (hint, options, recommended, signal, document) => {
|
|
839
|
-
const answer = await prompt(` Choose${hint}: `, signal);
|
|
840
|
-
const picked = pickOption(answer, options, recommended);
|
|
841
|
-
if (picked !== void 0) return picked;
|
|
842
|
-
if (document !== void 0 && answer.trim() === "d") say(document);
|
|
843
|
-
else say(style("dim", " Type an option's number or id."));
|
|
844
|
-
return choose(hint, options, recommended, signal, document);
|
|
845
|
-
};
|
|
846
|
-
const oneAtATime = (work) => {
|
|
847
|
-
const result = turn.then(work);
|
|
848
|
-
turn = result.catch(() => null);
|
|
849
|
-
return result;
|
|
850
|
-
};
|
|
851
|
-
return {
|
|
852
|
-
permit: ({ nodeId, toolName, input, signal, onNote }) => oneAtATime(async () => {
|
|
853
|
-
if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
|
|
854
|
-
say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} wants to run ${previewInput(toolName, input)}`);
|
|
855
|
-
if (!io.interactive) {
|
|
856
|
-
say(style("dim", ` denied: ${NO_ONE_TO_PERMIT}`));
|
|
857
|
-
return {
|
|
858
|
-
allow: false,
|
|
859
|
-
message: NO_ONE_TO_PERMIT
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
|
-
const answer = (await prompt(" Allow? [y/N] ", signal)).trim().toLowerCase();
|
|
863
|
-
if (answer === "y" || answer === "yes") return { allow: true };
|
|
864
|
-
await takeNote(onNote, signal);
|
|
865
|
-
return {
|
|
866
|
-
allow: false,
|
|
867
|
-
message: "The user did not allow this tool."
|
|
868
|
-
};
|
|
869
|
-
}),
|
|
870
|
-
ask: ({ nodeId, question, options, recommended, context, document, signal, onNote }) => oneAtATime(async () => {
|
|
871
|
-
if (signal?.aborted === true) throw new Error("the question was withdrawn");
|
|
872
|
-
const documentLines = document?.trimEnd().split("\n").length ?? 0;
|
|
873
|
-
if (document !== void 0 && documentLines <= SHORT_DOCUMENT_LINES) say(document);
|
|
874
|
-
say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} asks: ${question}`);
|
|
875
|
-
if (context !== void 0) say(style("dim", ` ${context.replaceAll("\n", "\n ")}`));
|
|
876
|
-
for (const [index, option] of options.entries()) say(optionLine(option, index, recommended));
|
|
877
|
-
if (!io.interactive) {
|
|
878
|
-
const answer = recommended ?? NO_ONE_TO_ASK;
|
|
879
|
-
say(style("dim", ` no terminal; answered ${answer}`));
|
|
880
|
-
return answer;
|
|
881
|
-
}
|
|
882
|
-
const fallback = options.findIndex((o) => o.id === recommended);
|
|
883
|
-
const hint = fallback === -1 ? "" : ` [${String(fallback + 1)}]`;
|
|
884
|
-
if (documentLines > SHORT_DOCUMENT_LINES) say(` Document: ${String(documentLines)} lines. Type d to read it.`);
|
|
885
|
-
const picked = await choose(hint, options, recommended, signal, document);
|
|
886
|
-
await takeNote(onNote, signal);
|
|
887
|
-
return picked;
|
|
888
|
-
}),
|
|
889
|
-
close: () => {
|
|
890
|
-
rl?.close();
|
|
891
|
-
}
|
|
892
|
-
};
|
|
893
|
-
};
|
|
894
|
-
// src/run/format-result.ts
|
|
895
|
-
const RUN_MARK = {
|
|
896
|
-
succeeded: style("green", "✓"),
|
|
897
|
-
failed: style("red", "✗"),
|
|
898
|
-
budget_exhausted: style("yellow", "!"),
|
|
899
|
-
quota_exhausted: style("yellow", "!"),
|
|
900
|
-
timed_out: style("yellow", "!"),
|
|
901
|
-
interrupted: style("yellow", "!")
|
|
902
|
-
};
|
|
903
|
-
/** `1 failure handled`, `2 failures handled`. */
|
|
904
|
-
const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" : "failures"} handled`;
|
|
905
|
-
/**
|
|
906
|
-
* The run summary: status, cost, what ran, what was skipped and which failures
|
|
907
|
-
* an edge handled, branches to look at, why, and mail to nodes that never ran.
|
|
908
|
-
*/
|
|
909
|
-
const formatRunResult = (result) => {
|
|
910
|
-
const ran = result.nodes.filter((n) => n.runs > 0).length;
|
|
911
|
-
const skipped = result.skipped ?? result.nodes.length - ran;
|
|
912
|
-
const handled = result.failuresHandled ?? 0;
|
|
913
|
-
const lines = ["", `${RUN_MARK[result.status]} run ${result.runId} ${result.status.replace("_", " ")} ${style("dim", `· ${cost(result.costUsd)} · ${String(ran)} ran, ${String(skipped)} skipped${handled === 0 ? "" : `, ${failuresHandled(handled)}`}`)}`];
|
|
914
|
-
for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
|
|
915
|
-
for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
|
|
916
|
-
for (const { node, messages } of result.unread ?? []) lines.push(` ${style(["yellow", "bold"], "warning")} ${node} never ran, so ${messages === 1 ? "1 message" : `${String(messages)} messages`} to it went unread`);
|
|
917
|
-
return lines;
|
|
918
|
-
};
|
|
919
|
-
// src/run/format.ts
|
|
920
|
-
/** Who answered a decision, as the log says it. */
|
|
921
|
-
const ANSWERED_BY = {
|
|
922
|
-
terminal: "terminal",
|
|
923
|
-
decide: "ork decide",
|
|
924
|
-
ui: "ork ui",
|
|
925
|
-
owner: "its owner"
|
|
926
|
-
};
|
|
927
|
-
const STATUS_MARK = {
|
|
928
|
-
pass: style("green", "✓"),
|
|
929
|
-
done: style("green", "✓"),
|
|
930
|
-
changes_requested: style("yellow", "!"),
|
|
931
|
-
fail: style("red", "✗")
|
|
932
|
-
};
|
|
933
|
-
const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
|
|
934
|
-
const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${line}`);
|
|
935
|
-
/** The usage line: cost, tokens, turns and time. */
|
|
936
|
-
const formatUsage = ({ usage }) => style("dim", [
|
|
937
|
-
cost(usage.costUsd),
|
|
938
|
-
`${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
|
|
939
|
-
`${String(usage.turns)} turns`,
|
|
940
|
-
seconds(usage.durationMs)
|
|
941
|
-
].join(" · "));
|
|
942
|
-
/** How a finished session reads on the terminal. */
|
|
943
|
-
const formatOutcome = (nodeId, outcome) => {
|
|
944
|
-
const { report } = outcome;
|
|
945
|
-
if (report === null) return [
|
|
946
|
-
`${style("red", "✗")} ${style("bold", nodeId)} ended without a report (${outcome.subtype})`,
|
|
947
|
-
...outcome.errors.map((e) => ` ${style("red", e)}`),
|
|
948
|
-
` ${formatUsage(outcome)}`
|
|
949
|
-
];
|
|
950
|
-
const lines = [`${STATUS_MARK[report.status] ?? "-"} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
|
|
951
|
-
if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
|
|
952
|
-
for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
|
|
953
|
-
for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
|
|
954
|
-
lines.push(` ${formatUsage(outcome)}`);
|
|
955
|
-
return lines;
|
|
956
|
-
};
|
|
957
|
-
/** The transcript lines for one event; tool names come from `tools`. */
|
|
958
|
-
const formatEvent = (nodeId, event, tools) => {
|
|
959
|
-
if (event.type === "session.started") {
|
|
960
|
-
const fork = event.fork === true ? ", a fork to answer a question" : "";
|
|
961
|
-
return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId}${fork})`)];
|
|
962
|
-
}
|
|
963
|
-
if (event.type === "assistant.text") return indent(event.text);
|
|
964
|
-
if (event.type === "session.waiting") return indent(event.reason);
|
|
965
|
-
if (event.type === "tool.called") {
|
|
966
|
-
tools.set(event.id, event.name);
|
|
967
|
-
return [style("dim", ` · ${previewInput(event.name, event.input)}`)];
|
|
968
|
-
}
|
|
969
|
-
if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
|
|
970
|
-
if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
|
|
971
|
-
return formatOutcome(nodeId, event.outcome);
|
|
972
|
-
};
|
|
973
|
-
const items = (count) => `${String(count)} ${count === 1 ? "item" : "items"}`;
|
|
974
|
-
/** Transcript lines for one run event, prefixed with the node they are from. */
|
|
975
|
-
const formatRunEvent = (event, width, tools, terminal = false) => {
|
|
976
|
-
const tag = (id) => style("cyan", `${id.padEnd(width)} │`);
|
|
977
|
-
if (event.type === "run.started") return [style("dim", `▸ run ${event.runId} of ${event.workflow} (starts at ${event.entries.join(", ")})`), ...event.settings?.trigger === void 0 ? [] : [style("dim", ` started by trigger ${event.settings.trigger}; untrusted inputs: ${(event.settings.untrusted ?? []).join(", ") || "none"}`)]];
|
|
978
|
-
if (event.type === "node.started") {
|
|
979
|
-
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
980
|
-
const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
|
|
981
|
-
return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
|
|
982
|
-
}
|
|
983
|
-
if (event.type === "node.event") {
|
|
984
|
-
const inner = event.event;
|
|
985
|
-
if (inner.type === "session.finished") return [];
|
|
986
|
-
const fork = inner.fork === true ? style("dim", "fork · ") : "";
|
|
987
|
-
if (inner.type === "session.started") {
|
|
988
|
-
const why = inner.fork === true ? ", to answer a question" : "";
|
|
989
|
-
return [`${tag(event.id)} ${fork}${style("dim", `${inner.model}, session ${inner.sessionId}${why}`)}`];
|
|
990
|
-
}
|
|
991
|
-
return formatEvent(event.id, event.event, tools).map((line) => `${tag(event.id)} ${fork}${line.replace(/^ {2}/u, "")}`);
|
|
992
|
-
}
|
|
993
|
-
if (event.type === "node.finished") {
|
|
994
|
-
const conflict = hasConflict(event.output);
|
|
995
|
-
const mark = event.ok ? style(conflict ? "yellow" : "green", conflict ? "!" : "✓") : style("red", "✗");
|
|
996
|
-
const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
|
|
997
|
-
return [`${tag(event.id)} ${mark} ${nodeVerdict(event.output, event.ok, event.error)}${spent}`];
|
|
998
|
-
}
|
|
999
|
-
if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
|
|
1000
|
-
if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
|
|
1001
|
-
if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
|
|
1002
|
-
if (event.type === "watch.changed") return [`${tag(event.id)} ${style("dim", `look ${String(event.looks)}:`)} ${event.said}`];
|
|
1003
|
-
if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
|
|
1004
|
-
if (event.type === "edge.taken") return [];
|
|
1005
|
-
if (event.type === "worktree") {
|
|
1006
|
-
const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
|
|
1007
|
-
const what = event.action === "removed" && event.branch !== void 0 ? `worktree of ${event.branch} (branch kept)` : `${event.branch ?? event.path}${detail}`;
|
|
1008
|
-
const line = `${event.action} ${what}`;
|
|
1009
|
-
return [`${tag(event.id)} ${event.action === "conflict" ? style("yellow", `! ${line}`) : style("dim", `⎇ ${line}`)}`];
|
|
1010
|
-
}
|
|
1011
|
-
if (event.type === "decision.opened") {
|
|
1012
|
-
const what = event.kind === "permission" ? "asks to" : "asks:";
|
|
1013
|
-
const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
|
|
1014
|
-
const where = event.owner === void 0 ? `decision ${event.decision}; ${terminal ? "answer here or with ork decide" : "ork decide"}` : `decision ${event.decision}, via ${event.owner}`;
|
|
1015
|
-
return [`${tag(event.node)} ${style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
|
|
1016
|
-
}
|
|
1017
|
-
if (event.type === "decision.answered") {
|
|
1018
|
-
const by = event.by === "owner" && event.owner !== void 0 ? event.owner : ANSWERED_BY[event.by];
|
|
1019
|
-
const said = event.note === void 0 ? "" : ` · said: ${event.note.replaceAll(/\s+/gu, " ")}`;
|
|
1020
|
-
return [`${tag(event.node)} ${style("dim", `→ ${event.answer} (${by})${said}`)}`];
|
|
1021
|
-
}
|
|
1022
|
-
if (event.type === "decision.withdrawn") return [`${tag(event.node)} ${style("dim", "→ question withdrawn")}`];
|
|
1023
|
-
if (event.type === "run.command") {
|
|
1024
|
-
const node = event.node === void 0 ? "" : ` --node ${event.node}`;
|
|
1025
|
-
return [style(["yellow", "bold"], `! ork ${event.command}${node} received`)];
|
|
1026
|
-
}
|
|
1027
|
-
if (event.type === "node.held") {
|
|
1028
|
-
const at = event.at === "tool" ? "its tool call waits for ork continue" : "it starts after ork continue";
|
|
1029
|
-
return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
|
|
1030
|
-
}
|
|
1031
|
-
if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
|
|
1032
|
-
if (event.type === "each.listed") {
|
|
1033
|
-
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
1034
|
-
return [`${tag(event.node)} ${style("dim", `${items(event.count)}${round}`)}`];
|
|
1035
|
-
}
|
|
1036
|
-
if (event.type === "each.exceeded") {
|
|
1037
|
-
const over = `${items(event.count)}, over maxItems ${String(event.maxItems)}`;
|
|
1038
|
-
return [`${tag(event.node)} ${style("yellow", `! ${over}`)}`];
|
|
1039
|
-
}
|
|
1040
|
-
if (event.type === "run.finished") return formatRunResult(event.result);
|
|
1041
|
-
if (event.type === "run.resumed") {
|
|
1042
|
-
const [one] = event.kept;
|
|
1043
|
-
const kept = one === void 0 ? "every node runs again" : event.kept.length === 1 ? `${one} keeps its result` : `${event.kept.join(", ")} keep their results`;
|
|
1044
|
-
return ["", style("dim", `▸ resumed: ${kept}`)];
|
|
1045
|
-
}
|
|
1046
|
-
if (event.type === "feedback.recorded") {
|
|
1047
|
-
const { target, kind, title } = event.feedback;
|
|
1048
|
-
const line = style("dim", `✎ feedback (${target}, ${kind}): ${title}`);
|
|
1049
|
-
return [event.node === void 0 ? line : `${tag(event.node)} ${line}`];
|
|
1050
|
-
}
|
|
1051
|
-
if (event.type === "user.message") {
|
|
1052
|
-
const note = event.delivered === "queued" ? " (for its next session)" : "";
|
|
1053
|
-
return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
|
|
1054
|
-
}
|
|
1055
|
-
return formatBusEvent(event).map((line) => `${tag(event.node)} ${line}`);
|
|
1056
|
-
};
|
|
1057
549
|
// src/runs/chat.ts
|
|
1058
550
|
/**
|
|
1059
551
|
* @file `ork chat`'s two paths. A node of a live run gets the message through
|
|
@@ -1103,6 +595,8 @@ const resumeChat = async (options) => {
|
|
|
1103
595
|
...template === void 0 ? {} : { template }
|
|
1104
596
|
});
|
|
1105
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;
|
|
1106
600
|
const servers = namedMcpEntries(run.project, plan.mcp);
|
|
1107
601
|
if (!servers.ok) return servers.error;
|
|
1108
602
|
let driver = options.driver ?? session.driver ?? plan.driver;
|
|
@@ -1120,6 +614,7 @@ const resumeChat = async (options) => {
|
|
|
1120
614
|
});
|
|
1121
615
|
const started = (await options.runtime(driver, placeFiles(place).runs)).start({
|
|
1122
616
|
...plan.spec,
|
|
617
|
+
...itemModel === void 0 ? {} : { model: itemModel },
|
|
1123
618
|
...servers.value,
|
|
1124
619
|
resume: session.sessionId,
|
|
1125
620
|
prompt: options.body,
|
|
@@ -1142,58 +637,14 @@ const resumeChat = async (options) => {
|
|
|
1142
637
|
}
|
|
1143
638
|
return failure;
|
|
1144
639
|
};
|
|
1145
|
-
// src/runs/logs.ts
|
|
1146
|
-
/**
|
|
1147
|
-
* @file `ork logs`: replays a stored run's events as the transcript `ork run`
|
|
1148
|
-
* printed, for the whole run or one node, and tails a live run until it
|
|
1149
|
-
* ends.
|
|
1150
|
-
*/
|
|
1151
|
-
/** Turns stored events into lines, keeping the state a transcript needs. */
|
|
1152
|
-
const createLogPrinter = (options) => {
|
|
1153
|
-
const tools = /* @__PURE__ */ new Map();
|
|
1154
|
-
let width = 8;
|
|
1155
|
-
return (stored) => {
|
|
1156
|
-
const { event } = stored;
|
|
1157
|
-
if (event.type === "run.started") width = Math.max(...event.nodes.map((n) => n.id.length), 1);
|
|
1158
|
-
if (options.node !== void 0 && !isNodeEvent(event, options.node)) return [];
|
|
1159
|
-
if (options.json) return [JSON.stringify(stored)];
|
|
1160
|
-
return formatRunEvent(event, width, tools);
|
|
1161
|
-
};
|
|
1162
|
-
};
|
|
1163
|
-
/**
|
|
1164
|
-
* Why `--node` names nothing in this run, or `null`: no event names it and the
|
|
1165
|
-
* run didn't declare it. A live run may still start it, so that is not
|
|
1166
|
-
* refused.
|
|
1167
|
-
*/
|
|
1168
|
-
const unknownNode = (stored, node, live) => {
|
|
1169
|
-
const named = stored.some(({ event }) => isNodeEvent(event, node));
|
|
1170
|
-
const declared = stored.some(({ event }) => event.type === "run.started" && event.nodes.some((n) => n.id === node));
|
|
1171
|
-
return named || declared || live ? null : `Run has no node ${node}.`;
|
|
1172
|
-
};
|
|
1173
|
-
/**
|
|
1174
|
-
* Prints a run's events; with `follow`, then each new one as it is stored:
|
|
1175
|
-
* pushed over the run's socket, or polled from the store when there is none.
|
|
1176
|
-
* Returns when `run.finished` is stored, the run's process is gone, or `signal`
|
|
1177
|
-
* aborts.
|
|
1178
|
-
*/
|
|
1179
|
-
const followRun = async (store, run, out, options) => {
|
|
1180
|
-
const print = createLogPrinter(options);
|
|
1181
|
-
if (await followEvents(store, run.id, (stored) => {
|
|
1182
|
-
for (const line of print(stored)) out(line);
|
|
1183
|
-
}, {
|
|
1184
|
-
follow: options.follow,
|
|
1185
|
-
...options.after === void 0 ? {} : { after: options.after },
|
|
1186
|
-
...options.intervalMs === void 0 ? {} : { intervalMs: options.intervalMs },
|
|
1187
|
-
...options.home === void 0 ? {} : { home: options.home },
|
|
1188
|
-
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
1189
|
-
}) === "crashed") out(style("red", `run ${run.id} stopped without finishing (its process is gone)`));
|
|
1190
|
-
};
|
|
1191
640
|
// src/run/node-notes.ts
|
|
1192
641
|
/**
|
|
1193
642
|
* @file What a run of one node (`ork run --node`) leaves out, said before its
|
|
1194
643
|
* session starts: the messages, steps and `forEach` it does not run. A node
|
|
1195
644
|
* with `forEach` (RFC-013) runs once with no item when it has `items`, and is
|
|
1196
645
|
* refused when it has `from`, as the node that reports its list is not run.
|
|
646
|
+
* And, after it, its run summary: how the node ended
|
|
647
|
+
* (specs/runtime/session-ends.md).
|
|
1197
648
|
*/
|
|
1198
649
|
/** A warning line, as the run prints it. */
|
|
1199
650
|
const noteLines = (lines) => lines.map((line) => `${style(["yellow", "bold"], "warning")} ${line}`);
|
|
@@ -1213,6 +664,32 @@ const oneNodeNotes = (workflow, id) => {
|
|
|
1213
664
|
...steps.map((step) => `${id}'s ${step}: does not run in a run of one node.`)
|
|
1214
665
|
] };
|
|
1215
666
|
};
|
|
667
|
+
/** The `node.finished` fields that say how it ended: its error and its end. */
|
|
668
|
+
const endedFields = (result) => ({
|
|
669
|
+
...result.error === void 0 ? {} : { error: result.error },
|
|
670
|
+
end: endOf(result)
|
|
671
|
+
});
|
|
672
|
+
/** The one-node run's result, its reason the node's message. */
|
|
673
|
+
const oneNodeResult = (runId, id, result, ok) => {
|
|
674
|
+
const costUsd = result.usage?.costUsd ?? 0;
|
|
675
|
+
return {
|
|
676
|
+
runId,
|
|
677
|
+
status: ok ? "succeeded" : "failed",
|
|
678
|
+
costUsd,
|
|
679
|
+
nodes: [{
|
|
680
|
+
id,
|
|
681
|
+
runs: 1,
|
|
682
|
+
ok: result.ok,
|
|
683
|
+
output: result.output,
|
|
684
|
+
costUsd,
|
|
685
|
+
...endedFields(result)
|
|
686
|
+
}],
|
|
687
|
+
reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
|
|
688
|
+
worktrees: [],
|
|
689
|
+
skipped: 0,
|
|
690
|
+
failuresHandled: 0
|
|
691
|
+
};
|
|
692
|
+
};
|
|
1216
693
|
// src/run/origin.ts
|
|
1217
694
|
/**
|
|
1218
695
|
* @file Where a run's feedback comes from, beyond what its events say: Ork's
|
|
@@ -1326,10 +803,11 @@ const runNode = async (options) => {
|
|
|
1326
803
|
...options.home === void 0 ? {} : { home: options.home }
|
|
1327
804
|
});
|
|
1328
805
|
const signals = createSignals(feedbackOrigin(path.resolve(options.cwd, options.file)));
|
|
806
|
+
const view = createView();
|
|
1329
807
|
const record = (raw) => {
|
|
1330
808
|
for (const event of signals(raw)) {
|
|
1331
809
|
control.publish(recorder(event));
|
|
1332
|
-
if (event.type === "feedback.recorded" && !options.json) for (const line of formatRunEvent(event, id.length, /* @__PURE__ */ new Map())) options.out(line);
|
|
810
|
+
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);
|
|
1333
811
|
}
|
|
1334
812
|
};
|
|
1335
813
|
const nodeType = workflow.nodes[id]?.type ?? "claude.worker";
|
|
@@ -1374,10 +852,9 @@ const runNode = async (options) => {
|
|
|
1374
852
|
});
|
|
1375
853
|
if (event.type === "session.finished") outcome = event.outcome;
|
|
1376
854
|
if (options.json) options.out(JSON.stringify(event));
|
|
1377
|
-
else for (const line of formatEvent(options.nodeId, event, tools)) options.out(line);
|
|
855
|
+
else for (const line of formatEvent(options.nodeId, event, tools, options.verbose, view)) options.out(line);
|
|
1378
856
|
}
|
|
1379
857
|
const result = resultOf(outcome);
|
|
1380
|
-
const costUsd = result.usage?.costUsd ?? 0;
|
|
1381
858
|
record({
|
|
1382
859
|
type: "node.finished",
|
|
1383
860
|
id,
|
|
@@ -1385,27 +862,11 @@ const runNode = async (options) => {
|
|
|
1385
862
|
ok: result.ok,
|
|
1386
863
|
output: result.output,
|
|
1387
864
|
...result.usage === void 0 ? {} : { usage: result.usage },
|
|
1388
|
-
...result
|
|
865
|
+
...endedFields(result)
|
|
1389
866
|
});
|
|
1390
867
|
record({
|
|
1391
868
|
type: "run.finished",
|
|
1392
|
-
result:
|
|
1393
|
-
runId,
|
|
1394
|
-
status: succeeded(outcome) ? "succeeded" : "failed",
|
|
1395
|
-
costUsd,
|
|
1396
|
-
nodes: [{
|
|
1397
|
-
id,
|
|
1398
|
-
runs: 1,
|
|
1399
|
-
ok: result.ok,
|
|
1400
|
-
output: result.output,
|
|
1401
|
-
costUsd,
|
|
1402
|
-
...result.error === void 0 ? {} : { error: result.error }
|
|
1403
|
-
}],
|
|
1404
|
-
reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
|
|
1405
|
-
worktrees: [],
|
|
1406
|
-
skipped: 0,
|
|
1407
|
-
failuresHandled: 0
|
|
1408
|
-
}
|
|
869
|
+
result: oneNodeResult(runId, id, result, succeeded(outcome))
|
|
1409
870
|
});
|
|
1410
871
|
await control.close();
|
|
1411
872
|
return {
|
|
@@ -1414,6 +875,59 @@ const runNode = async (options) => {
|
|
|
1414
875
|
};
|
|
1415
876
|
};
|
|
1416
877
|
/** Every value of a repeatable flag: `--input a=1 --input=b=2`. */
|
|
878
|
+
// src/run/show-calls.ts
|
|
879
|
+
/**
|
|
880
|
+
* @file A foreground caller's terminal shows the runs it calls (ADR-081): each
|
|
881
|
+
* `call.started` attaches to the called run under its call node, at any
|
|
882
|
+
* depth, and asks its decisions when a person is at the terminal. Once the
|
|
883
|
+
* caller has ended, following stops for a called run that still runs; one the
|
|
884
|
+
* caller stopped gets a moment to end first.
|
|
885
|
+
*/
|
|
886
|
+
/** How long a called run the caller stopped is followed while it stops. */
|
|
887
|
+
const STOP_GRACE_MS = 5e3;
|
|
888
|
+
const running = (store, id) => {
|
|
889
|
+
const [record] = store.lookup(id);
|
|
890
|
+
return record !== void 0 && liveStatus(record) === "running";
|
|
891
|
+
};
|
|
892
|
+
const showCalls = (options) => {
|
|
893
|
+
const { store, showCalls: show } = options;
|
|
894
|
+
const shown = [];
|
|
895
|
+
return {
|
|
896
|
+
/** Attaches to the run a `call.started` of the caller names. */
|
|
897
|
+
onEvent: (event) => {
|
|
898
|
+
if (event.type !== "call.started" || show === void 0 || options.json) return;
|
|
899
|
+
const stop = new AbortController();
|
|
900
|
+
const done = attachRun(store, event.run, options.out, {
|
|
901
|
+
basePath: [event.node],
|
|
902
|
+
signal: stop.signal,
|
|
903
|
+
...show.ask === void 0 ? { hintNoTerminal: true } : { human: show.ask },
|
|
904
|
+
...options.home === void 0 ? {} : { home: options.home }
|
|
905
|
+
}).catch((error) => {
|
|
906
|
+
options.err(`run ${event.run} could not be shown here: ${error instanceof Error ? error.message : String(error)}`);
|
|
907
|
+
});
|
|
908
|
+
shown.push({
|
|
909
|
+
run: event.run,
|
|
910
|
+
stop,
|
|
911
|
+
done
|
|
912
|
+
});
|
|
913
|
+
},
|
|
914
|
+
/**
|
|
915
|
+
* The caller has ended `status`: stops following the called runs still
|
|
916
|
+
* running, at once, or after a moment when the caller stopped them, and
|
|
917
|
+
* names those that still run.
|
|
918
|
+
*/
|
|
919
|
+
end: async (status) => {
|
|
920
|
+
const stopped = status === "interrupted" || status === "timed_out";
|
|
921
|
+
const timers = shown.filter(({ run }) => running(store, run)).map(({ stop }) => setTimeout(() => {
|
|
922
|
+
stop.abort();
|
|
923
|
+
}, stopped ? STOP_GRACE_MS : 0));
|
|
924
|
+
await Promise.all(shown.map(({ done }) => done));
|
|
925
|
+
for (const timer of timers) clearTimeout(timer);
|
|
926
|
+
const say = styler(createView().term);
|
|
927
|
+
for (const { run } of shown.filter(({ run: id }) => running(store, id))) options.out(say("dim", `↳ run ${run} still running · ork attach ${run}`));
|
|
928
|
+
}
|
|
929
|
+
};
|
|
930
|
+
};
|
|
1417
931
|
// src/run/calls.ts
|
|
1418
932
|
/**
|
|
1419
933
|
* @file What a run needs to reach other runs: its port for `workflow.call`
|
|
@@ -1431,6 +945,7 @@ const callPorts = (options) => {
|
|
|
1431
945
|
const trigger = options.trigger ?? options.resume?.settings.trigger;
|
|
1432
946
|
const untrusted = options.untrusted ?? options.resume?.settings.untrusted;
|
|
1433
947
|
const machineClaude = options.machineClaude ?? options.resume?.settings.machineClaude ?? false;
|
|
948
|
+
const shown = showCalls(options);
|
|
1434
949
|
const own = graph.workflow.limits?.budgetUsd;
|
|
1435
950
|
const budgetUsd = cap === void 0 ? own : Math.min(cap, own ?? Number.POSITIVE_INFINITY);
|
|
1436
951
|
return {
|
|
@@ -1448,11 +963,15 @@ const callPorts = (options) => {
|
|
|
1448
963
|
store,
|
|
1449
964
|
runId,
|
|
1450
965
|
place: options.place,
|
|
1451
|
-
emit:
|
|
966
|
+
emit: (event) => {
|
|
967
|
+
options.emit(event);
|
|
968
|
+
shown.onEvent(event);
|
|
969
|
+
},
|
|
1452
970
|
home,
|
|
1453
971
|
adopt: adoptedCalls(store, runId),
|
|
1454
972
|
newId: () => newRunId(store),
|
|
1455
973
|
machineClaude,
|
|
974
|
+
detach: options.detach,
|
|
1456
975
|
env: maskedEnv(maskedNamesOfRun(graph.workflow, options.place, store.projects())),
|
|
1457
976
|
resumeSession: (run, node, body, signal) => {
|
|
1458
977
|
const { blocked, leftUsd } = runLimits(store, options.place, home).now();
|
|
@@ -1470,6 +989,7 @@ const callPorts = (options) => {
|
|
|
1470
989
|
}) : Promise.resolve(`${NOT_STARTED}${blocked}.`);
|
|
1471
990
|
}
|
|
1472
991
|
}),
|
|
992
|
+
shown,
|
|
1473
993
|
...linksOff ? {} : { relay: createRelay({
|
|
1474
994
|
store,
|
|
1475
995
|
runId,
|
|
@@ -1693,6 +1213,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1693
1213
|
}, runDir) : appendRecorder(options.store, runId, runDir);
|
|
1694
1214
|
const width = Math.max(...Object.keys(workflow.nodes).map((id) => id.length));
|
|
1695
1215
|
const tools = /* @__PURE__ */ new Map();
|
|
1216
|
+
const view = createView();
|
|
1696
1217
|
const started = Promise.withResolvers();
|
|
1697
1218
|
const live = {};
|
|
1698
1219
|
const control = await startControl({
|
|
@@ -1712,7 +1233,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1712
1233
|
for (const event of signals(raw)) {
|
|
1713
1234
|
control.publish(record(event));
|
|
1714
1235
|
if (options.json) options.out(JSON.stringify(event));
|
|
1715
|
-
else for (const line of formatRunEvent(event, width, tools, options.human !== void 0)) options.out(line);
|
|
1236
|
+
else for (const line of formatRunEvent(event, width, tools, options.human !== void 0, options.verbose, view)) options.out(line);
|
|
1716
1237
|
}
|
|
1717
1238
|
};
|
|
1718
1239
|
const workspace = root === void 0 ? void 0 : createWorkspace({
|
|
@@ -1779,12 +1300,17 @@ const runWorkflowFile = async (options) => {
|
|
|
1779
1300
|
...workspace === void 0 ? {} : { workspace },
|
|
1780
1301
|
scrub: runScrubber(maskedNames, process.env),
|
|
1781
1302
|
limits: ready.limits,
|
|
1303
|
+
machine: {
|
|
1304
|
+
rules: machineRulesOf(loadProject(place.root, placeFiles(place).rules).project),
|
|
1305
|
+
ledger: machineSlotLedger(options.home === void 0 ? {} : { home: options.home })
|
|
1306
|
+
},
|
|
1782
1307
|
beforeFinish: takeLastCommands(control, started.promise)
|
|
1783
1308
|
});
|
|
1784
1309
|
live.handle = handle;
|
|
1785
1310
|
started.resolve(handle);
|
|
1786
1311
|
options.onRun?.(handle);
|
|
1787
1312
|
const result = await handle.done;
|
|
1313
|
+
await ports.shown.end(result.status);
|
|
1788
1314
|
clearOutputFiles(runDir, options.err);
|
|
1789
1315
|
if (result.status === "succeeded") clearStateFiles(runDir, options.err);
|
|
1790
1316
|
await endLinksOf(options.store, runId, result.status, options.home);
|
|
@@ -1813,7 +1339,7 @@ const isDriver = (value) => AGENT_DRIVERS.some((d) => d.id === value);
|
|
|
1813
1339
|
/** Why `--driver` refuses a value: a retired id names its replacement. */
|
|
1814
1340
|
const badDriver = (value) => `--${retiredDriverHint(value) ?? `driver must be one of ${AGENT_DRIVERS.map((d) => d.id).join(", ")}, got \`${value}\``}`;
|
|
1815
1341
|
/** Loads the drivers only when a run starts, so other commands stay fast. */
|
|
1816
|
-
const loadRuntime = async (driver, runs) => (await import("./src-
|
|
1342
|
+
const loadRuntime = async (driver, runs) => (await import("./src-D1QedD5y.mjs")).createRuntime(driver, sessionEnv(process.env), runs);
|
|
1817
1343
|
/**
|
|
1818
1344
|
* Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
|
|
1819
1345
|
* so that the run ends tidily and leaves no result unscrubbed.
|
|
@@ -1833,7 +1359,7 @@ const onInterrupt = (stop) => {
|
|
|
1833
1359
|
/** The terminal that answers a run's questions; none in a detached run. */
|
|
1834
1360
|
const terminalHuman = () => {
|
|
1835
1361
|
const detached = process.env[DETACHED_ENV] === "1";
|
|
1836
|
-
if (detached) writeErr(style("dim", "detached run: no terminal asks; answer with ork decide"));
|
|
1362
|
+
if (detached) writeErr(style("dim", "detached run: no terminal asks; answer with ork attach or ork decide"));
|
|
1837
1363
|
return detached ? void 0 : createTerminalHuman({
|
|
1838
1364
|
input: process.stdin,
|
|
1839
1365
|
output: process.stderr,
|
|
@@ -1886,6 +1412,11 @@ const run = defineCommand({
|
|
|
1886
1412
|
description: "Print the events as JSON lines",
|
|
1887
1413
|
default: false
|
|
1888
1414
|
},
|
|
1415
|
+
verbose: {
|
|
1416
|
+
type: "boolean",
|
|
1417
|
+
description: "Show each tool call whole, and why each failed node stopped, with its scrubbed details",
|
|
1418
|
+
default: false
|
|
1419
|
+
},
|
|
1889
1420
|
detach: {
|
|
1890
1421
|
type: "boolean",
|
|
1891
1422
|
description: "Run in the background; follow it with ork logs -f, answer with ork decide",
|
|
@@ -1948,7 +1479,7 @@ const run = defineCommand({
|
|
|
1948
1479
|
});
|
|
1949
1480
|
if (detached.ok) {
|
|
1950
1481
|
write(`▸ run ${detached.runId} started in the background`);
|
|
1951
|
-
write(style("dim", `
|
|
1482
|
+
write(style("dim", ` ${detachHint(detached.runId)}`));
|
|
1952
1483
|
} else {
|
|
1953
1484
|
writeErr(detached.error);
|
|
1954
1485
|
writeErr(style("dim", `(output: ${detached.log})`));
|
|
@@ -1967,6 +1498,7 @@ const run = defineCommand({
|
|
|
1967
1498
|
inPlace: args["in-place"],
|
|
1968
1499
|
...args["machine-claude"] ? { machineClaude: true } : {},
|
|
1969
1500
|
json: args.json,
|
|
1501
|
+
verbose: args.verbose,
|
|
1970
1502
|
runtime: loadRuntime,
|
|
1971
1503
|
store,
|
|
1972
1504
|
...human === void 0 ? {} : { human },
|
|
@@ -1987,6 +1519,7 @@ const run = defineCommand({
|
|
|
1987
1519
|
trigger: args.trigger,
|
|
1988
1520
|
untrusted: flagValues(rawArgs, "--untrusted")
|
|
1989
1521
|
},
|
|
1522
|
+
...human === void 0 ? {} : { showCalls: callsShown(human, process.stdin.isTTY) },
|
|
1990
1523
|
onRun: (handle) => {
|
|
1991
1524
|
onInterrupt(handle.interrupt);
|
|
1992
1525
|
}
|
|
@@ -2012,22 +1545,25 @@ const run = defineCommand({
|
|
|
2012
1545
|
* each message at its session's next step, and its replies stream here; a
|
|
2013
1546
|
* node of a finished run is answered by resuming its last session.
|
|
2014
1547
|
*/
|
|
2015
|
-
/**
|
|
2016
|
-
|
|
2017
|
-
|
|
1548
|
+
/**
|
|
1549
|
+
* Asks for one line; `null` once the input ends (Ctrl-D), Esc leaves, or
|
|
1550
|
+
* `signal` withdraws it. A live chat prints replies while it waits, which a
|
|
1551
|
+
* redrawn prompt can't share the screen with, so it asks line by line.
|
|
1552
|
+
*/
|
|
1553
|
+
const askLine = async (live = false, signal) => {
|
|
1554
|
+
const asked = await readText({
|
|
2018
1555
|
input: process.stdin,
|
|
2019
|
-
output: process.stdout
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
}
|
|
1556
|
+
output: process.stdout,
|
|
1557
|
+
...live ? { env: {
|
|
1558
|
+
...process.env,
|
|
1559
|
+
ORK_PLAIN: "1"
|
|
1560
|
+
} } : {}
|
|
1561
|
+
}, {
|
|
1562
|
+
message: "you",
|
|
1563
|
+
done: (value) => ` you › ${value ?? ""}`,
|
|
1564
|
+
...signal === void 0 ? {} : { signal }
|
|
1565
|
+
}).catch(() => ({ kind: "cancelled" }));
|
|
1566
|
+
return asked.kind === "text" ? asked.value : null;
|
|
2031
1567
|
};
|
|
2032
1568
|
/** Chats with a node of a live run until the input ends or the run does. */
|
|
2033
1569
|
const chatLive = async (store, run, node, message) => {
|
|
@@ -2049,7 +1585,7 @@ const chatLive = async (store, run, node, message) => {
|
|
|
2049
1585
|
abort.abort();
|
|
2050
1586
|
});
|
|
2051
1587
|
for (;;) {
|
|
2052
|
-
const line = await Promise.race([askLine(), following.then(() => null)]);
|
|
1588
|
+
const line = await Promise.race([askLine(true, abort.signal), following.then(() => null)]);
|
|
2053
1589
|
if (line === null || abort.signal.aborted) break;
|
|
2054
1590
|
if (line.trim() !== "") await tellLiveRun(store, run, node, line);
|
|
2055
1591
|
}
|
|
@@ -2145,6 +1681,27 @@ const chat = defineCommand({
|
|
|
2145
1681
|
}
|
|
2146
1682
|
}
|
|
2147
1683
|
});
|
|
1684
|
+
// src/commands/decide-list.ts
|
|
1685
|
+
/**
|
|
1686
|
+
* One line per open decision, for `--list`: on a terminal an ASK chip and the
|
|
1687
|
+
* node's chip with the id and the question after, cut to the screen; plain, the
|
|
1688
|
+
* id, the node and the question.
|
|
1689
|
+
*/
|
|
1690
|
+
const formatDecisions = (decisions, workflow, t = termOf(process.stdout)) => {
|
|
1691
|
+
if (decisions.length === 0) return [workflow === void 0 ? "No open decisions." : `No open decisions in workflow ${workflow}.`];
|
|
1692
|
+
const idWidth = Math.max(...decisions.map((d) => d.id.length));
|
|
1693
|
+
const nodeWidth = Math.max(...decisions.map((d) => plain(d.node).length));
|
|
1694
|
+
return decisions.map((d) => {
|
|
1695
|
+
const node = plain(d.node);
|
|
1696
|
+
const options = `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`;
|
|
1697
|
+
const document = d.document === void 0 ? "" : " +document";
|
|
1698
|
+
const via = d.owner === void 0 ? "" : ` via ${plain(d.owner)}`;
|
|
1699
|
+
if (!t.rich) return `${d.id} ${node}${via} ${plain(d.question)}${document} ${options}`;
|
|
1700
|
+
const lead = `${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, node)}${" ".repeat(nodeWidth - node.length)} ${bold(t, d.id.padEnd(idWidth))} `;
|
|
1701
|
+
const room = Math.max(20, t.width - (7 + nodeWidth + 2 + idWidth + 2));
|
|
1702
|
+
return `${lead}${cut(`${plain(d.question)}${document} ${options}${via}`, room)}`;
|
|
1703
|
+
});
|
|
1704
|
+
};
|
|
2148
1705
|
// src/commands/decide.ts
|
|
2149
1706
|
/**
|
|
2150
1707
|
* @file `ork decide`: answer a live run's open decisions from any terminal (it
|
|
@@ -2190,8 +1747,6 @@ const saidLine = (store, id) => {
|
|
|
2190
1747
|
const note = store.decision(id)?.note;
|
|
2191
1748
|
return note === void 0 ? "" : style("dim", ` · said: ${note}`);
|
|
2192
1749
|
};
|
|
2193
|
-
/** One line per open decision, for `--list`. */
|
|
2194
|
-
const formatDecisions = (decisions, workflow) => decisions.length === 0 ? [workflow === void 0 ? "No open decisions." : `No open decisions in workflow ${workflow}.`] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)}${d.owner === void 0 ? "" : style("dim", ` via ${d.owner}`)} ${d.question} ${d.document === void 0 ? "" : `${style("dim", "+document")} `}${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
|
|
2195
1750
|
const decide = defineCommand({
|
|
2196
1751
|
meta: {
|
|
2197
1752
|
name: "decide",
|
|
@@ -2246,6 +1801,11 @@ const decide = defineCommand({
|
|
|
2246
1801
|
rating: {
|
|
2247
1802
|
type: "string",
|
|
2248
1803
|
description: "…rating it good or bad"
|
|
1804
|
+
},
|
|
1805
|
+
verbose: {
|
|
1806
|
+
type: "boolean",
|
|
1807
|
+
description: "Show each question's whole context, not its first lines",
|
|
1808
|
+
default: false
|
|
2249
1809
|
}
|
|
2250
1810
|
},
|
|
2251
1811
|
run: async ({ args }) => {
|
|
@@ -2258,7 +1818,8 @@ const decide = defineCommand({
|
|
|
2258
1818
|
const human = createTerminalHuman({
|
|
2259
1819
|
input: process.stdin,
|
|
2260
1820
|
output: process.stdout,
|
|
2261
|
-
interactive: process.stdin.isTTY
|
|
1821
|
+
interactive: process.stdin.isTTY,
|
|
1822
|
+
verbose: args.verbose
|
|
2262
1823
|
});
|
|
2263
1824
|
try {
|
|
2264
1825
|
if (args.document) {
|
|
@@ -2298,7 +1859,7 @@ const decide = defineCommand({
|
|
|
2298
1859
|
return;
|
|
2299
1860
|
}
|
|
2300
1861
|
if (args.say !== void 0) {
|
|
2301
|
-
failed$2("--say goes with --id and --choose; answering here
|
|
1862
|
+
failed$2("--say goes with --id and --choose; answering here, press n to add a note (plain: it asks for one after each choice).");
|
|
2302
1863
|
return;
|
|
2303
1864
|
}
|
|
2304
1865
|
const filter = {};
|
|
@@ -2320,23 +1881,19 @@ const decide = defineCommand({
|
|
|
2320
1881
|
for (const line of formatDecisions(open, args.workflow)) write(line);
|
|
2321
1882
|
return;
|
|
2322
1883
|
}
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
const
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
};
|
|
2337
|
-
const choice = await human.ask(question);
|
|
2338
|
-
const answered = await answerDecision(store, d.id, choice, void 0, "decide", note);
|
|
2339
|
-
write(answered.ok ? `${style("green", "✓")} ${d.id}: ${choice}${saidLine(store, d.id)}\n` : `${style("yellow", "!")} ${answered.error}\n`);
|
|
1884
|
+
const collapses = canKey(process.stdin, process.stdout);
|
|
1885
|
+
for (const [index, d] of open.entries()) {
|
|
1886
|
+
const asked = await askStored(store, human, d, {
|
|
1887
|
+
by: "decide",
|
|
1888
|
+
place: {
|
|
1889
|
+
run: d.runId,
|
|
1890
|
+
decision: d.id,
|
|
1891
|
+
index: index + 1,
|
|
1892
|
+
total: open.length
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
if (asked.kind === "refused") write(`${style("yellow", "!")} ${asked.message}\n`);
|
|
1896
|
+
else if (asked.kind === "answered" && !collapses) write(`${style("green", "✓")} ${d.id}: ${asked.choice}${saidLine(store, d.id)}\n`);
|
|
2340
1897
|
}
|
|
2341
1898
|
} finally {
|
|
2342
1899
|
human?.close();
|
|
@@ -2395,7 +1952,7 @@ const stop = defineCommand({
|
|
|
2395
1952
|
write(`${style("green", "✓")} run ${run.id} ${liveStatus(now)}`);
|
|
2396
1953
|
return;
|
|
2397
1954
|
}
|
|
2398
|
-
await setTimeout(100);
|
|
1955
|
+
await setTimeout$1(100);
|
|
2399
1956
|
}
|
|
2400
1957
|
write(`Asked run ${run.id} to stop; it is still finishing its running nodes (see ork logs ${run.id} -f).`);
|
|
2401
1958
|
} finally {
|
|
@@ -2496,6 +2053,46 @@ const resume = defineCommand({
|
|
|
2496
2053
|
}
|
|
2497
2054
|
}
|
|
2498
2055
|
});
|
|
2056
|
+
// src/doctor/format.ts
|
|
2057
|
+
/** Marks are made when used, so `--plain` (set after load) is read. */
|
|
2058
|
+
const MARK = {
|
|
2059
|
+
ok: () => style("green", "✓"),
|
|
2060
|
+
warn: () => style("yellow", "!"),
|
|
2061
|
+
fail: () => style("red", "✗"),
|
|
2062
|
+
skip: () => style("dim", "-")
|
|
2063
|
+
};
|
|
2064
|
+
const CHECK_CHIP = {
|
|
2065
|
+
ok: {
|
|
2066
|
+
label: CHIP_LABELS.ok,
|
|
2067
|
+
role: "ok"
|
|
2068
|
+
},
|
|
2069
|
+
warn: {
|
|
2070
|
+
label: CHIP_LABELS.warn,
|
|
2071
|
+
role: "warn"
|
|
2072
|
+
},
|
|
2073
|
+
fail: {
|
|
2074
|
+
label: CHIP_LABELS.fail,
|
|
2075
|
+
role: "danger"
|
|
2076
|
+
},
|
|
2077
|
+
skip: {
|
|
2078
|
+
label: CHIP_LABELS.skip,
|
|
2079
|
+
role: "muted"
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
2082
|
+
/**
|
|
2083
|
+
* A check's lines. Rich: a chip (its words padded to line the details up), the
|
|
2084
|
+
* detail wrapped under its column, the hint dimmed and indented; plain: a mark
|
|
2085
|
+
* and the lines as they always were.
|
|
2086
|
+
*/
|
|
2087
|
+
const formatCheck = (result, term = termOf(process.stdout)) => {
|
|
2088
|
+
if (!term.rich) {
|
|
2089
|
+
const line = `${MARK[result.status]()} ${result.name.padEnd(20)} ${result.detail}`;
|
|
2090
|
+
return result.hint === void 0 ? [line] : [line, ` ${style("dim", result.hint)}`];
|
|
2091
|
+
}
|
|
2092
|
+
const { label, role } = CHECK_CHIP[result.status];
|
|
2093
|
+
const lead = `${chip(term, label.padEnd(4), role)} ${result.name.padEnd(20)} `;
|
|
2094
|
+
return [...hang(lead, result.detail, term.width), ...result.hint === void 0 ? [] : wrap(result.hint, term.width - 2).map((line) => ` ${paint(term, "muted", line)}`)];
|
|
2095
|
+
};
|
|
2499
2096
|
// src/doctor/lease-checks.ts
|
|
2500
2097
|
/**
|
|
2501
2098
|
* @file What `ork doctor` says of Command Code leases (RFC-014): it removes the
|
|
@@ -2508,7 +2105,7 @@ const resume = defineCommand({
|
|
|
2508
2105
|
* folder. Says nothing when there were none. `bin` is the `cmd` program.
|
|
2509
2106
|
*/
|
|
2510
2107
|
const leaseChecks = async (place, env = process.env, bin = "cmd") => {
|
|
2511
|
-
const { cleanStaleLeases, leaseLedger } = await import("./src-
|
|
2108
|
+
const { cleanStaleLeases, leaseLedger } = await import("./src-D1QedD5y.mjs");
|
|
2512
2109
|
const { cleaned, failed } = await cleanStaleLeases({
|
|
2513
2110
|
bin,
|
|
2514
2111
|
ledger: leaseLedger(placeFiles(place).runs),
|
|
@@ -2537,7 +2134,7 @@ const ORK_SKILLS = {
|
|
|
2537
2134
|
principles: "---\nname: principles\ndescription: Use when deciding how to split work into an Ork workflow - which steps are programs, which are Claude sessions, where a person decides - so it costs less than doing it by hand and can't go wrong quietly.\n---\n\n# How Ork splits work\n\nOrk exists to do long jobs for less than doing them by hand, and safely. Four\nrules decide the shape of a workflow.\n\n## 1. A program for what a program can do\n\nA model turn is for judgment. Anything a program can decide, it should: run the\ntests, read a pull request's state, count what changed, wait for CI.\n\n- **A tool node** (`tool.shell`, `tool.git`, `tool.gh`, `tool.aws`) or an\n **`mcp.call`** does it with no session. With `output: { format: json }`, its\n answer routes the run: `when: \"json.state == 'OPEN'\"`.\n- **`verify:`** on a session runs a check before its report counts, so a session\n can't claim success past failing tests. **`before:`** hands it what a program\n found, so it doesn't spend turns finding it.\n- **`watch:`** waits on CI or a review with a program that looks every few\n seconds; hours of waiting cost nothing.\n- **`notify:`** lets a program tell a running session what it found, with no\n model turn to carry the news.\n\n## 2. Spend where it pays\n\n- A small model and low effort for a narrow task (`model: haiku`,\n `effort: low`); the large ones where thinking pays.\n- `budgetUsd` on each session and on the run (`limits.budgetUsd`). A loop of\n sessions without a budget or `limits.timeout` gets a warning, for good reason.\n- Give a session what it needs in its `prompt` and `before:`, rather than\n letting it search; and `isolation: readonly` for a reviewer that only reads.\n\n## 3. Make a mistake impossible before checking for it\n\n- Declare what a node says: `output.schema` lists on tool JSON and Claude\n reports (`verdict: [approve, escalate]`). Conditions then compare against a\n closed list, and a run fails a value off it rather than matching nothing.\n- A verdict is a declared field, never words to search a summary for.\n- Give each condition its opposite, and each failure a handler (`when: \"!ok\"`).\n- Check every draft with Ork's `check` tool until it's clean.\n\n## 4. A person decides what can't be undone\n\nMerging, deploying, publishing, spending a lot: put a `human.gate` before it,\nwith the safe option `recommended`. For a run nobody watches, give it a\n`timeout` and the safe `default`. You never answer a gate yourself.\n\n## An example\n\nA review that a program checks first, and a person signs off:\n\n```yaml\nschemaVersion: 1\nname: careful-review\ndescription: Lint with a program, review with a small model, and ask before merging.\ninputs:\n pr:\n type: string\n required: true\nlimits:\n budgetUsd: 3\nnodes:\n lint:\n type: tool.shell\n command: pnpm lint\n allow: [pnpm lint]\n review:\n type: claude.worker\n prompt: \"Review pull request {{inputs.pr}}. The linter passed.\"\n model: haiku\n effort: low\n isolation: readonly\n budgetUsd: 1\n output:\n schema:\n verdict: [approve, escalate]\n ask:\n type: human.gate\n question: \"Merge pull request {{inputs.pr}}? The review says {{nodes.review.output.verdict}}.\"\n options: [merge, wait]\n recommended: wait\n merge:\n type: tool.gh\n command: pr merge {{inputs.pr}} --squash\n allow: [pr merge]\nedges:\n - lint -> review\n - review -> ask\n - from: ask\n to: merge\n when: \"choice == 'merge'\"\n```\n\n`lint` costs nothing, `review` is a small model with a cap, and nothing merges\nwithout a person.\n",
|
|
2538
2135
|
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",
|
|
2539
2136
|
setup: "---\nname: setup\ndescription: Use when the person asks you to set up Ork, install it, or get started with it in a repository - checking it's installed, setting up the project, making Ork's tools available to you, and a first workflow.\n---\n\n# Setting up Ork\n\nOrk runs workflows on the person's machine with their own accounts. Setting it\nup changes their machine and their repository, so say what each step does and\nask before it. Work through these in order, and stop where they say no.\n\n## 1. Check\n\n```sh\nork --version\nork doctor\n```\n\n- **No `ork`:** it needs Node 24.20 or later (`node --version`). The install is\n `npm install --global @krapht/ork`; show it and run it only when they say\n yes. The guide:\n https://github.com/mimshins/ork/blob/main/docs/getting-started/installation.md\n- **Read `ork doctor` to them.** It needs git, a git repository with a commit,\n and one agent driver. Don't run `git init` or a commit without asking.\n- **A driver not ready:** they log in themselves, with `claude auth login`, or\n by setting `ANTHROPIC_API_KEY` or a cloud provider in their own shell (either\n makes the claude-code driver ready). Never ask for a key, never take one pasted to you, and\n never write one into a file or a command. `gh` and `aws` are optional, and\n their logins are their own too (`gh auth login`, `aws sso login`).\n\n## 2. The project\n\n- **`ork init`** keeps Ork's files in the repository, in `.ork/`, committed with\n the code. It adds `.ork/runs/` to `.gitignore`, links `.claude/` and similar\n folders to `.ork/`, and writes a starter workflow, `hello`.\n- **`ork register`** keeps them beside it, in `~/.ork/projects/<id>/`, and\n writes nothing into the repository. For a repository whose process isn't\n theirs to change, or to try Ork first.\n\nExplain both and ask which. Skip the question when the repository decides it: a\n`.ork/` folder means `ork init` has run (`ork projects` lists what's\nregistered). Neither overwrites a file.\n\n## 3. Make Ork available to you\n\nIf Ork's tools (`workflows`, `check`) already answer you, skip this. Otherwise,\nby where you run, and asking before each change to a config:\n\n- **Claude Code:** Ork's plugin brings the skills and the tools. The person\n types these themselves:\n\n ```\n /plugin marketplace add mimshins/ork\n /plugin install ork@ork\n ```\n\n Or, in a project set up with `ork init`, the skills in the repository for\n everyone who clones it, then the tools:\n\n ```sh\n ork init --skills\n claude mcp add ork -- ork mcp\n ```\n\n New tools start when they restart Claude Code or reconnect `ork` in `/mcp`;\n so does the plugin's, once `ork` is installed.\n\n- **Another MCP client:** add a server named `ork` with command `ork` and args\n `[\"mcp\"]` to its MCP config, where its own docs say. Its tools find the\n project by its id, from `ork projects`.\n- **A shell only:** use the CLI. `ork --help` and each command's `--help` say\n what it takes.\n\n## 4. A first workflow\n\nOffer to write one for a task they name. Follow `/ork:write`, check each draft\n(the `check` tool, or `ork validate <file>`) until it has no error, then save\nit. Don't start it: a run spends their money. Start it only when they say so,\nin the background with its inputs (`start_run`, or\n`ork run <name> --detach --input name=value`), and follow it with `/ork:run`.\n\n## 5. Show them `ork ui`\n\nTell them to run `ork ui` in their own terminal: their runs, graphs and logs in\nthe browser, and where they answer what a run asks.\n\n## Never\n\n- Ask for, store or pass on a password, key or token.\n- Answer a run's decision: they answer in `ork ui` or with `ork decide` in\n their own terminal.\n- Install, edit a config, or start a run without a yes.\n",
|
|
2540
|
-
write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\nA worker, tool node, `mcp.call` or `workflow.call` can run once per entry of a\nlist: see [One node per item](#one-node-per-item).\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## One node per item\n\n`forEach` runs a worker, a tool node, an `mcp.call` or a `workflow.call` once\nper entry of a list, with no model turn to fan out or gather. Not a leader, a\ngate, a subagent or a control node.\n\n```yaml\nplan:\n type: claude.worker\n prompt: \"Split this into at most 6 independent tasks: {{inputs.goal}}\"\n isolation: readonly\n output:\n schema:\n tasks:\n list: { title: string, detail: string } # declare the list and its shape\nbuild:\n type: claude.worker\n prompt: \"Task {{index}}: {{item.title}}. {{item.detail}}\"\n isolation: worktree\n forEach:\n from: \"{{nodes.plan.output.tasks}}\" # exactly one path to a declared list\n maxItems: 6 # required with `from`: the cap on sessions started\n maxParallel: 3\n```\n\n- `forEach: { items: [a, b] }` takes a list written in the file instead.\n- `{{item}}` and `{{index}}` work only in a node with `forEach`, in `prompt`,\n `command`, `arguments`, `inputs`, `env` and `ref`. A program also gets
|
|
2137
|
+
write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\nA worker, tool node, `mcp.call` or `workflow.call` can run once per entry of a\nlist: see [One node per item](#one-node-per-item).\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## One node per item\n\n`forEach` runs a worker, a tool node, an `mcp.call` or a `workflow.call` once\nper entry of a list, with no model turn to fan out or gather. Not a leader, a\ngate, a subagent or a control node.\n\n```yaml\nplan:\n type: claude.worker\n prompt: \"Split this into at most 6 independent tasks: {{inputs.goal}}\"\n isolation: readonly\n output:\n schema:\n tasks:\n list: { title: string, detail: string } # declare the list and its shape\nbuild:\n type: claude.worker\n prompt: \"Task {{index}}: {{item.title}}. {{item.detail}}\"\n isolation: worktree\n forEach:\n from: \"{{nodes.plan.output.tasks}}\" # exactly one path to a declared list\n maxItems: 6 # required with `from`: the cap on sessions started\n maxParallel: 3\n```\n\n- `forEach: { items: [a, b] }` takes a list written in the file instead.\n- `{{item}}` and `{{index}}` work only in a node with `forEach`, in `prompt`,\n `command`, `arguments`, `inputs`, `env` and `ref`, and `{{item.<field>}}` in a\n Claude 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"
|
|
2541
2138
|
};
|
|
2542
2139
|
// src/project/init-skills.ts
|
|
2543
2140
|
/**
|
|
@@ -2637,16 +2234,17 @@ const DESCRIBE = {
|
|
|
2637
2234
|
folder: (link) => `${link.path} is a real folder, left as it is`
|
|
2638
2235
|
};
|
|
2639
2236
|
const describe = (link) => DESCRIBE[link.state](link);
|
|
2237
|
+
/** Labels are made when used, so `--plain` (set after load) is read. */
|
|
2640
2238
|
const LABEL = {
|
|
2641
|
-
linked: style("dim", "ok "),
|
|
2642
|
-
missing: style("yellow", "missing"),
|
|
2643
|
-
broken: style("red", "broken "),
|
|
2644
|
-
elsewhere: style("yellow", "kept "),
|
|
2645
|
-
folder: style("yellow", "kept ")
|
|
2239
|
+
linked: () => style("dim", "ok "),
|
|
2240
|
+
missing: () => style("yellow", "missing"),
|
|
2241
|
+
broken: () => style("red", "broken "),
|
|
2242
|
+
elsewhere: () => style("yellow", "kept "),
|
|
2243
|
+
folder: () => style("yellow", "kept ")
|
|
2646
2244
|
};
|
|
2647
2245
|
/** One line per folder; `created` marks the links this call made. */
|
|
2648
2246
|
const formatLinks = (links, created = []) => links.map((link) => {
|
|
2649
|
-
return `${created.includes(link.path) ? style("green", "linked ") : LABEL[link.state]} ${describe(link)}`;
|
|
2247
|
+
return `${created.includes(link.path) ? style("green", "linked ") : LABEL[link.state]()} ${describe(link)}`;
|
|
2650
2248
|
});
|
|
2651
2249
|
const checkOf = (tool, links) => {
|
|
2652
2250
|
const name = `${AGENT_TOOL_LAYOUTS.find((l) => l.id === tool)?.name ?? tool} links`;
|
|
@@ -2698,16 +2296,6 @@ const ORK_META = {
|
|
|
2698
2296
|
version: ORK_VERSION,
|
|
2699
2297
|
description: "Orchestrate Claude agents on your machine"
|
|
2700
2298
|
};
|
|
2701
|
-
const MARK = {
|
|
2702
|
-
ok: style("green", "✓"),
|
|
2703
|
-
warn: style("yellow", "!"),
|
|
2704
|
-
fail: style("red", "✗"),
|
|
2705
|
-
skip: style("dim", "-")
|
|
2706
|
-
};
|
|
2707
|
-
const formatCheck = (result) => {
|
|
2708
|
-
const line = `${MARK[result.status]} ${result.name.padEnd(20)} ${result.detail}`;
|
|
2709
|
-
return result.hint === void 0 ? [line] : [line, ` ${style("dim", result.hint)}`];
|
|
2710
|
-
};
|
|
2711
2299
|
const doctor = defineCommand({
|
|
2712
2300
|
meta: {
|
|
2713
2301
|
name: "doctor",
|
|
@@ -2765,7 +2353,7 @@ const agreed = (yes, command) => {
|
|
|
2765
2353
|
write(`Run it yourself: ${command}\nor run ork upgrade --yes.`);
|
|
2766
2354
|
return Promise.resolve(false);
|
|
2767
2355
|
}
|
|
2768
|
-
return confirm(`Run ${
|
|
2356
|
+
return confirm(`Run ${command}?`);
|
|
2769
2357
|
};
|
|
2770
2358
|
const upgrade = defineCommand({
|
|
2771
2359
|
meta: {
|
|
@@ -3146,10 +2734,25 @@ const relocateProject = (store, id, target) => {
|
|
|
3146
2734
|
created: false
|
|
3147
2735
|
};
|
|
3148
2736
|
};
|
|
3149
|
-
/**
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
2737
|
+
/**
|
|
2738
|
+
* One project's lines for `ork projects`; rich, its mode and a missing clone
|
|
2739
|
+
* are chips, its id is padded to `idWidth` and its root is cut to the screen.
|
|
2740
|
+
*/
|
|
2741
|
+
const formatProject = (project, term = termOf(process.stdout), idWidth = 0) => {
|
|
2742
|
+
const missing = !existsSync(project.root);
|
|
2743
|
+
if (!term.rich) {
|
|
2744
|
+
const gone = missing ? style("yellow", " (missing)") : "";
|
|
2745
|
+
return [`${style("bold", project.id)} ${style("dim", project.mode)} ${project.root}${gone}`, style("dim", ` files: ${shownPath(project.dir, project.root)}`)];
|
|
2746
|
+
}
|
|
2747
|
+
const mode = chip(term, project.mode.toUpperCase(), "muted");
|
|
2748
|
+
const gone = missing ? ` ${chip(term, "MISSING", "warn")}` : "";
|
|
2749
|
+
const room = term.width - (idWidth + 2 + project.mode.length + 2 + 2) - (missing ? 10 : 0);
|
|
2750
|
+
return [`${style("bold", project.id.padEnd(idWidth))} ${mode} ${cut(project.root, Math.max(8, room))}${gone}`, style("dim", cut(` files: ${shownPath(project.dir, project.root)}`, term.width))];
|
|
2751
|
+
};
|
|
2752
|
+
/** Every project's lines, their ids aligned when rich. */
|
|
2753
|
+
const formatProjects = (projects, term = termOf(process.stdout)) => {
|
|
2754
|
+
const idWidth = term.rich ? Math.max(0, ...projects.map((p) => p.id.length)) : 0;
|
|
2755
|
+
return projects.flatMap((p) => formatProject(p, term, idWidth));
|
|
3153
2756
|
};
|
|
3154
2757
|
/** What `ork register` prints: the project, and where its files go. */
|
|
3155
2758
|
const formatRegistered = (project, created) => {
|
|
@@ -3378,7 +2981,7 @@ const migrateHere = async (cwd, options) => {
|
|
|
3378
2981
|
fail("Pass --yes to migrate without a terminal.");
|
|
3379
2982
|
return;
|
|
3380
2983
|
}
|
|
3381
|
-
if (!options.yes && !await confirm("Go ahead?
|
|
2984
|
+
if (!options.yes && !await confirm("Go ahead?")) return;
|
|
3382
2985
|
applyMigration(plan, store);
|
|
3383
2986
|
write(`${style("green", "✓")} migrated. ${afterMigration(plan)}`);
|
|
3384
2987
|
} finally {
|
|
@@ -3392,7 +2995,7 @@ const migrateHere = async (cwd, options) => {
|
|
|
3392
2995
|
* place after the clone was moved or renamed, and its runs follow.
|
|
3393
2996
|
*/
|
|
3394
2997
|
/** Runs `use` with the store open, and closes it. */
|
|
3395
|
-
const withStore
|
|
2998
|
+
const withStore = (use) => {
|
|
3396
2999
|
const store = openStore();
|
|
3397
3000
|
try {
|
|
3398
3001
|
return use(store);
|
|
@@ -3412,7 +3015,7 @@ const forget = defineCommand({
|
|
|
3412
3015
|
},
|
|
3413
3016
|
args: { id },
|
|
3414
3017
|
run: ({ args }) => {
|
|
3415
|
-
if (withStore
|
|
3018
|
+
if (withStore((store) => store.forgetProject(args.id))) {
|
|
3416
3019
|
write(`${style("green", "forgot")} ${args.id} (its files are kept)`);
|
|
3417
3020
|
return;
|
|
3418
3021
|
}
|
|
@@ -3434,7 +3037,7 @@ const move = defineCommand({
|
|
|
3434
3037
|
}
|
|
3435
3038
|
},
|
|
3436
3039
|
run: ({ args }) => {
|
|
3437
|
-
const result = withStore
|
|
3040
|
+
const result = withStore((store) => relocateProject(store, args.id, args.path));
|
|
3438
3041
|
if (result.ok) {
|
|
3439
3042
|
write(`${style("green", "moved")} ${args.id} → ${result.project.root}`);
|
|
3440
3043
|
return;
|
|
@@ -3475,7 +3078,7 @@ const register = defineCommand({
|
|
|
3475
3078
|
json: json$2
|
|
3476
3079
|
},
|
|
3477
3080
|
run: ({ args }) => {
|
|
3478
|
-
const result = withStore
|
|
3081
|
+
const result = withStore((store) => registerProject(store, args.path ?? process.cwd(), args.id === void 0 ? {} : { id: args.id }));
|
|
3479
3082
|
if (!result.ok) {
|
|
3480
3083
|
writeErr(result.error);
|
|
3481
3084
|
process.exitCode = 1;
|
|
@@ -3495,10 +3098,10 @@ const projects = defineCommand({
|
|
|
3495
3098
|
},
|
|
3496
3099
|
run: ({ args, rawArgs }) => {
|
|
3497
3100
|
if (rawArgs[0] === "forget" || rawArgs[0] === "move") return;
|
|
3498
|
-
const all = withStore
|
|
3101
|
+
const all = withStore((store) => store.projects());
|
|
3499
3102
|
if (args.json) write(JSON.stringify(all, void 0, 2));
|
|
3500
3103
|
else if (all.length === 0) write("No projects registered yet: ork init (in-repo) or ork register (attached).");
|
|
3501
|
-
else for (const
|
|
3104
|
+
else for (const line of formatProjects(all)) write(line);
|
|
3502
3105
|
}
|
|
3503
3106
|
});
|
|
3504
3107
|
const migrate = defineCommand({
|
|
@@ -3548,20 +3151,6 @@ const migrate = defineCommand({
|
|
|
3548
3151
|
}, (file) => formatReport(validateFile(file, process.cwd())));
|
|
3549
3152
|
}
|
|
3550
3153
|
});
|
|
3551
|
-
// src/store.ts
|
|
3552
|
-
/**
|
|
3553
|
-
* @file Opening the user's store (`~/.ork/state.db`) for the length of one
|
|
3554
|
-
* command, and closing it after, whatever happens.
|
|
3555
|
-
*/
|
|
3556
|
-
/** Runs `body` with the user's store open, and closes it after. */
|
|
3557
|
-
const withStore = async (body) => {
|
|
3558
|
-
const store = openStore();
|
|
3559
|
-
try {
|
|
3560
|
-
await body(store);
|
|
3561
|
-
} finally {
|
|
3562
|
-
store.close();
|
|
3563
|
-
}
|
|
3564
|
-
};
|
|
3565
3154
|
// src/runs/feedback.ts
|
|
3566
3155
|
/**
|
|
3567
3156
|
* @file `ork feedback`'s helpers (specs/feedback.md): finding an item by a
|
|
@@ -3592,13 +3181,16 @@ const SEVERITY_STYLE = {
|
|
|
3592
3181
|
};
|
|
3593
3182
|
const about = (item) => `${item.target}/${item.kind}`;
|
|
3594
3183
|
/** One line per item, most recently seen first. */
|
|
3595
|
-
const formatFeedbackItems = (items, now = Date.now()) => {
|
|
3184
|
+
const formatFeedbackItems = (items, now = Date.now(), term = termOf(process.stdout)) => {
|
|
3596
3185
|
if (items.length === 0) return ["No feedback yet."];
|
|
3597
3186
|
const width = Math.max(...items.map((i) => about(i).length));
|
|
3598
3187
|
return items.map((item) => {
|
|
3599
|
-
const
|
|
3188
|
+
const mark = item.status === "new" ? "" : term.rich ? ` ${chip(term, item.status.toUpperCase(), item.status === "dismissed" ? "muted" : "accent")}` : ` ${style("dim", `[${item.status}]`)}`;
|
|
3600
3189
|
const times = `${String(item.count)}×`.padStart(4);
|
|
3601
|
-
|
|
3190
|
+
const when = ` ${style("dim", `· ${ago(item.lastSeen, now)}`)}`;
|
|
3191
|
+
const room = term.width - (item.id.length + 1 + times.length + 1 + width + 2) - widthOf(mark) - widthOf(when);
|
|
3192
|
+
const title = term.rich ? cut(item.title, Math.max(8, room)) : item.title;
|
|
3193
|
+
return `${style("dim", item.id)} ${times} ${style(SEVERITY_STYLE[item.severity], about(item).padEnd(width))} ${title}${mark}${when}`;
|
|
3602
3194
|
});
|
|
3603
3195
|
};
|
|
3604
3196
|
const plural = (n, word) => `${String(n)} ${word}${n === 1 ? "" : "s"}`;
|
|
@@ -3889,7 +3481,7 @@ const triage = defineCommand({
|
|
|
3889
3481
|
description: "Make every merge without asking",
|
|
3890
3482
|
default: false
|
|
3891
3483
|
} },
|
|
3892
|
-
run: ({ args }) => withStore(async (store) => {
|
|
3484
|
+
run: ({ args }) => withStore$1(async (store) => {
|
|
3893
3485
|
if (!args.yes && !process.stdin.isTTY) {
|
|
3894
3486
|
writeErr("Pass --yes to triage without a terminal.");
|
|
3895
3487
|
process.exitCode = 1;
|
|
@@ -3908,7 +3500,7 @@ const split = defineCommand({
|
|
|
3908
3500
|
description: "Take apart the items triage merged into one"
|
|
3909
3501
|
},
|
|
3910
3502
|
args: { item: itemArg$1 },
|
|
3911
|
-
run: ({ args }) => withStore((store) => {
|
|
3503
|
+
run: ({ args }) => withStore$1((store) => {
|
|
3912
3504
|
const found = findFeedbackItem(store, args.item);
|
|
3913
3505
|
if (!found.ok) {
|
|
3914
3506
|
writeErr(found.error);
|
|
@@ -4050,7 +3642,7 @@ const publish = defineCommand({
|
|
|
4050
3642
|
default: false
|
|
4051
3643
|
}
|
|
4052
3644
|
},
|
|
4053
|
-
run: ({ args }) => withStore(async (store) => {
|
|
3645
|
+
run: ({ args }) => withStore$1(async (store) => {
|
|
4054
3646
|
if (!process.stdin.isTTY) {
|
|
4055
3647
|
failed("publish shows what it will post and asks you first: run it in a terminal.");
|
|
4056
3648
|
return;
|
|
@@ -4106,7 +3698,7 @@ const exportFeedback = defineCommand({
|
|
|
4106
3698
|
description: "A file to write (default: the terminal)"
|
|
4107
3699
|
}
|
|
4108
3700
|
},
|
|
4109
|
-
run: ({ args }) => withStore((store) => {
|
|
3701
|
+
run: ({ args }) => withStore$1((store) => {
|
|
4110
3702
|
const records = store.exportFeedback({ all: args.all });
|
|
4111
3703
|
const text = exportLines(records);
|
|
4112
3704
|
if (args.output === void 0) {
|
|
@@ -4127,7 +3719,7 @@ const importFeedback = defineCommand({
|
|
|
4127
3719
|
description: "What `ork feedback export` wrote",
|
|
4128
3720
|
required: true
|
|
4129
3721
|
} },
|
|
4130
|
-
run: ({ args }) => withStore((store) => {
|
|
3722
|
+
run: ({ args }) => withStore$1((store) => {
|
|
4131
3723
|
let text;
|
|
4132
3724
|
try {
|
|
4133
3725
|
text = readFileSync(args.file, "utf8");
|
|
@@ -4152,7 +3744,7 @@ const importFeedback = defineCommand({
|
|
|
4152
3744
|
* `feedback-share.ts`.
|
|
4153
3745
|
*/
|
|
4154
3746
|
/** Runs `body` on the item `prefix` names, or says why there's none. */
|
|
4155
|
-
const withItem = (prefix, body) => withStore((store) => {
|
|
3747
|
+
const withItem = (prefix, body) => withStore$1((store) => {
|
|
4156
3748
|
const found = findFeedbackItem(store, prefix);
|
|
4157
3749
|
if (!found.ok) {
|
|
4158
3750
|
writeErr(found.error);
|
|
@@ -4190,7 +3782,7 @@ const feedback = defineCommand({
|
|
|
4190
3782
|
},
|
|
4191
3783
|
json: json$1
|
|
4192
3784
|
},
|
|
4193
|
-
run: ({ args }) => withStore((store) => {
|
|
3785
|
+
run: ({ args }) => withStore$1((store) => {
|
|
4194
3786
|
const items = store.feedbackItems({ all: args.all });
|
|
4195
3787
|
if (args.json) write(JSON.stringify(items, void 0, 2));
|
|
4196
3788
|
else for (const line of formatFeedbackItems(items)) write(line);
|
|
@@ -4252,7 +3844,7 @@ const feedback = defineCommand({
|
|
|
4252
3844
|
description: "good or bad"
|
|
4253
3845
|
}
|
|
4254
3846
|
},
|
|
4255
|
-
run: ({ args }) => withStore((store) => {
|
|
3847
|
+
run: ({ args }) => withStore$1((store) => {
|
|
4256
3848
|
const noted = noteRun(store, process.cwd(), {
|
|
4257
3849
|
...args,
|
|
4258
3850
|
text: args.message
|
|
@@ -4359,13 +3951,13 @@ const ps = defineCommand({
|
|
|
4359
3951
|
default: false
|
|
4360
3952
|
}
|
|
4361
3953
|
},
|
|
4362
|
-
run: ({ args }) => withStore((store) => {
|
|
3954
|
+
run: ({ args }) => withStore$1((store) => {
|
|
4363
3955
|
const limit = Number(args.limit ?? 20);
|
|
4364
3956
|
const runs = store.runs({
|
|
4365
3957
|
limit: Number.isInteger(limit) && limit > 0 ? limit : 20,
|
|
4366
3958
|
...args.all ? {} : { project: placeFor(process.cwd(), store.projects()).root }
|
|
4367
3959
|
});
|
|
4368
|
-
const statusOf = (run) =>
|
|
3960
|
+
const statusOf = (run) => listedStatus(run, run.status === "running" ? store.events(run.id).map((s) => s.event) : []);
|
|
4369
3961
|
if (args.json) write(JSON.stringify(runs, void 0, 2));
|
|
4370
3962
|
else for (const line of formatRuns(runs, args.all, Date.now(), statusOf)) write(line);
|
|
4371
3963
|
})
|
|
@@ -4396,9 +3988,14 @@ const logs = defineCommand({
|
|
|
4396
3988
|
type: "boolean",
|
|
4397
3989
|
description: "Print the stored events as JSON lines",
|
|
4398
3990
|
default: false
|
|
3991
|
+
},
|
|
3992
|
+
verbose: {
|
|
3993
|
+
type: "boolean",
|
|
3994
|
+
description: "Show each tool call whole, and why each failed node stopped, with its scrubbed details (always on with --node)",
|
|
3995
|
+
default: false
|
|
4399
3996
|
}
|
|
4400
3997
|
},
|
|
4401
|
-
run: ({ args }) => withStore(async (store) => {
|
|
3998
|
+
run: ({ args }) => withStore$1(async (store) => {
|
|
4402
3999
|
const found = findRun(store, args.run, process.cwd());
|
|
4403
4000
|
if (!found.ok) {
|
|
4404
4001
|
writeErr(found.error);
|
|
@@ -4418,6 +4015,7 @@ const logs = defineCommand({
|
|
|
4418
4015
|
await followRun(store, found.run, write, {
|
|
4419
4016
|
follow: args.follow,
|
|
4420
4017
|
json: args.json,
|
|
4018
|
+
verbose: args.verbose,
|
|
4421
4019
|
signal: abort.signal,
|
|
4422
4020
|
...args.node === void 0 ? {} : { node: args.node }
|
|
4423
4021
|
});
|
|
@@ -4436,7 +4034,7 @@ const tree = defineCommand({
|
|
|
4436
4034
|
default: false
|
|
4437
4035
|
}
|
|
4438
4036
|
},
|
|
4439
|
-
run: ({ args }) => withStore((store) => {
|
|
4037
|
+
run: ({ args }) => withStore$1((store) => {
|
|
4440
4038
|
const found = findRun(store, args.run, process.cwd());
|
|
4441
4039
|
if (!found.ok) {
|
|
4442
4040
|
writeErr(found.error);
|
|
@@ -4504,7 +4102,7 @@ const addAgent = (options) => {
|
|
|
4504
4102
|
* doesn't, and otherwise it asks on a terminal, once, saying the plugin has
|
|
4505
4103
|
* them too, or says how to add them later.
|
|
4506
4104
|
*/
|
|
4507
|
-
const QUESTION = "Add Ork's skills for Claude Code to .ork/skills/, so it writes workflows Ork accepts? Ork's Claude Code plugin has them too.
|
|
4105
|
+
const QUESTION = "Add Ork's skills for Claude Code to .ork/skills/, so it writes workflows Ork accepts? Ork's Claude Code plugin has them too.";
|
|
4508
4106
|
const HINT = "Ork's skills for Claude Code: add them here with `ork init --skills`, or install Ork's plugin in Claude Code (/plugin marketplace add mimshins/ork, then /plugin install ork@ork).";
|
|
4509
4107
|
/** Whether to write the skills: as asked, or the person's answer. */
|
|
4510
4108
|
const wanted = (root, asked) => {
|
|
@@ -4774,28 +4372,29 @@ const main = defineCommand({
|
|
|
4774
4372
|
run,
|
|
4775
4373
|
ps,
|
|
4776
4374
|
logs,
|
|
4375
|
+
attach: () => import("./attach-BmV5ckb9.mjs").then((module) => module.attach),
|
|
4777
4376
|
tree,
|
|
4778
|
-
usage: () => import("./usage-
|
|
4779
|
-
prune: () => import("./usage-
|
|
4377
|
+
usage: () => import("./usage-C2zx-X6q.mjs").then((module) => module.usage),
|
|
4378
|
+
prune: () => import("./usage-C2zx-X6q.mjs").then((module) => module.prune),
|
|
4780
4379
|
decide,
|
|
4781
|
-
relay: () => import("./relay-
|
|
4782
|
-
pause: () => import("./pause-
|
|
4783
|
-
continue: () => import("./pause-
|
|
4380
|
+
relay: () => import("./relay-DAsJA8wF.mjs").then((module) => module.relay),
|
|
4381
|
+
pause: () => import("./pause-BuS83WAI.mjs").then((module) => module.pause),
|
|
4382
|
+
continue: () => import("./pause-BuS83WAI.mjs").then((module) => module.continueRun),
|
|
4784
4383
|
stop,
|
|
4785
4384
|
chat,
|
|
4786
|
-
send: () => import("./send-
|
|
4385
|
+
send: () => import("./send-MFEQZH31.mjs").then((module) => module.send),
|
|
4787
4386
|
resume,
|
|
4788
4387
|
feedback,
|
|
4789
4388
|
context,
|
|
4790
|
-
worktrees: () => import("./worktrees-
|
|
4791
|
-
wt: () => import("./worktrees-
|
|
4792
|
-
watch: () => import("./watch-
|
|
4793
|
-
mcp: () => import("./mcp-
|
|
4794
|
-
ui: () => import("./ui-
|
|
4389
|
+
worktrees: () => import("./worktrees-f6SAbum5.mjs").then((module) => module.worktrees),
|
|
4390
|
+
wt: () => import("./worktrees-f6SAbum5.mjs").then((module) => module.worktrees),
|
|
4391
|
+
watch: () => import("./watch-DzCeqM05.mjs").then((module) => module.watch),
|
|
4392
|
+
mcp: () => import("./mcp-CM3b65AZ.mjs").then((module) => module.mcp),
|
|
4393
|
+
ui: () => import("./ui-C4Ys26oO.mjs").then((module) => module.ui),
|
|
4795
4394
|
doctor,
|
|
4796
4395
|
upgrade,
|
|
4797
|
-
completion: () => import("./completion-
|
|
4798
|
-
__complete: () => import("./completion-
|
|
4396
|
+
completion: () => import("./completion-CzElB8nS.mjs").then((module) => module.completion),
|
|
4397
|
+
__complete: () => import("./completion-CzElB8nS.mjs").then((module) => module.completeCommand(main))
|
|
4799
4398
|
}
|
|
4800
4399
|
});
|
|
4801
4400
|
// src/bin.ts
|
|
@@ -4805,15 +4404,16 @@ const main = defineCommand({
|
|
|
4805
4404
|
* starts the daily background check and ends with the notice (ADR-038); the
|
|
4806
4405
|
* background copy it starts only runs the check. It first clears git's local
|
|
4807
4406
|
* variables (a hook's `GIT_DIR`, `GIT_INDEX_FILE`, …), so nothing Ork starts
|
|
4808
|
-
* acts on the hook's repository (specs/worktrees.md). `--
|
|
4809
|
-
*
|
|
4810
|
-
*
|
|
4811
|
-
*
|
|
4407
|
+
* acts on the hook's repository (specs/worktrees.md). The global `--plain`
|
|
4408
|
+
* flag is taken out of the arguments and set as `ORK_PLAIN=1`. `--help`
|
|
4409
|
+
* leaves out the flags only Ork passes to itself. `ork __complete`, which tab
|
|
4410
|
+
* completion runs on every Tab, goes first: no update check, and no citty, so
|
|
4411
|
+
* a typed `--help` is a word to complete.
|
|
4812
4412
|
*/
|
|
4813
4413
|
clearGitLocalEnv();
|
|
4814
|
-
const argv = process.argv.slice(2);
|
|
4414
|
+
const argv = takePlainFlag(process.argv.slice(2));
|
|
4815
4415
|
if (argv[0] === "__complete") {
|
|
4816
|
-
const { printCompletions } = await import("./completion-
|
|
4416
|
+
const { printCompletions } = await import("./completion-CzElB8nS.mjs");
|
|
4817
4417
|
await printCompletions(main, argv.slice(1));
|
|
4818
4418
|
} else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
|
|
4819
4419
|
else {
|
|
@@ -4823,6 +4423,9 @@ else {
|
|
|
4823
4423
|
showUpdateNotice();
|
|
4824
4424
|
});
|
|
4825
4425
|
}
|
|
4826
|
-
await runMain(main, {
|
|
4426
|
+
await runMain(main, {
|
|
4427
|
+
rawArgs: argv,
|
|
4428
|
+
showUsage: showHelp
|
|
4429
|
+
});
|
|
4827
4430
|
}
|
|
4828
4431
|
export {};
|