@krapht/ork 0.4.0 → 1.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -12
- package/dist/{__build_info__-CtSVVuZE.mjs → __build_info__-DdTVd2p5.mjs} +36 -112
- package/dist/bin.mjs +663 -300
- package/dist/{clean-Dcr807be.mjs → clean-NnboOuz6.mjs} +1 -1
- package/dist/completion-snnYGjgK.mjs +405 -0
- package/dist/{context-C_bksb0C.mjs → context-DO3sBy9v.mjs} +11 -88
- package/dist/decide-B81wCZQg.mjs +113 -0
- package/dist/{find-DptmbhGd.mjs → find-BKW-uGey.mjs} +1 -1
- package/dist/{format-J-a21Yvn.mjs → format-CmTlTOjT.mjs} +71 -37
- package/dist/help-groups-BwYoAWsh.mjs +354 -0
- package/dist/{in-a-run-D1Qc5Mit.mjs → in-a-run-BPXX09Bs.mjs} +1 -1
- package/dist/{limits-BDZCmGEu.mjs → limits-JCve2QpI.mjs} +1 -1
- package/dist/{list-ZvY2OcLq.mjs → list-C7UEgmtd.mjs} +1 -1
- package/dist/{mcp-CHVKKg3B.mjs → mcp-CKPyNaKD.mjs} +7 -7
- package/dist/{output-CXSJO00X.mjs → output-B1V6ZJSx.mjs} +67 -17
- package/dist/{pause-BPSmg4yz.mjs → pause-B_SL_yDz.mjs} +4 -4
- package/dist/{relay-Do-aNbSD.mjs → relay-o-Luee1c.mjs} +2 -2
- package/dist/{remove-AwaMMaHf.mjs → remove-DAZ7ZCzt.mjs} +3 -3
- package/dist/{send-DhGBkowy.mjs → send-3B64fBNB.mjs} +3 -3
- package/dist/{src-CpDKIulI.mjs → src-BJTegEzY.mjs} +23 -13
- package/dist/{src-UWkk1sl9.mjs → src-C38BPzit.mjs} +251 -18
- package/dist/{src-CGqMati3.mjs → src-DHvWU4tX.mjs} +636 -217
- package/dist/{src-BHnxpJyH.mjs → src-Dyz_Gy1q.mjs} +35 -8
- package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
- package/dist/ui/assets/app-C8nJ6NpT.js +67 -0
- package/dist/ui/assets/app-CH7YC3in.css +1 -0
- package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
- package/dist/ui/assets/{index-MQNNgsyT.js → index-D83F2OKZ.js} +2 -2
- package/dist/ui/index.html +2 -3
- package/dist/{ui-DhutUMgQ.mjs → ui-CY4xxJUu.mjs} +5 -5
- package/dist/{usage-CT7Z-yjr.mjs → usage-CJnytoe4.mjs} +6 -6
- package/dist/{watch-0Qr2UfCQ.mjs → watch-BJB82ZJ7.mjs} +7 -7
- package/dist/workflows-CiM7S3l0.mjs +88 -0
- package/dist/{worktrees-DHl80k96.mjs → worktrees-86ySXxR6.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-DHvWU4tX.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__-DdTVd2p5.mjs";
|
|
4
|
+
import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-B81wCZQg.mjs";
|
|
5
|
+
import { n as newRunId, t as findRun } from "./find-BKW-uGey.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-B1V6ZJSx.mjs";
|
|
7
|
+
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-C7UEgmtd.mjs";
|
|
8
|
+
import { a as planClean, t as applyClean } from "./clean-NnboOuz6.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-CmTlTOjT.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-CiM7S3l0.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-DO3sBy9v.mjs";
|
|
13
|
+
import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-JCve2QpI.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-C38BPzit.mjs";
|
|
15
|
+
import { i as decideRefusal, n as chatRefusal } from "./in-a-run-BPXX09Bs.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
|
/**
|
|
@@ -581,6 +1031,12 @@ const resumeChat = async (options) => {
|
|
|
581
1031
|
...template === void 0 ? {} : { template }
|
|
582
1032
|
});
|
|
583
1033
|
if (!plan.ok) return plan.error;
|
|
1034
|
+
let driver = options.driver ?? session.driver ?? plan.driver;
|
|
1035
|
+
if (driver === void 0) {
|
|
1036
|
+
const ready = readyDriver(await (options.driverChecks ?? checksHere)());
|
|
1037
|
+
if (!ready.ok) return ["No Claude driver is ready here.", ...ready.hints].join(" ");
|
|
1038
|
+
driver = ready.driver;
|
|
1039
|
+
}
|
|
584
1040
|
const record = appendRecorder(store, run.id, runDir);
|
|
585
1041
|
record({
|
|
586
1042
|
type: "user.message",
|
|
@@ -588,7 +1044,7 @@ const resumeChat = async (options) => {
|
|
|
588
1044
|
body: options.body,
|
|
589
1045
|
delivered: "resumed"
|
|
590
1046
|
});
|
|
591
|
-
const started = (await options.runtime(
|
|
1047
|
+
const started = (await options.runtime(driver)).start({
|
|
592
1048
|
...plan.spec,
|
|
593
1049
|
resume: session.sessionId,
|
|
594
1050
|
prompt: options.body
|
|
@@ -705,6 +1161,15 @@ const runNode = async (options) => {
|
|
|
705
1161
|
pluginDir: path.join(runDir, "plugins", options.nodeId)
|
|
706
1162
|
});
|
|
707
1163
|
if (!plan.ok) return failed([plan.error]);
|
|
1164
|
+
const named = sessionDriver({
|
|
1165
|
+
override: options.driver,
|
|
1166
|
+
named: plan.driver
|
|
1167
|
+
});
|
|
1168
|
+
const ready = named === void 0 ? chooseReadyDriver([options.nodeId], true, await (options.driverChecks ?? checksHere)()) : null;
|
|
1169
|
+
if (ready?.ok === false) return failed(ready.lines);
|
|
1170
|
+
if (ready?.line !== void 0) options.err(ready.line);
|
|
1171
|
+
const driver = named ?? ready?.driver;
|
|
1172
|
+
if (driver === void 0) return failed(["No Claude driver is ready here."]);
|
|
708
1173
|
const planned = workflow.nodes[options.nodeId];
|
|
709
1174
|
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
1175
|
const skipped = planned === void 0 ? [] : [...beforeOf(planned, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in planned && planned.verify !== void 0 ? ["verify"] : []];
|
|
@@ -766,7 +1231,7 @@ const runNode = async (options) => {
|
|
|
766
1231
|
nodeType,
|
|
767
1232
|
round: 1
|
|
768
1233
|
});
|
|
769
|
-
const session = (await options.runtime(
|
|
1234
|
+
const session = (await options.runtime(driver)).start(plan.spec, {
|
|
770
1235
|
human: control.human,
|
|
771
1236
|
...graph === null ? {} : { feedback: createFeedback({
|
|
772
1237
|
graph,
|
|
@@ -813,7 +1278,9 @@ const runNode = async (options) => {
|
|
|
813
1278
|
...result.error === void 0 ? {} : { error: result.error }
|
|
814
1279
|
}],
|
|
815
1280
|
reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
|
|
816
|
-
worktrees: []
|
|
1281
|
+
worktrees: [],
|
|
1282
|
+
skipped: 0,
|
|
1283
|
+
failuresHandled: 0
|
|
817
1284
|
}
|
|
818
1285
|
});
|
|
819
1286
|
await control.close();
|
|
@@ -1009,6 +1476,16 @@ const preflight = async (options, given) => {
|
|
|
1009
1476
|
ok: false,
|
|
1010
1477
|
lines: refused
|
|
1011
1478
|
};
|
|
1479
|
+
const driver = await runReadyDriver(given.effective, {
|
|
1480
|
+
override: options.driver,
|
|
1481
|
+
recorded: options.resume?.settings.readyDriver,
|
|
1482
|
+
checks: options.driverChecks
|
|
1483
|
+
});
|
|
1484
|
+
if (!driver.ok) return {
|
|
1485
|
+
ok: false,
|
|
1486
|
+
lines: driver.lines
|
|
1487
|
+
};
|
|
1488
|
+
if (driver.line !== void 0) options.err(driver.line);
|
|
1012
1489
|
const checkout = await prepareCheckout(workflow, {
|
|
1013
1490
|
...options,
|
|
1014
1491
|
cwd: place.root
|
|
@@ -1019,7 +1496,8 @@ const preflight = async (options, given) => {
|
|
|
1019
1496
|
};
|
|
1020
1497
|
return {
|
|
1021
1498
|
...checkout,
|
|
1022
|
-
limits: limits.now
|
|
1499
|
+
limits: limits.now,
|
|
1500
|
+
...driver.driver === void 0 ? {} : { readyDriver: driver.driver }
|
|
1023
1501
|
};
|
|
1024
1502
|
};
|
|
1025
1503
|
// src/run/workflow.ts
|
|
@@ -1059,6 +1537,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1059
1537
|
const projects = options.store.projects();
|
|
1060
1538
|
const ready = await preflight(options, {
|
|
1061
1539
|
workflow,
|
|
1540
|
+
effective: graph.workflow,
|
|
1062
1541
|
place,
|
|
1063
1542
|
projects,
|
|
1064
1543
|
inputs: inputs.inputs
|
|
@@ -1119,6 +1598,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1119
1598
|
maskedNames
|
|
1120
1599
|
};
|
|
1121
1600
|
if (options.driver !== void 0) executorOptions.driver = options.driver;
|
|
1601
|
+
if (ready.readyDriver !== void 0) executorOptions.readyDriver = ready.readyDriver;
|
|
1122
1602
|
if (options.tools !== void 0) executorOptions.tools = options.tools;
|
|
1123
1603
|
const script = process.argv[1];
|
|
1124
1604
|
if (script !== void 0 && onPath(script)) executorOptions.orkBin = script;
|
|
@@ -1149,6 +1629,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1149
1629
|
settings: {
|
|
1150
1630
|
inPlace: options.inPlace,
|
|
1151
1631
|
...options.driver === void 0 ? {} : { driver: options.driver },
|
|
1632
|
+
...ready.readyDriver === void 0 ? {} : { readyDriver: ready.readyDriver },
|
|
1152
1633
|
...ports.settings
|
|
1153
1634
|
},
|
|
1154
1635
|
...ports.relay === void 0 ? {} : { relay: ports.relay },
|
|
@@ -1191,7 +1672,7 @@ const flagValues = (rawArgs, flag) => {
|
|
|
1191
1672
|
};
|
|
1192
1673
|
const isDriver = (value) => CLAUDE_DRIVERS.some((d) => d.id === value);
|
|
1193
1674
|
/** Loads the drivers only when a run starts, so other commands stay fast. */
|
|
1194
|
-
const loadRuntime = async (driver) => (await import("./src-
|
|
1675
|
+
const loadRuntime = async (driver) => (await import("./src-Dyz_Gy1q.mjs")).createRuntime(driver, sessionEnv(process.env));
|
|
1195
1676
|
/**
|
|
1196
1677
|
* Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
|
|
1197
1678
|
* so that the run ends tidily and leaves no result unscrubbed.
|
|
@@ -1514,8 +1995,9 @@ const chat = defineCommand({
|
|
|
1514
1995
|
/**
|
|
1515
1996
|
* @file `ork decide`: answer a live run's open decisions from any terminal (it
|
|
1516
1997
|
* writes to the store, then wakes the run on its socket; specs/messaging.md).
|
|
1517
|
-
*
|
|
1518
|
-
*
|
|
1998
|
+
* `--say` gives the answer a note, which goes to the run with it. With `--id`
|
|
1999
|
+
* and `--choose`, `--note` and `--rating` instead note how the decision's
|
|
2000
|
+
* node went, as the run's feedback (specs/feedback.md).
|
|
1519
2001
|
*/
|
|
1520
2002
|
const failed$2 = (message) => {
|
|
1521
2003
|
writeErr(message);
|
|
@@ -1548,8 +2030,13 @@ const noteProblem = (note, rating) => {
|
|
|
1548
2030
|
}, "--");
|
|
1549
2031
|
return checked.ok ? void 0 : checked.error;
|
|
1550
2032
|
};
|
|
2033
|
+
/** What the answer said, as stored (masked), after the answer's line. */
|
|
2034
|
+
const saidLine = (store, id) => {
|
|
2035
|
+
const note = store.decision(id)?.note;
|
|
2036
|
+
return note === void 0 ? "" : style("dim", ` · said: ${note}`);
|
|
2037
|
+
};
|
|
1551
2038
|
/** 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(", ")}]`)}`);
|
|
2039
|
+
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
2040
|
const decide = defineCommand({
|
|
1554
2041
|
meta: {
|
|
1555
2042
|
name: "decide",
|
|
@@ -1584,6 +2071,10 @@ const decide = defineCommand({
|
|
|
1584
2071
|
type: "string",
|
|
1585
2072
|
description: "…with this option"
|
|
1586
2073
|
},
|
|
2074
|
+
say: {
|
|
2075
|
+
type: "string",
|
|
2076
|
+
description: "…and say this with it: the answer's note, which goes to the run"
|
|
2077
|
+
},
|
|
1587
2078
|
note: {
|
|
1588
2079
|
type: "string",
|
|
1589
2080
|
description: "…and note how its node went; the note joins the run's feedback"
|
|
@@ -1616,12 +2107,12 @@ const decide = defineCommand({
|
|
|
1616
2107
|
failed$2(problem);
|
|
1617
2108
|
return;
|
|
1618
2109
|
}
|
|
1619
|
-
const answered = await answerDecision(store, args.id, args.choose);
|
|
2110
|
+
const answered = await answerDecision(store, args.id, args.choose, void 0, "decide", args.say);
|
|
1620
2111
|
if (!answered.ok) {
|
|
1621
2112
|
failed$2(answered.error);
|
|
1622
2113
|
return;
|
|
1623
2114
|
}
|
|
1624
|
-
write(`${style("green", "✓")} ${args.id}: ${args.choose}`);
|
|
2115
|
+
write(`${style("green", "✓")} ${args.id}: ${args.choose}${saidLine(store, args.id)}`);
|
|
1625
2116
|
const noted = noteDecision(store, args.id, args.note, args.rating);
|
|
1626
2117
|
if (noted === void 0) return;
|
|
1627
2118
|
if (noted.ok) write(`${style("green", "✓")} noted in item ${noted.item}`);
|
|
@@ -1632,6 +2123,10 @@ const decide = defineCommand({
|
|
|
1632
2123
|
failed$2("--note and --rating go with --id and --choose.");
|
|
1633
2124
|
return;
|
|
1634
2125
|
}
|
|
2126
|
+
if (args.say !== void 0) {
|
|
2127
|
+
failed$2("--say goes with --id and --choose; answering here asks for a note after each choice.");
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
1635
2130
|
const filter = {};
|
|
1636
2131
|
if (args.run !== void 0 && args.run !== "") {
|
|
1637
2132
|
const found = findRun(store, args.run, process.cwd());
|
|
@@ -1659,9 +2154,13 @@ const decide = defineCommand({
|
|
|
1659
2154
|
};
|
|
1660
2155
|
if (d.recommended !== void 0) question.recommended = d.recommended;
|
|
1661
2156
|
if (d.context !== void 0) question.context = d.context;
|
|
2157
|
+
let note;
|
|
2158
|
+
question.onNote = (given) => {
|
|
2159
|
+
note = given;
|
|
2160
|
+
};
|
|
1662
2161
|
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`);
|
|
2162
|
+
const answered = await answerDecision(store, d.id, choice, void 0, "decide", note);
|
|
2163
|
+
write(answered.ok ? `${style("green", "✓")} ${d.id}: ${choice}${saidLine(store, d.id)}\n` : `${style("yellow", "!")} ${answered.error}\n`);
|
|
1665
2164
|
}
|
|
1666
2165
|
} finally {
|
|
1667
2166
|
human?.close();
|
|
@@ -1732,7 +2231,7 @@ const stop = defineCommand({
|
|
|
1732
2231
|
const resume = defineCommand({
|
|
1733
2232
|
meta: {
|
|
1734
2233
|
name: "resume",
|
|
1735
|
-
description: "Continue a stopped or crashed
|
|
2234
|
+
description: "Continue a run that didn't succeed (stopped, failed or crashed): finished nodes keep their results"
|
|
1736
2235
|
},
|
|
1737
2236
|
args: {
|
|
1738
2237
|
run: {
|
|
@@ -1821,216 +2320,6 @@ const resume = defineCommand({
|
|
|
1821
2320
|
}
|
|
1822
2321
|
}
|
|
1823
2322
|
});
|
|
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
2323
|
// src/project/ork-skills.ts
|
|
2035
2324
|
/**
|
|
2036
2325
|
* @file The Claude Code plugin's skills (`plugins/ork/skills/`) as text, by
|
|
@@ -2041,6 +2330,7 @@ const triggerChecks = (home) => {
|
|
|
2041
2330
|
const ORK_SKILLS = {
|
|
2042
2331
|
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
2332
|
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",
|
|
2333
|
+
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
2334
|
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
2335
|
};
|
|
2046
2336
|
// src/project/init-skills.ts
|
|
@@ -2224,6 +2514,7 @@ const doctor = defineCommand({
|
|
|
2224
2514
|
orkCheck(),
|
|
2225
2515
|
...results,
|
|
2226
2516
|
...place.mode === "attached" ? [] : projectChecks(place.root),
|
|
2517
|
+
socketCheck(),
|
|
2227
2518
|
...triggerChecks()
|
|
2228
2519
|
]) for (const line of formatCheck(result)) write(line);
|
|
2229
2520
|
if (!ok) {
|
|
@@ -2287,7 +2578,8 @@ const upgrade = defineCommand({
|
|
|
2287
2578
|
const registry = registryOf(process.env);
|
|
2288
2579
|
const latest = await fetchLatest(ORK_NAME, {
|
|
2289
2580
|
...registry === void 0 ? {} : { registry },
|
|
2290
|
-
timeoutMs: 15e3
|
|
2581
|
+
timeoutMs: 15e3,
|
|
2582
|
+
current: ORK_VERSION
|
|
2291
2583
|
});
|
|
2292
2584
|
const plan = planUpgrade({
|
|
2293
2585
|
name: ORK_NAME,
|
|
@@ -2663,13 +2955,22 @@ const formatRegistered = (project, created) => {
|
|
|
2663
2955
|
/**
|
|
2664
2956
|
* @file Scaffolding a project: `.ork/workflows/` with a starter workflow and
|
|
2665
2957
|
* the `.gitignore` entry for run state, never overwriting a file; then the
|
|
2666
|
-
* tool folders the project already uses are linked to `.ork/`.
|
|
2958
|
+
* tool folders the project already uses are linked to `.ork/`. The starter
|
|
2959
|
+
* names no driver, as it is committed: a run takes the one ready on the
|
|
2960
|
+
* machine running it, and `ork init` says which that is here.
|
|
2667
2961
|
*/
|
|
2668
2962
|
/** Run state that never belongs in git. */
|
|
2669
2963
|
const GITIGNORE_ENTRIES = [".ork/runs/"];
|
|
2670
|
-
const
|
|
2964
|
+
const STARTER_NAME = "hello";
|
|
2965
|
+
/** Where `ork init` writes the starter, relative to the project root. */
|
|
2966
|
+
const STARTER_FILE = path.join(WORKFLOW_DIR, `${STARTER_NAME}${WORKFLOW_SUFFIX}`);
|
|
2967
|
+
/**
|
|
2968
|
+
* The starter workflow. It names no driver: it is committed, and one person's
|
|
2969
|
+
* login doesn't belong in a shared file.
|
|
2970
|
+
*/
|
|
2971
|
+
const starterWorkflow = () => ({
|
|
2671
2972
|
schemaVersion: 1,
|
|
2672
|
-
name:
|
|
2973
|
+
name: STARTER_NAME,
|
|
2673
2974
|
description: "A leader plans the task, a worker does it, and you approve the result.",
|
|
2674
2975
|
inputs: { task: {
|
|
2675
2976
|
type: "string",
|
|
@@ -2677,7 +2978,6 @@ const STARTER = {
|
|
|
2677
2978
|
description: "What to do."
|
|
2678
2979
|
} },
|
|
2679
2980
|
defaults: {
|
|
2680
|
-
driver: "sdk",
|
|
2681
2981
|
model: "sonnet",
|
|
2682
2982
|
isolation: "worktree"
|
|
2683
2983
|
},
|
|
@@ -2702,7 +3002,34 @@ const STARTER = {
|
|
|
2702
3002
|
}
|
|
2703
3003
|
},
|
|
2704
3004
|
edges: ["lead -> approve"]
|
|
3005
|
+
});
|
|
3006
|
+
/**
|
|
3007
|
+
* What `ork init` says of drivers, in one line: the project's `defaults.driver`
|
|
3008
|
+
* when it names one, else the driver a run would take here, the one ready
|
|
3009
|
+
* (`cli` when both are), or how to get one ready.
|
|
3010
|
+
*/
|
|
3011
|
+
const driverLine = (checks, projectDefault) => {
|
|
3012
|
+
if (projectDefault !== void 0) return `Runs use the ${projectDefault} driver, ${PROJECT_FILE}'s default, unless a workflow names another.`;
|
|
3013
|
+
const ready = readyDriver(checks);
|
|
3014
|
+
if (ready.ok) {
|
|
3015
|
+
const why = ready.ready === 1 ? ready.detail : `more than one is ready, and ${ready.driver} comes first`;
|
|
3016
|
+
return `Runs use the driver that's ready when they start: ${ready.driver} here (${why}).`;
|
|
3017
|
+
}
|
|
3018
|
+
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`."}`;
|
|
3019
|
+
};
|
|
3020
|
+
/**
|
|
3021
|
+
* The line for the project at `root`: `ork doctor`'s driver checks through
|
|
3022
|
+
* `run` and `env`, and the project's own default.
|
|
3023
|
+
*/
|
|
3024
|
+
const initDriverLine = async (run, env, root) => {
|
|
3025
|
+
const projectDefault = loadProject(root).project?.defaults?.driver;
|
|
3026
|
+
return driverLine(projectDefault === void 0 ? await driverChecks(run, env) : [], projectDefault);
|
|
2705
3027
|
};
|
|
3028
|
+
/**
|
|
3029
|
+
* The line for a starter about to be written in `root`, with the real tools and
|
|
3030
|
+
* environment; `null` when the starter is already there.
|
|
3031
|
+
*/
|
|
3032
|
+
const initDriverLineHere = (root) => existsSync(path.join(root, STARTER_FILE)) ? Promise.resolve(null) : initDriverLine(exec, process.env, root);
|
|
2706
3033
|
/** The run-state entries `root`'s `.gitignore` lacks. */
|
|
2707
3034
|
const unignored = (root) => {
|
|
2708
3035
|
const gitignore = path.join(root, ".gitignore");
|
|
@@ -2729,11 +3056,10 @@ const initProject = (cwd, options = {}) => {
|
|
|
2729
3056
|
const created = [];
|
|
2730
3057
|
const skipped = [];
|
|
2731
3058
|
mkdirSync(path.join(cwd, WORKFLOW_DIR), { recursive: true });
|
|
2732
|
-
|
|
2733
|
-
if (existsSync(path.join(cwd, starter))) skipped.push(starter);
|
|
3059
|
+
if (existsSync(path.join(cwd, STARTER_FILE))) skipped.push(STARTER_FILE);
|
|
2734
3060
|
else {
|
|
2735
|
-
writeFileSync(path.join(cwd,
|
|
2736
|
-
created.push(
|
|
3061
|
+
writeFileSync(path.join(cwd, STARTER_FILE), serializeWorkflow(starterWorkflow()));
|
|
3062
|
+
created.push(STARTER_FILE);
|
|
2737
3063
|
}
|
|
2738
3064
|
const missing = ignoreRunState(cwd);
|
|
2739
3065
|
if (missing.length > 0) created.push(`.gitignore (${missing.join(", ")})`);
|
|
@@ -3100,8 +3426,9 @@ const noteRun = (store, cwd, args) => {
|
|
|
3100
3426
|
* @file `ork feedback apply` (ADR-059): runs Ork's own `ork:feedback-apply` in
|
|
3101
3427
|
* the project an item came from, with the item as its untrusted input. Its
|
|
3102
3428
|
* 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
|
-
*
|
|
3429
|
+
* Ork never merges. Kept, the item is `resolved`, unless its branch holds no
|
|
3430
|
+
* new commits: then there is nothing to keep, and, as when discarded, the
|
|
3431
|
+
* run's worktree and branch go and the item stays `accepted`.
|
|
3105
3432
|
*/
|
|
3106
3433
|
/** The most of an item's records the worker reads, newest first. */
|
|
3107
3434
|
const RECORDS = 10;
|
|
@@ -3386,8 +3713,9 @@ const split = defineCommand({
|
|
|
3386
3713
|
/**
|
|
3387
3714
|
* @file `ork feedback export` and `import` (specs/feedback.md): feedback
|
|
3388
3715
|
* 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
|
|
3716
|
+
* between machines without GitHub. A line is checked before it's added (one
|
|
3717
|
+
* from a newer Ork says so), and a record already here (its run and event) is
|
|
3718
|
+
* skipped.
|
|
3391
3719
|
*/
|
|
3392
3720
|
const Text = z.string();
|
|
3393
3721
|
const LineSchema = z.strictObject({
|
|
@@ -3425,6 +3753,11 @@ const exportLines = (records) => records.map(({ id: _id, ...record }) => JSON.st
|
|
|
3425
3753
|
feedbackVersion: 1,
|
|
3426
3754
|
...record
|
|
3427
3755
|
})).map((line) => `${line}\n`).join("");
|
|
3756
|
+
/** Why a line from a newer Ork is left out, or `null` when it isn't one. */
|
|
3757
|
+
const newerVersion = (value) => {
|
|
3758
|
+
const version = value !== null && typeof value === "object" ? Reflect.get(value, "feedbackVersion") : void 0;
|
|
3759
|
+
return typeof version === "number" && version > 1 ? `made by a newer Ork (\`feedbackVersion: ${String(version)}\`; this one reads ${String(1)}). Upgrade with \`ork upgrade\`.` : null;
|
|
3760
|
+
};
|
|
3428
3761
|
/** The records in a JSONL file's text, and what was wrong with the rest. */
|
|
3429
3762
|
const parseLines = (text) => {
|
|
3430
3763
|
const records = [];
|
|
@@ -3438,6 +3771,11 @@ const parseLines = (text) => {
|
|
|
3438
3771
|
problems.push(`line ${String(index + 1)}: not JSON`);
|
|
3439
3772
|
continue;
|
|
3440
3773
|
}
|
|
3774
|
+
const newer = newerVersion(value);
|
|
3775
|
+
if (newer !== null) {
|
|
3776
|
+
problems.push(`line ${String(index + 1)}: ${newer}`);
|
|
3777
|
+
continue;
|
|
3778
|
+
}
|
|
3441
3779
|
const parsed = LineSchema.safeParse(value);
|
|
3442
3780
|
if (!parsed.success) {
|
|
3443
3781
|
const [issue] = parsed.error.issues;
|
|
@@ -3626,7 +3964,7 @@ const json$1 = {
|
|
|
3626
3964
|
const feedback = defineCommand({
|
|
3627
3965
|
meta: {
|
|
3628
3966
|
name: "feedback",
|
|
3629
|
-
description: "What got in the way in your runs, grouped: list, show, note,
|
|
3967
|
+
description: "What got in the way in your runs, grouped: list, show, note, dismiss, restore, accept, apply, triage, split, publish, export, import"
|
|
3630
3968
|
},
|
|
3631
3969
|
subCommands: {
|
|
3632
3970
|
list: defineCommand({
|
|
@@ -3973,7 +4311,8 @@ const skillsStep = async (root, options) => {
|
|
|
3973
4311
|
};
|
|
3974
4312
|
// src/commands/setup.ts
|
|
3975
4313
|
/**
|
|
3976
|
-
* @file Setting up: `ork init` scaffolds `.ork/` in a project,
|
|
4314
|
+
* @file Setting up: `ork init` scaffolds `.ork/` in a project, with a starter
|
|
4315
|
+
* that names no driver (it says which a run takes here), records it, and
|
|
3977
4316
|
* links the tool folders it uses; `ork link` points tools' agents and skills
|
|
3978
4317
|
* folders at `.ork/` (never in an attached project); `ork agents` lists,
|
|
3979
4318
|
* shows, adds and removes the agents nodes can take. `ork doctor` and `ork
|
|
@@ -3996,10 +4335,12 @@ const init = defineCommand({
|
|
|
3996
4335
|
}
|
|
3997
4336
|
},
|
|
3998
4337
|
run: async ({ args }) => {
|
|
4338
|
+
const cwd = process.cwd();
|
|
4339
|
+
const driverLine = await initDriverLineHere(cwd);
|
|
3999
4340
|
const store = openStore();
|
|
4000
4341
|
let result;
|
|
4001
4342
|
try {
|
|
4002
|
-
result = initRecorded(store,
|
|
4343
|
+
result = initRecorded(store, cwd, { link: args.link });
|
|
4003
4344
|
} finally {
|
|
4004
4345
|
store.close();
|
|
4005
4346
|
}
|
|
@@ -4011,8 +4352,9 @@ const init = defineCommand({
|
|
|
4011
4352
|
const { created, skipped, links } = result;
|
|
4012
4353
|
for (const item of created) write(`${style("green", "created")} ${item}`);
|
|
4013
4354
|
for (const item of skipped) write(`${style("dim", "exists")} ${item}`);
|
|
4355
|
+
if (driverLine !== null && created.includes(STARTER_FILE)) write(driverLine);
|
|
4014
4356
|
for (const line of formatLinks(links.filter((l) => l.state !== "linked"))) write(line);
|
|
4015
|
-
await skillsStep(
|
|
4357
|
+
await skillsStep(cwd, {
|
|
4016
4358
|
skills: args.skills,
|
|
4017
4359
|
link: args.link
|
|
4018
4360
|
});
|
|
@@ -4024,11 +4366,18 @@ const link = defineCommand({
|
|
|
4024
4366
|
name: "link",
|
|
4025
4367
|
description: "Point tools' agents and skills folders (.claude/agents, …) at .ork/"
|
|
4026
4368
|
},
|
|
4027
|
-
args: {
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4369
|
+
args: {
|
|
4370
|
+
tools: {
|
|
4371
|
+
type: "positional",
|
|
4372
|
+
description: "The tools to link, e.g. claude opencode (default: the ones the project already uses)",
|
|
4373
|
+
required: false
|
|
4374
|
+
},
|
|
4375
|
+
check: {
|
|
4376
|
+
type: "boolean",
|
|
4377
|
+
description: "Only show each folder's state; change nothing",
|
|
4378
|
+
default: false
|
|
4379
|
+
}
|
|
4380
|
+
},
|
|
4032
4381
|
run: ({ args }) => {
|
|
4033
4382
|
const place = projectAt(process.cwd());
|
|
4034
4383
|
const { root } = place;
|
|
@@ -4189,7 +4538,10 @@ const agents = defineCommand({
|
|
|
4189
4538
|
}
|
|
4190
4539
|
});
|
|
4191
4540
|
// src/main.ts
|
|
4192
|
-
/**
|
|
4541
|
+
/**
|
|
4542
|
+
* @file The root `ork` command and its subcommands. `ork --help` lists them in
|
|
4543
|
+
* the groups of `help-groups.ts`.
|
|
4544
|
+
*/
|
|
4193
4545
|
const main = defineCommand({
|
|
4194
4546
|
meta: ORK_META,
|
|
4195
4547
|
subCommands: {
|
|
@@ -4204,25 +4556,27 @@ const main = defineCommand({
|
|
|
4204
4556
|
ps,
|
|
4205
4557
|
logs,
|
|
4206
4558
|
tree,
|
|
4207
|
-
usage: () => import("./usage-
|
|
4208
|
-
prune: () => import("./usage-
|
|
4559
|
+
usage: () => import("./usage-CJnytoe4.mjs").then((module) => module.usage),
|
|
4560
|
+
prune: () => import("./usage-CJnytoe4.mjs").then((module) => module.prune),
|
|
4209
4561
|
decide,
|
|
4210
|
-
relay: () => import("./relay-
|
|
4211
|
-
pause: () => import("./pause-
|
|
4212
|
-
continue: () => import("./pause-
|
|
4562
|
+
relay: () => import("./relay-o-Luee1c.mjs").then((module) => module.relay),
|
|
4563
|
+
pause: () => import("./pause-B_SL_yDz.mjs").then((module) => module.pause),
|
|
4564
|
+
continue: () => import("./pause-B_SL_yDz.mjs").then((module) => module.continueRun),
|
|
4213
4565
|
stop,
|
|
4214
4566
|
chat,
|
|
4215
|
-
send: () => import("./send-
|
|
4567
|
+
send: () => import("./send-3B64fBNB.mjs").then((module) => module.send),
|
|
4216
4568
|
resume,
|
|
4217
4569
|
feedback,
|
|
4218
4570
|
context,
|
|
4219
|
-
worktrees: () => import("./worktrees-
|
|
4220
|
-
wt: () => import("./worktrees-
|
|
4221
|
-
watch: () => import("./watch-
|
|
4222
|
-
mcp: () => import("./mcp-
|
|
4223
|
-
ui: () => import("./ui-
|
|
4571
|
+
worktrees: () => import("./worktrees-86ySXxR6.mjs").then((module) => module.worktrees),
|
|
4572
|
+
wt: () => import("./worktrees-86ySXxR6.mjs").then((module) => module.worktrees),
|
|
4573
|
+
watch: () => import("./watch-BJB82ZJ7.mjs").then((module) => module.watch),
|
|
4574
|
+
mcp: () => import("./mcp-CKPyNaKD.mjs").then((module) => module.mcp),
|
|
4575
|
+
ui: () => import("./ui-CY4xxJUu.mjs").then((module) => module.ui),
|
|
4224
4576
|
doctor,
|
|
4225
|
-
upgrade
|
|
4577
|
+
upgrade,
|
|
4578
|
+
completion: () => import("./completion-snnYGjgK.mjs").then((module) => module.completion),
|
|
4579
|
+
__complete: () => import("./completion-snnYGjgK.mjs").then((module) => module.completeCommand(main))
|
|
4226
4580
|
}
|
|
4227
4581
|
});
|
|
4228
4582
|
// src/bin.ts
|
|
@@ -4230,17 +4584,26 @@ const main = defineCommand({
|
|
|
4230
4584
|
* @file The `ork` executable: runs the CLI's root command. The published
|
|
4231
4585
|
* package's `bin` points at its bundle. When the new-version check is on, it
|
|
4232
4586
|
* starts the daily background check and ends with the notice (ADR-038); the
|
|
4233
|
-
* background copy it starts only runs the check.
|
|
4587
|
+
* background copy it starts only runs the check. It first clears git's local
|
|
4588
|
+
* variables (a hook's `GIT_DIR`, `GIT_INDEX_FILE`, …), so nothing Ork starts
|
|
4589
|
+
* acts on the hook's repository (specs/worktrees.md). `--help` leaves out the
|
|
4590
|
+
* flags only Ork passes to itself. `ork __complete`, which tab completion
|
|
4591
|
+
* runs on every Tab, goes first: no update check, and no citty, so a typed
|
|
4592
|
+
* `--help` is a word to complete.
|
|
4234
4593
|
*/
|
|
4235
|
-
|
|
4594
|
+
clearGitLocalEnv();
|
|
4595
|
+
const argv = process.argv.slice(2);
|
|
4596
|
+
if (argv[0] === "__complete") {
|
|
4597
|
+
const { printCompletions } = await import("./completion-snnYGjgK.mjs");
|
|
4598
|
+
await printCompletions(main, argv.slice(1));
|
|
4599
|
+
} else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
|
|
4236
4600
|
else {
|
|
4237
|
-
const argv = process.argv.slice(2);
|
|
4238
4601
|
if (updateCheckOff(process.env, argv, process.stderr.isTTY) === null) {
|
|
4239
4602
|
startUpdateCheck();
|
|
4240
4603
|
process.once("exit", () => {
|
|
4241
4604
|
showUpdateNotice();
|
|
4242
4605
|
});
|
|
4243
4606
|
}
|
|
4244
|
-
await runMain(main);
|
|
4607
|
+
await runMain(main, { showUsage: showHelp });
|
|
4245
4608
|
}
|
|
4246
4609
|
export {};
|