toolroll 0.9.30 → 0.9.32
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 +35 -0
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +8 -2
- package/dist/batch-checks.d.ts +15 -0
- package/dist/batch-checks.js +244 -0
- package/dist/batch-policy.d.ts +23 -0
- package/dist/batch-policy.js +25 -0
- package/dist/browser/workspace.js +13 -13
- package/dist/builder.js +14 -3
- package/dist/chat-actions.js +2 -2
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +21 -2
- package/dist/checkout-cleanup.d.ts +2 -1
- package/dist/checkout-cleanup.js +24 -8
- package/dist/desktop-update.d.ts +1 -1
- package/dist/exec.d.ts +2 -0
- package/dist/exec.js +11 -0
- package/dist/flow-gallery-ui.d.ts +14 -9
- package/dist/flow-gallery-ui.js +71 -29
- package/dist/flow-gallery.d.ts +26 -2
- package/dist/flow-gallery.js +182 -7
- package/dist/lead-capabilities.d.ts +0 -7
- package/dist/lead-capabilities.js +11 -1
- package/dist/mate-doors.js +2 -2
- package/dist/mate.d.ts +6 -2
- package/dist/mate.js +63 -38
- package/dist/mcp-connect.d.ts +3 -1
- package/dist/mcp-connect.js +11 -6
- package/dist/never-index.d.ts +3 -0
- package/dist/never-index.js +21 -0
- package/dist/operate.js +86 -2
- package/dist/orphan-sweep.d.ts +63 -0
- package/dist/orphan-sweep.js +235 -0
- package/dist/result-follow-ups.d.ts +22 -2
- package/dist/result-follow-ups.js +44 -10
- package/dist/serve.js +52 -19
- package/dist/shared-deps.d.ts +18 -2
- package/dist/shared-deps.js +129 -15
- package/dist/staged-runtimes.d.ts +42 -0
- package/dist/staged-runtimes.js +191 -0
- package/dist/storage-sweep.d.ts +50 -0
- package/dist/storage-sweep.js +203 -0
- package/dist/storage-ui.d.ts +5 -0
- package/dist/storage-ui.js +30 -4
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +15 -3
- package/dist/store.d.ts +17 -2
- package/dist/store.js +37 -5
- package/dist/subscription-chat.d.ts +3 -1
- package/dist/subscription-chat.js +1 -0
- package/dist/surface.js +1 -0
- package/dist/task-status.d.ts +13 -0
- package/dist/task-status.js +37 -3
- package/dist/team-leads.js +6 -2
- package/dist/team-runtime.js +2 -1
- package/dist/test-temp.d.ts +20 -2
- package/dist/test-temp.js +53 -13
- package/dist/toolroll-update.js +2 -0
- package/dist/workspace-ui.js +3 -1
- package/dist/worktree.js +9 -0
- package/package.json +1 -1
package/dist/store.d.ts
CHANGED
|
@@ -21,6 +21,14 @@ import { type RetentionKind, type RetentionPeriods } from "./retention.js";
|
|
|
21
21
|
import { type CheckoutCleanup } from "./storage.js";
|
|
22
22
|
import { type LedgerChainReport } from "./ledger-chain.js";
|
|
23
23
|
import { type ApprovalGate, type ApprovalRules, type ApproverKind, type Filer } from "./approval-policy.js";
|
|
24
|
+
/** A lead reply stopped at its deadline, in the thread as plain words: what happened, what it means, one next step. The
|
|
25
|
+
* proposals it made before then are kept; the cost line only when an unknown cost paused chat. */
|
|
26
|
+
export declare function mateTimeoutNotice(keptProposals: boolean, unknownCost?: boolean): string;
|
|
27
|
+
/** Whether a turn's proposals stand: it answered, or it stopped at its deadline after its completed tool calls proposed them. */
|
|
28
|
+
export declare function mateTurnKeepsProposals(turn: {
|
|
29
|
+
state: string;
|
|
30
|
+
failureReason: string | null;
|
|
31
|
+
} | null): boolean;
|
|
24
32
|
/** What the lead promised to follow up on (lead-commitments.ts; no version bump: additive only, so a build that
|
|
25
33
|
* predates it still opens the store). `condition_json` is a task, attempt, check or time; `check_at` is when to look
|
|
26
34
|
* next; `channel` is where it reports (`chat`: the conversation it was promised in); a promise lapses at `expires_at`. */
|
|
@@ -6916,12 +6924,17 @@ export declare class Store {
|
|
|
6916
6924
|
tokensIn: number;
|
|
6917
6925
|
tokensOut: number;
|
|
6918
6926
|
failureReason?: string;
|
|
6919
|
-
/** Answered turns: the assistant row, written in the same transaction as the promotion. */
|
|
6927
|
+
/** Answered turns: the assistant row, written in the same transaction as the promotion. A timed-out turn's notice. */
|
|
6920
6928
|
message?: {
|
|
6921
6929
|
text: string;
|
|
6922
6930
|
activity: string;
|
|
6923
6931
|
};
|
|
6932
|
+
/** A timed-out turn keeps what its completed, validated tool calls proposed (its drafts go pending with its notice). */
|
|
6933
|
+
keepProposals?: boolean;
|
|
6924
6934
|
}, now: Date): boolean;
|
|
6935
|
+
/** A turn stopped at its deadline: the proposals its completed tool calls drafted (each validated when drafted) go pending, as an
|
|
6936
|
+
* answered turn's do; anything the provider sent after them was never acted on. Every other failure still deletes its drafts. */
|
|
6937
|
+
private keepTimedOutMateProposals;
|
|
6925
6938
|
/** Revocation's companion for a loop in flight (finding 5): every live
|
|
6926
6939
|
* turn of the approver fails NOW, charged its whole reservation, its
|
|
6927
6940
|
* generation moved so the returning loop's terminal CAS loses. */
|
|
@@ -6929,7 +6942,9 @@ export declare class Store {
|
|
|
6929
6942
|
/** The crash sweep for turns: past the deadline, failed. The session is
|
|
6930
6943
|
* charged the settled sum of the steps that answered — unless any step
|
|
6931
6944
|
* is unproven (dispatched and never settled, or latched), in which case
|
|
6932
|
-
* the WHOLE reservation is charged (finding 1).
|
|
6945
|
+
* the WHOLE reservation is charged (finding 1). Like a turn that stopped
|
|
6946
|
+
* at its own deadline, it keeps the proposals its completed tool calls
|
|
6947
|
+
* drafted and says, in the thread, that the reply took too long. */
|
|
6933
6948
|
sweepStaleMateTurns(now: Date): number;
|
|
6934
6949
|
/** A task_ref by its row id: the external id and repo — for decisions, which hang off runs. */
|
|
6935
6950
|
refById(id: number): {
|
package/dist/store.js
CHANGED
|
@@ -71,7 +71,7 @@ import { PROVIDER_AUTH_SCHEMA } from "./provider-auth.js";
|
|
|
71
71
|
import { REVIEW_SCHEMA, reviewSwitchWords } from "./review-switch.js";
|
|
72
72
|
import { SPEND_SCHEMA, billingOf, budgetStates, canPrice, claudeMachineBilling, countsToward, filersOf, monthOf, priceWork, seenBilling as seenBillingOf, spendItems, teammateFilers, usd } from "./spend.js";
|
|
73
73
|
import { DEFAULT_PERIODS, RETENTION_SCHEMA, periodWords, widenRetentionSchema } from "./retention.js";
|
|
74
|
-
import { CHECKOUT_CLEANUP_SCHEMA, DEFAULT_CLEANUP, cleanupWords } from "./storage.js";
|
|
74
|
+
import { CHECKOUT_CLEANUP_SCHEMA, STORAGE_SWEEP_SCHEMA, DEFAULT_CLEANUP, cleanupWords } from "./storage.js";
|
|
75
75
|
import { IN_RANGE, LEDGER_CHAIN_SCHEMA, safeWhole, sealLedger, verifyLedgerChain } from "./ledger-chain.js";
|
|
76
76
|
import { NO_RULES, filerFor, isProtectedWork, protectedChanges, rulesWords } from "./approval-policy.js";
|
|
77
77
|
import { configBase, envValue, namedPath } from "./names.js";
|
|
@@ -253,6 +253,19 @@ CREATE TABLE IF NOT EXISTS project_mute (
|
|
|
253
253
|
PRIMARY KEY (account, repo)
|
|
254
254
|
);
|
|
255
255
|
`;
|
|
256
|
+
/** A lead reply stopped at its deadline, in the thread as plain words: what happened, what it means, one next step. The
|
|
257
|
+
* proposals it made before then are kept; the cost line only when an unknown cost paused chat. */
|
|
258
|
+
export function mateTimeoutNotice(keptProposals, unknownCost = false) {
|
|
259
|
+
return [
|
|
260
|
+
unknownCost ? "The reply took too long and was stopped, and its cost isn't known yet, so chat is paused." : "The reply took too long and was stopped.",
|
|
261
|
+
keptProposals ? "What it proposed is below." : null,
|
|
262
|
+
unknownCost ? "Confirm the cost on the Chat page to turn chat back on." : "Send your message again, or ask for less at once.",
|
|
263
|
+
].filter(part => part !== null).join(" ");
|
|
264
|
+
}
|
|
265
|
+
/** Whether a turn's proposals stand: it answered, or it stopped at its deadline after its completed tool calls proposed them. */
|
|
266
|
+
export function mateTurnKeepsProposals(turn) {
|
|
267
|
+
return turn !== null && (turn.state === "answered" || turn.state === "failed" && (turn.failureReason === "timeout" || turn.failureReason === "crashed"));
|
|
268
|
+
}
|
|
256
269
|
/** What the lead promised to follow up on (lead-commitments.ts; no version bump: additive only, so a build that
|
|
257
270
|
* predates it still opens the store). `condition_json` is a task, attempt, check or time; `check_at` is when to look
|
|
258
271
|
* next; `channel` is where it reports (`chat`: the conversation it was promised in); a promise lapses at `expires_at`. */
|
|
@@ -3992,6 +4005,8 @@ function initializeStore(db, file) {
|
|
|
3992
4005
|
// 1-day evidence: an older file's retention_setting only allowed 7 days or more (no version bump: its rows carry over).
|
|
3993
4006
|
widenRetentionSchema(db);
|
|
3994
4007
|
db.exec(CHECKOUT_CLEANUP_SCHEMA);
|
|
4008
|
+
// The daily storage sweep's record (no version bump: a new table, nothing carried over).
|
|
4009
|
+
db.exec(STORAGE_SWEEP_SCHEMA);
|
|
3995
4010
|
db.exec(BACKUP_SCHEMA);
|
|
3996
4011
|
// Sprint 8: the organisation policy (one row, or none: nothing restricted).
|
|
3997
4012
|
db.exec(POLICY_SCHEMA);
|
|
@@ -20282,12 +20297,23 @@ export class Store {
|
|
|
20282
20297
|
}
|
|
20283
20298
|
}
|
|
20284
20299
|
else {
|
|
20285
|
-
|
|
20300
|
+
if (outcome.keepProposals === true)
|
|
20301
|
+
this.keepTimedOutMateProposals(id);
|
|
20302
|
+
else
|
|
20303
|
+
this.discardMateProposals(id);
|
|
20286
20304
|
this.discardMateTurnEvidence(id);
|
|
20305
|
+
if (outcome.message !== undefined) {
|
|
20306
|
+
this.appendMateMessage({ thread: Number(before["thread"]), turn: id, role: "assistant", text: outcome.message.text, activity: outcome.message.activity }, now);
|
|
20307
|
+
}
|
|
20287
20308
|
}
|
|
20288
20309
|
return true;
|
|
20289
20310
|
});
|
|
20290
20311
|
}
|
|
20312
|
+
/** A turn stopped at its deadline: the proposals its completed tool calls drafted (each validated when drafted) go pending, as an
|
|
20313
|
+
* answered turn's do; anything the provider sent after them was never acted on. Every other failure still deletes its drafts. */
|
|
20314
|
+
keepTimedOutMateProposals(turn) {
|
|
20315
|
+
return Number(this.db.prepare("UPDATE mate_proposal SET state = 'pending' WHERE turn = ? AND state = 'drafting'").run(turn).changes);
|
|
20316
|
+
}
|
|
20291
20317
|
/** Revocation's companion for a loop in flight (finding 5): every live
|
|
20292
20318
|
* turn of the approver fails NOW, charged its whole reservation, its
|
|
20293
20319
|
* generation moved so the returning loop's terminal CAS loses. */
|
|
@@ -20309,12 +20335,14 @@ export class Store {
|
|
|
20309
20335
|
/** The crash sweep for turns: past the deadline, failed. The session is
|
|
20310
20336
|
* charged the settled sum of the steps that answered — unless any step
|
|
20311
20337
|
* is unproven (dispatched and never settled, or latched), in which case
|
|
20312
|
-
* the WHOLE reservation is charged (finding 1).
|
|
20338
|
+
* the WHOLE reservation is charged (finding 1). Like a turn that stopped
|
|
20339
|
+
* at its own deadline, it keeps the proposals its completed tool calls
|
|
20340
|
+
* drafted and says, in the thread, that the reply took too long. */
|
|
20313
20341
|
sweepStaleMateTurns(now) {
|
|
20314
20342
|
return this.transact(() => {
|
|
20315
20343
|
this.sweepStaleChatTurns(now);
|
|
20316
20344
|
const rows = this.db
|
|
20317
|
-
.prepare("SELECT id, session, reserved_microusd AS reserved FROM mate_turn WHERE state IN ('queued','running') AND deadline_at < ?")
|
|
20345
|
+
.prepare("SELECT id, session, thread, reserved_microusd AS reserved FROM mate_turn WHERE state IN ('queued','running') AND deadline_at < ?")
|
|
20318
20346
|
.all(now.toISOString());
|
|
20319
20347
|
for (const row of rows) {
|
|
20320
20348
|
const id = Number(row["id"]);
|
|
@@ -20329,8 +20357,12 @@ export class Store {
|
|
|
20329
20357
|
.prepare("UPDATE mate_turn SET state = 'failed', failure_reason = 'crashed', generation = generation + 1, settled_microusd = ?, finished_at = ? WHERE id = ?")
|
|
20330
20358
|
.run(spent, now.toISOString(), id);
|
|
20331
20359
|
this.db.prepare("UPDATE mate_session SET spent_microusd = spent_microusd + ? WHERE id = ?").run(spent, Number(row["session"]));
|
|
20332
|
-
this.
|
|
20360
|
+
const kept = this.keepTimedOutMateProposals(id);
|
|
20333
20361
|
this.discardMateTurnEvidence(id);
|
|
20362
|
+
const latched = this.db.prepare("SELECT 1 AS hit FROM chat_turn WHERE mate_turn = ? AND unknown_spend = 1 AND acknowledged_at IS NULL LIMIT 1").get(id) !== undefined;
|
|
20363
|
+
const open = this.db.prepare("SELECT 1 AS hit FROM mate_thread WHERE id = ? AND closed_at IS NULL").get(Number(row["thread"])) !== undefined;
|
|
20364
|
+
if (open)
|
|
20365
|
+
this.appendMateMessage({ thread: Number(row["thread"]), turn: id, role: "assistant", text: mateTimeoutNotice(kept > 0, latched), activity: null }, now);
|
|
20334
20366
|
}
|
|
20335
20367
|
return rows.length;
|
|
20336
20368
|
});
|
|
@@ -10,6 +10,8 @@ export type SubscriptionMateRequest = {
|
|
|
10
10
|
history: readonly MateHistoryMessage[];
|
|
11
11
|
tools: readonly MateToolSchema[];
|
|
12
12
|
timeoutMs: number;
|
|
13
|
+
/** The turn's own deadline: aborting it ends the harness's whole process group. */
|
|
14
|
+
signal?: AbortSignal;
|
|
13
15
|
/** The reply as it is written, cumulative, when the harness can stream it
|
|
14
16
|
* (Claude). Display only: the finished answer is still parsed whole. */
|
|
15
17
|
onText?: (text: string) => void;
|
|
@@ -23,7 +25,7 @@ export type SubscriptionMateRunner = (request: SubscriptionMateRequest) => Promi
|
|
|
23
25
|
}>;
|
|
24
26
|
type CommandRunner = (file: string, args: readonly string[], options: Parameters<typeof run>[2]) => Promise<ExecResult>;
|
|
25
27
|
/** The model-visible request. Tool results are already redacted by mateView. */
|
|
26
|
-
export declare function composeSubscriptionMatePrompt(request: Omit<SubscriptionMateRequest, "provider" | "model" | "timeoutMs">): string;
|
|
28
|
+
export declare function composeSubscriptionMatePrompt(request: Omit<SubscriptionMateRequest, "provider" | "model" | "timeoutMs" | "signal">): string;
|
|
27
29
|
/** Strictly turn the harness's structured output into the provider-neutral answer. */
|
|
28
30
|
export declare function parseSubscriptionMateAnswer(text: string, usage?: {
|
|
29
31
|
tokensIn?: unknown;
|
|
@@ -224,6 +224,7 @@ export async function performSubscriptionMateRequest(request, runner = run) {
|
|
|
224
224
|
cwd: dir,
|
|
225
225
|
stdin: prompt,
|
|
226
226
|
timeoutMs: request.timeoutMs,
|
|
227
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
227
228
|
// Events repeat the answer (deltas, the message, the result line).
|
|
228
229
|
maxBuffer: streaming ? RESPONSE_CAP_BYTES * 16 : RESPONSE_CAP_BYTES,
|
|
229
230
|
omitEnv: ALL_CREDENTIAL_ENV,
|
package/dist/surface.js
CHANGED
|
@@ -338,6 +338,7 @@ export const COMMAND_GUIDE = [
|
|
|
338
338
|
operator("runner retire", "retire a worker"),
|
|
339
339
|
operator("runner bind", "replace which repositories a worker may build in"),
|
|
340
340
|
operator("runner capacity", "set how many tasks a worker runs at once; running work carries on, the new number applies at the next claim"),
|
|
341
|
+
operator("project checks", "show or set a project's batch checks (--batch on|off, off by default): results that finish within 10 minutes share one full check on a temporary batch commit; nothing is merged into a real branch"),
|
|
341
342
|
operator("project concurrency", "set how many of a project's tasks build at once (default 2, never past the worker's capacity); running work carries on"),
|
|
342
343
|
operator("coordinator mint", "mint the MCP filing credential — repo-bound, rate-limited, token shown once"),
|
|
343
344
|
operator("coordinator revoke", "revoke an MCP filing credential"),
|
package/dist/task-status.d.ts
CHANGED
|
@@ -53,7 +53,20 @@ export type ChecksFact = {
|
|
|
53
53
|
level?: "quick" | "full" | "off" | null;
|
|
54
54
|
/** A follow-up check (Run checks) waiting or running on this commit. */
|
|
55
55
|
running?: "quick" | "full" | null;
|
|
56
|
+
/** A batch check (batch-checks.ts): waiting for its batch, or how it was checked. */
|
|
57
|
+
batch?: ChecksBatch | null;
|
|
56
58
|
};
|
|
59
|
+
/** `waiting`: the result waits to be checked with others. Otherwise how its check ran: `together` on the
|
|
60
|
+
* temporary batch commit `tested` with `peers`; `split` (its batch failed), `conflict` or `alone` on its own commit. */
|
|
61
|
+
export type ChecksBatch = {
|
|
62
|
+
state: "waiting" | "together" | "split" | "conflict" | "alone";
|
|
63
|
+
tested: string | null;
|
|
64
|
+
peers: string[];
|
|
65
|
+
};
|
|
66
|
+
/** "T2", "T2 and T3", "T2, T3 and 2 more": short enough for one line. The full list is on the result. */
|
|
67
|
+
export declare function peerWords(peers: readonly string[], shown?: number): string;
|
|
68
|
+
/** "Checked together with T2 on 1a2b3c4": every surface says a shared check the same way, naming the commit it tested. */
|
|
69
|
+
export declare function checkedTogetherWords(batch: ChecksBatch): string | null;
|
|
57
70
|
export type PullRequestFact = {
|
|
58
71
|
state: "none" | "opening" | "open" | "merged" | "closed" | "failed";
|
|
59
72
|
number: number | null;
|
package/dist/task-status.js
CHANGED
|
@@ -26,6 +26,21 @@ export const CHECKS_LABEL = "Project checks";
|
|
|
26
26
|
export const PR_CI_LABEL = "PR CI";
|
|
27
27
|
/** The one verb for opening a saved result, on every surface. */
|
|
28
28
|
export const OPEN_RESULT = "Open result";
|
|
29
|
+
/** "T2", "T2 and T3", "T2, T3 and 2 more": short enough for one line. The full list is on the result. */
|
|
30
|
+
export function peerWords(peers, shown = 2) {
|
|
31
|
+
if (peers.length <= shown)
|
|
32
|
+
return peers.length <= 1 ? peers.join("") : `${peers.slice(0, -1).join(", ")} and ${peers.at(-1)}`;
|
|
33
|
+
return `${peers.slice(0, shown).join(", ")} and ${peers.length - shown} more`;
|
|
34
|
+
}
|
|
35
|
+
/** "Checked together with T2 on 1a2b3c4": every surface says a shared check the same way, naming the commit it tested. */
|
|
36
|
+
export function checkedTogetherWords(batch) {
|
|
37
|
+
const sha = short(batch.tested);
|
|
38
|
+
return batch.state === "together" && batch.peers.length > 0 && sha !== null ? `Checked together with ${peerWords(batch.peers)} on ${sha}` : null;
|
|
39
|
+
}
|
|
40
|
+
const BATCH_WHY = {
|
|
41
|
+
split: "Its batch check failed, so it was checked on its own.",
|
|
42
|
+
conflict: "It conflicted with another result when merged for a batch check, so it was checked on its own.",
|
|
43
|
+
};
|
|
29
44
|
const short = (sha) => sha !== null && /^[a-f0-9]{7,40}$/.test(sha) ? sha.slice(0, 7) : null;
|
|
30
45
|
export function headlineOf(facts) {
|
|
31
46
|
switch (facts.stage) {
|
|
@@ -64,8 +79,15 @@ function sentenceOf(headline, facts) {
|
|
|
64
79
|
return "The report is ready to read. Read it, then mark it complete.";
|
|
65
80
|
if (facts.checks?.level === "off" && facts.checks.status !== "passed")
|
|
66
81
|
return "Checks are off for this project. Review the change, then mark it complete.";
|
|
67
|
-
if (facts.checks?.status === "passed")
|
|
82
|
+
if (facts.checks?.status === "passed") {
|
|
83
|
+
// The Checks row names the others and the batch commit; the sentence doesn't repeat them.
|
|
84
|
+
const peers = facts.checks.batch?.state === "together" ? facts.checks.batch.peers.length : 0;
|
|
85
|
+
if (peers > 0)
|
|
86
|
+
return `Checks passed together with ${peers === 1 ? "1 other result" : `${peers} other results`}. Review the change, then mark it complete.`;
|
|
68
87
|
return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} passed${sha === null ? "" : ` on ${sha}`}. Review the change, then mark it complete.`;
|
|
88
|
+
}
|
|
89
|
+
if (facts.checks?.batch?.state === "waiting")
|
|
90
|
+
return "Its full check runs with other results within 10 minutes. Review the change, then mark it complete.";
|
|
69
91
|
// Never claim a check that isn't known to have passed.
|
|
70
92
|
if (facts.checks == null)
|
|
71
93
|
return "Review the change, then mark it complete.";
|
|
@@ -93,8 +115,19 @@ function detailsOf(headline, facts) {
|
|
|
93
115
|
const checksHref = facts.links?.checks ?? facts.links?.result ?? null;
|
|
94
116
|
const runChecks = facts.links?.runChecks === undefined ? null : { label: "Run checks", href: facts.links.runChecks };
|
|
95
117
|
const quick = checks.level === "quick";
|
|
96
|
-
|
|
118
|
+
const batch = checks.batch ?? null;
|
|
119
|
+
const together = batch === null ? null : checkedTogetherWords(batch);
|
|
120
|
+
const batchWhy = batch === null ? null : BATCH_WHY[batch.state] ?? null;
|
|
121
|
+
if (batch?.state === "waiting")
|
|
122
|
+
row("checks", CHECKS_LABEL, "Waiting to check with other results", "running", { href: checksHref });
|
|
123
|
+
else if (checks.running != null)
|
|
97
124
|
row("checks", CHECKS_LABEL, `${checks.running === "quick" ? "Quick" : "Full"} checks running`, "running", { href: checksHref });
|
|
125
|
+
else if (checks.status === "passed" && together !== null)
|
|
126
|
+
row("checks", CHECKS_LABEL, together, "ok", { href: checksHref });
|
|
127
|
+
else if (checks.status === "passed" && batchWhy !== null)
|
|
128
|
+
row("checks", CHECKS_LABEL, `Passed on its own${sha === null ? "" : ` on ${sha}`}`, "ok", { href: checksHref, why: batchWhy });
|
|
129
|
+
else if (checks.status === "failed" && batchWhy !== null)
|
|
130
|
+
row("checks", CHECKS_LABEL, `Failed on its own${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref, why: batchWhy } : { action: { label: "See what failed", href: checksHref }, why: batchWhy });
|
|
98
131
|
else if (checks.status === "passed")
|
|
99
132
|
row("checks", CHECKS_LABEL, `${quick ? "Quick checks passed" : "Passed"}${sha === null ? "" : ` on ${sha}`}`, "ok", quick && runChecks !== null && headline !== "Complete" ? { href: checksHref, action: { label: "Run full checks", href: runChecks.href } } : { href: checksHref });
|
|
100
133
|
else if (checks.status === "failed")
|
|
@@ -320,7 +353,8 @@ export function assignmentStatusFacts(assignment, options = {}) {
|
|
|
320
353
|
const report = receipt?.completionKind === "research-report";
|
|
321
354
|
const checks = receipt === null || !withResult ? null
|
|
322
355
|
: { status: receipt.checks.status, exitCode: receipt.checks.exitCode, head: receipt.head,
|
|
323
|
-
...(receipt.checks.level == null ? {} : { level: receipt.checks.level }), ...(receipt.checks.running == null ? {} : { running: receipt.checks.running })
|
|
356
|
+
...(receipt.checks.level == null ? {} : { level: receipt.checks.level }), ...(receipt.checks.running == null ? {} : { running: receipt.checks.running }),
|
|
357
|
+
...(receipt.checks.batch == null ? {} : { batch: receipt.checks.batch }) };
|
|
324
358
|
const publication = assignment.publication;
|
|
325
359
|
const pullRequest = options.pullRequest !== undefined ? options.pullRequest
|
|
326
360
|
: publication === null ? (withResult && !report ? { state: "none", number: null, url: null, ci: null, error: null } : null)
|
package/dist/team-leads.js
CHANGED
|
@@ -32,6 +32,10 @@ function role(value) {
|
|
|
32
32
|
refuse('invalid', 'Choose Viewer, Contributor or Manager.');
|
|
33
33
|
return value;
|
|
34
34
|
}
|
|
35
|
+
/** A failed turn's words for its message: none when the turn said why in the thread itself (it stopped at its deadline). */
|
|
36
|
+
function savedFailure(reason) {
|
|
37
|
+
return reason === 'timeout' || reason === 'crashed' ? null : String(reason ?? 'The saved turn failed.');
|
|
38
|
+
}
|
|
35
39
|
function hash(value) { return createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
|
|
36
40
|
function stable(value) {
|
|
37
41
|
if (Array.isArray(value))
|
|
@@ -461,7 +465,7 @@ export class TeamLeads {
|
|
|
461
465
|
const uncertain = this.deliveryUncertain(Number(row['turn_id']));
|
|
462
466
|
const cancelled = Number(row['stop_requested']) === 1 || !this.authorStillAllowed(row);
|
|
463
467
|
const status = uncertain ? 'uncertain' : cancelled ? 'cancelled' : row['turn_state'] === 'answered' ? 'answered' : 'failed';
|
|
464
|
-
const error = uncertain ? 'Delivery could not be confirmed. Inspect saved activity before continuing.' : cancelled ? 'Stopped or conversation access changed.' : status === 'failed' ?
|
|
468
|
+
const error = uncertain ? 'Delivery could not be confirmed. Inspect saved activity before continuing.' : cancelled ? 'Stopped or conversation access changed.' : status === 'failed' ? savedFailure(row['failure_reason']) : null;
|
|
465
469
|
const result = this.db.prepare("UPDATE team_message SET status=?,error=?,generation=generation+1,revision=revision+1 WHERE message=? AND status='running' AND generation=?").run(status, error, Number(row['message']), Number(row['generation']));
|
|
466
470
|
if (Number(result.changes) === 1) {
|
|
467
471
|
changed++;
|
|
@@ -480,7 +484,7 @@ export class TeamLeads {
|
|
|
480
484
|
for (const row of rows) {
|
|
481
485
|
const turn = row['turn_id'] === null ? null : this.store.getMateTurn(Number(row['turn_id']));
|
|
482
486
|
const status = turn?.state === 'answered' ? 'answered' : turn?.state === 'failed' && !this.deliveryUncertain(turn.id) ? 'failed' : 'uncertain';
|
|
483
|
-
const error = status === 'uncertain' ? 'Delivery unconfirmed after the previous worker stopped. Inspect activity before continuing.' : status === 'failed' ? (turn?.failureReason
|
|
487
|
+
const error = status === 'uncertain' ? 'Delivery unconfirmed after the previous worker stopped. Inspect activity before continuing.' : status === 'failed' ? savedFailure(turn?.failureReason) : null;
|
|
484
488
|
this.db.prepare('UPDATE team_message SET status=?,generation=generation+1,revision=revision+1,error=? WHERE message=?').run(status, error, Number(row['message']));
|
|
485
489
|
this.event(String(row['lead']), String(row['conversation']), status === 'uncertain' ? 'delivery-unconfirmed' : 'message-reconciled', String(row['author']), now);
|
|
486
490
|
}
|
package/dist/team-runtime.js
CHANGED
|
@@ -166,9 +166,10 @@ export function createTeamRuntime(options) {
|
|
|
166
166
|
const state = admission(claim.actor, claim.conversationId);
|
|
167
167
|
if (domain.defer(claim, result.message, clock()))
|
|
168
168
|
waiting.set(claim.messageId, state.stamp);
|
|
169
|
+
// A turn that saved its outcome in the thread (stopped at its deadline) is said there, not again on the message.
|
|
169
170
|
}
|
|
170
171
|
else
|
|
171
|
-
domain.finish(claim, { status: 'unknownSpend' in result && result.unknownSpend ? 'uncertain' : 'failed', error: result.message, ...('turn' in result ? { turnId: result.turn } : {}) }, clock());
|
|
172
|
+
domain.finish(claim, { status: 'unknownSpend' in result && result.unknownSpend ? 'uncertain' : 'failed', ...('saved' in result && result.saved ? {} : { error: result.message }), ...('turn' in result ? { turnId: result.turn } : {}) }, clock());
|
|
172
173
|
}
|
|
173
174
|
catch {
|
|
174
175
|
// An exception may have followed native admission. Preserve an explicit
|
package/dist/test-temp.d.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
/** In a suite's temp root: who made it (scripts/suite-lifecycle.mjs). */
|
|
2
|
+
export declare const OWNER_FILE = ".toolroll-temp-owner";
|
|
3
|
+
export type TempOwner = {
|
|
4
|
+
pid: number;
|
|
5
|
+
startedAt: number;
|
|
6
|
+
};
|
|
1
7
|
/** Name prefixes of what tests, journeys and scripts leave in the temp folder. */
|
|
2
8
|
export declare const TEST_TEMP_PREFIXES: readonly string[];
|
|
9
|
+
/** Who made a temp root, from its owner file; null when it has none (older suites, scripts, Playwright). */
|
|
10
|
+
export declare function tempOwner(path: string): TempOwner | null;
|
|
3
11
|
export declare function isTestTemp(name: string): boolean;
|
|
4
12
|
/** Where they collect: the temp folder (and /tmp, where some scripts write). */
|
|
5
13
|
export declare function tempRoots(): string[];
|
|
@@ -12,8 +20,18 @@ export declare function testTempFolders(roots: readonly string[], now: Date): {
|
|
|
12
20
|
all: TempLeftover[];
|
|
13
21
|
stale: TempLeftover[];
|
|
14
22
|
};
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
export type RemoveOptions = {
|
|
24
|
+
/** Whether a temp root's owner (by its owner file) is still running: its root stays, however old it looks. */
|
|
25
|
+
ownerAlive?: (owner: TempOwner) => boolean;
|
|
26
|
+
/** At most this many go in one pass (a sweep after weeks can find 100,000); `more` says some were left. */
|
|
27
|
+
max?: number;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Remove the test temp folders nothing has touched for a day; each is looked at again right before it goes (still a
|
|
31
|
+
* folder of its own, not a link, still directly in its temp folder, still a day untouched, its owner gone).
|
|
32
|
+
*/
|
|
33
|
+
export declare function removeStaleTestTemp(roots: readonly string[], now: Date, options?: RemoveOptions): {
|
|
17
34
|
removed: string[];
|
|
18
35
|
failed: string[];
|
|
36
|
+
more: boolean;
|
|
19
37
|
};
|
package/dist/test-temp.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Test temp folders (`toolroll storage`, `storage clean
|
|
3
|
-
* system temp folder. The
|
|
4
|
-
* test/temp-root.ts); a run
|
|
5
|
-
*
|
|
2
|
+
* Test temp folders (`toolroll storage`, `storage clean`, the plane's daily storage sweep): what the unit tests,
|
|
3
|
+
* journeys and scripts make in the system temp folder. The suites delete their own on pass, failure, timeout and
|
|
4
|
+
* interrupt (scripts/suite-lifecycle.mjs, test/temp-root.ts); a run killed outright, a proof that keeps its output and
|
|
5
|
+
* older versions leave folders behind. `toolroll storage` counts them; the daily sweep (storage-sweep.ts) and
|
|
6
|
+
* `storage clean` remove the ones nothing has touched for a day, never one whose owner is still running.
|
|
6
7
|
*
|
|
7
8
|
* Every prefix the tests and scripts give tmpdir() is covered here (test-temp.test.ts reads the sources to keep it
|
|
8
9
|
* so), and Playwright's own browser profiles.
|
|
9
10
|
*/
|
|
10
|
-
import {
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { lstatSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
11
13
|
import { tmpdir } from "node:os";
|
|
12
|
-
import { join } from "node:path";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
15
|
const DAY_MS = 86_400_000;
|
|
16
|
+
/** In a suite's temp root: who made it (scripts/suite-lifecycle.mjs). */
|
|
17
|
+
export const OWNER_FILE = ".toolroll-temp-owner";
|
|
14
18
|
/** Name prefixes of what tests, journeys and scripts leave in the temp folder. */
|
|
15
19
|
export const TEST_TEMP_PREFIXES = [
|
|
16
20
|
"so-", "standing-orders-", "toolroll-", "no-wt", "epoch", "cancel-floor",
|
|
@@ -18,10 +22,21 @@ export const TEST_TEMP_PREFIXES = [
|
|
|
18
22
|
"flow-goal-", "flow-scratch", "flows-chat-", "invoke-attest-", "knowledge-test-", "learning-", "peek-", "phone-origin-",
|
|
19
23
|
"proposal-repo-", "race-", "refresh-migration-", "refresh-refusal-", "release-check-", "shared-chat-", "skills-test-",
|
|
20
24
|
"task-status-surfaces-", "template-repo-", "template-test-", "v14-migrate-", "v17-migrate-", "watch-anchor-",
|
|
21
|
-
"needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-", "deploy-before-swap-", "one-vocabulary-", "accept-finishes-", "lead-promises-", "lead-people-", "sizing-repo-",
|
|
25
|
+
"needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-", "deploy-before-swap-", "one-vocabulary-", "accept-finishes-", "lead-promises-", "lead-people-", "sizing-repo-", "batch-checks-", "deps-identity-",
|
|
22
26
|
// Playwright's browser profiles and artifacts.
|
|
23
27
|
"playwright_chromiumdev_profile-", "playwright_firefoxdev_profile-", "playwright-artifacts-",
|
|
24
28
|
];
|
|
29
|
+
/** Who made a temp root, from its owner file; null when it has none (older suites, scripts, Playwright). */
|
|
30
|
+
export function tempOwner(path) {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(readFileSync(join(path, OWNER_FILE), "utf8"));
|
|
33
|
+
const startedAt = typeof parsed.startedAt === "string" ? Date.parse(parsed.startedAt) : NaN;
|
|
34
|
+
return Number.isSafeInteger(parsed.pid) && Number(parsed.pid) > 1 && Number.isFinite(startedAt) ? { pid: Number(parsed.pid), startedAt } : null;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
25
40
|
export function isTestTemp(name) {
|
|
26
41
|
return TEST_TEMP_PREFIXES.some(prefix => name.startsWith(prefix));
|
|
27
42
|
}
|
|
@@ -79,20 +94,45 @@ export function testTempFolders(roots, now) {
|
|
|
79
94
|
}
|
|
80
95
|
return { all, stale: all.filter(one => now.getTime() - one.touchedAt > DAY_MS) };
|
|
81
96
|
}
|
|
82
|
-
/**
|
|
83
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Remove the test temp folders nothing has touched for a day; each is looked at again right before it goes (still a
|
|
99
|
+
* folder of its own, not a link, still directly in its temp folder, still a day untouched, its owner gone).
|
|
100
|
+
*/
|
|
101
|
+
export function removeStaleTestTemp(roots, now, options = {}) {
|
|
84
102
|
const removed = [];
|
|
85
103
|
const failed = [];
|
|
86
|
-
|
|
104
|
+
const stale = testTempFolders(roots, now).stale;
|
|
105
|
+
const max = options.max ?? Infinity;
|
|
106
|
+
const inside = new Set(roots.map(root => resolve(root)));
|
|
107
|
+
for (const one of stale) {
|
|
108
|
+
if (removed.length + failed.length >= max)
|
|
109
|
+
return { removed, failed, more: true };
|
|
87
110
|
try {
|
|
88
|
-
|
|
111
|
+
const stat = lstatSync(one.path);
|
|
112
|
+
if (stat.isSymbolicLink() || !inside.has(resolve(dirname(one.path))))
|
|
113
|
+
continue;
|
|
114
|
+
if (now.getTime() - (stat.isDirectory() ? touchedAt(one.path) : stat.mtimeMs) <= DAY_MS)
|
|
115
|
+
continue;
|
|
116
|
+
const owner = stat.isDirectory() ? tempOwner(one.path) : null;
|
|
117
|
+
if (owner !== null && (options.ownerAlive ?? (() => false))(owner))
|
|
89
118
|
continue;
|
|
90
|
-
|
|
119
|
+
removeTree(one.path);
|
|
91
120
|
removed.push(one.path);
|
|
92
121
|
}
|
|
93
122
|
catch {
|
|
94
123
|
failed.push(one.path);
|
|
95
124
|
}
|
|
96
125
|
}
|
|
97
|
-
return { removed, failed };
|
|
126
|
+
return { removed, failed, more: false };
|
|
127
|
+
}
|
|
128
|
+
/** rm -r; a tree a test made read-only (a shared dependency copy, a locked fixture) is made writable first. */
|
|
129
|
+
function removeTree(path) {
|
|
130
|
+
try {
|
|
131
|
+
rmSync(path, { recursive: true, force: true, maxRetries: 3 });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
catch { /* read-only inside */ }
|
|
135
|
+
if (process.platform !== "win32")
|
|
136
|
+
spawnSync("chmod", ["-R", "u+w", path], { timeout: 120_000 });
|
|
137
|
+
rmSync(path, { recursive: true, force: true, maxRetries: 3 });
|
|
98
138
|
}
|
package/dist/toolroll-update.js
CHANGED
|
@@ -36,6 +36,7 @@ import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, re
|
|
|
36
36
|
import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
|
|
37
37
|
import { NAME } from "./names.js";
|
|
38
38
|
import { isNewer, REGISTRY } from "./releases.js";
|
|
39
|
+
import { markNeverIndex } from "./never-index.js";
|
|
39
40
|
/** Loaded on first use (as backup.ts and store.ts do), so modules that only import this one (the console,
|
|
40
41
|
* and tests that load it in a browser-like environment) never need `node:sqlite` itself. */
|
|
41
42
|
function sqlite() {
|
|
@@ -699,6 +700,7 @@ function newJournal(kind, o, now) {
|
|
|
699
700
|
const id = randomUUID(), stageDir = join(resolve(o.stateDir), "staged-upgrades", `${kind === "update" ? "release" : "rollback"}-${o.to.version}-${id.slice(0, 8)}`);
|
|
700
701
|
mkdirSync(stageDir, { recursive: true, mode: 0o700 });
|
|
701
702
|
chmodSync(stageDir, 0o700);
|
|
703
|
+
markNeverIndex(dirname(stageDir));
|
|
702
704
|
return { version: 1, id, kind, stateDir: resolve(o.stateDir), databaseFile: o.databaseFile, stageDir, from: o.from, to: o.to, when: o.when, at: o.at, actor: o.actor, phase: "scheduled", detail: "", steps: [], startedAt: now.toISOString(), updatedAt: now.toISOString() };
|
|
703
705
|
}
|
|
704
706
|
function assertNoneActive(stateDir) {
|
package/dist/workspace-ui.js
CHANGED
|
@@ -371,7 +371,9 @@ function sentenceOf(text) {
|
|
|
371
371
|
* the saved log. Null when the log printed nothing. */
|
|
372
372
|
export function lastErrorLineOf(log) {
|
|
373
373
|
const lines = log.split("\n");
|
|
374
|
-
const printed = (text) => text.trim() !== "" && !/^\$ /.test(text) && !/^\(exit -?[0-9]+\)$/.test(text.trim()) && !/^--- (stdout|stderr) ---$/.test(text.trim())
|
|
374
|
+
const printed = (text) => text.trim() !== "" && !/^\$ /.test(text) && !/^\(exit -?[0-9]+\)$/.test(text.trim()) && !/^--- (stdout|stderr) ---$/.test(text.trim())
|
|
375
|
+
// A section's own header ("=== Project check · attempt 1 ===") is the log's, never the check's output.
|
|
376
|
+
&& !/^=== .* ===$/.test(text.trim());
|
|
375
377
|
const stderr = lines.findIndex(text => text.trim() === "--- stderr ---");
|
|
376
378
|
const pick = (from, to) => {
|
|
377
379
|
let last = null;
|
package/dist/worktree.js
CHANGED
|
@@ -31,6 +31,7 @@ import { currentBootId, provenDeadByBootChange } from "./boot-identity.js";
|
|
|
31
31
|
import { HANDOFF_PREFIX, MAILBOX_SUFFIX, looksLikeProtocolFile, readMailbox } from "./evidence.js";
|
|
32
32
|
import { parseHandoff } from "./decision.js";
|
|
33
33
|
import { diskBytes } from "./storage.js";
|
|
34
|
+
import { markNeverIndex } from "./never-index.js";
|
|
34
35
|
/**
|
|
35
36
|
* The note a lease leaves in the checkout, naming the process holding it.
|
|
36
37
|
*
|
|
@@ -337,6 +338,14 @@ export class WorktreePool {
|
|
|
337
338
|
const branchExists = request.base !== undefined && request.reuseBranch === true &&
|
|
338
339
|
(await this.git(request.repo, ["rev-parse", "--verify", "--quiet", `refs/heads/${request.branch}`], READ_ONLY)).code === 0;
|
|
339
340
|
const fromBase = request.base !== undefined && !branchExists;
|
|
341
|
+
// Spotlight leaves the checkouts alone: the marker sits in the pool and the project's folder, never inside a
|
|
342
|
+
// checkout, where a build's `git add -A` would commit it (never-index.ts).
|
|
343
|
+
try {
|
|
344
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
345
|
+
}
|
|
346
|
+
catch { /* git reports it */ }
|
|
347
|
+
markNeverIndex(this.options.root);
|
|
348
|
+
markNeverIndex(dirname(path));
|
|
340
349
|
const add = await this.git(request.repo, [
|
|
341
350
|
"worktree",
|
|
342
351
|
"add",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.32",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|