@krapht/ork 0.4.0 → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -12
- package/dist/{__build_info__-CtSVVuZE.mjs → __build_info__-D9lSlMFy.mjs} +36 -112
- package/dist/bin.mjs +688 -306
- package/dist/{clean-Dcr807be.mjs → clean-CA_Oi2Wl.mjs} +1 -1
- package/dist/completion-DBGfL-gN.mjs +405 -0
- package/dist/{context-C_bksb0C.mjs → context-6x3kXREO.mjs} +11 -88
- package/dist/decide-CoIJh34e.mjs +113 -0
- package/dist/{find-DptmbhGd.mjs → find-rBBk7Jui.mjs} +1 -1
- package/dist/{format-J-a21Yvn.mjs → format-zMmhqyLE.mjs} +71 -37
- package/dist/help-groups-BwYoAWsh.mjs +354 -0
- package/dist/{in-a-run-D1Qc5Mit.mjs → in-a-run-DdAakpRR.mjs} +1 -1
- package/dist/{limits-BDZCmGEu.mjs → limits-BPM9wUlV.mjs} +1 -1
- package/dist/{list-ZvY2OcLq.mjs → list-Bv5Lxu7z.mjs} +1 -1
- package/dist/{mcp-CHVKKg3B.mjs → mcp-DM30MFBw.mjs} +7 -7
- package/dist/{output-CXSJO00X.mjs → output-TV_j7Ni0.mjs} +67 -17
- package/dist/{pause-BPSmg4yz.mjs → pause-C7lJkSma.mjs} +4 -4
- package/dist/{relay-Do-aNbSD.mjs → relay-BipJYszF.mjs} +2 -2
- package/dist/{remove-AwaMMaHf.mjs → remove-BDs9isB6.mjs} +3 -3
- package/dist/{send-DhGBkowy.mjs → send-nbkjf3sc.mjs} +3 -3
- package/dist/{src-CpDKIulI.mjs → src-B0TT7DBi.mjs} +28 -15
- package/dist/{src-BHnxpJyH.mjs → src-B9v0lDc6.mjs} +58 -10
- package/dist/{src-UWkk1sl9.mjs → src-BxUJPGa5.mjs} +252 -18
- package/dist/{src-CGqMati3.mjs → src-hrj2h0IZ.mjs} +640 -220
- package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
- package/dist/ui/assets/app-BNT0SuIz.css +1 -0
- package/dist/ui/assets/app-CmkynZyW.js +67 -0
- package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
- package/dist/ui/assets/{index-MQNNgsyT.js → index-CPx74lFf.js} +2 -2
- package/dist/ui/index.html +2 -3
- package/dist/{ui-DhutUMgQ.mjs → ui-B98FSboa.mjs} +5 -5
- package/dist/{usage-CT7Z-yjr.mjs → usage-EW7DI6Z7.mjs} +6 -6
- package/dist/{watch-0Qr2UfCQ.mjs → watch-BAkSGaOp.mjs} +7 -7
- package/dist/workflows-vsEohqa7.mjs +88 -0
- package/dist/{worktrees-DHl80k96.mjs → worktrees-Ahj9rdWK.mjs} +5 -5
- package/package.json +3 -3
- package/dist/ui/assets/app-BgVDC_5l.js +0 -67
- package/dist/ui/assets/app-DVf68fj5.css +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { C as
|
|
4
|
-
import { n as
|
|
5
|
-
import {
|
|
6
|
-
import { n as
|
|
7
|
-
import {
|
|
8
|
-
import { a as
|
|
9
|
-
import {
|
|
10
|
-
import { i as
|
|
11
|
-
import {
|
|
12
|
-
import { i as
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
2
|
+
import { Bn as serializeWorkflow, Bt as FEEDBACK_TARGETS, Cn as runDirOf, D as inspectProject, Dn as projectRootOf, Dt as userMessageText, En as inside, Ft as PROJECT_FILE, H as tellNode, Ht as planPost, I as createCalls, It as gitRulesOf, J as tellLiveRun, Jn as beforeOf, Jt as normalizeTitle, Kn as settingsOf, Kt as createFeedback, L as addressOf, Lt as loadProject, M as clearGitLocalEnv, N as parseInputPairs, P as resolveInputs, R as endLink, Rt as FEEDBACK_KINDS, Sn as projectsHome, Tt as startRun, Un as WorkflowSchema, V as peerOf, W as flattenTree, Wt as sendPost, Xt as maskReplacer, Yn as isClaudeNode, _ as lateTriggers, _n as hasInRepoFiles, _t as runStatus, an as wakeRun, ar as CLAUDE_DRIVERS, bn as placeFiles, cn as planMoveInto, d as runningRunIds, dn as toolsInUse, dt as namingNoDriver, er as INTEGRATIONS, et as adoptedCalls, f as runningScript, fn as createAgent, ft as sessionDriver, h as isNewer, hn as loadAgents, ht as liveStatus, i as NOT_STARTED, in as socketRoom, ir as ORK_LAYOUT, it as replayLoopAnswers, l as planUpgrade, ln as linkTools, mn as catalogAgent, n as resolveBase, nn as serveRun, nt as resumable, on as planAgentMigration, or as firstLine, ot as detachRun, pn as deleteAgent, q as requestStop, qt as fingerprint, rn as socketPath, rr as AGENT_TOOL_LAYOUTS, rt as createReplay, sn as applySteps, st as followEvents, t as createWorkspace, tt as detachResume, u as releaseNotes, un as toolLinks, ut as planNodeSession, v as triggersReport, vn as inRepoPlace, vt as runTree, wn as shownPath, wt as walkSummary, xn as placeFor, yn as newProjectId, z as endLinksOf, zt as FEEDBACK_SEVERITIES } from "./src-hrj2h0IZ.mjs";
|
|
3
|
+
import { C as feedbackActions, S as placeOf, _ as previewPost, b as acceptItem, c as checkDue, d as pendingNotice, f as readUpdateCache, g as nodeSession, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, n as ORK_VERSION, o as updateSwitchOff, p as refreshUpdateCache, r as isSwitchedOn, s as runInstall, t as ORK_NAME, u as newerVersion$1, v as checkNote, w as openFixRun, y as recordNote } from "./__build_info__-D9lSlMFy.mjs";
|
|
4
|
+
import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-CoIJh34e.mjs";
|
|
5
|
+
import { n as newRunId, t as findRun } from "./find-rBBk7Jui.mjs";
|
|
6
|
+
import { a as write, c as openStore, i as style, l as storePath, n as cost, o as writeErr, s as nodeOf, t as confirm } from "./output-TV_j7Ni0.mjs";
|
|
7
|
+
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-Bv5Lxu7z.mjs";
|
|
8
|
+
import { a as planClean, t as applyClean } from "./clean-CA_Oi2Wl.mjs";
|
|
9
|
+
import { c as subcommandsOf, i as argsOf, n as groupedEntries, o as publicArgs, s as resolved, t as groupOf } from "./help-groups-BwYoAWsh.mjs";
|
|
10
|
+
import { a as formatRunEvent, i as formatEvent, n as formatRuns, o as createTerminalHuman, r as formatTree, t as ago } from "./format-zMmhqyLE.mjs";
|
|
11
|
+
import { c as registeredProjects, i as refusedBuiltin, n as WORKFLOW_SUFFIX, o as workflowArg, s as projectAt, t as WORKFLOW_DIR } from "./workflows-vsEohqa7.mjs";
|
|
12
|
+
import { a as validate, c as warningLines, i as loadWorkflow, o as validateFile, r as formatReport, s as walkLoaded, t as context } from "./context-6x3kXREO.mjs";
|
|
13
|
+
import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-BPM9wUlV.mjs";
|
|
14
|
+
import { _ as resultOf, a as builtinExecutors, d as maskedEnv, f as maskedNamesOfRun, g as clearOutputFiles, h as sessionEnv, i as onPath, l as clearStateFiles, m as runScrubber, n as missingProgramMessage, r as missingPrograms, t as envProblems } from "./src-BxUJPGa5.mjs";
|
|
15
|
+
import { i as decideRefusal, n as chatRefusal } from "./in-a-run-DdAakpRR.mjs";
|
|
15
16
|
import path from "node:path";
|
|
16
17
|
import { stringify } from "yaml";
|
|
17
18
|
import { z } from "zod";
|
|
@@ -20,7 +21,38 @@ import { createHash } from "node:crypto";
|
|
|
20
21
|
import { homedir } from "node:os";
|
|
21
22
|
import { execFile, spawn } from "node:child_process";
|
|
22
23
|
import { setTimeout } from "node:timers/promises";
|
|
24
|
+
import { defineCommand, renderUsage, runMain } from "citty";
|
|
25
|
+
import { createInterface } from "node:readline/promises";
|
|
23
26
|
import { styleText } from "node:util";
|
|
27
|
+
// ../integrations/src/ready.ts
|
|
28
|
+
/**
|
|
29
|
+
* @file Which Claude driver is ready on this machine: each driver's own check
|
|
30
|
+
* (what `ork doctor` shows), and the one a run takes for the nodes that name
|
|
31
|
+
* no driver, so a committed workflow never carries one person's login.
|
|
32
|
+
*/
|
|
33
|
+
/** Each registered Claude driver's own check, in registry order. */
|
|
34
|
+
const driverChecks = (run, env) => Promise.all(CLAUDE_DRIVERS.map(async (driver) => ({
|
|
35
|
+
id: driver.id,
|
|
36
|
+
result: await driver.doctor(run, env)
|
|
37
|
+
})));
|
|
38
|
+
/**
|
|
39
|
+
* The driver ready here: the one whose check passes; with more than one,
|
|
40
|
+
* {@link PREFERRED_DRIVER}; with none, each driver's hint.
|
|
41
|
+
*/
|
|
42
|
+
const readyDriver = (checks) => {
|
|
43
|
+
const ready = checks.filter(({ result }) => result.status === "ok");
|
|
44
|
+
const pick = ready.find(({ id }) => id === "cli") ?? ready.at(0);
|
|
45
|
+
if (pick === void 0) return {
|
|
46
|
+
ok: false,
|
|
47
|
+
hints: checks.flatMap(({ result }) => result.hint === void 0 ? [] : [result.hint])
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
ok: true,
|
|
51
|
+
driver: pick.id,
|
|
52
|
+
detail: pick.result.detail,
|
|
53
|
+
ready: ready.length
|
|
54
|
+
};
|
|
55
|
+
};
|
|
24
56
|
// ../core/src/control/queue.ts
|
|
25
57
|
const PERMISSION_OPTIONS = [{
|
|
26
58
|
id: "allow",
|
|
@@ -30,6 +62,8 @@ const PERMISSION_OPTIONS = [{
|
|
|
30
62
|
label: "Deny"
|
|
31
63
|
}];
|
|
32
64
|
const DENIED = "The user did not allow this tool.";
|
|
65
|
+
/** The deny message a session gets, with the person's words when given. */
|
|
66
|
+
const deniedMessage = (note) => note === void 0 ? DENIED : `${DENIED} They said: ${note}`;
|
|
33
67
|
/** At most `max` characters of a tool's input, for the decision's context. */
|
|
34
68
|
const inputPreview = (input, max = 400) => {
|
|
35
69
|
const json = JSON.stringify(input);
|
|
@@ -56,22 +90,30 @@ const createDecisionQueue = (options) => {
|
|
|
56
90
|
options: record.options
|
|
57
91
|
};
|
|
58
92
|
if (record.recommended !== void 0) event.recommended = record.recommended;
|
|
93
|
+
if (record.owner !== void 0) event.owner = record.owner;
|
|
59
94
|
emit(event);
|
|
60
95
|
const { promise, resolve, reject } = Promise.withResolvers();
|
|
61
96
|
const settled = new AbortController();
|
|
62
|
-
|
|
97
|
+
/** `note` is the stored one: masked, and absent when blank. */
|
|
98
|
+
const finish = (answer, by, note) => {
|
|
63
99
|
if (settled.signal.aborted) return;
|
|
64
100
|
settled.abort();
|
|
65
|
-
|
|
101
|
+
const answered = {
|
|
66
102
|
type: "decision.answered",
|
|
67
103
|
node: open.node,
|
|
68
104
|
decision: id,
|
|
69
105
|
answer,
|
|
70
106
|
by
|
|
71
|
-
}
|
|
72
|
-
|
|
107
|
+
};
|
|
108
|
+
if (note !== void 0) answered.note = note;
|
|
109
|
+
emit(answered);
|
|
110
|
+
resolve(note === void 0 ? {
|
|
73
111
|
answer,
|
|
74
112
|
by
|
|
113
|
+
} : {
|
|
114
|
+
answer,
|
|
115
|
+
by,
|
|
116
|
+
note
|
|
75
117
|
});
|
|
76
118
|
};
|
|
77
119
|
const withdraw = () => {
|
|
@@ -90,13 +132,13 @@ const createDecisionQueue = (options) => {
|
|
|
90
132
|
const fromStore = async () => {
|
|
91
133
|
try {
|
|
92
134
|
const closed = await options.waitFor(id, settled.signal);
|
|
93
|
-
if (closed.status === "answered" && closed.answer !== void 0) finish(closed.answer, closed.answeredBy ?? "decide");
|
|
135
|
+
if (closed.status === "answered" && closed.answer !== void 0) finish(closed.answer, closed.answeredBy ?? "decide", closed.note);
|
|
94
136
|
} catch {}
|
|
95
137
|
};
|
|
96
138
|
const fromTerminal = async (ask) => {
|
|
97
139
|
try {
|
|
98
|
-
const answer = await ask(settled.signal);
|
|
99
|
-
if (store.answerDecision(id, answer, "terminal")) finish(answer, "terminal");
|
|
140
|
+
const { answer, note } = await ask(settled.signal);
|
|
141
|
+
if (store.answerDecision(id, answer, "terminal", note)) finish(answer, "terminal", store.decision(id)?.note);
|
|
100
142
|
} catch {}
|
|
101
143
|
};
|
|
102
144
|
fromStore();
|
|
@@ -113,31 +155,92 @@ const createDecisionQueue = (options) => {
|
|
|
113
155
|
};
|
|
114
156
|
if (question.recommended !== void 0) open.recommended = question.recommended;
|
|
115
157
|
if (question.context !== void 0) open.context = question.context;
|
|
158
|
+
if (question.owner !== void 0) open.owner = question.owner;
|
|
116
159
|
const { local } = options;
|
|
117
|
-
return decide(open, local === void 0 ? void 0 : (signal) =>
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
160
|
+
return decide(open, local === void 0 ? void 0 : (signal) => {
|
|
161
|
+
let note;
|
|
162
|
+
return local.ask({
|
|
163
|
+
...question,
|
|
164
|
+
signal,
|
|
165
|
+
onNote: (given) => {
|
|
166
|
+
note = given;
|
|
167
|
+
}
|
|
168
|
+
}).then((answer) => note === void 0 ? { answer } : {
|
|
169
|
+
answer,
|
|
170
|
+
note
|
|
171
|
+
});
|
|
172
|
+
}, question.signal).then(({ answer, note }) => {
|
|
173
|
+
if (note !== void 0) question.onNote?.(note);
|
|
174
|
+
return answer;
|
|
175
|
+
});
|
|
176
|
+
},
|
|
177
|
+
settled: (question) => {
|
|
178
|
+
count += 1;
|
|
179
|
+
const id = `${runId}-${String(count)}`;
|
|
180
|
+
const { owner } = question;
|
|
181
|
+
const record = store.openDecision({
|
|
182
|
+
id,
|
|
183
|
+
runId,
|
|
184
|
+
node: question.nodeId,
|
|
185
|
+
kind: "question",
|
|
186
|
+
question: question.question,
|
|
187
|
+
options: question.options,
|
|
188
|
+
owner,
|
|
189
|
+
...question.recommended === void 0 ? {} : { recommended: question.recommended },
|
|
190
|
+
...question.context === void 0 ? {} : { context: question.context }
|
|
191
|
+
});
|
|
192
|
+
emit({
|
|
193
|
+
type: "decision.opened",
|
|
194
|
+
node: record.node,
|
|
195
|
+
decision: id,
|
|
196
|
+
kind: "question",
|
|
197
|
+
question: record.question,
|
|
198
|
+
options: record.options,
|
|
199
|
+
owner,
|
|
200
|
+
...record.recommended === void 0 ? {} : { recommended: record.recommended }
|
|
201
|
+
});
|
|
202
|
+
store.answerDecision(id, question.answer, "owner", question.note);
|
|
203
|
+
const note = store.decision(id)?.note;
|
|
204
|
+
emit({
|
|
205
|
+
type: "decision.answered",
|
|
206
|
+
node: record.node,
|
|
207
|
+
decision: id,
|
|
208
|
+
answer: question.answer,
|
|
209
|
+
by: "owner",
|
|
210
|
+
owner,
|
|
211
|
+
...note === void 0 ? {} : { note }
|
|
212
|
+
});
|
|
121
213
|
},
|
|
122
214
|
permit: async (request) => {
|
|
123
215
|
const { local } = options;
|
|
124
216
|
let localMessage = DENIED;
|
|
125
|
-
const { answer } = await decide({
|
|
217
|
+
const { answer, note } = await decide({
|
|
126
218
|
node: request.nodeId,
|
|
127
219
|
kind: "permission",
|
|
128
220
|
question: `Allow ${request.toolName}?`,
|
|
129
221
|
options: PERMISSION_OPTIONS,
|
|
130
222
|
context: inputPreview(request.input)
|
|
131
|
-
}, local === void 0 ? void 0 : (signal) =>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
223
|
+
}, local === void 0 ? void 0 : (signal) => {
|
|
224
|
+
let said;
|
|
225
|
+
return local.permit({
|
|
226
|
+
...request,
|
|
227
|
+
signal,
|
|
228
|
+
onNote: (given) => {
|
|
229
|
+
said = given;
|
|
230
|
+
}
|
|
231
|
+
}).then((result) => {
|
|
232
|
+
if (!result.allow) localMessage = result.message;
|
|
233
|
+
const choice = result.allow ? "allow" : "deny";
|
|
234
|
+
return said === void 0 ? { answer: choice } : {
|
|
235
|
+
answer: choice,
|
|
236
|
+
note: said
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
if (note !== void 0) request.onNote?.(note);
|
|
138
241
|
return answer === "allow" ? { allow: true } : {
|
|
139
242
|
allow: false,
|
|
140
|
-
message: localMessage
|
|
243
|
+
message: note === void 0 ? localMessage : deniedMessage(note)
|
|
141
244
|
};
|
|
142
245
|
}
|
|
143
246
|
};
|
|
@@ -343,7 +446,7 @@ const createSignals = (options = {}) => {
|
|
|
343
446
|
}
|
|
344
447
|
if (event.type !== "node.event") return;
|
|
345
448
|
const inner = event.event;
|
|
346
|
-
if (inner.type === "session.started") sessions.set(event.id, {
|
|
449
|
+
if (inner.type === "session.started" && inner.fork !== true) sessions.set(event.id, {
|
|
347
450
|
model: inner.model,
|
|
348
451
|
...inner.driver === void 0 ? {} : { driver: inner.driver }
|
|
349
452
|
});
|
|
@@ -530,10 +633,357 @@ const appendRecorder = (store, runId, runDir) => {
|
|
|
530
633
|
return stored;
|
|
531
634
|
};
|
|
532
635
|
};
|
|
533
|
-
/** Begins `run` in the store; `runDir` gets the JSONL copy. */
|
|
534
|
-
const createRecorder = (store, run, runDir) => {
|
|
535
|
-
store.begin(run);
|
|
536
|
-
return appendRecorder(store, run.id, runDir);
|
|
636
|
+
/** Begins `run` in the store; `runDir` gets the JSONL copy. */
|
|
637
|
+
const createRecorder = (store, run, runDir) => {
|
|
638
|
+
store.begin(run);
|
|
639
|
+
return appendRecorder(store, run.id, runDir);
|
|
640
|
+
};
|
|
641
|
+
// src/help-root.ts
|
|
642
|
+
/** Where the user docs start. */
|
|
643
|
+
const DOCS_URL = "https://github.com/mimshins/ork/blob/main/docs/README.md";
|
|
644
|
+
const rowOf = (entry) => {
|
|
645
|
+
const short = groupOf(entry.name)?.short ?? entry.description;
|
|
646
|
+
const alias = entry.aliases.length === 0 ? "" : ` (alias: ${entry.aliases.join(", ")})`;
|
|
647
|
+
return {
|
|
648
|
+
name: entry.name,
|
|
649
|
+
line: `${short}${alias}`
|
|
650
|
+
};
|
|
651
|
+
};
|
|
652
|
+
const groupedRows = (entries) => groupedEntries(entries).map((group) => ({
|
|
653
|
+
title: group.title,
|
|
654
|
+
rows: group.entries.map((entry) => rowOf(entry))
|
|
655
|
+
}));
|
|
656
|
+
/** A group's title, its rows with names `width` wide, and a blank line. */
|
|
657
|
+
const groupLines = (group, width) => [style("bold", `${group.title}:`)].concat(group.rows.map((row) => ` ${style("cyan", row.name.padEnd(width))} ${row.line}`), [""]);
|
|
658
|
+
/** The root command's help text. */
|
|
659
|
+
const renderRootHelp = async (cmd) => {
|
|
660
|
+
const meta = cmd.meta === void 0 ? {} : await resolved(cmd.meta);
|
|
661
|
+
const name = meta.name ?? "ork";
|
|
662
|
+
const groups = groupedRows(await subcommandsOf(cmd));
|
|
663
|
+
const width = Math.max(...groups.flatMap((g) => g.rows.map((r) => r.name.length)));
|
|
664
|
+
return [
|
|
665
|
+
`${meta.description ?? ""}.`,
|
|
666
|
+
`Docs: ${DOCS_URL}`,
|
|
667
|
+
"",
|
|
668
|
+
...groups.flatMap((group) => groupLines(group, width)),
|
|
669
|
+
`Usage: ${name} <command> [flags]`,
|
|
670
|
+
"",
|
|
671
|
+
`Use "${name} <command> --help" for more about a command.`,
|
|
672
|
+
`${name} v${meta.version ?? ""}`
|
|
673
|
+
].join("\n");
|
|
674
|
+
};
|
|
675
|
+
// src/help.ts
|
|
676
|
+
/**
|
|
677
|
+
* @file `ork <command> --help`: citty's usage, without the flags Ork passes to
|
|
678
|
+
* itself (the command tree's filter), and with `<command> [flags]` in place
|
|
679
|
+
* of citty's `a|b|c` list for a command with subcommands. The root command's
|
|
680
|
+
* help is grouped (`help-root.ts`).
|
|
681
|
+
*/
|
|
682
|
+
/** Citty's `a|b|c` usage list of a command's shown subcommands. */
|
|
683
|
+
const choicesOf = async (cmd) => (await subcommandsOf(cmd)).flatMap((entry) => entry.hidden ? [] : [entry.name].concat(entry.aliases)).join("|");
|
|
684
|
+
/** The usage of `cmd` (under `parent`), as people see it. */
|
|
685
|
+
const renderHelp = async (cmd, parent) => {
|
|
686
|
+
const shown = {
|
|
687
|
+
args: publicArgs(await argsOf(cmd)),
|
|
688
|
+
...cmd.meta === void 0 ? {} : { meta: cmd.meta },
|
|
689
|
+
...cmd.subCommands === void 0 ? {} : { subCommands: cmd.subCommands }
|
|
690
|
+
};
|
|
691
|
+
if (cmd.subCommands !== void 0 && parent === void 0) return renderRootHelp(shown);
|
|
692
|
+
const above = parent?.meta === void 0 ? void 0 : { meta: parent.meta };
|
|
693
|
+
const usage = await renderUsage(shown, above);
|
|
694
|
+
if (cmd.subCommands === void 0) return usage;
|
|
695
|
+
const choices = await choicesOf(shown);
|
|
696
|
+
return usage.replace(` [OPTIONS] ${choices}`, ` <command> [flags]`).replace(` ${choices}`, ` <command> [flags]`);
|
|
697
|
+
};
|
|
698
|
+
/** Prints {@link renderHelp}'s usage, as citty prints its own. */
|
|
699
|
+
const showHelp = async (cmd, parent) => {
|
|
700
|
+
process.stdout.write(`${await renderHelp(cmd, parent)}\n\n`);
|
|
701
|
+
};
|
|
702
|
+
// src/update/notify.ts
|
|
703
|
+
/**
|
|
704
|
+
* @file The new-version notice (ADR-038). At most once a day a command starts a
|
|
705
|
+
* detached copy of `ork` that asks the npm registry for the latest version
|
|
706
|
+
* and records it in `~/.ork/update-check.json`, so no command waits on the
|
|
707
|
+
* network; a later command, when a newer version is known, ends with one line
|
|
708
|
+
* on stderr (once a day). Off in CI, without a terminal, with `--json`, and
|
|
709
|
+
* with `ORK_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1`.
|
|
710
|
+
*/
|
|
711
|
+
/** Set in the background copy of `ork` that only runs the check. */
|
|
712
|
+
const UPDATE_CHILD_ENV = "ORK_UPDATE_CHECK_CHILD";
|
|
713
|
+
/**
|
|
714
|
+
* Why the automatic check is off for this command, or `null` when it is on: the
|
|
715
|
+
* environment's switches (core's, which `ork ui` reads too), then where a
|
|
716
|
+
* notice on stderr would go.
|
|
717
|
+
*/
|
|
718
|
+
const updateCheckOff = (env, argv, terminal) => {
|
|
719
|
+
const switched = updateSwitchOff(env);
|
|
720
|
+
if (switched !== null) return switched === "CI" ? "in CI" : "turned off";
|
|
721
|
+
if (isSwitchedOn(env["ORK_DETACHED"]) || !terminal) return "no terminal";
|
|
722
|
+
if (argv.includes("--json")) return "JSON output";
|
|
723
|
+
if (argv[0] === "upgrade") return "ork upgrade checks for itself";
|
|
724
|
+
if (argv[0] === "ui" && argv.includes("--serve")) return "ork ui's server checks for itself";
|
|
725
|
+
return null;
|
|
726
|
+
};
|
|
727
|
+
/** The notice's text. */
|
|
728
|
+
const updateNotice = (latest, current) => `Ork ${latest} is out (you have ${current}). Upgrade with: ork upgrade`;
|
|
729
|
+
/**
|
|
730
|
+
* Starts the background check when one is due. Records the attempt first, so
|
|
731
|
+
* commands started in the next moments do not start one too.
|
|
732
|
+
*/
|
|
733
|
+
const startUpdateCheck = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
|
|
734
|
+
try {
|
|
735
|
+
const cache = readUpdateCache(file);
|
|
736
|
+
if (!checkDue(cache, now)) return;
|
|
737
|
+
writeUpdateCache(file, {
|
|
738
|
+
...cache,
|
|
739
|
+
checkedAt: now.toISOString()
|
|
740
|
+
});
|
|
741
|
+
const child = spawn(process.execPath, [...process.execArgv, process.argv[1] ?? ""], {
|
|
742
|
+
detached: true,
|
|
743
|
+
stdio: "ignore",
|
|
744
|
+
env: {
|
|
745
|
+
...process.env,
|
|
746
|
+
[UPDATE_CHILD_ENV]: "1"
|
|
747
|
+
}
|
|
748
|
+
});
|
|
749
|
+
child.on("error", () => {});
|
|
750
|
+
child.unref();
|
|
751
|
+
} catch {}
|
|
752
|
+
};
|
|
753
|
+
/** The background copy's whole job: ask the registry, record the answer. */
|
|
754
|
+
const runUpdateChild = async () => {
|
|
755
|
+
const registry = registryOf(process.env);
|
|
756
|
+
await refreshUpdateCache(updateCachePath(), ORK_NAME, /* @__PURE__ */ new Date(), {
|
|
757
|
+
current: ORK_VERSION,
|
|
758
|
+
...registry === void 0 ? {} : { registry }
|
|
759
|
+
});
|
|
760
|
+
};
|
|
761
|
+
/** Prints the notice when a newer version is known and not told today. */
|
|
762
|
+
const showUpdateNotice = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
|
|
763
|
+
try {
|
|
764
|
+
const cache = readUpdateCache(file);
|
|
765
|
+
const latest = pendingNotice(cache, ORK_VERSION, now);
|
|
766
|
+
if (cache === null || latest === null) return;
|
|
767
|
+
const text = updateNotice(latest, ORK_VERSION);
|
|
768
|
+
process.stderr.write(`\n${styleText("yellow", text, { stream: process.stderr })}\n`);
|
|
769
|
+
writeUpdateCache(file, {
|
|
770
|
+
...cache,
|
|
771
|
+
notified: {
|
|
772
|
+
version: latest,
|
|
773
|
+
at: now.toISOString()
|
|
774
|
+
}
|
|
775
|
+
});
|
|
776
|
+
} catch {}
|
|
777
|
+
};
|
|
778
|
+
// src/update/upgrade.ts
|
|
779
|
+
/**
|
|
780
|
+
* @file `ork doctor`'s line for Ork itself (ADR-038): the running version
|
|
781
|
+
* against the last update check, read from the record without asking the
|
|
782
|
+
* network. What `ork upgrade` does is core's (`planUpgrade`, `runInstall`),
|
|
783
|
+
* shared with the server behind `ork ui`.
|
|
784
|
+
*/
|
|
785
|
+
/** `ork doctor`'s line for Ork itself, from the last check. */
|
|
786
|
+
const versionCheck = (current, cache, checksOff) => {
|
|
787
|
+
const newer = newerVersion$1(cache, current);
|
|
788
|
+
if (newer !== null) return {
|
|
789
|
+
name: "ork",
|
|
790
|
+
status: "warn",
|
|
791
|
+
detail: `${current}; ${newer} is out`,
|
|
792
|
+
hint: "Run ork upgrade."
|
|
793
|
+
};
|
|
794
|
+
return {
|
|
795
|
+
name: "ork",
|
|
796
|
+
status: "ok",
|
|
797
|
+
detail: `${current} (${cache?.latest === void 0 ? checksOff ? "update check off" : "not checked for updates yet" : `the latest as of ${cache.checkedAt.slice(0, 10)}`})`
|
|
798
|
+
};
|
|
799
|
+
};
|
|
800
|
+
// src/doctor/checks.ts
|
|
801
|
+
/**
|
|
802
|
+
* @file The checks behind `ork doctor`: Ork's own version against the last
|
|
803
|
+
* update check, each tool's presence and login status (the drivers' own
|
|
804
|
+
* checks, which `ork run` also reads to pick the one ready), and whether a
|
|
805
|
+
* run's socket path fits, through an injectable `exec` so tests need no real
|
|
806
|
+
* tools. Never prints a token or an account.
|
|
807
|
+
*/
|
|
808
|
+
const TIMEOUT_MS = 15e3;
|
|
809
|
+
const exec = (program, args) => new Promise((resolve) => {
|
|
810
|
+
execFile(program, args, {
|
|
811
|
+
timeout: TIMEOUT_MS,
|
|
812
|
+
encoding: "utf8"
|
|
813
|
+
}, (error, stdout) => {
|
|
814
|
+
resolve({
|
|
815
|
+
ok: error === null,
|
|
816
|
+
stdout
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
/** Ork's own version, against the last update check (no network). */
|
|
821
|
+
const orkCheck = () => versionCheck(ORK_VERSION, readUpdateCache(updateCachePath()), updateCheckOff(process.env, [], true) !== null);
|
|
822
|
+
const MIN_NODE = [24, 20];
|
|
823
|
+
const checkNode = (version) => {
|
|
824
|
+
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
825
|
+
const [minMajor, minMinor] = MIN_NODE;
|
|
826
|
+
return major > minMajor || major === minMajor && minor >= minMinor ? {
|
|
827
|
+
name: "node",
|
|
828
|
+
status: "ok",
|
|
829
|
+
detail: `v${version}`
|
|
830
|
+
} : {
|
|
831
|
+
name: "node",
|
|
832
|
+
status: "fail",
|
|
833
|
+
detail: `v${version}`,
|
|
834
|
+
hint: `Ork needs Node ${MIN_NODE.join(".")} or later.`
|
|
835
|
+
};
|
|
836
|
+
};
|
|
837
|
+
const checkGit = async (run) => {
|
|
838
|
+
const version = await run("git", ["--version"]);
|
|
839
|
+
if (!version.ok) return [{
|
|
840
|
+
name: "git",
|
|
841
|
+
status: "fail",
|
|
842
|
+
detail: "not found",
|
|
843
|
+
hint: "Install git; Ork gives each writing node its own worktree."
|
|
844
|
+
}];
|
|
845
|
+
const inside = await run("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
846
|
+
return [{
|
|
847
|
+
name: "git",
|
|
848
|
+
status: "ok",
|
|
849
|
+
detail: firstLine(version.stdout)
|
|
850
|
+
}, inside.ok ? {
|
|
851
|
+
name: "git repo",
|
|
852
|
+
status: "ok",
|
|
853
|
+
detail: "inside a work tree"
|
|
854
|
+
} : {
|
|
855
|
+
name: "git repo",
|
|
856
|
+
status: "warn",
|
|
857
|
+
detail: "not a git repository",
|
|
858
|
+
hint: "Run Ork inside a git repository so nodes can use worktrees."
|
|
859
|
+
}];
|
|
860
|
+
};
|
|
861
|
+
/**
|
|
862
|
+
* Runs every check: Node and git, then every registered Claude driver and
|
|
863
|
+
* integration (ADR-022). Node, git, and at least one driver are required;
|
|
864
|
+
* integrations are optional.
|
|
865
|
+
*/
|
|
866
|
+
const runChecks = async (run, env, nodeVersion) => {
|
|
867
|
+
const [git, checked, tools] = await Promise.all([
|
|
868
|
+
checkGit(run),
|
|
869
|
+
driverChecks(run, env),
|
|
870
|
+
Promise.all(INTEGRATIONS.flatMap((i) => i.doctor === void 0 ? [] : [i.doctor(run, env)]))
|
|
871
|
+
]);
|
|
872
|
+
const drivers = checked.map(({ result }) => result);
|
|
873
|
+
const results = [
|
|
874
|
+
checkNode(nodeVersion),
|
|
875
|
+
...git,
|
|
876
|
+
...drivers,
|
|
877
|
+
...tools
|
|
878
|
+
];
|
|
879
|
+
return {
|
|
880
|
+
results,
|
|
881
|
+
ok: drivers.some((d) => d.status === "ok") && !results.some((r) => r.status === "fail")
|
|
882
|
+
};
|
|
883
|
+
};
|
|
884
|
+
/**
|
|
885
|
+
* Whether a run's socket fits under `~` (L38). Past the limit a run can't
|
|
886
|
+
* listen, and nothing else says so: answers, commands and followers fall back
|
|
887
|
+
* to the store, a second at a time. `home` is the user's `~`.
|
|
888
|
+
*/
|
|
889
|
+
const socketCheck = (home, platform) => {
|
|
890
|
+
const { bytes, max } = socketRoom(home, platform);
|
|
891
|
+
if (max === void 0 || bytes <= max) return {
|
|
892
|
+
name: "sockets",
|
|
893
|
+
status: "ok",
|
|
894
|
+
detail: max === void 0 ? "not Unix sockets here" : `${String(bytes)} of ${String(max)} bytes`
|
|
895
|
+
};
|
|
896
|
+
return {
|
|
897
|
+
name: "sockets",
|
|
898
|
+
status: "warn",
|
|
899
|
+
detail: `a run's socket path is ${String(bytes)} bytes, past the ${String(max)} this system allows`,
|
|
900
|
+
hint: "Runs still work, but answers, commands and `ork logs --follow` wait up to a second for each, as they read the store instead. A shorter home folder path fixes it."
|
|
901
|
+
};
|
|
902
|
+
};
|
|
903
|
+
/**
|
|
904
|
+
* What it says of triggers (RFC-011): a long-lived process can die quietly, so
|
|
905
|
+
* a live trigger that hasn't looked when it should have is worth saying. `home`
|
|
906
|
+
* is the user's `~` (tests pass a folder).
|
|
907
|
+
*/
|
|
908
|
+
const triggerChecks = (home) => {
|
|
909
|
+
const store = openStore(storePath(home));
|
|
910
|
+
try {
|
|
911
|
+
const report = triggersReport(store, store.projects(), home);
|
|
912
|
+
const broken = report.broken.map(({ file, message }) => ({
|
|
913
|
+
name: "triggers",
|
|
914
|
+
status: "warn",
|
|
915
|
+
detail: `${file}: ${message}`,
|
|
916
|
+
hint: "Its triggers are off until it can be read."
|
|
917
|
+
}));
|
|
918
|
+
const live = report.triggers.filter(({ standing }) => standing === "live");
|
|
919
|
+
if (live.length === 0) return broken;
|
|
920
|
+
const late = lateTriggers(report, /* @__PURE__ */ new Date());
|
|
921
|
+
if (late.length === 0) return [...broken, {
|
|
922
|
+
name: "ork watch",
|
|
923
|
+
status: "ok",
|
|
924
|
+
detail: `${String(live.length)} trigger(s) looking`
|
|
925
|
+
}];
|
|
926
|
+
return [...broken, {
|
|
927
|
+
name: "ork watch",
|
|
928
|
+
status: "warn",
|
|
929
|
+
detail: `not looking: ${late.map(({ trigger }) => trigger).join(", ")}`,
|
|
930
|
+
hint: report.pid === void 0 ? "Start it with `ork watch --detach`." : "See `ork watch --status`."
|
|
931
|
+
}];
|
|
932
|
+
} finally {
|
|
933
|
+
store.close();
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
// src/run/ready-driver.ts
|
|
937
|
+
/**
|
|
938
|
+
* @file The driver a run takes for the session nodes that name none (core's
|
|
939
|
+
* `driver.ts` has the whole rule): the one `ork doctor`'s own checks find
|
|
940
|
+
* ready on this machine, said in one line. With none ready, the run stops
|
|
941
|
+
* before any session starts, with the hints `ork doctor` gives. A resumed run
|
|
942
|
+
* keeps the driver it recorded.
|
|
943
|
+
*/
|
|
944
|
+
/** The real checks: the tools on this machine and its environment. */
|
|
945
|
+
const checksHere = () => driverChecks(exec, process.env);
|
|
946
|
+
const subject = (names, all) => {
|
|
947
|
+
if (all) return "No driver is named, so the run uses";
|
|
948
|
+
const [only] = names;
|
|
949
|
+
return names.length === 1 && only !== void 0 ? `${only} names no driver, so it uses` : `${names.join(", ")} name no driver, so they use`;
|
|
950
|
+
};
|
|
951
|
+
/**
|
|
952
|
+
* The driver for `names`, the session nodes that name none, from `checks`.
|
|
953
|
+
* `all` says whether they are every session node of the run.
|
|
954
|
+
*/
|
|
955
|
+
const chooseReadyDriver = (names, all, checks) => {
|
|
956
|
+
const ready = readyDriver(checks);
|
|
957
|
+
if (!ready.ok) return {
|
|
958
|
+
ok: false,
|
|
959
|
+
lines: [
|
|
960
|
+
`No Claude driver is ready here, and ${names.join(", ")} ${names.length === 1 ? "names" : "name"} none.`,
|
|
961
|
+
...ready.hints,
|
|
962
|
+
"See `ork doctor`."
|
|
963
|
+
]
|
|
964
|
+
};
|
|
965
|
+
const why = ready.ready === 1 ? `the one ready here (${ready.detail})` : `more than one is ready, and a run takes ${ready.driver} first`;
|
|
966
|
+
return {
|
|
967
|
+
ok: true,
|
|
968
|
+
driver: ready.driver,
|
|
969
|
+
line: `${subject(names, all)} the ${ready.driver} driver: ${why}.`
|
|
970
|
+
};
|
|
971
|
+
};
|
|
972
|
+
/**
|
|
973
|
+
* What a run takes for its session nodes that name no driver: nothing when
|
|
974
|
+
* `--driver` overrides them all or each names one; the driver a resumed run
|
|
975
|
+
* recorded; otherwise the one ready here.
|
|
976
|
+
*/
|
|
977
|
+
const runReadyDriver = async (workflow, options) => {
|
|
978
|
+
if (options.override !== void 0) return { ok: true };
|
|
979
|
+
const names = namingNoDriver(workflow);
|
|
980
|
+
if (names.length === 0) return { ok: true };
|
|
981
|
+
if (options.recorded !== void 0) return {
|
|
982
|
+
ok: true,
|
|
983
|
+
driver: options.recorded
|
|
984
|
+
};
|
|
985
|
+
const sessions = Object.values(workflow.nodes).filter((node) => node.type === "claude.leader" || node.type === "claude.worker").length;
|
|
986
|
+
return chooseReadyDriver(names, names.length === sessions, await (options.checks ?? checksHere)());
|
|
537
987
|
};
|
|
538
988
|
// src/runs/chat.ts
|
|
539
989
|
/**
|
|
@@ -541,7 +991,8 @@ const createRecorder = (store, run, runDir) => {
|
|
|
541
991
|
* the store and the run's socket, and the run's bus pushes it into the node's
|
|
542
992
|
* session (specs/messaging.md#chat; `tellLiveRun` and `nodeSession` are in
|
|
543
993
|
* core). A node of a finished run gets it by resuming its last Claude session
|
|
544
|
-
* where it ran,
|
|
994
|
+
* where it ran, as the run's sessions were set up (ADR-065), and the exchange
|
|
995
|
+
* is added to the run's record.
|
|
545
996
|
*/
|
|
546
997
|
/** The declared node a spawned worker (`<template>-<n>`) is an instance of. */
|
|
547
998
|
const templateOf = (nodes, id) => {
|
|
@@ -549,13 +1000,16 @@ const templateOf = (nodes, id) => {
|
|
|
549
1000
|
const template = id.replace(/-\d+$/u, "");
|
|
550
1001
|
return template in nodes ? template : void 0;
|
|
551
1002
|
};
|
|
1003
|
+
/** The run's sessions loaded the machine's Claude setup, so this one does. */
|
|
1004
|
+
const machineClaudeOf = (events) => events.some(({ event }) => event.type === "run.started" && event.settings?.machineClaude === true) ? { machineClaude: true } : {};
|
|
552
1005
|
/**
|
|
553
1006
|
* Resumes a finished node's last session with the user's message, prints the
|
|
554
1007
|
* session, and adds it to the run's record. Returns why it could not.
|
|
555
1008
|
*/
|
|
556
1009
|
const resumeChat = async (options) => {
|
|
557
1010
|
const { store, run, node } = options;
|
|
558
|
-
const
|
|
1011
|
+
const events = store.events(run.id, 0);
|
|
1012
|
+
const { session, outputs } = nodeSession(events, node);
|
|
559
1013
|
if (!session.known) return `${node} is not a node of run ${run.id}.`;
|
|
560
1014
|
if (!session.chattable) return `${node} is not a leader or worker; only they have sessions to talk to.`;
|
|
561
1015
|
if (session.sessionId === void 0) return `${node} has no Claude session to resume in run ${run.id}.`;
|
|
@@ -581,6 +1035,12 @@ const resumeChat = async (options) => {
|
|
|
581
1035
|
...template === void 0 ? {} : { template }
|
|
582
1036
|
});
|
|
583
1037
|
if (!plan.ok) return plan.error;
|
|
1038
|
+
let driver = options.driver ?? session.driver ?? plan.driver;
|
|
1039
|
+
if (driver === void 0) {
|
|
1040
|
+
const ready = readyDriver(await (options.driverChecks ?? checksHere)());
|
|
1041
|
+
if (!ready.ok) return ["No Claude driver is ready here.", ...ready.hints].join(" ");
|
|
1042
|
+
driver = ready.driver;
|
|
1043
|
+
}
|
|
584
1044
|
const record = appendRecorder(store, run.id, runDir);
|
|
585
1045
|
record({
|
|
586
1046
|
type: "user.message",
|
|
@@ -588,10 +1048,11 @@ const resumeChat = async (options) => {
|
|
|
588
1048
|
body: options.body,
|
|
589
1049
|
delivered: "resumed"
|
|
590
1050
|
});
|
|
591
|
-
const started = (await options.runtime(
|
|
1051
|
+
const started = (await options.runtime(driver)).start({
|
|
592
1052
|
...plan.spec,
|
|
593
1053
|
resume: session.sessionId,
|
|
594
|
-
prompt: options.body
|
|
1054
|
+
prompt: options.body,
|
|
1055
|
+
...machineClaudeOf(events)
|
|
595
1056
|
}, { human: options.human });
|
|
596
1057
|
if (options.signal?.aborted === true) started.interrupt();
|
|
597
1058
|
options.signal?.addEventListener("abort", () => {
|
|
@@ -705,6 +1166,15 @@ const runNode = async (options) => {
|
|
|
705
1166
|
pluginDir: path.join(runDir, "plugins", options.nodeId)
|
|
706
1167
|
});
|
|
707
1168
|
if (!plan.ok) return failed([plan.error]);
|
|
1169
|
+
const named = sessionDriver({
|
|
1170
|
+
override: options.driver,
|
|
1171
|
+
named: plan.driver
|
|
1172
|
+
});
|
|
1173
|
+
const ready = named === void 0 ? chooseReadyDriver([options.nodeId], true, await (options.driverChecks ?? checksHere)()) : null;
|
|
1174
|
+
if (ready?.ok === false) return failed(ready.lines);
|
|
1175
|
+
if (ready?.line !== void 0) options.err(ready.line);
|
|
1176
|
+
const driver = named ?? ready?.driver;
|
|
1177
|
+
if (driver === void 0) return failed(["No Claude driver is ready here."]);
|
|
708
1178
|
const planned = workflow.nodes[options.nodeId];
|
|
709
1179
|
if (planned !== void 0 && "notify" in planned && planned.notify) options.err(`${style(["yellow", "bold"], "warning")} ${options.nodeId}'s notify: does nothing here: a run of one node has no message bus.`);
|
|
710
1180
|
const skipped = planned === void 0 ? [] : [...beforeOf(planned, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in planned && planned.verify !== void 0 ? ["verify"] : []];
|
|
@@ -758,7 +1228,8 @@ const runNode = async (options) => {
|
|
|
758
1228
|
nodes: [{
|
|
759
1229
|
id,
|
|
760
1230
|
type: nodeType
|
|
761
|
-
}]
|
|
1231
|
+
}],
|
|
1232
|
+
...options.machineClaude === true ? { settings: { machineClaude: true } } : {}
|
|
762
1233
|
});
|
|
763
1234
|
record({
|
|
764
1235
|
type: "node.started",
|
|
@@ -766,7 +1237,9 @@ const runNode = async (options) => {
|
|
|
766
1237
|
nodeType,
|
|
767
1238
|
round: 1
|
|
768
1239
|
});
|
|
769
|
-
const
|
|
1240
|
+
const runtime = await options.runtime(driver);
|
|
1241
|
+
if (options.machineClaude === true) plan.spec.machineClaude = true;
|
|
1242
|
+
const session = runtime.start(plan.spec, {
|
|
770
1243
|
human: control.human,
|
|
771
1244
|
...graph === null ? {} : { feedback: createFeedback({
|
|
772
1245
|
graph,
|
|
@@ -813,7 +1286,9 @@ const runNode = async (options) => {
|
|
|
813
1286
|
...result.error === void 0 ? {} : { error: result.error }
|
|
814
1287
|
}],
|
|
815
1288
|
reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
|
|
816
|
-
worktrees: []
|
|
1289
|
+
worktrees: [],
|
|
1290
|
+
skipped: 0,
|
|
1291
|
+
failuresHandled: 0
|
|
817
1292
|
}
|
|
818
1293
|
});
|
|
819
1294
|
await control.close();
|
|
@@ -828,7 +1303,8 @@ const runNode = async (options) => {
|
|
|
828
1303
|
* @file What a run needs to reach other runs: its port for `workflow.call`
|
|
829
1304
|
* nodes (RFC-004), its relay when links are on (RFC-003), and the budget a
|
|
830
1305
|
* caller has capped it to. A called run being resumed keeps its caller and
|
|
831
|
-
* its cap, which the run recorded
|
|
1306
|
+
* its cap, which the run recorded, and a run's called runs load the machine's
|
|
1307
|
+
* Claude setup when it does (ADR-065).
|
|
832
1308
|
*/
|
|
833
1309
|
const callPorts = (options) => {
|
|
834
1310
|
const { graph, store, runId, home, human, runtime, out } = options;
|
|
@@ -838,6 +1314,7 @@ const callPorts = (options) => {
|
|
|
838
1314
|
const baseRef = options.baseRef ?? options.resume?.settings.baseRef;
|
|
839
1315
|
const trigger = options.trigger ?? options.resume?.settings.trigger;
|
|
840
1316
|
const untrusted = options.untrusted ?? options.resume?.settings.untrusted;
|
|
1317
|
+
const machineClaude = options.machineClaude ?? options.resume?.settings.machineClaude ?? false;
|
|
841
1318
|
const own = graph.workflow.limits?.budgetUsd;
|
|
842
1319
|
const budgetUsd = cap === void 0 ? own : Math.min(cap, own ?? Number.POSITIVE_INFINITY);
|
|
843
1320
|
return {
|
|
@@ -859,6 +1336,7 @@ const callPorts = (options) => {
|
|
|
859
1336
|
home,
|
|
860
1337
|
adopt: adoptedCalls(store, runId),
|
|
861
1338
|
newId: () => newRunId(store),
|
|
1339
|
+
machineClaude,
|
|
862
1340
|
env: maskedEnv(maskedNamesOfRun(graph.workflow, options.place, store.projects())),
|
|
863
1341
|
resumeSession: (run, node, body, signal) => {
|
|
864
1342
|
const { blocked, leftUsd } = runLimits(store, options.place, home).now();
|
|
@@ -888,7 +1366,8 @@ const callPorts = (options) => {
|
|
|
888
1366
|
...calledBy === void 0 ? {} : { calledBy },
|
|
889
1367
|
...baseRef === void 0 ? {} : { baseRef },
|
|
890
1368
|
...trigger === void 0 ? {} : { trigger },
|
|
891
|
-
...untrusted === void 0 ? {} : { untrusted }
|
|
1369
|
+
...untrusted === void 0 ? {} : { untrusted },
|
|
1370
|
+
...machineClaude ? { machineClaude } : {}
|
|
892
1371
|
}
|
|
893
1372
|
};
|
|
894
1373
|
};
|
|
@@ -1009,6 +1488,16 @@ const preflight = async (options, given) => {
|
|
|
1009
1488
|
ok: false,
|
|
1010
1489
|
lines: refused
|
|
1011
1490
|
};
|
|
1491
|
+
const driver = await runReadyDriver(given.effective, {
|
|
1492
|
+
override: options.driver,
|
|
1493
|
+
recorded: options.resume?.settings.readyDriver,
|
|
1494
|
+
checks: options.driverChecks
|
|
1495
|
+
});
|
|
1496
|
+
if (!driver.ok) return {
|
|
1497
|
+
ok: false,
|
|
1498
|
+
lines: driver.lines
|
|
1499
|
+
};
|
|
1500
|
+
if (driver.line !== void 0) options.err(driver.line);
|
|
1012
1501
|
const checkout = await prepareCheckout(workflow, {
|
|
1013
1502
|
...options,
|
|
1014
1503
|
cwd: place.root
|
|
@@ -1019,7 +1508,8 @@ const preflight = async (options, given) => {
|
|
|
1019
1508
|
};
|
|
1020
1509
|
return {
|
|
1021
1510
|
...checkout,
|
|
1022
|
-
limits: limits.now
|
|
1511
|
+
limits: limits.now,
|
|
1512
|
+
...driver.driver === void 0 ? {} : { readyDriver: driver.driver }
|
|
1023
1513
|
};
|
|
1024
1514
|
};
|
|
1025
1515
|
// src/run/workflow.ts
|
|
@@ -1059,6 +1549,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1059
1549
|
const projects = options.store.projects();
|
|
1060
1550
|
const ready = await preflight(options, {
|
|
1061
1551
|
workflow,
|
|
1552
|
+
effective: graph.workflow,
|
|
1062
1553
|
place,
|
|
1063
1554
|
projects,
|
|
1064
1555
|
inputs: inputs.inputs
|
|
@@ -1119,6 +1610,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1119
1610
|
maskedNames
|
|
1120
1611
|
};
|
|
1121
1612
|
if (options.driver !== void 0) executorOptions.driver = options.driver;
|
|
1613
|
+
if (ready.readyDriver !== void 0) executorOptions.readyDriver = ready.readyDriver;
|
|
1122
1614
|
if (options.tools !== void 0) executorOptions.tools = options.tools;
|
|
1123
1615
|
const script = process.argv[1];
|
|
1124
1616
|
if (script !== void 0 && onPath(script)) executorOptions.orkBin = script;
|
|
@@ -1135,6 +1627,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1135
1627
|
human: control.human,
|
|
1136
1628
|
emit: onEvent
|
|
1137
1629
|
});
|
|
1630
|
+
if (ports.settings.machineClaude === true) executorOptions.machineClaude = true;
|
|
1138
1631
|
clearOutputFiles(runDir, options.err);
|
|
1139
1632
|
const handle = startRun({
|
|
1140
1633
|
graph: ports.graph,
|
|
@@ -1149,6 +1642,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1149
1642
|
settings: {
|
|
1150
1643
|
inPlace: options.inPlace,
|
|
1151
1644
|
...options.driver === void 0 ? {} : { driver: options.driver },
|
|
1645
|
+
...ready.readyDriver === void 0 ? {} : { readyDriver: ready.readyDriver },
|
|
1152
1646
|
...ports.settings
|
|
1153
1647
|
},
|
|
1154
1648
|
...ports.relay === void 0 ? {} : { relay: ports.relay },
|
|
@@ -1191,7 +1685,7 @@ const flagValues = (rawArgs, flag) => {
|
|
|
1191
1685
|
};
|
|
1192
1686
|
const isDriver = (value) => CLAUDE_DRIVERS.some((d) => d.id === value);
|
|
1193
1687
|
/** Loads the drivers only when a run starts, so other commands stay fast. */
|
|
1194
|
-
const loadRuntime = async (driver) => (await import("./src-
|
|
1688
|
+
const loadRuntime = async (driver) => (await import("./src-B9v0lDc6.mjs")).createRuntime(driver, sessionEnv(process.env));
|
|
1195
1689
|
/**
|
|
1196
1690
|
* Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
|
|
1197
1691
|
* so that the run ends tidily and leaves no result unscrubbed.
|
|
@@ -1247,6 +1741,11 @@ const run = defineCommand({
|
|
|
1247
1741
|
description: "Run although instructions or skills have uncommitted changes (worktrees get the committed versions)",
|
|
1248
1742
|
default: false
|
|
1249
1743
|
},
|
|
1744
|
+
"machine-claude": {
|
|
1745
|
+
type: "boolean",
|
|
1746
|
+
description: "Sessions load this machine's Claude setup: its MCP servers and Claude's memory (default: clean sessions)",
|
|
1747
|
+
default: false
|
|
1748
|
+
},
|
|
1250
1749
|
json: {
|
|
1251
1750
|
type: "boolean",
|
|
1252
1751
|
description: "Print the events as JSON lines",
|
|
@@ -1331,6 +1830,7 @@ const run = defineCommand({
|
|
|
1331
1830
|
cwd,
|
|
1332
1831
|
inputs: flagValues(rawArgs, "--input"),
|
|
1333
1832
|
inPlace: args["in-place"],
|
|
1833
|
+
...args["machine-claude"] ? { machineClaude: true } : {},
|
|
1334
1834
|
json: args.json,
|
|
1335
1835
|
runtime: loadRuntime,
|
|
1336
1836
|
store,
|
|
@@ -1514,8 +2014,9 @@ const chat = defineCommand({
|
|
|
1514
2014
|
/**
|
|
1515
2015
|
* @file `ork decide`: answer a live run's open decisions from any terminal (it
|
|
1516
2016
|
* writes to the store, then wakes the run on its socket; specs/messaging.md).
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
2017
|
+
* `--say` gives the answer a note, which goes to the run with it. With `--id`
|
|
2018
|
+
* and `--choose`, `--note` and `--rating` instead note how the decision's
|
|
2019
|
+
* node went, as the run's feedback (specs/feedback.md).
|
|
1519
2020
|
*/
|
|
1520
2021
|
const failed$2 = (message) => {
|
|
1521
2022
|
writeErr(message);
|
|
@@ -1548,8 +2049,13 @@ const noteProblem = (note, rating) => {
|
|
|
1548
2049
|
}, "--");
|
|
1549
2050
|
return checked.ok ? void 0 : checked.error;
|
|
1550
2051
|
};
|
|
2052
|
+
/** What the answer said, as stored (masked), after the answer's line. */
|
|
2053
|
+
const saidLine = (store, id) => {
|
|
2054
|
+
const note = store.decision(id)?.note;
|
|
2055
|
+
return note === void 0 ? "" : style("dim", ` · said: ${note}`);
|
|
2056
|
+
};
|
|
1551
2057
|
/** One line per open decision, for `--list`. */
|
|
1552
|
-
const formatDecisions = (decisions) => decisions.length === 0 ? ["No open decisions."] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)} ${d.question} ${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
|
|
2058
|
+
const formatDecisions = (decisions) => decisions.length === 0 ? ["No open decisions."] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)}${d.owner === void 0 ? "" : style("dim", ` via ${d.owner}`)} ${d.question} ${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
|
|
1553
2059
|
const decide = defineCommand({
|
|
1554
2060
|
meta: {
|
|
1555
2061
|
name: "decide",
|
|
@@ -1584,6 +2090,10 @@ const decide = defineCommand({
|
|
|
1584
2090
|
type: "string",
|
|
1585
2091
|
description: "…with this option"
|
|
1586
2092
|
},
|
|
2093
|
+
say: {
|
|
2094
|
+
type: "string",
|
|
2095
|
+
description: "…and say this with it: the answer's note, which goes to the run"
|
|
2096
|
+
},
|
|
1587
2097
|
note: {
|
|
1588
2098
|
type: "string",
|
|
1589
2099
|
description: "…and note how its node went; the note joins the run's feedback"
|
|
@@ -1616,12 +2126,12 @@ const decide = defineCommand({
|
|
|
1616
2126
|
failed$2(problem);
|
|
1617
2127
|
return;
|
|
1618
2128
|
}
|
|
1619
|
-
const answered = await answerDecision(store, args.id, args.choose);
|
|
2129
|
+
const answered = await answerDecision(store, args.id, args.choose, void 0, "decide", args.say);
|
|
1620
2130
|
if (!answered.ok) {
|
|
1621
2131
|
failed$2(answered.error);
|
|
1622
2132
|
return;
|
|
1623
2133
|
}
|
|
1624
|
-
write(`${style("green", "✓")} ${args.id}: ${args.choose}`);
|
|
2134
|
+
write(`${style("green", "✓")} ${args.id}: ${args.choose}${saidLine(store, args.id)}`);
|
|
1625
2135
|
const noted = noteDecision(store, args.id, args.note, args.rating);
|
|
1626
2136
|
if (noted === void 0) return;
|
|
1627
2137
|
if (noted.ok) write(`${style("green", "✓")} noted in item ${noted.item}`);
|
|
@@ -1632,6 +2142,10 @@ const decide = defineCommand({
|
|
|
1632
2142
|
failed$2("--note and --rating go with --id and --choose.");
|
|
1633
2143
|
return;
|
|
1634
2144
|
}
|
|
2145
|
+
if (args.say !== void 0) {
|
|
2146
|
+
failed$2("--say goes with --id and --choose; answering here asks for a note after each choice.");
|
|
2147
|
+
return;
|
|
2148
|
+
}
|
|
1635
2149
|
const filter = {};
|
|
1636
2150
|
if (args.run !== void 0 && args.run !== "") {
|
|
1637
2151
|
const found = findRun(store, args.run, process.cwd());
|
|
@@ -1659,9 +2173,13 @@ const decide = defineCommand({
|
|
|
1659
2173
|
};
|
|
1660
2174
|
if (d.recommended !== void 0) question.recommended = d.recommended;
|
|
1661
2175
|
if (d.context !== void 0) question.context = d.context;
|
|
2176
|
+
let note;
|
|
2177
|
+
question.onNote = (given) => {
|
|
2178
|
+
note = given;
|
|
2179
|
+
};
|
|
1662
2180
|
const choice = await human.ask(question);
|
|
1663
|
-
const answered = await answerDecision(store, d.id, choice);
|
|
1664
|
-
write(answered.ok ? `${style("green", "✓")} ${d.id}: ${choice}\n` : `${style("yellow", "!")} ${answered.error}\n`);
|
|
2181
|
+
const answered = await answerDecision(store, d.id, choice, void 0, "decide", note);
|
|
2182
|
+
write(answered.ok ? `${style("green", "✓")} ${d.id}: ${choice}${saidLine(store, d.id)}\n` : `${style("yellow", "!")} ${answered.error}\n`);
|
|
1665
2183
|
}
|
|
1666
2184
|
} finally {
|
|
1667
2185
|
human?.close();
|
|
@@ -1732,7 +2250,7 @@ const stop = defineCommand({
|
|
|
1732
2250
|
const resume = defineCommand({
|
|
1733
2251
|
meta: {
|
|
1734
2252
|
name: "resume",
|
|
1735
|
-
description: "Continue a stopped or crashed
|
|
2253
|
+
description: "Continue a run that didn't succeed (stopped, failed or crashed): finished nodes keep their results"
|
|
1736
2254
|
},
|
|
1737
2255
|
args: {
|
|
1738
2256
|
run: {
|
|
@@ -1821,216 +2339,6 @@ const resume = defineCommand({
|
|
|
1821
2339
|
}
|
|
1822
2340
|
}
|
|
1823
2341
|
});
|
|
1824
|
-
// src/update/notify.ts
|
|
1825
|
-
/**
|
|
1826
|
-
* @file The new-version notice (ADR-038). At most once a day a command starts a
|
|
1827
|
-
* detached copy of `ork` that asks the npm registry for the latest version
|
|
1828
|
-
* and records it in `~/.ork/update-check.json`, so no command waits on the
|
|
1829
|
-
* network; a later command, when a newer version is known, ends with one line
|
|
1830
|
-
* on stderr (once a day). Off in CI, without a terminal, with `--json`, and
|
|
1831
|
-
* with `ORK_NO_UPDATE_CHECK=1` or `NO_UPDATE_NOTIFIER=1`.
|
|
1832
|
-
*/
|
|
1833
|
-
/** Set in the background copy of `ork` that only runs the check. */
|
|
1834
|
-
const UPDATE_CHILD_ENV = "ORK_UPDATE_CHECK_CHILD";
|
|
1835
|
-
/**
|
|
1836
|
-
* Why the automatic check is off for this command, or `null` when it is on: the
|
|
1837
|
-
* environment's switches (core's, which `ork ui` reads too), then where a
|
|
1838
|
-
* notice on stderr would go.
|
|
1839
|
-
*/
|
|
1840
|
-
const updateCheckOff = (env, argv, terminal) => {
|
|
1841
|
-
const switched = updateSwitchOff(env);
|
|
1842
|
-
if (switched !== null) return switched === "CI" ? "in CI" : "turned off";
|
|
1843
|
-
if (isSwitchedOn(env["ORK_DETACHED"]) || !terminal) return "no terminal";
|
|
1844
|
-
if (argv.includes("--json")) return "JSON output";
|
|
1845
|
-
if (argv[0] === "upgrade") return "ork upgrade checks for itself";
|
|
1846
|
-
if (argv[0] === "ui" && argv.includes("--serve")) return "ork ui's server checks for itself";
|
|
1847
|
-
return null;
|
|
1848
|
-
};
|
|
1849
|
-
/** The notice's text. */
|
|
1850
|
-
const updateNotice = (latest, current) => `Ork ${latest} is out (you have ${current}). Upgrade with: ork upgrade`;
|
|
1851
|
-
/**
|
|
1852
|
-
* Starts the background check when one is due. Records the attempt first, so
|
|
1853
|
-
* commands started in the next moments do not start one too.
|
|
1854
|
-
*/
|
|
1855
|
-
const startUpdateCheck = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
|
|
1856
|
-
try {
|
|
1857
|
-
const cache = readUpdateCache(file);
|
|
1858
|
-
if (!checkDue(cache, now)) return;
|
|
1859
|
-
writeUpdateCache(file, {
|
|
1860
|
-
...cache,
|
|
1861
|
-
checkedAt: now.toISOString()
|
|
1862
|
-
});
|
|
1863
|
-
const child = spawn(process.execPath, [...process.execArgv, process.argv[1] ?? ""], {
|
|
1864
|
-
detached: true,
|
|
1865
|
-
stdio: "ignore",
|
|
1866
|
-
env: {
|
|
1867
|
-
...process.env,
|
|
1868
|
-
[UPDATE_CHILD_ENV]: "1"
|
|
1869
|
-
}
|
|
1870
|
-
});
|
|
1871
|
-
child.on("error", () => {});
|
|
1872
|
-
child.unref();
|
|
1873
|
-
} catch {}
|
|
1874
|
-
};
|
|
1875
|
-
/** The background copy's whole job: ask the registry, record the answer. */
|
|
1876
|
-
const runUpdateChild = async () => {
|
|
1877
|
-
const registry = registryOf(process.env);
|
|
1878
|
-
await refreshUpdateCache(updateCachePath(), ORK_NAME, /* @__PURE__ */ new Date(), registry === void 0 ? {} : { registry });
|
|
1879
|
-
};
|
|
1880
|
-
/** Prints the notice when a newer version is known and not told today. */
|
|
1881
|
-
const showUpdateNotice = (file = updateCachePath(), now = /* @__PURE__ */ new Date()) => {
|
|
1882
|
-
try {
|
|
1883
|
-
const cache = readUpdateCache(file);
|
|
1884
|
-
const latest = pendingNotice(cache, ORK_VERSION, now);
|
|
1885
|
-
if (cache === null || latest === null) return;
|
|
1886
|
-
const text = updateNotice(latest, ORK_VERSION);
|
|
1887
|
-
process.stderr.write(`\n${styleText("yellow", text, { stream: process.stderr })}\n`);
|
|
1888
|
-
writeUpdateCache(file, {
|
|
1889
|
-
...cache,
|
|
1890
|
-
notified: {
|
|
1891
|
-
version: latest,
|
|
1892
|
-
at: now.toISOString()
|
|
1893
|
-
}
|
|
1894
|
-
});
|
|
1895
|
-
} catch {}
|
|
1896
|
-
};
|
|
1897
|
-
// src/update/upgrade.ts
|
|
1898
|
-
/**
|
|
1899
|
-
* @file `ork doctor`'s line for Ork itself (ADR-038): the running version
|
|
1900
|
-
* against the last update check, read from the record without asking the
|
|
1901
|
-
* network. What `ork upgrade` does is core's (`planUpgrade`, `runInstall`),
|
|
1902
|
-
* shared with the server behind `ork ui`.
|
|
1903
|
-
*/
|
|
1904
|
-
/** `ork doctor`'s line for Ork itself, from the last check. */
|
|
1905
|
-
const versionCheck = (current, cache, checksOff) => {
|
|
1906
|
-
const newer = newerVersion(cache, current);
|
|
1907
|
-
if (newer !== null) return {
|
|
1908
|
-
name: "ork",
|
|
1909
|
-
status: "warn",
|
|
1910
|
-
detail: `${current}; ${newer} is out`,
|
|
1911
|
-
hint: "Run ork upgrade."
|
|
1912
|
-
};
|
|
1913
|
-
return {
|
|
1914
|
-
name: "ork",
|
|
1915
|
-
status: "ok",
|
|
1916
|
-
detail: `${current} (${cache?.latest === void 0 ? checksOff ? "update check off" : "not checked for updates yet" : `the latest as of ${cache.checkedAt.slice(0, 10)}`})`
|
|
1917
|
-
};
|
|
1918
|
-
};
|
|
1919
|
-
// src/doctor/checks.ts
|
|
1920
|
-
/**
|
|
1921
|
-
* @file The checks behind `ork doctor`: Ork's own version against the last
|
|
1922
|
-
* update check, and each tool's presence and login status, through an
|
|
1923
|
-
* injectable `exec` so tests need no real tools. Never prints a token or an
|
|
1924
|
-
* account.
|
|
1925
|
-
*/
|
|
1926
|
-
const TIMEOUT_MS = 15e3;
|
|
1927
|
-
const exec = (program, args) => new Promise((resolve) => {
|
|
1928
|
-
execFile(program, args, {
|
|
1929
|
-
timeout: TIMEOUT_MS,
|
|
1930
|
-
encoding: "utf8"
|
|
1931
|
-
}, (error, stdout) => {
|
|
1932
|
-
resolve({
|
|
1933
|
-
ok: error === null,
|
|
1934
|
-
stdout
|
|
1935
|
-
});
|
|
1936
|
-
});
|
|
1937
|
-
});
|
|
1938
|
-
/** Ork's own version, against the last update check (no network). */
|
|
1939
|
-
const orkCheck = () => versionCheck(ORK_VERSION, readUpdateCache(updateCachePath()), updateCheckOff(process.env, [], true) !== null);
|
|
1940
|
-
const MIN_NODE = [24, 20];
|
|
1941
|
-
const checkNode = (version) => {
|
|
1942
|
-
const [major = 0, minor = 0] = version.split(".").map(Number);
|
|
1943
|
-
const [minMajor, minMinor] = MIN_NODE;
|
|
1944
|
-
return major > minMajor || major === minMajor && minor >= minMinor ? {
|
|
1945
|
-
name: "node",
|
|
1946
|
-
status: "ok",
|
|
1947
|
-
detail: `v${version}`
|
|
1948
|
-
} : {
|
|
1949
|
-
name: "node",
|
|
1950
|
-
status: "fail",
|
|
1951
|
-
detail: `v${version}`,
|
|
1952
|
-
hint: `Ork needs Node ${MIN_NODE.join(".")} or later.`
|
|
1953
|
-
};
|
|
1954
|
-
};
|
|
1955
|
-
const checkGit = async (run) => {
|
|
1956
|
-
const version = await run("git", ["--version"]);
|
|
1957
|
-
if (!version.ok) return [{
|
|
1958
|
-
name: "git",
|
|
1959
|
-
status: "fail",
|
|
1960
|
-
detail: "not found",
|
|
1961
|
-
hint: "Install git; Ork gives each writing node its own worktree."
|
|
1962
|
-
}];
|
|
1963
|
-
const inside = await run("git", ["rev-parse", "--is-inside-work-tree"]);
|
|
1964
|
-
return [{
|
|
1965
|
-
name: "git",
|
|
1966
|
-
status: "ok",
|
|
1967
|
-
detail: firstLine(version.stdout)
|
|
1968
|
-
}, inside.ok ? {
|
|
1969
|
-
name: "git repo",
|
|
1970
|
-
status: "ok",
|
|
1971
|
-
detail: "inside a work tree"
|
|
1972
|
-
} : {
|
|
1973
|
-
name: "git repo",
|
|
1974
|
-
status: "warn",
|
|
1975
|
-
detail: "not a git repository",
|
|
1976
|
-
hint: "Run Ork inside a git repository so nodes can use worktrees."
|
|
1977
|
-
}];
|
|
1978
|
-
};
|
|
1979
|
-
/**
|
|
1980
|
-
* Runs every check: Node and git, then every registered Claude driver and
|
|
1981
|
-
* integration (ADR-022). Node, git, and at least one driver are required;
|
|
1982
|
-
* integrations are optional.
|
|
1983
|
-
*/
|
|
1984
|
-
const runChecks = async (run, env, nodeVersion) => {
|
|
1985
|
-
const [git, drivers, tools] = await Promise.all([
|
|
1986
|
-
checkGit(run),
|
|
1987
|
-
Promise.all(CLAUDE_DRIVERS.map((d) => d.doctor(run, env))),
|
|
1988
|
-
Promise.all(INTEGRATIONS.flatMap((i) => i.doctor === void 0 ? [] : [i.doctor(run, env)]))
|
|
1989
|
-
]);
|
|
1990
|
-
const results = [
|
|
1991
|
-
checkNode(nodeVersion),
|
|
1992
|
-
...git,
|
|
1993
|
-
...drivers,
|
|
1994
|
-
...tools
|
|
1995
|
-
];
|
|
1996
|
-
return {
|
|
1997
|
-
results,
|
|
1998
|
-
ok: drivers.some((d) => d.status === "ok") && !results.some((r) => r.status === "fail")
|
|
1999
|
-
};
|
|
2000
|
-
};
|
|
2001
|
-
/**
|
|
2002
|
-
* What it says of triggers (RFC-011): a long-lived process can die quietly, so
|
|
2003
|
-
* a live trigger that hasn't looked when it should have is worth saying. `home`
|
|
2004
|
-
* is the user's `~` (tests pass a folder).
|
|
2005
|
-
*/
|
|
2006
|
-
const triggerChecks = (home) => {
|
|
2007
|
-
const store = openStore(storePath(home));
|
|
2008
|
-
try {
|
|
2009
|
-
const report = triggersReport(store, store.projects(), home);
|
|
2010
|
-
const broken = report.broken.map(({ file, message }) => ({
|
|
2011
|
-
name: "triggers",
|
|
2012
|
-
status: "warn",
|
|
2013
|
-
detail: `${file}: ${message}`,
|
|
2014
|
-
hint: "Its triggers are off until it can be read."
|
|
2015
|
-
}));
|
|
2016
|
-
const live = report.triggers.filter(({ standing }) => standing === "live");
|
|
2017
|
-
if (live.length === 0) return broken;
|
|
2018
|
-
const late = lateTriggers(report, /* @__PURE__ */ new Date());
|
|
2019
|
-
if (late.length === 0) return [...broken, {
|
|
2020
|
-
name: "ork watch",
|
|
2021
|
-
status: "ok",
|
|
2022
|
-
detail: `${String(live.length)} trigger(s) looking`
|
|
2023
|
-
}];
|
|
2024
|
-
return [...broken, {
|
|
2025
|
-
name: "ork watch",
|
|
2026
|
-
status: "warn",
|
|
2027
|
-
detail: `not looking: ${late.map(({ trigger }) => trigger).join(", ")}`,
|
|
2028
|
-
hint: report.pid === void 0 ? "Start it with `ork watch --detach`." : "See `ork watch --status`."
|
|
2029
|
-
}];
|
|
2030
|
-
} finally {
|
|
2031
|
-
store.close();
|
|
2032
|
-
}
|
|
2033
|
-
};
|
|
2034
2342
|
// src/project/ork-skills.ts
|
|
2035
2343
|
/**
|
|
2036
2344
|
* @file The Claude Code plugin's skills (`plugins/ork/skills/`) as text, by
|
|
@@ -2041,6 +2349,7 @@ const triggerChecks = (home) => {
|
|
|
2041
2349
|
const ORK_SKILLS = {
|
|
2042
2350
|
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",
|
|
2043
2351
|
run: "---\nname: run\ndescription: Use when starting, following or explaining an Ork run - what it's doing, why it failed, what it cost, and what waits on the person - and when the person asks about their Ork runs.\n---\n\n# Running and following Ork workflows\n\nOrk runs workflows on the person's machine with their accounts, so a run spends\ntheir money and can push, merge or deploy. Start one only when they ask, and\nnever answer a decision for them.\n\n## Starting\n\n1. Find it: Ork's `workflows` tool gives each workflow's name and inputs.\n2. Check it first with `check` (`workflow: <name>`). Don't start one with an\n error.\n3. Start it with `start_run`, giving every required input. Claude Code asks the\n person first, unless they've allowed it. It answers with the run's id; the run goes on in the\n background, whatever happens to this conversation.\n\nWithout Ork's tools: `ork run <workflow> --detach --input name=value`.\n\n## Following\n\n- `run` gives a run's status, what it waits on, its cost so far, and each\n node's state. `runs` lists the newest runs.\n- `run_events` gives its newest events; `node` narrows them, and `last` or\n `after` widens them. A `node.finished` event with `ok: false` says why a node\n failed.\n- `explain_context` says what a node's session was given: its agent,\n instructions, skills and files. Use it when a node didn't know something.\n- `usage` says what runs spent, by workflow and model, and how close the limits\n are.\n\nSay what you found plainly: which node, what it said, and what it cost. Quote a\nnode's own words rather than retelling them.\n\n## What only the person does\n\n- **Decisions.** `decisions` lists the questions waiting on them: a gate, a loop\n that ran out of rounds, a permission. Tell them what each asks, the options,\n and the recommended one, and that they answer in `ork ui` or with\n `ork decide` in their own terminal. Don't answer one: never run\n `ork decide --id … --choose …`; with Ork's plugin, a hook stops it.\n- **Stopping, pausing, resuming.** Tell them the command: `ork stop <run>`,\n `ork pause <run>`, `ork continue <run>`, `ork resume <run>`. A node can be\n stopped alone with `--node <id>`.\n\n## Without Ork's tools\n\n| Want | Command |\n| ---------------------- | ------------------------------ |\n| Runs, newest first | `ork ps` |\n| A run's transcript | `ork logs <run>` (`--follow`) |\n| Its nodes and cost | `ork tree <run>` |\n| What runs spent | `ork usage` |\n| What waits on a person | `ork decide --list` |\n| A node's context | `ork context explain <wf> <n>` |\n",
|
|
2352
|
+
setup: "---\nname: setup\ndescription: Use when the person asks you to set up Ork, install it, or get started with it in a repository - checking it's installed, setting up the project, making Ork's tools available to you, and a first workflow.\n---\n\n# Setting up Ork\n\nOrk runs workflows on the person's machine with their own accounts. Setting it\nup changes their machine and their repository, so say what each step does and\nask before it. Work through these in order, and stop where they say no.\n\n## 1. Check\n\n```sh\nork --version\nork doctor\n```\n\n- **No `ork`:** it needs Node 24.20 or later (`node --version`). The install is\n `npm install --global @krapht/ork`; show it and run it only when they say\n yes. The guide:\n https://github.com/mimshins/ork/blob/main/docs/getting-started/installation.md\n- **Read `ork doctor` to them.** It needs git, a git repository with a commit,\n and one Claude driver. Don't run `git init` or a commit without asking.\n- **A driver not ready:** they log in themselves, with `claude` (the CLI\n driver), or by setting `ANTHROPIC_API_KEY` or a cloud provider in their own\n shell (the SDK driver). 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",
|
|
2044
2353
|
write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## Loops\n\nEvery cycle needs an edge with `maxRounds`. Give the way back a condition and\nthe way out its opposite, as `!ok` and `ok` above; a plain edge beside a loop\nfires every round. When a loop runs out, Ork asks a person what to do.\n\n## What `check` refuses, and why\n\n- **A value the source can't have** (`when.value`): `status == 'failed'` (it's\n `fail`), a gate's `choice == 'aprove'` for `approve`, a value outside a\n declared list. The message names the values it can have.\n- **A loop no condition ends**, a plain edge beside a loop, a loop of sessions\n with no `limits.budgetUsd` or `limits.timeout`: warnings, each worth fixing.\n- **`no way the walk tried reaches x`**: Ork tried every way through, and none\n got to `x`. A join waiting on a branch a first-match router never takes is\n the usual cause.\n"
|
|
2045
2354
|
};
|
|
2046
2355
|
// src/project/init-skills.ts
|
|
@@ -2224,6 +2533,7 @@ const doctor = defineCommand({
|
|
|
2224
2533
|
orkCheck(),
|
|
2225
2534
|
...results,
|
|
2226
2535
|
...place.mode === "attached" ? [] : projectChecks(place.root),
|
|
2536
|
+
socketCheck(),
|
|
2227
2537
|
...triggerChecks()
|
|
2228
2538
|
]) for (const line of formatCheck(result)) write(line);
|
|
2229
2539
|
if (!ok) {
|
|
@@ -2287,7 +2597,8 @@ const upgrade = defineCommand({
|
|
|
2287
2597
|
const registry = registryOf(process.env);
|
|
2288
2598
|
const latest = await fetchLatest(ORK_NAME, {
|
|
2289
2599
|
...registry === void 0 ? {} : { registry },
|
|
2290
|
-
timeoutMs: 15e3
|
|
2600
|
+
timeoutMs: 15e3,
|
|
2601
|
+
current: ORK_VERSION
|
|
2291
2602
|
});
|
|
2292
2603
|
const plan = planUpgrade({
|
|
2293
2604
|
name: ORK_NAME,
|
|
@@ -2663,13 +2974,22 @@ const formatRegistered = (project, created) => {
|
|
|
2663
2974
|
/**
|
|
2664
2975
|
* @file Scaffolding a project: `.ork/workflows/` with a starter workflow and
|
|
2665
2976
|
* the `.gitignore` entry for run state, never overwriting a file; then the
|
|
2666
|
-
* tool folders the project already uses are linked to `.ork/`.
|
|
2977
|
+
* tool folders the project already uses are linked to `.ork/`. The starter
|
|
2978
|
+
* names no driver, as it is committed: a run takes the one ready on the
|
|
2979
|
+
* machine running it, and `ork init` says which that is here.
|
|
2667
2980
|
*/
|
|
2668
2981
|
/** Run state that never belongs in git. */
|
|
2669
2982
|
const GITIGNORE_ENTRIES = [".ork/runs/"];
|
|
2670
|
-
const
|
|
2983
|
+
const STARTER_NAME = "hello";
|
|
2984
|
+
/** Where `ork init` writes the starter, relative to the project root. */
|
|
2985
|
+
const STARTER_FILE = path.join(WORKFLOW_DIR, `${STARTER_NAME}${WORKFLOW_SUFFIX}`);
|
|
2986
|
+
/**
|
|
2987
|
+
* The starter workflow. It names no driver: it is committed, and one person's
|
|
2988
|
+
* login doesn't belong in a shared file.
|
|
2989
|
+
*/
|
|
2990
|
+
const starterWorkflow = () => ({
|
|
2671
2991
|
schemaVersion: 1,
|
|
2672
|
-
name:
|
|
2992
|
+
name: STARTER_NAME,
|
|
2673
2993
|
description: "A leader plans the task, a worker does it, and you approve the result.",
|
|
2674
2994
|
inputs: { task: {
|
|
2675
2995
|
type: "string",
|
|
@@ -2677,7 +2997,6 @@ const STARTER = {
|
|
|
2677
2997
|
description: "What to do."
|
|
2678
2998
|
} },
|
|
2679
2999
|
defaults: {
|
|
2680
|
-
driver: "sdk",
|
|
2681
3000
|
model: "sonnet",
|
|
2682
3001
|
isolation: "worktree"
|
|
2683
3002
|
},
|
|
@@ -2702,7 +3021,34 @@ const STARTER = {
|
|
|
2702
3021
|
}
|
|
2703
3022
|
},
|
|
2704
3023
|
edges: ["lead -> approve"]
|
|
3024
|
+
});
|
|
3025
|
+
/**
|
|
3026
|
+
* What `ork init` says of drivers, in one line: the project's `defaults.driver`
|
|
3027
|
+
* when it names one, else the driver a run would take here, the one ready
|
|
3028
|
+
* (`cli` when both are), or how to get one ready.
|
|
3029
|
+
*/
|
|
3030
|
+
const driverLine = (checks, projectDefault) => {
|
|
3031
|
+
if (projectDefault !== void 0) return `Runs use the ${projectDefault} driver, ${PROJECT_FILE}'s default, unless a workflow names another.`;
|
|
3032
|
+
const ready = readyDriver(checks);
|
|
3033
|
+
if (ready.ok) {
|
|
3034
|
+
const why = ready.ready === 1 ? ready.detail : `more than one is ready, and ${ready.driver} comes first`;
|
|
3035
|
+
return `Runs use the driver that's ready when they start: ${ready.driver} here (${why}).`;
|
|
3036
|
+
}
|
|
3037
|
+
return `Runs use the driver that's ready when they start, and none is ready here yet: ${checks.find(({ id }) => id === "cli")?.result.hint ?? "see `ork doctor`."}`;
|
|
3038
|
+
};
|
|
3039
|
+
/**
|
|
3040
|
+
* The line for the project at `root`: `ork doctor`'s driver checks through
|
|
3041
|
+
* `run` and `env`, and the project's own default.
|
|
3042
|
+
*/
|
|
3043
|
+
const initDriverLine = async (run, env, root) => {
|
|
3044
|
+
const projectDefault = loadProject(root).project?.defaults?.driver;
|
|
3045
|
+
return driverLine(projectDefault === void 0 ? await driverChecks(run, env) : [], projectDefault);
|
|
2705
3046
|
};
|
|
3047
|
+
/**
|
|
3048
|
+
* The line for a starter about to be written in `root`, with the real tools and
|
|
3049
|
+
* environment; `null` when the starter is already there.
|
|
3050
|
+
*/
|
|
3051
|
+
const initDriverLineHere = (root) => existsSync(path.join(root, STARTER_FILE)) ? Promise.resolve(null) : initDriverLine(exec, process.env, root);
|
|
2706
3052
|
/** The run-state entries `root`'s `.gitignore` lacks. */
|
|
2707
3053
|
const unignored = (root) => {
|
|
2708
3054
|
const gitignore = path.join(root, ".gitignore");
|
|
@@ -2729,11 +3075,10 @@ const initProject = (cwd, options = {}) => {
|
|
|
2729
3075
|
const created = [];
|
|
2730
3076
|
const skipped = [];
|
|
2731
3077
|
mkdirSync(path.join(cwd, WORKFLOW_DIR), { recursive: true });
|
|
2732
|
-
|
|
2733
|
-
if (existsSync(path.join(cwd, starter))) skipped.push(starter);
|
|
3078
|
+
if (existsSync(path.join(cwd, STARTER_FILE))) skipped.push(STARTER_FILE);
|
|
2734
3079
|
else {
|
|
2735
|
-
writeFileSync(path.join(cwd,
|
|
2736
|
-
created.push(
|
|
3080
|
+
writeFileSync(path.join(cwd, STARTER_FILE), serializeWorkflow(starterWorkflow()));
|
|
3081
|
+
created.push(STARTER_FILE);
|
|
2737
3082
|
}
|
|
2738
3083
|
const missing = ignoreRunState(cwd);
|
|
2739
3084
|
if (missing.length > 0) created.push(`.gitignore (${missing.join(", ")})`);
|
|
@@ -3100,8 +3445,9 @@ const noteRun = (store, cwd, args) => {
|
|
|
3100
3445
|
* @file `ork feedback apply` (ADR-059): runs Ork's own `ork:feedback-apply` in
|
|
3101
3446
|
* the project an item came from, with the item as its untrusted input. Its
|
|
3102
3447
|
* worker proposes a fix on a branch and its gate asks to keep or discard it;
|
|
3103
|
-
* Ork never merges. Kept, the item is `resolved
|
|
3104
|
-
*
|
|
3448
|
+
* Ork never merges. Kept, the item is `resolved`, unless its branch holds no
|
|
3449
|
+
* new commits: then there is nothing to keep, and, as when discarded, the
|
|
3450
|
+
* run's worktree and branch go and the item stays `accepted`.
|
|
3105
3451
|
*/
|
|
3106
3452
|
/** The most of an item's records the worker reads, newest first. */
|
|
3107
3453
|
const RECORDS = 10;
|
|
@@ -3386,8 +3732,9 @@ const split = defineCommand({
|
|
|
3386
3732
|
/**
|
|
3387
3733
|
* @file `ork feedback export` and `import` (specs/feedback.md): feedback
|
|
3388
3734
|
* records as JSON lines, one record a line, so a team can move feedback
|
|
3389
|
-
* between machines without GitHub. A line is checked before it's added
|
|
3390
|
-
* record already here (its run and event) is
|
|
3735
|
+
* between machines without GitHub. A line is checked before it's added (one
|
|
3736
|
+
* from a newer Ork says so), and a record already here (its run and event) is
|
|
3737
|
+
* skipped.
|
|
3391
3738
|
*/
|
|
3392
3739
|
const Text = z.string();
|
|
3393
3740
|
const LineSchema = z.strictObject({
|
|
@@ -3425,6 +3772,11 @@ const exportLines = (records) => records.map(({ id: _id, ...record }) => JSON.st
|
|
|
3425
3772
|
feedbackVersion: 1,
|
|
3426
3773
|
...record
|
|
3427
3774
|
})).map((line) => `${line}\n`).join("");
|
|
3775
|
+
/** Why a line from a newer Ork is left out, or `null` when it isn't one. */
|
|
3776
|
+
const newerVersion = (value) => {
|
|
3777
|
+
const version = value !== null && typeof value === "object" ? Reflect.get(value, "feedbackVersion") : void 0;
|
|
3778
|
+
return typeof version === "number" && version > 1 ? `made by a newer Ork (\`feedbackVersion: ${String(version)}\`; this one reads ${String(1)}). Upgrade with \`ork upgrade\`.` : null;
|
|
3779
|
+
};
|
|
3428
3780
|
/** The records in a JSONL file's text, and what was wrong with the rest. */
|
|
3429
3781
|
const parseLines = (text) => {
|
|
3430
3782
|
const records = [];
|
|
@@ -3438,6 +3790,11 @@ const parseLines = (text) => {
|
|
|
3438
3790
|
problems.push(`line ${String(index + 1)}: not JSON`);
|
|
3439
3791
|
continue;
|
|
3440
3792
|
}
|
|
3793
|
+
const newer = newerVersion(value);
|
|
3794
|
+
if (newer !== null) {
|
|
3795
|
+
problems.push(`line ${String(index + 1)}: ${newer}`);
|
|
3796
|
+
continue;
|
|
3797
|
+
}
|
|
3441
3798
|
const parsed = LineSchema.safeParse(value);
|
|
3442
3799
|
if (!parsed.success) {
|
|
3443
3800
|
const [issue] = parsed.error.issues;
|
|
@@ -3626,7 +3983,7 @@ const json$1 = {
|
|
|
3626
3983
|
const feedback = defineCommand({
|
|
3627
3984
|
meta: {
|
|
3628
3985
|
name: "feedback",
|
|
3629
|
-
description: "What got in the way in your runs, grouped: list, show, note,
|
|
3986
|
+
description: "What got in the way in your runs, grouped: list, show, note, dismiss, restore, accept, apply, triage, split, publish, export, import"
|
|
3630
3987
|
},
|
|
3631
3988
|
subCommands: {
|
|
3632
3989
|
list: defineCommand({
|
|
@@ -3973,7 +4330,8 @@ const skillsStep = async (root, options) => {
|
|
|
3973
4330
|
};
|
|
3974
4331
|
// src/commands/setup.ts
|
|
3975
4332
|
/**
|
|
3976
|
-
* @file Setting up: `ork init` scaffolds `.ork/` in a project,
|
|
4333
|
+
* @file Setting up: `ork init` scaffolds `.ork/` in a project, with a starter
|
|
4334
|
+
* that names no driver (it says which a run takes here), records it, and
|
|
3977
4335
|
* links the tool folders it uses; `ork link` points tools' agents and skills
|
|
3978
4336
|
* folders at `.ork/` (never in an attached project); `ork agents` lists,
|
|
3979
4337
|
* shows, adds and removes the agents nodes can take. `ork doctor` and `ork
|
|
@@ -3996,10 +4354,12 @@ const init = defineCommand({
|
|
|
3996
4354
|
}
|
|
3997
4355
|
},
|
|
3998
4356
|
run: async ({ args }) => {
|
|
4357
|
+
const cwd = process.cwd();
|
|
4358
|
+
const driverLine = await initDriverLineHere(cwd);
|
|
3999
4359
|
const store = openStore();
|
|
4000
4360
|
let result;
|
|
4001
4361
|
try {
|
|
4002
|
-
result = initRecorded(store,
|
|
4362
|
+
result = initRecorded(store, cwd, { link: args.link });
|
|
4003
4363
|
} finally {
|
|
4004
4364
|
store.close();
|
|
4005
4365
|
}
|
|
@@ -4011,8 +4371,9 @@ const init = defineCommand({
|
|
|
4011
4371
|
const { created, skipped, links } = result;
|
|
4012
4372
|
for (const item of created) write(`${style("green", "created")} ${item}`);
|
|
4013
4373
|
for (const item of skipped) write(`${style("dim", "exists")} ${item}`);
|
|
4374
|
+
if (driverLine !== null && created.includes(STARTER_FILE)) write(driverLine);
|
|
4014
4375
|
for (const line of formatLinks(links.filter((l) => l.state !== "linked"))) write(line);
|
|
4015
|
-
await skillsStep(
|
|
4376
|
+
await skillsStep(cwd, {
|
|
4016
4377
|
skills: args.skills,
|
|
4017
4378
|
link: args.link
|
|
4018
4379
|
});
|
|
@@ -4024,11 +4385,18 @@ const link = defineCommand({
|
|
|
4024
4385
|
name: "link",
|
|
4025
4386
|
description: "Point tools' agents and skills folders (.claude/agents, …) at .ork/"
|
|
4026
4387
|
},
|
|
4027
|
-
args: {
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4388
|
+
args: {
|
|
4389
|
+
tools: {
|
|
4390
|
+
type: "positional",
|
|
4391
|
+
description: "The tools to link, e.g. claude opencode (default: the ones the project already uses)",
|
|
4392
|
+
required: false
|
|
4393
|
+
},
|
|
4394
|
+
check: {
|
|
4395
|
+
type: "boolean",
|
|
4396
|
+
description: "Only show each folder's state; change nothing",
|
|
4397
|
+
default: false
|
|
4398
|
+
}
|
|
4399
|
+
},
|
|
4032
4400
|
run: ({ args }) => {
|
|
4033
4401
|
const place = projectAt(process.cwd());
|
|
4034
4402
|
const { root } = place;
|
|
@@ -4189,7 +4557,10 @@ const agents = defineCommand({
|
|
|
4189
4557
|
}
|
|
4190
4558
|
});
|
|
4191
4559
|
// src/main.ts
|
|
4192
|
-
/**
|
|
4560
|
+
/**
|
|
4561
|
+
* @file The root `ork` command and its subcommands. `ork --help` lists them in
|
|
4562
|
+
* the groups of `help-groups.ts`.
|
|
4563
|
+
*/
|
|
4193
4564
|
const main = defineCommand({
|
|
4194
4565
|
meta: ORK_META,
|
|
4195
4566
|
subCommands: {
|
|
@@ -4204,25 +4575,27 @@ const main = defineCommand({
|
|
|
4204
4575
|
ps,
|
|
4205
4576
|
logs,
|
|
4206
4577
|
tree,
|
|
4207
|
-
usage: () => import("./usage-
|
|
4208
|
-
prune: () => import("./usage-
|
|
4578
|
+
usage: () => import("./usage-EW7DI6Z7.mjs").then((module) => module.usage),
|
|
4579
|
+
prune: () => import("./usage-EW7DI6Z7.mjs").then((module) => module.prune),
|
|
4209
4580
|
decide,
|
|
4210
|
-
relay: () => import("./relay-
|
|
4211
|
-
pause: () => import("./pause-
|
|
4212
|
-
continue: () => import("./pause-
|
|
4581
|
+
relay: () => import("./relay-BipJYszF.mjs").then((module) => module.relay),
|
|
4582
|
+
pause: () => import("./pause-C7lJkSma.mjs").then((module) => module.pause),
|
|
4583
|
+
continue: () => import("./pause-C7lJkSma.mjs").then((module) => module.continueRun),
|
|
4213
4584
|
stop,
|
|
4214
4585
|
chat,
|
|
4215
|
-
send: () => import("./send-
|
|
4586
|
+
send: () => import("./send-nbkjf3sc.mjs").then((module) => module.send),
|
|
4216
4587
|
resume,
|
|
4217
4588
|
feedback,
|
|
4218
4589
|
context,
|
|
4219
|
-
worktrees: () => import("./worktrees-
|
|
4220
|
-
wt: () => import("./worktrees-
|
|
4221
|
-
watch: () => import("./watch-
|
|
4222
|
-
mcp: () => import("./mcp-
|
|
4223
|
-
ui: () => import("./ui-
|
|
4590
|
+
worktrees: () => import("./worktrees-Ahj9rdWK.mjs").then((module) => module.worktrees),
|
|
4591
|
+
wt: () => import("./worktrees-Ahj9rdWK.mjs").then((module) => module.worktrees),
|
|
4592
|
+
watch: () => import("./watch-BAkSGaOp.mjs").then((module) => module.watch),
|
|
4593
|
+
mcp: () => import("./mcp-DM30MFBw.mjs").then((module) => module.mcp),
|
|
4594
|
+
ui: () => import("./ui-B98FSboa.mjs").then((module) => module.ui),
|
|
4224
4595
|
doctor,
|
|
4225
|
-
upgrade
|
|
4596
|
+
upgrade,
|
|
4597
|
+
completion: () => import("./completion-DBGfL-gN.mjs").then((module) => module.completion),
|
|
4598
|
+
__complete: () => import("./completion-DBGfL-gN.mjs").then((module) => module.completeCommand(main))
|
|
4226
4599
|
}
|
|
4227
4600
|
});
|
|
4228
4601
|
// src/bin.ts
|
|
@@ -4230,17 +4603,26 @@ const main = defineCommand({
|
|
|
4230
4603
|
* @file The `ork` executable: runs the CLI's root command. The published
|
|
4231
4604
|
* package's `bin` points at its bundle. When the new-version check is on, it
|
|
4232
4605
|
* starts the daily background check and ends with the notice (ADR-038); the
|
|
4233
|
-
* background copy it starts only runs the check.
|
|
4606
|
+
* background copy it starts only runs the check. It first clears git's local
|
|
4607
|
+
* variables (a hook's `GIT_DIR`, `GIT_INDEX_FILE`, …), so nothing Ork starts
|
|
4608
|
+
* acts on the hook's repository (specs/worktrees.md). `--help` leaves out the
|
|
4609
|
+
* flags only Ork passes to itself. `ork __complete`, which tab completion
|
|
4610
|
+
* runs on every Tab, goes first: no update check, and no citty, so a typed
|
|
4611
|
+
* `--help` is a word to complete.
|
|
4234
4612
|
*/
|
|
4235
|
-
|
|
4613
|
+
clearGitLocalEnv();
|
|
4614
|
+
const argv = process.argv.slice(2);
|
|
4615
|
+
if (argv[0] === "__complete") {
|
|
4616
|
+
const { printCompletions } = await import("./completion-DBGfL-gN.mjs");
|
|
4617
|
+
await printCompletions(main, argv.slice(1));
|
|
4618
|
+
} else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
|
|
4236
4619
|
else {
|
|
4237
|
-
const argv = process.argv.slice(2);
|
|
4238
4620
|
if (updateCheckOff(process.env, argv, process.stderr.isTTY) === null) {
|
|
4239
4621
|
startUpdateCheck();
|
|
4240
4622
|
process.once("exit", () => {
|
|
4241
4623
|
showUpdateNotice();
|
|
4242
4624
|
});
|
|
4243
4625
|
}
|
|
4244
|
-
await runMain(main);
|
|
4626
|
+
await runMain(main, { showUsage: showHelp });
|
|
4245
4627
|
}
|
|
4246
4628
|
export {};
|