toolroll 0.8.1 → 0.8.2
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/CHANGELOG.md +18 -0
- package/dist/agent-onboard.d.ts +4 -0
- package/dist/agent-onboard.js +20 -5
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +6 -6
- package/dist/checkout-cleanup.d.ts +84 -0
- package/dist/checkout-cleanup.js +245 -0
- package/dist/first-run.d.ts +4 -1
- package/dist/first-run.js +2 -1
- package/dist/mcp.js +26 -0
- package/dist/operate.d.ts +0 -4
- package/dist/operate.js +103 -21
- package/dist/prompt.d.ts +7 -2
- package/dist/prompt.js +36 -7
- package/dist/serve.js +60 -11
- package/dist/storage-ui.d.ts +10 -0
- package/dist/storage-ui.js +67 -0
- package/dist/storage.d.ts +15 -1
- package/dist/storage.js +62 -7
- package/dist/store.d.ts +5 -5
- package/dist/store.js +18 -16
- package/dist/toolroll-update-ui.d.ts +2 -0
- package/dist/toolroll-update-ui.js +1 -1
- package/dist/toolroll-update.d.ts +26 -6
- package/dist/toolroll-update.js +188 -50
- package/dist/worktree.d.ts +24 -10
- package/dist/worktree.js +84 -44
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { type CheckoutCleanup } from "./storage.js";
|
|
2
|
+
import type { Store } from "./store.js";
|
|
3
|
+
import { type WorktreePool } from "./worktree.js";
|
|
4
|
+
/** A release candidate's checkout holds the build a deploy installs: kept a week after its task was done. */
|
|
5
|
+
export declare const CANDIDATE_KEEP_MS: number;
|
|
6
|
+
/** The worker's pass removes at most this many; a clean-up by hand has no cap. */
|
|
7
|
+
export declare const AUTO_MAX = 20;
|
|
8
|
+
export type KeepWhy = "in use" | "task not finished" | "waiting for review" | "release candidate" | "has changes" | "has commits" | "unreadable" | "git refused" | "not due yet" | "cleanup is off" | "no task";
|
|
9
|
+
export type CheckoutItem = {
|
|
10
|
+
path: string;
|
|
11
|
+
repo: string;
|
|
12
|
+
branch: string;
|
|
13
|
+
taskId: string | null;
|
|
14
|
+
bytes: number;
|
|
15
|
+
releasedAt: string | null;
|
|
16
|
+
/** null: it goes. */
|
|
17
|
+
why: KeepWhy | null;
|
|
18
|
+
/** With "not due yet" or "release candidate": when it may go. */
|
|
19
|
+
dueAt?: string;
|
|
20
|
+
};
|
|
21
|
+
export type CheckoutPlan = {
|
|
22
|
+
at: string;
|
|
23
|
+
cleanup: CheckoutCleanup;
|
|
24
|
+
manual: boolean;
|
|
25
|
+
go: CheckoutItem[];
|
|
26
|
+
stay: CheckoutItem[];
|
|
27
|
+
/** Every checkout on disk. */
|
|
28
|
+
count: number;
|
|
29
|
+
totalBytes: number;
|
|
30
|
+
freeBytes: number;
|
|
31
|
+
waitingReview: number;
|
|
32
|
+
withChanges: number;
|
|
33
|
+
};
|
|
34
|
+
type Status = {
|
|
35
|
+
keep: KeepWhy | null;
|
|
36
|
+
taskId: string;
|
|
37
|
+
finishedAt: string | null;
|
|
38
|
+
dueAt?: string;
|
|
39
|
+
};
|
|
40
|
+
/** Where every task stands for cleanup, by task_ref id and by each branch it or its runs used. */
|
|
41
|
+
export declare function taskStatuses(store: Store, now: Date): {
|
|
42
|
+
byRef: Map<number, Status>;
|
|
43
|
+
byBranch: Map<string, Status>;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* What a clean-up would remove now, how much space it would free, and what stays and why. `manual` is a clean-up by
|
|
47
|
+
* hand (it doesn't wait for the setting); otherwise it is the worker's pass under the setting. Reads only.
|
|
48
|
+
*/
|
|
49
|
+
export declare function checkoutPlan(store: Store, pool: WorktreePool, now: Date, options: {
|
|
50
|
+
manual: boolean;
|
|
51
|
+
repo?: string;
|
|
52
|
+
}): Promise<CheckoutPlan>;
|
|
53
|
+
/** Names exactly what a preview would remove, so a confirmation removes that and nothing else. */
|
|
54
|
+
export declare function previewDigest(plan: Pick<CheckoutPlan, "go">): string;
|
|
55
|
+
export type CleanResult = {
|
|
56
|
+
removed: CheckoutItem[];
|
|
57
|
+
kept: {
|
|
58
|
+
path: string;
|
|
59
|
+
why: KeepWhy;
|
|
60
|
+
}[];
|
|
61
|
+
freed: number;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Remove what may go: the worker's pass (`manual` false: under the setting, at most AUTO_MAX, one repository) or a
|
|
65
|
+
* clean-up by hand (no cap, every repository). Each checkout is asked again right before it goes; each removal is in
|
|
66
|
+
* the ledger.
|
|
67
|
+
*/
|
|
68
|
+
export declare function cleanCheckouts(store: Store, pool: WorktreePool, now: () => Date, options: {
|
|
69
|
+
manual: boolean;
|
|
70
|
+
actor: string;
|
|
71
|
+
repo?: string;
|
|
72
|
+
only?: ReadonlySet<string>;
|
|
73
|
+
}): Promise<CleanResult>;
|
|
74
|
+
/** Throw away a checkout kept for its changes, on purpose; its branch stays. In the ledger. */
|
|
75
|
+
export declare function discardCheckout(store: Store, pool: WorktreePool, path: string, now: Date, actor: string): Promise<{
|
|
76
|
+
ok: true;
|
|
77
|
+
bytes: number;
|
|
78
|
+
} | {
|
|
79
|
+
ok: false;
|
|
80
|
+
message: string;
|
|
81
|
+
}>;
|
|
82
|
+
/** "Waiting for review", "Has changes" … as a sentence fragment for a person. */
|
|
83
|
+
export declare function whyWords(item: Pick<CheckoutItem, "why" | "dueAt">): string;
|
|
84
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkout cleanup (Settings → Storage, `toolroll storage clean`): which task checkouts can go, and removing them.
|
|
3
|
+
*
|
|
4
|
+
* A finished task's clean checkout goes as the setting says: when its task is complete or cancelled (the default),
|
|
5
|
+
* 2 days or a week after that, or never (only a clean-up by hand). A clean-up by hand doesn't wait for the setting.
|
|
6
|
+
* Only the working copy goes: its branch, and so every commit, stays, and a later lease of the branch makes a new one.
|
|
7
|
+
*
|
|
8
|
+
* Always kept: a checkout in use, one whose task isn't finished (on hold, failed, a revision under way), a result
|
|
9
|
+
* still waiting for review, a release candidate for a week after it was done (a deploy installs from it), a checkout
|
|
10
|
+
* with changes (files Toolroll wrote itself aside), and one with commits on no branch. A checkout no task names
|
|
11
|
+
* (adopted after a crash) goes only in a clean-up by hand. Every removal is in the ledger.
|
|
12
|
+
*/
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import { basename } from "node:path";
|
|
16
|
+
import { taskBranches } from "./names.js";
|
|
17
|
+
import { COMPLETION_ACTION } from "./result-completion.js";
|
|
18
|
+
import { CLEANUP_CHOICES, bytesWords, diskBytes } from "./storage.js";
|
|
19
|
+
import { worktreePath } from "./worktree.js";
|
|
20
|
+
const DAY_MS = 86_400_000;
|
|
21
|
+
/** A release candidate's checkout holds the build a deploy installs: kept a week after its task was done. */
|
|
22
|
+
export const CANDIDATE_KEEP_MS = 7 * DAY_MS;
|
|
23
|
+
/** The worker's pass removes at most this many; a clean-up by hand has no cap. */
|
|
24
|
+
export const AUTO_MAX = 20;
|
|
25
|
+
/** Git inspections at once. */
|
|
26
|
+
const INSPECT_AT_ONCE = 8;
|
|
27
|
+
const STRENGTH = { "in use": 5, "task not finished": 4, "waiting for review": 3, "release candidate": 2 };
|
|
28
|
+
const strength = (one) => one === undefined ? -1 : one.keep === null ? 0 : STRENGTH[one.keep] ?? 1;
|
|
29
|
+
const stronger = (a, b) => strength(b) > strength(a) || (strength(b) === 0 && strength(a) === 0 && (b.finishedAt ?? "") > (a.finishedAt ?? "")) ? b : a;
|
|
30
|
+
/** Where every task stands for cleanup, by task_ref id and by each branch it or its runs used. */
|
|
31
|
+
export function taskStatuses(store, now) {
|
|
32
|
+
const db = store.handle;
|
|
33
|
+
const refs = db.prepare(`SELECT r.id AS ref, r.external_id AS id, r.revision_of AS revision_of, t.state AS state, t.updated_at AS updated_at,
|
|
34
|
+
EXISTS (SELECT 1 FROM hold h WHERE h.task_ref = r.id) AS held,
|
|
35
|
+
EXISTS (SELECT 1 FROM run live WHERE live.task_ref = r.id AND live.finished_at IS NULL) AS live,
|
|
36
|
+
EXISTS (SELECT 1 FROM task_scope s WHERE s.task_id = r.external_id AND s.candidate IS NOT NULL) AS candidate
|
|
37
|
+
FROM task_ref r LEFT JOIN task t ON t.id = r.external_id AND r.backend = 'built-in'`).all();
|
|
38
|
+
const runs = db.prepare("SELECT id, task_ref, branch FROM run").all();
|
|
39
|
+
const refOfRun = new Map(runs.map(row => [Number(row["id"]), Number(row["task_ref"])]));
|
|
40
|
+
// The completion names its family's root; its run names the result it completes.
|
|
41
|
+
const completed = new Map();
|
|
42
|
+
const completedRef = new Map();
|
|
43
|
+
for (const row of db.prepare("SELECT task_id, run_id, at FROM action_ledger WHERE action = ?").all(COMPLETION_ACTION)) {
|
|
44
|
+
const at = String(row["at"]);
|
|
45
|
+
if (row["task_id"] !== null) {
|
|
46
|
+
const id = String(row["task_id"]);
|
|
47
|
+
if ((completed.get(id) ?? "") < at)
|
|
48
|
+
completed.set(id, at);
|
|
49
|
+
}
|
|
50
|
+
const ref = row["run_id"] === null ? undefined : refOfRun.get(Number(row["run_id"]));
|
|
51
|
+
if (ref !== undefined && (completedRef.get(ref) ?? "") < at)
|
|
52
|
+
completedRef.set(ref, at);
|
|
53
|
+
}
|
|
54
|
+
const byRef = new Map();
|
|
55
|
+
const byId = new Map();
|
|
56
|
+
const parentOf = new Map();
|
|
57
|
+
for (const row of refs) {
|
|
58
|
+
const ref = Number(row["ref"]);
|
|
59
|
+
const id = String(row["id"]);
|
|
60
|
+
const state = row["state"] === null ? null : String(row["state"]);
|
|
61
|
+
const updated = row["updated_at"] === null ? null : String(row["updated_at"]);
|
|
62
|
+
let status;
|
|
63
|
+
if (Number(row["live"]) === 1)
|
|
64
|
+
status = { keep: "in use", taskId: id, finishedAt: null };
|
|
65
|
+
else if (state === null || Number(row["held"]) === 1 || (state !== "done" && state !== "cancelled"))
|
|
66
|
+
status = { keep: "task not finished", taskId: id, finishedAt: null };
|
|
67
|
+
else if (state === "cancelled")
|
|
68
|
+
status = { keep: null, taskId: id, finishedAt: updated };
|
|
69
|
+
else {
|
|
70
|
+
const completedAt = [completed.get(id), completedRef.get(ref)].filter((one) => one !== undefined).sort().at(-1) ?? null;
|
|
71
|
+
if (Number(row["candidate"]) === 1) {
|
|
72
|
+
const since = completedAt ?? updated ?? now.toISOString();
|
|
73
|
+
const until = new Date(Date.parse(since) + CANDIDATE_KEEP_MS).toISOString();
|
|
74
|
+
status = until > now.toISOString() ? { keep: "release candidate", taskId: id, finishedAt: since, dueAt: until } : { keep: null, taskId: id, finishedAt: since };
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
status = completedAt === null ? { keep: "waiting for review", taskId: id, finishedAt: null } : { keep: null, taskId: id, finishedAt: completedAt };
|
|
78
|
+
}
|
|
79
|
+
byRef.set(ref, status);
|
|
80
|
+
byId.set(id, stronger(byId.get(id), status));
|
|
81
|
+
if (row["revision_of"] !== null)
|
|
82
|
+
parentOf.set(id, String(row["revision_of"]));
|
|
83
|
+
}
|
|
84
|
+
// A revision under way keeps what its ancestors worked on: it builds on their branch.
|
|
85
|
+
for (const [id, status] of byId) {
|
|
86
|
+
if (status.keep === null || status.keep === "release candidate")
|
|
87
|
+
continue;
|
|
88
|
+
const seen = new Set([id]);
|
|
89
|
+
for (let parent = parentOf.get(id); parent !== undefined && !seen.has(parent); parent = parentOf.get(parent)) {
|
|
90
|
+
seen.add(parent);
|
|
91
|
+
const lifted = { ...status, taskId: parent };
|
|
92
|
+
byId.set(parent, stronger(byId.get(parent), lifted));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const row of refs)
|
|
96
|
+
byRef.set(Number(row["ref"]), stronger(byRef.get(Number(row["ref"])), { ...byId.get(String(row["id"])), taskId: String(row["id"]) }));
|
|
97
|
+
const byBranch = new Map();
|
|
98
|
+
const note = (branch, status) => byBranch.set(branch, stronger(byBranch.get(branch), status));
|
|
99
|
+
for (const row of refs)
|
|
100
|
+
for (const branch of taskBranches(String(row["id"])))
|
|
101
|
+
note(branch, byRef.get(Number(row["ref"])));
|
|
102
|
+
for (const row of runs) {
|
|
103
|
+
if (row["branch"] === null)
|
|
104
|
+
continue;
|
|
105
|
+
const status = byRef.get(Number(row["task_ref"]));
|
|
106
|
+
if (status !== undefined)
|
|
107
|
+
note(String(row["branch"]), status);
|
|
108
|
+
}
|
|
109
|
+
return { byRef, byBranch };
|
|
110
|
+
}
|
|
111
|
+
/** Where one checkout's task stands: by its task, its branch, or (branch unknown) the name a lease of a branch would give it. */
|
|
112
|
+
function statusOf(row, statuses) {
|
|
113
|
+
let found = stronger(row.taskRef === null ? undefined : statuses.byRef.get(row.taskRef), statuses.byBranch.get(row.branch));
|
|
114
|
+
if (found === undefined) {
|
|
115
|
+
const name = basename(row.path);
|
|
116
|
+
for (const [branch, status] of statuses.byBranch)
|
|
117
|
+
if (basename(worktreePath("/", row.repo, branch)) === name)
|
|
118
|
+
found = stronger(found, status);
|
|
119
|
+
}
|
|
120
|
+
return found;
|
|
121
|
+
}
|
|
122
|
+
/** Why a checkout stays before anyone looks at its files, or null: it may go (as far as its task and the setting say). */
|
|
123
|
+
function keptFor(pool, row, statuses, now, cleanup) {
|
|
124
|
+
const status = statusOf(row, statuses);
|
|
125
|
+
const taskId = status?.taskId ?? null;
|
|
126
|
+
if (row.releasedAt === null || row.runner !== null || pool.inUse(row.path).held)
|
|
127
|
+
return { why: "in use", taskId };
|
|
128
|
+
if (status !== undefined && status.keep !== null)
|
|
129
|
+
return { why: status.keep, taskId, ...(status.dueAt === undefined ? {} : { dueAt: status.dueAt }) };
|
|
130
|
+
if (cleanup === "manual")
|
|
131
|
+
return { why: null, taskId };
|
|
132
|
+
// A checkout no task names (adopted after a crash, say) has no task to finish: only a clean-up by hand takes it.
|
|
133
|
+
if (status === undefined)
|
|
134
|
+
return { why: "no task", taskId };
|
|
135
|
+
if (cleanup === "finished")
|
|
136
|
+
return { why: null, taskId };
|
|
137
|
+
if (cleanup === "never")
|
|
138
|
+
return { why: "cleanup is off", taskId };
|
|
139
|
+
const days = CLEANUP_CHOICES.find(one => one.value === cleanup).days;
|
|
140
|
+
const since = [status.finishedAt, row.releasedAt].filter((one) => one !== null).sort().at(-1);
|
|
141
|
+
const dueAt = new Date(Date.parse(since) + days * DAY_MS).toISOString();
|
|
142
|
+
return dueAt > now.toISOString() ? { why: "not due yet", taskId, dueAt } : { why: null, taskId };
|
|
143
|
+
}
|
|
144
|
+
async function eachAtMost(items, limit, work) {
|
|
145
|
+
const out = new Array(items.length);
|
|
146
|
+
let next = 0;
|
|
147
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
148
|
+
while (next < items.length) {
|
|
149
|
+
const at = next++;
|
|
150
|
+
out[at] = await work(items[at]);
|
|
151
|
+
}
|
|
152
|
+
}));
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* What a clean-up would remove now, how much space it would free, and what stays and why. `manual` is a clean-up by
|
|
157
|
+
* hand (it doesn't wait for the setting); otherwise it is the worker's pass under the setting. Reads only.
|
|
158
|
+
*/
|
|
159
|
+
export async function checkoutPlan(store, pool, now, options) {
|
|
160
|
+
const cleanup = store.checkoutCleanup();
|
|
161
|
+
const rows = store.listWorktrees().filter(row => existsSync(row.path) && (options.repo === undefined || row.repo === options.repo));
|
|
162
|
+
const sizes = diskBytes(rows.map(row => row.path));
|
|
163
|
+
const statuses = taskStatuses(store, now);
|
|
164
|
+
const items = await eachAtMost(rows, INSPECT_AT_ONCE, async (row) => {
|
|
165
|
+
const kept = keptFor(pool, row, statuses, now, options.manual ? "manual" : cleanup);
|
|
166
|
+
let why = kept.why;
|
|
167
|
+
if (why === null) {
|
|
168
|
+
const found = await pool.inspect(row.path);
|
|
169
|
+
why = found === "clean" ? null : found;
|
|
170
|
+
}
|
|
171
|
+
return { path: row.path, repo: row.repo, branch: row.branch, taskId: kept.taskId, bytes: sizes.get(row.path) ?? 0, releasedAt: row.releasedAt, why, ...(kept.dueAt === undefined ? {} : { dueAt: kept.dueAt }) };
|
|
172
|
+
});
|
|
173
|
+
const go = items.filter(one => one.why === null).sort((a, b) => b.bytes - a.bytes);
|
|
174
|
+
const stay = items.filter(one => one.why !== null).sort((a, b) => (a.why ?? "").localeCompare(b.why ?? "") || b.bytes - a.bytes);
|
|
175
|
+
return {
|
|
176
|
+
at: now.toISOString(), cleanup, manual: options.manual, go, stay,
|
|
177
|
+
count: items.length, totalBytes: items.reduce((sum, one) => sum + one.bytes, 0), freeBytes: go.reduce((sum, one) => sum + one.bytes, 0),
|
|
178
|
+
waitingReview: stay.filter(one => one.why === "waiting for review").length, withChanges: stay.filter(one => one.why === "has changes").length,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/** Names exactly what a preview would remove, so a confirmation removes that and nothing else. */
|
|
182
|
+
export function previewDigest(plan) {
|
|
183
|
+
return createHash("sha256").update(plan.go.map(one => one.path).sort().join("\u0000"), "utf8").digest("hex").slice(0, 32);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Remove what may go: the worker's pass (`manual` false: under the setting, at most AUTO_MAX, one repository) or a
|
|
187
|
+
* clean-up by hand (no cap, every repository). Each checkout is asked again right before it goes; each removal is in
|
|
188
|
+
* the ledger.
|
|
189
|
+
*/
|
|
190
|
+
export async function cleanCheckouts(store, pool, now, options) {
|
|
191
|
+
const cleanup = store.checkoutCleanup();
|
|
192
|
+
if (!options.manual && cleanup === "never")
|
|
193
|
+
return { removed: [], kept: [], freed: 0 };
|
|
194
|
+
// `only`: the checkouts a person saw in the preview; nothing they didn't see goes.
|
|
195
|
+
const rows = store.listWorktrees().filter(row => existsSync(row.path) && (options.repo === undefined || row.repo === options.repo) && (options.only === undefined || options.only.has(row.path)));
|
|
196
|
+
let statuses = taskStatuses(store, now());
|
|
197
|
+
const sizes = new Map();
|
|
198
|
+
const wanted = (row, fresh) => {
|
|
199
|
+
// Right before a checkout goes, where its task stands is read afresh, and its size taken for the ledger.
|
|
200
|
+
if (fresh)
|
|
201
|
+
statuses = taskStatuses(store, now());
|
|
202
|
+
const go = keptFor(pool, row, statuses, now(), options.manual ? "manual" : cleanup).why === null;
|
|
203
|
+
if (go && fresh)
|
|
204
|
+
sizes.set(row.path, diskBytes([row.path]).get(row.path) ?? 0);
|
|
205
|
+
return go;
|
|
206
|
+
};
|
|
207
|
+
const pruned = await pool.prune(rows, wanted, options.manual ? Infinity : AUTO_MAX);
|
|
208
|
+
const removed = [];
|
|
209
|
+
for (const row of pruned.removed) {
|
|
210
|
+
const taskId = row.taskRef === null ? null : store.externalIdFor(row.taskRef);
|
|
211
|
+
const bytes = sizes.get(row.path) ?? 0;
|
|
212
|
+
removed.push({ path: row.path, repo: row.repo, branch: row.branch, taskId, bytes, releasedAt: row.releasedAt, why: null });
|
|
213
|
+
store.recordAction({ at: now().toISOString(), actor: options.actor, repo: row.repo, taskId, runId: null, action: "checkout removed", outcome: "removed",
|
|
214
|
+
source: options.manual ? "request" : "work", detail: `${basename(row.path)} (released ${row.releasedAt?.slice(0, 10) ?? "?"}, about ${bytesWords(bytes)}); branch ${row.branch} kept` });
|
|
215
|
+
}
|
|
216
|
+
return { removed, kept: pruned.kept, freed: removed.reduce((sum, one) => sum + one.bytes, 0) };
|
|
217
|
+
}
|
|
218
|
+
/** Throw away a checkout kept for its changes, on purpose; its branch stays. In the ledger. */
|
|
219
|
+
export async function discardCheckout(store, pool, path, now, actor) {
|
|
220
|
+
const bytes = existsSync(path) ? diskBytes([path]).get(path) ?? 0 : 0;
|
|
221
|
+
const done = await pool.discardChanges(path);
|
|
222
|
+
if (!done.ok)
|
|
223
|
+
return done;
|
|
224
|
+
const taskId = done.row.taskRef === null ? null : store.externalIdFor(done.row.taskRef);
|
|
225
|
+
store.recordAction({ at: now.toISOString(), actor, repo: done.row.repo, taskId, runId: null, action: "checkout discarded", outcome: "removed", source: "request",
|
|
226
|
+
detail: `${basename(path)} and its uncommitted changes (about ${bytesWords(bytes)}); branch ${done.row.branch} kept` });
|
|
227
|
+
return { ok: true, bytes };
|
|
228
|
+
}
|
|
229
|
+
/** "Waiting for review", "Has changes" … as a sentence fragment for a person. */
|
|
230
|
+
export function whyWords(item) {
|
|
231
|
+
switch (item.why) {
|
|
232
|
+
case null: return "goes";
|
|
233
|
+
case "in use": return "in use";
|
|
234
|
+
case "task not finished": return "its task isn't finished";
|
|
235
|
+
case "waiting for review": return "its result is waiting for review";
|
|
236
|
+
case "release candidate": return `release candidate, kept until ${item.dueAt?.slice(0, 10) ?? "a week after"}`;
|
|
237
|
+
case "has changes": return "has changes";
|
|
238
|
+
case "has commits": return "has commits on no branch";
|
|
239
|
+
case "unreadable": return "git couldn't read it";
|
|
240
|
+
case "git refused": return "git wouldn't remove it";
|
|
241
|
+
case "not due yet": return `due ${item.dueAt?.slice(0, 10) ?? "later"}`;
|
|
242
|
+
case "cleanup is off": return "automatic cleanup is off";
|
|
243
|
+
case "no task": return "no task names it; only a clean-up by hand removes it";
|
|
244
|
+
}
|
|
245
|
+
}
|
package/dist/first-run.d.ts
CHANGED
|
@@ -9,14 +9,17 @@ export type FirstRunAction = {
|
|
|
9
9
|
kind: "command";
|
|
10
10
|
command: string;
|
|
11
11
|
};
|
|
12
|
+
/** `checking`: the answer is not in yet, so the step is shown neither done nor to do. */
|
|
12
13
|
export type FirstRunStep = {
|
|
13
14
|
key: "agent" | "project" | "task";
|
|
14
15
|
title: string;
|
|
15
16
|
done: boolean;
|
|
16
17
|
action: FirstRunAction | null;
|
|
18
|
+
checking?: true;
|
|
17
19
|
};
|
|
20
|
+
/** agentSignedIn is null until this machine's first sign-in check has answered. */
|
|
18
21
|
export type FirstRunFacts = {
|
|
19
|
-
agentSignedIn: boolean;
|
|
22
|
+
agentSignedIn: boolean | null;
|
|
20
23
|
projects: number;
|
|
21
24
|
hasTask: boolean;
|
|
22
25
|
firstResultAt: string | null;
|
package/dist/first-run.js
CHANGED
|
@@ -15,7 +15,8 @@ export function firstRunSteps(facts) {
|
|
|
15
15
|
return null;
|
|
16
16
|
const project = facts.projects > 0;
|
|
17
17
|
return [
|
|
18
|
-
{ key: "agent", title: "Agent signed in", done:
|
|
18
|
+
facts.agentSignedIn === null ? { key: "agent", title: "Agent signed in", done: false, action: null, checking: true }
|
|
19
|
+
: { key: "agent", title: "Agent signed in", done: facts.agentSignedIn, action: facts.agentSignedIn ? null : { kind: "command", command: SIGN_IN_COMMAND } },
|
|
19
20
|
{ key: "project", title: "Project added", done: project, action: project ? null : { kind: "link", label: "Add a project", href: "/projects" } },
|
|
20
21
|
{ key: "task", title: "Your first task", done: facts.hasTask, action: facts.hasTask ? null : { kind: "link", label: "New task", href: "/tasks/new" } },
|
|
21
22
|
];
|
package/dist/mcp.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TASK_SCOPE_TEXT_SCHEMA } from "./task-text.js";
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
+
import { statSync } from 'node:fs';
|
|
3
4
|
import { repositoryContextRead } from './repository-context.js';
|
|
4
5
|
import { ASSIGNMENT_TOOLS, assignmentForCoordinator } from "./assignment-adapters.js";
|
|
5
6
|
/**
|
|
@@ -367,12 +368,30 @@ function proposeTools() {
|
|
|
367
368
|
make("answer", "Propose an answer to an open decision you read with get_decision, with a rationale. The approver confirms where every consequence and the builder's recommendation are shown; an irreversible option needs their explicit confirmation.", { decision: { type: "integer", minimum: 1 }, option: { type: "string", minLength: 1, maxLength: 64 }, rationale: { type: "string", maxLength: 400 } }, ["decision", "option", "rationale"]),
|
|
368
369
|
];
|
|
369
370
|
}
|
|
371
|
+
function fileIdentity(path) {
|
|
372
|
+
if (path === null)
|
|
373
|
+
return null;
|
|
374
|
+
try {
|
|
375
|
+
const at = statSync(path);
|
|
376
|
+
return { path, dev: at.dev, ino: at.ino };
|
|
377
|
+
}
|
|
378
|
+
catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/** Still the same file at the same path: not moved aside, deleted, or replaced by a rename. */
|
|
383
|
+
function sameFile(was) {
|
|
384
|
+
const now = fileIdentity(was.path);
|
|
385
|
+
return now !== null && now.dev === was.dev && now.ino === was.ino;
|
|
386
|
+
}
|
|
370
387
|
/** Serve until EOF. The store is already open through the non-migrating
|
|
371
388
|
* door; the token was startup-verified by the caller (and dies here again
|
|
372
389
|
* if it does not hold). */
|
|
373
390
|
export function serveMcp(store, token, io, clock = () => new Date(), enrolled = null, evidenceRoot) {
|
|
374
391
|
/** Per connection: which decisions this credential read in full (v3). */
|
|
375
392
|
const readDecisions = new Set();
|
|
393
|
+
/** The database file this server opened, by identity (null in memory). */
|
|
394
|
+
const database = fileIdentity(store.databaseFile());
|
|
376
395
|
const auth = authenticateCoordinator(store, token);
|
|
377
396
|
if (!auth.ok) {
|
|
378
397
|
return {
|
|
@@ -414,6 +433,13 @@ export function serveMcp(store, token, io, clock = () => new Date(), enrolled =
|
|
|
414
433
|
tools: TOOLS.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })),
|
|
415
434
|
});
|
|
416
435
|
const callTool = (id, era, params) => {
|
|
436
|
+
// A restore or rollback puts a different file at the database's path: this server's connection still reads and
|
|
437
|
+
// writes the one it replaced, so it answers nothing more and exits (the agent starts it again on the new one).
|
|
438
|
+
if (database !== null && !sameFile(database)) {
|
|
439
|
+
error(id, -32000, "the database was replaced underneath this server (a Toolroll restore or rollback) — restart it");
|
|
440
|
+
io.exit(0);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
417
443
|
// ONE snapshot per call (review finding 3): the version check, the
|
|
418
444
|
// credential re-read, and every data read share a transaction, so a
|
|
419
445
|
// concurrent migration cannot slip between them; filing's own
|
package/dist/operate.d.ts
CHANGED
|
@@ -22,10 +22,6 @@ export declare const EXIT: {
|
|
|
22
22
|
readonly usage: 2;
|
|
23
23
|
readonly refused: 3;
|
|
24
24
|
};
|
|
25
|
-
/** Storage retention: how long a finished task's clean checkout is kept after it was let go, and a result marked
|
|
26
|
-
* complete's or a release candidate's after it was (its checkout holds the build a deploy installs). */
|
|
27
|
-
export declare const CHECKOUT_KEEP_MS: number;
|
|
28
|
-
export declare const RESULT_KEEP_MS: number;
|
|
29
25
|
export type OperateOptions = {
|
|
30
26
|
/** Native-shell proof key, passed in memory rather than command-line arguments. */
|
|
31
27
|
desktopIdentity?: string;
|
package/dist/operate.js
CHANGED
|
@@ -3,7 +3,8 @@ import { parseProtectedPaths } from "./approval-policy.js";
|
|
|
3
3
|
import { rulesSummary } from "./approval-rules-ui.js";
|
|
4
4
|
import { evidencePack, exportDay, ledgerExportChunks, standaloneEvidenceHtml } from "./evidence-pack.js";
|
|
5
5
|
import { matchesOutsideCheckpoint } from "./ledger-chain.js";
|
|
6
|
-
import { bytesWords, storageReport } from "./storage.js";
|
|
6
|
+
import { CLEANUP_CHOICES, bytesWords, cleanupWords, parseCleanup, storageReport } from "./storage.js";
|
|
7
|
+
import { checkoutPlan, cleanCheckouts, discardCheckout, whyWords } from "./checkout-cleanup.js";
|
|
7
8
|
import { MIN_DAYS, RETENTION_KINDS, countWords, dailyRetention, isRetentionKind, lastSweepAt, parsePeriod, periodWords, retentionPlan, sweepWords } from "./retention.js";
|
|
8
9
|
import { checkPolicy, parseList, policyParts } from "./policy.js";
|
|
9
10
|
import { billingOf, budgetHoldWords, budgetLabel, budgetStates, monthNamed, monthOf, spendItems, teammateNames, usd as spendUsd } from "./spend.js";
|
|
@@ -193,10 +194,6 @@ import { PACKAGE_VERSION } from "./version.js";
|
|
|
193
194
|
* caller deciding whether to try the next task or wake somebody up.
|
|
194
195
|
*/
|
|
195
196
|
export const EXIT = { ok: 0, failed: 1, usage: 2, refused: 3 };
|
|
196
|
-
/** Storage retention: how long a finished task's clean checkout is kept after it was let go, and a result marked
|
|
197
|
-
* complete's or a release candidate's after it was (its checkout holds the build a deploy installs). */
|
|
198
|
-
export const CHECKOUT_KEEP_MS = 2 * 24 * 60 * 60_000;
|
|
199
|
-
export const RESULT_KEEP_MS = 7 * 24 * 60 * 60_000;
|
|
200
197
|
const STATES = ["queued", "running", "done", "failed", "cancelled"];
|
|
201
198
|
export const OPERATE_HELP = `toolroll — operating the queue
|
|
202
199
|
|
|
@@ -213,6 +210,9 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
213
210
|
--not-requester on|off, --protect none|project, --protect-paths "a/**,b/**"
|
|
214
211
|
toolroll task evidence <id> the task's evidence pack as JSON (--html for a printable page; --out <file>)
|
|
215
212
|
toolroll storage where the disk goes: database, build checkouts, releases, evidence
|
|
213
|
+
toolroll storage clean [--yes] preview removing finished tasks' clean checkouts (their branches stay); --yes removes them
|
|
214
|
+
toolroll storage cleanup <when> remove a finished task's clean checkout: finished|2d|7d|never (instance operator)
|
|
215
|
+
toolroll storage discard <path> --yes throw away a checkout kept for its changes; its branch stays
|
|
216
216
|
toolroll monitoring where the audit stream and traces go, and how each destination is doing
|
|
217
217
|
toolroll check-progress <run> current or final approved check progress
|
|
218
218
|
toolroll spend [--month YYYY-MM] [--csv] what agent work cost, by project, person, teammate and model
|
|
@@ -823,13 +823,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
823
823
|
return exportCommand(positional, flags, context);
|
|
824
824
|
case "policy":
|
|
825
825
|
return policyCommand(positional, flags, context);
|
|
826
|
-
case "storage":
|
|
827
|
-
|
|
828
|
-
return succeed(context.write, context.json, "storage", report, () => [
|
|
829
|
-
`${report.folder}: ${bytesWords(report.total)}`,
|
|
830
|
-
...report.lines.map(one => ` ${one.what.padEnd(17)} ${bytesWords(one.bytes).padStart(8)}${one.count === undefined ? "" : ` (${one.count})`}${one.note === undefined ? "" : ` ${one.note}`}`),
|
|
831
|
-
]);
|
|
832
|
-
}
|
|
826
|
+
case "storage":
|
|
827
|
+
return storageCommand(positional, flags, context);
|
|
833
828
|
case "monitoring": {
|
|
834
829
|
// Where the audit stream and traces go, and how each destination is doing. Settings → Monitoring changes it.
|
|
835
830
|
const settings = readMonitoring(dirname(context.databaseFile));
|
|
@@ -4313,13 +4308,9 @@ async function reconcileCommand(flags, context) {
|
|
|
4313
4308
|
reaped: reaped.map(claim => claim.leaseId),
|
|
4314
4309
|
});
|
|
4315
4310
|
}
|
|
4316
|
-
// Storage
|
|
4317
|
-
//
|
|
4318
|
-
const pruned = await
|
|
4319
|
-
for (const row of pruned.removed) {
|
|
4320
|
-
store.recordAction({ at: clock().toISOString(), actor: "worker", repo, taskId: row.taskRef === null ? null : store.externalIdFor(row.taskRef), runId: null,
|
|
4321
|
-
action: "checkout removed", outcome: "removed", source: "work", detail: `${basename(row.path)} (released ${row.releasedAt?.slice(0, 10) ?? "?"})` });
|
|
4322
|
-
}
|
|
4311
|
+
// Checkout cleanup (Settings → Storage): finished work's clean checkouts go as the setting says (by default when
|
|
4312
|
+
// the task is complete or cancelled); their branches stay. Each removal is in the ledger.
|
|
4313
|
+
const pruned = await cleanCheckouts(store, worktrees, clock, { manual: false, actor: "worker", repo });
|
|
4323
4314
|
// Retention (Settings → Retention): once a day, what's older than its setting goes; one ledger entry says what.
|
|
4324
4315
|
let retention = null;
|
|
4325
4316
|
try {
|
|
@@ -4341,7 +4332,7 @@ async function reconcileCommand(flags, context) {
|
|
|
4341
4332
|
reaped: reaped.map(claim => claim.leaseId),
|
|
4342
4333
|
adopted: adoption.adopted,
|
|
4343
4334
|
forgotten: adoption.forgotten,
|
|
4344
|
-
checkoutsRemoved: pruned.removed.map(
|
|
4335
|
+
checkoutsRemoved: pruned.removed.map(one => one.path),
|
|
4345
4336
|
checkoutsKept: pruned.kept,
|
|
4346
4337
|
liveViewsSwept: liveSwept.removed.length,
|
|
4347
4338
|
retention: retention === null ? null : { counts: retention.counts, freedBytes: retention.freed },
|
|
@@ -4352,7 +4343,7 @@ async function reconcileCommand(flags, context) {
|
|
|
4352
4343
|
...(reaped.length === 0 ? [] : [`Reaped ${reaped.length} expired lease(s).`]),
|
|
4353
4344
|
...adoption.adopted.map(path => `Adopted ${path} — released, unverified, somebody should look.`),
|
|
4354
4345
|
...adoption.forgotten.map(path => `Forgot ${path} — its directory is gone.`),
|
|
4355
|
-
...(pruned.removed.length === 0 ? [] : [`Removed ${pruned.removed.length} checkout(s)
|
|
4346
|
+
...(pruned.removed.length === 0 ? [] : [`Removed ${pruned.removed.length} finished task checkout(s), about ${bytesWords(pruned.freed)}; their branches stay.`]),
|
|
4356
4347
|
...(liveSwept.removed.length === 0 ? [] : [`Cleared ${liveSwept.removed.length} finished live view(s).`]),
|
|
4357
4348
|
...(retention === null || retained.length === 0 ? [] : [`Retention removed ${sweepWords(retention.counts, retention.freed)}.`]),
|
|
4358
4349
|
]);
|
|
@@ -11323,6 +11314,97 @@ async function retentionCommand(positional, flags, context) {
|
|
|
11323
11314
|
const periods = store.setRetentionPeriod(kind, days, acting.name, context.clock());
|
|
11324
11315
|
return succeed(context.write, context.json, command, { periods }, () => [`${label(kind)}: kept ${days === null ? "forever" : `for ${periodWords(days)}`}.`]);
|
|
11325
11316
|
}
|
|
11317
|
+
/** `storage`, `storage cleanup [<when>]`, `storage clean [--yes]`, `storage discard <path> --yes`: where the disk goes,
|
|
11318
|
+
* and task checkouts: when a finished task's clean checkout is removed, cleaning up now, and throwing away one kept for
|
|
11319
|
+
* its changes. Changing anything takes an instance operator. */
|
|
11320
|
+
async function storageCommand(positional, flags, context) {
|
|
11321
|
+
const [action, ...rest] = positional;
|
|
11322
|
+
const command = `storage ${action ?? ""}`.trim();
|
|
11323
|
+
const store = context.store;
|
|
11324
|
+
const changes = (action === "cleanup" && rest.length > 0) || (action === "clean" && flags.has("yes")) || action === "discard";
|
|
11325
|
+
const allowed = new Set([...(changes ? ["as", "token", "token-file", "token-env"] : []), ...(action === "clean" || action === "discard" ? ["yes"] : []), "db", "json"]);
|
|
11326
|
+
for (const name of flags.keys())
|
|
11327
|
+
if (!allowed.has(name))
|
|
11328
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not a ${command} option.`, EXIT.usage);
|
|
11329
|
+
const pool = new WorktreePool(store, { root: join(dirname(context.databaseFile), "worktrees"), ...(context.gitRunner === undefined ? {} : { runner: context.gitRunner }) });
|
|
11330
|
+
const operator = async () => {
|
|
11331
|
+
const acting = await askCredentials(flags, context);
|
|
11332
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11333
|
+
return acting !== null && verified !== null && verified.ok && store.isInstanceOperator(acting.name) ? acting.name : null;
|
|
11334
|
+
};
|
|
11335
|
+
const refused = (what) => fail(context.write, context.json, command, "refused", `An instance operator ${what}: pass --as and --token (or use the remembered login).`, EXIT.refused);
|
|
11336
|
+
const summary = (plan) => [
|
|
11337
|
+
`Checkouts: ${plan.count}, ${bytesWords(plan.totalBytes)}. ${plan.waitingReview} waiting for review, ${plan.withChanges} kept because they have changes.`,
|
|
11338
|
+
plan.go.length === 0 ? "A clean up now would free nothing." : `A clean up now would free about ${bytesWords(plan.freeBytes)} (${plan.go.length} checkout${plan.go.length === 1 ? "" : "s"}): toolroll storage clean`,
|
|
11339
|
+
`A finished task's clean checkout is removed ${cleanupWords(plan.cleanup)}; its branch stays.`,
|
|
11340
|
+
];
|
|
11341
|
+
const itemLine = (one) => ` ${bytesWords(one.bytes).padStart(8)} ${one.path}${one.why === null ? "" : ` (${whyWords(one)})`}`;
|
|
11342
|
+
if (action === undefined) {
|
|
11343
|
+
const plan = await checkoutPlan(store, pool, context.clock(), { manual: true });
|
|
11344
|
+
const report = storageReport(store, context.databaseFile, plan.go.length === 0 ? "nothing to clean up" : `about ${bytesWords(plan.freeBytes)} to clean up`);
|
|
11345
|
+
return succeed(context.write, context.json, "storage", { ...report, checkouts: checkoutSummary(plan) }, () => [
|
|
11346
|
+
`${report.folder}: ${bytesWords(report.total)}`,
|
|
11347
|
+
...report.lines.map(one => ` ${one.what.padEnd(17)} ${bytesWords(one.bytes).padStart(8)}${one.count === undefined ? "" : ` (${one.count})`}${one.note === undefined ? "" : ` ${one.note}`}`),
|
|
11348
|
+
...summary(plan),
|
|
11349
|
+
]);
|
|
11350
|
+
}
|
|
11351
|
+
if (action === "cleanup") {
|
|
11352
|
+
if (rest.length === 0) {
|
|
11353
|
+
const cleanup = store.checkoutCleanup();
|
|
11354
|
+
return succeed(context.write, context.json, command, { cleanup }, () => [`A finished task's clean checkout is removed ${cleanupWords(cleanup)}; its branch stays.`]);
|
|
11355
|
+
}
|
|
11356
|
+
const cleanup = rest.length === 1 ? parseCleanup(rest[0]) : undefined;
|
|
11357
|
+
if (cleanup === undefined)
|
|
11358
|
+
return fail(context.write, context.json, command, "usage", `Use storage cleanup <${CLEANUP_CHOICES.map(one => one.value).join("|")}>.`, EXIT.usage);
|
|
11359
|
+
const actor = await operator();
|
|
11360
|
+
if (actor === null)
|
|
11361
|
+
return refused("sets checkout cleanup");
|
|
11362
|
+
const saved = store.setCheckoutCleanup(cleanup, actor, context.clock());
|
|
11363
|
+
return succeed(context.write, context.json, command, { cleanup: saved }, () => [`Saved. A finished task's clean checkout is removed ${cleanupWords(saved)}; its branch stays.`]);
|
|
11364
|
+
}
|
|
11365
|
+
if (action === "clean") {
|
|
11366
|
+
if (rest.length > 0)
|
|
11367
|
+
return fail(context.write, context.json, command, "usage", "Use storage clean to preview, then storage clean --yes.", EXIT.usage);
|
|
11368
|
+
if (!flags.has("yes")) {
|
|
11369
|
+
const plan = await checkoutPlan(store, pool, context.clock(), { manual: true });
|
|
11370
|
+
return succeed(context.write, context.json, command, { preview: true, go: plan.go, stay: plan.stay, freeBytes: plan.freeBytes }, () => [
|
|
11371
|
+
...(plan.go.length === 0 ? ["Nothing to clean up."] : [`Would remove ${plan.go.length} checkout${plan.go.length === 1 ? "" : "s"}, freeing about ${bytesWords(plan.freeBytes)} (their branches stay):`, ...plan.go.map(itemLine)]),
|
|
11372
|
+
...(plan.stay.length === 0 ? [] : [`Stays (${plan.stay.length}):`, ...plan.stay.map(itemLine)]),
|
|
11373
|
+
plan.go.length === 0 ? "Nothing was removed." : "Nothing was removed. Run toolroll storage clean --yes to remove them.",
|
|
11374
|
+
]);
|
|
11375
|
+
}
|
|
11376
|
+
const actor = await operator();
|
|
11377
|
+
if (actor === null)
|
|
11378
|
+
return refused("cleans up checkouts");
|
|
11379
|
+
const done = await cleanCheckouts(store, pool, context.clock, { manual: true, actor });
|
|
11380
|
+
return succeed(context.write, context.json, command, { removed: done.removed, kept: done.kept, freedBytes: done.freed }, () => [
|
|
11381
|
+
done.removed.length === 0 ? "Nothing to clean up." : `Removed ${done.removed.length} checkout${done.removed.length === 1 ? "" : "s"}, about ${bytesWords(done.freed)}; their branches stay.`,
|
|
11382
|
+
...done.removed.map(itemLine),
|
|
11383
|
+
...done.kept.map(one => ` kept ${one.path} (${whyWords({ why: one.why })})`),
|
|
11384
|
+
]);
|
|
11385
|
+
}
|
|
11386
|
+
if (action === "discard") {
|
|
11387
|
+
const [path, ...extra] = rest;
|
|
11388
|
+
if (path === undefined || extra.length > 0)
|
|
11389
|
+
return fail(context.write, context.json, command, "usage", "Use storage discard <path> --yes.", EXIT.usage);
|
|
11390
|
+
const target = resolve(path);
|
|
11391
|
+
if (store.getWorktree(target) === null)
|
|
11392
|
+
return fail(context.write, context.json, command, "not-found", `${target} is not a task checkout. toolroll storage clean lists them.`, EXIT.refused);
|
|
11393
|
+
if (!flags.has("yes"))
|
|
11394
|
+
return fail(context.write, context.json, command, "usage", `This throws away ${target} and its uncommitted changes (its branch stays). Add --yes to discard it.`, EXIT.usage);
|
|
11395
|
+
const actor = await operator();
|
|
11396
|
+
if (actor === null)
|
|
11397
|
+
return refused("discards a checkout");
|
|
11398
|
+
const done = await discardCheckout(store, pool, target, context.clock(), actor);
|
|
11399
|
+
if (!done.ok)
|
|
11400
|
+
return fail(context.write, context.json, command, "refused", done.message, EXIT.refused);
|
|
11401
|
+
return succeed(context.write, context.json, command, { discarded: target, freedBytes: done.bytes }, () => [`Discarded ${target}, about ${bytesWords(done.bytes)}; its branch stays.`]);
|
|
11402
|
+
}
|
|
11403
|
+
return fail(context.write, context.json, command, "usage", "Use storage, storage cleanup <when>, storage clean [--yes] or storage discard <path> --yes.", EXIT.usage);
|
|
11404
|
+
}
|
|
11405
|
+
function checkoutSummary(plan) {
|
|
11406
|
+
return { count: plan.count, totalBytes: plan.totalBytes, waitingReview: plan.waitingReview, withChanges: plan.withChanges, cleanBytes: plan.freeBytes, cleanCount: plan.go.length, cleanup: plan.cleanup };
|
|
11407
|
+
}
|
|
11326
11408
|
/** `policy show | set` (sprint 8): the organisation policy. Setting it takes an instance operator's credentials; each rule
|
|
11327
11409
|
* that changed is in the ledger, before → after. A flag left out keeps its rule; "any" lifts it. */
|
|
11328
11410
|
async function policyCommand(positional, flags, context) {
|
package/dist/prompt.d.ts
CHANGED
|
@@ -7,12 +7,17 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/** Whether asking is even possible: a human on both ends of the pipe. */
|
|
9
9
|
export declare function interactive(): boolean;
|
|
10
|
+
/** The program names of this process's ancestors, nearest first: each one's command and, for a script run by node
|
|
11
|
+
* or python, the script's name. Empty where `ps` cannot say. */
|
|
12
|
+
export declare function ancestorPrograms(): string[];
|
|
10
13
|
/**
|
|
11
14
|
* Whether a coding agent is running this command. An agent's shell can be a
|
|
12
15
|
* real terminal, yet whatever is printed lands in its transcript and nobody
|
|
13
|
-
* types at the prompt, so it is treated like no terminal at all.
|
|
16
|
+
* types at the prompt, so it is treated like no terminal at all. The agent is
|
|
17
|
+
* known by a variable it sets or, for one that sets none, by the program above
|
|
18
|
+
* this one. `programs` is read only for this process's own environment.
|
|
14
19
|
*/
|
|
15
|
-
export declare function underAgent(env?: Record<string, string | undefined
|
|
20
|
+
export declare function underAgent(env?: Record<string, string | undefined>, programs?: () => readonly string[]): boolean;
|
|
16
21
|
export declare function ask(question: string): Promise<string>;
|
|
17
22
|
/** Like ask, but what is typed never echoes — passwords live here. */
|
|
18
23
|
/**
|