toolroll 0.9.62 → 0.9.65
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 +80 -0
- package/README.md +66 -8
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +203 -10
- package/dist/agent-fence.js +623 -41
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +36 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +48 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +117 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +263 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +6 -1
- package/dist/builder.js +34 -13
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +199 -17
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/check-levels.js +3 -0
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +20 -1
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +5 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +24 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/cli.d.ts +8 -0
- package/dist/contracts/cli.js +3 -1
- package/dist/contracts/console-api.d.ts +30 -4
- package/dist/contracts/console-api.js +13 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/integration-metadata.d.ts +2 -1
- package/dist/contracts/integration-metadata.js +2 -1
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/discord-chat.js +4 -1
- package/dist/dispatch.js +7 -4
- package/dist/doctor.d.ts +53 -0
- package/dist/doctor.js +427 -0
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/evidence-pack.d.ts +1 -1
- package/dist/evidence-pack.js +2 -2
- package/dist/exec.d.ts +81 -0
- package/dist/exec.js +368 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-code.d.ts +2 -2
- package/dist/flow-code.js +4 -15
- package/dist/flow-engine.js +35 -11
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +84 -11
- package/dist/flow-triggers.js +554 -87
- package/dist/flow-unattended-words.d.ts +28 -0
- package/dist/flow-unattended-words.js +50 -0
- package/dist/flow-unattended.d.ts +227 -0
- package/dist/flow-unattended.js +776 -0
- package/dist/flows-cli.d.ts +28 -1
- package/dist/flows-cli.js +63 -12
- package/dist/flows-ui.d.ts +2 -3
- package/dist/flows-ui.js +56 -5
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/google-mail.d.ts +1 -0
- package/dist/google-mail.js +61 -18
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +6 -3
- package/dist/integrations.d.ts +17 -3
- package/dist/integrations.js +38 -18
- package/dist/invoke.d.ts +17 -0
- package/dist/invoke.js +510 -182
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/landed-cleanup.d.ts +3 -2
- package/dist/landed-cleanup.js +12 -5
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +163 -4
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +258 -0
- package/dist/live-terminal.js +649 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-connect.d.ts +8 -7
- package/dist/mcp-connect.js +49 -51
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/oauth-run-token.d.ts +51 -0
- package/dist/oauth-run-token.js +127 -0
- package/dist/operate.d.ts +2 -0
- package/dist/operate.js +282 -18
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/plan-auto.d.ts +11 -0
- package/dist/plan-auto.js +62 -11
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/process-recovery-eligibility.d.ts +1 -1
- package/dist/project-access.d.ts +3 -0
- package/dist/project-access.js +3 -0
- package/dist/project-delete-ui.d.ts +12 -0
- package/dist/project-delete-ui.js +41 -9
- package/dist/project-delete.d.ts +3 -1
- package/dist/project-delete.js +12 -6
- package/dist/project-file.d.ts +3 -0
- package/dist/project-file.js +16 -0
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +103 -27
- package/dist/project-tools.js +399 -100
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider-home.d.ts +22 -0
- package/dist/provider-home.js +99 -0
- package/dist/provider.d.ts +42 -5
- package/dist/provider.js +97 -19
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.js +14 -8
- package/dist/remote-main.d.ts +4 -0
- package/dist/remote-main.js +3 -3
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +171 -6
- package/dist/scope.js +251 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +258 -43
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +6 -1
- package/dist/server/chrome.js +18 -7
- package/dist/server/console.css +52 -38
- package/dist/server/flows.js +65 -13
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +4 -1
- package/dist/server/render-pages.js +5 -4
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +110 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +49 -5
- package/dist/server/runtime.d.ts +34 -3
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +132 -13
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +320 -74
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-sweep-child.js +3 -1
- package/dist/storage-sweep-process.d.ts +12 -5
- package/dist/storage-sweep-process.js +23 -6
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +466 -19
- package/dist/store.js +1185 -63
- package/dist/subagent-desk.js +1 -1
- package/dist/subagent-tools.d.ts +1 -1
- package/dist/summary.js +3 -0
- package/dist/surface.js +6 -4
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/teardown-git.d.ts +14 -0
- package/dist/teardown-git.js +119 -0
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +39 -0
- package/dist/tool-broker.js +370 -0
- package/dist/tool-launcher.js +5 -4
- package/dist/tool-secret-store.d.ts +170 -0
- package/dist/tool-secret-store.js +834 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +14 -3
- package/dist/tools-ui.d.ts +7 -1
- package/dist/tools-ui.js +19 -10
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +21 -5
- package/dist/workspace-revision.js +3 -1
- package/dist/worktree.d.ts +10 -4
- package/dist/worktree.js +70 -29
- package/package.json +13 -2
- package/scripts/provider-canary.mjs +210 -31
package/dist/work-index.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export type WorkIndexOptions = {
|
|
|
47
47
|
state?: TaskState;
|
|
48
48
|
leadId?: string;
|
|
49
49
|
root?: string;
|
|
50
|
+
/** Only these families (by root task id): the sidebar reads its rows' own words this way. */
|
|
51
|
+
roots?: readonly string[];
|
|
50
52
|
};
|
|
51
53
|
/** How the list is grouped: the Needs you rows by their ask, then building, then the rest. */
|
|
52
54
|
export type WorkIndexGroup = Ask | 'building' | 'rest';
|
|
@@ -85,6 +87,33 @@ export declare function workIndexPage(store: Store, now: Date, access: WorkSumma
|
|
|
85
87
|
* permission-bound and bypasses pagination, never action-time verification. */
|
|
86
88
|
export declare function workIndexTask(store: Store, taskId: string, now: Date, access: WorkSummaryAccess, root?: string): WorkIndexItem | null;
|
|
87
89
|
export declare function workCountsByProject(store: Store, now: Date, access: WorkSummaryAccess): WorkProjectCounts[];
|
|
90
|
+
/** One row per task family the person may see (the Tasks list's own projection: current version, admission, unplaced
|
|
91
|
+
* work and the ceiling as the list applies them), with what the cockpit's states read (agent-state.ts): its code, whether
|
|
92
|
+
* it waits on a person, whether a lead took it on, and its live, unfinished and result runs. Every family, no page. */
|
|
93
|
+
export type WorkStateRow = {
|
|
94
|
+
repo: string | null;
|
|
95
|
+
rootId: string;
|
|
96
|
+
code: string;
|
|
97
|
+
needs: boolean;
|
|
98
|
+
personNeed: boolean;
|
|
99
|
+
leadClaim: boolean;
|
|
100
|
+
liveRun: number | null;
|
|
101
|
+
unfinished: number | null;
|
|
102
|
+
resultRun: number | null;
|
|
103
|
+
/** The Tasks list ranks it as waiting its turn (custody confirmations from another computer included). */
|
|
104
|
+
waits: boolean;
|
|
105
|
+
/** What it needs now, as an identity: its code and the question, hold, result or stop it is about. A new one of
|
|
106
|
+
* these — or a changed plan or approval terms (its digests) — is a new thing to need a person, even on the same task. */
|
|
107
|
+
version: string;
|
|
108
|
+
/** When what it needs now began: the newest of its plan's proposal, its question, its hold, its result and its stop
|
|
109
|
+
* (each fixed once written, so an unrelated edit never moves it). Newer asks are named first. */
|
|
110
|
+
at: string;
|
|
111
|
+
/** The agent its ask is about: the live one, or the one whose question or decision it parked on. */
|
|
112
|
+
askRun: number | null;
|
|
113
|
+
/** Every task_ref of the family (its versions), for counting each of its live attempts. */
|
|
114
|
+
members: number[];
|
|
115
|
+
};
|
|
116
|
+
export declare function workStateRows(store: Store, now: Date, access: WorkSummaryAccess): WorkStateRow[];
|
|
88
117
|
/** Families whose headline is Ready for review, newest first: the Tasks
|
|
89
118
|
* list's own rows, so a completed family (root or any revision), a failed
|
|
90
119
|
* check and a superseded version never count. */
|
package/dist/work-index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { leadNameOf } from './lead-identity.js';
|
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
-
import { readProjectAccess } from './project-access.js';
|
|
8
|
+
import { accountReachesSql, readProjectAccess } from './project-access.js';
|
|
9
9
|
import { pauseOnTaskProviders, providerName, signInReason } from './provider-auth.js';
|
|
10
10
|
import { scopeTermsProblem, stopFactOf } from './store.js';
|
|
11
11
|
import { assignmentChecksForRun } from './assignment.js';
|
|
@@ -19,6 +19,7 @@ import { withFollowUps } from './result-follow-ups.js';
|
|
|
19
19
|
import { LANDED_ACTOR, landedReasonOf } from './result-completion.js';
|
|
20
20
|
import { ASKS, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, processNeedOf, resultHoldUpSentence } from './needs-you.js';
|
|
21
21
|
import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
|
|
22
|
+
import { grantStandsSql } from './flow-unattended.js';
|
|
22
23
|
export const WORK_INDEX_PAGE_LIMIT = 40;
|
|
23
24
|
export const WORK_INDEX_MAX_LIMIT = 100;
|
|
24
25
|
const EMPTY = { all: 0, 'needs-you': 0, running: 0, completed: 0 };
|
|
@@ -134,9 +135,10 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
|
|
|
134
135
|
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
|
|
135
136
|
WHERE json_extract(leg.value,'$.phase')='build'))) signed_out,
|
|
136
137
|
(sc.approved_at IS NOT NULL AND sc.approved_by IS NOT NULL AND sc.approved_digest=sc.digest) approved,
|
|
137
|
-
(sc.approval_basis IS NOT 'mode' OR EXISTS(SELECT 1 FROM operating_mode om JOIN approver signer ON signer.name=om.signed_by
|
|
138
|
+
((sc.approval_basis IS NOT 'mode' AND sc.approval_basis IS NOT 'grant') OR (sc.approval_basis='mode' AND EXISTS(SELECT 1 FROM operating_mode om JOIN approver signer ON signer.name=om.signed_by
|
|
138
139
|
WHERE om.repo=c.repo AND om.digest=sc.mode_digest AND om.revoked_at IS NULL AND om.absolute_expiry>$now
|
|
139
|
-
AND signer.role='approver' AND signer.revoked_at IS NULL))
|
|
140
|
+
AND signer.role='approver' AND signer.revoked_at IS NULL AND ${accountReachesSql("signer", "c.repo")}))
|
|
141
|
+
OR (sc.approval_basis='grant' AND ${grantStandsSql("sc.mode_digest", "$now", "c.repo")})) dispatch_approval,
|
|
140
142
|
CASE WHEN sc.task_id IS NULL THEN 0 ELSE so_work_scope_valid(json_object(
|
|
141
143
|
'touches',sc.touches,'acceptance_json',sc.acceptance_json,'budget_microusd',sc.budget_microusd,
|
|
142
144
|
'goal',CASE WHEN typeof(sc.goal)='text' THEN '' ELSE sc.goal END,
|
|
@@ -386,7 +388,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
386
388
|
const scope = cursorScope(access, options, view), cursor = readCursor(options.cursor, scope);
|
|
387
389
|
const filter = view === 'needs-you' ? 'needs=1' : view === 'running' ? 'family_running=1' : view === 'completed' ? "code='complete'" : '1';
|
|
388
390
|
const rows = prepared(store, `${projection}, selected_page AS MATERIALIZED (
|
|
389
|
-
SELECT * FROM ranked WHERE ${filter} AND ($taskId IS NULL OR root_ref IN (SELECT root_ref FROM members WHERE id=$taskId)) AND ($cursorRoot=0 OR
|
|
391
|
+
SELECT * FROM ranked WHERE ${filter} AND ($taskId IS NULL OR root_ref IN (SELECT root_ref FROM members WHERE id=$taskId)) AND ($roots IS NULL OR root_id IN (SELECT value FROM json_each($roots))) AND ($cursorRoot=0 OR
|
|
390
392
|
${ORDER}>$cursorRank OR (${ORDER}=$cursorRank AND (sort_at<$cursorAt OR (sort_at=$cursorAt AND root_ref<$cursorRoot))))
|
|
391
393
|
ORDER BY ${ORDER},sort_at DESC,root_ref DESC LIMIT $limit
|
|
392
394
|
), page AS (SELECT p.*,COALESCE(p.result_id,(SELECT id FROM run WHERE task_ref=p.ref_id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1)) page_result_id FROM selected_page p), totals AS (SELECT ${TOTALS},${GROUP_TOTALS} FROM ranked), project_totals AS (
|
|
@@ -413,7 +415,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
|
|
|
413
415
|
'agent_ask_run',(SELECT run FROM agent_question WHERE id=page.agent_ask),'agent_ask_text',(SELECT substr(question,1,300) FROM agent_question WHERE id=page.agent_ask),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
|
|
414
416
|
'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),
|
|
415
417
|
'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`)
|
|
416
|
-
.all({ ...parameters(now, access, options, custody), $taskId: taskId, $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
|
|
418
|
+
.all({ ...parameters(now, access, options, custody), $taskId: taskId, $roots: options.roots === undefined ? null : JSON.stringify(options.roots), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
|
|
417
419
|
const selected = rows.slice(1).map(row => JSON.parse(String(row['row_json'])));
|
|
418
420
|
const page = selected.slice(0, limit), last = page.at(-1);
|
|
419
421
|
const nextCursor = selected.length > limit && last !== undefined ? Buffer.from(JSON.stringify({ version: 2, scope,
|
|
@@ -440,6 +442,20 @@ export function workCountsByProject(store, now, access) {
|
|
|
440
442
|
.all({ ...parameters(now, access, {}, custodyReadings(store)), $recent: new Date(now.getTime() - 86_400_000).toISOString() })
|
|
441
443
|
.map(row => ({ repo: s(row, 'repo'), totals: counts(row), queued: n(row, 'queued'), doneRecently: n(row, 'done_recently') }));
|
|
442
444
|
}
|
|
445
|
+
export function workStateRows(store, now, access) {
|
|
446
|
+
const projection = registerValidators(store);
|
|
447
|
+
return store.handle.prepare(`${projection} SELECT repo,root_id,code,needs,person_need,lead_claim,live_run,unfinished,result_id,rank,
|
|
448
|
+
code||':'||COALESCE(question_id,'')||':'||COALESCE(agent_ask,'')||':'||COALESCE(hold_id,'')||':'||COALESCE(result_id,'')||':'||COALESCE(stop_run,'')||':'||COALESCE(digest,'')||':'||COALESCE(scope_digest,'') version,
|
|
449
|
+
COALESCE(NULLIF(MAX(COALESCE(proposed_at,''),COALESCE(finished_at,''),
|
|
450
|
+
COALESCE((SELECT created_at FROM decision WHERE id=question_id),''),COALESCE((SELECT asked_at FROM agent_question WHERE id=agent_ask),''),
|
|
451
|
+
COALESCE((SELECT held_at FROM hold WHERE id=hold_id),''),COALESCE((SELECT requested_at FROM run_stop WHERE run=stop_run),'')),''),sort_at) ask_at,
|
|
452
|
+
COALESCE(live_run,(SELECT run FROM decision WHERE id=question_id),(SELECT run FROM agent_question WHERE id=agent_ask)) ask_run,
|
|
453
|
+
(SELECT json_group_array(m.ref_id) FROM members m WHERE m.root_ref=ranked.root_ref) members FROM ranked`)
|
|
454
|
+
.all(parameters(now, access, {}, custodyReadings(store)))
|
|
455
|
+
.map(row => ({ repo: s(row, 'repo'), rootId: String(row['root_id']), code: String(row['code']), needs: n(row, 'needs') === 1, personNeed: n(row, 'person_need') === 1,
|
|
456
|
+
leadClaim: n(row, 'lead_claim') === 1, liveRun: row['live_run'] == null ? null : n(row, 'live_run'), unfinished: row['unfinished'] == null ? null : n(row, 'unfinished'),
|
|
457
|
+
resultRun: row['result_id'] == null ? null : n(row, 'result_id'), waits: Number(row['rank']) === 2, version: String(row['version']), at: String(row['ask_at'] ?? ''), askRun: row['ask_run'] == null ? null : n(row, 'ask_run'), members: JSON.parse(String(row['members'] ?? '[]')).map(Number) }));
|
|
458
|
+
}
|
|
443
459
|
/** Families whose headline is Ready for review, newest first: the Tasks
|
|
444
460
|
* list's own rows, so a completed family (root or any revision), a failed
|
|
445
461
|
* check and a superseded version never count. */
|
|
@@ -6,7 +6,9 @@ const MAX_AGE_MS = 60_000;
|
|
|
6
6
|
const OMIT = new Set(['schema_version', 'service_cursor', 'wake', 'notification_delivery', 'sqlite_sequence',
|
|
7
7
|
'team_read', 'team_request', 'push_delivery', 'chat_message_ref',
|
|
8
8
|
// Who is watching an agent right now: renewed by every open page's keep-alive, never news.
|
|
9
|
-
'agent_view'
|
|
9
|
+
'agent_view',
|
|
10
|
+
// A run's How it ran, derived from its own records on first view (run-efficiency.ts): a cache, never news.
|
|
11
|
+
'run_efficiency']);
|
|
10
12
|
const QUIET = {
|
|
11
13
|
runner: ['heartbeat_at'], claim: ['heartbeat_at', 'expires_at'], watch_lease: ['heartbeat_at', 'expires_at'],
|
|
12
14
|
watch_episode: ['ticks'], provider_readiness: ['observed_at'],
|
package/dist/worktree.d.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { type ExecResult, type RunOptions } from "./exec.js";
|
|
26
26
|
import type { Store, WorktreeRow } from "./store.js";
|
|
27
|
+
import { type Git } from "./teardown-git.js";
|
|
27
28
|
export type Runner = (file: string, args: readonly string[], options?: RunOptions) => Promise<ExecResult>;
|
|
28
29
|
export type PoolOptions = {
|
|
29
30
|
/** Where worktrees are created. Outside the repo, and outside any sync root. */
|
|
@@ -123,6 +124,8 @@ export declare const WORKTREE_TIMEOUT_MS = 60000;
|
|
|
123
124
|
* the pool root through its own slashes.
|
|
124
125
|
*/
|
|
125
126
|
export declare function worktreePath(root: string, repo: string, branch: string): string;
|
|
127
|
+
/** The pool root a checkout at `path` of `repo` was made under (worktreePath's layout), or null for one it didn't make. */
|
|
128
|
+
export declare function poolRootOf(path: string, repo: string): string | null;
|
|
126
129
|
/** Record a potentially writing subprocess against the original checkout lease. */
|
|
127
130
|
export declare function recordWorktreeProcess(store: Store, path: string, runner: string, pid: number, leaseEpoch?: string | null): void;
|
|
128
131
|
/** Read-only occupancy witness shared with stop settlement. */
|
|
@@ -281,17 +284,18 @@ export declare class WorktreePool {
|
|
|
281
284
|
/**
|
|
282
285
|
* Whether a let-go checkout could go without losing anything: clean (build output that .gitignore hides is not
|
|
283
286
|
* work, and neither are the files Toolroll itself wrote in there), and everything its HEAD reaches on a branch, tag
|
|
284
|
-
* or remote. Reads only.
|
|
287
|
+
* or remote. Reads only, through `git` when given (teardown's hardened git).
|
|
285
288
|
*/
|
|
286
|
-
inspect(path: string): Promise<"clean" | KeptWhy>;
|
|
289
|
+
inspect(path: string, git?: Git): Promise<"clean" | KeptWhy>;
|
|
287
290
|
/**
|
|
288
291
|
* Keep storage in check: remove the working copies nobody needs any more. Of `rows`, a checkout goes when it is
|
|
289
292
|
* let go, nothing holds it, `wanted` says it may go (asked again right before it goes, so a task coming back or a
|
|
290
293
|
* lease since keeps it) and `inspect` finds it clean, then clean still right before it goes (git refuses an edit
|
|
291
294
|
* made after that). Only the working copy goes: its branch, and so every commit,
|
|
292
|
-
* stays, and a later lease of the same branch makes a new one. At most `max` go per pass.
|
|
295
|
+
* stays, and a later lease of the same branch makes a new one. At most `max` go per pass. Every git command goes
|
|
296
|
+
* through `git` when given: landed cleanup passes its hardened teardown git (teardown-git.ts).
|
|
293
297
|
*/
|
|
294
|
-
prune(rows: readonly WorktreeRow[], wanted: (row: WorktreeRow, fresh: boolean) => boolean | Promise<boolean>, max?: number): Promise<{
|
|
298
|
+
prune(rows: readonly WorktreeRow[], wanted: (row: WorktreeRow, fresh: boolean) => boolean | Promise<boolean>, max?: number, git?: Git): Promise<{
|
|
295
299
|
removed: WorktreeRow[];
|
|
296
300
|
kept: {
|
|
297
301
|
path: string;
|
|
@@ -342,5 +346,7 @@ export declare class WorktreePool {
|
|
|
342
346
|
* hides is not work, but a file somebody dropped in and never staged is.
|
|
343
347
|
*/
|
|
344
348
|
private isDirty;
|
|
349
|
+
/** All disposal and inspection paths share the hardened teardown boundary. */
|
|
350
|
+
private readonly teardown;
|
|
345
351
|
private git;
|
|
346
352
|
}
|
package/dist/worktree.js
CHANGED
|
@@ -23,15 +23,16 @@
|
|
|
23
23
|
* over `.git` internals corrupts both.
|
|
24
24
|
*/
|
|
25
25
|
import { createHash, randomBytes } from "node:crypto";
|
|
26
|
-
import { existsSync, readFileSync, writeFileSync, rmSync, realpathSync, mkdirSync, readdirSync, renameSync } from "node:fs";
|
|
26
|
+
import { existsSync, lstatSync, readFileSync, writeFileSync, rmSync, realpathSync, mkdirSync, readdirSync, renameSync } from "node:fs";
|
|
27
27
|
import { hostname } from "node:os";
|
|
28
|
-
import { dirname, join } from "node:path";
|
|
28
|
+
import { basename, dirname, join, relative, resolve, sep } 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, looksLikeProtocolFile, readMailbox } from "./evidence.js";
|
|
32
32
|
import { parseHandoff } from "./decision.js";
|
|
33
33
|
import { diskBytes } from "./storage.js";
|
|
34
34
|
import { markNeverIndex } from "./never-index.js";
|
|
35
|
+
import { teardownGit } from "./teardown-git.js";
|
|
35
36
|
/**
|
|
36
37
|
* The note a lease leaves in the checkout, naming the process holding it.
|
|
37
38
|
*
|
|
@@ -129,6 +130,31 @@ export function worktreePath(root, repo, branch) {
|
|
|
129
130
|
.slice(0, 8);
|
|
130
131
|
return join(root, safeSegment(basenameOf(repo)), `${safeSegment(branch)}-${digest}`);
|
|
131
132
|
}
|
|
133
|
+
/** The pool root a checkout at `path` of `repo` was made under (worktreePath's layout), or null for one it didn't make. */
|
|
134
|
+
export function poolRootOf(path, repo) {
|
|
135
|
+
const project = dirname(path);
|
|
136
|
+
return basename(project) === safeSegment(basenameOf(repo)) && /-[0-9a-f]{8}$/.test(basename(path)) ? dirname(project) : null;
|
|
137
|
+
}
|
|
138
|
+
/** Inspect names, not their destinations, before the plane writes into a pool. */
|
|
139
|
+
function safePoolPath(root, target) {
|
|
140
|
+
const base = resolve(root);
|
|
141
|
+
const rel = relative(base, resolve(target));
|
|
142
|
+
if (rel.startsWith("..") || rel.startsWith(sep))
|
|
143
|
+
return false;
|
|
144
|
+
let path = base;
|
|
145
|
+
for (const component of ["", ...rel.split(sep)]) {
|
|
146
|
+
path = join(path, component);
|
|
147
|
+
try {
|
|
148
|
+
if (!lstatSync(path).isDirectory())
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error.code !== "ENOENT")
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
132
158
|
function normalisePath(path) {
|
|
133
159
|
return path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
134
160
|
}
|
|
@@ -210,20 +236,21 @@ export function worktreeProcessOccupancy(path) {
|
|
|
210
236
|
* reset or clean a tree saves this first.
|
|
211
237
|
*/
|
|
212
238
|
export async function saveWorkPatch(runner, path, file, header) {
|
|
213
|
-
const
|
|
239
|
+
const git = teardownGit(runner);
|
|
240
|
+
const status = await git(path, [...READ_ONLY, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=all"]);
|
|
214
241
|
if (status.code !== 0)
|
|
215
242
|
return { ok: false, message: `${path} could not be inspected before its work was saved` };
|
|
216
243
|
const untracked = status.stdout
|
|
217
244
|
.split("\n")
|
|
218
245
|
.filter(line => line.startsWith("?? ") && !line.trimEnd().endsWith(MARKER))
|
|
219
246
|
.map(line => line.slice(3).trim());
|
|
220
|
-
const tracked = await
|
|
247
|
+
const tracked = await git(path, [...READ_ONLY, "diff", "--binary", "--ignore-submodules=all", "HEAD"]);
|
|
221
248
|
if (tracked.code !== 0)
|
|
222
249
|
return { ok: false, message: `${path}: git diff failed while saving its work (${firstLine(tracked.stderr)})` };
|
|
223
250
|
const parts = [tracked.stdout];
|
|
224
251
|
for (const one of untracked) {
|
|
225
252
|
// --no-index exits 1 when the sides differ; that is the expected answer.
|
|
226
|
-
const diff = await
|
|
253
|
+
const diff = await git(path, [...READ_ONLY, "diff", "--binary", "--no-index", "--ignore-submodules=all", "--", "/dev/null", one]);
|
|
227
254
|
if (diff.code > 1)
|
|
228
255
|
return { ok: false, message: `${path}: ${one} could not be captured (${firstLine(diff.stderr)})` };
|
|
229
256
|
parts.push(diff.stdout);
|
|
@@ -304,6 +331,8 @@ export class WorktreePool {
|
|
|
304
331
|
}
|
|
305
332
|
async lease(request) {
|
|
306
333
|
const target = worktreePath(this.options.root, request.repo, request.branch);
|
|
334
|
+
if (!safePoolPath(this.options.root, target))
|
|
335
|
+
return { ok: false, reason: "git", message: "The checkout pool contains a symlink or an unreadable folder." };
|
|
307
336
|
if (busy.has(target))
|
|
308
337
|
return { ok: false, reason: "in-use", message: `${target} is busy; try again in a moment` };
|
|
309
338
|
const release = reserveCheckout(this.store, target);
|
|
@@ -331,6 +360,9 @@ export class WorktreePool {
|
|
|
331
360
|
};
|
|
332
361
|
}
|
|
333
362
|
const path = worktreePath(this.options.root, request.repo, request.branch);
|
|
363
|
+
const unsafe = () => ({ ok: false, reason: "git", message: "The checkout pool contains a symlink or an unreadable folder." });
|
|
364
|
+
if (!safePoolPath(this.options.root, path))
|
|
365
|
+
return unsafe();
|
|
334
366
|
const existing = this.store.getWorktree(path);
|
|
335
367
|
// The machine outranks the database here. A row can say "released" while a
|
|
336
368
|
// process that never got to write its own ending is still working in the
|
|
@@ -408,10 +440,14 @@ export class WorktreePool {
|
|
|
408
440
|
const fromBase = request.base !== undefined && !branchExists;
|
|
409
441
|
// Spotlight leaves the checkouts alone: the marker sits in the pool and the project's folder, never inside a
|
|
410
442
|
// checkout, where a build's `git add -A` would commit it (never-index.ts).
|
|
443
|
+
if (!safePoolPath(this.options.root, path))
|
|
444
|
+
return unsafe();
|
|
411
445
|
try {
|
|
412
446
|
mkdirSync(dirname(path), { recursive: true });
|
|
413
447
|
}
|
|
414
|
-
catch {
|
|
448
|
+
catch {
|
|
449
|
+
return unsafe();
|
|
450
|
+
}
|
|
415
451
|
markNeverIndex(this.options.root);
|
|
416
452
|
markNeverIndex(dirname(path));
|
|
417
453
|
// A new branch is created first, on its own, so its reflog's first entry says Toolroll made it and when.
|
|
@@ -426,10 +462,12 @@ export class WorktreePool {
|
|
|
426
462
|
return { ok: false, reason: "git", message: firstLine(branch.stderr) };
|
|
427
463
|
made = oid;
|
|
428
464
|
}
|
|
465
|
+
if (!safePoolPath(this.options.root, path))
|
|
466
|
+
return unsafe();
|
|
429
467
|
const add = await this.git(request.repo, ["worktree", "add", path, request.branch]);
|
|
430
468
|
if (add.code !== 0) {
|
|
431
469
|
if (made !== null)
|
|
432
|
-
await this.
|
|
470
|
+
await this.teardown(request.repo, ["update-ref", "-d", `refs/heads/${request.branch}`, made]);
|
|
433
471
|
return { ok: false, reason: "git", message: firstLine(add.stderr) };
|
|
434
472
|
}
|
|
435
473
|
}
|
|
@@ -592,10 +630,10 @@ export class WorktreePool {
|
|
|
592
630
|
}
|
|
593
631
|
/** Back to HEAD, untracked gone, our lease note kept; proven clean after. */
|
|
594
632
|
async resetTree(path) {
|
|
595
|
-
const checkout = await this.
|
|
633
|
+
const checkout = await this.teardown(path, ["checkout", "--", "."]);
|
|
596
634
|
if (checkout.code !== 0)
|
|
597
635
|
return { ok: false, message: `${path}: git checkout failed while resetting (${firstLine(checkout.stderr)})` };
|
|
598
|
-
const clean = await this.
|
|
636
|
+
const clean = await this.teardown(path, ["clean", "-fd", "-e", MARKER]);
|
|
599
637
|
if (clean.code !== 0)
|
|
600
638
|
return { ok: false, message: `${path}: git clean failed while resetting (${firstLine(clean.stderr)})` };
|
|
601
639
|
const dirty = await this.isDirty(path);
|
|
@@ -683,17 +721,17 @@ export class WorktreePool {
|
|
|
683
721
|
const occupied = this.inUse(path);
|
|
684
722
|
if (occupied.held)
|
|
685
723
|
return { ok: false, message: `${path} is being used by process ${occupied.by} — leaving it alone` };
|
|
686
|
-
const removed = await this.
|
|
724
|
+
const removed = await this.teardown(existing.repo, ["worktree", "remove", "--force", path]);
|
|
687
725
|
if (removed.code !== 0)
|
|
688
726
|
return { ok: false, message: `git worktree remove: ${removed.stderr.trim() || `exit ${removed.code}`}` };
|
|
689
727
|
// The branch is disposable too; a failure here is bookkeeping, not custody.
|
|
690
|
-
await this.
|
|
728
|
+
await this.teardown(existing.repo, ["branch", "-D", existing.branch]);
|
|
691
729
|
this.store.forgetWorktree(path);
|
|
692
730
|
void now;
|
|
693
731
|
return { ok: true };
|
|
694
732
|
}
|
|
695
733
|
async orphans(repo) {
|
|
696
|
-
const listed = await this.
|
|
734
|
+
const listed = await this.teardown(repo, ["worktree", "list", "--porcelain"]);
|
|
697
735
|
// A listing that failed is not a listing that came back empty. Treating
|
|
698
736
|
// it as empty would report every stored row as missing — and anything
|
|
699
737
|
// acting on that answer would erase real bookkeeping over a git hiccup.
|
|
@@ -790,14 +828,14 @@ export class WorktreePool {
|
|
|
790
828
|
/**
|
|
791
829
|
* Whether a let-go checkout could go without losing anything: clean (build output that .gitignore hides is not
|
|
792
830
|
* work, and neither are the files Toolroll itself wrote in there), and everything its HEAD reaches on a branch, tag
|
|
793
|
-
* or remote. Reads only.
|
|
831
|
+
* or remote. Reads only, through `git` when given (teardown's hardened git).
|
|
794
832
|
*/
|
|
795
|
-
async inspect(path) {
|
|
796
|
-
const dirty = await this.isDirty(path, true);
|
|
833
|
+
async inspect(path, git = this.teardown) {
|
|
834
|
+
const dirty = await this.isDirty(path, true, git);
|
|
797
835
|
if (dirty !== false)
|
|
798
836
|
return dirty === null ? "unreadable" : "has changes";
|
|
799
837
|
// Commits only this checkout's HEAD reaches (a detached HEAD moved on) exist nowhere else.
|
|
800
|
-
const own = await
|
|
838
|
+
const own = await git(path, [...READ_ONLY, "rev-list", "-1", "HEAD", "--not", "--branches", "--tags", "--remotes"]);
|
|
801
839
|
if (own.code !== 0)
|
|
802
840
|
return "unreadable";
|
|
803
841
|
return own.stdout.trim() === "" ? "clean" : "has commits";
|
|
@@ -807,9 +845,10 @@ export class WorktreePool {
|
|
|
807
845
|
* let go, nothing holds it, `wanted` says it may go (asked again right before it goes, so a task coming back or a
|
|
808
846
|
* lease since keeps it) and `inspect` finds it clean, then clean still right before it goes (git refuses an edit
|
|
809
847
|
* made after that). Only the working copy goes: its branch, and so every commit,
|
|
810
|
-
* stays, and a later lease of the same branch makes a new one. At most `max` go per pass.
|
|
848
|
+
* stays, and a later lease of the same branch makes a new one. At most `max` go per pass. Every git command goes
|
|
849
|
+
* through `git` when given: landed cleanup passes its hardened teardown git (teardown-git.ts).
|
|
811
850
|
*/
|
|
812
|
-
async prune(rows, wanted, max = Infinity) {
|
|
851
|
+
async prune(rows, wanted, max = Infinity, git = this.teardown) {
|
|
813
852
|
const removed = [];
|
|
814
853
|
const kept = [];
|
|
815
854
|
const idle = (row, releasedAt) => row !== null && row.runner === null && row.releasedAt === releasedAt && !this.inUse(row.path).held;
|
|
@@ -823,7 +862,7 @@ export class WorktreePool {
|
|
|
823
862
|
continue;
|
|
824
863
|
if (!existsSync(row.path) || !idle(row, row.releasedAt))
|
|
825
864
|
continue;
|
|
826
|
-
const found = await this.inspect(row.path);
|
|
865
|
+
const found = await this.inspect(row.path, git);
|
|
827
866
|
if (found !== "clean") {
|
|
828
867
|
kept.push({ path: row.path, why: found });
|
|
829
868
|
continue;
|
|
@@ -843,7 +882,7 @@ export class WorktreePool {
|
|
|
843
882
|
const allowed = wanted(now, true);
|
|
844
883
|
if (!(typeof allowed === "boolean" ? allowed : await allowed))
|
|
845
884
|
continue;
|
|
846
|
-
const gone = await this.removeCleanCopy(row);
|
|
885
|
+
const gone = await this.removeCleanCopy(row, git);
|
|
847
886
|
if (gone !== "removed") {
|
|
848
887
|
kept.push({ path: row.path, why: gone });
|
|
849
888
|
continue;
|
|
@@ -867,7 +906,7 @@ export class WorktreePool {
|
|
|
867
906
|
const idle = (now) => now !== null && now.runner === null && now.releasedAt !== null && now.releasedAt === row.releasedAt && !this.inUse(row.path).held;
|
|
868
907
|
if (busy.has(row.path) || !existsSync(row.path) || !idle(this.store.getWorktree(row.path)))
|
|
869
908
|
return { ok: false, message: `${row.path} is in use` };
|
|
870
|
-
const listed = await this.
|
|
909
|
+
const listed = await this.teardown(row.path, [...READ_ONLY, "ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "-z"]);
|
|
871
910
|
if (listed.code !== 0)
|
|
872
911
|
return { ok: false, message: `git couldn't list ${row.path}` };
|
|
873
912
|
const drop = listed.stdout.split("\0").filter(one => one.endsWith("/")).map(one => one.slice(0, -1))
|
|
@@ -949,7 +988,7 @@ export class WorktreePool {
|
|
|
949
988
|
catch {
|
|
950
989
|
return false;
|
|
951
990
|
}
|
|
952
|
-
const gone = await this.
|
|
991
|
+
const gone = await this.teardown(row.repo, ["worktree", "remove", "--force", row.path]);
|
|
953
992
|
if (gone.code !== 0) {
|
|
954
993
|
rmSync(note, { force: true });
|
|
955
994
|
return false;
|
|
@@ -963,11 +1002,11 @@ export class WorktreePool {
|
|
|
963
1002
|
* removed without --force, so git itself refuses an edit made since. Only Toolroll's own files go first, because
|
|
964
1003
|
* git's check would count them.
|
|
965
1004
|
*/
|
|
966
|
-
async removeCleanCopy(row) {
|
|
967
|
-
const found = await this.inspect(row.path);
|
|
1005
|
+
async removeCleanCopy(row, git) {
|
|
1006
|
+
const found = await this.inspect(row.path, git);
|
|
968
1007
|
if (found !== "clean")
|
|
969
1008
|
return found;
|
|
970
|
-
const status = await
|
|
1009
|
+
const status = await git(row.path, [...READ_ONLY, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=all"]);
|
|
971
1010
|
if (status.code !== 0)
|
|
972
1011
|
return "unreadable";
|
|
973
1012
|
for (const line of status.stdout.split("\n")) {
|
|
@@ -975,9 +1014,9 @@ export class WorktreePool {
|
|
|
975
1014
|
if (name === MARKER || (name !== "" && looksLikeProtocolFile(name)))
|
|
976
1015
|
rmSync(join(row.path, name), { recursive: true, force: true });
|
|
977
1016
|
}
|
|
978
|
-
const gone = await
|
|
1017
|
+
const gone = await git(row.repo, ["worktree", "remove", row.path]);
|
|
979
1018
|
if (gone.code !== 0)
|
|
980
|
-
return (await this.isDirty(row.path)) === false ? "git refused" : "has changes";
|
|
1019
|
+
return (await this.isDirty(row.path, false, git)) === false ? "git refused" : "has changes";
|
|
981
1020
|
this.forgetRemoved(row);
|
|
982
1021
|
return "removed";
|
|
983
1022
|
}
|
|
@@ -995,8 +1034,8 @@ export class WorktreePool {
|
|
|
995
1034
|
* `--untracked-files=all` and no `--ignored`: build output that .gitignore
|
|
996
1035
|
* hides is not work, but a file somebody dropped in and never staged is.
|
|
997
1036
|
*/
|
|
998
|
-
async isDirty(path, ignoreOwnFiles = false) {
|
|
999
|
-
const status = await
|
|
1037
|
+
async isDirty(path, ignoreOwnFiles = false, git = this.teardown) {
|
|
1038
|
+
const status = await git(path, [...READ_ONLY, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=all"]);
|
|
1000
1039
|
if (status.code !== 0)
|
|
1001
1040
|
return null;
|
|
1002
1041
|
// Our own lease note is not the operator's work. Without this the marker
|
|
@@ -1010,6 +1049,8 @@ export class WorktreePool {
|
|
|
1010
1049
|
.filter(line => line.trim() !== "")
|
|
1011
1050
|
.some(line => !line.trimEnd().endsWith(MARKER) && !(ignoreOwnFiles && line.startsWith("?? ") && looksLikeProtocolFile(line.slice(3).trim())));
|
|
1012
1051
|
}
|
|
1052
|
+
/** All disposal and inspection paths share the hardened teardown boundary. */
|
|
1053
|
+
teardown = (cwd, args) => teardownGit(this.runner)(cwd, args);
|
|
1013
1054
|
git(cwd, args, prefix = []) {
|
|
1014
1055
|
return this.runner(GIT, [...prefix, ...args], { cwd, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
1015
1056
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.65",
|
|
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"
|
|
7
7
|
},
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "tsc -p tsconfig.build.json && node scripts/browser-build.mjs && node scripts/postbuild.mjs",
|
|
10
|
-
"test": "vitest run",
|
|
10
|
+
"test": "vitest run --exclude '**/*.slow.test.ts'",
|
|
11
|
+
"test:slow": "vitest run .slow.test",
|
|
12
|
+
"test:all": "vitest run",
|
|
13
|
+
"test:attacks": "vitest run --config test/attacks/vitest.config.ts",
|
|
14
|
+
"check:quick": "node scripts/quick-check.mjs",
|
|
11
15
|
"dev": "tsx src/cli.ts",
|
|
12
16
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.browser.json",
|
|
13
17
|
"certify:provider": "npm run build && node scripts/provider-canary.mjs",
|
|
@@ -78,6 +82,9 @@
|
|
|
78
82
|
"@types/node": "26.6.4",
|
|
79
83
|
"@types/react": "19.3.0",
|
|
80
84
|
"@types/react-dom": "19.3.0",
|
|
85
|
+
"@types/ws": "8.18.2",
|
|
86
|
+
"@xterm/addon-fit": "0.11.0",
|
|
87
|
+
"@xterm/xterm": "6.0.0",
|
|
81
88
|
"@xyflow/react": "12.12.0",
|
|
82
89
|
"class-variance-authority": "0.7.1",
|
|
83
90
|
"clsx": "2.1.1",
|
|
@@ -122,7 +129,11 @@
|
|
|
122
129
|
"mailparser": "3.9.36",
|
|
123
130
|
"nodemailer": "10.0.15",
|
|
124
131
|
"typescript-parser": "npm:typescript@6.0.3",
|
|
132
|
+
"ws": "8.22.0",
|
|
125
133
|
"yaml": "2.9.1",
|
|
126
134
|
"zod": "^4.6.5"
|
|
135
|
+
},
|
|
136
|
+
"optionalDependencies": {
|
|
137
|
+
"@lydell/node-pty": "1.2.0-beta.15"
|
|
127
138
|
}
|
|
128
139
|
}
|