@krapht/ork 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/dist/__build_info__-CtSVVuZE.mjs +691 -0
- package/dist/bin.mjs +1654 -3532
- package/dist/clean-Dcr807be.mjs +88 -0
- package/dist/context-C_bksb0C.mjs +351 -0
- package/dist/find-DptmbhGd.mjs +47 -0
- package/dist/format-J-a21Yvn.mjs +393 -0
- package/dist/in-a-run-D1Qc5Mit.mjs +22 -0
- package/dist/limits-BDZCmGEu.mjs +44 -0
- package/dist/list-ZvY2OcLq.mjs +110 -0
- package/dist/mcp-CHVKKg3B.mjs +91 -0
- package/dist/output-CXSJO00X.mjs +900 -0
- package/dist/pause-BPSmg4yz.mjs +65 -0
- package/dist/relay-Do-aNbSD.mjs +122 -0
- package/dist/remove-AwaMMaHf.mjs +194 -0
- package/dist/report-DK4LGqwF.mjs +129 -0
- package/dist/send-DhGBkowy.mjs +163 -0
- package/dist/{src-DAsOYtoR.mjs → src-BHnxpJyH.mjs} +496 -193
- package/dist/src-CGqMati3.mjs +12505 -0
- package/dist/src-CpDKIulI.mjs +3118 -0
- package/dist/src-UWkk1sl9.mjs +1899 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +631 -0
- package/dist/ui/assets/app-BgVDC_5l.js +67 -0
- package/dist/ui/assets/app-DVf68fj5.css +1 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-C51Di1Mf.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-CPJFfJgk.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-D2cXvDHF.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-DfuvdVgn.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-CkxHOdCb.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-XB2BPmDK.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-BY1KTvO5.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-C7aYOB1D.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-B2yvC1Cq.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-D-GG86Jb.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-CfzLURNc.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-DI-rJ0UV.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-Buglb9-a.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-CrWEgvU_.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-BS9BGVxz.woff2 +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-DjbiTEEA.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-CmnCPGa0.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-Q_ksOahU.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CKEaXIlx.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CZEPLOgu.woff +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Bs29RrUZ.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Dx9-epDo.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-BOaIZNA2.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-gapTbOY8.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-CN2lyvyL.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-uokXdC-Q.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-CA1yjETN.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-DFOURf8L.woff2 +0 -0
- package/dist/ui/assets/index-BiO54L90.css +1 -0
- package/dist/ui/assets/index-MQNNgsyT.js +10 -0
- package/dist/ui/assets/theme-3xH5jxKs.js +1 -0
- package/dist/ui/favicon.ico +0 -0
- package/dist/ui/favicon.svg +4 -0
- package/dist/ui/index.html +30 -0
- package/dist/ui/ork-wordmark-small-on-light.svg +4 -0
- package/dist/ui/ork-wordmark-small.svg +4 -0
- package/dist/ui-DhutUMgQ.mjs +472 -0
- package/dist/usage-CT7Z-yjr.mjs +419 -0
- package/dist/watch-0Qr2UfCQ.mjs +394 -0
- package/dist/worktrees-DHl80k96.mjs +212 -0
- package/package.json +6 -4
- package/dist/src-CJ_zF0Zj.mjs +0 -5433
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { H as requestContinue, U as requestPause } from "./src-CGqMati3.mjs";
|
|
2
|
+
import { t as findRun } from "./find-DptmbhGd.mjs";
|
|
3
|
+
import { a as write, c as openStore, i as style, o as writeErr } from "./output-CXSJO00X.mjs";
|
|
4
|
+
import { r as continueRefusal } from "./in-a-run-D1Qc5Mit.mjs";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
// src/commands/pause.ts
|
|
7
|
+
/**
|
|
8
|
+
* @file `ork pause` and `ork continue`: pause a live run, or one of its nodes,
|
|
9
|
+
* from any terminal, and let it go on (ADR-041). Nothing new starts while a
|
|
10
|
+
* run is paused, and a running session waits before its next tool call;
|
|
11
|
+
* nothing it is doing is cut short. Both write to the store, then wake the
|
|
12
|
+
* run on its socket (specs/messaging.md).
|
|
13
|
+
*/
|
|
14
|
+
const args = {
|
|
15
|
+
run: {
|
|
16
|
+
type: "positional",
|
|
17
|
+
description: "A run id or a prefix (default: this project's newest)",
|
|
18
|
+
required: false
|
|
19
|
+
},
|
|
20
|
+
node: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Only this node, not the whole run"
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** Finds the run, asks it, and says what happens next. */
|
|
26
|
+
const ask = async (runArg, request, done) => {
|
|
27
|
+
const store = openStore();
|
|
28
|
+
try {
|
|
29
|
+
const found = findRun(store, runArg, process.cwd());
|
|
30
|
+
const asked = found.ok ? await request(store, found.run) : found;
|
|
31
|
+
if (!asked.ok) {
|
|
32
|
+
writeErr(asked.error);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (found.ok) write(`${style("green", "✓")} ${done(found.run)}`);
|
|
37
|
+
} finally {
|
|
38
|
+
store.close();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const pause = defineCommand({
|
|
42
|
+
meta: {
|
|
43
|
+
name: "pause",
|
|
44
|
+
description: "Pause a live run, or one node, before its next tool call"
|
|
45
|
+
},
|
|
46
|
+
args,
|
|
47
|
+
run: ({ args: given }) => ask(given.run, (store, run) => requestPause(store, run, void 0, given.node), (run) => given.node === void 0 ? `Run ${run.id} pauses: nothing new starts, and running sessions wait before their next tool call. Go on with: ork continue ${run.id}` : `${given.node} in run ${run.id} pauses before its next tool call. Go on with: ork continue ${run.id} --node ${given.node}`)
|
|
48
|
+
});
|
|
49
|
+
const continueRun = defineCommand({
|
|
50
|
+
meta: {
|
|
51
|
+
name: "continue",
|
|
52
|
+
description: "Let a paused run, or one paused node, go on"
|
|
53
|
+
},
|
|
54
|
+
args,
|
|
55
|
+
run: async ({ args: given }) => {
|
|
56
|
+
const refused = continueRefusal();
|
|
57
|
+
if (refused !== null) {
|
|
58
|
+
writeErr(refused);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await ask(given.run, (store, run) => requestContinue(store, run, void 0, given.node), (run) => given.node === void 0 ? `Run ${run.id} goes on.` : `${given.node} in run ${run.id} goes on.`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
export { continueRun, pause };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { B as unlinkNodes, L as linkNodes, P as addressOf } from "./src-CGqMati3.mjs";
|
|
2
|
+
import { a as write, c as openStore, i as style, o as writeErr } from "./output-CXSJO00X.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
// src/commands/relay.ts
|
|
5
|
+
/**
|
|
6
|
+
* @file `ork relay`: introduce two running nodes of different runs so they can
|
|
7
|
+
* message each other, end that, and list what is linked (RFC-003,
|
|
8
|
+
* specs/messaging.md). Each side's session is told at its next step, through
|
|
9
|
+
* the store and its run's socket. Not `ork link`, which points tools' folders
|
|
10
|
+
* at `.ork/`. A node's address always has a slash, so `end` and `list` cannot
|
|
11
|
+
* be taken for one.
|
|
12
|
+
*/
|
|
13
|
+
const relayAction = (first, second, third) => {
|
|
14
|
+
if (first === "list") return second === void 0 ? { kind: "list" } : {
|
|
15
|
+
kind: "list",
|
|
16
|
+
run: second
|
|
17
|
+
};
|
|
18
|
+
if (first === "end") return second === void 0 || third === void 0 ? { kind: "usage" } : {
|
|
19
|
+
kind: "end",
|
|
20
|
+
a: second,
|
|
21
|
+
b: third
|
|
22
|
+
};
|
|
23
|
+
return first === void 0 || second === void 0 ? { kind: "usage" } : {
|
|
24
|
+
kind: "link",
|
|
25
|
+
a: first,
|
|
26
|
+
b: second
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const USAGE = `Name two nodes of different runs as <run>/<node>:
|
|
30
|
+
ork relay <a> <b> let them message each other
|
|
31
|
+
ork relay end <a> <b> end that
|
|
32
|
+
ork relay list [run] list what is linked`;
|
|
33
|
+
/** The link's two ends, its count, and whether it is over. */
|
|
34
|
+
const formatLink = (link) => `${addressOf(link.a)} <-> ${addressOf(link.b)} ${String(link.messages)}/${String(link.maxMessages)} messages${link.endedAt === void 0 ? "" : style("dim", " (ended)")}`;
|
|
35
|
+
const refuse = (error) => {
|
|
36
|
+
writeErr(error);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
};
|
|
39
|
+
/** A whole number above zero, `undefined` when not given, `null` when wrong. */
|
|
40
|
+
const maxMessagesOf = (given) => {
|
|
41
|
+
if (given === void 0) return void 0;
|
|
42
|
+
const limit = Number(given);
|
|
43
|
+
return Number.isInteger(limit) && limit > 0 ? limit : null;
|
|
44
|
+
};
|
|
45
|
+
const relay = defineCommand({
|
|
46
|
+
meta: {
|
|
47
|
+
name: "relay",
|
|
48
|
+
description: "Let two running nodes of different runs message each other; `end` and `list` too"
|
|
49
|
+
},
|
|
50
|
+
args: {
|
|
51
|
+
first: {
|
|
52
|
+
type: "positional",
|
|
53
|
+
description: "A node as <run>/<node>, or `end`, or `list`",
|
|
54
|
+
required: false
|
|
55
|
+
},
|
|
56
|
+
second: {
|
|
57
|
+
type: "positional",
|
|
58
|
+
description: "The other node",
|
|
59
|
+
required: false
|
|
60
|
+
},
|
|
61
|
+
third: {
|
|
62
|
+
type: "positional",
|
|
63
|
+
description: "With `end`: the other node",
|
|
64
|
+
required: false
|
|
65
|
+
},
|
|
66
|
+
"max-messages": {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: `Ask you whether to go on after this many messages (default ${String(100)})`
|
|
69
|
+
},
|
|
70
|
+
all: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: "With `list`: include links that have ended",
|
|
73
|
+
default: false
|
|
74
|
+
},
|
|
75
|
+
json: {
|
|
76
|
+
type: "boolean",
|
|
77
|
+
description: "With `list`: print JSON",
|
|
78
|
+
default: false
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
run: async ({ args }) => {
|
|
82
|
+
const action = relayAction(args.first, args.second, args.third);
|
|
83
|
+
if (action.kind === "usage") {
|
|
84
|
+
refuse(USAGE);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const store = openStore();
|
|
88
|
+
try {
|
|
89
|
+
if (action.kind === "list") {
|
|
90
|
+
const found = store.links({
|
|
91
|
+
...action.run === void 0 ? {} : { run: action.run },
|
|
92
|
+
live: !args.all
|
|
93
|
+
});
|
|
94
|
+
if (args.json) write(JSON.stringify(found));
|
|
95
|
+
else if (found.length === 0) write("No links.");
|
|
96
|
+
else for (const one of found) write(formatLink(one));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (action.kind === "end") {
|
|
100
|
+
const ended = await unlinkNodes(store, action.a, action.b);
|
|
101
|
+
if (ended.ok) write(`${style("green", "✓")} Ended the link. Both are told.`);
|
|
102
|
+
else refuse(ended.error);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const maxMessages = maxMessagesOf(args["max-messages"]);
|
|
106
|
+
if (maxMessages === null) {
|
|
107
|
+
refuse("--max-messages takes a whole number above zero.");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const linked = await linkNodes(store, action.a, action.b, { maxMessages });
|
|
111
|
+
if (!linked.ok) {
|
|
112
|
+
refuse(linked.error);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const [a, b] = [addressOf(linked.link.a), addressOf(linked.link.b)];
|
|
116
|
+
write(linked.already ? `${a} and ${b} are already linked.` : `${style("green", "✓")} Linked ${a} and ${b}. Both are told at their next step.`);
|
|
117
|
+
} finally {
|
|
118
|
+
store.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
export { relay };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { D as git, k as lines, lt as liveStatus } from "./src-CGqMati3.mjs";
|
|
2
|
+
import { n as listOrkWorktrees, t as createdWorktrees } from "./list-ZvY2OcLq.mjs";
|
|
3
|
+
import { i as keyOf, n as applyStep, r as cleanStep, s as stepPrunes } from "./clean-Dcr807be.mjs";
|
|
4
|
+
// ../core/src/worktree/words.ts
|
|
5
|
+
/**
|
|
6
|
+
* @file The plain words for what `ork worktrees` shows, in one place, so the
|
|
7
|
+
* CLI and the Worktrees panel say the same thing: an item's name, its merge
|
|
8
|
+
* state, its state as parts (each marked when it's work that removing would
|
|
9
|
+
* lose), a plan's line, and why an item can't be removed. The CLI colors
|
|
10
|
+
* them; the panel lays them out.
|
|
11
|
+
*/
|
|
12
|
+
/** An item's name: its branch, or its run and node when created detached. */
|
|
13
|
+
const labelOf = (item) => item.branch ?? `${item.runId}/${item.nodeId}`;
|
|
14
|
+
/** Merged into the current branch, not, or detached on no branch at all. */
|
|
15
|
+
const mergeStateOf = (item) => {
|
|
16
|
+
if (item.branch === void 0 && item.checkedOut === void 0) return "detached";
|
|
17
|
+
return item.merged ? "merged" : "unmerged";
|
|
18
|
+
};
|
|
19
|
+
/** `3 commits`, `1 commit`. */
|
|
20
|
+
const counted = (count, noun) => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
21
|
+
/**
|
|
22
|
+
* An item's state, in order: where its worktree is when it isn't plain, the
|
|
23
|
+
* workflow's own branch, uncommitted changes, unmerged or unreachable commits,
|
|
24
|
+
* and the main checkout or another worktree holding the branch. `into` names
|
|
25
|
+
* the branch "not merged" is measured against, when the words should say it.
|
|
26
|
+
*/
|
|
27
|
+
const stateParts = (item, into) => {
|
|
28
|
+
const parts = [];
|
|
29
|
+
const part = (text, risk = false) => {
|
|
30
|
+
parts.push({
|
|
31
|
+
text,
|
|
32
|
+
risk
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
if (item.path === null) part("no worktree");
|
|
36
|
+
else if (item.missing) part("missing (prunable)");
|
|
37
|
+
if (item.checkedOut !== void 0) part(`on the workflow's branch ${item.checkedOut}`);
|
|
38
|
+
if (item.dirty) part("uncommitted changes", true);
|
|
39
|
+
const state = mergeStateOf(item);
|
|
40
|
+
if (state === "unmerged") {
|
|
41
|
+
const where = into === void 0 ? "" : ` into ${into}`;
|
|
42
|
+
part(`${counted(item.ahead, "commit")} not merged${where}`);
|
|
43
|
+
}
|
|
44
|
+
if (state === "detached") {
|
|
45
|
+
if (item.ahead > 0) part(`${counted(item.ahead, "commit")} on no branch`, true);
|
|
46
|
+
else if (item.path !== null && !item.missing) part("nothing committed");
|
|
47
|
+
}
|
|
48
|
+
if (item.inMainCheckout) part("checked out in the main checkout");
|
|
49
|
+
if (item.heldBy !== void 0) part(`checked out in ${item.heldBy}`);
|
|
50
|
+
return parts;
|
|
51
|
+
};
|
|
52
|
+
/** What a step does, as a plan says it. */
|
|
53
|
+
const planWhat = (step) => {
|
|
54
|
+
const actions = [
|
|
55
|
+
...step.removeWorktree ? ["remove worktree"] : [],
|
|
56
|
+
...stepPrunes(step) && !step.removeWorktree ? ["prune worktree"] : [],
|
|
57
|
+
...step.deleteBranch ? ["delete branch"] : []
|
|
58
|
+
];
|
|
59
|
+
return actions.length > 0 ? actions.join(" + ") : "keep";
|
|
60
|
+
};
|
|
61
|
+
/** `ork/r1/lead: remove worktree`, `r1/check (detached): keep (…)`. */
|
|
62
|
+
const planLine = (step) => {
|
|
63
|
+
const { item } = step;
|
|
64
|
+
return {
|
|
65
|
+
text: `${item.branch === void 0 && item.checkedOut === void 0 ? `${labelOf(item)} (detached)` : labelOf(item)}: ${planWhat(step)}`,
|
|
66
|
+
...step.skip === void 0 ? {} : { reason: step.skip }
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Why an item can't be removed at all, even forced, if it can't: the main
|
|
71
|
+
* checkout or a worktree Ork didn't create holds it, or its run is still
|
|
72
|
+
* going.
|
|
73
|
+
*/
|
|
74
|
+
const removeRefusal = (item, live) => {
|
|
75
|
+
if (item.inMainCheckout) return "checked out in the main checkout; switch it to another branch first";
|
|
76
|
+
if (item.heldBy !== void 0) return `checked out in ${item.heldBy}, a worktree Ork didn't create; switch it to another branch first`;
|
|
77
|
+
return live ? `run ${item.runId} is still going; stop it first` : void 0;
|
|
78
|
+
};
|
|
79
|
+
// ../core/src/worktree/remove.ts
|
|
80
|
+
/** The ids of the project's runs whose process is still going. */
|
|
81
|
+
const liveRunIds = (store, root) => new Set(store.runs({ project: root }).filter((run) => liveStatus(run) === "running").map((run) => run.id));
|
|
82
|
+
/**
|
|
83
|
+
* Whether `root` is a repository with a commit, and the branch checked out
|
|
84
|
+
* there, which "merged" means merged into (null when its HEAD is detached).
|
|
85
|
+
*/
|
|
86
|
+
const projectHead = async (root) => {
|
|
87
|
+
if ((await git(root, [
|
|
88
|
+
"rev-parse",
|
|
89
|
+
"--verify",
|
|
90
|
+
"--quiet",
|
|
91
|
+
"HEAD"
|
|
92
|
+
])).code !== 0) return { ok: false };
|
|
93
|
+
const answer = await git(root, [
|
|
94
|
+
"symbolic-ref",
|
|
95
|
+
"--short",
|
|
96
|
+
"-q",
|
|
97
|
+
"HEAD"
|
|
98
|
+
]);
|
|
99
|
+
return {
|
|
100
|
+
ok: true,
|
|
101
|
+
branch: answer.code === 0 ? answer.stdout.trim() : null
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
/** What Ork created in the repo at `root`, from its runs' records and git. */
|
|
105
|
+
const readWorktrees = async (store, root) => ({
|
|
106
|
+
items: await listOrkWorktrees(root, createdWorktrees(store, root)),
|
|
107
|
+
live: liveRunIds(store, root)
|
|
108
|
+
});
|
|
109
|
+
/**
|
|
110
|
+
* The plan to remove one item, named by `keyOf`. A branch with no worktree is
|
|
111
|
+
* removed by deleting it, so `deleteBranch` is implied there.
|
|
112
|
+
*/
|
|
113
|
+
const planRemove = ({ items, live }, key, deleteBranch) => {
|
|
114
|
+
const item = items.find((i) => keyOf(i) === key);
|
|
115
|
+
if (item === void 0) return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: `No Ork worktree or branch ${key}.`,
|
|
118
|
+
gone: true
|
|
119
|
+
};
|
|
120
|
+
const refusal = removeRefusal(item, live.has(item.runId));
|
|
121
|
+
if (refusal !== void 0) return {
|
|
122
|
+
ok: false,
|
|
123
|
+
error: `Can't remove ${labelOf(item)}: ${refusal}.`,
|
|
124
|
+
gone: false
|
|
125
|
+
};
|
|
126
|
+
const branches = item.branch !== void 0 && (deleteBranch || item.path === null);
|
|
127
|
+
const careful = cleanStep(item, {
|
|
128
|
+
branches,
|
|
129
|
+
force: false,
|
|
130
|
+
live
|
|
131
|
+
});
|
|
132
|
+
if (careful.skip === void 0) return {
|
|
133
|
+
ok: true,
|
|
134
|
+
plan: {
|
|
135
|
+
step: careful,
|
|
136
|
+
force: false
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
ok: true,
|
|
141
|
+
plan: {
|
|
142
|
+
step: cleanStep(item, {
|
|
143
|
+
branches,
|
|
144
|
+
force: true,
|
|
145
|
+
live
|
|
146
|
+
}),
|
|
147
|
+
force: true
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
const out = async (cwd, args) => lines((await git(cwd, args)).stdout);
|
|
152
|
+
/** What carrying out `plan` would lose, read from git. */
|
|
153
|
+
const lostWork = async (root, { step }) => {
|
|
154
|
+
const { item } = step;
|
|
155
|
+
const inside = item.path !== null && !item.missing ? item.path : null;
|
|
156
|
+
const changes = inside !== null && item.dirty && step.removeWorktree ? await out(inside, ["status", "--porcelain"]) : [];
|
|
157
|
+
if (step.deleteBranch && item.branch !== void 0 && !item.merged) return {
|
|
158
|
+
changes,
|
|
159
|
+
commits: await out(root, [
|
|
160
|
+
"log",
|
|
161
|
+
"--oneline",
|
|
162
|
+
`HEAD..${item.branch}`
|
|
163
|
+
]),
|
|
164
|
+
commitsOn: "unmerged"
|
|
165
|
+
};
|
|
166
|
+
if (item.branch === void 0 && item.checkedOut === void 0 && inside !== null && item.ahead > 0 && step.removeWorktree) return {
|
|
167
|
+
changes,
|
|
168
|
+
commits: await out(inside, [
|
|
169
|
+
"log",
|
|
170
|
+
"--oneline",
|
|
171
|
+
"HEAD",
|
|
172
|
+
"--not",
|
|
173
|
+
"--branches",
|
|
174
|
+
"--tags",
|
|
175
|
+
"--remotes"
|
|
176
|
+
]),
|
|
177
|
+
commitsOn: "no branch"
|
|
178
|
+
};
|
|
179
|
+
return {
|
|
180
|
+
changes,
|
|
181
|
+
commits: [],
|
|
182
|
+
commitsOn: null
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Carries out a remove plan; what it did goes into `done` as it goes. The
|
|
187
|
+
* caller has checked that a plan needing `force` was asked to force. It touches
|
|
188
|
+
* only its own item, as each of clean's steps does.
|
|
189
|
+
*/
|
|
190
|
+
const applyRemove = async (root, { step, force }, done = []) => {
|
|
191
|
+
await applyStep(root, step, force, done);
|
|
192
|
+
return done;
|
|
193
|
+
};
|
|
194
|
+
export { readWorktrees as a, planLine as c, projectHead as i, removeRefusal as l, lostWork as n, labelOf as o, planRemove as r, mergeStateOf as s, applyRemove as t, stateParts as u };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// ../core/src/usage/report.ts
|
|
2
|
+
const USAGE_DIMENSIONS = [
|
|
3
|
+
"day",
|
|
4
|
+
"week",
|
|
5
|
+
"month",
|
|
6
|
+
"workflow",
|
|
7
|
+
"node",
|
|
8
|
+
"model",
|
|
9
|
+
"driver",
|
|
10
|
+
"project"
|
|
11
|
+
];
|
|
12
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
13
|
+
/** `2026-11-03`, in the machine's own time zone. */
|
|
14
|
+
const dayOf = (date) => `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
15
|
+
/** The Monday of a time's week. */
|
|
16
|
+
const weekOf = (date) => {
|
|
17
|
+
const monday = new Date(date);
|
|
18
|
+
monday.setDate(date.getDate() - (date.getDay() + 6) % 7);
|
|
19
|
+
return dayOf(monday);
|
|
20
|
+
};
|
|
21
|
+
const KEYS = {
|
|
22
|
+
day: (row) => dayOf(new Date(row.at)),
|
|
23
|
+
week: (row) => weekOf(new Date(row.at)),
|
|
24
|
+
month: (row) => dayOf(new Date(row.at)).slice(0, 7),
|
|
25
|
+
workflow: (row) => row.workflow,
|
|
26
|
+
node: (row) => `${row.workflow}/${row.template ?? row.node}`,
|
|
27
|
+
model: (row) => row.model ?? "unknown",
|
|
28
|
+
driver: (row) => row.driver ?? "unknown",
|
|
29
|
+
project: (row) => row.project
|
|
30
|
+
};
|
|
31
|
+
const TIMES = /* @__PURE__ */ new Set([
|
|
32
|
+
"day",
|
|
33
|
+
"week",
|
|
34
|
+
"month"
|
|
35
|
+
]);
|
|
36
|
+
const totalsOf = (rows) => {
|
|
37
|
+
const totals = {
|
|
38
|
+
costUsd: 0,
|
|
39
|
+
inputTokens: 0,
|
|
40
|
+
outputTokens: 0,
|
|
41
|
+
cacheReadTokens: 0,
|
|
42
|
+
cacheWriteTokens: 0,
|
|
43
|
+
turns: 0,
|
|
44
|
+
durationMs: 0,
|
|
45
|
+
sessions: rows.length,
|
|
46
|
+
runs: new Set(rows.map((row) => row.runId)).size,
|
|
47
|
+
failedUsd: 0,
|
|
48
|
+
reworkUsd: 0,
|
|
49
|
+
chatUsd: 0,
|
|
50
|
+
cacheHitRate: 0
|
|
51
|
+
};
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
totals.costUsd += row.costUsd;
|
|
54
|
+
totals.inputTokens += row.inputTokens;
|
|
55
|
+
totals.outputTokens += row.outputTokens;
|
|
56
|
+
totals.cacheReadTokens += row.cacheReadTokens;
|
|
57
|
+
totals.cacheWriteTokens += row.cacheWriteTokens;
|
|
58
|
+
totals.turns += row.turns;
|
|
59
|
+
totals.durationMs += row.durationMs;
|
|
60
|
+
if (row.failed) totals.failedUsd += row.costUsd;
|
|
61
|
+
if (row.kind === "chat") totals.chatUsd += row.costUsd;
|
|
62
|
+
else if (row.round > 1) totals.reworkUsd += row.costUsd;
|
|
63
|
+
}
|
|
64
|
+
const read = totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens;
|
|
65
|
+
totals.cacheHitRate = read === 0 ? 0 : totals.cacheReadTokens / read;
|
|
66
|
+
return totals;
|
|
67
|
+
};
|
|
68
|
+
const groupBy = (rows, dimension) => {
|
|
69
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
const key = KEYS[dimension](row);
|
|
72
|
+
const bucket = buckets.get(key);
|
|
73
|
+
if (bucket === void 0) buckets.set(key, [row]);
|
|
74
|
+
else bucket.push(row);
|
|
75
|
+
}
|
|
76
|
+
const groups = [...buckets].map(([key, bucket]) => Object.assign(totalsOf(bucket), { key }));
|
|
77
|
+
return TIMES.has(dimension) ? groups.toSorted((a, b) => a.key.localeCompare(b.key)) : groups.toSorted((a, b) => b.costUsd - a.costUsd);
|
|
78
|
+
};
|
|
79
|
+
/** Below this, a percentage says nothing worth acting on. */
|
|
80
|
+
const MIN_SPEND_USD = .5;
|
|
81
|
+
const REWORK_SHARE = .2;
|
|
82
|
+
const FAILED_SHARE = .1;
|
|
83
|
+
const CONCENTRATED_SHARE = .5;
|
|
84
|
+
const LOW_CACHE_RATE = .5;
|
|
85
|
+
const MIN_CACHE_TOKENS = 1e5;
|
|
86
|
+
const percent = (share) => `${String(Math.round(share * 100))}%`;
|
|
87
|
+
const hintsOf = (totals, nodes) => {
|
|
88
|
+
const spend = totals.costUsd;
|
|
89
|
+
if (spend < MIN_SPEND_USD) return [];
|
|
90
|
+
const hints = [];
|
|
91
|
+
if (totals.reworkUsd / spend >= REWORK_SHARE) hints.push({
|
|
92
|
+
code: "rework",
|
|
93
|
+
message: `${percent(totals.reworkUsd / spend)} of the spend went to loop rounds after the first. Sharper briefs or an earlier check can stop work coming back.`
|
|
94
|
+
});
|
|
95
|
+
if (totals.failedUsd / spend >= FAILED_SHARE) hints.push({
|
|
96
|
+
code: "failed",
|
|
97
|
+
message: `${percent(totals.failedUsd / spend)} of the spend went to sessions that failed. A cheap check before them, or a lower budget per session, can stop them sooner.`
|
|
98
|
+
});
|
|
99
|
+
const top = nodes[0];
|
|
100
|
+
if (top !== void 0 && nodes.length > 1 && top.costUsd / spend >= CONCENTRATED_SHARE) hints.push({
|
|
101
|
+
code: "concentrated",
|
|
102
|
+
message: `${top.key} took ${percent(top.costUsd / spend)} of the spend. It is the first place to try a smaller model, a lower effort or a tighter brief.`
|
|
103
|
+
});
|
|
104
|
+
if (totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens >= MIN_CACHE_TOKENS && totals.cacheHitRate < LOW_CACHE_RATE) hints.push({
|
|
105
|
+
code: "cache",
|
|
106
|
+
message: `Only ${percent(totals.cacheHitRate)} of input tokens came from the prompt cache. Long runs of fresh sessions cost more than resumed ones.`
|
|
107
|
+
});
|
|
108
|
+
return hints;
|
|
109
|
+
};
|
|
110
|
+
/** Everything a report shows, from the ledger's rows. */
|
|
111
|
+
const usageReport = (rows) => {
|
|
112
|
+
const totals = totalsOf(rows);
|
|
113
|
+
const groups = {
|
|
114
|
+
day: groupBy(rows, "day"),
|
|
115
|
+
week: groupBy(rows, "week"),
|
|
116
|
+
month: groupBy(rows, "month"),
|
|
117
|
+
workflow: groupBy(rows, "workflow"),
|
|
118
|
+
node: groupBy(rows, "node"),
|
|
119
|
+
model: groupBy(rows, "model"),
|
|
120
|
+
driver: groupBy(rows, "driver"),
|
|
121
|
+
project: groupBy(rows, "project")
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
totals,
|
|
125
|
+
groups,
|
|
126
|
+
hints: hintsOf(totals, groups.node)
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
export { usageReport as n, USAGE_DIMENSIONS as t };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Xt as wakeRun, Yt as socketPath, lt as liveStatus, wt as scriptContactsOf } from "./src-CGqMati3.mjs";
|
|
2
|
+
import { c as openStore, o as writeErr } from "./output-CXSJO00X.mjs";
|
|
3
|
+
import { u as inheritedScrubber } from "./src-UWkk1sl9.mjs";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
// ../core/src/remote/send.ts
|
|
6
|
+
/**
|
|
7
|
+
* @file `ork send` (RFC-005): a program in a tool node writes to a session on
|
|
8
|
+
* its run's bus. The check is on the sender's side, as a refusal must be an
|
|
9
|
+
* exit code a script can act on; then a row in the store and a wake, as for a
|
|
10
|
+
* message between linked runs. `ORK_RUN` and `ORK_NODE` say who it is, and
|
|
11
|
+
* they are attribution, not proof: the check stops mistakes, not a program
|
|
12
|
+
* running as the same user.
|
|
13
|
+
*/
|
|
14
|
+
/** A first value, as for links; real use will show whether it is the right one. */
|
|
15
|
+
const DEFAULT_SCRIPT_MESSAGES = 100;
|
|
16
|
+
/**
|
|
17
|
+
* Why this sender may not write to this node; `null` when it may. The body is
|
|
18
|
+
* not needed, so a program is told before it pipes a whole message in.
|
|
19
|
+
*/
|
|
20
|
+
const scriptRefusal = (store, send) => {
|
|
21
|
+
const { run: runId, node, to } = send;
|
|
22
|
+
if (runId === void 0 || node === void 0) return "`ork send` runs inside a tool node of a run: `ORK_RUN` and `ORK_NODE` are not set.";
|
|
23
|
+
if (runId.startsWith("node-")) return `Run ${runId} runs one node, which has no message bus; run the whole workflow.`;
|
|
24
|
+
const run = store.lookup(runId).find((found) => found.id === runId);
|
|
25
|
+
if (run === void 0) return `There is no run ${runId}.`;
|
|
26
|
+
const status = liveStatus(run);
|
|
27
|
+
if (status !== "running") return `Run ${runId} is not running (${status}).`;
|
|
28
|
+
const started = store.events(runId).map((stored) => stored.event).findLast((event) => event.type === "run.started");
|
|
29
|
+
if (started === void 0) return `Run ${runId} has not started yet.`;
|
|
30
|
+
const own = started.nodes.find((found) => found.id === node);
|
|
31
|
+
if (own === void 0 || !own.type.startsWith("tool.")) return `${node} is not a tool node of run ${runId}.`;
|
|
32
|
+
const contacts = scriptContactsOf(node, started.nodes, started.channels ?? []);
|
|
33
|
+
if (contacts.includes(to)) return null;
|
|
34
|
+
return contacts.length === 0 ? `\`${node}\` has no one to write to: give it an owner, or a channel under \`messages:\`.` : `\`${to}\` is not a contact of \`${node}\`, which can write to \`${contacts.join("`, `")}\` (its owner and its channels).`;
|
|
35
|
+
};
|
|
36
|
+
/** Reading all of it would cost as much as sending it. */
|
|
37
|
+
const TOO_LONG = "The message is over 64 KiB. Write it to a file and send the file's path.";
|
|
38
|
+
/** Checks the message, writes it for the run, and wakes it. */
|
|
39
|
+
const sendFromScript = async (store, send, home) => {
|
|
40
|
+
const refused = scriptRefusal(store, send);
|
|
41
|
+
if (refused !== null) return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: refused
|
|
44
|
+
};
|
|
45
|
+
if (Buffer.byteLength(send.body) > 65536) return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: TOO_LONG
|
|
48
|
+
};
|
|
49
|
+
const run = String(send.run);
|
|
50
|
+
const node = String(send.node);
|
|
51
|
+
if (!store.scriptCommand(run, {
|
|
52
|
+
kind: "script",
|
|
53
|
+
node: send.to,
|
|
54
|
+
from: node,
|
|
55
|
+
body: send.body
|
|
56
|
+
}, DEFAULT_SCRIPT_MESSAGES)) return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: `\`${node}\` has sent ${String(DEFAULT_SCRIPT_MESSAGES)} messages in this run, which is the most it may.`
|
|
59
|
+
};
|
|
60
|
+
await wakeRun(socketPath(run, home));
|
|
61
|
+
return { ok: true };
|
|
62
|
+
};
|
|
63
|
+
// src/commands/send.ts
|
|
64
|
+
/**
|
|
65
|
+
* @file `ork send` (RFC-005): a program in a tool node writes to a session on
|
|
66
|
+
* its run's bus. It prints nothing when it works, as what a program prints is
|
|
67
|
+
* its node's output, and says why on stderr, with exit 1, when it is refused:
|
|
68
|
+
* before reading a message it could not send, and for anything wrong with the
|
|
69
|
+
* command itself, which would otherwise go to stdout as usage.
|
|
70
|
+
*/
|
|
71
|
+
const refuse = (message) => {
|
|
72
|
+
writeErr(message);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
};
|
|
75
|
+
/** Stdin, up to the limit: past it, the rest is not worth holding. */
|
|
76
|
+
const readBounded = async () => {
|
|
77
|
+
const chunks = [];
|
|
78
|
+
let size = 0;
|
|
79
|
+
for await (const chunk of process.stdin) {
|
|
80
|
+
const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
81
|
+
size += part.length;
|
|
82
|
+
if (size > 65536) return null;
|
|
83
|
+
chunks.push(part);
|
|
84
|
+
}
|
|
85
|
+
return Buffer.concat(chunks);
|
|
86
|
+
};
|
|
87
|
+
/** The message, or why it can't go. */
|
|
88
|
+
const bodyOf = async (given) => {
|
|
89
|
+
if (given !== void 0) return { body: given };
|
|
90
|
+
const read = await readBounded();
|
|
91
|
+
if (read === null) return { refused: TOO_LONG };
|
|
92
|
+
try {
|
|
93
|
+
return { body: new TextDecoder("utf-8", { fatal: true }).decode(read) };
|
|
94
|
+
} catch {
|
|
95
|
+
return { refused: "The message is not UTF-8 text." };
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const deliver = async (store, to, given) => {
|
|
99
|
+
const sender = {
|
|
100
|
+
run: process.env["ORK_RUN"],
|
|
101
|
+
node: process.env["ORK_NODE"],
|
|
102
|
+
to
|
|
103
|
+
};
|
|
104
|
+
const refused = scriptRefusal(store, sender);
|
|
105
|
+
if (refused !== null) {
|
|
106
|
+
refuse(refused);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const message = await bodyOf(given);
|
|
110
|
+
if ("refused" in message) {
|
|
111
|
+
refuse(message.refused);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (message.body.replaceAll(/[\p{Cc}\s]/gu, "") === "") {
|
|
115
|
+
refuse("The message is empty.");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const sent = await sendFromScript(store, {
|
|
119
|
+
...sender,
|
|
120
|
+
body: inheritedScrubber(process.env)(message.body)
|
|
121
|
+
});
|
|
122
|
+
if (!sent.ok) refuse(sent.error);
|
|
123
|
+
};
|
|
124
|
+
const send = defineCommand({
|
|
125
|
+
meta: {
|
|
126
|
+
name: "send",
|
|
127
|
+
description: "From a tool node's program: write to a session in its run"
|
|
128
|
+
},
|
|
129
|
+
args: {
|
|
130
|
+
to: {
|
|
131
|
+
type: "positional",
|
|
132
|
+
description: "The node to write to: the owner, or a channel",
|
|
133
|
+
required: false
|
|
134
|
+
},
|
|
135
|
+
message: {
|
|
136
|
+
type: "string",
|
|
137
|
+
alias: "m",
|
|
138
|
+
description: "The message"
|
|
139
|
+
},
|
|
140
|
+
stdin: {
|
|
141
|
+
type: "boolean",
|
|
142
|
+
description: "Read the message from stdin",
|
|
143
|
+
default: false
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
run: async ({ args }) => {
|
|
147
|
+
if (args.to === void 0 || args._.length !== 1) {
|
|
148
|
+
refuse("Give one node to write to: ork send <to> -m <body>.");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (args.message === void 0 === !args.stdin) {
|
|
152
|
+
refuse("Give the message with -m or --stdin, one of them.");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const store = openStore();
|
|
156
|
+
try {
|
|
157
|
+
await deliver(store, args.to, args.message);
|
|
158
|
+
} finally {
|
|
159
|
+
store.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
export { send };
|