toolroll 0.5.0 → 0.7.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/README.md +38 -24
- package/dist/accent-colors.d.ts +6 -3
- package/dist/accent-colors.js +9 -4
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +23 -23
- package/dist/browser-workspace.d.ts +46 -0
- package/dist/builder.d.ts +18 -1
- package/dist/builder.js +177 -5
- package/dist/chat-delivery.js +6 -4
- package/dist/claim.d.ts +15 -1
- package/dist/claim.js +30 -2
- package/dist/control-ui.js +1 -1
- package/dist/daemon.d.ts +9 -0
- package/dist/daemon.js +21 -0
- package/dist/decision.d.ts +6 -0
- package/dist/decision.js +12 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispatch.d.ts +3 -1
- package/dist/dispatch.js +13 -0
- package/dist/dispose.js +11 -7
- package/dist/exhaustion.d.ts +14 -0
- package/dist/exhaustion.js +35 -0
- package/dist/install-method.d.ts +17 -0
- package/dist/install-method.js +61 -0
- package/dist/invoke.js +26 -0
- package/dist/lead-status.d.ts +7 -0
- package/dist/lead-status.js +15 -2
- package/dist/mate.js +5 -2
- package/dist/operate.d.ts +5 -0
- package/dist/operate.js +109 -15
- package/dist/provider-auth.d.ts +138 -0
- package/dist/provider-auth.js +291 -0
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +8 -1
- package/dist/releases.d.ts +76 -0
- package/dist/releases.js +225 -0
- package/dist/serve.d.ts +4 -0
- package/dist/serve.js +207 -76
- package/dist/setup-guide.d.ts +2 -2
- package/dist/setup-guide.js +2 -0
- package/dist/skills.d.ts +6 -0
- package/dist/skills.js +8 -0
- package/dist/store.d.ts +2 -2
- package/dist/store.js +13 -4
- package/dist/subscription-chat.js +3 -0
- package/dist/update-notice.d.ts +5 -0
- package/dist/update-notice.js +4 -0
- package/dist/work-index.js +22 -4
- package/dist/worktree.d.ts +13 -0
- package/dist/worktree.js +55 -29
- package/package.json +1 -1
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
+
content?: never;
|
|
18
19
|
ok: false;
|
|
19
20
|
message: string;
|
|
20
21
|
plan?: never;
|
|
21
|
-
content?: never;
|
|
22
22
|
fingerprint?: never;
|
|
23
23
|
installed?: never;
|
|
24
24
|
} | {
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
+
content?: never;
|
|
33
34
|
ok: false;
|
|
34
35
|
message: string;
|
|
35
36
|
plan?: never;
|
|
36
|
-
content?: never;
|
|
37
37
|
fingerprint?: never;
|
|
38
38
|
installed?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
package/dist/setup-guide.js
CHANGED
|
@@ -81,6 +81,8 @@ export function previewProjectInstructions(repo) {
|
|
|
81
81
|
const content = skillContent();
|
|
82
82
|
const current = existsSync(plan.skillPath) ? readFileSync(plan.skillPath, "utf8") : null;
|
|
83
83
|
const fingerprint = createHash("sha256").update(JSON.stringify({ repo, current, content })).digest("hex");
|
|
84
|
+
// Installed means the CURRENT instructions are in place: a stale copy, or one only in the folder from before the
|
|
85
|
+
// rename, still offers Review so applyInstall can bring it up to date (the first-run checklist counts either).
|
|
84
86
|
return { ok: true, plan, content, fingerprint, installed: current === content };
|
|
85
87
|
}
|
|
86
88
|
export function addProjectInstructions(repo, fingerprint) {
|
package/dist/skills.d.ts
CHANGED
|
@@ -33,6 +33,12 @@ export declare const CONTEXT_END = "<!-- standing-orders:end -->";
|
|
|
33
33
|
export declare function skillContent(): string;
|
|
34
34
|
/** The AGENTS.md block, marker-fenced so reinstalls replace only themselves. */
|
|
35
35
|
export declare function contextBlock(): string;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a repository has the Toolroll skill: a SKILL.md in the current
|
|
38
|
+
* folder, or in the one installs before the rename used. The setup guide
|
|
39
|
+
* and the console's first-run checklist both ask this, so they agree.
|
|
40
|
+
*/
|
|
41
|
+
export declare function projectSkillInstalled(repo: string): boolean;
|
|
36
42
|
export type InstallPlan = {
|
|
37
43
|
skillPath: string;
|
|
38
44
|
skillAction: "create" | "replace" | "refuse-foreign";
|
package/dist/skills.js
CHANGED
|
@@ -72,6 +72,14 @@ in the plane: read \`skills get knowledge\`, search it with
|
|
|
72
72
|
merge anything yourself.
|
|
73
73
|
${CONTEXT_END}`;
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether a repository has the Toolroll skill: a SKILL.md in the current
|
|
77
|
+
* folder, or in the one installs before the rename used. The setup guide
|
|
78
|
+
* and the console's first-run checklist both ask this, so they agree.
|
|
79
|
+
*/
|
|
80
|
+
export function projectSkillInstalled(repo) {
|
|
81
|
+
return [SKILL_DIR, LEGACY_SKILL_DIR].some(dir => existsSync(join(repo, dir, SKILL_FILE)));
|
|
82
|
+
}
|
|
75
83
|
/** What install would do, computed without writing anything. */
|
|
76
84
|
export function planInstall(repo, writeContext) {
|
|
77
85
|
const skillPath = join(repo, SKILL_DIR, SKILL_FILE);
|
package/dist/store.d.ts
CHANGED
|
@@ -74,7 +74,7 @@ export type SealedLedgerEntry = LedgerEntry & {
|
|
|
74
74
|
hash: string;
|
|
75
75
|
} | null;
|
|
76
76
|
};
|
|
77
|
-
export declare const SCHEMA_VERSION =
|
|
77
|
+
export declare const SCHEMA_VERSION = 109;
|
|
78
78
|
export type ApiTokenRow = {
|
|
79
79
|
id: string;
|
|
80
80
|
account: string;
|
|
@@ -4820,7 +4820,7 @@ export declare class Store {
|
|
|
4820
4820
|
* recovered-draft admission and the fallback admission's `recoveredFrom`:
|
|
4821
4821
|
* the interrupted attempt whose draft a fresh run inherits is THIS
|
|
4822
4822
|
* task's own builder or repair turn, left in this very worktree on this
|
|
4823
|
-
* branch, either ended as `failed` for `interrupted` or still open under
|
|
4823
|
+
* branch, either ended as `failed` for `interrupted` (or `no-handoff`) or still open under
|
|
4824
4824
|
* a lease nobody holds any more. A planner, scout, or reviewer; a
|
|
4825
4825
|
* parked, built, or otherwise failed run; a foreign workspace; or an
|
|
4826
4826
|
* attempt still being built under a live claim is no grant — the words
|
package/dist/store.js
CHANGED
|
@@ -64,6 +64,7 @@ import { LEDGER_SCHEMA, LEDGER_TABLE, LEDGER_V54_COLUMNS, LEDGER_V54_TABLE, inst
|
|
|
64
64
|
import { PLAN_AUTO_SCHEMA } from "./plan-auto.js";
|
|
65
65
|
import { RECIPE_SCHEMA } from "./recipes.js";
|
|
66
66
|
import { POLICY_SCHEMA, agentRefusal, approvalRefusal, attendedRefusal, policyParts, readPolicy, underCeiling, sessionCeilingRefusal } from "./policy.js";
|
|
67
|
+
import { PROVIDER_AUTH_SCHEMA } from "./provider-auth.js";
|
|
67
68
|
import { SPEND_SCHEMA, billingOf, budgetStates, canPrice, claudeMachineBilling, countsToward, filersOf, monthOf, priceWork, seenBilling as seenBillingOf, spendItems, teammateFilers, usd } from "./spend.js";
|
|
68
69
|
import { FOREVER, RETENTION_SCHEMA, periodWords } from "./retention.js";
|
|
69
70
|
import { IN_RANGE, LEDGER_CHAIN_SCHEMA, safeWhole, sealLedger, verifyLedgerChain } from "./ledger-chain.js";
|
|
@@ -249,7 +250,9 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
249
250
|
// v105 prices every run (reported, or tokens at the catalogue price) and keeps monthly budgets per project, person, teammate and installation.
|
|
250
251
|
// v106 keeps the organisation policy, retention periods, backup settings and runs, check progress, and runner capacity changes.
|
|
251
252
|
// v107 keeps a check's progress and result in one record (run_check absorbs check_progress).
|
|
252
|
-
|
|
253
|
+
// v108 keeps sign-in pauses: one incident per provider whose sign-in stopped working (provider_auth_pause).
|
|
254
|
+
// v109 remembers which sign-in pause parked a task (task_ref.auth_wait_pause), so reviews and other runs outside the tick wait for it too.
|
|
255
|
+
export const SCHEMA_VERSION = 109;
|
|
253
256
|
/** v102: a project's approval rules, and each person's approval of an exact scope (two are needed for protected work). */
|
|
254
257
|
const APPROVAL_SCHEMA = `
|
|
255
258
|
CREATE TABLE IF NOT EXISTS approval_policy (
|
|
@@ -3687,6 +3690,10 @@ function initializeStore(db, file) {
|
|
|
3687
3690
|
db.exec(LEDGER_CHAIN_SCHEMA);
|
|
3688
3691
|
db.exec(MONITORING_SCHEMA);
|
|
3689
3692
|
db.exec(SPEND_SCHEMA);
|
|
3693
|
+
// Sign-in pauses: one row per incident of a provider's sign-in no longer working.
|
|
3694
|
+
db.exec(PROVIDER_AUTH_SCHEMA);
|
|
3695
|
+
// The sign-in pause the dispatch gate last left this task waiting on (null: none), for the work index.
|
|
3696
|
+
addColumn(db, "task_ref", "auth_wait_pause", "INTEGER REFERENCES provider_auth_pause(id)");
|
|
3690
3697
|
db.exec(RETENTION_SCHEMA);
|
|
3691
3698
|
db.exec(BACKUP_SCHEMA);
|
|
3692
3699
|
// Sprint 8: the organisation policy (one row, or none: nothing restricted).
|
|
@@ -9785,7 +9792,7 @@ export class Store {
|
|
|
9785
9792
|
const row = this.db
|
|
9786
9793
|
.prepare(`SELECT id FROM run
|
|
9787
9794
|
WHERE worktree = ?
|
|
9788
|
-
AND (outcome IS NULL OR reason
|
|
9795
|
+
AND (outcome IS NULL OR reason IN ('interrupted', 'no-handoff') OR parent_run IS NOT NULL)
|
|
9789
9796
|
ORDER BY id DESC LIMIT 1`)
|
|
9790
9797
|
.get(path);
|
|
9791
9798
|
return row === undefined ? null : Number(row.id);
|
|
@@ -15614,7 +15621,7 @@ export class Store {
|
|
|
15614
15621
|
* recovered-draft admission and the fallback admission's `recoveredFrom`:
|
|
15615
15622
|
* the interrupted attempt whose draft a fresh run inherits is THIS
|
|
15616
15623
|
* task's own builder or repair turn, left in this very worktree on this
|
|
15617
|
-
* branch, either ended as `failed` for `interrupted` or still open under
|
|
15624
|
+
* branch, either ended as `failed` for `interrupted` (or `no-handoff`) or still open under
|
|
15618
15625
|
* a lease nobody holds any more. A planner, scout, or reviewer; a
|
|
15619
15626
|
* parked, built, or otherwise failed run; a foreign workspace; or an
|
|
15620
15627
|
* attempt still being built under a live claim is no grant — the words
|
|
@@ -15627,7 +15634,9 @@ export class Store {
|
|
|
15627
15634
|
return `run #${parent.id} is a ${parent.role} run — a recovered draft is a builder's or its repair turn's`;
|
|
15628
15635
|
if (parent.worktree !== run.worktree || parent.branch !== run.branch)
|
|
15629
15636
|
return `run #${parent.id} left its draft in ${parent.worktree ?? "no worktree"} on ${parent.branch ?? "no branch"}, not ${run.worktree} on ${run.branch}`;
|
|
15630
|
-
|
|
15637
|
+
// An attempt that stopped before its handoff (run 2085) left its work in
|
|
15638
|
+
// place for exactly this: the retry inherits it, like an interrupted one's.
|
|
15639
|
+
const interrupted = parent.outcome === null || (parent.outcome === "failed" && (parent.reason === "interrupted" || parent.reason === "no-handoff"));
|
|
15631
15640
|
if (!interrupted)
|
|
15632
15641
|
return `run #${parent.id} ended as ${parent.outcome}${parent.reason === null ? "" : ` (${parent.reason})`} — only an interrupted attempt's draft is recovered`;
|
|
15633
15642
|
if (parent.outcome === null && this.liveClaimByLease(parent.leaseId, now) !== null)
|
|
@@ -53,6 +53,9 @@ export function composeSubscriptionMatePrompt(request) {
|
|
|
53
53
|
request.system,
|
|
54
54
|
"SUBSCRIPTION HARNESS PROTOCOL:",
|
|
55
55
|
"Do not use any harness tools or inspect the computer. The only current state is DATA and TOOL RESULTS below.",
|
|
56
|
+
// The model tried host tools as its own tool calls, which the harness refuses, and sometimes then told the operator the
|
|
57
|
+
// project search "wasn't available" (gate run, 2026-09-30). Host tools are requested only through calls.
|
|
58
|
+
"Host tools are not tools you can call yourself here: calling one directly always fails. Request one only by listing it in calls of the JSON object you return. Every AVAILABLE HOST TOOL below is available this way; never say one is unavailable. For what a project's files or code say, request get_project_context.",
|
|
56
59
|
"Return the required JSON object. To request a host tool, append {id, name, argumentsJson} to calls; argumentsJson is the JSON serialization of that tool's argument object.",
|
|
57
60
|
"The host validates and runs those calls, then gives you another step. When finished, return a non-empty text and an empty calls array.",
|
|
58
61
|
`AVAILABLE HOST TOOLS:\n${JSON.stringify(request.tools)}`,
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** The update notice's words, shared by the server page and the console. Plain text; callers escape. */
|
|
2
|
+
export function updateNoticeWords(notice) {
|
|
3
|
+
return notice.security ? `Toolroll ${notice.version} is available, with security fixes` : `Toolroll ${notice.version} is available`;
|
|
4
|
+
}
|
package/dist/work-index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* not verify artifacts, inspect processes, or authorize task operations. */
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { readProjectAccess } from './project-access.js';
|
|
5
|
+
import { pauseOnTaskProviders, providerName, signInReason } from './provider-auth.js';
|
|
5
6
|
import { scopeTermsProblem } from './store.js';
|
|
6
7
|
export const WORK_INDEX_PAGE_LIMIT = 40;
|
|
7
8
|
export const WORK_INDEX_MAX_LIMIT = 100;
|
|
@@ -14,10 +15,12 @@ const s = (row, key) => row[key] == null ? null : String(row[key]);
|
|
|
14
15
|
* admitted child needs its parent's exact revision brief, in the same repo.
|
|
15
16
|
* All subsequent reads are scalar/indexed metadata, never artifact bytes.
|
|
16
17
|
* MATERIALIZED prevents counts + page selection repeating correlated reads. */
|
|
18
|
+
/** A gate note counts only while its pause is on one of the task's current providers. */
|
|
19
|
+
const GATE_PAUSE_ON_TASK = pauseOnTaskProviders('p', 'c.ref_id');
|
|
17
20
|
const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
18
21
|
SELECT t.id, t.title, t.state, t.created_at, t.updated_at, r.id ref_id, r.repo,
|
|
19
22
|
r.revision_of, r.revision_brief_artifact, r.assigned_runner, r.plan,
|
|
20
|
-
r.deliverable, r.coordinator_cid, r.capability_requirements, r.park_rate
|
|
23
|
+
r.deliverable, r.coordinator_cid, r.capability_requirements, r.park_rate, r.auth_wait_pause
|
|
21
24
|
FROM task_ref r JOIN task t ON t.id = r.external_id
|
|
22
25
|
WHERE r.backend = 'built-in' AND ((r.repo IS NULL AND $unplaced = 1)
|
|
23
26
|
OR (r.repo IS NOT NULL AND ($all = 1 OR r.repo IN (SELECT value FROM json_each($repos)))))
|
|
@@ -82,6 +85,14 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
82
85
|
(SELECT id FROM run WHERE task_ref=c.ref_id AND lease_id=c.live_lease AND outcome IS NULL ORDER BY id DESC LIMIT 1) live_run,
|
|
83
86
|
h.owner_kind hold_kind,h.reason hold_reason,h.until hold_until,
|
|
84
87
|
sc.task_id scope_id,sc.digest,sc.approved_at,sc.proposed_at,sc.profile_state,
|
|
88
|
+
-- The pause the dispatch gate itself left this task waiting on (the provider it resolved, pins,
|
|
89
|
+
-- configuration and planners included) first; the approved terms answer before any gate has looked.
|
|
90
|
+
EXISTS(SELECT 1 FROM provider_auth_pause p WHERE p.id=c.auth_wait_pause AND p.lifted_at IS NULL AND ${GATE_PAUSE_ON_TASK}) gate_signed_out,
|
|
91
|
+
COALESCE((SELECT p.provider||':'||p.auth_mode FROM provider_auth_pause p WHERE p.id=c.auth_wait_pause AND p.lifted_at IS NULL AND ${GATE_PAUSE_ON_TASK}),
|
|
92
|
+
(SELECT p.provider||':'||p.auth_mode FROM provider_auth_pause p WHERE p.lifted_at IS NULL AND p.provider IN (
|
|
93
|
+
SELECT json_extract(sc.approved_profile_json,'$.provider') WHERE json_valid(sc.approved_profile_json)
|
|
94
|
+
UNION ALL SELECT json_extract(leg.value,'$.provider') FROM json_each(CASE WHEN json_valid(sc.approved_route_json) THEN sc.approved_route_json ELSE '{}' END,'$.legs') leg
|
|
95
|
+
WHERE json_extract(leg.value,'$.phase')='build'))) signed_out,
|
|
85
96
|
(sc.approved_at IS NOT NULL AND sc.approved_by IS NOT NULL AND sc.approved_digest=sc.digest) approved,
|
|
86
97
|
(sc.approval_basis IS NOT 'mode' OR EXISTS(SELECT 1 FROM operating_mode om JOIN approver signer ON signer.name=om.signed_by
|
|
87
98
|
WHERE om.repo=c.repo AND om.digest=sc.mode_digest AND om.revoked_at IS NULL AND om.absolute_expiry>$now
|
|
@@ -176,15 +187,16 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
176
187
|
WHEN worker_count=0 THEN 'no-worker-registered'
|
|
177
188
|
WHEN online_count=0 THEN 'no-worker-online'
|
|
178
189
|
WHEN available_count=0 THEN 'worker-at-capacity'
|
|
190
|
+
WHEN signed_out IS NOT NULL AND (gate_signed_out OR NOT (plan='requested' AND NOT COALESCE(approved AND dispatch_approval,0))) THEN 'signed-out'
|
|
179
191
|
WHEN (plan='requested' OR park_rate>0) AND (SELECT COUNT(*) FROM decision WHERE state<>'answered' AND answered_at IS NULL)>=5 THEN 'decision-queue'
|
|
180
192
|
WHEN plan='requested' AND NOT COALESCE(approved AND dispatch_approval,0) THEN 'planning-ready'
|
|
181
193
|
WHEN deliverable='report' THEN 'scouting-ready'
|
|
182
194
|
ELSE 'queued' END code
|
|
183
195
|
FROM checked f
|
|
184
196
|
), ranked AS MATERIALIZED (
|
|
185
|
-
SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') needs,
|
|
197
|
+
SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','signed-out','planning-ready','scouting-ready','queued') needs,
|
|
186
198
|
CASE WHEN code='complete' THEN 3 WHEN code='cancelled' THEN 4 WHEN code='running' THEN 1
|
|
187
|
-
WHEN code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') THEN 2 ELSE 0 END rank,
|
|
199
|
+
WHEN code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','signed-out','planning-ready','scouting-ready','queued') THEN 2 ELSE 0 END rank,
|
|
188
200
|
CASE WHEN code='complete' THEN completed.at ELSE family_updated END sort_at,
|
|
189
201
|
completed.actor checked_actor,completed.at checked_at,completed.outcome checked_digest
|
|
190
202
|
FROM classified c LEFT JOIN action_ledger completed ON completed.id=c.checked_id WHERE ($state IS NULL OR c.state=$state) AND ($leadId IS NULL OR EXISTS(SELECT 1 FROM team_task_owner own WHERE own.task_ref=c.root_ref AND own.lead=$leadId))
|
|
@@ -283,7 +295,7 @@ export function workIndexPage(store, now, access, options = {}) {
|
|
|
283
295
|
'earlier_id',earlier_id,'broken',broken,'live_run',live_run,'result_id',page_result_id,'result_outcome',(SELECT outcome FROM run WHERE id=page_result_id),
|
|
284
296
|
'publication_url',(SELECT pr_url FROM publication WHERE run=page_result_id),'code',code,'needs',needs,'family_running',family_running,'rank',rank,'sort_at',sort_at,
|
|
285
297
|
'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'question_id',question_id,
|
|
286
|
-
'dependency_id',dependency_id,'dependency_state',dependency_state,'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,300),'stop_run',stop_run,'unfinished',unfinished,
|
|
298
|
+
'dependency_id',dependency_id,'dependency_state',dependency_state,'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,300),'stop_run',stop_run,'unfinished',unfinished,'signed_out',signed_out,
|
|
287
299
|
'question_run',(SELECT run FROM decision WHERE id=page.question_id),
|
|
288
300
|
'question_task',(SELECT r.external_id FROM decision d JOIN run ON run.id=d.run JOIN task_ref r ON r.id=run.task_ref WHERE d.id=page.question_id)) FROM page`)
|
|
289
301
|
.all({ ...parameters(now, access, options), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
|
|
@@ -336,6 +348,7 @@ function itemOf(row, principal) {
|
|
|
336
348
|
'no-worker-registered': ['Needs your decision', 'No connected builder includes this project.', 'start-worker', 'Check connection'],
|
|
337
349
|
'no-worker-online': ['Needs your decision', 'The builder has stopped checking in.', 'start-worker', 'Check connection'],
|
|
338
350
|
'worker-at-capacity': ['Waiting for worker capacity', 'This task starts when an eligible worker has capacity.', null, ''],
|
|
351
|
+
'signed-out': ['Waiting for a sign-in', 'Its agent needs you to sign in again; it starts on its own once that works.', null, ''],
|
|
339
352
|
'retry-scheduled': ['Retry scheduled', 'The attempt is backing off before retrying.', null, ''],
|
|
340
353
|
updating: ['Waiting for app update', 'New work resumes after the update.', null, ''],
|
|
341
354
|
'planning-ready': ['Planner ready', 'A connected worker can draft the plan.', null, ''],
|
|
@@ -343,6 +356,11 @@ function itemOf(row, principal) {
|
|
|
343
356
|
queued: ['Ready to run', 'A connected worker can claim this task.', null, ''],
|
|
344
357
|
};
|
|
345
358
|
let [label, detail, actionCode, actionLabel] = words[code];
|
|
359
|
+
if (code === 'signed-out') {
|
|
360
|
+
const [provider = '', authMode = 'subscription'] = (s(row, 'signed_out') ?? '').split(':');
|
|
361
|
+
label = signInReason({ provider: provider, authMode: authMode === 'api-key' ? 'api-key' : 'subscription' });
|
|
362
|
+
detail = `It starts again on its own once ${providerName(provider)} works.`;
|
|
363
|
+
}
|
|
346
364
|
if (code === 'running' && n(row, 'version_count') > 1)
|
|
347
365
|
label = 'Revising';
|
|
348
366
|
if (code === 'terminal-dependency' && s(row, 'dependency_id') !== null)
|
package/dist/worktree.d.ts
CHANGED
|
@@ -95,6 +95,19 @@ export declare function worktreeProcessOccupancy(path: string): {
|
|
|
95
95
|
} | {
|
|
96
96
|
held: false;
|
|
97
97
|
};
|
|
98
|
+
/**
|
|
99
|
+
* A checkout's uncommitted work as one patch — tracked changes against HEAD,
|
|
100
|
+
* then each untracked file against nothing (the lease note aside) — written
|
|
101
|
+
* to `file`. Nothing in the checkout is touched. Every road that is about to
|
|
102
|
+
* reset or clean a tree saves this first.
|
|
103
|
+
*/
|
|
104
|
+
export declare function saveWorkPatch(runner: Runner, path: string, file: string, header: string): Promise<{
|
|
105
|
+
ok: true;
|
|
106
|
+
file: string;
|
|
107
|
+
} | {
|
|
108
|
+
ok: false;
|
|
109
|
+
message: string;
|
|
110
|
+
}>;
|
|
98
111
|
export declare class WorktreePool {
|
|
99
112
|
private readonly store;
|
|
100
113
|
private readonly options;
|
package/dist/worktree.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { createHash, randomBytes } from "node:crypto";
|
|
26
26
|
import { existsSync, readFileSync, writeFileSync, rmSync, realpathSync, mkdirSync, readdirSync } from "node:fs";
|
|
27
27
|
import { hostname } from "node:os";
|
|
28
|
-
import { basename, join } from "node:path";
|
|
28
|
+
import { basename, dirname, join } from "node:path";
|
|
29
29
|
import { run } from "./exec.js";
|
|
30
30
|
import { currentBootId, provenDeadByBootChange } from "./boot-identity.js";
|
|
31
31
|
import { HANDOFF_PREFIX, MAILBOX_SUFFIX, readMailbox } from "./evidence.js";
|
|
@@ -134,6 +134,40 @@ export function worktreeProcessOccupancy(path) {
|
|
|
134
134
|
return code === "ESRCH" ? { held: false } : { held: true, by: pid };
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* A checkout's uncommitted work as one patch — tracked changes against HEAD,
|
|
139
|
+
* then each untracked file against nothing (the lease note aside) — written
|
|
140
|
+
* to `file`. Nothing in the checkout is touched. Every road that is about to
|
|
141
|
+
* reset or clean a tree saves this first.
|
|
142
|
+
*/
|
|
143
|
+
export async function saveWorkPatch(runner, path, file, header) {
|
|
144
|
+
const status = await runner(GIT, [...READ_ONLY, "status", "--porcelain", "--untracked-files=all"], { cwd: path, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
145
|
+
if (status.code !== 0)
|
|
146
|
+
return { ok: false, message: `${path} could not be inspected before its work was saved` };
|
|
147
|
+
const untracked = status.stdout
|
|
148
|
+
.split("\n")
|
|
149
|
+
.filter(line => line.startsWith("?? ") && !line.trimEnd().endsWith(MARKER))
|
|
150
|
+
.map(line => line.slice(3).trim());
|
|
151
|
+
const tracked = await runner(GIT, [...READ_ONLY, "diff", "--binary", "HEAD"], { cwd: path, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
152
|
+
if (tracked.code !== 0)
|
|
153
|
+
return { ok: false, message: `${path}: git diff failed while saving its work (${firstLine(tracked.stderr)})` };
|
|
154
|
+
const parts = [tracked.stdout];
|
|
155
|
+
for (const one of untracked) {
|
|
156
|
+
// --no-index exits 1 when the sides differ; that is the expected answer.
|
|
157
|
+
const diff = await runner(GIT, [...READ_ONLY, "diff", "--binary", "--no-index", "--", "/dev/null", one], { cwd: path, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
158
|
+
if (diff.code > 1)
|
|
159
|
+
return { ok: false, message: `${path}: ${one} could not be captured (${firstLine(diff.stderr)})` };
|
|
160
|
+
parts.push(diff.stdout);
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
|
|
164
|
+
writeFileSync(file, header + parts.join(""), { mode: 0o600 });
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
return { ok: false, message: `${path}: its work could not be written to ${file} (${error instanceof Error ? error.message : String(error)})` };
|
|
168
|
+
}
|
|
169
|
+
return { ok: true, file };
|
|
170
|
+
}
|
|
137
171
|
export class WorktreePool {
|
|
138
172
|
store;
|
|
139
173
|
options;
|
|
@@ -271,7 +305,7 @@ export class WorktreePool {
|
|
|
271
305
|
}
|
|
272
306
|
const previousRun = this.store.latestRunInWorktree(path);
|
|
273
307
|
const recoveryRun = this.store.latestRecoverableRunInWorktree(path);
|
|
274
|
-
const recovery = recoveryRun === null ? null : this.recoverableDraft(path, recoveryRun);
|
|
308
|
+
const recovery = recoveryRun === null ? null : this.recoverableDraft(path, recoveryRun, previousRun);
|
|
275
309
|
const kept = await this.keepLeftover(path, request.reclaim.evidenceRoot, request.now);
|
|
276
310
|
if (!kept.ok)
|
|
277
311
|
return { ok: false, reason: "git", message: kept.message };
|
|
@@ -354,7 +388,7 @@ export class WorktreePool {
|
|
|
354
388
|
* the old result. It classifies the recovery context for the next freshly
|
|
355
389
|
* fenced attempt, which must review and prove the inherited work itself.
|
|
356
390
|
*/
|
|
357
|
-
recoverableDraft(path, runId) {
|
|
391
|
+
recoverableDraft(path, runId, latestRun) {
|
|
358
392
|
const prior = this.store.getRun(runId);
|
|
359
393
|
if (prior === null)
|
|
360
394
|
return null;
|
|
@@ -368,6 +402,23 @@ export class WorktreePool {
|
|
|
368
402
|
return { runId: source.id, kind: "partial" };
|
|
369
403
|
}
|
|
370
404
|
}
|
|
405
|
+
// An attempt that stopped before its handoff (run 2085) left work it
|
|
406
|
+
// never got to hand off: kept in place for the next attempt, never reset
|
|
407
|
+
// — until a later attempt fails some other way. That failure may have
|
|
408
|
+
// broken the tree, and the work is already saved (its own patch, and the
|
|
409
|
+
// leftover patch the caller writes first), so the tree can be reset. A
|
|
410
|
+
// crash or a sign-in that stopped working says nothing about the tree.
|
|
411
|
+
// A resumed or repair turn is judged as part of the attempt it belongs to.
|
|
412
|
+
const attemptOf = (one) => one !== null && one.role === "repair" && one.parentRun !== null ? this.store.getRun(one.parentRun) ?? one : one;
|
|
413
|
+
const brokeTree = (one) => one !== null && one.outcome === "failed" && one.reason !== "no-handoff" && one.reason !== "interrupted" && one.reason !== "auth-expired";
|
|
414
|
+
const attempt = attemptOf(prior);
|
|
415
|
+
const later = latestRun !== null && latestRun > runId ? attemptOf(this.store.getRun(latestRun)) : null;
|
|
416
|
+
const unhanded = attempt.outcome === "failed" && attempt.reason === "no-handoff"
|
|
417
|
+
? attempt
|
|
418
|
+
: attempt.parentRun === null ? null : this.store.getRun(attempt.parentRun);
|
|
419
|
+
if (unhanded !== null && unhanded.outcome === "failed" && unhanded.reason === "no-handoff") {
|
|
420
|
+
return brokeTree(attempt) || brokeTree(later) ? null : { runId: unhanded.id, kind: "partial" };
|
|
421
|
+
}
|
|
371
422
|
if (prior.outcome !== null && (prior.outcome !== "failed" || prior.reason !== "interrupted"))
|
|
372
423
|
return null;
|
|
373
424
|
let names;
|
|
@@ -395,35 +446,10 @@ export class WorktreePool {
|
|
|
395
446
|
* run is on record). Nothing is deleted here.
|
|
396
447
|
*/
|
|
397
448
|
async keepLeftover(path, evidenceRoot, now) {
|
|
398
|
-
const status = await this.runner(GIT, [...READ_ONLY, "status", "--porcelain", "--untracked-files=all"], { cwd: path, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
399
|
-
if (status.code !== 0)
|
|
400
|
-
return { ok: false, message: `${path} could not be inspected before reclaiming it` };
|
|
401
|
-
const untracked = status.stdout
|
|
402
|
-
.split("\n")
|
|
403
|
-
.filter(line => line.startsWith("?? ") && !line.trimEnd().endsWith(MARKER))
|
|
404
|
-
.map(line => line.slice(3).trim());
|
|
405
|
-
const tracked = await this.runner(GIT, [...READ_ONLY, "diff", "--binary", "HEAD"], { cwd: path, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
406
|
-
if (tracked.code !== 0)
|
|
407
|
-
return { ok: false, message: `${path}: git diff failed while reclaiming (${firstLine(tracked.stderr)})` };
|
|
408
|
-
const parts = [tracked.stdout];
|
|
409
|
-
for (const file of untracked) {
|
|
410
|
-
// --no-index exits 1 when the sides differ; that is the expected answer.
|
|
411
|
-
const one = await this.runner(GIT, [...READ_ONLY, "diff", "--binary", "--no-index", "--", "/dev/null", file], { cwd: path, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
412
|
-
if (one.code > 1)
|
|
413
|
-
return { ok: false, message: `${path}: ${file} could not be captured (${firstLine(one.stderr)})` };
|
|
414
|
-
parts.push(one.stdout);
|
|
415
|
-
}
|
|
416
449
|
const lastRun = this.store.latestRunInWorktree(path);
|
|
417
450
|
const dir = lastRun === null ? join(evidenceRoot, "leftover") : join(evidenceRoot, String(lastRun));
|
|
418
451
|
const file = join(dir, lastRun === null ? `${now.toISOString().replace(/[:.]/g, "-")}.patch` : "leftover.patch");
|
|
419
|
-
|
|
420
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
421
|
-
writeFileSync(file, `# leftover from ${path}\n# kept ${now.toISOString()} before the tree was reset for the next attempt\n` + parts.join(""), { mode: 0o600 });
|
|
422
|
-
}
|
|
423
|
-
catch (error) {
|
|
424
|
-
return { ok: false, message: `${path}: the leftover could not be written to ${file} (${error instanceof Error ? error.message : String(error)})` };
|
|
425
|
-
}
|
|
426
|
-
return { ok: true, file };
|
|
452
|
+
return saveWorkPatch(this.runner, path, file, `# leftover from ${path}\n# kept ${now.toISOString()} before the tree was reset for the next attempt\n`);
|
|
427
453
|
}
|
|
428
454
|
/** Back to HEAD, untracked gone, our lease note kept; proven clean after. */
|
|
429
455
|
async resetTree(path) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
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"
|