@krapht/ork 1.0.0-alpha.10 → 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/dist/{__build_info__-DFF18K8y.mjs → __build_info__-CIchF6nr.mjs} +3 -72
- package/dist/bin.mjs +1066 -387
- package/dist/chips-BA7SrrH5.mjs +43 -0
- package/dist/{clean-DVaEH9e7.mjs → clean-Di-KT7Mf.mjs} +1 -1
- package/dist/{completion-Dv5kf598.mjs → completion-C5D0sEs5.mjs} +22 -15
- package/dist/{context-CEaKOVmG.mjs → context-BaNshWr9.mjs} +43 -31
- package/dist/{decide-3l9q8UtX.mjs → decide-B6PnKeqo.mjs} +1 -1
- package/dist/{find-DEjjOlTG.mjs → find-BicBbGMg.mjs} +1 -1
- package/dist/{format-Cb5_m-Qd.mjs → format-lWhN-jYR.mjs} +130 -35
- package/dist/{in-a-run-BS9Nc-JW.mjs → in-a-run-CgkVouyG.mjs} +1 -1
- package/dist/{limits-BSwEjQST.mjs → limits-jZQp9ZJC.mjs} +1 -1
- package/dist/{list-Qxr6rwsY.mjs → list-CEaJ07jd.mjs} +1 -1
- package/dist/{mcp-CslnG5wf.mjs → mcp-DYgVA9f5.mjs} +6 -6
- package/dist/output-CM95njYt.mjs +421 -0
- package/dist/{pause-D7OVB5Ac.mjs → pause-D6PuSlNl.mjs} +4 -4
- package/dist/{relay-EceRIebO.mjs → relay-DNx7be9k.mjs} +2 -2
- package/dist/{remove-0IQZVWmI.mjs → remove-CpMb4NDl.mjs} +3 -3
- package/dist/{report-DK4LGqwF.mjs → report-CEN8VoUb.mjs} +31 -20
- package/dist/{send-C0uwfUYi.mjs → send-CLs_fyg7.mjs} +3 -3
- package/dist/{src-cmmXKHn1.mjs → src-9iGkjnsy.mjs} +234 -150
- package/dist/{src-i5k2Y-kf.mjs → src-B6KvTm7B.mjs} +78 -11
- package/dist/{src-oaJYIddp.mjs → src-BnZgZGrf.mjs} +523 -81
- package/dist/{src-Df6KOIDC.mjs → src-D6_xjm-x.mjs} +1722 -825
- package/dist/ui/assets/{app-qs8Lofv6.js → app-CxURph7a.js} +16 -16
- package/dist/ui/assets/app-QoHVi-mb.css +1 -0
- package/dist/ui/assets/{index-DV1ZaOk-.js → index-By_8KKG6.js} +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/{ui-CBkHJdqk.mjs → ui-CIci4UBW.mjs} +4 -4
- package/dist/{usage-Cd8pEYyK.mjs → usage-GceBGMjU.mjs} +43 -32
- package/dist/{watch-DW5R2OPO.mjs → watch-BmoybMQg.mjs} +28 -16
- package/dist/{workflows-BT1gDRIV.mjs → workflows-DZvW3i1e.mjs} +3 -3
- package/dist/{worktrees-g1MQMGie.mjs → worktrees-Be51E2rB.mjs} +11 -10
- package/package.json +1 -1
- package/dist/output-Bqljtmp_.mjs +0 -40
- package/dist/ui/assets/app-C8uhHVYp.css +0 -1
package/dist/bin.mjs
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { $ as
|
|
3
|
-
import { C as
|
|
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 { n as listOrkWorktrees, t as createdWorktrees } from "./list-
|
|
7
|
-
import { a as planClean, t as applyClean } from "./clean-
|
|
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";
|
|
8
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";
|
|
9
|
-
import { a as write, i as style, n as cost, o as writeErr, t as confirm } from "./output-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { a as
|
|
14
|
-
import { i 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
23
|
import { setTimeout } from "node:timers/promises";
|
|
23
24
|
import { defineCommand, renderUsage, runMain } from "citty";
|
|
@@ -306,165 +307,6 @@ const startControl = async (options) => {
|
|
|
306
307
|
}
|
|
307
308
|
};
|
|
308
309
|
};
|
|
309
|
-
// ../core/src/feedback/signals.ts
|
|
310
|
-
const ORK_TOOL = "mcp__ork__";
|
|
311
|
-
/**
|
|
312
|
-
* An observer for one run's events: give it each event, and record what it
|
|
313
|
-
* returns, in order (the event, its origin filled in when it is feedback, then
|
|
314
|
-
* any feedback it signals).
|
|
315
|
-
*/
|
|
316
|
-
const createSignals = (options = {}) => {
|
|
317
|
-
let workflow;
|
|
318
|
-
const nodeTypes = /* @__PURE__ */ new Map();
|
|
319
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
320
|
-
const tools = /* @__PURE__ */ new Map();
|
|
321
|
-
const asked = /* @__PURE__ */ new Map();
|
|
322
|
-
const seen = /* @__PURE__ */ new Set();
|
|
323
|
-
const originOf = (node) => {
|
|
324
|
-
const origin = {};
|
|
325
|
-
if (workflow !== void 0) origin.workflow = workflow;
|
|
326
|
-
if (options.workflowHash !== void 0) origin.workflowHash = options.workflowHash;
|
|
327
|
-
const type = node === void 0 ? void 0 : nodeTypes.get(node);
|
|
328
|
-
if (type !== void 0) origin.nodeType = type;
|
|
329
|
-
const session = node === void 0 ? void 0 : sessions.get(node);
|
|
330
|
-
if (session !== void 0) origin.model = session.model;
|
|
331
|
-
if (session?.driver !== void 0) origin.driver = session.driver;
|
|
332
|
-
if (options.version !== void 0) origin.version = options.version;
|
|
333
|
-
return origin;
|
|
334
|
-
};
|
|
335
|
-
const record = (node, signal) => {
|
|
336
|
-
const print = fingerprint(signal);
|
|
337
|
-
if (seen.has(print)) return [];
|
|
338
|
-
seen.add(print);
|
|
339
|
-
return [{
|
|
340
|
-
type: "feedback.recorded",
|
|
341
|
-
...node === void 0 ? {} : { node },
|
|
342
|
-
feedback: {
|
|
343
|
-
source: "system",
|
|
344
|
-
kind: signal.kind,
|
|
345
|
-
target: signal.target,
|
|
346
|
-
severity: signal.severity,
|
|
347
|
-
title: signal.title,
|
|
348
|
-
body: signal.body,
|
|
349
|
-
evidence: signal.evidence ?? [],
|
|
350
|
-
code: signal.code,
|
|
351
|
-
fingerprint: print,
|
|
352
|
-
origin: originOf(node)
|
|
353
|
-
}
|
|
354
|
-
}];
|
|
355
|
-
};
|
|
356
|
-
const answered = (question, answer) => {
|
|
357
|
-
if (question.kind === "permission") {
|
|
358
|
-
if (answer !== "deny") return null;
|
|
359
|
-
const tool = question.question.replace(/^Allow /u, "").replace(/\?$/u, "");
|
|
360
|
-
return [question.node, {
|
|
361
|
-
code: "permission.denied",
|
|
362
|
-
kind: "friction",
|
|
363
|
-
target: "workflow",
|
|
364
|
-
severity: "low",
|
|
365
|
-
title: `${question.node} was denied ${tool}`,
|
|
366
|
-
body: "Allow it in the node's `tools.allow` if it needs it, or tell it in its prompt not to use it.",
|
|
367
|
-
evidence: [tool]
|
|
368
|
-
}];
|
|
369
|
-
}
|
|
370
|
-
if (question.recommended === void 0 || answer === question.recommended) return null;
|
|
371
|
-
return [question.node, {
|
|
372
|
-
code: "recommendation.overridden",
|
|
373
|
-
kind: "context-gap",
|
|
374
|
-
target: "workflow",
|
|
375
|
-
severity: "low",
|
|
376
|
-
title: `The answer to ${question.node}'s question was not the one it recommended`,
|
|
377
|
-
body: `Asked: ${question.question} Recommended: ${question.recommended}. Answered: ${answer}.`
|
|
378
|
-
}];
|
|
379
|
-
};
|
|
380
|
-
/** The feedback an event signals, if any, and the node it is about. */
|
|
381
|
-
const signalOf = (event) => {
|
|
382
|
-
if (event.type === "loop.exhausted") return [event.from, {
|
|
383
|
-
code: "loop.exhausted",
|
|
384
|
-
kind: "workflow",
|
|
385
|
-
target: "workflow",
|
|
386
|
-
severity: "medium",
|
|
387
|
-
title: `The loop ${event.from} -> ${event.to} ran out of its ${String(event.maxRounds)} rounds`,
|
|
388
|
-
body: "Raise `maxRounds`, or make what the loop's nodes are asked to fix clearer so it settles sooner."
|
|
389
|
-
}];
|
|
390
|
-
if (event.type === "run.finished" && event.result.status === "budget_exhausted") return [void 0, {
|
|
391
|
-
code: "budget.exhausted",
|
|
392
|
-
kind: "workflow",
|
|
393
|
-
target: "workflow",
|
|
394
|
-
severity: "medium",
|
|
395
|
-
title: "The run's budget ran out",
|
|
396
|
-
body: `The run stopped after spending $${event.result.costUsd.toFixed(4)}. Raise \`limits.budgetUsd\`, or give nodes cheaper models or smaller budgets.`
|
|
397
|
-
}];
|
|
398
|
-
if (event.type === "decision.answered") {
|
|
399
|
-
const question = asked.get(event.decision);
|
|
400
|
-
return question === void 0 ? null : answered(question, event.answer);
|
|
401
|
-
}
|
|
402
|
-
if (event.type === "node.event" && event.event.type === "tool.result") {
|
|
403
|
-
const name = tools.get(`${event.id}\0${event.event.id}`);
|
|
404
|
-
if (!event.event.isError || name?.startsWith(ORK_TOOL) !== true) return null;
|
|
405
|
-
const tool = name.slice(10);
|
|
406
|
-
return [event.id, {
|
|
407
|
-
code: "tool.error",
|
|
408
|
-
kind: "bug",
|
|
409
|
-
target: "ork",
|
|
410
|
-
severity: "medium",
|
|
411
|
-
title: `Ork's ${tool} tool returned an error`,
|
|
412
|
-
body: `A call to \`${tool}\` failed; the node's events show the call.`,
|
|
413
|
-
evidence: [name]
|
|
414
|
-
}];
|
|
415
|
-
}
|
|
416
|
-
if (event.type === "node.finished" && nodeTypes.get(event.id) === "human.gate" && event.output["answeredBy"] === "timeout") return [event.id, {
|
|
417
|
-
code: "gate.timeout",
|
|
418
|
-
kind: "workflow",
|
|
419
|
-
target: "workflow",
|
|
420
|
-
severity: "low",
|
|
421
|
-
title: `The gate ${event.id} timed out and took its default`,
|
|
422
|
-
body: "No one answered in time. Lengthen its `timeout`, or keep it if the default was right."
|
|
423
|
-
}];
|
|
424
|
-
return null;
|
|
425
|
-
};
|
|
426
|
-
/** Keeps what later events and origins need. */
|
|
427
|
-
const observe = (event) => {
|
|
428
|
-
if (event.type === "run.started") workflow = event.workflow;
|
|
429
|
-
if (event.type === "node.started") nodeTypes.set(event.id, event.nodeType);
|
|
430
|
-
if (event.type === "decision.opened") {
|
|
431
|
-
const question = {
|
|
432
|
-
node: event.node,
|
|
433
|
-
kind: event.kind,
|
|
434
|
-
question: event.question
|
|
435
|
-
};
|
|
436
|
-
if (event.recommended !== void 0) question.recommended = event.recommended;
|
|
437
|
-
asked.set(event.decision, question);
|
|
438
|
-
}
|
|
439
|
-
if (event.type !== "node.event") return;
|
|
440
|
-
const inner = event.event;
|
|
441
|
-
if (inner.type === "session.started" && inner.fork !== true) sessions.set(event.id, {
|
|
442
|
-
model: inner.model,
|
|
443
|
-
...inner.driver === void 0 ? {} : { driver: inner.driver }
|
|
444
|
-
});
|
|
445
|
-
if (inner.type === "tool.called") tools.set(`${event.id}\0${inner.id}`, inner.name);
|
|
446
|
-
};
|
|
447
|
-
return (event) => {
|
|
448
|
-
observe(event);
|
|
449
|
-
if (event.type === "feedback.recorded") {
|
|
450
|
-
if (seen.has(event.feedback.fingerprint)) return [];
|
|
451
|
-
seen.add(event.feedback.fingerprint);
|
|
452
|
-
const origin = {
|
|
453
|
-
...originOf(event.node),
|
|
454
|
-
...event.feedback.origin
|
|
455
|
-
};
|
|
456
|
-
return [{
|
|
457
|
-
...event,
|
|
458
|
-
feedback: {
|
|
459
|
-
...event.feedback,
|
|
460
|
-
origin
|
|
461
|
-
}
|
|
462
|
-
}];
|
|
463
|
-
}
|
|
464
|
-
const signal = signalOf(event);
|
|
465
|
-
return signal === null ? [event] : [event, ...record(...signal)];
|
|
466
|
-
};
|
|
467
|
-
};
|
|
468
310
|
/** A title's words, as fingerprints compare them. */
|
|
469
311
|
const titleWords = (title) => new Set(normalizeTitle(title).split(" ").filter((word) => word !== ""));
|
|
470
312
|
/** The share of two titles' words they have in common (Jaccard). */
|
|
@@ -506,6 +348,140 @@ const proposeMerges = (items) => {
|
|
|
506
348
|
};
|
|
507
349
|
}).filter((merge) => merge !== void 0).toSorted((a, b) => firstSeen(a.into, b.into));
|
|
508
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
|
+
});
|
|
509
485
|
// ../core/src/remote/relay.ts
|
|
510
486
|
/**
|
|
511
487
|
* @file A run's side of the relay (RFC-003): its nodes' links, read from the
|
|
@@ -716,6 +692,11 @@ const createRecorder = (store, run, runDir) => {
|
|
|
716
692
|
// src/help-root.ts
|
|
717
693
|
/** Where the user docs start. */
|
|
718
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
|
+
}];
|
|
719
700
|
const rowOf = (entry) => {
|
|
720
701
|
const short = groupOf(entry.name)?.short ?? entry.description;
|
|
721
702
|
const alias = entry.aliases.length === 0 ? "" : ` (alias: ${entry.aliases.join(", ")})`;
|
|
@@ -743,6 +724,9 @@ const renderRootHelp = async (cmd) => {
|
|
|
743
724
|
...groups.flatMap((group) => groupLines(group, width)),
|
|
744
725
|
`Usage: ${name} <command> [flags]`,
|
|
745
726
|
"",
|
|
727
|
+
"Flags for every command:",
|
|
728
|
+
...GLOBAL_FLAGS.map((f) => ` ${f.name} ${f.description}`),
|
|
729
|
+
"",
|
|
746
730
|
`Use "${name} <command> --help" for more about a command.`,
|
|
747
731
|
`${name} v${meta.version ?? ""}`
|
|
748
732
|
].join("\n");
|
|
@@ -766,27 +750,408 @@ const renderHelp = async (cmd, parent) => {
|
|
|
766
750
|
if (cmd.subCommands !== void 0 && parent === void 0) return renderRootHelp(shown);
|
|
767
751
|
const above = parent?.meta === void 0 ? void 0 : { meta: parent.meta };
|
|
768
752
|
const usage = await renderUsage(shown, above);
|
|
769
|
-
|
|
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}`;
|
|
770
755
|
const choices = await choicesOf(shown);
|
|
771
|
-
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}`;
|
|
772
757
|
};
|
|
773
758
|
/** Prints {@link renderHelp}'s usage, as citty prints its own. */
|
|
774
759
|
const showHelp = async (cmd, parent) => {
|
|
775
760
|
process.stdout.write(`${await renderHelp(cmd, parent)}\n\n`);
|
|
776
761
|
};
|
|
762
|
+
// src/human-card.ts
|
|
763
|
+
/** Lines of context a card shows before `d` (or `--verbose`) shows the rest. */
|
|
764
|
+
const CONTEXT_LINES = 6;
|
|
765
|
+
/** Text from outside with its escapes gone and its line breaks kept. */
|
|
766
|
+
const clean = (text) => text.split("\n").map((line) => plain(line)).join("\n");
|
|
767
|
+
/** The card above the options, in the rich look or the plain lines. */
|
|
768
|
+
const cardLines = (t, q, place, verbose, document) => {
|
|
769
|
+
const context = q.context === void 0 ? void 0 : clean(q.context);
|
|
770
|
+
if (!t.rich) return {
|
|
771
|
+
lines: [
|
|
772
|
+
...place === void 0 ? [] : [`run ${place.run} · decision ${place.decision}`],
|
|
773
|
+
...document === void 0 ? [] : [document],
|
|
774
|
+
`? ${plain(q.nodeId)} asks: ${clean(q.question)}`,
|
|
775
|
+
...context === void 0 ? [] : [` ${context.replaceAll("\n", "\n ")}`]
|
|
776
|
+
],
|
|
777
|
+
moreContext: false
|
|
778
|
+
};
|
|
779
|
+
const shown = context === void 0 ? [] : wrap(context, t.width - 3);
|
|
780
|
+
const cutShort = !verbose && shown.length > CONTEXT_LINES;
|
|
781
|
+
const body = cutShort ? shown.slice(0, CONTEXT_LINES) : shown;
|
|
782
|
+
const where = place === void 0 ? "" : ` ${paint(t, "muted", `run ${place.run} · ${String(place.index)} of ${String(place.total)}`)}`;
|
|
783
|
+
return {
|
|
784
|
+
lines: [
|
|
785
|
+
"",
|
|
786
|
+
` ${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, plain(q.nodeId))}${where}`,
|
|
787
|
+
...wrap(clean(q.question), t.width - 1).map((l) => ` ${bold(t, l)}`),
|
|
788
|
+
...body.map((l) => paint(t, "muted", ` ${l}`)),
|
|
789
|
+
...cutShort ? [paint(t, "muted", ` … ${String(shown.length - CONTEXT_LINES)} more lines (d)`)] : []
|
|
790
|
+
],
|
|
791
|
+
moreContext: cutShort
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
/** The line above a permission prompt: who wants to run what. */
|
|
795
|
+
const permitLine = (t, nodeId, preview) => {
|
|
796
|
+
const node = plain(nodeId);
|
|
797
|
+
const what = plain(preview);
|
|
798
|
+
if (!t.rich) return `? ${node} wants to run ${what}`;
|
|
799
|
+
const room = Math.max(20, t.width - node.length - 24);
|
|
800
|
+
return ` ${chip(t, CHIP_LABELS.ask, "accent")} ${nodeChip(t, node)} wants to run ${cut(what, room)}`;
|
|
801
|
+
};
|
|
802
|
+
/**
|
|
803
|
+
* The line a card collapses to once answered: `✓ 509da722-1 · add-invalid-node`
|
|
804
|
+
* (the decision, or the node when there is none, and the choice), or a note
|
|
805
|
+
* that it was left for later.
|
|
806
|
+
*/
|
|
807
|
+
const doneLine = (t, place, nodeId, answer) => {
|
|
808
|
+
const who = place?.decision ?? plain(nodeId);
|
|
809
|
+
if (answer.kind === "later") return ` ${paint(t, "muted", "·")} ${cut(`${who} left for later`, t.width - 3)}`;
|
|
810
|
+
const said = answer.note === void 0 ? "" : ` · said: ${plain(answer.note)}`;
|
|
811
|
+
return ` ${paint(t, "ok", "✓")} ${cut(`${who} · ${answer.id}${said}`, t.width - 3)}`;
|
|
812
|
+
};
|
|
813
|
+
// src/term/text.ts
|
|
814
|
+
/**
|
|
815
|
+
* @file The text prompt: one line of text, edited in place on a terminal
|
|
816
|
+
* (arrows, Home/End, Backspace, Delete, Ctrl-U to clear), collapsing to one
|
|
817
|
+
* line when answered; Enter sends, Esc (or Ctrl-D on an empty line) leaves
|
|
818
|
+
* it. Plain mode asks `message: ` and reads a line, as the prompts always
|
|
819
|
+
* did. `editLine` is the editing itself, which the select prompt reuses for
|
|
820
|
+
* its note.
|
|
821
|
+
*/
|
|
822
|
+
const at = (next, to) => ({
|
|
823
|
+
value: next.join(""),
|
|
824
|
+
pos: to
|
|
825
|
+
});
|
|
826
|
+
/** `state` after `key`; keys that do not edit leave it as it is. */
|
|
827
|
+
const editLine = (state, key) => {
|
|
828
|
+
const { value, pos } = state;
|
|
829
|
+
const chars = Array.from(value);
|
|
830
|
+
if (key.name === "char") return at(chars.toSpliced(pos, 0, key.text), pos + 1);
|
|
831
|
+
if (key.name === "backspace") return pos === 0 ? state : at(chars.toSpliced(pos - 1, 1), pos - 1);
|
|
832
|
+
if (key.name === "delete") return at(chars.toSpliced(pos, 1), pos);
|
|
833
|
+
if (key.name === "left") return {
|
|
834
|
+
value,
|
|
835
|
+
pos: Math.max(0, pos - 1)
|
|
836
|
+
};
|
|
837
|
+
if (key.name === "right") return {
|
|
838
|
+
value,
|
|
839
|
+
pos: Math.min(chars.length, pos + 1)
|
|
840
|
+
};
|
|
841
|
+
if (key.name === "home" || key.name === "ctrl-a") return {
|
|
842
|
+
value,
|
|
843
|
+
pos: 0
|
|
844
|
+
};
|
|
845
|
+
if (key.name === "end" || key.name === "ctrl-e") return {
|
|
846
|
+
value,
|
|
847
|
+
pos: chars.length
|
|
848
|
+
};
|
|
849
|
+
if (key.name === "ctrl-u") return at(chars.slice(pos), 0);
|
|
850
|
+
return state;
|
|
851
|
+
};
|
|
852
|
+
/**
|
|
853
|
+
* The line to draw for `state` after `lead`, within `width`, and the column of
|
|
854
|
+
* the cursor in it. A value too long for the room scrolls to keep the cursor in
|
|
855
|
+
* view.
|
|
856
|
+
*/
|
|
857
|
+
const showLine = (lead, state, width, leadWidth) => {
|
|
858
|
+
const chars = Array.from(state.value);
|
|
859
|
+
const room = Math.max(1, width - leadWidth - 1);
|
|
860
|
+
const start = Math.max(0, state.pos - room + 1);
|
|
861
|
+
return {
|
|
862
|
+
line: `${lead}${chars.slice(start, start + room).join("")}`,
|
|
863
|
+
column: leadWidth + state.pos - start
|
|
864
|
+
};
|
|
865
|
+
};
|
|
866
|
+
const defaultDone$1 = (io, message) => (value) => {
|
|
867
|
+
const term = termOf(io.output, io.env);
|
|
868
|
+
const mark = paint(term, "ok", "✓");
|
|
869
|
+
return cut(` ${mark} ${message} · ${value === void 0 || value === "" ? "skipped" : value}`, term.width);
|
|
870
|
+
};
|
|
871
|
+
/** Asks for one line of text. */
|
|
872
|
+
const readText = async (io, options) => {
|
|
873
|
+
const { message, signal } = options;
|
|
874
|
+
if (signal?.aborted === true) throw withdrawn(signal);
|
|
875
|
+
if (!canKey(io.input, io.output, io.env)) {
|
|
876
|
+
const answer = await readLine(io, ` ${message}: `, signal);
|
|
877
|
+
return answer === void 0 ? { kind: "cancelled" } : {
|
|
878
|
+
kind: "text",
|
|
879
|
+
value: answer.trim()
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const done = options.done ?? defaultDone$1(io, message);
|
|
883
|
+
const term = () => termOf(io.output, io.env);
|
|
884
|
+
const region = createRegion(io.output, term);
|
|
885
|
+
const initial = options.initial ?? "";
|
|
886
|
+
return new Promise((resolve, reject) => {
|
|
887
|
+
let state = {
|
|
888
|
+
value: initial,
|
|
889
|
+
pos: Array.from(initial).length
|
|
890
|
+
};
|
|
891
|
+
const lead = ` ${paint(term(), "accent", "?")} ${bold(term(), message)} › `;
|
|
892
|
+
const leadWidth = Array.from(` ? ${message} › `).length;
|
|
893
|
+
const render = () => {
|
|
894
|
+
const { line, column } = showLine(lead, state, term().width, leadWidth);
|
|
895
|
+
region.draw([line], column);
|
|
896
|
+
};
|
|
897
|
+
const finish = (result, line) => {
|
|
898
|
+
stop();
|
|
899
|
+
io.output.off("resize", render);
|
|
900
|
+
signal?.removeEventListener("abort", onAbort);
|
|
901
|
+
region.finish(line);
|
|
902
|
+
resolve(result);
|
|
903
|
+
};
|
|
904
|
+
const onAbort = () => {
|
|
905
|
+
stop();
|
|
906
|
+
io.output.off("resize", render);
|
|
907
|
+
region.finish();
|
|
908
|
+
reject(withdrawn(signal));
|
|
909
|
+
};
|
|
910
|
+
const stop = readKeys(io.input, (key) => {
|
|
911
|
+
if (key.name === "enter") {
|
|
912
|
+
const value = state.value.trim();
|
|
913
|
+
finish({
|
|
914
|
+
kind: "text",
|
|
915
|
+
value
|
|
916
|
+
}, done(value));
|
|
917
|
+
} else if (key.name === "escape" || key.name === "ctrl-d" && state.value === "") finish({ kind: "cancelled" }, done());
|
|
918
|
+
else if (key.name === "ctrl-c") {
|
|
919
|
+
finish({ kind: "interrupted" });
|
|
920
|
+
interrupt(io);
|
|
921
|
+
} else {
|
|
922
|
+
state = editLine(state, key);
|
|
923
|
+
render();
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
io.output.on("resize", render);
|
|
927
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
928
|
+
render();
|
|
929
|
+
});
|
|
930
|
+
};
|
|
931
|
+
// src/term/select.ts
|
|
932
|
+
/**
|
|
933
|
+
* @file The select prompt: a list of options moved with ↑/↓ (or `j`/`k`),
|
|
934
|
+
* Home/End and `1`–`9`, chosen with Enter. `n` adds a note (one line, edited
|
|
935
|
+
* in place), `d` shows or hides the details (a document), `q` or Esc leaves
|
|
936
|
+
* the question for later, Ctrl-C stops. It redraws only its own lines,
|
|
937
|
+
* follows a resize, and collapses to one line once answered. Plain mode
|
|
938
|
+
* prints the options as `1) id label - description` and asks `Choose [n]: `,
|
|
939
|
+
* as `ork decide` always did; there a note is asked for after the choice when
|
|
940
|
+
* the prompt takes one.
|
|
941
|
+
*/
|
|
942
|
+
const NOTE_PROMPT = "Note for the run (optional, Enter to skip)";
|
|
943
|
+
/** The option an answer names: its number, or its id; empty is `recommended`. */
|
|
944
|
+
const pickOption = (answer, options, recommended) => {
|
|
945
|
+
const value = answer.trim();
|
|
946
|
+
if (value === "") return recommended;
|
|
947
|
+
const index = Number(value);
|
|
948
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
|
|
949
|
+
return options.find((o) => o.id === value)?.id;
|
|
950
|
+
};
|
|
951
|
+
const label = (option) => option.label === void 0 ? option.id : `${option.id} ${option.label}`;
|
|
952
|
+
const defaultDone = (options) => (result, term) => {
|
|
953
|
+
if (result.kind === "later") return ` ${paint(term, "muted", "·")} left for later`;
|
|
954
|
+
const option = options.find((o) => o.id === result.id);
|
|
955
|
+
return cut(` ${paint(term, "ok", "✓")} ${option === void 0 ? result.id : label(option)}`, term.width);
|
|
956
|
+
};
|
|
957
|
+
/** The numbered option lines of a plain prompt: ` 1) id label - description`. */
|
|
958
|
+
const plainOptions = (options, recommended) => options.map((option, i) => {
|
|
959
|
+
const description = option.description === void 0 ? "" : ` - ${option.description}`;
|
|
960
|
+
const mark = option.id === recommended ? " (recommended)" : "";
|
|
961
|
+
const name = option.label === void 0 ? "" : ` ${option.label}`;
|
|
962
|
+
return ` ${String(i + 1)}) ${option.id}${name}${description}${mark}`;
|
|
963
|
+
});
|
|
964
|
+
/** The plain prompt: numbered options, then `Choose [n]: ` until one fits. */
|
|
965
|
+
const selectPlain = (io, o) => {
|
|
966
|
+
const term = termOf(io.output, io.env);
|
|
967
|
+
const say = (text) => {
|
|
968
|
+
io.output.write(`${text}\n`);
|
|
969
|
+
};
|
|
970
|
+
for (const line of o.header?.(term) ?? []) say(line);
|
|
971
|
+
for (const line of plainOptions(o.options, o.recommended)) say(line);
|
|
972
|
+
for (const line of o.footer ?? []) say(line);
|
|
973
|
+
const at = o.options.findIndex((opt) => opt.id === o.recommended);
|
|
974
|
+
const hint = at === -1 ? "" : ` [${String(at + 1)}]`;
|
|
975
|
+
const ask = async () => {
|
|
976
|
+
const answer = await readLine(io, ` Choose${hint}: `, o.signal);
|
|
977
|
+
if (answer === void 0) throw new Error("input ended");
|
|
978
|
+
const id = pickOption(answer, o.options, o.recommended);
|
|
979
|
+
if (id === void 0) {
|
|
980
|
+
if (o.details !== void 0 && answer.trim() === "d") say(o.details);
|
|
981
|
+
else say(" Type an option's number or id.");
|
|
982
|
+
return ask();
|
|
983
|
+
}
|
|
984
|
+
if (o.note !== true) return {
|
|
985
|
+
kind: "chosen",
|
|
986
|
+
id
|
|
987
|
+
};
|
|
988
|
+
const noted = await readText(io, {
|
|
989
|
+
message: o.notePrompt ?? NOTE_PROMPT,
|
|
990
|
+
...o.signal === void 0 ? {} : { signal: o.signal }
|
|
991
|
+
});
|
|
992
|
+
return noted.kind === "text" && noted.value !== "" ? {
|
|
993
|
+
kind: "chosen",
|
|
994
|
+
id,
|
|
995
|
+
note: noted.value
|
|
996
|
+
} : {
|
|
997
|
+
kind: "chosen",
|
|
998
|
+
id
|
|
999
|
+
};
|
|
1000
|
+
};
|
|
1001
|
+
return ask();
|
|
1002
|
+
};
|
|
1003
|
+
/** Asks for one of `options`. */
|
|
1004
|
+
const select = (io, o) => {
|
|
1005
|
+
if (o.signal?.aborted === true) return Promise.reject(withdrawn(o.signal));
|
|
1006
|
+
if (!canKey(io.input, io.output, io.env)) return selectPlain(io, o);
|
|
1007
|
+
const term = () => termOf(io.output, io.env);
|
|
1008
|
+
const region = createRegion(io.output, term);
|
|
1009
|
+
const done = o.done ?? defaultDone(o.options);
|
|
1010
|
+
return new Promise((resolve, reject) => {
|
|
1011
|
+
const start = o.options.findIndex((opt) => opt.id === o.recommended);
|
|
1012
|
+
let cursor = Math.max(0, start);
|
|
1013
|
+
let showDetails = false;
|
|
1014
|
+
let note;
|
|
1015
|
+
let editing;
|
|
1016
|
+
const optionLines = (t) => o.options.flatMap((option, i) => {
|
|
1017
|
+
const selected = i === cursor;
|
|
1018
|
+
const number = i < 9 ? `${String(i + 1)}.` : " ";
|
|
1019
|
+
const mark = option.id === o.recommended ? " (recommended)" : "";
|
|
1020
|
+
const head = cut(`${number} ${label(option)}`, t.width - 4 - mark.length);
|
|
1021
|
+
const text = selected ? apply$1(t, ["bold", "cyan"], head) : head;
|
|
1022
|
+
const lines = [` ${selected ? paint(t, "accent", "❯") : " "} ${text}${mark === "" ? "" : paint(t, "ok", mark)}`];
|
|
1023
|
+
for (const line of wrap(option.description ?? "", t.width - 7)) if (line !== "") lines.push(paint(t, "muted", ` ${line}`));
|
|
1024
|
+
return lines;
|
|
1025
|
+
});
|
|
1026
|
+
const keyLine = (t) => {
|
|
1027
|
+
const keys = editing === void 0 ? [
|
|
1028
|
+
"↑/↓ move",
|
|
1029
|
+
"Enter choose",
|
|
1030
|
+
...o.note === true ? ["n note"] : [],
|
|
1031
|
+
...o.details === void 0 ? [] : [`d ${showDetails ? "hide" : "show"} details`],
|
|
1032
|
+
...o.later === true ? ["q later"] : []
|
|
1033
|
+
] : ["Enter save", "Esc cancel"];
|
|
1034
|
+
return paint(t, "muted", cut(` ${keys.join(" · ")}`, t.width));
|
|
1035
|
+
};
|
|
1036
|
+
const render = () => {
|
|
1037
|
+
const t = term();
|
|
1038
|
+
const head = o.header?.(t) ?? [];
|
|
1039
|
+
const body = optionLines(t);
|
|
1040
|
+
const noteLine = note === void 0 ? [] : [paint(t, "muted", cut(` note: ${note}`, t.width))];
|
|
1041
|
+
const above = [
|
|
1042
|
+
...head,
|
|
1043
|
+
...body,
|
|
1044
|
+
...noteLine
|
|
1045
|
+
];
|
|
1046
|
+
const room = Math.max(3, t.rows - above.length - 3);
|
|
1047
|
+
const all = showDetails && o.details !== void 0 ? wrap(o.details.trimEnd(), t.width - 3) : [];
|
|
1048
|
+
const detail = (all.length > room ? [...all.slice(0, room - 1), `… ${String(all.length - room + 1)} more lines`] : all).map((line) => paint(t, "muted", ` ${line}`));
|
|
1049
|
+
const lines = [
|
|
1050
|
+
...above,
|
|
1051
|
+
...detail,
|
|
1052
|
+
keyLine(t)
|
|
1053
|
+
];
|
|
1054
|
+
if (editing === void 0) {
|
|
1055
|
+
region.draw(lines);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
const lead = ` ${paint(t, "accent", "✎")} Note › `;
|
|
1059
|
+
const { line, column } = showLine(lead, editing, t.width, 10);
|
|
1060
|
+
region.draw([...lines, line], column);
|
|
1061
|
+
};
|
|
1062
|
+
const finish = (result) => {
|
|
1063
|
+
stop();
|
|
1064
|
+
cleanup();
|
|
1065
|
+
region.finish(done(result, term()));
|
|
1066
|
+
resolve(result);
|
|
1067
|
+
};
|
|
1068
|
+
const cleanup = () => {
|
|
1069
|
+
io.output.off("resize", render);
|
|
1070
|
+
o.signal?.removeEventListener("abort", onAbort);
|
|
1071
|
+
};
|
|
1072
|
+
const onAbort = () => {
|
|
1073
|
+
stop();
|
|
1074
|
+
cleanup();
|
|
1075
|
+
region.finish();
|
|
1076
|
+
reject(withdrawn(o.signal));
|
|
1077
|
+
};
|
|
1078
|
+
const edit = (key) => {
|
|
1079
|
+
if (editing === void 0) return;
|
|
1080
|
+
if (key.name === "enter") {
|
|
1081
|
+
const value = editing.value.trim();
|
|
1082
|
+
note = value === "" ? void 0 : value;
|
|
1083
|
+
editing = void 0;
|
|
1084
|
+
} else if (key.name === "escape") editing = void 0;
|
|
1085
|
+
else editing = editLine(editing, key);
|
|
1086
|
+
render();
|
|
1087
|
+
};
|
|
1088
|
+
const move = (to) => {
|
|
1089
|
+
cursor = Math.min(o.options.length - 1, Math.max(0, to));
|
|
1090
|
+
render();
|
|
1091
|
+
};
|
|
1092
|
+
const stop = readKeys(io.input, (key) => {
|
|
1093
|
+
if (key.name === "ctrl-c") {
|
|
1094
|
+
stop();
|
|
1095
|
+
cleanup();
|
|
1096
|
+
region.finish();
|
|
1097
|
+
resolve({ kind: "interrupted" });
|
|
1098
|
+
interrupt(io);
|
|
1099
|
+
} else if (editing !== void 0) edit(key);
|
|
1100
|
+
else if (key.name === "enter") {
|
|
1101
|
+
const id = o.options[cursor]?.id;
|
|
1102
|
+
if (id === void 0) return;
|
|
1103
|
+
finish(note === void 0 ? {
|
|
1104
|
+
kind: "chosen",
|
|
1105
|
+
id
|
|
1106
|
+
} : {
|
|
1107
|
+
kind: "chosen",
|
|
1108
|
+
id,
|
|
1109
|
+
note
|
|
1110
|
+
});
|
|
1111
|
+
} else if (key.name === "up") move(cursor - 1);
|
|
1112
|
+
else if (key.name === "down") move(cursor + 1);
|
|
1113
|
+
else if (key.name === "home") move(0);
|
|
1114
|
+
else if (key.name === "end") move(o.options.length - 1);
|
|
1115
|
+
else if (key.name === "escape" && o.later === true) finish({ kind: "later" });
|
|
1116
|
+
else if (key.name === "char") {
|
|
1117
|
+
const c = key.text;
|
|
1118
|
+
if (c === "k") move(cursor - 1);
|
|
1119
|
+
else if (c === "j") move(cursor + 1);
|
|
1120
|
+
else if (c >= "1" && c <= "9") {
|
|
1121
|
+
if (Number(c) <= o.options.length) move(Number(c) - 1);
|
|
1122
|
+
} else if (c === "n" && o.note === true) {
|
|
1123
|
+
const value = note ?? "";
|
|
1124
|
+
editing = {
|
|
1125
|
+
value,
|
|
1126
|
+
pos: Array.from(value).length
|
|
1127
|
+
};
|
|
1128
|
+
render();
|
|
1129
|
+
} else if (c === "d" && o.details !== void 0) {
|
|
1130
|
+
showDetails = !showDetails;
|
|
1131
|
+
render();
|
|
1132
|
+
} else if (c === "q" && o.later === true) finish({ kind: "later" });
|
|
1133
|
+
}
|
|
1134
|
+
});
|
|
1135
|
+
io.output.on("resize", render);
|
|
1136
|
+
o.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1137
|
+
render();
|
|
1138
|
+
});
|
|
1139
|
+
};
|
|
777
1140
|
// src/human.ts
|
|
778
1141
|
/**
|
|
779
1142
|
* @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
|
|
780
1143
|
* permission prompts on the terminal, one at a time, and denies or defers
|
|
781
|
-
* when no one is there.
|
|
782
|
-
*
|
|
783
|
-
*
|
|
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`.
|
|
784
1149
|
*/
|
|
785
1150
|
const PREVIEW = 160;
|
|
786
1151
|
/** A document longer than this is offered (`d`) rather than printed. */
|
|
787
1152
|
const SHORT_DOCUMENT_LINES = 40;
|
|
788
1153
|
const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
|
|
789
|
-
const
|
|
1154
|
+
const NOTE_MESSAGE = "Note for the run (optional, Enter to skip)";
|
|
790
1155
|
const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
|
|
791
1156
|
/** One line that shows what a tool is about to do. */
|
|
792
1157
|
const previewInput = (toolName, input) => {
|
|
@@ -794,20 +1159,12 @@ const previewInput = (toolName, input) => {
|
|
|
794
1159
|
const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
|
|
795
1160
|
return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
|
|
796
1161
|
};
|
|
797
|
-
/**
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
return options.find((o) => o.id === value)?.id;
|
|
804
|
-
};
|
|
805
|
-
const optionLine = (option, index, recommended) => {
|
|
806
|
-
const label = option.label === void 0 ? "" : ` ${option.label}`;
|
|
807
|
-
const description = option.description === void 0 ? "" : style("dim", ` - ${option.description}`);
|
|
808
|
-
const mark = option.id === recommended ? style("green", " (recommended)") : "";
|
|
809
|
-
return ` ${String(index + 1)}) ${style("bold", option.id)}${label}${description}${mark}`;
|
|
810
|
-
};
|
|
1162
|
+
/** Never answers, until `signal` withdraws it: what a stopped prompt leaves. */
|
|
1163
|
+
const parked = (signal) => new Promise((_, reject) => {
|
|
1164
|
+
signal?.addEventListener("abort", () => {
|
|
1165
|
+
reject(withdrawn(signal));
|
|
1166
|
+
});
|
|
1167
|
+
});
|
|
811
1168
|
/**
|
|
812
1169
|
* Answers permission prompts and `ask_user` questions on the terminal, one at a
|
|
813
1170
|
* time. Without a terminal it denies tools and leaves questions to the node. A
|
|
@@ -815,90 +1172,257 @@ const optionLine = (option, index, recommended) => {
|
|
|
815
1172
|
*/
|
|
816
1173
|
const createTerminalHuman = (io) => {
|
|
817
1174
|
let rl;
|
|
1175
|
+
let closed;
|
|
818
1176
|
let turn = Promise.resolve();
|
|
819
1177
|
const say = (text) => {
|
|
820
1178
|
io.output.write(`${text}\n`);
|
|
821
1179
|
};
|
|
822
|
-
|
|
1180
|
+
/** The one `readline` the plain prompts share; a rich prompt never opens it. */
|
|
1181
|
+
const readLine = (text, signal) => {
|
|
823
1182
|
if (rl === void 0) {
|
|
824
|
-
|
|
1183
|
+
const opened = createInterface({
|
|
825
1184
|
input: io.input,
|
|
826
|
-
output: io.output
|
|
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
|
+
});
|
|
827
1193
|
});
|
|
828
|
-
|
|
1194
|
+
rl = opened;
|
|
829
1195
|
}
|
|
830
|
-
|
|
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 }
|
|
831
1205
|
};
|
|
1206
|
+
const term = () => termOf(io.output, io.env);
|
|
832
1207
|
/** The optional line after a choice, when the asker takes a note. */
|
|
833
1208
|
const takeNote = async (onNote, signal) => {
|
|
834
1209
|
if (onNote === void 0) return;
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
if (picked !== void 0) return picked;
|
|
842
|
-
if (document !== void 0 && answer.trim() === "d") say(document);
|
|
843
|
-
else say(style("dim", " Type an option's number or id."));
|
|
844
|
-
return choose(hint, options, recommended, signal, document);
|
|
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);
|
|
845
1216
|
};
|
|
846
1217
|
const oneAtATime = (work) => {
|
|
847
1218
|
const result = turn.then(work);
|
|
848
1219
|
turn = result.catch(() => null);
|
|
849
1220
|
return result;
|
|
850
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
|
+
});
|
|
851
1262
|
return {
|
|
852
1263
|
permit: ({ nodeId, toolName, input, signal, onNote }) => oneAtATime(async () => {
|
|
853
1264
|
if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
|
|
854
|
-
|
|
1265
|
+
const t = term();
|
|
1266
|
+
if (t.rich) say("");
|
|
1267
|
+
say(permitLine(t, nodeId, previewInput(toolName, input)));
|
|
855
1268
|
if (!io.interactive) {
|
|
856
|
-
say(
|
|
1269
|
+
say(paint(t, "muted", ` denied: ${NO_ONE_TO_PERMIT}`));
|
|
857
1270
|
return {
|
|
858
1271
|
allow: false,
|
|
859
1272
|
message: NO_ONE_TO_PERMIT
|
|
860
1273
|
};
|
|
861
1274
|
}
|
|
862
|
-
const
|
|
863
|
-
|
|
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 };
|
|
864
1281
|
await takeNote(onNote, signal);
|
|
865
1282
|
return {
|
|
866
1283
|
allow: false,
|
|
867
1284
|
message: "The user did not allow this tool."
|
|
868
1285
|
};
|
|
869
1286
|
}),
|
|
870
|
-
ask: (
|
|
871
|
-
|
|
872
|
-
const documentLines = document?.trimEnd().split("\n").length ?? 0;
|
|
873
|
-
if (document !== void 0 && documentLines <= SHORT_DOCUMENT_LINES) say(document);
|
|
874
|
-
say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} asks: ${question}`);
|
|
875
|
-
if (context !== void 0) say(style("dim", ` ${context.replaceAll("\n", "\n ")}`));
|
|
876
|
-
for (const [index, option] of options.entries()) say(optionLine(option, index, recommended));
|
|
877
|
-
if (!io.interactive) {
|
|
878
|
-
const answer = recommended ?? NO_ONE_TO_ASK;
|
|
879
|
-
say(style("dim", ` no terminal; answered ${answer}`));
|
|
880
|
-
return answer;
|
|
881
|
-
}
|
|
882
|
-
const fallback = options.findIndex((o) => o.id === recommended);
|
|
883
|
-
const hint = fallback === -1 ? "" : ` [${String(fallback + 1)}]`;
|
|
884
|
-
if (documentLines > SHORT_DOCUMENT_LINES) say(` Document: ${String(documentLines)} lines. Type d to read it.`);
|
|
885
|
-
const picked = await choose(hint, options, recommended, signal, document);
|
|
886
|
-
await takeNote(onNote, signal);
|
|
887
|
-
return picked;
|
|
888
|
-
}),
|
|
1287
|
+
ask: async (q) => await asking(q) ?? NO_ONE_TO_ASK,
|
|
1288
|
+
askAt: (q, place) => asking(q, place),
|
|
889
1289
|
close: () => {
|
|
890
1290
|
rl?.close();
|
|
891
1291
|
}
|
|
892
1292
|
};
|
|
893
1293
|
};
|
|
1294
|
+
// src/run/format-end.ts
|
|
1295
|
+
/**
|
|
1296
|
+
* @file How a node's end reads on the terminal (specs/runtime/session-ends.md):
|
|
1297
|
+
* its kind's label and Ork's own message, a note for a kind that may pass,
|
|
1298
|
+
* the setup hint for an account or provider end, and the scrubbed raw detail
|
|
1299
|
+
* only when asked for. Never the provider's own words first.
|
|
1300
|
+
*/
|
|
1301
|
+
/** What a transient kind says in place of its hint on the log line. */
|
|
1302
|
+
const MAY_PASS = "(may pass; ork resume retries)";
|
|
1303
|
+
/** `Turn limit: reached its turn limit (8)`, with a note when it may pass. */
|
|
1304
|
+
const endHeadline = (end) => {
|
|
1305
|
+
const { label, transient } = END_KINDS[end.kind];
|
|
1306
|
+
return `${label}: ${end.message}${transient ? ` ${MAY_PASS}` : ""}`;
|
|
1307
|
+
};
|
|
1308
|
+
/**
|
|
1309
|
+
* The lines under a headline: the hint for an account or provider end (what to
|
|
1310
|
+
* set up) or whenever `full` (`--verbose`, one node's log), and the detail only
|
|
1311
|
+
* when `full`. A transient kind's hint is its retry wording, which the headline
|
|
1312
|
+
* already says, so it is not printed twice.
|
|
1313
|
+
*/
|
|
1314
|
+
const endLines = (end, full, term = createView().term) => {
|
|
1315
|
+
const style = styler(term);
|
|
1316
|
+
const { actor, hint, transient } = END_KINDS[end.kind];
|
|
1317
|
+
const setup = actor === "account" || actor === "provider";
|
|
1318
|
+
const lines = [];
|
|
1319
|
+
if (hint !== "" && !transient && (full || setup)) lines.push(` ${style("dim", hint)}`);
|
|
1320
|
+
if (full && end.detail !== void 0) lines.push(style("dim", " details:"), ...end.detail.split("\n").map((line) => style("dim", ` ${line}`)));
|
|
1321
|
+
return lines;
|
|
1322
|
+
};
|
|
1323
|
+
/**
|
|
1324
|
+
* A finished node's line under `tag`: its verdict, or for a failure its kind
|
|
1325
|
+
* and message (and the lines under it). The agent's own summary stays for a
|
|
1326
|
+
* `fail` it reported, and an old run's text no rule reads stays as stored.
|
|
1327
|
+
*/
|
|
1328
|
+
const formatFinished = (event, tag, verbose, term = createView().term, rest = tag) => {
|
|
1329
|
+
const style = styler(term);
|
|
1330
|
+
const conflict = hasConflict(event.output);
|
|
1331
|
+
const mark = event.ok ? style(conflict ? "yellow" : "green", conflict ? "!" : "✓") : style("red", "✗");
|
|
1332
|
+
const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
|
|
1333
|
+
const end = endOf(event);
|
|
1334
|
+
if (!(!event.ok && end.kind !== "reported_failure" && (event.end !== void 0 || end.kind !== "unknown"))) return [`${tag} ${mark} ${nodeVerdict(event.output, event.ok, event.error)}${spent}`];
|
|
1335
|
+
return [`${tag} ${mark} ${endHeadline(end)}${spent}`, ...endLines(end, verbose, term).map((line) => `${rest} ${line}`)];
|
|
1336
|
+
};
|
|
1337
|
+
// src/run/format-outcome.ts
|
|
1338
|
+
/**
|
|
1339
|
+
* @file How a finished session reads on the terminal: its report or its end,
|
|
1340
|
+
* the branch, artifacts and fields, and the usage line.
|
|
1341
|
+
*/
|
|
1342
|
+
const STATUS_MARK = {
|
|
1343
|
+
pass: {
|
|
1344
|
+
mark: "✓",
|
|
1345
|
+
color: "green"
|
|
1346
|
+
},
|
|
1347
|
+
done: {
|
|
1348
|
+
mark: "✓",
|
|
1349
|
+
color: "green"
|
|
1350
|
+
},
|
|
1351
|
+
changes_requested: {
|
|
1352
|
+
mark: "!",
|
|
1353
|
+
color: "yellow"
|
|
1354
|
+
},
|
|
1355
|
+
fail: {
|
|
1356
|
+
mark: "✗",
|
|
1357
|
+
color: "red"
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
|
|
1361
|
+
/** The usage line: cost, tokens, turns and time. */
|
|
1362
|
+
const formatUsage = ({ usage }, term = createView().term) => styler(term)("dim", [
|
|
1363
|
+
cost(usage.costUsd),
|
|
1364
|
+
`${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
|
|
1365
|
+
`${String(usage.turns)} turns`,
|
|
1366
|
+
seconds(usage.durationMs)
|
|
1367
|
+
].join(" · "));
|
|
1368
|
+
/** How a finished session reads on the terminal. */
|
|
1369
|
+
const formatOutcome = (nodeId, outcome, verbose = false, term = createView().term) => {
|
|
1370
|
+
const style = styler(term);
|
|
1371
|
+
const { report } = outcome;
|
|
1372
|
+
if (report === null) {
|
|
1373
|
+
const end = outcomeEnd(outcome);
|
|
1374
|
+
return [
|
|
1375
|
+
`${style("red", "✗")} ${style("bold", nodeId)} ${endHeadline(end)}`,
|
|
1376
|
+
...endLines(end, verbose, term),
|
|
1377
|
+
` ${formatUsage(outcome, term)}`
|
|
1378
|
+
];
|
|
1379
|
+
}
|
|
1380
|
+
const shown = STATUS_MARK[report.status];
|
|
1381
|
+
const lines = [`${shown === void 0 ? "-" : style(shown.color, shown.mark)} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
|
|
1382
|
+
if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
|
|
1383
|
+
for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
|
|
1384
|
+
for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
|
|
1385
|
+
lines.push(` ${formatUsage(outcome, term)}`);
|
|
1386
|
+
return lines;
|
|
1387
|
+
};
|
|
894
1388
|
// src/run/format-result.ts
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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"
|
|
902
1426
|
};
|
|
903
1427
|
/** `1 failure handled`, `2 failures handled`. */
|
|
904
1428
|
const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" : "failures"} handled`;
|
|
@@ -906,17 +1430,73 @@ const failuresHandled = (count) => `${String(count)} ${count === 1 ? "failure" :
|
|
|
906
1430
|
* The run summary: status, cost, what ran, what was skipped and which failures
|
|
907
1431
|
* an edge handled, branches to look at, why, and mail to nodes that never ran.
|
|
908
1432
|
*/
|
|
909
|
-
const formatRunResult = (result) => {
|
|
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);
|
|
910
1437
|
const ran = result.nodes.filter((n) => n.runs > 0).length;
|
|
911
1438
|
const skipped = result.skipped ?? result.nodes.length - ran;
|
|
912
1439
|
const handled = result.failuresHandled ?? 0;
|
|
913
|
-
const lines = ["", `${
|
|
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)}`}`)}`];
|
|
914
1441
|
for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
|
|
915
1442
|
for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
|
|
916
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`);
|
|
917
1444
|
return lines;
|
|
918
1445
|
};
|
|
1446
|
+
// src/run/format-tool.ts
|
|
1447
|
+
/**
|
|
1448
|
+
* @file How a session's own words and tool calls read in the transcript: the
|
|
1449
|
+
* agent's text (indented plain, wrapped rich), and a tool call as `Tool first
|
|
1450
|
+
* line` cut to the screen with `+N lines`, a path inside the node's worktree
|
|
1451
|
+
* relative to it, or whole with `--verbose`. Untrusted text goes through
|
|
1452
|
+
* `plain()` first.
|
|
1453
|
+
*/
|
|
1454
|
+
const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${plain(line)}`);
|
|
1455
|
+
/** Agent text under a 2-column indent, its long lines wrapped to `room`. */
|
|
1456
|
+
const wrapped = (text, room) => {
|
|
1457
|
+
const width = Math.max(1, room - 2);
|
|
1458
|
+
return text.trimEnd().split("\n").flatMap((line) => {
|
|
1459
|
+
const safe = plain(line).trimEnd();
|
|
1460
|
+
return widthOf(safe) <= width ? [safe] : wrap(safe, width);
|
|
1461
|
+
}).map((line) => ` ${line}`);
|
|
1462
|
+
};
|
|
1463
|
+
/** A tool's input as the call says it: the key that names its target, whole. */
|
|
1464
|
+
const toolText = (input, root) => {
|
|
1465
|
+
const key = TOOL_INPUT_KEYS.find((k) => typeof input[k] === "string");
|
|
1466
|
+
const text = key === void 0 ? JSON.stringify(input) : String(input[key]);
|
|
1467
|
+
if (root === void 0) return text;
|
|
1468
|
+
const base = root.replace(/\/+$/u, "");
|
|
1469
|
+
return text === base ? "." : text.replaceAll(`${base}/`, "");
|
|
1470
|
+
};
|
|
1471
|
+
/**
|
|
1472
|
+
* A tool call: rich, `Tool first line` cut to `room` with `+N lines` when it
|
|
1473
|
+
* had more; plain, today's one dim line. `verbose` shows the call whole.
|
|
1474
|
+
*/
|
|
1475
|
+
const toolLines = (name, input, view, room, verbose, node) => {
|
|
1476
|
+
const { term } = view;
|
|
1477
|
+
const style = styler(term);
|
|
1478
|
+
if (!term.rich && !verbose) return [style("dim", ` · ${plain(previewInput(name, input))}`)];
|
|
1479
|
+
const label = plain(name);
|
|
1480
|
+
const [first = "", ...rest] = toolText(input, view.roots.get(node)).trimEnd().split("\n").map((line) => plain(line));
|
|
1481
|
+
const lead = term.rich ? " " : " · ";
|
|
1482
|
+
const sep = term.rich ? " " : " ";
|
|
1483
|
+
const headWidth = widthOf(lead) + widthOf(label) + sep.length;
|
|
1484
|
+
const styledHead = `${style("dim", lead)}${style("bold", label)}${style("dim", sep)}`;
|
|
1485
|
+
if (verbose) {
|
|
1486
|
+
const pad = " ".repeat(headWidth);
|
|
1487
|
+
return [`${styledHead}${style("dim", first)}`, ...rest.map((line) => style("dim", `${pad}${line}`))];
|
|
1488
|
+
}
|
|
1489
|
+
const more = rest.length === 0 ? "" : ` +${String(rest.length)} ${rest.length === 1 ? "line" : "lines"}`;
|
|
1490
|
+
return [`${styledHead}${style("dim", cut(first.trim(), Math.max(1, room - headWidth - more.length)))}${style("dim", more)}`];
|
|
1491
|
+
};
|
|
919
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;
|
|
920
1500
|
/** Who answered a decision, as the log says it. */
|
|
921
1501
|
const ANSWERED_BY = {
|
|
922
1502
|
terminal: "terminal",
|
|
@@ -924,57 +1504,40 @@ const ANSWERED_BY = {
|
|
|
924
1504
|
ui: "ork ui",
|
|
925
1505
|
owner: "its owner"
|
|
926
1506
|
};
|
|
927
|
-
const STATUS_MARK = {
|
|
928
|
-
pass: style("green", "✓"),
|
|
929
|
-
done: style("green", "✓"),
|
|
930
|
-
changes_requested: style("yellow", "!"),
|
|
931
|
-
fail: style("red", "✗")
|
|
932
|
-
};
|
|
933
|
-
const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
|
|
934
|
-
const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${line}`);
|
|
935
|
-
/** The usage line: cost, tokens, turns and time. */
|
|
936
|
-
const formatUsage = ({ usage }) => style("dim", [
|
|
937
|
-
cost(usage.costUsd),
|
|
938
|
-
`${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
|
|
939
|
-
`${String(usage.turns)} turns`,
|
|
940
|
-
seconds(usage.durationMs)
|
|
941
|
-
].join(" · "));
|
|
942
|
-
/** How a finished session reads on the terminal. */
|
|
943
|
-
const formatOutcome = (nodeId, outcome) => {
|
|
944
|
-
const { report } = outcome;
|
|
945
|
-
if (report === null) return [
|
|
946
|
-
`${style("red", "✗")} ${style("bold", nodeId)} ended without a report (${outcome.subtype})`,
|
|
947
|
-
...outcome.errors.map((e) => ` ${style("red", e)}`),
|
|
948
|
-
` ${formatUsage(outcome)}`
|
|
949
|
-
];
|
|
950
|
-
const lines = [`${STATUS_MARK[report.status] ?? "-"} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
|
|
951
|
-
if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
|
|
952
|
-
for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
|
|
953
|
-
for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
|
|
954
|
-
lines.push(` ${formatUsage(outcome)}`);
|
|
955
|
-
return lines;
|
|
956
|
-
};
|
|
957
1507
|
/** The transcript lines for one event; tool names come from `tools`. */
|
|
958
|
-
const formatEvent = (nodeId, event, 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);
|
|
959
1511
|
if (event.type === "session.started") {
|
|
960
1512
|
const fork = event.fork === true ? ", a fork to answer a question" : "";
|
|
961
1513
|
return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId}${fork})`)];
|
|
962
1514
|
}
|
|
963
|
-
if (event.type === "assistant.text") return indent(event.text);
|
|
1515
|
+
if (event.type === "assistant.text") return term.rich ? wrapped(event.text, room) : indent(event.text);
|
|
964
1516
|
if (event.type === "session.waiting") return indent(event.reason);
|
|
965
1517
|
if (event.type === "tool.called") {
|
|
966
1518
|
tools.set(event.id, event.name);
|
|
967
|
-
return
|
|
1519
|
+
return toolLines(event.name, event.input, view, room, verbose, nodeId);
|
|
968
1520
|
}
|
|
969
1521
|
if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
|
|
970
1522
|
if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
|
|
971
|
-
return formatOutcome(nodeId, event.outcome);
|
|
1523
|
+
return formatOutcome(nodeId, event.outcome, verbose, term);
|
|
972
1524
|
};
|
|
973
1525
|
const items = (count) => `${String(count)} ${count === 1 ? "item" : "items"}`;
|
|
974
1526
|
/** Transcript lines for one run event, prefixed with the node they are from. */
|
|
975
|
-
const formatRunEvent = (event, width, tools, terminal = false) => {
|
|
976
|
-
const
|
|
977
|
-
|
|
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
|
+
}
|
|
978
1541
|
if (event.type === "node.started") {
|
|
979
1542
|
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
980
1543
|
const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
|
|
@@ -988,14 +1551,10 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
|
|
|
988
1551
|
const why = inner.fork === true ? ", to answer a question" : "";
|
|
989
1552
|
return [`${tag(event.id)} ${fork}${style("dim", `${inner.model}, session ${inner.sessionId}${why}`)}`];
|
|
990
1553
|
}
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
if (event.type === "node.finished") {
|
|
994
|
-
const conflict = hasConflict(event.output);
|
|
995
|
-
const mark = event.ok ? style(conflict ? "yellow" : "green", conflict ? "!" : "✓") : style("red", "✗");
|
|
996
|
-
const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
|
|
997
|
-
return [`${tag(event.id)} ${mark} ${nodeVerdict(event.output, event.ok, event.error)}${spent}`];
|
|
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")}`));
|
|
998
1556
|
}
|
|
1557
|
+
if (event.type === "node.finished") return formatFinished(event, tag(event.id), verbose, term, blank(event.id));
|
|
999
1558
|
if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
|
|
1000
1559
|
if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
|
|
1001
1560
|
if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
|
|
@@ -1003,6 +1562,7 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
|
|
|
1003
1562
|
if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
|
|
1004
1563
|
if (event.type === "edge.taken") return [];
|
|
1005
1564
|
if (event.type === "worktree") {
|
|
1565
|
+
if (event.action !== "removed") view.roots.set(event.id, event.path);
|
|
1006
1566
|
const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
|
|
1007
1567
|
const what = event.action === "removed" && event.branch !== void 0 ? `worktree of ${event.branch} (branch kept)` : `${event.branch ?? event.path}${detail}`;
|
|
1008
1568
|
const line = `${event.action} ${what}`;
|
|
@@ -1012,7 +1572,7 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
|
|
|
1012
1572
|
const what = event.kind === "permission" ? "asks to" : "asks:";
|
|
1013
1573
|
const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
|
|
1014
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}`;
|
|
1015
|
-
return [`${tag(event.node)} ${style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
|
|
1575
|
+
return [`${tag(event.node)} ${term.rich ? chip(term, CHIP_LABELS.ask, "accent") : style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(${where})`)}`];
|
|
1016
1576
|
}
|
|
1017
1577
|
if (event.type === "decision.answered") {
|
|
1018
1578
|
const by = event.by === "owner" && event.owner !== void 0 ? event.owner : ANSWERED_BY[event.by];
|
|
@@ -1029,6 +1589,8 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
|
|
|
1029
1589
|
return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
|
|
1030
1590
|
}
|
|
1031
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")}`];
|
|
1032
1594
|
if (event.type === "each.listed") {
|
|
1033
1595
|
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
1034
1596
|
return [`${tag(event.node)} ${style("dim", `${items(event.count)}${round}`)}`];
|
|
@@ -1037,11 +1599,10 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
|
|
|
1037
1599
|
const over = `${items(event.count)}, over maxItems ${String(event.maxItems)}`;
|
|
1038
1600
|
return [`${tag(event.node)} ${style("yellow", `! ${over}`)}`];
|
|
1039
1601
|
}
|
|
1040
|
-
if (event.type === "run.finished") return formatRunResult(event.result);
|
|
1602
|
+
if (event.type === "run.finished") return formatRunResult(event.result, term);
|
|
1041
1603
|
if (event.type === "run.resumed") {
|
|
1042
1604
|
const [one] = event.kept;
|
|
1043
|
-
|
|
1044
|
-
return ["", style("dim", `▸ resumed: ${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`}`)];
|
|
1045
1606
|
}
|
|
1046
1607
|
if (event.type === "feedback.recorded") {
|
|
1047
1608
|
const { target, kind, title } = event.feedback;
|
|
@@ -1052,7 +1613,7 @@ const formatRunEvent = (event, width, tools, terminal = false) => {
|
|
|
1052
1613
|
const note = event.delivered === "queued" ? " (for its next session)" : "";
|
|
1053
1614
|
return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
|
|
1054
1615
|
}
|
|
1055
|
-
return
|
|
1616
|
+
return under(event.node, formatBusEvent(event, term));
|
|
1056
1617
|
};
|
|
1057
1618
|
// src/runs/chat.ts
|
|
1058
1619
|
/**
|
|
@@ -1151,13 +1712,14 @@ const resumeChat = async (options) => {
|
|
|
1151
1712
|
/** Turns stored events into lines, keeping the state a transcript needs. */
|
|
1152
1713
|
const createLogPrinter = (options) => {
|
|
1153
1714
|
const tools = /* @__PURE__ */ new Map();
|
|
1715
|
+
const view = createView();
|
|
1154
1716
|
let width = 8;
|
|
1155
1717
|
return (stored) => {
|
|
1156
1718
|
const { event } = stored;
|
|
1157
1719
|
if (event.type === "run.started") width = Math.max(...event.nodes.map((n) => n.id.length), 1);
|
|
1158
1720
|
if (options.node !== void 0 && !isNodeEvent(event, options.node)) return [];
|
|
1159
1721
|
if (options.json) return [JSON.stringify(stored)];
|
|
1160
|
-
return formatRunEvent(event, width, tools);
|
|
1722
|
+
return formatRunEvent(event, width, tools, false, options.verbose === true || options.node !== void 0, view);
|
|
1161
1723
|
};
|
|
1162
1724
|
};
|
|
1163
1725
|
/**
|
|
@@ -1194,6 +1756,8 @@ const followRun = async (store, run, out, options) => {
|
|
|
1194
1756
|
* session starts: the messages, steps and `forEach` it does not run. A node
|
|
1195
1757
|
* with `forEach` (RFC-013) runs once with no item when it has `items`, and is
|
|
1196
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).
|
|
1197
1761
|
*/
|
|
1198
1762
|
/** A warning line, as the run prints it. */
|
|
1199
1763
|
const noteLines = (lines) => lines.map((line) => `${style(["yellow", "bold"], "warning")} ${line}`);
|
|
@@ -1213,6 +1777,32 @@ const oneNodeNotes = (workflow, id) => {
|
|
|
1213
1777
|
...steps.map((step) => `${id}'s ${step}: does not run in a run of one node.`)
|
|
1214
1778
|
] };
|
|
1215
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
|
+
};
|
|
1216
1806
|
// src/run/origin.ts
|
|
1217
1807
|
/**
|
|
1218
1808
|
* @file Where a run's feedback comes from, beyond what its events say: Ork's
|
|
@@ -1326,10 +1916,11 @@ const runNode = async (options) => {
|
|
|
1326
1916
|
...options.home === void 0 ? {} : { home: options.home }
|
|
1327
1917
|
});
|
|
1328
1918
|
const signals = createSignals(feedbackOrigin(path.resolve(options.cwd, options.file)));
|
|
1919
|
+
const view = createView();
|
|
1329
1920
|
const record = (raw) => {
|
|
1330
1921
|
for (const event of signals(raw)) {
|
|
1331
1922
|
control.publish(recorder(event));
|
|
1332
|
-
if (event.type === "feedback.recorded" && !options.json) for (const line of formatRunEvent(event, id.length, /* @__PURE__ */ new Map())) options.out(line);
|
|
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);
|
|
1333
1924
|
}
|
|
1334
1925
|
};
|
|
1335
1926
|
const nodeType = workflow.nodes[id]?.type ?? "claude.worker";
|
|
@@ -1374,10 +1965,9 @@ const runNode = async (options) => {
|
|
|
1374
1965
|
});
|
|
1375
1966
|
if (event.type === "session.finished") outcome = event.outcome;
|
|
1376
1967
|
if (options.json) options.out(JSON.stringify(event));
|
|
1377
|
-
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);
|
|
1378
1969
|
}
|
|
1379
1970
|
const result = resultOf(outcome);
|
|
1380
|
-
const costUsd = result.usage?.costUsd ?? 0;
|
|
1381
1971
|
record({
|
|
1382
1972
|
type: "node.finished",
|
|
1383
1973
|
id,
|
|
@@ -1385,27 +1975,11 @@ const runNode = async (options) => {
|
|
|
1385
1975
|
ok: result.ok,
|
|
1386
1976
|
output: result.output,
|
|
1387
1977
|
...result.usage === void 0 ? {} : { usage: result.usage },
|
|
1388
|
-
...result
|
|
1978
|
+
...endedFields(result)
|
|
1389
1979
|
});
|
|
1390
1980
|
record({
|
|
1391
1981
|
type: "run.finished",
|
|
1392
|
-
result:
|
|
1393
|
-
runId,
|
|
1394
|
-
status: succeeded(outcome) ? "succeeded" : "failed",
|
|
1395
|
-
costUsd,
|
|
1396
|
-
nodes: [{
|
|
1397
|
-
id,
|
|
1398
|
-
runs: 1,
|
|
1399
|
-
ok: result.ok,
|
|
1400
|
-
output: result.output,
|
|
1401
|
-
costUsd,
|
|
1402
|
-
...result.error === void 0 ? {} : { error: result.error }
|
|
1403
|
-
}],
|
|
1404
|
-
reasons: result.error === void 0 ? [] : [`${id}: ${result.error}`],
|
|
1405
|
-
worktrees: [],
|
|
1406
|
-
skipped: 0,
|
|
1407
|
-
failuresHandled: 0
|
|
1408
|
-
}
|
|
1982
|
+
result: oneNodeResult(runId, id, result, succeeded(outcome))
|
|
1409
1983
|
});
|
|
1410
1984
|
await control.close();
|
|
1411
1985
|
return {
|
|
@@ -1693,6 +2267,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1693
2267
|
}, runDir) : appendRecorder(options.store, runId, runDir);
|
|
1694
2268
|
const width = Math.max(...Object.keys(workflow.nodes).map((id) => id.length));
|
|
1695
2269
|
const tools = /* @__PURE__ */ new Map();
|
|
2270
|
+
const view = createView();
|
|
1696
2271
|
const started = Promise.withResolvers();
|
|
1697
2272
|
const live = {};
|
|
1698
2273
|
const control = await startControl({
|
|
@@ -1712,7 +2287,7 @@ const runWorkflowFile = async (options) => {
|
|
|
1712
2287
|
for (const event of signals(raw)) {
|
|
1713
2288
|
control.publish(record(event));
|
|
1714
2289
|
if (options.json) options.out(JSON.stringify(event));
|
|
1715
|
-
else for (const line of formatRunEvent(event, width, tools, options.human !== void 0)) options.out(line);
|
|
2290
|
+
else for (const line of formatRunEvent(event, width, tools, options.human !== void 0, options.verbose, view)) options.out(line);
|
|
1716
2291
|
}
|
|
1717
2292
|
};
|
|
1718
2293
|
const workspace = root === void 0 ? void 0 : createWorkspace({
|
|
@@ -1779,6 +2354,10 @@ const runWorkflowFile = async (options) => {
|
|
|
1779
2354
|
...workspace === void 0 ? {} : { workspace },
|
|
1780
2355
|
scrub: runScrubber(maskedNames, process.env),
|
|
1781
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
|
+
},
|
|
1782
2361
|
beforeFinish: takeLastCommands(control, started.promise)
|
|
1783
2362
|
});
|
|
1784
2363
|
live.handle = handle;
|
|
@@ -1813,7 +2392,7 @@ const isDriver = (value) => AGENT_DRIVERS.some((d) => d.id === value);
|
|
|
1813
2392
|
/** Why `--driver` refuses a value: a retired id names its replacement. */
|
|
1814
2393
|
const badDriver = (value) => `--${retiredDriverHint(value) ?? `driver must be one of ${AGENT_DRIVERS.map((d) => d.id).join(", ")}, got \`${value}\``}`;
|
|
1815
2394
|
/** Loads the drivers only when a run starts, so other commands stay fast. */
|
|
1816
|
-
const loadRuntime = async (driver, runs) => (await import("./src-
|
|
2395
|
+
const loadRuntime = async (driver, runs) => (await import("./src-BnZgZGrf.mjs")).createRuntime(driver, sessionEnv(process.env), runs);
|
|
1817
2396
|
/**
|
|
1818
2397
|
* Ctrl-C asks the run to stop, and so does a terminal closing or a polite kill,
|
|
1819
2398
|
* so that the run ends tidily and leaves no result unscrubbed.
|
|
@@ -1886,6 +2465,11 @@ const run = defineCommand({
|
|
|
1886
2465
|
description: "Print the events as JSON lines",
|
|
1887
2466
|
default: false
|
|
1888
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
|
+
},
|
|
1889
2473
|
detach: {
|
|
1890
2474
|
type: "boolean",
|
|
1891
2475
|
description: "Run in the background; follow it with ork logs -f, answer with ork decide",
|
|
@@ -1967,6 +2551,7 @@ const run = defineCommand({
|
|
|
1967
2551
|
inPlace: args["in-place"],
|
|
1968
2552
|
...args["machine-claude"] ? { machineClaude: true } : {},
|
|
1969
2553
|
json: args.json,
|
|
2554
|
+
verbose: args.verbose,
|
|
1970
2555
|
runtime: loadRuntime,
|
|
1971
2556
|
store,
|
|
1972
2557
|
...human === void 0 ? {} : { human },
|
|
@@ -2012,22 +2597,25 @@ const run = defineCommand({
|
|
|
2012
2597
|
* each message at its session's next step, and its replies stream here; a
|
|
2013
2598
|
* node of a finished run is answered by resuming its last session.
|
|
2014
2599
|
*/
|
|
2015
|
-
/**
|
|
2016
|
-
|
|
2017
|
-
|
|
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({
|
|
2018
2607
|
input: process.stdin,
|
|
2019
|
-
output: process.stdout
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
}
|
|
2029
|
-
|
|
2030
|
-
}
|
|
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;
|
|
2031
2619
|
};
|
|
2032
2620
|
/** Chats with a node of a live run until the input ends or the run does. */
|
|
2033
2621
|
const chatLive = async (store, run, node, message) => {
|
|
@@ -2049,7 +2637,7 @@ const chatLive = async (store, run, node, message) => {
|
|
|
2049
2637
|
abort.abort();
|
|
2050
2638
|
});
|
|
2051
2639
|
for (;;) {
|
|
2052
|
-
const line = await Promise.race([askLine(), following.then(() => null)]);
|
|
2640
|
+
const line = await Promise.race([askLine(true, abort.signal), following.then(() => null)]);
|
|
2053
2641
|
if (line === null || abort.signal.aborted) break;
|
|
2054
2642
|
if (line.trim() !== "") await tellLiveRun(store, run, node, line);
|
|
2055
2643
|
}
|
|
@@ -2145,6 +2733,27 @@ const chat = defineCommand({
|
|
|
2145
2733
|
}
|
|
2146
2734
|
}
|
|
2147
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
|
+
};
|
|
2148
2757
|
// src/commands/decide.ts
|
|
2149
2758
|
/**
|
|
2150
2759
|
* @file `ork decide`: answer a live run's open decisions from any terminal (it
|
|
@@ -2190,8 +2799,6 @@ const saidLine = (store, id) => {
|
|
|
2190
2799
|
const note = store.decision(id)?.note;
|
|
2191
2800
|
return note === void 0 ? "" : style("dim", ` · said: ${note}`);
|
|
2192
2801
|
};
|
|
2193
|
-
/** One line per open decision, for `--list`. */
|
|
2194
|
-
const formatDecisions = (decisions, workflow) => decisions.length === 0 ? [workflow === void 0 ? "No open decisions." : `No open decisions in workflow ${workflow}.`] : decisions.map((d) => `${style("bold", d.id)} ${style("cyan", d.node)}${d.owner === void 0 ? "" : style("dim", ` via ${d.owner}`)} ${d.question} ${d.document === void 0 ? "" : `${style("dim", "+document")} `}${style("dim", `[${d.options.map((o) => o.id === d.recommended ? `${o.id}*` : o.id).join(", ")}]`)}`);
|
|
2195
2802
|
const decide = defineCommand({
|
|
2196
2803
|
meta: {
|
|
2197
2804
|
name: "decide",
|
|
@@ -2246,6 +2853,11 @@ const decide = defineCommand({
|
|
|
2246
2853
|
rating: {
|
|
2247
2854
|
type: "string",
|
|
2248
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
|
|
2249
2861
|
}
|
|
2250
2862
|
},
|
|
2251
2863
|
run: async ({ args }) => {
|
|
@@ -2258,7 +2870,8 @@ const decide = defineCommand({
|
|
|
2258
2870
|
const human = createTerminalHuman({
|
|
2259
2871
|
input: process.stdin,
|
|
2260
2872
|
output: process.stdout,
|
|
2261
|
-
interactive: process.stdin.isTTY
|
|
2873
|
+
interactive: process.stdin.isTTY,
|
|
2874
|
+
verbose: args.verbose
|
|
2262
2875
|
});
|
|
2263
2876
|
try {
|
|
2264
2877
|
if (args.document) {
|
|
@@ -2298,7 +2911,7 @@ const decide = defineCommand({
|
|
|
2298
2911
|
return;
|
|
2299
2912
|
}
|
|
2300
2913
|
if (args.say !== void 0) {
|
|
2301
|
-
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).");
|
|
2302
2915
|
return;
|
|
2303
2916
|
}
|
|
2304
2917
|
const filter = {};
|
|
@@ -2320,8 +2933,8 @@ const decide = defineCommand({
|
|
|
2320
2933
|
for (const line of formatDecisions(open, args.workflow)) write(line);
|
|
2321
2934
|
return;
|
|
2322
2935
|
}
|
|
2323
|
-
|
|
2324
|
-
|
|
2936
|
+
const collapses = canKey(process.stdin, process.stdout);
|
|
2937
|
+
for (const [index, d] of open.entries()) {
|
|
2325
2938
|
const question = {
|
|
2326
2939
|
nodeId: d.node,
|
|
2327
2940
|
question: d.question,
|
|
@@ -2334,9 +2947,16 @@ const decide = defineCommand({
|
|
|
2334
2947
|
question.onNote = (given) => {
|
|
2335
2948
|
note = given;
|
|
2336
2949
|
};
|
|
2337
|
-
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;
|
|
2338
2957
|
const answered = await answerDecision(store, d.id, choice, void 0, "decide", note);
|
|
2339
|
-
|
|
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`);
|
|
2340
2960
|
}
|
|
2341
2961
|
} finally {
|
|
2342
2962
|
human?.close();
|
|
@@ -2496,6 +3116,46 @@ const resume = defineCommand({
|
|
|
2496
3116
|
}
|
|
2497
3117
|
}
|
|
2498
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
|
+
};
|
|
2499
3159
|
// src/doctor/lease-checks.ts
|
|
2500
3160
|
/**
|
|
2501
3161
|
* @file What `ork doctor` says of Command Code leases (RFC-014): it removes the
|
|
@@ -2508,7 +3168,7 @@ const resume = defineCommand({
|
|
|
2508
3168
|
* folder. Says nothing when there were none. `bin` is the `cmd` program.
|
|
2509
3169
|
*/
|
|
2510
3170
|
const leaseChecks = async (place, env = process.env, bin = "cmd") => {
|
|
2511
|
-
const { cleanStaleLeases, leaseLedger } = await import("./src-
|
|
3171
|
+
const { cleanStaleLeases, leaseLedger } = await import("./src-BnZgZGrf.mjs");
|
|
2512
3172
|
const { cleaned, failed } = await cleanStaleLeases({
|
|
2513
3173
|
bin,
|
|
2514
3174
|
ledger: leaseLedger(placeFiles(place).runs),
|
|
@@ -2637,16 +3297,17 @@ const DESCRIBE = {
|
|
|
2637
3297
|
folder: (link) => `${link.path} is a real folder, left as it is`
|
|
2638
3298
|
};
|
|
2639
3299
|
const describe = (link) => DESCRIBE[link.state](link);
|
|
3300
|
+
/** Labels are made when used, so `--plain` (set after load) is read. */
|
|
2640
3301
|
const LABEL = {
|
|
2641
|
-
linked: style("dim", "ok "),
|
|
2642
|
-
missing: style("yellow", "missing"),
|
|
2643
|
-
broken: style("red", "broken "),
|
|
2644
|
-
elsewhere: style("yellow", "kept "),
|
|
2645
|
-
folder: style("yellow", "kept ")
|
|
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 ")
|
|
2646
3307
|
};
|
|
2647
3308
|
/** One line per folder; `created` marks the links this call made. */
|
|
2648
3309
|
const formatLinks = (links, created = []) => links.map((link) => {
|
|
2649
|
-
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)}`;
|
|
2650
3311
|
});
|
|
2651
3312
|
const checkOf = (tool, links) => {
|
|
2652
3313
|
const name = `${AGENT_TOOL_LAYOUTS.find((l) => l.id === tool)?.name ?? tool} links`;
|
|
@@ -2698,16 +3359,6 @@ const ORK_META = {
|
|
|
2698
3359
|
version: ORK_VERSION,
|
|
2699
3360
|
description: "Orchestrate Claude agents on your machine"
|
|
2700
3361
|
};
|
|
2701
|
-
const MARK = {
|
|
2702
|
-
ok: style("green", "✓"),
|
|
2703
|
-
warn: style("yellow", "!"),
|
|
2704
|
-
fail: style("red", "✗"),
|
|
2705
|
-
skip: style("dim", "-")
|
|
2706
|
-
};
|
|
2707
|
-
const formatCheck = (result) => {
|
|
2708
|
-
const line = `${MARK[result.status]} ${result.name.padEnd(20)} ${result.detail}`;
|
|
2709
|
-
return result.hint === void 0 ? [line] : [line, ` ${style("dim", result.hint)}`];
|
|
2710
|
-
};
|
|
2711
3362
|
const doctor = defineCommand({
|
|
2712
3363
|
meta: {
|
|
2713
3364
|
name: "doctor",
|
|
@@ -2765,7 +3416,7 @@ const agreed = (yes, command) => {
|
|
|
2765
3416
|
write(`Run it yourself: ${command}\nor run ork upgrade --yes.`);
|
|
2766
3417
|
return Promise.resolve(false);
|
|
2767
3418
|
}
|
|
2768
|
-
return confirm(`Run ${
|
|
3419
|
+
return confirm(`Run ${command}?`);
|
|
2769
3420
|
};
|
|
2770
3421
|
const upgrade = defineCommand({
|
|
2771
3422
|
meta: {
|
|
@@ -3146,10 +3797,25 @@ const relocateProject = (store, id, target) => {
|
|
|
3146
3797
|
created: false
|
|
3147
3798
|
};
|
|
3148
3799
|
};
|
|
3149
|
-
/**
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
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));
|
|
3153
3819
|
};
|
|
3154
3820
|
/** What `ork register` prints: the project, and where its files go. */
|
|
3155
3821
|
const formatRegistered = (project, created) => {
|
|
@@ -3378,7 +4044,7 @@ const migrateHere = async (cwd, options) => {
|
|
|
3378
4044
|
fail("Pass --yes to migrate without a terminal.");
|
|
3379
4045
|
return;
|
|
3380
4046
|
}
|
|
3381
|
-
if (!options.yes && !await confirm("Go ahead?
|
|
4047
|
+
if (!options.yes && !await confirm("Go ahead?")) return;
|
|
3382
4048
|
applyMigration(plan, store);
|
|
3383
4049
|
write(`${style("green", "✓")} migrated. ${afterMigration(plan)}`);
|
|
3384
4050
|
} finally {
|
|
@@ -3498,7 +4164,7 @@ const projects = defineCommand({
|
|
|
3498
4164
|
const all = withStore$1((store) => store.projects());
|
|
3499
4165
|
if (args.json) write(JSON.stringify(all, void 0, 2));
|
|
3500
4166
|
else if (all.length === 0) write("No projects registered yet: ork init (in-repo) or ork register (attached).");
|
|
3501
|
-
else for (const
|
|
4167
|
+
else for (const line of formatProjects(all)) write(line);
|
|
3502
4168
|
}
|
|
3503
4169
|
});
|
|
3504
4170
|
const migrate = defineCommand({
|
|
@@ -3592,13 +4258,16 @@ const SEVERITY_STYLE = {
|
|
|
3592
4258
|
};
|
|
3593
4259
|
const about = (item) => `${item.target}/${item.kind}`;
|
|
3594
4260
|
/** One line per item, most recently seen first. */
|
|
3595
|
-
const formatFeedbackItems = (items, now = Date.now()) => {
|
|
4261
|
+
const formatFeedbackItems = (items, now = Date.now(), term = termOf(process.stdout)) => {
|
|
3596
4262
|
if (items.length === 0) return ["No feedback yet."];
|
|
3597
4263
|
const width = Math.max(...items.map((i) => about(i).length));
|
|
3598
4264
|
return items.map((item) => {
|
|
3599
|
-
const
|
|
4265
|
+
const mark = item.status === "new" ? "" : term.rich ? ` ${chip(term, item.status.toUpperCase(), item.status === "dismissed" ? "muted" : "accent")}` : ` ${style("dim", `[${item.status}]`)}`;
|
|
3600
4266
|
const times = `${String(item.count)}×`.padStart(4);
|
|
3601
|
-
|
|
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}`;
|
|
3602
4271
|
});
|
|
3603
4272
|
};
|
|
3604
4273
|
const plural = (n, word) => `${String(n)} ${word}${n === 1 ? "" : "s"}`;
|
|
@@ -4365,7 +5034,7 @@ const ps = defineCommand({
|
|
|
4365
5034
|
limit: Number.isInteger(limit) && limit > 0 ? limit : 20,
|
|
4366
5035
|
...args.all ? {} : { project: placeFor(process.cwd(), store.projects()).root }
|
|
4367
5036
|
});
|
|
4368
|
-
const statusOf = (run) =>
|
|
5037
|
+
const statusOf = (run) => listedStatus(run, run.status === "running" ? store.events(run.id).map((s) => s.event) : []);
|
|
4369
5038
|
if (args.json) write(JSON.stringify(runs, void 0, 2));
|
|
4370
5039
|
else for (const line of formatRuns(runs, args.all, Date.now(), statusOf)) write(line);
|
|
4371
5040
|
})
|
|
@@ -4396,6 +5065,11 @@ const logs = defineCommand({
|
|
|
4396
5065
|
type: "boolean",
|
|
4397
5066
|
description: "Print the stored events as JSON lines",
|
|
4398
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
|
|
4399
5073
|
}
|
|
4400
5074
|
},
|
|
4401
5075
|
run: ({ args }) => withStore(async (store) => {
|
|
@@ -4418,6 +5092,7 @@ const logs = defineCommand({
|
|
|
4418
5092
|
await followRun(store, found.run, write, {
|
|
4419
5093
|
follow: args.follow,
|
|
4420
5094
|
json: args.json,
|
|
5095
|
+
verbose: args.verbose,
|
|
4421
5096
|
signal: abort.signal,
|
|
4422
5097
|
...args.node === void 0 ? {} : { node: args.node }
|
|
4423
5098
|
});
|
|
@@ -4504,7 +5179,7 @@ const addAgent = (options) => {
|
|
|
4504
5179
|
* doesn't, and otherwise it asks on a terminal, once, saying the plugin has
|
|
4505
5180
|
* them too, or says how to add them later.
|
|
4506
5181
|
*/
|
|
4507
|
-
const QUESTION = "Add Ork's skills for Claude Code to .ork/skills/, so it writes workflows Ork accepts? Ork's Claude Code plugin has them too.
|
|
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.";
|
|
4508
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).";
|
|
4509
5184
|
/** Whether to write the skills: as asked, or the person's answer. */
|
|
4510
5185
|
const wanted = (root, asked) => {
|
|
@@ -4775,27 +5450,27 @@ const main = defineCommand({
|
|
|
4775
5450
|
ps,
|
|
4776
5451
|
logs,
|
|
4777
5452
|
tree,
|
|
4778
|
-
usage: () => import("./usage-
|
|
4779
|
-
prune: () => import("./usage-
|
|
5453
|
+
usage: () => import("./usage-GceBGMjU.mjs").then((module) => module.usage),
|
|
5454
|
+
prune: () => import("./usage-GceBGMjU.mjs").then((module) => module.prune),
|
|
4780
5455
|
decide,
|
|
4781
|
-
relay: () => import("./relay-
|
|
4782
|
-
pause: () => import("./pause-
|
|
4783
|
-
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),
|
|
4784
5459
|
stop,
|
|
4785
5460
|
chat,
|
|
4786
|
-
send: () => import("./send-
|
|
5461
|
+
send: () => import("./send-CLs_fyg7.mjs").then((module) => module.send),
|
|
4787
5462
|
resume,
|
|
4788
5463
|
feedback,
|
|
4789
5464
|
context,
|
|
4790
|
-
worktrees: () => import("./worktrees-
|
|
4791
|
-
wt: () => import("./worktrees-
|
|
4792
|
-
watch: () => import("./watch-
|
|
4793
|
-
mcp: () => import("./mcp-
|
|
4794
|
-
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),
|
|
4795
5470
|
doctor,
|
|
4796
5471
|
upgrade,
|
|
4797
|
-
completion: () => import("./completion-
|
|
4798
|
-
__complete: () => import("./completion-
|
|
5472
|
+
completion: () => import("./completion-C5D0sEs5.mjs").then((module) => module.completion),
|
|
5473
|
+
__complete: () => import("./completion-C5D0sEs5.mjs").then((module) => module.completeCommand(main))
|
|
4799
5474
|
}
|
|
4800
5475
|
});
|
|
4801
5476
|
// src/bin.ts
|
|
@@ -4805,15 +5480,16 @@ const main = defineCommand({
|
|
|
4805
5480
|
* starts the daily background check and ends with the notice (ADR-038); the
|
|
4806
5481
|
* background copy it starts only runs the check. It first clears git's local
|
|
4807
5482
|
* variables (a hook's `GIT_DIR`, `GIT_INDEX_FILE`, …), so nothing Ork starts
|
|
4808
|
-
* acts on the hook's repository (specs/worktrees.md). `--
|
|
4809
|
-
*
|
|
4810
|
-
*
|
|
4811
|
-
*
|
|
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.
|
|
4812
5488
|
*/
|
|
4813
5489
|
clearGitLocalEnv();
|
|
4814
|
-
const argv = process.argv.slice(2);
|
|
5490
|
+
const argv = takePlainFlag(process.argv.slice(2));
|
|
4815
5491
|
if (argv[0] === "__complete") {
|
|
4816
|
-
const { printCompletions } = await import("./completion-
|
|
5492
|
+
const { printCompletions } = await import("./completion-C5D0sEs5.mjs");
|
|
4817
5493
|
await printCompletions(main, argv.slice(1));
|
|
4818
5494
|
} else if (process.env["ORK_UPDATE_CHECK_CHILD"] === "1") await runUpdateChild();
|
|
4819
5495
|
else {
|
|
@@ -4823,6 +5499,9 @@ else {
|
|
|
4823
5499
|
showUpdateNotice();
|
|
4824
5500
|
});
|
|
4825
5501
|
}
|
|
4826
|
-
await runMain(main, {
|
|
5502
|
+
await runMain(main, {
|
|
5503
|
+
rawArgs: argv,
|
|
5504
|
+
showUsage: showHelp
|
|
5505
|
+
});
|
|
4827
5506
|
}
|
|
4828
5507
|
export {};
|