toolroll 0.9.83 → 0.9.84
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 +27 -0
- package/README.md +4 -3
- package/dist/assignment-ui.js +5 -1
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +22 -22
- package/dist/browser-crew.js +5 -1
- package/dist/browser-workspace.d.ts +7 -0
- package/dist/contracts/console-api.d.ts +2 -1
- package/dist/contracts/console-api.js +5 -1
- package/dist/control-setup.d.ts +9 -0
- package/dist/control-setup.js +31 -4
- package/dist/control-ui.d.ts +7 -1
- package/dist/control-ui.js +3 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/dispatch.d.ts +15 -0
- package/dist/dispatch.js +26 -4
- package/dist/first-run.d.ts +5 -2
- package/dist/first-run.js +13 -2
- package/dist/folder-look.d.ts +3 -0
- package/dist/folder-look.js +8 -4
- package/dist/guides.js +4 -3
- package/dist/model-catalog.d.ts +5 -0
- package/dist/model-catalog.js +11 -2
- package/dist/model-choices.d.ts +188 -0
- package/dist/model-choices.js +405 -0
- package/dist/model-names.d.ts +6 -0
- package/dist/model-names.js +34 -0
- package/dist/models-ui.d.ts +69 -5
- package/dist/models-ui.js +53 -10
- package/dist/needs-you.d.ts +8 -0
- package/dist/needs-you.js +8 -0
- package/dist/operate.d.ts +21 -0
- package/dist/operate.js +96 -26
- package/dist/project-admission.d.ts +4 -1
- package/dist/project-admission.js +23 -2
- package/dist/project-identity.d.ts +9 -2
- package/dist/project-identity.js +80 -33
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/serve.js +60 -13
- package/dist/server/chat.js +5 -4
- package/dist/server/chrome.d.ts +3 -0
- package/dist/server/chrome.js +5 -2
- package/dist/server/console.css +5 -2
- package/dist/server/flows.js +4 -0
- package/dist/server/http.d.ts +1 -0
- package/dist/server/http.js +6 -0
- package/dist/server/pages.js +42 -6
- package/dist/server/people-tokens.js +40 -20
- package/dist/server/render-pages.d.ts +10 -0
- package/dist/server/render-pages.js +29 -14
- package/dist/server/render-people.d.ts +14 -2
- package/dist/server/render-people.js +35 -12
- package/dist/server/render-settings.js +1 -0
- package/dist/server/render-tasks.js +7 -14
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/route-table.js +8 -1
- package/dist/server/settings.d.ts +58 -1
- package/dist/server/settings.js +198 -69
- package/dist/server/signup-gate.d.ts +9 -0
- package/dist/server/signup-gate.js +40 -0
- package/dist/server/tasks.js +12 -2
- package/dist/settings-kit-script.d.ts +21 -0
- package/dist/settings-kit-script.js +67 -0
- package/dist/settings-kit.d.ts +137 -0
- package/dist/settings-kit.js +186 -0
- package/dist/settings-proofs.d.ts +64 -0
- package/dist/settings-proofs.js +101 -0
- package/dist/setup-guide.js +4 -2
- package/dist/spend-report.d.ts +87 -0
- package/dist/spend-report.js +265 -0
- package/dist/spend-ui.d.ts +29 -8
- package/dist/spend-ui.js +74 -39
- package/dist/task-sizing.d.ts +5 -3
- package/dist/task-sizing.js +4 -4
- package/package.json +1 -1
package/dist/browser-crew.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { chatControlHref, chatResultHref } from './chat-controls.js';
|
|
2
|
+
import { startBuilderHref } from './needs-you.js';
|
|
2
3
|
/** The index is navigation only. Controls retain their exact owning task, run,
|
|
3
4
|
* decision and approval ceremony; opening a result keeps its saved execution. */
|
|
4
5
|
export function browserWorkActionHref(item) {
|
|
@@ -25,13 +26,16 @@ export function browserWorkActionHref(item) {
|
|
|
25
26
|
}
|
|
26
27
|
if (runId !== null && (action.code === 'inspect-run' || action.code === 'reconcile-run'))
|
|
27
28
|
return `/r/${runId}`;
|
|
29
|
+
// The task page only repeats that no builder is connected: lead to the command that connects one.
|
|
30
|
+
if (action.code === 'start-worker')
|
|
31
|
+
return startBuilderHref(taskId);
|
|
28
32
|
const anchor = action.code === 'approve-scope' ? '#approve'
|
|
29
33
|
: action.code === 'inspect-stop' || action.code === 'resume-run' ? '#task-control'
|
|
30
34
|
// Retry (and Build again) is the task's status card itself, at the top: no anchor to its options.
|
|
31
35
|
: action.code === 'unhold' ? '#task-actions'
|
|
32
36
|
: action.code === 'write-scope' || action.code === 'select-agent' ? '#scope'
|
|
33
37
|
: action.code === 'inspect-hold' ? '#holds'
|
|
34
|
-
: action.code === '
|
|
38
|
+
: action.code === 'repair-dependency' ? '#run-status' : '';
|
|
35
39
|
return `/t/${encodeURIComponent(item.rootId)}?version=${encodeURIComponent(taskId)}` + anchor;
|
|
36
40
|
}
|
|
37
41
|
/** Render an already-admitted page without repeating its database query. Every task reads its one headline, the
|
|
@@ -1464,6 +1464,13 @@ export type BrowserWorkspace = {
|
|
|
1464
1464
|
sensitive: boolean;
|
|
1465
1465
|
/** Passwordless is on for this signed-in browser: ceremony password fields are left out (docs/plans/passwordless.md). */
|
|
1466
1466
|
passwordless?: boolean;
|
|
1467
|
+
/** The session's open project (its path), or null: the sidebar's project picker names it. */
|
|
1468
|
+
openProject?: string | null;
|
|
1469
|
+
/** The settings frame's breadcrumb ("Settings / <page>"; settings-kit.ts): the header shows it as links. */
|
|
1470
|
+
crumbs?: {
|
|
1471
|
+
label: string;
|
|
1472
|
+
href: string | null;
|
|
1473
|
+
}[];
|
|
1467
1474
|
/** What this person named their lead (Settings → Lead): the chat header and every lead message say it. */
|
|
1468
1475
|
leadName?: string;
|
|
1469
1476
|
refreshUrl: string;
|
|
@@ -106,7 +106,8 @@ export declare const CONSOLE_FORMS: {
|
|
|
106
106
|
readonly toolsConnect: FormContract<"access" | "also" | "kit" | "password" | "repo" | "service" | "shown" | "template">;
|
|
107
107
|
readonly toolsChange: FormContract<"action" | "catalog" | "import" | "name" | "password" | "repo" | "secret" | "secrets" | "shown" | "target" | "transport" | "value">;
|
|
108
108
|
readonly appearance: FormContract<"accent" | "quiet" | "return" | "theme">;
|
|
109
|
-
readonly models: FormContract<"
|
|
109
|
+
readonly models: FormContract<"enabled" | "tool">;
|
|
110
|
+
readonly modelsChange: FormContract<"build" | "build-light" | "build-strong" | "change" | "digest" | "password" | "plan" | "plan-strong" | "preset" | "repair" | "review" | "review-strong" | "scope" | "switch" | "to">;
|
|
110
111
|
readonly knowledgeRefresh: FormContract<"repo">;
|
|
111
112
|
readonly knowledgeProposal: FormContract<"decision" | "proposal" | "repo">;
|
|
112
113
|
readonly knowledgeDecision: FormContract<"action" | "claim" | "decision" | "reason" | "repo" | "why">;
|
|
@@ -115,7 +115,11 @@ export const CONSOLE_FORMS = {
|
|
|
115
115
|
toolsConnect: formContract("POST /settings/tools/connect", ["repo", "also", "service", "template", "kit", "shown", "password", "access"]),
|
|
116
116
|
toolsChange: formContract("POST /settings/tools/change", ["repo", "shown", "action", "name", "password", "catalog", "transport", "target", "secrets", "import", "secret", "value"]),
|
|
117
117
|
appearance: formContract("POST /settings/appearance", ["accent", "quiet", "return", "theme"]),
|
|
118
|
-
models: formContract("POST /settings/models/check, /settings/models/watch, /settings/models/update
|
|
118
|
+
models: formContract("POST /settings/models/check, /settings/models/watch, /settings/models/update", ["enabled", "tool"]),
|
|
119
|
+
// The one Models editor (docs/plans/spend-dashboard.md slice 3): every role at a scope in one save, or a previewed
|
|
120
|
+
// preset or switch; the digest is what the page showed, and the change digest what the preview showed.
|
|
121
|
+
modelsChange: formContract("POST /settings/models/agent, /settings/models/apply", ["password", "scope", "digest", "change", "preset", "switch", "to",
|
|
122
|
+
"plan", "plan-strong", "build-light", "build", "build-strong", "repair", "review", "review-strong"]),
|
|
119
123
|
knowledgeRefresh: formContract("POST /settings/knowledge/refresh", ["repo"]),
|
|
120
124
|
knowledgeProposal: formContract("POST /settings/knowledge/proposal", ["repo", "decision", "proposal"]),
|
|
121
125
|
knowledgeDecision: formContract("POST /settings/knowledge/decision", ["repo", "action", "claim", "why", "decision", "reason"]),
|
package/dist/control-setup.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare function previewSetup(store: Store, repo: string, inputs: SetupIn
|
|
|
13
13
|
seconds?: never;
|
|
14
14
|
provider?: never;
|
|
15
15
|
model?: never;
|
|
16
|
+
models?: never;
|
|
16
17
|
} | {
|
|
17
18
|
message?: never;
|
|
18
19
|
ok: true;
|
|
@@ -21,6 +22,13 @@ export declare function previewSetup(store: Store, repo: string, inputs: SetupIn
|
|
|
21
22
|
seconds: number;
|
|
22
23
|
provider: "claude" | "cline" | "codex" | "cursor" | "gemini" | "goose" | "opencode" | "openrouter" | "qwen";
|
|
23
24
|
model: string;
|
|
25
|
+
models: {
|
|
26
|
+
rows: (import("./model-choices.js").DiffRow & {
|
|
27
|
+
inherited: import("./model-choices.js").Pair | null;
|
|
28
|
+
})[];
|
|
29
|
+
state: string;
|
|
30
|
+
change: string;
|
|
31
|
+
} | null;
|
|
24
32
|
};
|
|
25
33
|
export declare function approveSetup(store: Store, repo: string, inputs: SetupInputs, fingerprint: string, by: string, token: string, now: Date): {
|
|
26
34
|
ok: false;
|
|
@@ -30,6 +38,7 @@ export declare function approveSetup(store: Store, repo: string, inputs: SetupIn
|
|
|
30
38
|
seconds?: never;
|
|
31
39
|
provider?: never;
|
|
32
40
|
model?: never;
|
|
41
|
+
models?: never;
|
|
33
42
|
} | {
|
|
34
43
|
ok: true;
|
|
35
44
|
};
|
package/dist/control-setup.js
CHANGED
|
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { hasDisguisedText } from "./decision.js";
|
|
3
3
|
import { authenticateApprover } from "./scope.js";
|
|
4
4
|
import { isProviderId, validateSpec } from "./provider.js";
|
|
5
|
+
import { INSTALLATION_SCOPE } from "./agentconfig.js";
|
|
6
|
+
import { ALL_SLOTS, applyModelChoices, DEFAULT_PRESET, presetChange, previewModelChange, rowAt } from "./model-choices.js";
|
|
5
7
|
export function previewSetup(store, repo, inputs) {
|
|
6
8
|
if (!isProviderId(inputs.provider))
|
|
7
9
|
return { ok: false, message: "Choose an installed provider." };
|
|
@@ -18,19 +20,36 @@ export function previewSetup(store, repo, inputs) {
|
|
|
18
20
|
const seconds = Number(inputs.seconds);
|
|
19
21
|
if (!/^[1-9][0-9]*$/.test(inputs.seconds) || !Number.isInteger(seconds) || seconds < 1 || seconds > 3600)
|
|
20
22
|
return { ok: false, message: "Setup timeout must be 1–3600 seconds." };
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
+
const models = installationModels(store);
|
|
24
|
+
const fingerprint = createHash("sha256").update(JSON.stringify({ repo, inputs, beforeSetup: store.liveWorktreeSetup(repo), beforeConfig: store.phaseConfig(repo, "build"),
|
|
25
|
+
...(models === null ? {} : { models: [models.state, models.change] }) })).digest("hex");
|
|
26
|
+
return { ok: true, fingerprint, command, seconds, provider: inputs.provider, model: inputs.model.trim(), models };
|
|
23
27
|
}
|
|
24
28
|
export function approveSetup(store, repo, inputs, fingerprint, by, token, now) {
|
|
25
29
|
return store.transact(() => {
|
|
26
|
-
|
|
30
|
+
const proved = authenticateApprover(store, by, token, repo);
|
|
31
|
+
if (!proved.ok)
|
|
27
32
|
return { ok: false, message: "Your operator credential is required." };
|
|
28
33
|
const preview = previewSetup(store, repo, inputs);
|
|
29
34
|
if (!preview.ok)
|
|
30
35
|
return preview;
|
|
31
36
|
if (preview.fingerprint !== fingerprint)
|
|
32
37
|
return { ok: false, message: "Setup changed. Review it again." };
|
|
33
|
-
|
|
38
|
+
if (preview.models !== null) {
|
|
39
|
+
// The installation's models, through Settings → Models' one write path (an instance operator's act). The proof
|
|
40
|
+
// above ran in this same transaction, so the generation read here is the one it was checked against.
|
|
41
|
+
if (!store.isInstanceOperator(by))
|
|
42
|
+
return { ok: false, message: "Models for this installation aren't chosen yet. An instance operator sets them up first." };
|
|
43
|
+
const applied = applyModelChoices(store, { scope: INSTALLATION_SCOPE, change: presetChange(DEFAULT_PRESET), expected: { state: preview.models.state, change: preview.models.change }, strict: true, knownProject: () => false }, { actor: by, generation: store.accountOf(by)?.generation ?? -1, method: proved.method, live: () => true }, now);
|
|
44
|
+
if (!applied.ok)
|
|
45
|
+
return { ok: false, message: applied.problem };
|
|
46
|
+
}
|
|
47
|
+
// The project's own builder only when it differs from what it inherits: a project left on the installation's
|
|
48
|
+
// routine builder keeps following the installation's sizes and presets.
|
|
49
|
+
const inherited = store.phaseConfig(INSTALLATION_SCOPE, "build");
|
|
50
|
+
const same = inherited !== null && inherited.provider === preview.provider && inherited.model === preview.model;
|
|
51
|
+
if (!same || store.phaseConfig(repo, "build") !== null)
|
|
52
|
+
store.setPhaseConfig(repo, "build", preview.provider, preview.model, by, now);
|
|
34
53
|
if (preview.command === "")
|
|
35
54
|
store.clearWorktreeSetup(repo, by, now);
|
|
36
55
|
else
|
|
@@ -38,3 +57,11 @@ export function approveSetup(store, repo, inputs, fingerprint, by, token, now) {
|
|
|
38
57
|
return { ok: true };
|
|
39
58
|
});
|
|
40
59
|
}
|
|
60
|
+
// Last in the file, so the password tripwire's function positions (password-freshness.test.ts) stay as reviewed.
|
|
61
|
+
/** A new installation, nothing chosen yet: setup applies the default preset ("Sized by job") with the project's
|
|
62
|
+
* builder, previewed here row by row, so the first task files on exact models for every size. */
|
|
63
|
+
function installationModels(store) {
|
|
64
|
+
if (!ALL_SLOTS.every(slot => rowAt(store, INSTALLATION_SCOPE, slot) === null))
|
|
65
|
+
return null;
|
|
66
|
+
return previewModelChange(store, INSTALLATION_SCOPE, presetChange(DEFAULT_PRESET));
|
|
67
|
+
}
|
package/dist/control-ui.d.ts
CHANGED
|
@@ -28,4 +28,10 @@ export declare function controlSetupHtml(data: {
|
|
|
28
28
|
};
|
|
29
29
|
}): Html;
|
|
30
30
|
/** `fields` may still carry the session's csrf (a caller's habit): postForm writes the one CSRF field. */
|
|
31
|
-
export declare function setupPreviewHtml(inputs: SetupInputs, fields: Record<string, string
|
|
31
|
+
export declare function setupPreviewHtml(inputs: SetupInputs, fields: Record<string, string>, models?: {
|
|
32
|
+
label: string;
|
|
33
|
+
rows: {
|
|
34
|
+
role: string;
|
|
35
|
+
model: string;
|
|
36
|
+
}[];
|
|
37
|
+
} | null): Html;
|
package/dist/control-ui.js
CHANGED
|
@@ -27,7 +27,8 @@ export function controlSetupHtml(data) {
|
|
|
27
27
|
html `<p>Give the agent the project's Toolroll handoff instructions.</p>${postForm("/control/instructions-preview", html `<button>Review instructions</button>`, { hidden: { repo: data.repo } })}`}</section><p><a href="/fleet">Configure planning, repair, and review agents</a> · <a href="/tasks/new">Describe a task</a></p>`;
|
|
28
28
|
}
|
|
29
29
|
/** `fields` may still carry the session's csrf (a caller's habit): postForm writes the one CSRF field. */
|
|
30
|
-
export function setupPreviewHtml(inputs, fields) {
|
|
30
|
+
export function setupPreviewHtml(inputs, fields, models = null) {
|
|
31
31
|
const { csrf: _csrf, ...rest } = fields;
|
|
32
|
-
|
|
32
|
+
const chosen = models === null ? "" : html `<p><strong>Models for every project:</strong> ${models.label}</p><ul>${models.rows.map(row => html `<li>${row.role}: <span class="mono">${row.model}</span></li>`)}</ul><p class="meta">Change them later in Settings → Models.</p>`;
|
|
33
|
+
return html `<h1>Approve project setup</h1><div class="card"><p>${fields.repo ?? ""}</p><p><strong>Builder:</strong> ${inputs.provider} · ${inputs.model}</p>${chosen}<p><strong>Preparation:</strong></p><pre class="recap">${inputs.command || "No preparation command"}</pre><p>Preparation timeout: ${inputs.seconds} seconds.</p><p>The builder default applies to new tasks. Preparation applies to subsequent runs, including already approved tasks. Existing signed task settings and verification rules remain in force.</p>${postForm("/control/setup-approve", html `${reauth(html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>`)}<button>Approve project setup</button>`, { hidden: { ...rest, ...inputs } })}</div>`;
|
|
33
34
|
}
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -97,7 +97,6 @@ export declare function sqliteLock(file: string, busyTimeoutMs?: number): Databa
|
|
|
97
97
|
export declare function lockHeld(file: string): boolean;
|
|
98
98
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
99
99
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
100
|
-
version?: never;
|
|
101
100
|
active: boolean;
|
|
102
101
|
phase: string;
|
|
103
102
|
detail: string;
|
|
@@ -111,6 +110,7 @@ export declare function desktopUpdateStatus(stateDir: string): {
|
|
|
111
110
|
wasRunning?: never;
|
|
112
111
|
backupPath?: never;
|
|
113
112
|
updateId?: never;
|
|
113
|
+
version?: never;
|
|
114
114
|
buildId?: never;
|
|
115
115
|
workDir?: never;
|
|
116
116
|
} | {
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* fleet observations on top for people and agents reading the queue. A
|
|
7
7
|
* diagnosis can go stale; it can never grant a lease.
|
|
8
8
|
*/
|
|
9
|
+
import { type Runner } from "./runner.js";
|
|
9
10
|
import { type ChatSnapshot, type ReviewRequestOrigin, type ReviewRetryState, type Store, type TaskState } from "./store.js";
|
|
10
11
|
export declare const DEFAULT_MAX_OPEN_DECISIONS = 5;
|
|
11
12
|
export type TaskReadinessBlocker = {
|
|
@@ -95,6 +96,20 @@ export declare function dispatchRoleFor(store: Store, taskRef: number, now: Date
|
|
|
95
96
|
/** The first requirement this task fails, in words, or null. */
|
|
96
97
|
export declare function missingCapability(store: Store, taskRef: number, dispatchRepo: string | null, now: Date): string | null;
|
|
97
98
|
/** The read-side lifecycle answer. Project access limits dependency detail, never the readiness gate. */
|
|
99
|
+
/** The builders that may take work in `repo`, as dispatch decides: registered and not retired (`all`), bound to the
|
|
100
|
+
* project (`eligible`; only the assigned one when a task names one), and checking in now (`alive`). The first-run
|
|
101
|
+
* "Builder connected" step reads the same answer, so it is done only when a task there could start. */
|
|
102
|
+
export declare function buildersFor(store: Store, repo: string, now: Date, assignedRunner?: string | null): {
|
|
103
|
+
all: Runner[];
|
|
104
|
+
eligible: Runner[];
|
|
105
|
+
alive: Runner[];
|
|
106
|
+
};
|
|
107
|
+
/** Whether a task in `repo` could start for want of nothing but a person: a live builder bound to it. */
|
|
108
|
+
export declare function builderReadyFor(store: Store, repo: string | null, now: Date): boolean;
|
|
109
|
+
/** The project the first-run "Builder connected" step speaks for, on Chat and the board alike: the open one; else the
|
|
110
|
+
* saved project of the first filed task the viewer may see (All projects); else, only before any task exists, the
|
|
111
|
+
* first managed project. Null when tasks exist but none sits in a project the viewer may see: then nothing says done. */
|
|
112
|
+
export declare function firstRunBuilderRepo(store: Store, selected: string | null, visible: (repo: string | null) => boolean, managed: readonly string[]): string | null;
|
|
98
113
|
export declare function diagnoseTaskDispatch(store: Store, taskId: string, now: Date, readableRepos?: readonly string[] | null): DispatchDiagnosis | null;
|
|
99
114
|
export declare function diagnosisIsDispatchable(diagnosis: DispatchDiagnosis | null): boolean;
|
|
100
115
|
/** Attach the same lifecycle answer to the bounded fleet snapshot used by
|
package/dist/dispatch.js
CHANGED
|
@@ -160,6 +160,31 @@ function approvedProfile(scope) {
|
|
|
160
160
|
return scope.approvedProfile ?? scope.profile ?? null;
|
|
161
161
|
}
|
|
162
162
|
/** The read-side lifecycle answer. Project access limits dependency detail, never the readiness gate. */
|
|
163
|
+
/** The builders that may take work in `repo`, as dispatch decides: registered and not retired (`all`), bound to the
|
|
164
|
+
* project (`eligible`; only the assigned one when a task names one), and checking in now (`alive`). The first-run
|
|
165
|
+
* "Builder connected" step reads the same answer, so it is done only when a task there could start. */
|
|
166
|
+
export function buildersFor(store, repo, now, assignedRunner = null) {
|
|
167
|
+
const all = store.listRunners().filter(one => one.retiredAt === null);
|
|
168
|
+
const bound = all.filter(one => one.repos.includes(repo));
|
|
169
|
+
const eligible = assignedRunner === null ? bound : bound.filter(one => one.name === assignedRunner);
|
|
170
|
+
return { all, eligible, alive: eligible.filter(one => isAlive(one, now)) };
|
|
171
|
+
}
|
|
172
|
+
/** Whether a task in `repo` could start for want of nothing but a person: a live builder bound to it. */
|
|
173
|
+
export function builderReadyFor(store, repo, now) {
|
|
174
|
+
return repo !== null && buildersFor(store, repo, now).alive.length > 0;
|
|
175
|
+
}
|
|
176
|
+
/** The project the first-run "Builder connected" step speaks for, on Chat and the board alike: the open one; else the
|
|
177
|
+
* saved project of the first filed task the viewer may see (All projects); else, only before any task exists, the
|
|
178
|
+
* first managed project. Null when tasks exist but none sits in a project the viewer may see: then nothing says done. */
|
|
179
|
+
export function firstRunBuilderRepo(store, selected, visible, managed) {
|
|
180
|
+
if (selected !== null)
|
|
181
|
+
return selected;
|
|
182
|
+
const placed = store.handle.prepare("SELECT repo FROM task_ref WHERE repo IS NOT NULL ORDER BY id").all();
|
|
183
|
+
const first = placed.find(row => visible(row.repo));
|
|
184
|
+
if (first !== undefined)
|
|
185
|
+
return first.repo;
|
|
186
|
+
return store.hasAnyWork() ? null : managed[0] ?? null;
|
|
187
|
+
}
|
|
163
188
|
export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
|
|
164
189
|
const task = store.getTask(taskId);
|
|
165
190
|
const ref = store.lookupRef(taskId);
|
|
@@ -271,9 +296,7 @@ export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
|
|
|
271
296
|
const gap = missingCapability(store, ref.id, ref.repo, now);
|
|
272
297
|
if (gap !== null)
|
|
273
298
|
return answer("missing-requirement", "waiting", "Missing a requirement", gap, { action: "repair-capability", role });
|
|
274
|
-
const all = store.
|
|
275
|
-
const bound = all.filter(one => one.repos.includes(ref.repo));
|
|
276
|
-
const eligible = ref.assignedRunner === null ? bound : bound.filter(one => one.name === ref.assignedRunner);
|
|
299
|
+
const { all, eligible, alive } = buildersFor(store, ref.repo, now, ref.assignedRunner);
|
|
277
300
|
if (eligible.length === 0) {
|
|
278
301
|
const detail = ref.assignedRunner === null
|
|
279
302
|
? all.length === 0
|
|
@@ -282,7 +305,6 @@ export function diagnoseTaskDispatch(store, taskId, now, readableRepos = null) {
|
|
|
282
305
|
: `This task is assigned to ${ref.assignedRunner}, but that builder is not connected to this project.`;
|
|
283
306
|
return answer("no-worker-registered", "waiting", "Builder not connected", detail, { action: "start-worker", role });
|
|
284
307
|
}
|
|
285
|
-
const alive = eligible.filter(one => isAlive(one, now));
|
|
286
308
|
if (alive.length === 0) {
|
|
287
309
|
return answer("no-worker-online", "waiting", "Builder disconnected", `${eligible.length === 1 ? "The builder for this project has" : "All builders for this project have"} stopped checking in. This task starts automatically when ${eligible.length === 1 ? "it reconnects" : "one reconnects"}.`, { action: "start-worker", role });
|
|
288
310
|
}
|
package/dist/first-run.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type FirstRunAction = {
|
|
|
25
25
|
};
|
|
26
26
|
/** `checking`: the answer is not in yet, so the step is shown neither done nor to do. */
|
|
27
27
|
export type FirstRunStep = {
|
|
28
|
-
key: "agent" | "project" | "task";
|
|
28
|
+
key: "agent" | "project" | "builder" | "task";
|
|
29
29
|
title: string;
|
|
30
30
|
done: boolean;
|
|
31
31
|
action: FirstRunAction | null;
|
|
@@ -39,8 +39,11 @@ export type FirstRunFacts = {
|
|
|
39
39
|
firstResultAt: string | null;
|
|
40
40
|
signInCommand?: string;
|
|
41
41
|
};
|
|
42
|
-
/** The
|
|
42
|
+
/** The steps (the builder step is added by withBuilderStep), each done or with the one action that does it; null once the first result has arrived. */
|
|
43
43
|
export declare function firstRunSteps(facts: FirstRunFacts): FirstRunStep[] | null;
|
|
44
|
+
/** The builder step, before the first task: under `toolroll serve` nothing connects a builder, and a task filed then
|
|
45
|
+
* waits forever, so the steps never all read done without one. It leads to Start a builder, which shows the command. */
|
|
46
|
+
export declare function withBuilderStep(steps: FirstRunStep[] | null, connected: boolean): FirstRunStep[] | null;
|
|
44
47
|
export type OpenIssue = {
|
|
45
48
|
number: number;
|
|
46
49
|
title: string;
|
package/dist/first-run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The first run:
|
|
2
|
+
* The first run: four plain steps to a first result, and three first tasks
|
|
3
3
|
* to try. Everything here is derived from live facts on each render; the
|
|
4
4
|
* list retires permanently with the first Ready result (the
|
|
5
5
|
* 'first-success-at' installation fact). A suggestion only drafts words in
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { run } from "./exec.js";
|
|
9
9
|
import { hardenedGit } from "./teardown-git.js";
|
|
10
10
|
import { githubIssues } from "./issues.js";
|
|
11
|
+
import { START_BUILDER_HREF } from "./needs-you.js";
|
|
11
12
|
export const SIGN_IN_COMMAND = "claude auth login";
|
|
12
13
|
export const SANDBOX_COMMAND = "npx toolroll demo";
|
|
13
14
|
/** How a person starts on a real project (the demo hands off to this). */
|
|
@@ -27,7 +28,7 @@ export function signInCommandFor(states, platform) {
|
|
|
27
28
|
return "codex login";
|
|
28
29
|
return `npm install -g @anthropic-ai/claude-code${platform === "win32" ? ";" : " &&"} ${SIGN_IN_COMMAND}`;
|
|
29
30
|
}
|
|
30
|
-
/** The
|
|
31
|
+
/** The steps (the builder step is added by withBuilderStep), each done or with the one action that does it; null once the first result has arrived. */
|
|
31
32
|
export function firstRunSteps(facts) {
|
|
32
33
|
if (facts.firstResultAt !== null)
|
|
33
34
|
return null;
|
|
@@ -39,6 +40,16 @@ export function firstRunSteps(facts) {
|
|
|
39
40
|
{ key: "task", title: "Your first task", done: facts.hasTask, action: facts.hasTask ? null : { kind: "link", label: "New task", href: "/tasks/new" } },
|
|
40
41
|
];
|
|
41
42
|
}
|
|
43
|
+
/** The builder step, before the first task: under `toolroll serve` nothing connects a builder, and a task filed then
|
|
44
|
+
* waits forever, so the steps never all read done without one. It leads to Start a builder, which shows the command. */
|
|
45
|
+
export function withBuilderStep(steps, connected) {
|
|
46
|
+
if (steps === null)
|
|
47
|
+
return null;
|
|
48
|
+
const builder = { key: "builder", title: "Builder connected", done: connected,
|
|
49
|
+
action: connected ? null : { kind: "link", label: "Start a builder", href: START_BUILDER_HREF } };
|
|
50
|
+
const task = steps.findIndex(one => one.key === "task");
|
|
51
|
+
return task < 0 ? [...steps, builder] : [...steps.slice(0, task), builder, ...steps.slice(task)];
|
|
52
|
+
}
|
|
42
53
|
export const GENERIC_FIRST_TASKS = [
|
|
43
54
|
{ source: "generic", label: "Add a test for an untested function", draft: "Find one function without a test and add a focused test for it." },
|
|
44
55
|
{ source: "generic", label: "Fix a lint warning", draft: "Fix one lint warning without changing behaviour." },
|
package/dist/folder-look.d.ts
CHANGED
|
@@ -51,9 +51,12 @@ export type FolderLookRun = {
|
|
|
51
51
|
};
|
|
52
52
|
/** Starts one look. Production starts a child process; tests pass their own. */
|
|
53
53
|
export type FolderLookRunner = (request: FolderLookRequest, deadlineMs: number) => FolderLookRun;
|
|
54
|
+
/** `started`: when the process that answered was started, in this console's order of starts. A caller that joined a
|
|
55
|
+
* look already under way gets that look's start, never its own, so an older answer always reads as older. */
|
|
54
56
|
export type FolderLook<T> = {
|
|
55
57
|
ok: true;
|
|
56
58
|
value: T;
|
|
59
|
+
started?: number;
|
|
57
60
|
} | {
|
|
58
61
|
ok: false;
|
|
59
62
|
why: "no-answer" | "failed";
|
package/dist/folder-look.js
CHANGED
|
@@ -189,6 +189,8 @@ export const childLook = (request, deadlineMs) => {
|
|
|
189
189
|
/** Looks whose process hasn't ended, by request: callers asking the same thing share one; each holds a child slot
|
|
190
190
|
* until its process is gone. */
|
|
191
191
|
const inFlight = new Map();
|
|
192
|
+
/** Looks started so far: each look's place in that order is fixed when its process starts. */
|
|
193
|
+
let starts = 0;
|
|
192
194
|
/** The same request however its paths were spelled: resolved paths, roots in order. */
|
|
193
195
|
function lookKey(request) {
|
|
194
196
|
const sorted = (paths) => paths.map(one => resolve(one)).sort();
|
|
@@ -215,13 +217,15 @@ export async function lookFolders(request, options = {}) {
|
|
|
215
217
|
if (run === undefined) {
|
|
216
218
|
if (inFlight.size >= FOLDER_LOOK_CHILDREN)
|
|
217
219
|
return { ok: false, why: "no-answer" };
|
|
220
|
+
let started;
|
|
218
221
|
try {
|
|
219
|
-
|
|
222
|
+
started = (options.runner ?? childLook)(request, deadlineMs);
|
|
220
223
|
}
|
|
221
224
|
catch {
|
|
222
225
|
return { ok: false, why: "failed" };
|
|
223
226
|
}
|
|
224
|
-
const mine =
|
|
227
|
+
const mine = { ...started, started: ++starts };
|
|
228
|
+
run = mine;
|
|
225
229
|
inFlight.set(key, mine);
|
|
226
230
|
const forget = () => { if (inFlight.get(key) === mine)
|
|
227
231
|
inFlight.delete(key); };
|
|
@@ -238,7 +242,7 @@ export async function lookFolders(request, options = {}) {
|
|
|
238
242
|
const answer = first.value;
|
|
239
243
|
if (answer === null || typeof answer !== "object" || answer.ok !== true)
|
|
240
244
|
return { ok: false, why: "failed" };
|
|
241
|
-
return { ok: true, value: answer.value };
|
|
245
|
+
return { ok: true, value: answer.value, started: run.started };
|
|
242
246
|
}
|
|
243
247
|
finally {
|
|
244
248
|
clearTimeout(timer);
|
|
@@ -287,7 +291,7 @@ export async function lookIdentities(paths, options = {}) {
|
|
|
287
291
|
if (row !== null && typeof row === "object" && text(row.path) && (row.real === null || text(row.real)))
|
|
288
292
|
out.set(row.path, row.real);
|
|
289
293
|
}
|
|
290
|
-
return { ok: true, value: out };
|
|
294
|
+
return { ok: true, value: out, ...(looked.started === undefined ? {} : { started: looked.started }) };
|
|
291
295
|
}
|
|
292
296
|
/** A project's own `.mcp.json` (parsed object, or null when absent or not one), read off the thread. */
|
|
293
297
|
export async function lookProjectMcp(repo, options = {}) {
|
package/dist/guides.js
CHANGED
|
@@ -287,9 +287,10 @@ ${AUTHORITY_LINE}
|
|
|
287
287
|
|
|
288
288
|
- One command, inside the repository: \`npx toolroll up\` (or
|
|
289
289
|
\`bunx toolroll up\`; Node 22.13+ must be installed — Bun's own
|
|
290
|
-
runtime cannot host it). \`up\` mints the first login and
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
runtime cannot host it). \`up\` mints the first login and saves it beside the
|
|
291
|
+
database as \`up-login.txt\` (every start prints that path and the
|
|
292
|
+
\`cat\` command that shows the password, never the password itself),
|
|
293
|
+
registers the machine as a worker, runs the console, opens the browser. Later starts ask for the
|
|
293
294
|
password at most once and remember it on that machine.
|
|
294
295
|
- From a phone over a tailnet: add \`--host 0.0.0.0 --allow-host
|
|
295
296
|
<name>:4180\`.
|
package/dist/model-catalog.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export declare const RUNTIME_TOOLS: Record<RuntimeTool, {
|
|
|
47
47
|
pkg: string;
|
|
48
48
|
}>;
|
|
49
49
|
export declare const WATCH_EVERY_MS: number;
|
|
50
|
+
/** The current Claude models by exact id, offered even before the first catalog check has run. */
|
|
51
|
+
export declare const CLAUDE_EXACT: readonly {
|
|
52
|
+
value: string;
|
|
53
|
+
label: string;
|
|
54
|
+
}[];
|
|
50
55
|
/** `anthropic/claude-opus-5.5` → `claude-opus-5-5`, the name the Claude CLI and API use. */
|
|
51
56
|
export declare function claudeIdOf(openrouterId: string): string | null;
|
|
52
57
|
/** The Codex CLI's own list — account-specific, maintained by the CLI. */
|
package/dist/model-catalog.js
CHANGED
|
@@ -58,6 +58,12 @@ export const WATCH_EVERY_MS = 6 * 3_600_000;
|
|
|
58
58
|
const NEW_FOR_MS = 14 * 86_400_000;
|
|
59
59
|
const FAMILIES = ["opus", "sonnet", "haiku", "fable"];
|
|
60
60
|
const ALIAS_WORDS = { opus: "complex tasks", sonnet: "everyday coding", haiku: "smaller tasks" };
|
|
61
|
+
/** The current Claude models by exact id, offered even before the first catalog check has run. */
|
|
62
|
+
export const CLAUDE_EXACT = Object.freeze([
|
|
63
|
+
{ value: "claude-opus-5-5", label: "Claude Opus 5.5" },
|
|
64
|
+
{ value: "claude-sonnet-5-5", label: "Claude Sonnet 5.5" },
|
|
65
|
+
{ value: "claude-haiku-5-5", label: "Claude Haiku 5.5" },
|
|
66
|
+
]);
|
|
61
67
|
const title = (word) => word.charAt(0).toUpperCase() + word.slice(1);
|
|
62
68
|
const stripVendor = (name) => name.replace(/^(Anthropic|OpenAI|Google): /, "");
|
|
63
69
|
/** `anthropic/claude-opus-5.5` → `claude-opus-5-5`, the name the Claude CLI and API use. */
|
|
@@ -182,12 +188,15 @@ export function modelWords(store, provider, model) {
|
|
|
182
188
|
export function modelOptions(store, provider, now, home = homedir()) {
|
|
183
189
|
const option = (model) => ({ value: model.id, label: `${model.name} · ${priceWords(model)}${isNewModel(model, now) ? " · New" : ""}`, isNew: isNewModel(model, now) });
|
|
184
190
|
if (provider === "claude") {
|
|
191
|
+
// Exact ids first (Settings → Models presets write only these); the short names float to the newest version.
|
|
192
|
+
const seen = seenModels(store, "claude").slice(0, 20);
|
|
193
|
+
const builtIn = CLAUDE_EXACT.filter(one => !seen.some(model => model.id === one.value)).map(one => ({ ...one, isNew: false }));
|
|
185
194
|
const aliases = ["sonnet", "opus", "haiku"].map(alias => {
|
|
186
195
|
const target = aliasTarget(store, alias);
|
|
187
196
|
// Short enough that a phone's select still shows which model it means.
|
|
188
|
-
return { value: alias, label: target === null ? `${title(alias)} — ${ALIAS_WORDS[alias]}` : `${title(alias)} ·
|
|
197
|
+
return { value: alias, label: target === null ? `${title(alias)} (floating) — ${ALIAS_WORDS[alias]}` : `${title(alias)} (floating) · now ${target.name.replace(/^Claude /, "")}`, isNew: false };
|
|
189
198
|
});
|
|
190
|
-
return [...
|
|
199
|
+
return [...seen.map(option), ...builtIn, ...aliases];
|
|
191
200
|
}
|
|
192
201
|
if (provider === "codex") {
|
|
193
202
|
const live = codexCatalog(home);
|