@krapht/ork 1.0.0-alpha.1 → 1.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/dist/{__build_info__-D9lSlMFy.mjs → __build_info__-CIchF6nr.mjs} +2 -2
- package/dist/bin.mjs +1479 -600
- package/dist/chips-BA7SrrH5.mjs +43 -0
- package/dist/{clean-CA_Oi2Wl.mjs → clean-Di-KT7Mf.mjs} +5 -2
- package/dist/{completion-DBGfL-gN.mjs → completion-C5D0sEs5.mjs} +23 -16
- package/dist/context-BaNshWr9.mjs +695 -0
- package/dist/{decide-CoIJh34e.mjs → decide-B6PnKeqo.mjs} +3 -3
- package/dist/{find-rBBk7Jui.mjs → find-BicBbGMg.mjs} +1 -1
- package/dist/format-lWhN-jYR.mjs +286 -0
- package/dist/{in-a-run-DdAakpRR.mjs → in-a-run-CgkVouyG.mjs} +1 -1
- package/dist/{limits-BPM9wUlV.mjs → limits-jZQp9ZJC.mjs} +1 -1
- package/dist/{list-Bv5Lxu7z.mjs → list-CEaJ07jd.mjs} +11 -4
- package/dist/{mcp-DM30MFBw.mjs → mcp-DYgVA9f5.mjs} +6 -6
- package/dist/output-CM95njYt.mjs +421 -0
- package/dist/{pause-C7lJkSma.mjs → pause-D6PuSlNl.mjs} +4 -4
- package/dist/{relay-BipJYszF.mjs → relay-DNx7be9k.mjs} +2 -2
- package/dist/{remove-BDs9isB6.mjs → remove-CpMb4NDl.mjs} +14 -7
- package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
- package/dist/{send-nbkjf3sc.mjs → send-CLs_fyg7.mjs} +3 -3
- package/dist/{src-BxUJPGa5.mjs → src-9iGkjnsy.mjs} +487 -341
- package/dist/{src-B0TT7DBi.mjs → src-B6KvTm7B.mjs} +106 -90
- package/dist/src-BnZgZGrf.mjs +2697 -0
- package/dist/{src-hrj2h0IZ.mjs → src-D6_xjm-x.mjs} +6719 -3389
- package/dist/ui/THIRD_PARTY_LICENSES.txt +594 -0
- package/dist/ui/assets/app-CxURph7a.js +75 -0
- package/dist/ui/assets/app-QoHVi-mb.css +1 -0
- package/dist/ui/assets/{index-CPx74lFf.js → index-By_8KKG6.js} +3 -3
- package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/{ui-B98FSboa.mjs → ui-CIci4UBW.mjs} +4 -4
- package/dist/{usage-EW7DI6Z7.mjs → usage-GceBGMjU.mjs} +43 -32
- package/dist/{watch-BAkSGaOp.mjs → watch-BmoybMQg.mjs} +28 -16
- package/dist/{workflows-vsEohqa7.mjs → workflows-DZvW3i1e.mjs} +4 -3
- package/dist/{worktrees-Ahj9rdWK.mjs → worktrees-Be51E2rB.mjs} +84 -24
- package/package.json +2 -4
- package/dist/context-6x3kXREO.mjs +0 -274
- package/dist/format-zMmhqyLE.mjs +0 -427
- package/dist/output-TV_j7Ni0.mjs +0 -950
- package/dist/src-B9v0lDc6.mjs +0 -1204
- package/dist/ui/assets/app-BNT0SuIz.css +0 -1
- package/dist/ui/assets/app-CmkynZyW.js +0 -67
- package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -1,58 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Bn as
|
|
3
|
-
import { C as feedbackActions, S as placeOf, _ as previewPost, b as acceptItem,
|
|
4
|
-
import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-
|
|
5
|
-
import { n as newRunId, t as findRun } from "./find-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { a as planClean, t as applyClean } from "./clean-CA_Oi2Wl.mjs";
|
|
2
|
+
import { $ as DETACHED_ENV, $t as endOf, An as linkTools, Ar as AGENT_DRIVERS, At as nestItems, Bn as placeFiles, C as openStore, Cn as wakeRun, Ct as runStatus, Dn as planAgentMigration, Dr as readyDriver, E as inspectProject, En as maskSecrets, Er as driverChecks, Et as templateOf, Fn as catalogAgent, Gt as PROJECT_FILE, H as tellNode, Hn as projectsHome, I as createCalls, In as loadAgents, J as tellLiveRun, Jt as machineRulesOf, Kn as inside, Kt as gitRulesOf, L as addressOf, Ln as hasInRepoFiles, M as parseInputPairs, Mn as toolsInUse, N as resolveInputs, Nn as createAgent, On as applySteps, Or as AGENT_TOOL_LAYOUTS, Pn as deleteAgent, Q as listKey, Qt as createSignals, R as endLink, Rn as inRepoPlace, S as isNodeEvent, Tn as maskReplacer, Un as runDirOf, V as peerOf, Vn as placeFor, W as flattenTree, Wn as shownPath, X as detachResume, Y as adoptedCalls, Z as resumable, _t as startRun, a as releaseNotes, an as FEEDBACK_SEVERITIES, bn as serveRun, br as retiredDriverHint, cn as planPost, cr as serializeWorkflow, dr as WorkflowSchema, dt as sessionDriver, en as outcomeEnd, et as detachRun, fn as createFeedback, gr as isClaudeNode, gt as walkSummary, i as planUpgrade, in as FEEDBACK_KINDS, ir as beforeOf, j as clearGitLocalEnv, jn as toolLinks, kn as planMoveInto, kr as ORK_LAYOUT, lt as planNodeSession, mn as normalizeTitle, mr as settingsOf, n as resolveBase, nn as END_KINDS, nt as NOT_STARTED, o as runningRunIds, on as FEEDBACK_TARGETS, q as requestStop, qn as projectRootOf, qt as loadProject, s as runningScript, t as createWorkspace, tt as followEvents, u as isNewer, un as sendPost, wt as runTree, x as activeTree, xn as socketPath, xt as liveStatus, yt as userMessageText, z as endLinksOf, zn as newProjectId, zt as slotWaitLine } from "./src-D6_xjm-x.mjs";
|
|
3
|
+
import { C as feedbackActions, S as placeOf, _ as previewPost, b as acceptItem, f as readUpdateCache, g as nodeSession, h as writeUpdateCache, i as registryOf, l as fetchLatest, m as updateCachePath, n as ORK_VERSION, s as runInstall, t as ORK_NAME, v as checkNote, w as openFixRun, y as recordNote } from "./__build_info__-CIchF6nr.mjs";
|
|
4
|
+
import { i as builtinWorkflowFile, n as pendingDecisions, r as builtinProblem, t as answerDecision } from "./decide-B6PnKeqo.mjs";
|
|
5
|
+
import { n as newRunId, t as findRun } from "./find-BicBbGMg.mjs";
|
|
6
|
+
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-CEaJ07jd.mjs";
|
|
7
|
+
import { a as planClean, t as applyClean } from "./clean-Di-KT7Mf.mjs";
|
|
9
8
|
import { c as subcommandsOf, i as argsOf, n as groupedEntries, o as publicArgs, s as resolved, t as groupOf } from "./help-groups-BwYoAWsh.mjs";
|
|
10
|
-
import { a as
|
|
11
|
-
import {
|
|
12
|
-
import { a as
|
|
13
|
-
import { i as
|
|
14
|
-
import { _ as
|
|
15
|
-
import { i as
|
|
9
|
+
import { C as widthOf, S as hang, T as readKeys, _ as canKey, a as write, b as termOf, d as bold, f as paint, g as withdrawn, h as readLine, i as style, m as interrupt, n as cost, o as writeErr, p as createRegion, r as plain, s as confirmOrStop, t as confirm, u as apply$1, v as plainEnv, w as wrap, x as cut, y as takePlainFlag } from "./output-CM95njYt.mjs";
|
|
10
|
+
import { n as chip, r as nodeChip, t as CHIP_LABELS } from "./chips-BA7SrrH5.mjs";
|
|
11
|
+
import { _ as namedMcpEntries, a as builtinExecutors, d as maskedEnv, f as maskedNamesOfRun, g as clearOutputFiles, h as sessionEnv, i as onPath, l as clearStateFiles, m as runScrubber, n as missingProgramMessage, r as missingPrograms, t as envProblems, v as resultOf } from "./src-9iGkjnsy.mjs";
|
|
12
|
+
import { c as registeredProjects, i as refusedBuiltin, n as WORKFLOW_SUFFIX, o as workflowArg, s as projectAt, t as WORKFLOW_DIR } from "./workflows-DZvW3i1e.mjs";
|
|
13
|
+
import { C as startUpdateCheck, S as showUpdateNotice, _ as runChecks, a as validateFile, c as warningLines, d as runReadyDriver, f as unfitLines, g as orkCheck, h as headWarning, i as validate, l as checksHere, m as experimentsCheck, o as walkLoaded, p as exec, r as loadWorkflow, s as formatReport, t as context, u as chooseReadyDriver, v as socketCheck, w as updateCheckOff, x as runUpdateChild, y as triggerChecks } from "./context-BaNshWr9.mjs";
|
|
14
|
+
import { c as nodeVerdict, d as createView, f as styler, i as listedStatus, l as firstLine, n as formatRuns, r as formatTree, s as hasConflict, t as ago, u as formatBusEvent } from "./format-lWhN-jYR.mjs";
|
|
15
|
+
import { i as runLimits, r as limitsRefusal, t as chatNotice } from "./limits-jZQp9ZJC.mjs";
|
|
16
|
+
import { i as decideRefusal, n as chatRefusal } from "./in-a-run-CgkVouyG.mjs";
|
|
16
17
|
import path from "node:path";
|
|
17
18
|
import { stringify } from "yaml";
|
|
18
19
|
import { z } from "zod";
|
|
19
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
20
|
-
import { createHash } from "node:crypto";
|
|
20
|
+
import { appendFileSync, existsSync, linkSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
21
22
|
import { homedir } from "node:os";
|
|
22
|
-
import { execFile, spawn } from "node:child_process";
|
|
23
23
|
import { setTimeout } from "node:timers/promises";
|
|
24
24
|
import { defineCommand, renderUsage, runMain } from "citty";
|
|
25
25
|
import { createInterface } from "node:readline/promises";
|
|
26
|
-
import { styleText } from "node:util";
|
|
27
|
-
// ../integrations/src/ready.ts
|
|
28
|
-
/**
|
|
29
|
-
* @file Which Claude driver is ready on this machine: each driver's own check
|
|
30
|
-
* (what `ork doctor` shows), and the one a run takes for the nodes that name
|
|
31
|
-
* no driver, so a committed workflow never carries one person's login.
|
|
32
|
-
*/
|
|
33
|
-
/** Each registered Claude driver's own check, in registry order. */
|
|
34
|
-
const driverChecks = (run, env) => Promise.all(CLAUDE_DRIVERS.map(async (driver) => ({
|
|
35
|
-
id: driver.id,
|
|
36
|
-
result: await driver.doctor(run, env)
|
|
37
|
-
})));
|
|
38
|
-
/**
|
|
39
|
-
* The driver ready here: the one whose check passes; with more than one,
|
|
40
|
-
* {@link PREFERRED_DRIVER}; with none, each driver's hint.
|
|
41
|
-
*/
|
|
42
|
-
const readyDriver = (checks) => {
|
|
43
|
-
const ready = checks.filter(({ result }) => result.status === "ok");
|
|
44
|
-
const pick = ready.find(({ id }) => id === "cli") ?? ready.at(0);
|
|
45
|
-
if (pick === void 0) return {
|
|
46
|
-
ok: false,
|
|
47
|
-
hints: checks.flatMap(({ result }) => result.hint === void 0 ? [] : [result.hint])
|
|
48
|
-
};
|
|
49
|
-
return {
|
|
50
|
-
ok: true,
|
|
51
|
-
driver: pick.id,
|
|
52
|
-
detail: pick.result.detail,
|
|
53
|
-
ready: ready.length
|
|
54
|
-
};
|
|
55
|
-
};
|
|
56
26
|
// ../core/src/control/queue.ts
|
|
57
27
|
const PERMISSION_OPTIONS = [{
|
|
58
28
|
id: "allow",
|
|
@@ -64,14 +34,33 @@ const PERMISSION_OPTIONS = [{
|
|
|
64
34
|
const DENIED = "The user did not allow this tool.";
|
|
65
35
|
/** The deny message a session gets, with the person's words when given. */
|
|
66
36
|
const deniedMessage = (note) => note === void 0 ? DENIED : `${DENIED} They said: ${note}`;
|
|
67
|
-
/**
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
37
|
+
/** The input fields that say what a tool will do, the first one present. */
|
|
38
|
+
const TOOL_INPUT_KEYS = [
|
|
39
|
+
"command",
|
|
40
|
+
"file_path",
|
|
41
|
+
"path",
|
|
42
|
+
"url",
|
|
43
|
+
"pattern"
|
|
44
|
+
];
|
|
45
|
+
/** A tool's input for the decision's context: as written, cut at `max`. */
|
|
46
|
+
const inputContext = (input, max = 4e3) => {
|
|
47
|
+
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
48
|
+
const text = key === void 0 ? JSON.stringify(input, void 0, 2) : String(input[key]);
|
|
49
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
71
50
|
};
|
|
72
51
|
const createDecisionQueue = (options) => {
|
|
73
52
|
const { store, runId, emit } = options;
|
|
74
|
-
|
|
53
|
+
const earlier = store.decisions({ runId });
|
|
54
|
+
let count = Math.max(0, ...earlier.map(({ id }) => Number(id.slice(runId.length + 1)) || 0));
|
|
55
|
+
for (const { id, node, status } of earlier) {
|
|
56
|
+
if (status !== "open") continue;
|
|
57
|
+
store.withdrawDecision(id);
|
|
58
|
+
emit({
|
|
59
|
+
type: "decision.withdrawn",
|
|
60
|
+
node,
|
|
61
|
+
decision: id
|
|
62
|
+
});
|
|
63
|
+
}
|
|
75
64
|
/** Opens a decision and settles it with the first answer. */
|
|
76
65
|
const decide = (open, local, withdrawn) => {
|
|
77
66
|
count += 1;
|
|
@@ -155,12 +144,16 @@ const createDecisionQueue = (options) => {
|
|
|
155
144
|
};
|
|
156
145
|
if (question.recommended !== void 0) open.recommended = question.recommended;
|
|
157
146
|
if (question.context !== void 0) open.context = question.context;
|
|
147
|
+
if (question.document !== void 0) open.document = question.document;
|
|
158
148
|
if (question.owner !== void 0) open.owner = question.owner;
|
|
159
149
|
const { local } = options;
|
|
160
150
|
return decide(open, local === void 0 ? void 0 : (signal) => {
|
|
161
151
|
let note;
|
|
162
152
|
return local.ask({
|
|
163
153
|
...question,
|
|
154
|
+
question: maskSecrets(question.question),
|
|
155
|
+
...question.context === void 0 ? {} : { context: maskSecrets(question.context) },
|
|
156
|
+
...question.document === void 0 ? {} : { document: maskSecrets(question.document) },
|
|
164
157
|
signal,
|
|
165
158
|
onNote: (given) => {
|
|
166
159
|
note = given;
|
|
@@ -219,7 +212,7 @@ const createDecisionQueue = (options) => {
|
|
|
219
212
|
kind: "permission",
|
|
220
213
|
question: `Allow ${request.toolName}?`,
|
|
221
214
|
options: PERMISSION_OPTIONS,
|
|
222
|
-
context:
|
|
215
|
+
context: inputContext(request.input)
|
|
223
216
|
}, local === void 0 ? void 0 : (signal) => {
|
|
224
217
|
let said;
|
|
225
218
|
return local.permit({
|
|
@@ -236,7 +229,7 @@ const createDecisionQueue = (options) => {
|
|
|
236
229
|
note: said
|
|
237
230
|
};
|
|
238
231
|
});
|
|
239
|
-
});
|
|
232
|
+
}, request.signal);
|
|
240
233
|
if (note !== void 0) request.onNote?.(note);
|
|
241
234
|
return answer === "allow" ? { allow: true } : {
|
|
242
235
|
allow: false,
|
|
@@ -314,165 +307,6 @@ const startControl = async (options) => {
|
|
|
314
307
|
}
|
|
315
308
|
};
|
|
316
309
|
};
|
|
317
|
-
// ../core/src/feedback/signals.ts
|
|
318
|
-
const ORK_TOOL = "mcp__ork__";
|
|
319
|
-
/**
|
|
320
|
-
* An observer for one run's events: give it each event, and record what it
|
|
321
|
-
* returns, in order (the event, its origin filled in when it is feedback, then
|
|
322
|
-
* any feedback it signals).
|
|
323
|
-
*/
|
|
324
|
-
const createSignals = (options = {}) => {
|
|
325
|
-
let workflow;
|
|
326
|
-
const nodeTypes = /* @__PURE__ */ new Map();
|
|
327
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
328
|
-
const tools = /* @__PURE__ */ new Map();
|
|
329
|
-
const asked = /* @__PURE__ */ new Map();
|
|
330
|
-
const seen = /* @__PURE__ */ new Set();
|
|
331
|
-
const originOf = (node) => {
|
|
332
|
-
const origin = {};
|
|
333
|
-
if (workflow !== void 0) origin.workflow = workflow;
|
|
334
|
-
if (options.workflowHash !== void 0) origin.workflowHash = options.workflowHash;
|
|
335
|
-
const type = node === void 0 ? void 0 : nodeTypes.get(node);
|
|
336
|
-
if (type !== void 0) origin.nodeType = type;
|
|
337
|
-
const session = node === void 0 ? void 0 : sessions.get(node);
|
|
338
|
-
if (session !== void 0) origin.model = session.model;
|
|
339
|
-
if (session?.driver !== void 0) origin.driver = session.driver;
|
|
340
|
-
if (options.version !== void 0) origin.version = options.version;
|
|
341
|
-
return origin;
|
|
342
|
-
};
|
|
343
|
-
const record = (node, signal) => {
|
|
344
|
-
const print = fingerprint(signal);
|
|
345
|
-
if (seen.has(print)) return [];
|
|
346
|
-
seen.add(print);
|
|
347
|
-
return [{
|
|
348
|
-
type: "feedback.recorded",
|
|
349
|
-
...node === void 0 ? {} : { node },
|
|
350
|
-
feedback: {
|
|
351
|
-
source: "system",
|
|
352
|
-
kind: signal.kind,
|
|
353
|
-
target: signal.target,
|
|
354
|
-
severity: signal.severity,
|
|
355
|
-
title: signal.title,
|
|
356
|
-
body: signal.body,
|
|
357
|
-
evidence: signal.evidence ?? [],
|
|
358
|
-
code: signal.code,
|
|
359
|
-
fingerprint: print,
|
|
360
|
-
origin: originOf(node)
|
|
361
|
-
}
|
|
362
|
-
}];
|
|
363
|
-
};
|
|
364
|
-
const answered = (question, answer) => {
|
|
365
|
-
if (question.kind === "permission") {
|
|
366
|
-
if (answer !== "deny") return null;
|
|
367
|
-
const tool = question.question.replace(/^Allow /u, "").replace(/\?$/u, "");
|
|
368
|
-
return [question.node, {
|
|
369
|
-
code: "permission.denied",
|
|
370
|
-
kind: "friction",
|
|
371
|
-
target: "workflow",
|
|
372
|
-
severity: "low",
|
|
373
|
-
title: `${question.node} was denied ${tool}`,
|
|
374
|
-
body: "Allow it in the node's `tools.allow` if it needs it, or tell it in its prompt not to use it.",
|
|
375
|
-
evidence: [tool]
|
|
376
|
-
}];
|
|
377
|
-
}
|
|
378
|
-
if (question.recommended === void 0 || answer === question.recommended) return null;
|
|
379
|
-
return [question.node, {
|
|
380
|
-
code: "recommendation.overridden",
|
|
381
|
-
kind: "context-gap",
|
|
382
|
-
target: "workflow",
|
|
383
|
-
severity: "low",
|
|
384
|
-
title: `The answer to ${question.node}'s question was not the one it recommended`,
|
|
385
|
-
body: `Asked: ${question.question} Recommended: ${question.recommended}. Answered: ${answer}.`
|
|
386
|
-
}];
|
|
387
|
-
};
|
|
388
|
-
/** The feedback an event signals, if any, and the node it is about. */
|
|
389
|
-
const signalOf = (event) => {
|
|
390
|
-
if (event.type === "loop.exhausted") return [event.from, {
|
|
391
|
-
code: "loop.exhausted",
|
|
392
|
-
kind: "workflow",
|
|
393
|
-
target: "workflow",
|
|
394
|
-
severity: "medium",
|
|
395
|
-
title: `The loop ${event.from} -> ${event.to} ran out of its ${String(event.maxRounds)} rounds`,
|
|
396
|
-
body: "Raise `maxRounds`, or make what the loop's nodes are asked to fix clearer so it settles sooner."
|
|
397
|
-
}];
|
|
398
|
-
if (event.type === "run.finished" && event.result.status === "budget_exhausted") return [void 0, {
|
|
399
|
-
code: "budget.exhausted",
|
|
400
|
-
kind: "workflow",
|
|
401
|
-
target: "workflow",
|
|
402
|
-
severity: "medium",
|
|
403
|
-
title: "The run's budget ran out",
|
|
404
|
-
body: `The run stopped after spending $${event.result.costUsd.toFixed(4)}. Raise \`limits.budgetUsd\`, or give nodes cheaper models or smaller budgets.`
|
|
405
|
-
}];
|
|
406
|
-
if (event.type === "decision.answered") {
|
|
407
|
-
const question = asked.get(event.decision);
|
|
408
|
-
return question === void 0 ? null : answered(question, event.answer);
|
|
409
|
-
}
|
|
410
|
-
if (event.type === "node.event" && event.event.type === "tool.result") {
|
|
411
|
-
const name = tools.get(`${event.id}\0${event.event.id}`);
|
|
412
|
-
if (!event.event.isError || name?.startsWith(ORK_TOOL) !== true) return null;
|
|
413
|
-
const tool = name.slice(10);
|
|
414
|
-
return [event.id, {
|
|
415
|
-
code: "tool.error",
|
|
416
|
-
kind: "bug",
|
|
417
|
-
target: "ork",
|
|
418
|
-
severity: "medium",
|
|
419
|
-
title: `Ork's ${tool} tool returned an error`,
|
|
420
|
-
body: `A call to \`${tool}\` failed; the node's events show the call.`,
|
|
421
|
-
evidence: [name]
|
|
422
|
-
}];
|
|
423
|
-
}
|
|
424
|
-
if (event.type === "node.finished" && nodeTypes.get(event.id) === "human.gate" && event.output["answeredBy"] === "timeout") return [event.id, {
|
|
425
|
-
code: "gate.timeout",
|
|
426
|
-
kind: "workflow",
|
|
427
|
-
target: "workflow",
|
|
428
|
-
severity: "low",
|
|
429
|
-
title: `The gate ${event.id} timed out and took its default`,
|
|
430
|
-
body: "No one answered in time. Lengthen its `timeout`, or keep it if the default was right."
|
|
431
|
-
}];
|
|
432
|
-
return null;
|
|
433
|
-
};
|
|
434
|
-
/** Keeps what later events and origins need. */
|
|
435
|
-
const observe = (event) => {
|
|
436
|
-
if (event.type === "run.started") workflow = event.workflow;
|
|
437
|
-
if (event.type === "node.started") nodeTypes.set(event.id, event.nodeType);
|
|
438
|
-
if (event.type === "decision.opened") {
|
|
439
|
-
const question = {
|
|
440
|
-
node: event.node,
|
|
441
|
-
kind: event.kind,
|
|
442
|
-
question: event.question
|
|
443
|
-
};
|
|
444
|
-
if (event.recommended !== void 0) question.recommended = event.recommended;
|
|
445
|
-
asked.set(event.decision, question);
|
|
446
|
-
}
|
|
447
|
-
if (event.type !== "node.event") return;
|
|
448
|
-
const inner = event.event;
|
|
449
|
-
if (inner.type === "session.started" && inner.fork !== true) sessions.set(event.id, {
|
|
450
|
-
model: inner.model,
|
|
451
|
-
...inner.driver === void 0 ? {} : { driver: inner.driver }
|
|
452
|
-
});
|
|
453
|
-
if (inner.type === "tool.called") tools.set(`${event.id}\0${inner.id}`, inner.name);
|
|
454
|
-
};
|
|
455
|
-
return (event) => {
|
|
456
|
-
observe(event);
|
|
457
|
-
if (event.type === "feedback.recorded") {
|
|
458
|
-
if (seen.has(event.feedback.fingerprint)) return [];
|
|
459
|
-
seen.add(event.feedback.fingerprint);
|
|
460
|
-
const origin = {
|
|
461
|
-
...originOf(event.node),
|
|
462
|
-
...event.feedback.origin
|
|
463
|
-
};
|
|
464
|
-
return [{
|
|
465
|
-
...event,
|
|
466
|
-
feedback: {
|
|
467
|
-
...event.feedback,
|
|
468
|
-
origin
|
|
469
|
-
}
|
|
470
|
-
}];
|
|
471
|
-
}
|
|
472
|
-
const signal = signalOf(event);
|
|
473
|
-
return signal === null ? [event] : [event, ...record(...signal)];
|
|
474
|
-
};
|
|
475
|
-
};
|
|
476
310
|
/** A title's words, as fingerprints compare them. */
|
|
477
311
|
const titleWords = (title) => new Set(normalizeTitle(title).split(" ").filter((word) => word !== ""));
|
|
478
312
|
/** The share of two titles' words they have in common (Jaccard). */
|
|
@@ -514,6 +348,140 @@ const proposeMerges = (items) => {
|
|
|
514
348
|
};
|
|
515
349
|
}).filter((merge) => merge !== void 0).toSorted((a, b) => firstSeen(a.into, b.into));
|
|
516
350
|
};
|
|
351
|
+
/** `ORK_HEAVY_SLOTS` as a count; anything but a positive integer is the default. */
|
|
352
|
+
const heavySlots = (env = process.env) => {
|
|
353
|
+
const count = Number(env["ORK_HEAVY_SLOTS"]);
|
|
354
|
+
return Number.isInteger(count) && count > 0 ? count : 2;
|
|
355
|
+
};
|
|
356
|
+
const codeOf = (error) => error instanceof Error && "code" in error ? error.code : void 0;
|
|
357
|
+
const alive = (pid) => {
|
|
358
|
+
if (pid <= 0) return false;
|
|
359
|
+
try {
|
|
360
|
+
process.kill(pid, 0);
|
|
361
|
+
return true;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return codeOf(error) === "EPERM";
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const read = (file) => {
|
|
367
|
+
try {
|
|
368
|
+
const value = JSON.parse(readFileSync(file, "utf8"));
|
|
369
|
+
if (typeof value !== "object" || value === null || !("id" in value && "run" in value && "node" in value && "pid" in value)) return;
|
|
370
|
+
const { id, run, node, pid } = value;
|
|
371
|
+
return typeof id === "string" && typeof run === "string" && typeof node === "string" && typeof pid === "number" ? {
|
|
372
|
+
id,
|
|
373
|
+
run,
|
|
374
|
+
node,
|
|
375
|
+
pid
|
|
376
|
+
} : void 0;
|
|
377
|
+
} catch {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
const listed = (dir) => {
|
|
382
|
+
try {
|
|
383
|
+
return readdirSync(dir).filter((name) => name.endsWith(".json")).toSorted();
|
|
384
|
+
} catch {
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
/** Written whole (a rename), so no reader sees half a record. */
|
|
389
|
+
const writeWhole = (file, record) => {
|
|
390
|
+
const draft = `${file}.${record.id}.tmp`;
|
|
391
|
+
writeFileSync(draft, JSON.stringify(record), { mode: 384 });
|
|
392
|
+
renameSync(draft, file);
|
|
393
|
+
};
|
|
394
|
+
/** Deletes a record if it is still `id`'s. */
|
|
395
|
+
const dropIf = (file, id) => {
|
|
396
|
+
if (read(file)?.id === id) rmSync(file, { force: true });
|
|
397
|
+
};
|
|
398
|
+
let tickets = 0;
|
|
399
|
+
/** A ticket's name: sorted by when it was queued. */
|
|
400
|
+
const ticketName = (id) => `${String(Date.now()).padStart(15, "0")}-${String(process.pid).padStart(8, "0")}-${String(tickets += 1).padStart(6, "0")}-${id}.json`;
|
|
401
|
+
/** The ledger in `dir`, with `slots` slots. */
|
|
402
|
+
const fileSlotLedger = (options) => {
|
|
403
|
+
const { dir, slots } = options;
|
|
404
|
+
const queue = path.join(dir, "queue");
|
|
405
|
+
const slotFile = (index) => path.join(dir, `slot-${String(index)}.json`);
|
|
406
|
+
const records = () => Array.from({ length: slots }, (_, index) => {
|
|
407
|
+
const file = slotFile(index);
|
|
408
|
+
return {
|
|
409
|
+
file,
|
|
410
|
+
held: read(file)
|
|
411
|
+
};
|
|
412
|
+
});
|
|
413
|
+
const holders = () => records().flatMap(({ held }) => held === void 0 ? [] : [held]);
|
|
414
|
+
const sweep = () => {
|
|
415
|
+
const gone = [];
|
|
416
|
+
for (const file of [...records().map((record) => record.file), ...listed(queue).map((name) => path.join(queue, name))]) {
|
|
417
|
+
const held = read(file);
|
|
418
|
+
if (held === void 0 || alive(held.pid)) continue;
|
|
419
|
+
dropIf(file, held.id);
|
|
420
|
+
gone.push(held);
|
|
421
|
+
}
|
|
422
|
+
return gone;
|
|
423
|
+
};
|
|
424
|
+
/** Links `draft` into a free slot; true when it got one. */
|
|
425
|
+
const tryTake = (draft) => {
|
|
426
|
+
for (const { file, held } of records()) {
|
|
427
|
+
if (held !== void 0) continue;
|
|
428
|
+
try {
|
|
429
|
+
linkSync(draft, file);
|
|
430
|
+
return true;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (codeOf(error) !== "EEXIST") throw error;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return false;
|
|
436
|
+
};
|
|
437
|
+
const take = async (holder, { signal, onWait }) => {
|
|
438
|
+
mkdirSync(queue, {
|
|
439
|
+
recursive: true,
|
|
440
|
+
mode: 448
|
|
441
|
+
});
|
|
442
|
+
const record = {
|
|
443
|
+
id: randomUUID(),
|
|
444
|
+
...holder
|
|
445
|
+
};
|
|
446
|
+
const ticket = path.join(queue, ticketName(record.id));
|
|
447
|
+
writeWhole(ticket, record);
|
|
448
|
+
let told = false;
|
|
449
|
+
try {
|
|
450
|
+
for (;;) {
|
|
451
|
+
if (signal.aborted) return null;
|
|
452
|
+
sweep();
|
|
453
|
+
const free = records().filter(({ held }) => held === void 0).length;
|
|
454
|
+
if (listed(queue).indexOf(path.basename(ticket)) < free && tryTake(ticket)) {
|
|
455
|
+
const release = () => {
|
|
456
|
+
for (const { file, held } of records()) if (held?.id === record.id) dropIf(file, record.id);
|
|
457
|
+
};
|
|
458
|
+
return release;
|
|
459
|
+
}
|
|
460
|
+
if (!told) {
|
|
461
|
+
told = true;
|
|
462
|
+
onWait?.(holders());
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
await setTimeout(options.pollMs ?? 500, void 0, { signal });
|
|
466
|
+
} catch {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} finally {
|
|
471
|
+
rmSync(ticket, { force: true });
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
return {
|
|
475
|
+
take,
|
|
476
|
+
holders,
|
|
477
|
+
sweep
|
|
478
|
+
};
|
|
479
|
+
};
|
|
480
|
+
/** The machine's ledger: `~/.ork/slots`, `ORK_HEAVY_SLOTS` slots. */
|
|
481
|
+
const machineSlotLedger = (options = {}) => fileSlotLedger({
|
|
482
|
+
dir: path.join(options.home ?? homedir(), ".ork", "slots"),
|
|
483
|
+
slots: heavySlots(options.env)
|
|
484
|
+
});
|
|
517
485
|
// ../core/src/remote/relay.ts
|
|
518
486
|
/**
|
|
519
487
|
* @file A run's side of the relay (RFC-003): its nodes' links, read from the
|
|
@@ -613,6 +581,89 @@ const createRelay = (options) => {
|
|
|
613
581
|
}
|
|
614
582
|
};
|
|
615
583
|
};
|
|
584
|
+
// ../core/src/run/replay.ts
|
|
585
|
+
const NO_FAN_OUTS = {
|
|
586
|
+
lists: /* @__PURE__ */ new Map(),
|
|
587
|
+
items: /* @__PURE__ */ new Map()
|
|
588
|
+
};
|
|
589
|
+
/** Whether an activation's input carries a result this run made anew. */
|
|
590
|
+
const carries = (fresh, input) => {
|
|
591
|
+
if (input === void 0) return false;
|
|
592
|
+
if (fresh.has(input)) return true;
|
|
593
|
+
const results = input["results"];
|
|
594
|
+
if (results === null || typeof results !== "object") return false;
|
|
595
|
+
return Object.values(results).some((value) => value !== null && typeof value === "object" && fresh.has(value));
|
|
596
|
+
};
|
|
597
|
+
/**
|
|
598
|
+
* Hands each node's recorded results back in order, until the node runs anew
|
|
599
|
+
* once (it failed or never finished last time, or its input is new); from then
|
|
600
|
+
* on it always runs. A node cut off mid-session continues that session, once.
|
|
601
|
+
*/
|
|
602
|
+
const createReplay = (history, cutOff = /* @__PURE__ */ new Map(), mail, earlier, fanOuts = NO_FAN_OUTS) => {
|
|
603
|
+
const sessions = new Map(cutOff);
|
|
604
|
+
const used = /* @__PURE__ */ new Map();
|
|
605
|
+
const anew = /* @__PURE__ */ new Set();
|
|
606
|
+
const fresh = /* @__PURE__ */ new WeakSet();
|
|
607
|
+
/** Fanned nodes whose list was resolved anew: their items can't be replayed. */
|
|
608
|
+
const renewed = /* @__PURE__ */ new Set();
|
|
609
|
+
const takeItem = (activation, of) => {
|
|
610
|
+
const recorded = fanOuts.items.get(activation.id)?.get(activation.round ?? 0);
|
|
611
|
+
if (recorded !== void 0 && !renewed.has(of) && !carries(fresh, activation.input)) return recorded;
|
|
612
|
+
anew.add(activation.id);
|
|
613
|
+
return null;
|
|
614
|
+
};
|
|
615
|
+
return {
|
|
616
|
+
take: (activation) => {
|
|
617
|
+
const { id } = activation;
|
|
618
|
+
if (activation.brief !== void 0 || anew.has(id)) return null;
|
|
619
|
+
if (activation.item !== void 0) return takeItem(activation, activation.item.of);
|
|
620
|
+
const index = used.get(id) ?? 0;
|
|
621
|
+
const recorded = history.get(id)?.[index];
|
|
622
|
+
if (recorded === void 0 || carries(fresh, activation.input)) {
|
|
623
|
+
anew.add(id);
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
used.set(id, index + 1);
|
|
627
|
+
return recorded;
|
|
628
|
+
},
|
|
629
|
+
ran: (result) => {
|
|
630
|
+
fresh.add(result.output);
|
|
631
|
+
},
|
|
632
|
+
cutOff: (activation) => {
|
|
633
|
+
const session = activation.brief === void 0 && (activation.item === void 0 || !renewed.has(activation.item.of)) ? sessions.get(activation.id) : void 0;
|
|
634
|
+
sessions.delete(activation.id);
|
|
635
|
+
return session;
|
|
636
|
+
},
|
|
637
|
+
listed: (id, round, source) => {
|
|
638
|
+
const list = fanOuts.lists.get(listKey(id, round));
|
|
639
|
+
const ranAnew = typeof source === "object" && source !== null && fresh.has(source);
|
|
640
|
+
if (list !== void 0 && !ranAnew) return list;
|
|
641
|
+
renewed.add(id);
|
|
642
|
+
return null;
|
|
643
|
+
},
|
|
644
|
+
previousSession: ({ id, item, round = 0 }) => item === void 0 || renewed.has(item.of) ? void 0 : [...fanOuts.items.get(id) ?? []].findLast(([before]) => before < round)?.[1].sessionId,
|
|
645
|
+
lastOutput: (id) => history.get(id)?.at(-1)?.output,
|
|
646
|
+
kept: () => [...history.keys(), ...fanOuts.items.keys()],
|
|
647
|
+
...mail === void 0 ? {} : { mail },
|
|
648
|
+
...earlier === void 0 ? {} : { earlier }
|
|
649
|
+
};
|
|
650
|
+
};
|
|
651
|
+
/**
|
|
652
|
+
* The run's `HumanChannel`, answering questions about loops out of rounds as
|
|
653
|
+
* they were answered last time, in order; anything else goes to `human`.
|
|
654
|
+
*/
|
|
655
|
+
const replayLoopAnswers = (human, answers) => {
|
|
656
|
+
const left = [...answers];
|
|
657
|
+
return {
|
|
658
|
+
permit: (request) => human.permit(request),
|
|
659
|
+
...human.settled === void 0 ? {} : { settled: human.settled },
|
|
660
|
+
ask: (question) => {
|
|
661
|
+
const index = left.findIndex((a) => a.question === question.question);
|
|
662
|
+
const [given] = index === -1 ? [] : left.splice(index, 1);
|
|
663
|
+
return given === void 0 ? human.ask(question) : Promise.resolve(given.answer);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
};
|
|
616
667
|
// ../core/src/store/recorder.ts
|
|
617
668
|
/**
|
|
618
669
|
* @file Records a run as it happens: each event appended to the store and
|
|
@@ -641,6 +692,11 @@ const createRecorder = (store, run, runDir) => {
|
|
|
641
692
|
// src/help-root.ts
|
|
642
693
|
/** Where the user docs start. */
|
|
643
694
|
const DOCS_URL = "https://github.com/mimshins/ork/blob/main/docs/README.md";
|
|
695
|
+
/** The flags every command takes, which `bin.ts` handles before citty. */
|
|
696
|
+
const GLOBAL_FLAGS = [{
|
|
697
|
+
name: "--plain",
|
|
698
|
+
description: "Plain output: no color, no redrawn prompts (also ORK_PLAIN=1, NO_COLOR)"
|
|
699
|
+
}];
|
|
644
700
|
const rowOf = (entry) => {
|
|
645
701
|
const short = groupOf(entry.name)?.short ?? entry.description;
|
|
646
702
|
const alias = entry.aliases.length === 0 ? "" : ` (alias: ${entry.aliases.join(", ")})`;
|
|
@@ -668,6 +724,9 @@ const renderRootHelp = async (cmd) => {
|
|
|
668
724
|
...groups.flatMap((group) => groupLines(group, width)),
|
|
669
725
|
`Usage: ${name} <command> [flags]`,
|
|
670
726
|
"",
|
|
727
|
+
"Flags for every command:",
|
|
728
|
+
...GLOBAL_FLAGS.map((f) => ` ${f.name} ${f.description}`),
|
|
729
|
+
"",
|
|
671
730
|
`Use "${name} <command> --help" for more about a command.`,
|
|
672
731
|
`${name} v${meta.version ?? ""}`
|
|
673
732
|
].join("\n");
|
|
@@ -691,299 +750,870 @@ const renderHelp = async (cmd, parent) => {
|
|
|
691
750
|
if (cmd.subCommands !== void 0 && parent === void 0) return renderRootHelp(shown);
|
|
692
751
|
const above = parent?.meta === void 0 ? void 0 : { meta: parent.meta };
|
|
693
752
|
const usage = await renderUsage(shown, above);
|
|
694
|
-
|
|
753
|
+
const globals = `GLOBAL OPTIONS\n\n${GLOBAL_FLAGS.map((f) => ` ${f.name} ${f.description}`).join("\n")}`;
|
|
754
|
+
if (cmd.subCommands === void 0) return `${usage}\n\n${globals}`;
|
|
695
755
|
const choices = await choicesOf(shown);
|
|
696
|
-
return usage.replace(` [OPTIONS] ${choices}`, ` <command> [flags]`).replace(` ${choices}`, ` <command> [flags]`)
|
|
756
|
+
return `${usage.replace(` [OPTIONS] ${choices}`, ` <command> [flags]`).replace(` ${choices}`, ` <command> [flags]`)}\n\n${globals}`;
|
|
697
757
|
};
|
|
698
758
|
/** Prints {@link renderHelp}'s usage, as citty prints its own. */
|
|
699
759
|
const showHelp = async (cmd, parent) => {
|
|
700
760
|
process.stdout.write(`${await renderHelp(cmd, parent)}\n\n`);
|
|
701
761
|
};
|
|
702
|
-
// src/
|
|
762
|
+
// src/human-card.ts
|
|
763
|
+
/** Lines of context a card shows before `d` (or `--verbose`) shows the rest. */
|
|
764
|
+
const CONTEXT_LINES = 6;
|
|
765
|
+
/** Text from outside with its escapes gone and its line breaks kept. */
|
|
766
|
+
const clean = (text) => text.split("\n").map((line) => plain(line)).join("\n");
|
|
767
|
+
/** The card above the options, in the rich look or the plain lines. */
|
|
768
|
+
const cardLines = (t, q, place, verbose, document) => {
|
|
769
|
+
const context = q.context === void 0 ? void 0 : clean(q.context);
|
|
770
|
+
if (!t.rich) return {
|
|
771
|
+
lines: [
|
|
772
|
+
...place === void 0 ? [] : [`run ${place.run} · decision ${place.decision}`],
|
|
773
|
+
...document === void 0 ? [] : [document],
|
|
774
|
+
`? ${plain(q.nodeId)} asks: ${clean(q.question)}`,
|
|
775
|
+
...context === void 0 ? [] : [` ${context.replaceAll("\n", "\n ")}`]
|
|
776
|
+
],
|
|
777
|
+
moreContext: false
|
|
778
|
+
};
|
|
779
|
+
const shown = context === void 0 ? [] : wrap(context, t.width - 3);
|
|
780
|
+
const cutShort = !verbose && shown.length > CONTEXT_LINES;
|
|
781
|
+
const body = cutShort ? shown.slice(0, CONTEXT_LINES) : shown;
|
|
782
|
+
const where = place === void 0 ? "" : ` ${paint(t, "muted", `run ${place.run} · ${String(place.index)} of ${String(place.total)}`)}`;
|
|
783
|
+
return {
|
|
784
|
+
lines: [
|
|
785
|
+
"",
|
|
786
|
+
` ${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, plain(q.nodeId))}${where}`,
|
|
787
|
+
...wrap(clean(q.question), t.width - 1).map((l) => ` ${bold(t, l)}`),
|
|
788
|
+
...body.map((l) => paint(t, "muted", ` ${l}`)),
|
|
789
|
+
...cutShort ? [paint(t, "muted", ` … ${String(shown.length - CONTEXT_LINES)} more lines (d)`)] : []
|
|
790
|
+
],
|
|
791
|
+
moreContext: cutShort
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
/** The line above a permission prompt: who wants to run what. */
|
|
795
|
+
const permitLine = (t, nodeId, preview) => {
|
|
796
|
+
const node = plain(nodeId);
|
|
797
|
+
const what = plain(preview);
|
|
798
|
+
if (!t.rich) return `? ${node} wants to run ${what}`;
|
|
799
|
+
const room = Math.max(20, t.width - node.length - 24);
|
|
800
|
+
return ` ${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, node)} wants to run ${cut(what, room)}`;
|
|
801
|
+
};
|
|
703
802
|
/**
|
|
704
|
-
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
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`.
|
|
803
|
+
* The line a card collapses to once answered: `✓ 509da722-1 · add-invalid-node`
|
|
804
|
+
* (the decision, or the node when there is none, and the choice), or a note
|
|
805
|
+
* that it was left for later.
|
|
710
806
|
*/
|
|
711
|
-
|
|
712
|
-
const
|
|
807
|
+
const doneLine = (t, place, nodeId, answer) => {
|
|
808
|
+
const who = place?.decision ?? plain(nodeId);
|
|
809
|
+
if (answer.kind === "later") return ` ${paint(t, "muted", "·")} ${cut(`${who} left for later`, t.width - 3)}`;
|
|
810
|
+
const said = answer.note === void 0 ? "" : ` · said: ${plain(answer.note)}`;
|
|
811
|
+
return ` ${paint(t, "ok", "✓")} ${cut(`${who} · ${answer.id}${said}`, t.width - 3)}`;
|
|
812
|
+
};
|
|
813
|
+
// src/term/text.ts
|
|
713
814
|
/**
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
*
|
|
815
|
+
* @file The text prompt: one line of text, edited in place on a terminal
|
|
816
|
+
* (arrows, Home/End, Backspace, Delete, Ctrl-U to clear), collapsing to one
|
|
817
|
+
* line when answered; Enter sends, Esc (or Ctrl-D on an empty line) leaves
|
|
818
|
+
* it. Plain mode asks `message: ` and reads a line, as the prompts always
|
|
819
|
+
* did. `editLine` is the editing itself, which the select prompt reuses for
|
|
820
|
+
* its note.
|
|
717
821
|
*/
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
822
|
+
const at = (next, to) => ({
|
|
823
|
+
value: next.join(""),
|
|
824
|
+
pos: to
|
|
825
|
+
});
|
|
826
|
+
/** `state` after `key`; keys that do not edit leave it as it is. */
|
|
827
|
+
const editLine = (state, key) => {
|
|
828
|
+
const { value, pos } = state;
|
|
829
|
+
const chars = Array.from(value);
|
|
830
|
+
if (key.name === "char") return at(chars.toSpliced(pos, 0, key.text), pos + 1);
|
|
831
|
+
if (key.name === "backspace") return pos === 0 ? state : at(chars.toSpliced(pos - 1, 1), pos - 1);
|
|
832
|
+
if (key.name === "delete") return at(chars.toSpliced(pos, 1), pos);
|
|
833
|
+
if (key.name === "left") return {
|
|
834
|
+
value,
|
|
835
|
+
pos: Math.max(0, pos - 1)
|
|
836
|
+
};
|
|
837
|
+
if (key.name === "right") return {
|
|
838
|
+
value,
|
|
839
|
+
pos: Math.min(chars.length, pos + 1)
|
|
840
|
+
};
|
|
841
|
+
if (key.name === "home" || key.name === "ctrl-a") return {
|
|
842
|
+
value,
|
|
843
|
+
pos: 0
|
|
844
|
+
};
|
|
845
|
+
if (key.name === "end" || key.name === "ctrl-e") return {
|
|
846
|
+
value,
|
|
847
|
+
pos: chars.length
|
|
848
|
+
};
|
|
849
|
+
if (key.name === "ctrl-u") return at(chars.slice(pos), 0);
|
|
850
|
+
return state;
|
|
851
|
+
};
|
|
729
852
|
/**
|
|
730
|
-
*
|
|
731
|
-
*
|
|
853
|
+
* The line to draw for `state` after `lead`, within `width`, and the column of
|
|
854
|
+
* the cursor in it. A value too long for the room scrolls to keep the cursor in
|
|
855
|
+
* view.
|
|
732
856
|
*/
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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 {}
|
|
857
|
+
const showLine = (lead, state, width, leadWidth) => {
|
|
858
|
+
const chars = Array.from(state.value);
|
|
859
|
+
const room = Math.max(1, width - leadWidth - 1);
|
|
860
|
+
const start = Math.max(0, state.pos - room + 1);
|
|
861
|
+
return {
|
|
862
|
+
line: `${lead}${chars.slice(start, start + room).join("")}`,
|
|
863
|
+
column: leadWidth + state.pos - start
|
|
864
|
+
};
|
|
752
865
|
};
|
|
753
|
-
|
|
754
|
-
const
|
|
755
|
-
const
|
|
756
|
-
|
|
757
|
-
current: ORK_VERSION,
|
|
758
|
-
...registry === void 0 ? {} : { registry }
|
|
759
|
-
});
|
|
866
|
+
const defaultDone$1 = (io, message) => (value) => {
|
|
867
|
+
const term = termOf(io.output, io.env);
|
|
868
|
+
const mark = paint(term, "ok", "✓");
|
|
869
|
+
return cut(` ${mark} ${message} · ${value === void 0 || value === "" ? "skipped" : value}`, term.width);
|
|
760
870
|
};
|
|
761
|
-
/**
|
|
762
|
-
const
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
871
|
+
/** Asks for one line of text. */
|
|
872
|
+
const readText = async (io, options) => {
|
|
873
|
+
const { message, signal } = options;
|
|
874
|
+
if (signal?.aborted === true) throw withdrawn(signal);
|
|
875
|
+
if (!canKey(io.input, io.output, io.env)) {
|
|
876
|
+
const answer = await readLine(io, ` ${message}: `, signal);
|
|
877
|
+
return answer === void 0 ? { kind: "cancelled" } : {
|
|
878
|
+
kind: "text",
|
|
879
|
+
value: answer.trim()
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const done = options.done ?? defaultDone$1(io, message);
|
|
883
|
+
const term = () => termOf(io.output, io.env);
|
|
884
|
+
const region = createRegion(io.output, term);
|
|
885
|
+
const initial = options.initial ?? "";
|
|
886
|
+
return new Promise((resolve, reject) => {
|
|
887
|
+
let state = {
|
|
888
|
+
value: initial,
|
|
889
|
+
pos: Array.from(initial).length
|
|
890
|
+
};
|
|
891
|
+
const lead = ` ${paint(term(), "accent", "?")} ${bold(term(), message)} › `;
|
|
892
|
+
const leadWidth = Array.from(` ? ${message} › `).length;
|
|
893
|
+
const render = () => {
|
|
894
|
+
const { line, column } = showLine(lead, state, term().width, leadWidth);
|
|
895
|
+
region.draw([line], column);
|
|
896
|
+
};
|
|
897
|
+
const finish = (result, line) => {
|
|
898
|
+
stop();
|
|
899
|
+
io.output.off("resize", render);
|
|
900
|
+
signal?.removeEventListener("abort", onAbort);
|
|
901
|
+
region.finish(line);
|
|
902
|
+
resolve(result);
|
|
903
|
+
};
|
|
904
|
+
const onAbort = () => {
|
|
905
|
+
stop();
|
|
906
|
+
io.output.off("resize", render);
|
|
907
|
+
region.finish();
|
|
908
|
+
reject(withdrawn(signal));
|
|
909
|
+
};
|
|
910
|
+
const stop = readKeys(io.input, (key) => {
|
|
911
|
+
if (key.name === "enter") {
|
|
912
|
+
const value = state.value.trim();
|
|
913
|
+
finish({
|
|
914
|
+
kind: "text",
|
|
915
|
+
value
|
|
916
|
+
}, done(value));
|
|
917
|
+
} else if (key.name === "escape" || key.name === "ctrl-d" && state.value === "") finish({ kind: "cancelled" }, done());
|
|
918
|
+
else if (key.name === "ctrl-c") {
|
|
919
|
+
finish({ kind: "interrupted" });
|
|
920
|
+
interrupt(io);
|
|
921
|
+
} else {
|
|
922
|
+
state = editLine(state, key);
|
|
923
|
+
render();
|
|
774
924
|
}
|
|
775
925
|
});
|
|
776
|
-
|
|
926
|
+
io.output.on("resize", render);
|
|
927
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
928
|
+
render();
|
|
929
|
+
});
|
|
777
930
|
};
|
|
778
|
-
// src/
|
|
931
|
+
// src/term/select.ts
|
|
779
932
|
/**
|
|
780
|
-
* @file
|
|
781
|
-
*
|
|
782
|
-
*
|
|
783
|
-
*
|
|
933
|
+
* @file The select prompt: a list of options moved with ↑/↓ (or `j`/`k`),
|
|
934
|
+
* Home/End and `1`–`9`, chosen with Enter. `n` adds a note (one line, edited
|
|
935
|
+
* in place), `d` shows or hides the details (a document), `q` or Esc leaves
|
|
936
|
+
* the question for later, Ctrl-C stops. It redraws only its own lines,
|
|
937
|
+
* follows a resize, and collapses to one line once answered. Plain mode
|
|
938
|
+
* prints the options as `1) id label - description` and asks `Choose [n]: `,
|
|
939
|
+
* as `ork decide` always did; there a note is asked for after the choice when
|
|
940
|
+
* the prompt takes one.
|
|
784
941
|
*/
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
942
|
+
const NOTE_PROMPT = "Note for the run (optional, Enter to skip)";
|
|
943
|
+
/** The option an answer names: its number, or its id; empty is `recommended`. */
|
|
944
|
+
const pickOption = (answer, options, recommended) => {
|
|
945
|
+
const value = answer.trim();
|
|
946
|
+
if (value === "") return recommended;
|
|
947
|
+
const index = Number(value);
|
|
948
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
|
|
949
|
+
return options.find((o) => o.id === value)?.id;
|
|
950
|
+
};
|
|
951
|
+
const label = (option) => option.label === void 0 ? option.id : `${option.id} ${option.label}`;
|
|
952
|
+
const defaultDone = (options) => (result, term) => {
|
|
953
|
+
if (result.kind === "later") return ` ${paint(term, "muted", "·")} left for later`;
|
|
954
|
+
const option = options.find((o) => o.id === result.id);
|
|
955
|
+
return cut(` ${paint(term, "ok", "✓")} ${option === void 0 ? result.id : label(option)}`, term.width);
|
|
956
|
+
};
|
|
957
|
+
/** The numbered option lines of a plain prompt: ` 1) id label - description`. */
|
|
958
|
+
const plainOptions = (options, recommended) => options.map((option, i) => {
|
|
959
|
+
const description = option.description === void 0 ? "" : ` - ${option.description}`;
|
|
960
|
+
const mark = option.id === recommended ? " (recommended)" : "";
|
|
961
|
+
const name = option.label === void 0 ? "" : ` ${option.label}`;
|
|
962
|
+
return ` ${String(i + 1)}) ${option.id}${name}${description}${mark}`;
|
|
963
|
+
});
|
|
964
|
+
/** The plain prompt: numbered options, then `Choose [n]: ` until one fits. */
|
|
965
|
+
const selectPlain = (io, o) => {
|
|
966
|
+
const term = termOf(io.output, io.env);
|
|
967
|
+
const say = (text) => {
|
|
968
|
+
io.output.write(`${text}\n`);
|
|
793
969
|
};
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
970
|
+
for (const line of o.header?.(term) ?? []) say(line);
|
|
971
|
+
for (const line of plainOptions(o.options, o.recommended)) say(line);
|
|
972
|
+
for (const line of o.footer ?? []) say(line);
|
|
973
|
+
const at = o.options.findIndex((opt) => opt.id === o.recommended);
|
|
974
|
+
const hint = at === -1 ? "" : ` [${String(at + 1)}]`;
|
|
975
|
+
const ask = async () => {
|
|
976
|
+
const answer = await readLine(io, ` Choose${hint}: `, o.signal);
|
|
977
|
+
if (answer === void 0) throw new Error("input ended");
|
|
978
|
+
const id = pickOption(answer, o.options, o.recommended);
|
|
979
|
+
if (id === void 0) {
|
|
980
|
+
if (o.details !== void 0 && answer.trim() === "d") say(o.details);
|
|
981
|
+
else say(" Type an option's number or id.");
|
|
982
|
+
return ask();
|
|
983
|
+
}
|
|
984
|
+
if (o.note !== true) return {
|
|
985
|
+
kind: "chosen",
|
|
986
|
+
id
|
|
987
|
+
};
|
|
988
|
+
const noted = await readText(io, {
|
|
989
|
+
message: o.notePrompt ?? NOTE_PROMPT,
|
|
990
|
+
...o.signal === void 0 ? {} : { signal: o.signal }
|
|
991
|
+
});
|
|
992
|
+
return noted.kind === "text" && noted.value !== "" ? {
|
|
993
|
+
kind: "chosen",
|
|
994
|
+
id,
|
|
995
|
+
note: noted.value
|
|
996
|
+
} : {
|
|
997
|
+
kind: "chosen",
|
|
998
|
+
id
|
|
999
|
+
};
|
|
798
1000
|
};
|
|
1001
|
+
return ask();
|
|
1002
|
+
};
|
|
1003
|
+
/** Asks for one of `options`. */
|
|
1004
|
+
const select = (io, o) => {
|
|
1005
|
+
if (o.signal?.aborted === true) return Promise.reject(withdrawn(o.signal));
|
|
1006
|
+
if (!canKey(io.input, io.output, io.env)) return selectPlain(io, o);
|
|
1007
|
+
const term = () => termOf(io.output, io.env);
|
|
1008
|
+
const region = createRegion(io.output, term);
|
|
1009
|
+
const done = o.done ?? defaultDone(o.options);
|
|
1010
|
+
return new Promise((resolve, reject) => {
|
|
1011
|
+
const start = o.options.findIndex((opt) => opt.id === o.recommended);
|
|
1012
|
+
let cursor = Math.max(0, start);
|
|
1013
|
+
let showDetails = false;
|
|
1014
|
+
let note;
|
|
1015
|
+
let editing;
|
|
1016
|
+
const optionLines = (t) => o.options.flatMap((option, i) => {
|
|
1017
|
+
const selected = i === cursor;
|
|
1018
|
+
const number = i < 9 ? `${String(i + 1)}.` : " ";
|
|
1019
|
+
const mark = option.id === o.recommended ? " (recommended)" : "";
|
|
1020
|
+
const head = cut(`${number} ${label(option)}`, t.width - 4 - mark.length);
|
|
1021
|
+
const text = selected ? apply$1(t, ["bold", "cyan"], head) : head;
|
|
1022
|
+
const lines = [` ${selected ? paint(t, "accent", "❯") : " "} ${text}${mark === "" ? "" : paint(t, "ok", mark)}`];
|
|
1023
|
+
for (const line of wrap(option.description ?? "", t.width - 7)) if (line !== "") lines.push(paint(t, "muted", ` ${line}`));
|
|
1024
|
+
return lines;
|
|
1025
|
+
});
|
|
1026
|
+
const keyLine = (t) => {
|
|
1027
|
+
const keys = editing === void 0 ? [
|
|
1028
|
+
"↑/↓ move",
|
|
1029
|
+
"Enter choose",
|
|
1030
|
+
...o.note === true ? ["n note"] : [],
|
|
1031
|
+
...o.details === void 0 ? [] : [`d ${showDetails ? "hide" : "show"} details`],
|
|
1032
|
+
...o.later === true ? ["q later"] : []
|
|
1033
|
+
] : ["Enter save", "Esc cancel"];
|
|
1034
|
+
return paint(t, "muted", cut(` ${keys.join(" · ")}`, t.width));
|
|
1035
|
+
};
|
|
1036
|
+
const render = () => {
|
|
1037
|
+
const t = term();
|
|
1038
|
+
const head = o.header?.(t) ?? [];
|
|
1039
|
+
const body = optionLines(t);
|
|
1040
|
+
const noteLine = note === void 0 ? [] : [paint(t, "muted", cut(` note: ${note}`, t.width))];
|
|
1041
|
+
const above = [
|
|
1042
|
+
...head,
|
|
1043
|
+
...body,
|
|
1044
|
+
...noteLine
|
|
1045
|
+
];
|
|
1046
|
+
const room = Math.max(3, t.rows - above.length - 3);
|
|
1047
|
+
const all = showDetails && o.details !== void 0 ? wrap(o.details.trimEnd(), t.width - 3) : [];
|
|
1048
|
+
const detail = (all.length > room ? [...all.slice(0, room - 1), `… ${String(all.length - room + 1)} more lines`] : all).map((line) => paint(t, "muted", ` ${line}`));
|
|
1049
|
+
const lines = [
|
|
1050
|
+
...above,
|
|
1051
|
+
...detail,
|
|
1052
|
+
keyLine(t)
|
|
1053
|
+
];
|
|
1054
|
+
if (editing === void 0) {
|
|
1055
|
+
region.draw(lines);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const lead = ` ${paint(t, "accent", "✎")} Note › `;
|
|
1059
|
+
const { line, column } = showLine(lead, editing, t.width, 10);
|
|
1060
|
+
region.draw([...lines, line], column);
|
|
1061
|
+
};
|
|
1062
|
+
const finish = (result) => {
|
|
1063
|
+
stop();
|
|
1064
|
+
cleanup();
|
|
1065
|
+
region.finish(done(result, term()));
|
|
1066
|
+
resolve(result);
|
|
1067
|
+
};
|
|
1068
|
+
const cleanup = () => {
|
|
1069
|
+
io.output.off("resize", render);
|
|
1070
|
+
o.signal?.removeEventListener("abort", onAbort);
|
|
1071
|
+
};
|
|
1072
|
+
const onAbort = () => {
|
|
1073
|
+
stop();
|
|
1074
|
+
cleanup();
|
|
1075
|
+
region.finish();
|
|
1076
|
+
reject(withdrawn(o.signal));
|
|
1077
|
+
};
|
|
1078
|
+
const edit = (key) => {
|
|
1079
|
+
if (editing === void 0) return;
|
|
1080
|
+
if (key.name === "enter") {
|
|
1081
|
+
const value = editing.value.trim();
|
|
1082
|
+
note = value === "" ? void 0 : value;
|
|
1083
|
+
editing = void 0;
|
|
1084
|
+
} else if (key.name === "escape") editing = void 0;
|
|
1085
|
+
else editing = editLine(editing, key);
|
|
1086
|
+
render();
|
|
1087
|
+
};
|
|
1088
|
+
const move = (to) => {
|
|
1089
|
+
cursor = Math.min(o.options.length - 1, Math.max(0, to));
|
|
1090
|
+
render();
|
|
1091
|
+
};
|
|
1092
|
+
const stop = readKeys(io.input, (key) => {
|
|
1093
|
+
if (key.name === "ctrl-c") {
|
|
1094
|
+
stop();
|
|
1095
|
+
cleanup();
|
|
1096
|
+
region.finish();
|
|
1097
|
+
resolve({ kind: "interrupted" });
|
|
1098
|
+
interrupt(io);
|
|
1099
|
+
} else if (editing !== void 0) edit(key);
|
|
1100
|
+
else if (key.name === "enter") {
|
|
1101
|
+
const id = o.options[cursor]?.id;
|
|
1102
|
+
if (id === void 0) return;
|
|
1103
|
+
finish(note === void 0 ? {
|
|
1104
|
+
kind: "chosen",
|
|
1105
|
+
id
|
|
1106
|
+
} : {
|
|
1107
|
+
kind: "chosen",
|
|
1108
|
+
id,
|
|
1109
|
+
note
|
|
1110
|
+
});
|
|
1111
|
+
} else if (key.name === "up") move(cursor - 1);
|
|
1112
|
+
else if (key.name === "down") move(cursor + 1);
|
|
1113
|
+
else if (key.name === "home") move(0);
|
|
1114
|
+
else if (key.name === "end") move(o.options.length - 1);
|
|
1115
|
+
else if (key.name === "escape" && o.later === true) finish({ kind: "later" });
|
|
1116
|
+
else if (key.name === "char") {
|
|
1117
|
+
const c = key.text;
|
|
1118
|
+
if (c === "k") move(cursor - 1);
|
|
1119
|
+
else if (c === "j") move(cursor + 1);
|
|
1120
|
+
else if (c >= "1" && c <= "9") {
|
|
1121
|
+
if (Number(c) <= o.options.length) move(Number(c) - 1);
|
|
1122
|
+
} else if (c === "n" && o.note === true) {
|
|
1123
|
+
const value = note ?? "";
|
|
1124
|
+
editing = {
|
|
1125
|
+
value,
|
|
1126
|
+
pos: Array.from(value).length
|
|
1127
|
+
};
|
|
1128
|
+
render();
|
|
1129
|
+
} else if (c === "d" && o.details !== void 0) {
|
|
1130
|
+
showDetails = !showDetails;
|
|
1131
|
+
render();
|
|
1132
|
+
} else if (c === "q" && o.later === true) finish({ kind: "later" });
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
io.output.on("resize", render);
|
|
1136
|
+
o.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1137
|
+
render();
|
|
1138
|
+
});
|
|
799
1139
|
};
|
|
800
|
-
// src/
|
|
1140
|
+
// src/human.ts
|
|
801
1141
|
/**
|
|
802
|
-
* @file The
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
*
|
|
806
|
-
*
|
|
1142
|
+
* @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
|
|
1143
|
+
* permission prompts on the terminal, one at a time, and denies or defers
|
|
1144
|
+
* when no one is there. It draws through the terminal kit (`term/`): a card
|
|
1145
|
+
* with an ASK chip and a list moved with the arrow keys, a yes-or-no for a
|
|
1146
|
+
* permission, and one optional line for a note after a choice. Plain (not a
|
|
1147
|
+
* terminal, `NO_COLOR`, `--plain`) it keeps the numbered prompts. In a run it
|
|
1148
|
+
* is the decision queue's local answerer, racing `ork decide`.
|
|
807
1149
|
*/
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1150
|
+
const PREVIEW = 160;
|
|
1151
|
+
/** A document longer than this is offered (`d`) rather than printed. */
|
|
1152
|
+
const SHORT_DOCUMENT_LINES = 40;
|
|
1153
|
+
const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
|
|
1154
|
+
const NOTE_MESSAGE = "Note for the run (optional, Enter to skip)";
|
|
1155
|
+
const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
|
|
1156
|
+
/** One line that shows what a tool is about to do. */
|
|
1157
|
+
const previewInput = (toolName, input) => {
|
|
1158
|
+
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
1159
|
+
const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
|
|
1160
|
+
return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
|
|
1161
|
+
};
|
|
1162
|
+
/** Never answers, until `signal` withdraws it: what a stopped prompt leaves. */
|
|
1163
|
+
const parked = (signal) => new Promise((_, reject) => {
|
|
1164
|
+
signal?.addEventListener("abort", () => {
|
|
1165
|
+
reject(withdrawn(signal));
|
|
818
1166
|
});
|
|
819
1167
|
});
|
|
820
|
-
/**
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1168
|
+
/**
|
|
1169
|
+
* Answers permission prompts and `ask_user` questions on the terminal, one at a
|
|
1170
|
+
* time. Without a terminal it denies tools and leaves questions to the node. A
|
|
1171
|
+
* withdrawn prompt (answered elsewhere) rejects, which frees the turn.
|
|
1172
|
+
*/
|
|
1173
|
+
const createTerminalHuman = (io) => {
|
|
1174
|
+
let rl;
|
|
1175
|
+
let closed;
|
|
1176
|
+
let turn = Promise.resolve();
|
|
1177
|
+
const say = (text) => {
|
|
1178
|
+
io.output.write(`${text}\n`);
|
|
1179
|
+
};
|
|
1180
|
+
/** The one `readline` the plain prompts share; a rich prompt never opens it. */
|
|
1181
|
+
const readLine = (text, signal) => {
|
|
1182
|
+
if (rl === void 0) {
|
|
1183
|
+
const opened = createInterface({
|
|
1184
|
+
input: io.input,
|
|
1185
|
+
output: io.output,
|
|
1186
|
+
...plainEnv(io.env) ? { terminal: false } : {}
|
|
1187
|
+
});
|
|
1188
|
+
if (io.interrupt !== void 0) opened.on("SIGINT", io.interrupt);
|
|
1189
|
+
closed = new Promise((resolve) => {
|
|
1190
|
+
opened.once("close", () => {
|
|
1191
|
+
resolve(void 0);
|
|
1192
|
+
});
|
|
1193
|
+
});
|
|
1194
|
+
rl = opened;
|
|
1195
|
+
}
|
|
1196
|
+
const answer = signal === void 0 ? rl.question(text) : rl.question(text, { signal });
|
|
1197
|
+
return closed === void 0 ? answer : Promise.race([answer, closed]);
|
|
1198
|
+
};
|
|
1199
|
+
const kit = {
|
|
1200
|
+
input: io.input,
|
|
1201
|
+
output: io.output,
|
|
1202
|
+
readLine,
|
|
1203
|
+
...io.env === void 0 ? {} : { env: io.env },
|
|
1204
|
+
...io.interrupt === void 0 ? {} : { interrupt: io.interrupt }
|
|
1205
|
+
};
|
|
1206
|
+
const term = () => termOf(io.output, io.env);
|
|
1207
|
+
/** The optional line after a choice, when the asker takes a note. */
|
|
1208
|
+
const takeNote = async (onNote, signal) => {
|
|
1209
|
+
if (onNote === void 0) return;
|
|
1210
|
+
const noted = await readText(kit, {
|
|
1211
|
+
message: NOTE_MESSAGE,
|
|
1212
|
+
...signal === void 0 ? {} : { signal }
|
|
1213
|
+
});
|
|
1214
|
+
if (noted.kind === "interrupted") await parked(signal);
|
|
1215
|
+
if (noted.kind === "text" && noted.value !== "") onNote(noted.value);
|
|
1216
|
+
};
|
|
1217
|
+
const oneAtATime = (work) => {
|
|
1218
|
+
const result = turn.then(work);
|
|
1219
|
+
turn = result.catch(() => null);
|
|
1220
|
+
return result;
|
|
1221
|
+
};
|
|
1222
|
+
const asking = (q, place) => oneAtATime(async () => {
|
|
1223
|
+
const { signal } = q;
|
|
1224
|
+
if (signal?.aborted === true) throw new Error("the question was withdrawn");
|
|
1225
|
+
const t = term();
|
|
1226
|
+
const documentLines = q.document?.trimEnd().split("\n").length ?? 0;
|
|
1227
|
+
const document = q.document === void 0 ? void 0 : clean(q.document);
|
|
1228
|
+
const printed = document !== void 0 && documentLines <= SHORT_DOCUMENT_LINES;
|
|
1229
|
+
const shownDocument = t.rich || !printed ? void 0 : document;
|
|
1230
|
+
const header = (tt) => cardLines(tt, q, place, io.verbose === true, shownDocument).lines;
|
|
1231
|
+
const options = q.options.map((o) => ({
|
|
1232
|
+
id: o.id,
|
|
1233
|
+
...o.label === void 0 ? {} : { label: plain(o.label) },
|
|
1234
|
+
...o.description === void 0 ? {} : { description: plain(o.description) }
|
|
1235
|
+
}));
|
|
1236
|
+
const { moreContext } = cardLines(t, q, place, io.verbose === true);
|
|
1237
|
+
const details = t.rich ? [moreContext ? clean(q.context ?? "") : void 0, document].filter((part) => part !== void 0).join("\n\n") || void 0 : printed ? void 0 : document;
|
|
1238
|
+
if (!io.interactive) {
|
|
1239
|
+
for (const line of header(t)) say(line);
|
|
1240
|
+
for (const line of plainOptions(options, q.recommended)) say(line);
|
|
1241
|
+
const answer = q.recommended ?? NO_ONE_TO_ASK;
|
|
1242
|
+
say(paint(t, "muted", ` no terminal; answered ${answer}`));
|
|
1243
|
+
return answer;
|
|
1244
|
+
}
|
|
1245
|
+
const result = await select(kit, {
|
|
1246
|
+
header,
|
|
1247
|
+
options,
|
|
1248
|
+
...q.recommended === void 0 ? {} : { recommended: q.recommended },
|
|
1249
|
+
...details === void 0 ? {} : { details },
|
|
1250
|
+
...!t.rich && details !== void 0 ? { footer: [` Document: ${String(documentLines)} lines. Type d to read it.`] } : {},
|
|
1251
|
+
note: q.onNote !== void 0,
|
|
1252
|
+
notePrompt: NOTE_MESSAGE,
|
|
1253
|
+
later: place !== void 0,
|
|
1254
|
+
done: (answer, tt) => doneLine(tt, place, q.nodeId, answer),
|
|
1255
|
+
...signal === void 0 ? {} : { signal }
|
|
1256
|
+
});
|
|
1257
|
+
if (result.kind === "interrupted") return parked(signal);
|
|
1258
|
+
if (result.kind === "later") return null;
|
|
1259
|
+
if (result.note !== void 0) q.onNote?.(result.note);
|
|
1260
|
+
return result.id;
|
|
1261
|
+
});
|
|
1262
|
+
return {
|
|
1263
|
+
permit: ({ nodeId, toolName, input, signal, onNote }) => oneAtATime(async () => {
|
|
1264
|
+
if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
|
|
1265
|
+
const t = term();
|
|
1266
|
+
if (t.rich) say("");
|
|
1267
|
+
say(permitLine(t, nodeId, previewInput(toolName, input)));
|
|
1268
|
+
if (!io.interactive) {
|
|
1269
|
+
say(paint(t, "muted", ` denied: ${NO_ONE_TO_PERMIT}`));
|
|
1270
|
+
return {
|
|
1271
|
+
allow: false,
|
|
1272
|
+
message: NO_ONE_TO_PERMIT
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
const allowed = await confirmOrStop(kit, {
|
|
1276
|
+
question: "Allow?",
|
|
1277
|
+
...signal === void 0 ? {} : { signal }
|
|
1278
|
+
});
|
|
1279
|
+
if (allowed === null) return parked(signal);
|
|
1280
|
+
if (allowed) return { allow: true };
|
|
1281
|
+
await takeNote(onNote, signal);
|
|
1282
|
+
return {
|
|
1283
|
+
allow: false,
|
|
1284
|
+
message: "The user did not allow this tool."
|
|
1285
|
+
};
|
|
1286
|
+
}),
|
|
1287
|
+
ask: async (q) => await asking(q) ?? NO_ONE_TO_ASK,
|
|
1288
|
+
askAt: (q, place) => asking(q, place),
|
|
1289
|
+
close: () => {
|
|
1290
|
+
rl?.close();
|
|
1291
|
+
}
|
|
835
1292
|
};
|
|
836
1293
|
};
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
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
|
-
}];
|
|
1294
|
+
// src/run/format-end.ts
|
|
1295
|
+
/**
|
|
1296
|
+
* @file How a node's end reads on the terminal (specs/runtime/session-ends.md):
|
|
1297
|
+
* its kind's label and Ork's own message, a note for a kind that may pass,
|
|
1298
|
+
* the setup hint for an account or provider end, and the scrubbed raw detail
|
|
1299
|
+
* only when asked for. Never the provider's own words first.
|
|
1300
|
+
*/
|
|
1301
|
+
/** What a transient kind says in place of its hint on the log line. */
|
|
1302
|
+
const MAY_PASS = "(may pass; ork resume retries)";
|
|
1303
|
+
/** `Turn limit: reached its turn limit (8)`, with a note when it may pass. */
|
|
1304
|
+
const endHeadline = (end) => {
|
|
1305
|
+
const { label, transient } = END_KINDS[end.kind];
|
|
1306
|
+
return `${label}: ${end.message}${transient ? ` ${MAY_PASS}` : ""}`;
|
|
860
1307
|
};
|
|
861
1308
|
/**
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
*
|
|
1309
|
+
* The lines under a headline: the hint for an account or provider end (what to
|
|
1310
|
+
* set up) or whenever `full` (`--verbose`, one node's log), and the detail only
|
|
1311
|
+
* when `full`. A transient kind's hint is its retry wording, which the headline
|
|
1312
|
+
* already says, so it is not printed twice.
|
|
865
1313
|
*/
|
|
866
|
-
const
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
};
|
|
1314
|
+
const endLines = (end, full, term = createView().term) => {
|
|
1315
|
+
const style = styler(term);
|
|
1316
|
+
const { actor, hint, transient } = END_KINDS[end.kind];
|
|
1317
|
+
const setup = actor === "account" || actor === "provider";
|
|
1318
|
+
const lines = [];
|
|
1319
|
+
if (hint !== "" && !transient && (full || setup)) lines.push(` ${style("dim", hint)}`);
|
|
1320
|
+
if (full && end.detail !== void 0) lines.push(style("dim", " details:"), ...end.detail.split("\n").map((line) => style("dim", ` ${line}`)));
|
|
1321
|
+
return lines;
|
|
883
1322
|
};
|
|
884
1323
|
/**
|
|
885
|
-
*
|
|
886
|
-
*
|
|
887
|
-
*
|
|
1324
|
+
* A finished node's line under `tag`: its verdict, or for a failure its kind
|
|
1325
|
+
* and message (and the lines under it). The agent's own summary stays for a
|
|
1326
|
+
* `fail` it reported, and an old run's text no rule reads stays as stored.
|
|
888
1327
|
*/
|
|
889
|
-
const
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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
|
-
};
|
|
1328
|
+
const formatFinished = (event, tag, verbose, term = createView().term, rest = tag) => {
|
|
1329
|
+
const style = styler(term);
|
|
1330
|
+
const conflict = hasConflict(event.output);
|
|
1331
|
+
const mark = event.ok ? style(conflict ? "yellow" : "green", conflict ? "!" : "✓") : style("red", "✗");
|
|
1332
|
+
const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
|
|
1333
|
+
const end = endOf(event);
|
|
1334
|
+
if (!(!event.ok && end.kind !== "reported_failure" && (event.end !== void 0 || end.kind !== "unknown"))) return [`${tag} ${mark} ${nodeVerdict(event.output, event.ok, event.error)}${spent}`];
|
|
1335
|
+
return [`${tag} ${mark} ${endHeadline(end)}${spent}`, ...endLines(end, verbose, term).map((line) => `${rest} ${line}`)];
|
|
902
1336
|
};
|
|
1337
|
+
// src/run/format-outcome.ts
|
|
903
1338
|
/**
|
|
904
|
-
*
|
|
905
|
-
*
|
|
906
|
-
* is the user's `~` (tests pass a folder).
|
|
1339
|
+
* @file How a finished session reads on the terminal: its report or its end,
|
|
1340
|
+
* the branch, artifacts and fields, and the usage line.
|
|
907
1341
|
*/
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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();
|
|
1342
|
+
const STATUS_MARK = {
|
|
1343
|
+
pass: {
|
|
1344
|
+
mark: "✓",
|
|
1345
|
+
color: "green"
|
|
1346
|
+
},
|
|
1347
|
+
done: {
|
|
1348
|
+
mark: "✓",
|
|
1349
|
+
color: "green"
|
|
1350
|
+
},
|
|
1351
|
+
changes_requested: {
|
|
1352
|
+
mark: "!",
|
|
1353
|
+
color: "yellow"
|
|
1354
|
+
},
|
|
1355
|
+
fail: {
|
|
1356
|
+
mark: "✗",
|
|
1357
|
+
color: "red"
|
|
934
1358
|
}
|
|
935
1359
|
};
|
|
936
|
-
|
|
1360
|
+
const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
|
|
1361
|
+
/** The usage line: cost, tokens, turns and time. */
|
|
1362
|
+
const formatUsage = ({ usage }, term = createView().term) => styler(term)("dim", [
|
|
1363
|
+
cost(usage.costUsd),
|
|
1364
|
+
`${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
|
|
1365
|
+
`${String(usage.turns)} turns`,
|
|
1366
|
+
seconds(usage.durationMs)
|
|
1367
|
+
].join(" · "));
|
|
1368
|
+
/** How a finished session reads on the terminal. */
|
|
1369
|
+
const formatOutcome = (nodeId, outcome, verbose = false, term = createView().term) => {
|
|
1370
|
+
const style = styler(term);
|
|
1371
|
+
const { report } = outcome;
|
|
1372
|
+
if (report === null) {
|
|
1373
|
+
const end = outcomeEnd(outcome);
|
|
1374
|
+
return [
|
|
1375
|
+
`${style("red", "✗")} ${style("bold", nodeId)} ${endHeadline(end)}`,
|
|
1376
|
+
...endLines(end, verbose, term),
|
|
1377
|
+
` ${formatUsage(outcome, term)}`
|
|
1378
|
+
];
|
|
1379
|
+
}
|
|
1380
|
+
const shown = STATUS_MARK[report.status];
|
|
1381
|
+
const lines = [`${shown === void 0 ? "-" : style(shown.color, shown.mark)} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
|
|
1382
|
+
if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
|
|
1383
|
+
for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
|
|
1384
|
+
for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
|
|
1385
|
+
lines.push(` ${formatUsage(outcome, term)}`);
|
|
1386
|
+
return lines;
|
|
1387
|
+
};
|
|
1388
|
+
// src/run/format-result.ts
|
|
1389
|
+
/** How a run ended: its mark, and its chip's word and role when rich. */
|
|
1390
|
+
const RUN_END = {
|
|
1391
|
+
succeeded: {
|
|
1392
|
+
mark: "✓",
|
|
1393
|
+
ok: "ok",
|
|
1394
|
+
label: CHIP_LABELS.ok
|
|
1395
|
+
},
|
|
1396
|
+
failed: {
|
|
1397
|
+
mark: "✗",
|
|
1398
|
+
ok: "danger",
|
|
1399
|
+
label: CHIP_LABELS.fail
|
|
1400
|
+
},
|
|
1401
|
+
budget_exhausted: {
|
|
1402
|
+
mark: "!",
|
|
1403
|
+
ok: "warn",
|
|
1404
|
+
label: CHIP_LABELS.wait
|
|
1405
|
+
},
|
|
1406
|
+
quota_exhausted: {
|
|
1407
|
+
mark: "!",
|
|
1408
|
+
ok: "warn",
|
|
1409
|
+
label: CHIP_LABELS.wait
|
|
1410
|
+
},
|
|
1411
|
+
timed_out: {
|
|
1412
|
+
mark: "!",
|
|
1413
|
+
ok: "warn",
|
|
1414
|
+
label: CHIP_LABELS.wait
|
|
1415
|
+
},
|
|
1416
|
+
interrupted: {
|
|
1417
|
+
mark: "!",
|
|
1418
|
+
ok: "warn",
|
|
1419
|
+
label: CHIP_LABELS.wait
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
const MARK_COLOR = {
|
|
1423
|
+
ok: "green",
|
|
1424
|
+
danger: "red",
|
|
1425
|
+
warn: "yellow"
|
|
1426
|
+
};
|
|
1427
|
+
/** `1 failure handled`, `2 failures handled`. */
|
|
1428
|
+
const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" : "failures"} handled`;
|
|
937
1429
|
/**
|
|
938
|
-
*
|
|
939
|
-
*
|
|
940
|
-
* ready on this machine, said in one line. With none ready, the run stops
|
|
941
|
-
* before any session starts, with the hints `ork doctor` gives. A resumed run
|
|
942
|
-
* keeps the driver it recorded.
|
|
1430
|
+
* The run summary: status, cost, what ran, what was skipped and which failures
|
|
1431
|
+
* an edge handled, branches to look at, why, and mail to nodes that never ran.
|
|
943
1432
|
*/
|
|
944
|
-
|
|
945
|
-
const
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
const
|
|
949
|
-
|
|
1433
|
+
const formatRunResult = (result, term = createView().term) => {
|
|
1434
|
+
const style = styler(term);
|
|
1435
|
+
const end = RUN_END[result.status];
|
|
1436
|
+
const mark = term.rich ? chip(term, end.label, end.ok) : style(MARK_COLOR[end.ok], end.mark);
|
|
1437
|
+
const ran = result.nodes.filter((n) => n.runs > 0).length;
|
|
1438
|
+
const skipped = result.skipped ?? result.nodes.length - ran;
|
|
1439
|
+
const handled = result.failuresHandled ?? 0;
|
|
1440
|
+
const lines = ["", `${mark} run ${result.runId} ${result.status.replace("_", " ")} ${style("dim", `· ${cost(result.costUsd)} · ${String(ran)} ran, ${String(skipped)} skipped${handled === 0 ? "" : `, ${failuresHandled(handled)}`}`)}`];
|
|
1441
|
+
for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
|
|
1442
|
+
for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
|
|
1443
|
+
for (const { node, messages } of result.unread ?? []) lines.push(` ${style(["yellow", "bold"], "warning")} ${node} never ran, so ${messages === 1 ? "1 message" : `${String(messages)} messages`} to it went unread`);
|
|
1444
|
+
return lines;
|
|
950
1445
|
};
|
|
1446
|
+
// src/run/format-tool.ts
|
|
951
1447
|
/**
|
|
952
|
-
*
|
|
953
|
-
*
|
|
1448
|
+
* @file How a session's own words and tool calls read in the transcript: the
|
|
1449
|
+
* agent's text (indented plain, wrapped rich), and a tool call as `Tool first
|
|
1450
|
+
* line` cut to the screen with `+N lines`, a path inside the node's worktree
|
|
1451
|
+
* relative to it, or whole with `--verbose`. Untrusted text goes through
|
|
1452
|
+
* `plain()` first.
|
|
954
1453
|
*/
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
};
|
|
1454
|
+
const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${plain(line)}`);
|
|
1455
|
+
/** Agent text under a 2-column indent, its long lines wrapped to `room`. */
|
|
1456
|
+
const wrapped = (text, room) => {
|
|
1457
|
+
const width = Math.max(1, room - 2);
|
|
1458
|
+
return text.trimEnd().split("\n").flatMap((line) => {
|
|
1459
|
+
const safe = plain(line).trimEnd();
|
|
1460
|
+
return widthOf(safe) <= width ? [safe] : wrap(safe, width);
|
|
1461
|
+
}).map((line) => ` ${line}`);
|
|
1462
|
+
};
|
|
1463
|
+
/** A tool's input as the call says it: the key that names its target, whole. */
|
|
1464
|
+
const toolText = (input, root) => {
|
|
1465
|
+
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
1466
|
+
const text = key === void 0 ? JSON.stringify(input) : String(input[key]);
|
|
1467
|
+
if (root === void 0) return text;
|
|
1468
|
+
const base = root.replace(/\/+$/u, "");
|
|
1469
|
+
return text === base ? "." : text.replaceAll(`${base}/`, "");
|
|
971
1470
|
};
|
|
972
1471
|
/**
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
* recorded; otherwise the one ready here.
|
|
1472
|
+
* A tool call: rich, `Tool first line` cut to `room` with `+N lines` when it
|
|
1473
|
+
* had more; plain, today's one dim line. `verbose` shows the call whole.
|
|
976
1474
|
*/
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
const
|
|
980
|
-
if (
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const
|
|
986
|
-
|
|
1475
|
+
const toolLines = (name, input, view, room, verbose, node) => {
|
|
1476
|
+
const { term } = view;
|
|
1477
|
+
const style = styler(term);
|
|
1478
|
+
if (!term.rich && !verbose) return [style("dim", ` · ${plain(previewInput(name, input))}`)];
|
|
1479
|
+
const label = plain(name);
|
|
1480
|
+
const [first = "", ...rest] = toolText(input, view.roots.get(node)).trimEnd().split("\n").map((line) => plain(line));
|
|
1481
|
+
const lead = term.rich ? " " : " · ";
|
|
1482
|
+
const sep = term.rich ? " " : " ";
|
|
1483
|
+
const headWidth = widthOf(lead) + widthOf(label) + sep.length;
|
|
1484
|
+
const styledHead = `${style("dim", lead)}${style("bold", label)}${style("dim", sep)}`;
|
|
1485
|
+
if (verbose) {
|
|
1486
|
+
const pad = " ".repeat(headWidth);
|
|
1487
|
+
return [`${styledHead}${style("dim", first)}`, ...rest.map((line) => style("dim", `${pad}${line}`))];
|
|
1488
|
+
}
|
|
1489
|
+
const more = rest.length === 0 ? "" : ` +${String(rest.length)} ${rest.length === 1 ? "line" : "lines"}`;
|
|
1490
|
+
return [`${styledHead}${style("dim", cut(first.trim(), Math.max(1, room - headWidth - more.length)))}${style("dim", more)}`];
|
|
1491
|
+
};
|
|
1492
|
+
// src/run/format.ts
|
|
1493
|
+
/**
|
|
1494
|
+
* @file How runs read on the terminal: session events and run events with node
|
|
1495
|
+
* chips or prefixes; the node result is `format-outcome.ts`'s, a tool call
|
|
1496
|
+
* `format-tool.ts`'s and the run summary `format-result.ts`'s.
|
|
1497
|
+
*/
|
|
1498
|
+
/** A line's 2-column indent, after any style codes that open it. */
|
|
1499
|
+
const INDENT = /^((?:\u001B\[[0-9;]*m)*) {2}/u;
|
|
1500
|
+
/** Who answered a decision, as the log says it. */
|
|
1501
|
+
const ANSWERED_BY = {
|
|
1502
|
+
terminal: "terminal",
|
|
1503
|
+
decide: "ork decide",
|
|
1504
|
+
ui: "ork ui",
|
|
1505
|
+
owner: "its owner"
|
|
1506
|
+
};
|
|
1507
|
+
/** The transcript lines for one event; tool names come from `tools`. */
|
|
1508
|
+
const formatEvent = (nodeId, event, tools, verbose = false, view = createView(), room = view.term.width - 1) => {
|
|
1509
|
+
const { term } = view;
|
|
1510
|
+
const style = styler(term);
|
|
1511
|
+
if (event.type === "session.started") {
|
|
1512
|
+
const fork = event.fork === true ? ", a fork to answer a question" : "";
|
|
1513
|
+
return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId}${fork})`)];
|
|
1514
|
+
}
|
|
1515
|
+
if (event.type === "assistant.text") return term.rich ? wrapped(event.text, room) : indent(event.text);
|
|
1516
|
+
if (event.type === "session.waiting") return indent(event.reason);
|
|
1517
|
+
if (event.type === "tool.called") {
|
|
1518
|
+
tools.set(event.id, event.name);
|
|
1519
|
+
return toolLines(event.name, event.input, view, room, verbose, nodeId);
|
|
1520
|
+
}
|
|
1521
|
+
if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
|
|
1522
|
+
if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
|
|
1523
|
+
return formatOutcome(nodeId, event.outcome, verbose, term);
|
|
1524
|
+
};
|
|
1525
|
+
const items = (count) => `${String(count)} ${count === 1 ? "item" : "items"}`;
|
|
1526
|
+
/** Transcript lines for one run event, prefixed with the node they are from. */
|
|
1527
|
+
const formatRunEvent = (event, width, tools, terminal = false, verbose = false, view = createView()) => {
|
|
1528
|
+
const { term } = view;
|
|
1529
|
+
const style = styler(term);
|
|
1530
|
+
const tag = (id) => term.rich ? `${nodeChip(term, id)}${" ".repeat(Math.max(0, width - id.length))}` : style("cyan", `${id.padEnd(width)} │`);
|
|
1531
|
+
const blank = (id) => term.rich ? " ".repeat(widthOf(tag(id))) : tag(id);
|
|
1532
|
+
const under = (id, lines) => lines.map((line, i) => `${i === 0 ? tag(id) : blank(id)} ${line}`);
|
|
1533
|
+
if (event.type === "run.started") {
|
|
1534
|
+
const starts = `starts at ${event.entries.join(", ")}`;
|
|
1535
|
+
return [
|
|
1536
|
+
term.rich ? `${chip(term, CHIP_LABELS.run, "accent")} run ${style("bold", event.runId)} of ${event.workflow} ${style("dim", starts)}` : style("dim", `▸ run ${event.runId} of ${event.workflow} (${starts})`),
|
|
1537
|
+
...event.settings?.trigger === void 0 ? [] : [style("dim", ` started by trigger ${event.settings.trigger}; untrusted inputs: ${(event.settings.untrusted ?? []).join(", ") || "none"}`)],
|
|
1538
|
+
...term.rich ? [""] : []
|
|
1539
|
+
];
|
|
1540
|
+
}
|
|
1541
|
+
if (event.type === "node.started") {
|
|
1542
|
+
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
1543
|
+
const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
|
|
1544
|
+
return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
|
|
1545
|
+
}
|
|
1546
|
+
if (event.type === "node.event") {
|
|
1547
|
+
const inner = event.event;
|
|
1548
|
+
if (inner.type === "session.finished") return [];
|
|
1549
|
+
const fork = inner.fork === true ? style("dim", "fork · ") : "";
|
|
1550
|
+
if (inner.type === "session.started") {
|
|
1551
|
+
const why = inner.fork === true ? ", to answer a question" : "";
|
|
1552
|
+
return [`${tag(event.id)} ${fork}${style("dim", `${inner.model}, session ${inner.sessionId}${why}`)}`];
|
|
1553
|
+
}
|
|
1554
|
+
const room = term.width - widthOf(tag(event.id)) - 1 + 2 - (inner.fork === true ? 7 : 0);
|
|
1555
|
+
return under(event.id, formatEvent(event.id, event.event, tools, verbose, view, room).map((line) => `${fork}${line.replace(INDENT, "$1")}`));
|
|
1556
|
+
}
|
|
1557
|
+
if (event.type === "node.finished") return formatFinished(event, tag(event.id), verbose, term, blank(event.id));
|
|
1558
|
+
if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
|
|
1559
|
+
if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
|
|
1560
|
+
if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
|
|
1561
|
+
if (event.type === "watch.changed") return [`${tag(event.id)} ${style("dim", `look ${String(event.looks)}:`)} ${event.said}`];
|
|
1562
|
+
if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
|
|
1563
|
+
if (event.type === "edge.taken") return [];
|
|
1564
|
+
if (event.type === "worktree") {
|
|
1565
|
+
if (event.action !== "removed") view.roots.set(event.id, event.path);
|
|
1566
|
+
const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
|
|
1567
|
+
const what = event.action === "removed" && event.branch !== void 0 ? `worktree of ${event.branch} (branch kept)` : `${event.branch ?? event.path}${detail}`;
|
|
1568
|
+
const line = `${event.action} ${what}`;
|
|
1569
|
+
return [`${tag(event.id)} ${event.action === "conflict" ? style("yellow", `! ${line}`) : style("dim", `⎇ ${line}`)}`];
|
|
1570
|
+
}
|
|
1571
|
+
if (event.type === "decision.opened") {
|
|
1572
|
+
const what = event.kind === "permission" ? "asks to" : "asks:";
|
|
1573
|
+
const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
|
|
1574
|
+
const where = event.owner === void 0 ? `decision ${event.decision}; ${terminal ? "answer here or with ork decide" : "ork decide"}` : `decision ${event.decision}, via ${event.owner}`;
|
|
1575
|
+
return [`${tag(event.node)} ${term.rich ? chip(term, CHIP_LABELS.ask, "accent") : style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
|
|
1576
|
+
}
|
|
1577
|
+
if (event.type === "decision.answered") {
|
|
1578
|
+
const by = event.by === "owner" && event.owner !== void 0 ? event.owner : ANSWERED_BY[event.by];
|
|
1579
|
+
const said = event.note === void 0 ? "" : ` · said: ${event.note.replaceAll(/\s+/gu, " ")}`;
|
|
1580
|
+
return [`${tag(event.node)} ${style("dim", `→ ${event.answer} (${by})${said}`)}`];
|
|
1581
|
+
}
|
|
1582
|
+
if (event.type === "decision.withdrawn") return [`${tag(event.node)} ${style("dim", "→ question withdrawn")}`];
|
|
1583
|
+
if (event.type === "run.command") {
|
|
1584
|
+
const node = event.node === void 0 ? "" : ` --node ${event.node}`;
|
|
1585
|
+
return [style(["yellow", "bold"], `! ork ${event.command}${node} received`)];
|
|
1586
|
+
}
|
|
1587
|
+
if (event.type === "node.held") {
|
|
1588
|
+
const at = event.at === "tool" ? "its tool call waits for ork continue" : "it starts after ork continue";
|
|
1589
|
+
return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
|
|
1590
|
+
}
|
|
1591
|
+
if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
|
|
1592
|
+
if (event.type === "slot.waiting") return [`${tag(event.id)} ${style("yellow", `‖ ${slotWaitLine(event.holders)}`)}`];
|
|
1593
|
+
if (event.type === "slot.taken") return [`${tag(event.id)} ${style("dim", "▸ got a check slot")}`];
|
|
1594
|
+
if (event.type === "each.listed") {
|
|
1595
|
+
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
1596
|
+
return [`${tag(event.node)} ${style("dim", `${items(event.count)}${round}`)}`];
|
|
1597
|
+
}
|
|
1598
|
+
if (event.type === "each.exceeded") {
|
|
1599
|
+
const over = `${items(event.count)}, over maxItems ${String(event.maxItems)}`;
|
|
1600
|
+
return [`${tag(event.node)} ${style("yellow", `! ${over}`)}`];
|
|
1601
|
+
}
|
|
1602
|
+
if (event.type === "run.finished") return formatRunResult(event.result, term);
|
|
1603
|
+
if (event.type === "run.resumed") {
|
|
1604
|
+
const [one] = event.kept;
|
|
1605
|
+
return ["", style("dim", `▸ resumed: ${one === void 0 ? "every node runs again" : event.kept.length === 1 ? `${one} keeps its result` : `${event.kept.join(", ")} keep their results`}`)];
|
|
1606
|
+
}
|
|
1607
|
+
if (event.type === "feedback.recorded") {
|
|
1608
|
+
const { target, kind, title } = event.feedback;
|
|
1609
|
+
const line = style("dim", `✎ feedback (${target}, ${kind}): ${title}`);
|
|
1610
|
+
return [event.node === void 0 ? line : `${tag(event.node)} ${line}`];
|
|
1611
|
+
}
|
|
1612
|
+
if (event.type === "user.message") {
|
|
1613
|
+
const note = event.delivered === "queued" ? " (for its next session)" : "";
|
|
1614
|
+
return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
|
|
1615
|
+
}
|
|
1616
|
+
return under(event.node, formatBusEvent(event, term));
|
|
987
1617
|
};
|
|
988
1618
|
// src/runs/chat.ts
|
|
989
1619
|
/**
|
|
@@ -994,12 +1624,11 @@ const runReadyDriver = async (workflow, options) => {
|
|
|
994
1624
|
* where it ran, as the run's sessions were set up (ADR-065), and the exchange
|
|
995
1625
|
* is added to the run's record.
|
|
996
1626
|
*/
|
|
997
|
-
/**
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
};
|
|
1627
|
+
/**
|
|
1628
|
+
* The declared node a spawned worker or an item of a fanned node is an instance
|
|
1629
|
+
* of.
|
|
1630
|
+
*/
|
|
1631
|
+
const instanceOf = (nodes, id) => id in nodes ? void 0 : templateOf(id, new Set(Object.keys(nodes)));
|
|
1003
1632
|
/** The run's sessions loaded the machine's Claude setup, so this one does. */
|
|
1004
1633
|
const machineClaudeOf = (events) => events.some(({ event }) => event.type === "run.started" && event.settings?.machineClaude === true) ? { machineClaude: true } : {};
|
|
1005
1634
|
/**
|
|
@@ -1020,7 +1649,7 @@ const resumeChat = async (options) => {
|
|
|
1020
1649
|
const notice = chatNotice(store, place, options.home);
|
|
1021
1650
|
if (notice !== null) options.out(notice);
|
|
1022
1651
|
const runDir = runDirOf(place, run.id);
|
|
1023
|
-
const template =
|
|
1652
|
+
const template = instanceOf(workflow.nodes, node);
|
|
1024
1653
|
const plan = planNodeSession(workflow, node, {
|
|
1025
1654
|
cwd,
|
|
1026
1655
|
inputs: {},
|
|
@@ -1035,10 +1664,12 @@ const resumeChat = async (options) => {
|
|
|
1035
1664
|
...template === void 0 ? {} : { template }
|
|
1036
1665
|
});
|
|
1037
1666
|
if (!plan.ok) return plan.error;
|
|
1667
|
+
const servers = namedMcpEntries(run.project, plan.mcp);
|
|
1668
|
+
if (!servers.ok) return servers.error;
|
|
1038
1669
|
let driver = options.driver ?? session.driver ?? plan.driver;
|
|
1039
1670
|
if (driver === void 0) {
|
|
1040
1671
|
const ready = readyDriver(await (options.driverChecks ?? checksHere)());
|
|
1041
|
-
if (!ready.ok) return ["No
|
|
1672
|
+
if (!ready.ok) return ["No agent driver is ready here.", ...ready.hints].join(" ");
|
|
1042
1673
|
driver = ready.driver;
|
|
1043
1674
|
}
|
|
1044
1675
|
const record = appendRecorder(store, run.id, runDir);
|
|
@@ -1048,8 +1679,9 @@ const resumeChat = async (options) => {
|
|
|
1048
1679
|
body: options.body,
|
|
1049
1680
|
delivered: "resumed"
|
|
1050
1681
|
});
|
|
1051
|
-
const started = (await options.runtime(driver)).start({
|
|
1682
|
+
const started = (await options.runtime(driver, placeFiles(place).runs)).start({
|
|
1052
1683
|
...plan.spec,
|
|
1684
|
+
...servers.value,
|
|
1053
1685
|
resume: session.sessionId,
|
|
1054
1686
|
prompt: options.body,
|
|
1055
1687
|
...machineClaudeOf(events)
|
|
@@ -1080,16 +1712,27 @@ const resumeChat = async (options) => {
|
|
|
1080
1712
|
/** Turns stored events into lines, keeping the state a transcript needs. */
|
|
1081
1713
|
const createLogPrinter = (options) => {
|
|
1082
1714
|
const tools = /* @__PURE__ */ new Map();
|
|
1715
|
+
const view = createView();
|
|
1083
1716
|
let width = 8;
|
|
1084
1717
|
return (stored) => {
|
|
1085
1718
|
const { event } = stored;
|
|
1086
1719
|
if (event.type === "run.started") width = Math.max(...event.nodes.map((n) => n.id.length), 1);
|
|
1087
|
-
if (options.node !== void 0 &&
|
|
1720
|
+
if (options.node !== void 0 && !isNodeEvent(event, options.node)) return [];
|
|
1088
1721
|
if (options.json) return [JSON.stringify(stored)];
|
|
1089
|
-
return formatRunEvent(event, width, tools);
|
|
1722
|
+
return formatRunEvent(event, width, tools, false, options.verbose === true || options.node !== void 0, view);
|
|
1090
1723
|
};
|
|
1091
1724
|
};
|
|
1092
1725
|
/**
|
|
1726
|
+
* Why `--node` names nothing in this run, or `null`: no event names it and the
|
|
1727
|
+
* run didn't declare it. A live run may still start it, so that is not
|
|
1728
|
+
* refused.
|
|
1729
|
+
*/
|
|
1730
|
+
const unknownNode = (stored, node, live) => {
|
|
1731
|
+
const named = stored.some(({ event }) => isNodeEvent(event, node));
|
|
1732
|
+
const declared = stored.some(({ event }) => event.type === "run.started" && event.nodes.some((n) => n.id === node));
|
|
1733
|
+
return named || declared || live ? null : `Run has no node ${node}.`;
|
|
1734
|
+
};
|
|
1735
|
+
/**
|
|
1093
1736
|
* Prints a run's events; with `follow`, then each new one as it is stored:
|
|
1094
1737
|
* pushed over the run's socket, or polled from the store when there is none.
|
|
1095
1738
|
* Returns when `run.finished` is stored, the run's process is gone, or `signal`
|
|
@@ -1107,6 +1750,59 @@ const followRun = async (store, run, out, options) => {
|
|
|
1107
1750
|
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
1108
1751
|
}) === "crashed") out(style("red", `run ${run.id} stopped without finishing (its process is gone)`));
|
|
1109
1752
|
};
|
|
1753
|
+
// src/run/node-notes.ts
|
|
1754
|
+
/**
|
|
1755
|
+
* @file What a run of one node (`ork run --node`) leaves out, said before its
|
|
1756
|
+
* session starts: the messages, steps and `forEach` it does not run. A node
|
|
1757
|
+
* with `forEach` (RFC-013) runs once with no item when it has `items`, and is
|
|
1758
|
+
* refused when it has `from`, as the node that reports its list is not run.
|
|
1759
|
+
* And, after it, its run summary: how the node ended
|
|
1760
|
+
* (specs/runtime/session-ends.md).
|
|
1761
|
+
*/
|
|
1762
|
+
/** A warning line, as the run prints it. */
|
|
1763
|
+
const noteLines = (lines) => lines.map((line) => `${style(["yellow", "bold"], "warning")} ${line}`);
|
|
1764
|
+
/** Why a node can't run alone, or what a run of it leaves out. */
|
|
1765
|
+
const oneNodeNotes = (workflow, id) => {
|
|
1766
|
+
const node = workflow.nodes[id];
|
|
1767
|
+
if (node === void 0) return { warnings: [] };
|
|
1768
|
+
const each = "forEach" in node ? node.forEach : void 0;
|
|
1769
|
+
if (each !== void 0 && "from" in each) return {
|
|
1770
|
+
error: `\`${id}\` runs once for each item of ${each.from}, a list another node reports, and a run of one node does not run that node; run the whole workflow, or give it \`items\` of its own`,
|
|
1771
|
+
warnings: []
|
|
1772
|
+
};
|
|
1773
|
+
const steps = [...beforeOf(node, workflow.defaults) === void 0 ? [] : ["before"], ..."verify" in node && node.verify !== void 0 ? ["verify"] : []];
|
|
1774
|
+
return { warnings: [
|
|
1775
|
+
..."notify" in node && node.notify ? [`${id}'s notify: does nothing here: a run of one node has no message bus.`] : [],
|
|
1776
|
+
...each === void 0 ? [] : [`${id}'s forEach: does not fan out in a run of one node: it runs once, with no item.`],
|
|
1777
|
+
...steps.map((step) => `${id}'s ${step}: does not run in a run of one node.`)
|
|
1778
|
+
] };
|
|
1779
|
+
};
|
|
1780
|
+
/** The `node.finished` fields that say how it ended: its error and its end. */
|
|
1781
|
+
const endedFields = (result) => ({
|
|
1782
|
+
...result.error === void 0 ? {} : { error: result.error },
|
|
1783
|
+
end: endOf(result)
|
|
1784
|
+
});
|
|
1785
|
+
/** The one-node run's result, its reason the node's message. */
|
|
1786
|
+
const oneNodeResult = (runId, id, result, ok) => {
|
|
1787
|
+
const costUsd = result.usage?.costUsd ?? 0;
|
|
1788
|
+
return {
|
|
1789
|
+
runId,
|
|
1790
|
+
status: ok ? "succeeded" : "failed",
|
|
1791
|
+
costUsd,
|
|
1792
|
+
nodes: [{
|
|
1793
|
+
id,
|
|
1794
|
+
runs: 1,
|
|
1795
|
+
ok: result.ok,
|
|
1796
|
+
output: result.output,
|
|
1797
|
+
costUsd,
|
|
1798
|
+
...endedFields(result)
|
|
1799
|
+
}],
|
|
1800
|
+
reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
|
|
1801
|
+
worktrees: [],
|
|
1802
|
+
skipped: 0,
|
|
1803
|
+
failuresHandled: 0
|
|
1804
|
+
};
|
|
1805
|
+
};
|
|
1110
1806
|
// src/run/origin.ts
|
|
1111
1807
|
/**
|
|
1112
1808
|
* @file Where a run's feedback comes from, beyond what its events say: Ork's
|
|
@@ -1114,6 +1810,8 @@ const followRun = async (store, run, out, options) => {
|
|
|
1114
1810
|
* content hash.
|
|
1115
1811
|
*/
|
|
1116
1812
|
/** Ork's version and the workflow's hash, for the run's feedback records. */
|
|
1813
|
+
/** The Ork version a run records in `run.started` and `run.resumed`. */
|
|
1814
|
+
const ORK_RUN_VERSION = ORK_VERSION;
|
|
1117
1815
|
const feedbackOrigin = (workflowFile) => {
|
|
1118
1816
|
const origin = { version: ORK_VERSION };
|
|
1119
1817
|
try {
|
|
@@ -1147,6 +1845,8 @@ const runNode = async (options) => {
|
|
|
1147
1845
|
if (!pairs.ok) return failed([`--input: ${pairs.error}`]);
|
|
1148
1846
|
const inputs = resolveInputs(workflow, pairs.raw);
|
|
1149
1847
|
if (!inputs.ok) return failed(inputs.errors);
|
|
1848
|
+
const notes = oneNodeNotes(workflow, options.nodeId);
|
|
1849
|
+
if (notes.error !== void 0) return failed([notes.error]);
|
|
1150
1850
|
const limits = runLimits(options.store, place, options.home);
|
|
1151
1851
|
const refused = limitsRefusal(limits, workflow);
|
|
1152
1852
|
if (refused.length > 0) return failed(refused);
|
|
@@ -1166,6 +1866,8 @@ const runNode = async (options) => {
|
|
|
1166
1866
|
pluginDir: path.join(runDir, "plugins", options.nodeId)
|
|
1167
1867
|
});
|
|
1168
1868
|
if (!plan.ok) return failed([plan.error]);
|
|
1869
|
+
const servers = namedMcpEntries(projectRoot, plan.mcp);
|
|
1870
|
+
if (!servers.ok) return failed([servers.error]);
|
|
1169
1871
|
const named = sessionDriver({
|
|
1170
1872
|
override: options.driver,
|
|
1171
1873
|
named: plan.driver
|
|
@@ -1174,12 +1876,13 @@ const runNode = async (options) => {
|
|
|
1174
1876
|
if (ready?.ok === false) return failed(ready.lines);
|
|
1175
1877
|
if (ready?.line !== void 0) options.err(ready.line);
|
|
1176
1878
|
const driver = named ?? ready?.driver;
|
|
1177
|
-
if (driver === void 0) return failed(["No
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1879
|
+
if (driver === void 0) return failed(["No agent driver is ready here."]);
|
|
1880
|
+
const unfit = unfitLines(workflow, options.drivers, {
|
|
1881
|
+
override: driver,
|
|
1882
|
+
only: options.nodeId
|
|
1883
|
+
});
|
|
1884
|
+
if (unfit.length > 0) return failed(unfit);
|
|
1885
|
+
for (const line of noteLines([...notes.warnings, ...plan.warnings])) options.err(line);
|
|
1183
1886
|
const id = options.nodeId;
|
|
1184
1887
|
const recorder = createRecorder(options.store, {
|
|
1185
1888
|
id: runId,
|
|
@@ -1213,10 +1916,11 @@ const runNode = async (options) => {
|
|
|
1213
1916
|
...options.home === void 0 ? {} : { home: options.home }
|
|
1214
1917
|
});
|
|
1215
1918
|
const signals = createSignals(feedbackOrigin(path.resolve(options.cwd, options.file)));
|
|
1919
|
+
const view = createView();
|
|
1216
1920
|
const record = (raw) => {
|
|
1217
1921
|
for (const event of signals(raw)) {
|
|
1218
1922
|
control.publish(recorder(event));
|
|
1219
|
-
if (event.type === "feedback.recorded" && !options.json) for (const line of formatRunEvent(event, id.length, /* @__PURE__ */ new Map())) options.out(line);
|
|
1923
|
+
if (event.type === "feedback.recorded" && !options.json) for (const line of formatRunEvent(event, id.length, /* @__PURE__ */ new Map(), false, options.verbose, view)) options.out(line);
|
|
1220
1924
|
}
|
|
1221
1925
|
};
|
|
1222
1926
|
const nodeType = workflow.nodes[id]?.type ?? "claude.worker";
|
|
@@ -1237,9 +1941,12 @@ const runNode = async (options) => {
|
|
|
1237
1941
|
nodeType,
|
|
1238
1942
|
round: 1
|
|
1239
1943
|
});
|
|
1240
|
-
const runtime = await options.runtime(driver);
|
|
1944
|
+
const runtime = await options.runtime(driver, placeFiles(place).runs);
|
|
1241
1945
|
if (options.machineClaude === true) plan.spec.machineClaude = true;
|
|
1242
|
-
const session = runtime.start(
|
|
1946
|
+
const session = runtime.start({
|
|
1947
|
+
...plan.spec,
|
|
1948
|
+
...servers.value
|
|
1949
|
+
}, {
|
|
1243
1950
|
human: control.human,
|
|
1244
1951
|
...graph === null ? {} : { feedback: createFeedback({
|
|
1245
1952
|
graph,
|
|
@@ -1258,10 +1965,9 @@ const runNode = async (options) => {
|
|
|
1258
1965
|
});
|
|
1259
1966
|
if (event.type === "session.finished") outcome = event.outcome;
|
|
1260
1967
|
if (options.json) options.out(JSON.stringify(event));
|
|
1261
|
-
else for (const line of formatEvent(options.nodeId, event, tools)) options.out(line);
|
|
1968
|
+
else for (const line of formatEvent(options.nodeId, event, tools, options.verbose, view)) options.out(line);
|
|
1262
1969
|
}
|
|
1263
1970
|
const result = resultOf(outcome);
|
|
1264
|
-
const costUsd = result.usage?.costUsd ?? 0;
|
|
1265
1971
|
record({
|
|
1266
1972
|
type: "node.finished",
|
|
1267
1973
|
id,
|
|
@@ -1269,27 +1975,11 @@ const runNode = async (options) => {
|
|
|
1269
1975
|
ok: result.ok,
|
|
1270
1976
|
output: result.output,
|
|
1271
1977
|
...result.usage === void 0 ? {} : { usage: result.usage },
|
|
1272
|
-
...result
|
|
1978
|
+
...endedFields(result)
|
|
1273
1979
|
});
|
|
1274
1980
|
record({
|
|
1275
1981
|
type: "run.finished",
|
|
1276
|
-
result:
|
|
1277
|
-
runId,
|
|
1278
|
-
status: succeeded(outcome) ? "succeeded" : "failed",
|
|
1279
|
-
costUsd,
|
|
1280
|
-
nodes: [{
|
|
1281
|
-
id,
|
|
1282
|
-
runs: 1,
|
|
1283
|
-
ok: result.ok,
|
|
1284
|
-
output: result.output,
|
|
1285
|
-
costUsd,
|
|
1286
|
-
...result.error === void 0 ? {} : { error: result.error }
|
|
1287
|
-
}],
|
|
1288
|
-
reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
|
|
1289
|
-
worktrees: [],
|
|
1290
|
-
skipped: 0,
|
|
1291
|
-
failuresHandled: 0
|
|
1292
|
-
}
|
|
1982
|
+
result: oneNodeResult(runId, id, result, succeeded(outcome))
|
|
1293
1983
|
});
|
|
1294
1984
|
await control.close();
|
|
1295
1985
|
return {
|
|
@@ -1498,6 +2188,14 @@ const preflight = async (options, given) => {
|
|
|
1498
2188
|
lines: driver.lines
|
|
1499
2189
|
};
|
|
1500
2190
|
if (driver.line !== void 0) options.err(driver.line);
|
|
2191
|
+
const unfit = unfitLines(given.effective, options.drivers, {
|
|
2192
|
+
override: options.driver,
|
|
2193
|
+
ready: driver.driver ?? options.resume?.settings.readyDriver
|
|
2194
|
+
});
|
|
2195
|
+
if (unfit.length > 0) return {
|
|
2196
|
+
ok: false,
|
|
2197
|
+
lines: unfit
|
|
2198
|
+
};
|
|
1501
2199
|
const checkout = await prepareCheckout(workflow, {
|
|
1502
2200
|
...options,
|
|
1503
2201
|
cwd: place.root
|
|
@@ -1569,6 +2267,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1569
2267
|
}, runDir) : appendRecorder(options.store, runId, runDir);
|
|
1570
2268
|
const width = Math.max(...Object.keys(workflow.nodes).map((id) => id.length));
|
|
1571
2269
|
const tools = /* @__PURE__ */ new Map();
|
|
2270
|
+
const view = createView();
|
|
1572
2271
|
const started = Promise.withResolvers();
|
|
1573
2272
|
const live = {};
|
|
1574
2273
|
const control = await startControl({
|
|
@@ -1588,7 +2287,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1588
2287
|
for (const event of signals(raw)) {
|
|
1589
2288
|
control.publish(record(event));
|
|
1590
2289
|
if (options.json) options.out(JSON.stringify(event));
|
|
1591
|
-
else for (const line of formatRunEvent(event, width, tools)) options.out(line);
|
|
2290
|
+
else for (const line of formatRunEvent(event, width, tools, options.human !== void 0, options.verbose, view)) options.out(line);
|
|
1592
2291
|
}
|
|
1593
2292
|
};
|
|
1594
2293
|
const workspace = root === void 0 ? void 0 : createWorkspace({
|
|
@@ -1605,7 +2304,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1605
2304
|
});
|
|
1606
2305
|
const maskedNames = maskedNamesOfRun(workflow, place, projects);
|
|
1607
2306
|
const executorOptions = {
|
|
1608
|
-
runtimeFor: options.runtime,
|
|
2307
|
+
runtimeFor: (driver) => options.runtime(driver, placeFiles(place).runs),
|
|
1609
2308
|
inPlace: options.inPlace,
|
|
1610
2309
|
maskedNames
|
|
1611
2310
|
};
|
|
@@ -1614,10 +2313,11 @@ const runWorkflowFile = async (options) => {
|
|
|
1614
2313
|
if (options.tools !== void 0) executorOptions.tools = options.tools;
|
|
1615
2314
|
const script = process.argv[1];
|
|
1616
2315
|
if (script !== void 0 && onPath(script)) executorOptions.orkBin = script;
|
|
1617
|
-
const replay = options.resume === void 0 ? void 0 : createReplay(options.resume.history, options.resume.cutOff, options.resume.mail, options.resume.earlier);
|
|
2316
|
+
const replay = options.resume === void 0 ? void 0 : createReplay(options.resume.history, options.resume.cutOff, options.resume.mail, options.resume.earlier, options.resume.fanOuts);
|
|
1618
2317
|
if (replay !== void 0) onEvent({
|
|
1619
2318
|
type: "run.resumed",
|
|
1620
|
-
kept: replay.kept()
|
|
2319
|
+
kept: replay.kept(),
|
|
2320
|
+
ork: ORK_RUN_VERSION
|
|
1621
2321
|
});
|
|
1622
2322
|
const ports = callPorts({
|
|
1623
2323
|
...options,
|
|
@@ -1639,6 +2339,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1639
2339
|
executors: builtinExecutors(executorOptions),
|
|
1640
2340
|
human: options.resume === void 0 ? control.human : replayLoopAnswers(control.human, options.resume.loopAnswers),
|
|
1641
2341
|
onEvent,
|
|
2342
|
+
orkVersion: ORK_RUN_VERSION,
|
|
1642
2343
|
settings: {
|
|
1643
2344
|
inPlace: options.inPlace,
|
|
1644
2345
|
...options.driver === void 0 ? {} : { driver: options.driver },
|
|
@@ -1653,6 +2354,10 @@ const runWorkflowFile = async (options) => {
|
|
|
1653
2354
|
...workspace === void 0 ? {} : { workspace },
|
|
1654
2355
|
scrub: runScrubber(maskedNames, process.env),
|
|
1655
2356
|
limits: ready.limits,
|
|
2357
|
+
machine: {
|
|
2358
|
+
rules: machineRulesOf(loadProject(place.root, placeFiles(place).rules).project),
|
|
2359
|
+
ledger: machineSlotLedger(options.home === void 0 ? {} : { home: options.home })
|
|
2360
|
+
},
|
|
1656
2361
|
beforeFinish: takeLastCommands(control, started.promise)
|
|
1657
2362
|
});
|
|
1658
2363
|
live.handle = handle;
|
|
@@ -1683,9 +2388,11 @@ const flagValues = (rawArgs, flag) => {
|
|
|
1683
2388
|
}
|
|
1684
2389
|
return values;
|
|
1685
2390
|
};
|
|
1686
|
-
const isDriver = (value) =>
|
|
2391
|
+
const isDriver = (value) => AGENT_DRIVERS.some((d) => d.id === value);
|
|
2392
|
+
/** Why `--driver` refuses a value: a retired id names its replacement. */
|
|
2393
|
+
const badDriver = (value) => `--${retiredDriverHint(value) ?? `driver must be one of ${AGENT_DRIVERS.map((d) => d.id).join(", ")}, got \`${value}\``}`;
|
|
1687
2394
|
/** Loads the drivers only when a run starts, so other commands stay fast. */
|
|
1688
|
-
const loadRuntime = async (driver) => (await import("./src-
|
|
2395
|
+
const loadRuntime = async (driver, runs) => (await import("./src-BnZgZGrf.mjs")).createRuntime(driver, sessionEnv(process.env), runs);
|
|
1689
2396
|
/**
|
|
1690
2397
|
* Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
|
|
1691
2398
|
* so that the run ends tidily and leaves no result unscrubbed.
|
|
@@ -1703,11 +2410,18 @@ const onInterrupt = (stop) => {
|
|
|
1703
2410
|
process.once("SIGHUP", interrupt);
|
|
1704
2411
|
};
|
|
1705
2412
|
/** The terminal that answers a run's questions; none in a detached run. */
|
|
1706
|
-
const terminalHuman = () =>
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
2413
|
+
const terminalHuman = () => {
|
|
2414
|
+
const detached = process.env[DETACHED_ENV] === "1";
|
|
2415
|
+
if (detached) writeErr(style("dim", "detached run: no terminal asks; answer with ork decide"));
|
|
2416
|
+
return detached ? void 0 : createTerminalHuman({
|
|
2417
|
+
input: process.stdin,
|
|
2418
|
+
output: process.stderr,
|
|
2419
|
+
interactive: process.stdin.isTTY,
|
|
2420
|
+
interrupt: () => {
|
|
2421
|
+
process.kill(process.pid, "SIGINT");
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
2424
|
+
};
|
|
1711
2425
|
const run = defineCommand({
|
|
1712
2426
|
meta: {
|
|
1713
2427
|
name: "run",
|
|
@@ -1729,7 +2443,7 @@ const run = defineCommand({
|
|
|
1729
2443
|
},
|
|
1730
2444
|
driver: {
|
|
1731
2445
|
type: "string",
|
|
1732
|
-
description: "
|
|
2446
|
+
description: "claude-code (default: the driver ready here)"
|
|
1733
2447
|
},
|
|
1734
2448
|
"in-place": {
|
|
1735
2449
|
type: "boolean",
|
|
@@ -1751,6 +2465,11 @@ const run = defineCommand({
|
|
|
1751
2465
|
description: "Print the events as JSON lines",
|
|
1752
2466
|
default: false
|
|
1753
2467
|
},
|
|
2468
|
+
verbose: {
|
|
2469
|
+
type: "boolean",
|
|
2470
|
+
description: "Show each tool call whole, and why each failed node stopped, with its scrubbed details",
|
|
2471
|
+
default: false
|
|
2472
|
+
},
|
|
1754
2473
|
detach: {
|
|
1755
2474
|
type: "boolean",
|
|
1756
2475
|
description: "Run in the background; follow it with ork logs -f, answer with ork decide",
|
|
@@ -1787,7 +2506,7 @@ const run = defineCommand({
|
|
|
1787
2506
|
},
|
|
1788
2507
|
run: async ({ args, rawArgs }) => {
|
|
1789
2508
|
if (args.driver !== void 0 && !isDriver(args.driver)) {
|
|
1790
|
-
writeErr(
|
|
2509
|
+
writeErr(badDriver(args.driver));
|
|
1791
2510
|
process.exitCode = 1;
|
|
1792
2511
|
return;
|
|
1793
2512
|
}
|
|
@@ -1832,6 +2551,7 @@ const run = defineCommand({
|
|
|
1832
2551
|
inPlace: args["in-place"],
|
|
1833
2552
|
...args["machine-claude"] ? { machineClaude: true } : {},
|
|
1834
2553
|
json: args.json,
|
|
2554
|
+
verbose: args.verbose,
|
|
1835
2555
|
runtime: loadRuntime,
|
|
1836
2556
|
store,
|
|
1837
2557
|
...human === void 0 ? {} : { human },
|
|
@@ -1877,22 +2597,25 @@ const run = defineCommand({
|
|
|
1877
2597
|
* each message at its session's next step, and its replies stream here; a
|
|
1878
2598
|
* node of a finished run is answered by resuming its last session.
|
|
1879
2599
|
*/
|
|
1880
|
-
/**
|
|
1881
|
-
|
|
1882
|
-
|
|
2600
|
+
/**
|
|
2601
|
+
* Asks for one line; `null` once the input ends (Ctrl-D), Esc leaves, or
|
|
2602
|
+
* `signal` withdraws it. A live chat prints replies while it waits, which a
|
|
2603
|
+
* redrawn prompt can't share the screen with, so it asks line by line.
|
|
2604
|
+
*/
|
|
2605
|
+
const askLine = async (live = false, signal) => {
|
|
2606
|
+
const asked = await readText({
|
|
1883
2607
|
input: process.stdin,
|
|
1884
|
-
output: process.stdout
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
}
|
|
2608
|
+
output: process.stdout,
|
|
2609
|
+
...live ? { env: {
|
|
2610
|
+
...process.env,
|
|
2611
|
+
ORK_PLAIN: "1"
|
|
2612
|
+
} } : {}
|
|
2613
|
+
}, {
|
|
2614
|
+
message: "you",
|
|
2615
|
+
done: (value) => ` you › ${value ?? ""}`,
|
|
2616
|
+
...signal === void 0 ? {} : { signal }
|
|
2617
|
+
}).catch(() => ({ kind: "cancelled" }));
|
|
2618
|
+
return asked.kind === "text" ? asked.value : null;
|
|
1896
2619
|
};
|
|
1897
2620
|
/** Chats with a node of a live run until the input ends or the run does. */
|
|
1898
2621
|
const chatLive = async (store, run, node, message) => {
|
|
@@ -1914,7 +2637,7 @@ const chatLive = async (store, run, node, message) => {
|
|
|
1914
2637
|
abort.abort();
|
|
1915
2638
|
});
|
|
1916
2639
|
for (;;) {
|
|
1917
|
-
const line = await Promise.race([askLine(), following.then(() => null)]);
|
|
2640
|
+
const line = await Promise.race([askLine(true, abort.signal), following.then(() => null)]);
|
|
1918
2641
|
if (line === null || abort.signal.aborted) break;
|
|
1919
2642
|
if (line.trim() !== "") await tellLiveRun(store, run, node, line);
|
|
1920
2643
|
}
|
|
@@ -1945,7 +2668,7 @@ const chat = defineCommand({
|
|
|
1945
2668
|
},
|
|
1946
2669
|
driver: {
|
|
1947
2670
|
type: "string",
|
|
1948
|
-
description: "For a finished run:
|
|
2671
|
+
description: "For a finished run: claude-code (default: the one it ran with)"
|
|
1949
2672
|
}
|
|
1950
2673
|
},
|
|
1951
2674
|
run: async ({ args }) => {
|
|
@@ -1971,7 +2694,7 @@ const chat = defineCommand({
|
|
|
1971
2694
|
return;
|
|
1972
2695
|
}
|
|
1973
2696
|
if (args.driver !== void 0 && !isDriver(args.driver)) {
|
|
1974
|
-
writeErr(
|
|
2697
|
+
writeErr(`${badDriver(args.driver)}.`);
|
|
1975
2698
|
process.exitCode = 1;
|
|
1976
2699
|
return;
|
|
1977
2700
|
}
|
|
@@ -2010,13 +2733,35 @@ const chat = defineCommand({
|
|
|
2010
2733
|
}
|
|
2011
2734
|
}
|
|
2012
2735
|
});
|
|
2736
|
+
// src/commands/decide-list.ts
|
|
2737
|
+
/**
|
|
2738
|
+
* One line per open decision, for `--list`: on a terminal an ASK chip and the
|
|
2739
|
+
* node's chip with the id and the question after, cut to the screen; plain, the
|
|
2740
|
+
* id, the node and the question.
|
|
2741
|
+
*/
|
|
2742
|
+
const formatDecisions = (decisions, workflow, t = termOf(process.stdout)) => {
|
|
2743
|
+
if (decisions.length === 0) return [workflow === void 0 ? "No open decisions." : `No open decisions in workflow ${workflow}.`];
|
|
2744
|
+
const idWidth = Math.max(...decisions.map((d) => d.id.length));
|
|
2745
|
+
const nodeWidth = Math.max(...decisions.map((d) => plain(d.node).length));
|
|
2746
|
+
return decisions.map((d) => {
|
|
2747
|
+
const node = plain(d.node);
|
|
2748
|
+
const options = `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`;
|
|
2749
|
+
const document = d.document === void 0 ? "" : " +document";
|
|
2750
|
+
const via = d.owner === void 0 ? "" : ` via ${plain(d.owner)}`;
|
|
2751
|
+
if (!t.rich) return `${d.id} ${node}${via} ${plain(d.question)}${document} ${options}`;
|
|
2752
|
+
const lead = `${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, node)}${" ".repeat(nodeWidth - node.length)} ${bold(t, d.id.padEnd(idWidth))} `;
|
|
2753
|
+
const room = Math.max(20, t.width - (7 + nodeWidth + 2 + idWidth + 2));
|
|
2754
|
+
return `${lead}${cut(`${plain(d.question)}${document} ${options}${via}`, room)}`;
|
|
2755
|
+
});
|
|
2756
|
+
};
|
|
2013
2757
|
// src/commands/decide.ts
|
|
2014
2758
|
/**
|
|
2015
2759
|
* @file `ork decide`: answer a live run's open decisions from any terminal (it
|
|
2016
2760
|
* writes to the store, then wakes the run on its socket; specs/messaging.md).
|
|
2017
2761
|
* `--say` gives the answer a note, which goes to the run with it. With `--id`
|
|
2018
2762
|
* and `--choose`, `--note` and `--rating` instead note how the decision's
|
|
2019
|
-
* node went, as the run's feedback (specs/feedback.md).
|
|
2763
|
+
* node went, as the run's feedback (specs/feedback.md). `--document` prints
|
|
2764
|
+
* what a gate gave to read beside its question.
|
|
2020
2765
|
*/
|
|
2021
2766
|
const failed$2 = (message) => {
|
|
2022
2767
|
writeErr(message);
|
|
@@ -2054,8 +2799,6 @@ const saidLine = (store, id) => {
|
|
|
2054
2799
|
const note = store.decision(id)?.note;
|
|
2055
2800
|
return note === void 0 ? "" : style("dim", ` · said: ${note}`);
|
|
2056
2801
|
};
|
|
2057
|
-
/** One line per open decision, for `--list`. */
|
|
2058
|
-
const formatDecisions = (decisions) => decisions.length === 0 ? ["No open decisions."] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)}${d.owner === void 0 ? "" : style("dim", ` via ${d.owner}`)} ${d.question} ${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
|
|
2059
2802
|
const decide = defineCommand({
|
|
2060
2803
|
meta: {
|
|
2061
2804
|
name: "decide",
|
|
@@ -2082,6 +2825,15 @@ const decide = defineCommand({
|
|
|
2082
2825
|
description: "List them as JSON",
|
|
2083
2826
|
default: false
|
|
2084
2827
|
},
|
|
2828
|
+
workflow: {
|
|
2829
|
+
type: "string",
|
|
2830
|
+
description: "Only the decisions of this workflow's runs"
|
|
2831
|
+
},
|
|
2832
|
+
document: {
|
|
2833
|
+
type: "boolean",
|
|
2834
|
+
description: "Print the document of the decision --id names",
|
|
2835
|
+
default: false
|
|
2836
|
+
},
|
|
2085
2837
|
id: {
|
|
2086
2838
|
type: "string",
|
|
2087
2839
|
description: "Answer this decision…"
|
|
@@ -2101,10 +2853,15 @@ const decide = defineCommand({
|
|
|
2101
2853
|
rating: {
|
|
2102
2854
|
type: "string",
|
|
2103
2855
|
description: "…rating it good or bad"
|
|
2856
|
+
},
|
|
2857
|
+
verbose: {
|
|
2858
|
+
type: "boolean",
|
|
2859
|
+
description: "Show each question's whole context, not its first lines",
|
|
2860
|
+
default: false
|
|
2104
2861
|
}
|
|
2105
2862
|
},
|
|
2106
2863
|
run: async ({ args }) => {
|
|
2107
|
-
const refusal = args.id !== void 0 || args.choose !== void 0 || process.stdin.isTTY && !args.list && !args.json ? decideRefusal() : null;
|
|
2864
|
+
const refusal = !args.document && (args.id !== void 0 || args.choose !== void 0 || process.stdin.isTTY && !args.list && !args.json) ? decideRefusal() : null;
|
|
2108
2865
|
if (refusal !== null) {
|
|
2109
2866
|
failed$2(refusal);
|
|
2110
2867
|
return;
|
|
@@ -2113,9 +2870,20 @@ const decide = defineCommand({
|
|
|
2113
2870
|
const human = createTerminalHuman({
|
|
2114
2871
|
input: process.stdin,
|
|
2115
2872
|
output: process.stdout,
|
|
2116
|
-
interactive: process.stdin.isTTY
|
|
2873
|
+
interactive: process.stdin.isTTY,
|
|
2874
|
+
verbose: args.verbose
|
|
2117
2875
|
});
|
|
2118
2876
|
try {
|
|
2877
|
+
if (args.document) {
|
|
2878
|
+
if (args.id === void 0 || args.choose !== void 0) {
|
|
2879
|
+
failed$2("--document goes with --id, without --choose.");
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
const { document } = store.decision(args.id) ?? {};
|
|
2883
|
+
if (document === void 0) failed$2(`${args.id} has no document.`);
|
|
2884
|
+
else write(document);
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2119
2887
|
if (args.id !== void 0 || args.choose !== void 0) {
|
|
2120
2888
|
if (args.id === void 0 || args.choose === void 0) {
|
|
2121
2889
|
failed$2("--id and --choose go together.");
|
|
@@ -2143,10 +2911,11 @@ const decide = defineCommand({
|
|
|
2143
2911
|
return;
|
|
2144
2912
|
}
|
|
2145
2913
|
if (args.say !== void 0) {
|
|
2146
|
-
failed$2("--say goes with --id and --choose; answering here
|
|
2914
|
+
failed$2("--say goes with --id and --choose; answering here, press n to add a note (plain: it asks for one after each choice).");
|
|
2147
2915
|
return;
|
|
2148
2916
|
}
|
|
2149
2917
|
const filter = {};
|
|
2918
|
+
if (args.workflow !== void 0) filter.workflow = args.workflow;
|
|
2150
2919
|
if (args.run !== void 0 && args.run !== "") {
|
|
2151
2920
|
const found = findRun(store, args.run, process.cwd());
|
|
2152
2921
|
if (!found.ok) {
|
|
@@ -2161,11 +2930,11 @@ const decide = defineCommand({
|
|
|
2161
2930
|
return;
|
|
2162
2931
|
}
|
|
2163
2932
|
if (args.list || !process.stdin.isTTY || open.length === 0) {
|
|
2164
|
-
for (const line of formatDecisions(open)) write(line);
|
|
2933
|
+
for (const line of formatDecisions(open, args.workflow)) write(line);
|
|
2165
2934
|
return;
|
|
2166
2935
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2936
|
+
const collapses = canKey(process.stdin, process.stdout);
|
|
2937
|
+
for (const [index, d] of open.entries()) {
|
|
2169
2938
|
const question = {
|
|
2170
2939
|
nodeId: d.node,
|
|
2171
2940
|
question: d.question,
|
|
@@ -2173,13 +2942,21 @@ const decide = defineCommand({
|
|
|
2173
2942
|
};
|
|
2174
2943
|
if (d.recommended !== void 0) question.recommended = d.recommended;
|
|
2175
2944
|
if (d.context !== void 0) question.context = d.context;
|
|
2945
|
+
if (d.document !== void 0) question.document = d.document;
|
|
2176
2946
|
let note;
|
|
2177
2947
|
question.onNote = (given) => {
|
|
2178
2948
|
note = given;
|
|
2179
2949
|
};
|
|
2180
|
-
const choice = await human.
|
|
2950
|
+
const choice = await human.askAt(question, {
|
|
2951
|
+
run: d.runId,
|
|
2952
|
+
decision: d.id,
|
|
2953
|
+
index: index + 1,
|
|
2954
|
+
total: open.length
|
|
2955
|
+
});
|
|
2956
|
+
if (choice === null) continue;
|
|
2181
2957
|
const answered = await answerDecision(store, d.id, choice, void 0, "decide", note);
|
|
2182
|
-
|
|
2958
|
+
if (!answered.ok) write(`${style("yellow", "!")} ${answered.error}\n`);
|
|
2959
|
+
else if (!collapses) write(`${style("green", "✓")} ${d.id}: ${choice}${saidLine(store, d.id)}\n`);
|
|
2183
2960
|
}
|
|
2184
2961
|
} finally {
|
|
2185
2962
|
human?.close();
|
|
@@ -2264,7 +3041,7 @@ const resume = defineCommand({
|
|
|
2264
3041
|
},
|
|
2265
3042
|
driver: {
|
|
2266
3043
|
type: "string",
|
|
2267
|
-
description: "
|
|
3044
|
+
description: "claude-code (default: the one the run used)"
|
|
2268
3045
|
},
|
|
2269
3046
|
"in-place": {
|
|
2270
3047
|
type: "boolean",
|
|
@@ -2288,7 +3065,7 @@ const resume = defineCommand({
|
|
|
2288
3065
|
},
|
|
2289
3066
|
run: async ({ args, rawArgs }) => {
|
|
2290
3067
|
if (args.driver !== void 0 && !isDriver(args.driver)) {
|
|
2291
|
-
failed$1(
|
|
3068
|
+
failed$1(badDriver(args.driver));
|
|
2292
3069
|
return;
|
|
2293
3070
|
}
|
|
2294
3071
|
const store = openStore();
|
|
@@ -2339,6 +3116,76 @@ const resume = defineCommand({
|
|
|
2339
3116
|
}
|
|
2340
3117
|
}
|
|
2341
3118
|
});
|
|
3119
|
+
// src/doctor/format.ts
|
|
3120
|
+
/** Marks are made when used, so `--plain` (set after load) is read. */
|
|
3121
|
+
const MARK = {
|
|
3122
|
+
ok: () => style("green", "✓"),
|
|
3123
|
+
warn: () => style("yellow", "!"),
|
|
3124
|
+
fail: () => style("red", "✗"),
|
|
3125
|
+
skip: () => style("dim", "-")
|
|
3126
|
+
};
|
|
3127
|
+
const CHECK_CHIP = {
|
|
3128
|
+
ok: {
|
|
3129
|
+
label: CHIP_LABELS.ok,
|
|
3130
|
+
role: "ok"
|
|
3131
|
+
},
|
|
3132
|
+
warn: {
|
|
3133
|
+
label: CHIP_LABELS.warn,
|
|
3134
|
+
role: "warn"
|
|
3135
|
+
},
|
|
3136
|
+
fail: {
|
|
3137
|
+
label: CHIP_LABELS.fail,
|
|
3138
|
+
role: "danger"
|
|
3139
|
+
},
|
|
3140
|
+
skip: {
|
|
3141
|
+
label: CHIP_LABELS.skip,
|
|
3142
|
+
role: "muted"
|
|
3143
|
+
}
|
|
3144
|
+
};
|
|
3145
|
+
/**
|
|
3146
|
+
* A check's lines. Rich: a chip (its words padded to line the details up), the
|
|
3147
|
+
* detail wrapped under its column, the hint dimmed and indented; plain: a mark
|
|
3148
|
+
* and the lines as they always were.
|
|
3149
|
+
*/
|
|
3150
|
+
const formatCheck = (result, term = termOf(process.stdout)) => {
|
|
3151
|
+
if (!term.rich) {
|
|
3152
|
+
const line = `${MARK[result.status]()} ${result.name.padEnd(20)} ${result.detail}`;
|
|
3153
|
+
return result.hint === void 0 ? [line] : [line, ` ${style("dim", result.hint)}`];
|
|
3154
|
+
}
|
|
3155
|
+
const { label, role } = CHECK_CHIP[result.status];
|
|
3156
|
+
const lead = `${chip(term, label.padEnd(4), role)} ${result.name.padEnd(20)} `;
|
|
3157
|
+
return [...hang(lead, result.detail, term.width), ...result.hint === void 0 ? [] : wrap(result.hint, term.width - 2).map((line) => ` ${paint(term, "muted", line)}`)];
|
|
3158
|
+
};
|
|
3159
|
+
// src/doctor/lease-checks.ts
|
|
3160
|
+
/**
|
|
3161
|
+
* @file What `ork doctor` says of Command Code leases (RFC-014): it removes the
|
|
3162
|
+
* `ork` MCP entries a crashed session left in its checkout's local scope, and
|
|
3163
|
+
* says where. Never prints a token.
|
|
3164
|
+
*/
|
|
3165
|
+
/**
|
|
3166
|
+
* Removes the `ork` MCP entries a crashed Command Code session left in its
|
|
3167
|
+
* checkout's local scope (RFC-014), from the leases in the project's runs
|
|
3168
|
+
* folder. Says nothing when there were none. `bin` is the `cmd` program.
|
|
3169
|
+
*/
|
|
3170
|
+
const leaseChecks = async (place, env = process.env, bin = "cmd") => {
|
|
3171
|
+
const { cleanStaleLeases, leaseLedger } = await import("./src-BnZgZGrf.mjs");
|
|
3172
|
+
const { cleaned, failed } = await cleanStaleLeases({
|
|
3173
|
+
bin,
|
|
3174
|
+
ledger: leaseLedger(placeFiles(place).runs),
|
|
3175
|
+
env
|
|
3176
|
+
});
|
|
3177
|
+
const name = "command-code";
|
|
3178
|
+
return [...cleaned.length === 0 ? [] : [{
|
|
3179
|
+
name,
|
|
3180
|
+
status: "ok",
|
|
3181
|
+
detail: `removed the \`ork\` MCP entry a stopped session left in ${cleaned.join(", ")}`
|
|
3182
|
+
}], ...failed.length === 0 ? [] : [{
|
|
3183
|
+
name,
|
|
3184
|
+
status: "warn",
|
|
3185
|
+
detail: `could not remove the \`ork\` MCP entry a stopped session left in ${failed.join(", ")}`,
|
|
3186
|
+
hint: "Run `cmd mcp remove --scope local ork` in each of those folders."
|
|
3187
|
+
}]];
|
|
3188
|
+
};
|
|
2342
3189
|
// src/project/ork-skills.ts
|
|
2343
3190
|
/**
|
|
2344
3191
|
* @file The Claude Code plugin's skills (`plugins/ork/skills/`) as text, by
|
|
@@ -2349,8 +3196,8 @@ const resume = defineCommand({
|
|
|
2349
3196
|
const ORK_SKILLS = {
|
|
2350
3197
|
principles: "---\nname: principles\ndescription: Use when deciding how to split work into an Ork workflow - which steps are programs, which are Claude sessions, where a person decides - so it costs less than doing it by hand and can't go wrong quietly.\n---\n\n# How Ork splits work\n\nOrk exists to do long jobs for less than doing them by hand, and safely. Four\nrules decide the shape of a workflow.\n\n## 1. A program for what a program can do\n\nA model turn is for judgment. Anything a program can decide, it should: run the\ntests, read a pull request's state, count what changed, wait for CI.\n\n- **A tool node** (`tool.shell`, `tool.git`, `tool.gh`, `tool.aws`) or an\n **`mcp.call`** does it with no session. With `output: { format: json }`, its\n answer routes the run: `when: \"json.state == 'OPEN'\"`.\n- **`verify:`** on a session runs a check before its report counts, so a session\n can't claim success past failing tests. **`before:`** hands it what a program\n found, so it doesn't spend turns finding it.\n- **`watch:`** waits on CI or a review with a program that looks every few\n seconds; hours of waiting cost nothing.\n- **`notify:`** lets a program tell a running session what it found, with no\n model turn to carry the news.\n\n## 2. Spend where it pays\n\n- A small model and low effort for a narrow task (`model: haiku`,\n `effort: low`); the large ones where thinking pays.\n- `budgetUsd` on each session and on the run (`limits.budgetUsd`). A loop of\n sessions without a budget or `limits.timeout` gets a warning, for good reason.\n- Give a session what it needs in its `prompt` and `before:`, rather than\n letting it search; and `isolation: readonly` for a reviewer that only reads.\n\n## 3. Make a mistake impossible before checking for it\n\n- Declare what a node says: `output.schema` lists on tool JSON and Claude\n reports (`verdict: [approve, escalate]`). Conditions then compare against a\n closed list, and a run fails a value off it rather than matching nothing.\n- A verdict is a declared field, never words to search a summary for.\n- Give each condition its opposite, and each failure a handler (`when: \"!ok\"`).\n- Check every draft with Ork's `check` tool until it's clean.\n\n## 4. A person decides what can't be undone\n\nMerging, deploying, publishing, spending a lot: put a `human.gate` before it,\nwith the safe option `recommended`. For a run nobody watches, give it a\n`timeout` and the safe `default`. You never answer a gate yourself.\n\n## An example\n\nA review that a program checks first, and a person signs off:\n\n```yaml\nschemaVersion: 1\nname: careful-review\ndescription: Lint with a program, review with a small model, and ask before merging.\ninputs:\n pr:\n type: string\n required: true\nlimits:\n budgetUsd: 3\nnodes:\n lint:\n type: tool.shell\n command: pnpm lint\n allow: [pnpm lint]\n review:\n type: claude.worker\n prompt: \"Review pull request {{inputs.pr}}. The linter passed.\"\n model: haiku\n effort: low\n isolation: readonly\n budgetUsd: 1\n output:\n schema:\n verdict: [approve, escalate]\n ask:\n type: human.gate\n question: \"Merge pull request {{inputs.pr}}? The review says {{nodes.review.output.verdict}}.\"\n options: [merge, wait]\n recommended: wait\n merge:\n type: tool.gh\n command: pr merge {{inputs.pr}} --squash\n allow: [pr merge]\nedges:\n - lint -> review\n - review -> ask\n - from: ask\n to: merge\n when: \"choice == 'merge'\"\n```\n\n`lint` costs nothing, `review` is a small model with a cap, and nothing merges\nwithout a person.\n",
|
|
2351
3198
|
run: "---\nname: run\ndescription: Use when starting, following or explaining an Ork run - what it's doing, why it failed, what it cost, and what waits on the person - and when the person asks about their Ork runs.\n---\n\n# Running and following Ork workflows\n\nOrk runs workflows on the person's machine with their accounts, so a run spends\ntheir money and can push, merge or deploy. Start one only when they ask, and\nnever answer a decision for them.\n\n## Starting\n\n1. Find it: Ork's `workflows` tool gives each workflow's name and inputs.\n2. Check it first with `check` (`workflow: <name>`). Don't start one with an\n error.\n3. Start it with `start_run`, giving every required input. Claude Code asks the\n person first, unless they've allowed it. It answers with the run's id; the run goes on in the\n background, whatever happens to this conversation.\n\nWithout Ork's tools: `ork run <workflow> --detach --input name=value`.\n\n## Following\n\n- `run` gives a run's status, what it waits on, its cost so far, and each\n node's state. `runs` lists the newest runs.\n- `run_events` gives its newest events; `node` narrows them, and `last` or\n `after` widens them. A `node.finished` event with `ok: false` says why a node\n failed.\n- `explain_context` says what a node's session was given: its agent,\n instructions, skills and files. Use it when a node didn't know something.\n- `usage` says what runs spent, by workflow and model, and how close the limits\n are.\n\nSay what you found plainly: which node, what it said, and what it cost. Quote a\nnode's own words rather than retelling them.\n\n## What only the person does\n\n- **Decisions.** `decisions` lists the questions waiting on them: a gate, a loop\n that ran out of rounds, a permission. Tell them what each asks, the options,\n and the recommended one, and that they answer in `ork ui` or with\n `ork decide` in their own terminal. Don't answer one: never run\n `ork decide --id … --choose …`; with Ork's plugin, a hook stops it.\n- **Stopping, pausing, resuming.** Tell them the command: `ork stop <run>`,\n `ork pause <run>`, `ork continue <run>`, `ork resume <run>`. A node can be\n stopped alone with `--node <id>`.\n\n## Without Ork's tools\n\n| Want | Command |\n| ---------------------- | ------------------------------ |\n| Runs, newest first | `ork ps` |\n| A run's transcript | `ork logs <run>` (`--follow`) |\n| Its nodes and cost | `ork tree <run>` |\n| What runs spent | `ork usage` |\n| What waits on a person | `ork decide --list` |\n| A node's context | `ork context explain <wf> <n>` |\n",
|
|
2352
|
-
setup: "---\nname: setup\ndescription: Use when the person asks you to set up Ork, install it, or get started with it in a repository - checking it's installed, setting up the project, making Ork's tools available to you, and a first workflow.\n---\n\n# Setting up Ork\n\nOrk runs workflows on the person's machine with their own accounts. Setting it\nup changes their machine and their repository, so say what each step does and\nask before it. Work through these in order, and stop where they say no.\n\n## 1. Check\n\n```sh\nork --version\nork doctor\n```\n\n- **No `ork`:** it needs Node 24.20 or later (`node --version`). The install is\n `npm install --global @krapht/ork`; show it and run it only when they say\n yes. The guide:\n https://github.com/mimshins/ork/blob/main/docs/getting-started/installation.md\n- **Read `ork doctor` to them.** It needs git, a git repository with a commit,\n and one
|
|
2353
|
-
write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## Loops\n\nEvery cycle needs an edge with `maxRounds`. Give the way back a condition and\nthe way out its opposite, as `!ok` and `ok` above; a plain edge beside a loop\nfires every round. When a loop runs out, Ork asks a person what to do.\n\n## What `check` refuses, and why\n\n- **A value the source can't have** (`when.value`): `status == 'failed'` (it's\n `fail`), a gate's `choice == 'aprove'` for `approve`, a value outside a\n declared list. The message names the values it can have.\n- **A loop no condition ends**, a plain edge beside a loop, a loop of sessions\n with no `limits.budgetUsd` or `limits.timeout`: warnings, each worth fixing.\n- **`no way the walk tried reaches x`**: Ork tried every way through, and none\n got to `x`. A join waiting on a branch a first-match router never takes is\n the usual cause.\n"
|
|
3199
|
+
setup: "---\nname: setup\ndescription: Use when the person asks you to set up Ork, install it, or get started with it in a repository - checking it's installed, setting up the project, making Ork's tools available to you, and a first workflow.\n---\n\n# Setting up Ork\n\nOrk runs workflows on the person's machine with their own accounts. Setting it\nup changes their machine and their repository, so say what each step does and\nask before it. Work through these in order, and stop where they say no.\n\n## 1. Check\n\n```sh\nork --version\nork doctor\n```\n\n- **No `ork`:** it needs Node 24.20 or later (`node --version`). The install is\n `npm install --global @krapht/ork`; show it and run it only when they say\n yes. The guide:\n https://github.com/mimshins/ork/blob/main/docs/getting-started/installation.md\n- **Read `ork doctor` to them.** It needs git, a git repository with a commit,\n and one agent driver. Don't run `git init` or a commit without asking.\n- **A driver not ready:** they log in themselves, with `claude auth login`, or\n by setting `ANTHROPIC_API_KEY` or a cloud provider in their own shell (either\n makes the claude-code driver ready). Never ask for a key, never take one pasted to you, and\n never write one into a file or a command. `gh` and `aws` are optional, and\n their logins are their own too (`gh auth login`, `aws sso login`).\n\n## 2. The project\n\n- **`ork init`** keeps Ork's files in the repository, in `.ork/`, committed with\n the code. It adds `.ork/runs/` to `.gitignore`, links `.claude/` and similar\n folders to `.ork/`, and writes a starter workflow, `hello`.\n- **`ork register`** keeps them beside it, in `~/.ork/projects/<id>/`, and\n writes nothing into the repository. For a repository whose process isn't\n theirs to change, or to try Ork first.\n\nExplain both and ask which. Skip the question when the repository decides it: a\n`.ork/` folder means `ork init` has run (`ork projects` lists what's\nregistered). Neither overwrites a file.\n\n## 3. Make Ork available to you\n\nIf Ork's tools (`workflows`, `check`) already answer you, skip this. Otherwise,\nby where you run, and asking before each change to a config:\n\n- **Claude Code:** Ork's plugin brings the skills and the tools. The person\n types these themselves:\n\n ```\n /plugin marketplace add mimshins/ork\n /plugin install ork@ork\n ```\n\n Or, in a project set up with `ork init`, the skills in the repository for\n everyone who clones it, then the tools:\n\n ```sh\n ork init --skills\n claude mcp add ork -- ork mcp\n ```\n\n New tools start when they restart Claude Code or reconnect `ork` in `/mcp`;\n so does the plugin's, once `ork` is installed.\n\n- **Another MCP client:** add a server named `ork` with command `ork` and args\n `[\"mcp\"]` to its MCP config, where its own docs say. Its tools find the\n project by its id, from `ork projects`.\n- **A shell only:** use the CLI. `ork --help` and each command's `--help` say\n what it takes.\n\n## 4. A first workflow\n\nOffer to write one for a task they name. Follow `/ork:write`, check each draft\n(the `check` tool, or `ork validate <file>`) until it has no error, then save\nit. Don't start it: a run spends their money. Start it only when they say so,\nin the background with its inputs (`start_run`, or\n`ork run <name> --detach --input name=value`), and follow it with `/ork:run`.\n\n## 5. Show them `ork ui`\n\nTell them to run `ork ui` in their own terminal: their runs, graphs and logs in\nthe browser, and where they answer what a run asks.\n\n## Never\n\n- Ask for, store or pass on a password, key or token.\n- Answer a run's decision: they answer in `ork ui` or with `ork decide` in\n their own terminal.\n- Install, edit a config, or start a run without a yes.\n",
|
|
3200
|
+
write: "---\nname: write\ndescription: Use when writing or changing an Ork workflow (a `.ork/workflows/*.ork.yaml` file) - its nodes, edges, conditions and loops - and checking each draft until Ork accepts it.\n---\n\n# Writing an Ork workflow\n\nAn Ork workflow is one YAML file in the project, `.ork/workflows/<name>.ork.yaml`.\nIt names the nodes (Claude sessions, programs, a person's decisions) and the\nedges that carry work between them. Ork runs it on the person's machine with\ntheir own accounts.\n\n## How to work\n\n1. **Look first.** Ork's `workflows` tool lists the project's workflows; read\n one that's close. `agents` lists the agents a node can use. Before you split\n the work, read `/ork:principles`: a program where a program will do, a\n session only for judgment.\n2. **Don't guess a field.** `workflow_format` gives the format; ask it for one\n node type (`nodeType: \"human.gate\"`) to see that type's fields.\n3. **Check every draft** with Ork's `check` tool (`text`, or `workflow` for a\n file), and fix what it says until it has no error. Read its warnings: they\n mean something. Without Ork's tools, run `ork validate <file>`.\n4. **Then save the file**, beginning with the schema line below, so editors can\n check it too.\n\n## The shape of a file\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/mimshins/ork/main/packages/schema/workflow.schema.json\nschemaVersion: 1\nname: fix-and-ship\ndescription: Fix a failing test, check it with the test suite, and ask before opening a pull request.\ninputs:\n issue:\n type: string\n required: true\n description: What fails.\ndefaults:\n model: sonnet\n budgetUsd: 2\nlimits:\n budgetUsd: 10\nnodes:\n fix:\n type: claude.worker\n prompt: \"Fix this, and keep the change small: {{inputs.issue}}\"\n tests:\n type: tool.shell\n command: pnpm test\n allow: [pnpm test]\n ask:\n type: human.gate\n question: \"Open a pull request for {{nodes.fix.output.branch}}?\"\n options: [open, hold]\n recommended: hold\n pr:\n type: tool.gh\n command: pr create --fill --head {{nodes.fix.output.branch}}\n allow: [pr create]\nedges:\n - fix -> tests\n - from: tests\n to: fix\n when: \"!ok\"\n maxRounds: 2\n - from: tests\n to: ask\n when: ok\n - from: ask\n to: pr\n when: \"choice == 'open'\"\n```\n\nNode ids are lowercase: letters, digits, `-` and `_`. Secrets never go in the\nfile: a tool reads one with `env:` and `{{env.NAME}}`.\n\n## Nodes\n\n| Type | Does |\n| --------------------------------- | ------------------------------------------------------------------- |\n| `claude.worker` | A Claude session that does one task and reports |\n| `claude.leader` | A session that plans and delegates to the nodes it owns (`parent:`) |\n| `claude.subagent` | A subagent inside its parent's session; it takes no edges |\n| `tool.shell` | Any program, run without a shell; `allow:` lists command prefixes |\n| `tool.git`, `tool.gh`, `tool.aws` | The person's own `git`, `gh`, `aws`, with an allow list |\n| `mcp.call` | One tool call on an MCP server, with no session |\n| `human.gate` | A question with options and a recommended one |\n| `control.join` | Waits for its inputs: `all` (default), `any`, or `n` of them |\n| `control.router`, `control.split` | Pass their input on; a router takes its first matching edge |\n| `workflow.call` | Starts another workflow as its own run |\n\nA worker, tool node, `mcp.call` or `workflow.call` can run once per entry of a\nlist: see [One node per item](#one-node-per-item).\n\n- **A Claude node reports** `status` (`pass`, `changes_requested`, `done` or\n `fail`), `summary`, `artifacts`, `branch`, and any field its `output.schema`\n declares, such as `verdict: [approve, escalate]`. For another verdict, declare\n a field: `status` can't take other values.\n- **A tool node reports** `exitCode`, `stdout`, `stderr`, and with\n `output: { format: json, schema: { state: [OPEN, CLOSED] } }` its JSON as\n `json`. Declare the fields you read: a run then checks them, and a typo can't\n pass.\n- **A gate reports** `choice` and `answeredBy`. A `timeout` needs a `default`.\n\n## Edges and conditions\n\n- `a -> b`, `a -> b, c`, `b, c -> j` are plain edges, taken when the source\n succeeded. A failed node takes no plain edge: handle a failure with\n `when: \"!ok\"`, or `status == 'fail'` on a Claude node.\n- `{ from: a, to: b, when: \"...\" }` is taken when the condition holds. A bare\n name reads the source's output: `when: \"verdict == 'approve'\"`. Quote any\n condition that starts with `!`, or YAML reads it as a tag.\n- Conditions have `==`, `!=`, `<`, `>`, `&&`, `||`, `!`, and `any`, `all`,\n `count`, `len`, `contains`. A join's `statuses` suits `any(statuses, 'fail')`.\n- A node runs when any edge into it is taken; only a join waits. A node with no\n way in taken is skipped, and so is what follows it.\n- Templates, `{{inputs.x}}`, `{{nodes.a.output.field}}`, `{{run.id}}`, go in\n `prompt`, `instructions`, `command`, `cwd` and `question`. Each value stays\n one argument of a command, so it can't inject one.\n\n## One node per item\n\n`forEach` runs a worker, a tool node, an `mcp.call` or a `workflow.call` once\nper entry of a list, with no model turn to fan out or gather. Not a leader, a\ngate, a subagent or a control node.\n\n```yaml\nplan:\n type: claude.worker\n prompt: \"Split this into at most 6 independent tasks: {{inputs.goal}}\"\n isolation: readonly\n output:\n schema:\n tasks:\n list: { title: string, detail: string } # declare the list and its shape\nbuild:\n type: claude.worker\n prompt: \"Task {{index}}: {{item.title}}. {{item.detail}}\"\n isolation: worktree\n forEach:\n from: \"{{nodes.plan.output.tasks}}\" # exactly one path to a declared list\n maxItems: 6 # required with `from`: the cap on sessions started\n maxParallel: 3\n```\n\n- `forEach: { items: [a, b] }` takes a list written in the file instead.\n- `{{item}}` and `{{index}}` work only in a node with `forEach`, in `prompt`,\n `command`, `arguments`, `inputs`, `env` and `ref`. A program also gets\n `ORK_ITEM` and `ORK_INDEX`.\n- The next node reads `items`, `results` (each item's output, `summary`,\n `branch`), `statuses` and `count`: `{{nodes.build.output.results}}`,\n `when: \"all(statuses, 'pass')\"`. Items follow no edges; the node does.\n- `onFailure: finish` (default) runs every item; `stop` starts no more after a\n failure. A list over `maxItems` makes Ork ask a person.\n- Declare the source's `list`, or `check` warns `forEach.undeclared`. Fix\n `forEach.not-list`, `forEach.order` and `item.field` errors; they name the\n field or node.\n\n## Loops\n\nEvery cycle needs an edge with `maxRounds`. Give the way back a condition and\nthe way out its opposite, as `!ok` and `ok` above; a plain edge beside a loop\nfires every round. When a loop runs out, Ork asks a person what to do.\n\n## What `check` refuses, and why\n\n- **A value the source can't have** (`when.value`): `status == 'failed'` (it's\n `fail`), a gate's `choice == 'aprove'` for `approve`, a value outside a\n declared list. The message names the values it can have.\n- **A loop no condition ends**, a plain edge beside a loop, a loop of sessions\n with no `limits.budgetUsd` or `limits.timeout`: warnings, each worth fixing.\n- **`no way the walk tried reaches x`**: Ork tried every way through, and none\n got to `x`. A join waiting on a branch a first-match router never takes is\n the usual cause.\n"
|
|
2354
3201
|
};
|
|
2355
3202
|
// src/project/init-skills.ts
|
|
2356
3203
|
/**
|
|
@@ -2450,16 +3297,17 @@ const DESCRIBE = {
|
|
|
2450
3297
|
folder: (link) => `${link.path} is a real folder, left as it is`
|
|
2451
3298
|
};
|
|
2452
3299
|
const describe = (link) => DESCRIBE[link.state](link);
|
|
3300
|
+
/** Labels are made when used, so `--plain` (set after load) is read. */
|
|
2453
3301
|
const LABEL = {
|
|
2454
|
-
linked: style("dim", "ok "),
|
|
2455
|
-
missing: style("yellow", "missing"),
|
|
2456
|
-
broken: style("red", "broken "),
|
|
2457
|
-
elsewhere: style("yellow", "kept "),
|
|
2458
|
-
folder: style("yellow", "kept ")
|
|
3302
|
+
linked: () => style("dim", "ok "),
|
|
3303
|
+
missing: () => style("yellow", "missing"),
|
|
3304
|
+
broken: () => style("red", "broken "),
|
|
3305
|
+
elsewhere: () => style("yellow", "kept "),
|
|
3306
|
+
folder: () => style("yellow", "kept ")
|
|
2459
3307
|
};
|
|
2460
3308
|
/** One line per folder; `created` marks the links this call made. */
|
|
2461
3309
|
const formatLinks = (links, created = []) => links.map((link) => {
|
|
2462
|
-
return `${created.includes(link.path) ? style("green", "linked ") : LABEL[link.state]} ${describe(link)}`;
|
|
3310
|
+
return `${created.includes(link.path) ? style("green", "linked ") : LABEL[link.state]()} ${describe(link)}`;
|
|
2463
3311
|
});
|
|
2464
3312
|
const checkOf = (tool, links) => {
|
|
2465
3313
|
const name = `${AGENT_TOOL_LAYOUTS.find((l) => l.id === tool)?.name ?? tool} links`;
|
|
@@ -2498,7 +3346,7 @@ const projectChecks = (root) => [...linkChecks(root), ...skillChecks(root, ORK_V
|
|
|
2498
3346
|
* @file This installation of Ork: its name and version for the root command;
|
|
2499
3347
|
* `ork doctor`, which prints Ork's version against the last update check and
|
|
2500
3348
|
* the environment checks (Node, git, the user's own claude, gh and aws
|
|
2501
|
-
* logins, the project's links) and fails when no
|
|
3349
|
+
* logins, the project's links) and fails when no agent driver can run; and
|
|
2502
3350
|
* `ork upgrade`, which asks the npm registry for the latest version and, when
|
|
2503
3351
|
* it is newer, upgrades Ork the way it was installed after the user agrees
|
|
2504
3352
|
* (`--yes` skips the question; `--check` only reports). A clone or `npx` gets
|
|
@@ -2511,16 +3359,6 @@ const ORK_META = {
|
|
|
2511
3359
|
version: ORK_VERSION,
|
|
2512
3360
|
description: "Orchestrate Claude agents on your machine"
|
|
2513
3361
|
};
|
|
2514
|
-
const MARK = {
|
|
2515
|
-
ok: style("green", "✓"),
|
|
2516
|
-
warn: style("yellow", "!"),
|
|
2517
|
-
fail: style("red", "✗"),
|
|
2518
|
-
skip: style("dim", "-")
|
|
2519
|
-
};
|
|
2520
|
-
const formatCheck = (result) => {
|
|
2521
|
-
const line = `${MARK[result.status]} ${result.name.padEnd(20)} ${result.detail}`;
|
|
2522
|
-
return result.hint === void 0 ? [line] : [line, ` ${style("dim", result.hint)}`];
|
|
2523
|
-
};
|
|
2524
3362
|
const doctor = defineCommand({
|
|
2525
3363
|
meta: {
|
|
2526
3364
|
name: "doctor",
|
|
@@ -2529,17 +3367,21 @@ const doctor = defineCommand({
|
|
|
2529
3367
|
run: async () => {
|
|
2530
3368
|
const { results, ok } = await runChecks(exec, process.env, process.versions.node);
|
|
2531
3369
|
const place = projectAt(process.cwd());
|
|
3370
|
+
const experiments = experimentsCheck(place);
|
|
2532
3371
|
for (const result of [
|
|
2533
3372
|
orkCheck(),
|
|
2534
3373
|
...results,
|
|
2535
3374
|
...place.mode === "attached" ? [] : projectChecks(place.root),
|
|
2536
3375
|
socketCheck(),
|
|
2537
|
-
...triggerChecks()
|
|
3376
|
+
...triggerChecks(),
|
|
3377
|
+
...await leaseChecks(place),
|
|
3378
|
+
experiments
|
|
2538
3379
|
]) for (const line of formatCheck(result)) write(line);
|
|
2539
3380
|
if (!ok) {
|
|
2540
|
-
write(`\n${style("red", "Not ready:")} Ork needs Node, git, and at least one
|
|
3381
|
+
write(`\n${style("red", "Not ready:")} Ork needs Node, git, and at least one agent driver.`);
|
|
2541
3382
|
process.exitCode = 1;
|
|
2542
3383
|
}
|
|
3384
|
+
if (experiments.status === "fail") process.exitCode = 1;
|
|
2543
3385
|
}
|
|
2544
3386
|
});
|
|
2545
3387
|
/** The ids of runs still running anywhere on this machine. */
|
|
@@ -2574,7 +3416,7 @@ const agreed = (yes, command) => {
|
|
|
2574
3416
|
write(`Run it yourself: ${command}\nor run ork upgrade --yes.`);
|
|
2575
3417
|
return Promise.resolve(false);
|
|
2576
3418
|
}
|
|
2577
|
-
return confirm(`Run ${
|
|
3419
|
+
return confirm(`Run ${command}?`);
|
|
2578
3420
|
};
|
|
2579
3421
|
const upgrade = defineCommand({
|
|
2580
3422
|
meta: {
|
|
@@ -2955,10 +3797,25 @@ const relocateProject = (store, id, target) => {
|
|
|
2955
3797
|
created: false
|
|
2956
3798
|
};
|
|
2957
3799
|
};
|
|
2958
|
-
/**
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
3800
|
+
/**
|
|
3801
|
+
* One project's lines for `ork projects`; rich, its mode and a missing clone
|
|
3802
|
+
* are chips, its id is padded to `idWidth` and its root is cut to the screen.
|
|
3803
|
+
*/
|
|
3804
|
+
const formatProject = (project, term = termOf(process.stdout), idWidth = 0) => {
|
|
3805
|
+
const missing = !existsSync(project.root);
|
|
3806
|
+
if (!term.rich) {
|
|
3807
|
+
const gone = missing ? style("yellow", " (missing)") : "";
|
|
3808
|
+
return [`${style("bold", project.id)} ${style("dim", project.mode)} ${project.root}${gone}`, style("dim", ` files: ${shownPath(project.dir, project.root)}`)];
|
|
3809
|
+
}
|
|
3810
|
+
const mode = chip(term, project.mode.toUpperCase(), "muted");
|
|
3811
|
+
const gone = missing ? ` ${chip(term, "MISSING", "warn")}` : "";
|
|
3812
|
+
const room = term.width - (idWidth + 2 + project.mode.length + 2 + 2) - (missing ? 10 : 0);
|
|
3813
|
+
return [`${style("bold", project.id.padEnd(idWidth))} ${mode} ${cut(project.root, Math.max(8, room))}${gone}`, style("dim", cut(` files: ${shownPath(project.dir, project.root)}`, term.width))];
|
|
3814
|
+
};
|
|
3815
|
+
/** Every project's lines, their ids aligned when rich. */
|
|
3816
|
+
const formatProjects = (projects, term = termOf(process.stdout)) => {
|
|
3817
|
+
const idWidth = term.rich ? Math.max(0, ...projects.map((p) => p.id.length)) : 0;
|
|
3818
|
+
return projects.flatMap((p) => formatProject(p, term, idWidth));
|
|
2962
3819
|
};
|
|
2963
3820
|
/** What `ork register` prints: the project, and where its files go. */
|
|
2964
3821
|
const formatRegistered = (project, created) => {
|
|
@@ -3034,7 +3891,7 @@ const driverLine = (checks, projectDefault) => {
|
|
|
3034
3891
|
const why = ready.ready === 1 ? ready.detail : `more than one is ready, and ${ready.driver} comes first`;
|
|
3035
3892
|
return `Runs use the driver that's ready when they start: ${ready.driver} here (${why}).`;
|
|
3036
3893
|
}
|
|
3037
|
-
return `Runs use the driver that's ready when they start, and none is ready here yet: ${checks.find(({ id }) => id === "
|
|
3894
|
+
return `Runs use the driver that's ready when they start, and none is ready here yet: ${checks.find(({ id }) => id === "claude-code")?.result.hint ?? "see `ork doctor`."}`;
|
|
3038
3895
|
};
|
|
3039
3896
|
/**
|
|
3040
3897
|
* The line for the project at `root`: `ork doctor`'s driver checks through
|
|
@@ -3187,7 +4044,7 @@ const migrateHere = async (cwd, options) => {
|
|
|
3187
4044
|
fail("Pass --yes to migrate without a terminal.");
|
|
3188
4045
|
return;
|
|
3189
4046
|
}
|
|
3190
|
-
if (!options.yes && !await confirm("Go ahead?
|
|
4047
|
+
if (!options.yes && !await confirm("Go ahead?")) return;
|
|
3191
4048
|
applyMigration(plan, store);
|
|
3192
4049
|
write(`${style("green", "✓")} migrated. ${afterMigration(plan)}`);
|
|
3193
4050
|
} finally {
|
|
@@ -3307,7 +4164,7 @@ const projects = defineCommand({
|
|
|
3307
4164
|
const all = withStore$1((store) => store.projects());
|
|
3308
4165
|
if (args.json) write(JSON.stringify(all, void 0, 2));
|
|
3309
4166
|
else if (all.length === 0) write("No projects registered yet: ork init (in-repo) or ork register (attached).");
|
|
3310
|
-
else for (const
|
|
4167
|
+
else for (const line of formatProjects(all)) write(line);
|
|
3311
4168
|
}
|
|
3312
4169
|
});
|
|
3313
4170
|
const migrate = defineCommand({
|
|
@@ -3401,13 +4258,16 @@ const SEVERITY_STYLE = {
|
|
|
3401
4258
|
};
|
|
3402
4259
|
const about = (item) => `${item.target}/${item.kind}`;
|
|
3403
4260
|
/** One line per item, most recently seen first. */
|
|
3404
|
-
const formatFeedbackItems = (items, now = Date.now()) => {
|
|
4261
|
+
const formatFeedbackItems = (items, now = Date.now(), term = termOf(process.stdout)) => {
|
|
3405
4262
|
if (items.length === 0) return ["No feedback yet."];
|
|
3406
4263
|
const width = Math.max(...items.map((i) => about(i).length));
|
|
3407
4264
|
return items.map((item) => {
|
|
3408
|
-
const
|
|
4265
|
+
const mark = item.status === "new" ? "" : term.rich ? ` ${chip(term, item.status.toUpperCase(), item.status === "dismissed" ? "muted" : "accent")}` : ` ${style("dim", `[${item.status}]`)}`;
|
|
3409
4266
|
const times = `${String(item.count)}×`.padStart(4);
|
|
3410
|
-
|
|
4267
|
+
const when = ` ${style("dim", `· ${ago(item.lastSeen, now)}`)}`;
|
|
4268
|
+
const room = term.width - (item.id.length + 1 + times.length + 1 + width + 2) - widthOf(mark) - widthOf(when);
|
|
4269
|
+
const title = term.rich ? cut(item.title, Math.max(8, room)) : item.title;
|
|
4270
|
+
return `${style("dim", item.id)} ${times} ${style(SEVERITY_STYLE[item.severity], about(item).padEnd(width))} ${title}${mark}${when}`;
|
|
3411
4271
|
});
|
|
3412
4272
|
};
|
|
3413
4273
|
const plural = (n, word) => `${String(n)} ${word}${n === 1 ? "" : "s"}`;
|
|
@@ -3635,7 +4495,7 @@ const apply = defineCommand({
|
|
|
3635
4495
|
item: itemArg$1,
|
|
3636
4496
|
driver: {
|
|
3637
4497
|
type: "string",
|
|
3638
|
-
description: "
|
|
4498
|
+
description: "claude-code (default: the driver ready here)"
|
|
3639
4499
|
},
|
|
3640
4500
|
"run-id": {
|
|
3641
4501
|
type: "string",
|
|
@@ -3644,7 +4504,7 @@ const apply = defineCommand({
|
|
|
3644
4504
|
},
|
|
3645
4505
|
run: async ({ args }) => {
|
|
3646
4506
|
if (args.driver !== void 0 && !isDriver(args.driver)) {
|
|
3647
|
-
writeErr(
|
|
4507
|
+
writeErr(badDriver(args.driver));
|
|
3648
4508
|
process.exitCode = 1;
|
|
3649
4509
|
return;
|
|
3650
4510
|
}
|
|
@@ -4174,7 +5034,7 @@ const ps = defineCommand({
|
|
|
4174
5034
|
limit: Number.isInteger(limit) && limit > 0 ? limit : 20,
|
|
4175
5035
|
...args.all ? {} : { project: placeFor(process.cwd(), store.projects()).root }
|
|
4176
5036
|
});
|
|
4177
|
-
const statusOf = (run) =>
|
|
5037
|
+
const statusOf = (run) => listedStatus(run, run.status === "running" ? store.events(run.id).map((s) => s.event) : []);
|
|
4178
5038
|
if (args.json) write(JSON.stringify(runs, void 0, 2));
|
|
4179
5039
|
else for (const line of formatRuns(runs, args.all, Date.now(), statusOf)) write(line);
|
|
4180
5040
|
})
|
|
@@ -4205,6 +5065,11 @@ const logs = defineCommand({
|
|
|
4205
5065
|
type: "boolean",
|
|
4206
5066
|
description: "Print the stored events as JSON lines",
|
|
4207
5067
|
default: false
|
|
5068
|
+
},
|
|
5069
|
+
verbose: {
|
|
5070
|
+
type: "boolean",
|
|
5071
|
+
description: "Show each tool call whole, and why each failed node stopped, with its scrubbed details (always on with --node)",
|
|
5072
|
+
default: false
|
|
4208
5073
|
}
|
|
4209
5074
|
},
|
|
4210
5075
|
run: ({ args }) => withStore(async (store) => {
|
|
@@ -4214,6 +5079,12 @@ const logs = defineCommand({
|
|
|
4214
5079
|
process.exitCode = 1;
|
|
4215
5080
|
return;
|
|
4216
5081
|
}
|
|
5082
|
+
const problem = args.node === void 0 ? null : unknownNode(store.events(found.run.id), args.node, args.follow && found.run.status === "running");
|
|
5083
|
+
if (problem !== null) {
|
|
5084
|
+
writeErr(problem);
|
|
5085
|
+
process.exitCode = 1;
|
|
5086
|
+
return;
|
|
5087
|
+
}
|
|
4217
5088
|
const abort = new AbortController();
|
|
4218
5089
|
process.once("SIGINT", () => {
|
|
4219
5090
|
abort.abort();
|
|
@@ -4221,6 +5092,7 @@ const logs = defineCommand({
|
|
|
4221
5092
|
await followRun(store, found.run, write, {
|
|
4222
5093
|
follow: args.follow,
|
|
4223
5094
|
json: args.json,
|
|
5095
|
+
verbose: args.verbose,
|
|
4224
5096
|
signal: abort.signal,
|
|
4225
5097
|
...args.node === void 0 ? {} : { node: args.node }
|
|
4226
5098
|
});
|
|
@@ -4247,8 +5119,9 @@ const tree = defineCommand({
|
|
|
4247
5119
|
return;
|
|
4248
5120
|
}
|
|
4249
5121
|
const { run } = found;
|
|
4250
|
-
const
|
|
4251
|
-
const
|
|
5122
|
+
const stored = store.events(run.id);
|
|
5123
|
+
const events = stored.map((s) => s.event);
|
|
5124
|
+
const roots = nestItems(activeTree(stored));
|
|
4252
5125
|
if (args.json) {
|
|
4253
5126
|
write(JSON.stringify(roots, void 0, 2));
|
|
4254
5127
|
return;
|
|
@@ -4306,7 +5179,7 @@ const addAgent = (options) => {
|
|
|
4306
5179
|
* doesn't, and otherwise it asks on a terminal, once, saying the plugin has
|
|
4307
5180
|
* them too, or says how to add them later.
|
|
4308
5181
|
*/
|
|
4309
|
-
const QUESTION = "Add Ork's skills for Claude Code to .ork/skills/, so it writes workflows Ork accepts? Ork's Claude Code plugin has them too.
|
|
5182
|
+
const QUESTION = "Add Ork's skills for Claude Code to .ork/skills/, so it writes workflows Ork accepts? Ork's Claude Code plugin has them too.";
|
|
4310
5183
|
const HINT = "Ork's skills for Claude Code: add them here with `ork init --skills`, or install Ork's plugin in Claude Code (/plugin marketplace add mimshins/ork, then /plugin install ork@ork).";
|
|
4311
5184
|
/** Whether to write the skills: as asked, or the person's answer. */
|
|
4312
5185
|
const wanted = (root, asked) => {
|
|
@@ -4377,6 +5250,8 @@ const init = defineCommand({
|
|
|
4377
5250
|
skills: args.skills,
|
|
4378
5251
|
link: args.link
|
|
4379
5252
|
});
|
|
5253
|
+
const noCommit = await headWarning(exec);
|
|
5254
|
+
if (noCommit !== null) write(style("yellow", `\n! git: ${noCommit}`));
|
|
4380
5255
|
write(`\nNext: edit ${WORKFLOW_DIR}/, then run ${style("bold", "ork validate")}.`);
|
|
4381
5256
|
}
|
|
4382
5257
|
});
|
|
@@ -4575,27 +5450,27 @@ const main = defineCommand({
|
|
|
4575
5450
|
ps,
|
|
4576
5451
|
logs,
|
|
4577
5452
|
tree,
|
|
4578
|
-
usage: () => import("./usage-
|
|
4579
|
-
prune: () => import("./usage-
|
|
5453
|
+
usage: () => import("./usage-GceBGMjU.mjs").then((module) => module.usage),
|
|
5454
|
+
prune: () => import("./usage-GceBGMjU.mjs").then((module) => module.prune),
|
|
4580
5455
|
decide,
|
|
4581
|
-
relay: () => import("./relay-
|
|
4582
|
-
pause: () => import("./pause-
|
|
4583
|
-
continue: () => import("./pause-
|
|
5456
|
+
relay: () => import("./relay-DNx7be9k.mjs").then((module) => module.relay),
|
|
5457
|
+
pause: () => import("./pause-D6PuSlNl.mjs").then((module) => module.pause),
|
|
5458
|
+
continue: () => import("./pause-D6PuSlNl.mjs").then((module) => module.continueRun),
|
|
4584
5459
|
stop,
|
|
4585
5460
|
chat,
|
|
4586
|
-
send: () => import("./send-
|
|
5461
|
+
send: () => import("./send-CLs_fyg7.mjs").then((module) => module.send),
|
|
4587
5462
|
resume,
|
|
4588
5463
|
feedback,
|
|
4589
5464
|
context,
|
|
4590
|
-
worktrees: () => import("./worktrees-
|
|
4591
|
-
wt: () => import("./worktrees-
|
|
4592
|
-
watch: () => import("./watch-
|
|
4593
|
-
mcp: () => import("./mcp-
|
|
4594
|
-
ui: () => import("./ui-
|
|
5465
|
+
worktrees: () => import("./worktrees-Be51E2rB.mjs").then((module) => module.worktrees),
|
|
5466
|
+
wt: () => import("./worktrees-Be51E2rB.mjs").then((module) => module.worktrees),
|
|
5467
|
+
watch: () => import("./watch-BmoybMQg.mjs").then((module) => module.watch),
|
|
5468
|
+
mcp: () => import("./mcp-DYgVA9f5.mjs").then((module) => module.mcp),
|
|
5469
|
+
ui: () => import("./ui-CIci4UBW.mjs").then((module) => module.ui),
|
|
4595
5470
|
doctor,
|
|
4596
5471
|
upgrade,
|
|
4597
|
-
completion: () => import("./completion-
|
|
4598
|
-
__complete: () => import("./completion-
|
|
5472
|
+
completion: () => import("./completion-C5D0sEs5.mjs").then((module) => module.completion),
|
|
5473
|
+
__complete: () => import("./completion-C5D0sEs5.mjs").then((module) => module.completeCommand(main))
|
|
4599
5474
|
}
|
|
4600
5475
|
});
|
|
4601
5476
|
// src/bin.ts
|
|
@@ -4605,15 +5480,16 @@ const main = defineCommand({
|
|
|
4605
5480
|
* starts the daily background check and ends with the notice (ADR-038); the
|
|
4606
5481
|
* background copy it starts only runs the check. It first clears git's local
|
|
4607
5482
|
* variables (a hook's `GIT_DIR`, `GIT_INDEX_FILE`, …), so nothing Ork starts
|
|
4608
|
-
* acts on the hook's repository (specs/worktrees.md). `--
|
|
4609
|
-
*
|
|
4610
|
-
*
|
|
4611
|
-
*
|
|
5483
|
+
* acts on the hook's repository (specs/worktrees.md). The global `--plain`
|
|
5484
|
+
* flag is taken out of the arguments and set as `ORK_PLAIN=1`. `--help`
|
|
5485
|
+
* leaves out the flags only Ork passes to itself. `ork __complete`, which tab
|
|
5486
|
+
* completion runs on every Tab, goes first: no update check, and no citty, so
|
|
5487
|
+
* a typed `--help` is a word to complete.
|
|
4612
5488
|
*/
|
|
4613
5489
|
clearGitLocalEnv();
|
|
4614
|
-
const argv = process.argv.slice(2);
|
|
5490
|
+
const argv = takePlainFlag(process.argv.slice(2));
|
|
4615
5491
|
if (argv[0] === "__complete") {
|
|
4616
|
-
const { printCompletions } = await import("./completion-
|
|
5492
|
+
const { printCompletions } = await import("./completion-C5D0sEs5.mjs");
|
|
4617
5493
|
await printCompletions(main, argv.slice(1));
|
|
4618
5494
|
} else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
|
|
4619
5495
|
else {
|
|
@@ -4623,6 +5499,9 @@ else {
|
|
|
4623
5499
|
showUpdateNotice();
|
|
4624
5500
|
});
|
|
4625
5501
|
}
|
|
4626
|
-
await runMain(main, {
|
|
5502
|
+
await runMain(main, {
|
|
5503
|
+
rawArgs: argv,
|
|
5504
|
+
showUsage: showHelp
|
|
5505
|
+
});
|
|
4627
5506
|
}
|
|
4628
5507
|
export {};
|