toolroll 0.9.74 → 0.9.76
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/dist/agent-fence.js +8 -0
- package/dist/approval-authority.js +6 -2
- package/dist/approval-rules-ui.d.ts +11 -0
- package/dist/approval-rules-ui.js +13 -1
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +27 -8
- package/dist/batch-checks.js +3 -1
- package/dist/browser/workspace.js +27 -27
- package/dist/browser-workspace.d.ts +1 -1
- package/dist/builder.js +1 -1
- package/dist/chat-acceptance.d.ts +4 -1
- package/dist/chat-acceptance.js +13 -3
- package/dist/cli.js +1 -1
- package/dist/coding-handoff.js +4 -3
- package/dist/coding-ui.js +2 -2
- package/dist/coding-workspace.js +23 -9
- package/dist/contracts/console-api.d.ts +6 -1
- package/dist/contracts/console-api.js +11 -0
- package/dist/control-ui.d.ts +3 -2
- package/dist/control-ui.js +7 -2
- package/dist/demo.js +5 -3
- package/dist/deploy-gate.d.ts +3 -0
- package/dist/deploy-gate.js +3 -2
- package/dist/desktop-update.d.ts +4 -1
- package/dist/desktop-update.js +43 -9
- package/dist/discover.js +6 -1
- package/dist/first-run.d.ts +8 -5
- package/dist/first-run.js +42 -7
- package/dist/flow-card-task.js +3 -2
- package/dist/flow-unattended.d.ts +2 -0
- package/dist/flow-unattended.js +12 -1
- package/dist/git-incident.d.ts +49 -0
- package/dist/git-incident.js +69 -0
- package/dist/git-views.d.ts +20 -0
- package/dist/git-views.js +120 -0
- package/dist/grant.js +3 -1
- package/dist/integrations.d.ts +4 -3
- package/dist/integrations.js +5 -6
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-follow.js +3 -1
- package/dist/openrouter-models.d.ts +5 -2
- package/dist/openrouter-models.js +7 -4
- package/dist/operate.js +9 -1
- package/dist/planner.js +5 -1
- package/dist/project-knowledge.js +1 -1
- package/dist/project-learning.js +65 -7
- package/dist/project-tools.js +19 -1
- package/dist/pull-request-flow.js +4 -0
- package/dist/pull-request-ui.d.ts +1 -1
- package/dist/pull-request-ui.js +5 -1
- package/dist/push.d.ts +2 -0
- package/dist/push.js +24 -19
- package/dist/read-only-request.d.ts +4 -0
- package/dist/read-only-request.js +15 -0
- package/dist/release-adapters.d.ts +24 -2
- package/dist/release-adapters.js +136 -10
- package/dist/release-authority.d.ts +46 -0
- package/dist/release-authority.js +64 -0
- package/dist/release-auto.d.ts +153 -0
- package/dist/release-auto.js +496 -0
- package/dist/release-cli.js +56 -2
- package/dist/release-coverage.js +1 -1
- package/dist/release.d.ts +105 -1
- package/dist/release.js +229 -9
- package/dist/repository-context.js +5 -2
- package/dist/result-follow-ups.js +23 -2
- package/dist/result-review.js +5 -1
- package/dist/scope.d.ts +31 -2
- package/dist/scope.js +44 -3
- package/dist/scout.js +5 -1
- package/dist/serve.js +162 -6
- package/dist/server/agents-live-filed.d.ts +17 -0
- package/dist/server/agents-live-filed.js +97 -0
- package/dist/server/agents-live-room.js +4 -1
- package/dist/server/chat.js +31 -23
- package/dist/server/http.d.ts +5 -0
- package/dist/server/last-checks.d.ts +17 -0
- package/dist/server/last-checks.js +22 -0
- package/dist/server/pages.js +18 -4
- package/dist/server/people-tokens.js +7 -4
- package/dist/server/render-pages.d.ts +2 -1
- package/dist/server/render-pages.js +28 -24
- package/dist/server/render-results.d.ts +11 -2
- package/dist/server/render-results.js +54 -12
- package/dist/server/render-settings.js +3 -3
- package/dist/server/render-tasks.d.ts +10 -0
- package/dist/server/render-tasks.js +30 -17
- package/dist/server/route-table.js +17 -1
- package/dist/server/runtime.d.ts +16 -1
- package/dist/server/settings.js +111 -31
- package/dist/server/task-views.js +8 -1
- package/dist/server/tasks.js +11 -2
- package/dist/storage-ui.d.ts +2 -0
- package/dist/storage-ui.js +1 -1
- package/dist/storage.js +2 -1
- package/dist/store.d.ts +4 -1
- package/dist/store.js +52 -13
- package/dist/subagent-work.js +3 -0
- package/dist/task-base.js +1 -1
- package/dist/task-status.d.ts +28 -1
- package/dist/task-status.js +93 -11
- package/dist/teardown-git.d.ts +59 -5
- package/dist/teardown-git.js +1022 -90
- package/dist/telegram-progress.js +4 -4
- package/dist/telegram-status.js +8 -3
- package/dist/test-infra.d.ts +26 -0
- package/dist/test-infra.js +321 -0
- package/dist/tool-secret-store.js +8 -0
- package/dist/toolroll-update-ui.d.ts +2 -0
- package/dist/toolroll-update-ui.js +4 -3
- package/dist/toolroll-update.js +10 -9
- package/dist/work-index.js +29 -12
- package/dist/workspace-ui.d.ts +5 -0
- package/dist/workspace-ui.js +38 -10
- package/dist/worktree.d.ts +4 -0
- package/dist/worktree.js +13 -7
- package/package.json +1 -1
package/dist/builder.js
CHANGED
|
@@ -255,7 +255,7 @@ const BRANCH_QUERIES = [
|
|
|
255
255
|
function buildCheckoutGit(given, worktree, mainCheckout = null) {
|
|
256
256
|
if (given !== run)
|
|
257
257
|
return given;
|
|
258
|
-
const hardened = checkoutGit(given);
|
|
258
|
+
const hardened = checkoutGit(given, "build", { repo: mainCheckout });
|
|
259
259
|
const real = (path) => { try {
|
|
260
260
|
return realpathSync(resolve(path));
|
|
261
261
|
}
|
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
import type { Store } from "./store.js";
|
|
5
5
|
import type { VerifiedApprover } from "./principal.js";
|
|
6
6
|
import { type CriterionMatrixRow } from "./proof.js";
|
|
7
|
+
import { type ApprovedCheckState } from "./task-status.js";
|
|
7
8
|
export declare const ACCEPTANCE_PAGE_SIZE = 3;
|
|
9
|
+
/** `check`: the approved check read now (task-status.ts approvedCheckOf); undefined when this read had no saved files,
|
|
10
|
+
* and then a verified result claims nothing about its check. */
|
|
8
11
|
export declare function acceptanceStatus(proof: {
|
|
9
12
|
verdict: string;
|
|
10
13
|
reasons: readonly string[];
|
|
11
14
|
matrix: readonly CriterionMatrixRow[];
|
|
12
|
-
} | null, accepted: boolean): string;
|
|
15
|
+
} | null, accepted: boolean, check?: ApprovedCheckState | null): string;
|
|
13
16
|
export declare function readAcceptanceEvidence(store: Store, who: VerifiedApprover, root: string | undefined, task: string, runId?: number, offset?: number): {
|
|
14
17
|
ok: false;
|
|
15
18
|
message: string;
|
package/dist/chat-acceptance.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { readVerifiedArtifact, readVerifiedProofForRun, scanForSecrets } from "./evidence.js";
|
|
2
2
|
import { verificationEvidence } from "./verification-evidence.js";
|
|
3
3
|
import { manualReviewOnly } from "./proof.js";
|
|
4
|
+
import { assignmentChecksForRun } from "./assignment.js";
|
|
5
|
+
import { approvedCheckOf, approvedCheckStateOf, checksFactOf } from "./task-status.js";
|
|
4
6
|
export const ACCEPTANCE_PAGE_SIZE = 3;
|
|
5
|
-
|
|
7
|
+
/** `check`: the approved check read now (task-status.ts approvedCheckOf); undefined when this read had no saved files,
|
|
8
|
+
* and then a verified result claims nothing about its check. */
|
|
9
|
+
export function acceptanceStatus(proof, accepted, check) {
|
|
6
10
|
if (accepted)
|
|
7
11
|
return manualReviewOnly(proof) ? "Accepted by a person" : "Accepted with an exception";
|
|
8
12
|
if (manualReviewOnly(proof))
|
|
9
13
|
return "You check the remaining requirements";
|
|
14
|
+
if (proof?.verdict === "verified" && check !== "passed") {
|
|
15
|
+
return check === undefined ? "Open the result to see its checks" : check === "failed" ? "Checks failed" : check === "running" ? "Checks running" : "The check result couldn't be read";
|
|
16
|
+
}
|
|
10
17
|
return proof === null ? "No checks recorded" : ({ verified: "Checks passed", attested: "Checks reported by the agent", short: "Some required material is missing", refuted: "Saved result conflicts with the approved scope" }[proof.verdict] ?? "Some required material is missing");
|
|
11
18
|
}
|
|
12
19
|
export function readAcceptanceEvidence(store, who, root, task, runId, offset = 0) {
|
|
@@ -48,6 +55,9 @@ export function readAcceptanceEvidence(store, who, root, task, runId, offset = 0
|
|
|
48
55
|
else if (!proof.ok)
|
|
49
56
|
problems.push("The saved proof is unavailable or cannot be read.");
|
|
50
57
|
const verification = root === undefined ? null : verificationEvidence(store, root, run.id);
|
|
58
|
+
// The one answer to "did the approved check pass" (task-status.ts), with follow-ups on the same commit.
|
|
59
|
+
const checkNow = root === undefined || run.role === "scout" ? undefined
|
|
60
|
+
: approvedCheckStateOf(approvedCheckOf(recorded?.verdict ?? null, recorded?.reasons ?? [], checksFactOf(assignmentChecksForRun(store, root, run.id, new Date()), run.headRevision)));
|
|
51
61
|
let gate = { status: "No machine verification receipt available" };
|
|
52
62
|
if (verification !== null && !verification.ok) {
|
|
53
63
|
problems.push(verification.problem);
|
|
@@ -55,7 +65,7 @@ export function readAcceptanceEvidence(store, who, root, task, runId, offset = 0
|
|
|
55
65
|
}
|
|
56
66
|
else if (verification?.ok && verification.bytes !== null) {
|
|
57
67
|
const receipt = JSON.parse(verification.bytes);
|
|
58
|
-
gate = { status: receipt.result.ran ? (receipt.result.exitCode === 0 ? "Passed" : "Failed") : "Did not run", exitCode: receipt.result.exitCode ?? null, command: "The approved project verification command", logShortened: receipt.log.truncated };
|
|
68
|
+
gate = { status: receipt.result.ran ? (receipt.result.exitCode === 0 ? checkNow === undefined || checkNow === "passed" ? "Passed" : "Passed when it ran; not counted now" : "Failed") : "Did not run", exitCode: receipt.result.exitCode ?? null, command: "The approved project verification command", logShortened: receipt.log.truncated };
|
|
59
69
|
}
|
|
60
70
|
// These are summaries for conversation, not substitute approval terms.
|
|
61
71
|
// Full statements, caveats and references remain on the authenticated screen.
|
|
@@ -71,7 +81,7 @@ export function readAcceptanceEvidence(store, who, root, task, runId, offset = 0
|
|
|
71
81
|
return { ok: true, body: {
|
|
72
82
|
task, run: run.id, title: store.getTask(task)?.title ?? task, currentExecution: family.current.id,
|
|
73
83
|
isCurrent: family.current.id === task && store.runsFor(ref.id).find(one => one.finishedAt !== null && ["builder", "repair", "scout"].includes(one.role))?.id === run.id,
|
|
74
|
-
status: acceptanceStatus(recorded, acceptance !== null), recordedVerdict: recorded?.verdict ?? null,
|
|
84
|
+
status: acceptanceStatus(recorded, acceptance !== null, checkNow), recordedVerdict: recorded?.verdict ?? null,
|
|
75
85
|
accepted: acceptance !== null, acceptance: acceptance === null ? null : { at: acceptance.acceptedAt, note: brief(acceptance.note) },
|
|
76
86
|
reasons: (recorded?.reasons ?? []).map(one => brief(one)), gate,
|
|
77
87
|
criteriaTotal: matrix.length, nextCriterionOffset: offset + page.length < matrix.length ? offset + page.length : null,
|
package/dist/cli.js
CHANGED
|
@@ -1301,7 +1301,7 @@ async function runDemoCommand(argv, write) {
|
|
|
1301
1301
|
const now = new Date();
|
|
1302
1302
|
const { sandbox, store, seed, evidenceRoot, passwordFile, lead } = createDemoSandbox(now);
|
|
1303
1303
|
const server = createDecisionServer({
|
|
1304
|
-
store, evidenceRoot, clock: () => new Date(), repos: seed.repos, demoLead: lead,
|
|
1304
|
+
store, evidenceRoot, clock: () => new Date(), repos: seed.repos, demoLead: lead, backgroundChecks: true,
|
|
1305
1305
|
...(allowedHosts.length === 0 ? {} : { allowedHosts }),
|
|
1306
1306
|
});
|
|
1307
1307
|
await new Promise((ready, failed) => {
|
package/dist/coding-handoff.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** A committed native session enters the existing review flow, never another executor. */
|
|
2
|
+
import { checkoutGitSync } from './teardown-git.js';
|
|
2
3
|
import { execFileSync } from 'node:child_process';
|
|
3
4
|
import { lstatSync, realpathSync } from 'node:fs';
|
|
4
5
|
import { sqliteRuntime } from './sqlite-runtime.js';
|
|
@@ -52,11 +53,11 @@ function catalog(store, readOnly) {
|
|
|
52
53
|
throw error;
|
|
53
54
|
}
|
|
54
55
|
}
|
|
56
|
+
// The coding checkout is written by an agent (its config, hooks, filters): every read goes through the hardened
|
|
57
|
+
// checkout git, which the code page's GET preview also uses (docs/plans/get-side-effects.md).
|
|
55
58
|
function git(repo, args) {
|
|
56
59
|
try {
|
|
57
|
-
return
|
|
58
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60_000, maxBuffer: 1_048_576,
|
|
59
|
-
}).trimEnd();
|
|
60
|
+
return checkoutGitSync(repo, ['--no-replace-objects', '--no-lazy-fetch', ...args]).trimEnd();
|
|
60
61
|
}
|
|
61
62
|
catch {
|
|
62
63
|
throw Error('The committed coding result could not be verified in Git. Check the project and try again.');
|
package/dist/coding-ui.js
CHANGED
|
@@ -76,7 +76,7 @@ export function codingWorkspaceHtml(input) {
|
|
|
76
76
|
const blocked = !s.nativeThreadId || !['ready', 'working', 'failed', 'interrupted'].includes(s.status);
|
|
77
77
|
const messages = selected.items.filter(item => item.type !== 'reasoning').map(itemHtml);
|
|
78
78
|
const composer = postForm(`${sessionUrl(s.id)}/send`, html `<label for="coding-prompt">Message Codex</label><textarea id="coding-prompt" name="prompt" rows="2" maxlength="24000" required placeholder="Describe a change or ask about the result…">${input.draft ?? ''}</textarea><div class="coding-actions"><button type="submit"${blocked ? html ` disabled` : ''}>${s.status === 'working' ? 'Send update' : 'Send message'}</button></div>`, { attrs: { 'data-coding-form': 'send', class: 'coding-composer', hidden: s.status === 'closed' }, hidden: { requestId: randomUUID() } });
|
|
79
|
-
main = html `<header class="coding-header"><div><h1>${s.title}</h1><p class="coding-meta">${projectName(s.repo)} · Codex${s.model ? html ` · ${s.model}` : ''}</p></div><details class="coding-session-detail"><summary aria-label="Session details">Details</summary><dl><dt>Branch</dt><dd>${s.branch}</dd><dt>Worktree</dt><dd>${s.worktree}</dd><dt>Native session</dt><dd>${s.nativeThreadId ?? 'Not established'}</dd>${s.initialRequest ? html `<dt>Initial request</dt><dd class="coding-text">${s.initialRequest.prompt}</dd>` : ''}</dl></details></header><div class="coding-state-row"><p id="coding-state" role="status">${stateLabel(s, selected.requests)}</p><div id="coding-controls">${controlsHtml(s)}</div></div><div id="coding-requests">${selected.requests.map(r => requestHtml(r, s.id))}</div><div id="coding-conversation" class="coding-conversation" tabindex="0" aria-label="Conversation"${s.status === 'closed' && messages.length === 0 ? html ` hidden` : ''}>${messages.length ? messages : html `<p class="coding-meta coding-no-messages">No messages yet.</p>`}</div><button type="button" id="coding-latest" class="coding-secondary" hidden>Latest update ↓</button>${composer}<details id="coding-changes" class="coding-changes"><summary>Changes</summary><div id="coding-change-content"><p class="coding-meta">Open to load the current changes.</p></div><button type="button" id="coding-refresh-changes" class="coding-secondary">Refresh changes</button><noscript
|
|
79
|
+
main = html `<header class="coding-header"><div><h1>${s.title}</h1><p class="coding-meta">${projectName(s.repo)} · Codex${s.model ? html ` · ${s.model}` : ''}</p></div><details class="coding-session-detail"><summary aria-label="Session details">Details</summary><dl><dt>Branch</dt><dd>${s.branch}</dd><dt>Worktree</dt><dd>${s.worktree}</dd><dt>Native session</dt><dd>${s.nativeThreadId ?? 'Not established'}</dd>${s.initialRequest ? html `<dt>Initial request</dt><dd class="coding-text">${s.initialRequest.prompt}</dd>` : ''}</dl></details></header><div class="coding-state-row"><p id="coding-state" role="status">${stateLabel(s, selected.requests)}</p><div id="coding-controls">${controlsHtml(s)}</div></div><div id="coding-requests">${selected.requests.map(r => requestHtml(r, s.id))}</div><div id="coding-conversation" class="coding-conversation" tabindex="0" aria-label="Conversation"${s.status === 'closed' && messages.length === 0 ? html ` hidden` : ''}>${messages.length ? messages : html `<p class="coding-meta coding-no-messages">No messages yet.</p>`}</div><button type="button" id="coding-latest" class="coding-secondary" hidden>Latest update ↓</button>${composer}<details id="coding-changes" class="coding-changes"><summary>Changes</summary><div id="coding-change-content"><p class="coding-meta">Open to load the current changes.</p></div><button type="button" id="coding-refresh-changes" class="coding-secondary">Refresh changes</button><noscript>${postForm(`${sessionUrl(s.id)}/changes`, html `<button type="submit" class="coding-secondary">View changes</button>`)}</noscript><p id="coding-shipping"${['closed', 'uncertain'].includes(s.status) ? html ` hidden` : ''}>${postForm(`${sessionUrl(s.id)}/ship/preview`, html `<button type="submit" class="coding-link-button">Review for shipping</button>`)}</p></details>`;
|
|
80
80
|
}
|
|
81
81
|
return html `<section class="coding-workspace" data-coding-session="${selected?.session.id ?? ''}" data-coding-owner="${selected?.session.owner ?? input.owner ?? ''}" data-coding-project="${selectedRepo ?? input.projects[0]?.path ?? ''}" data-coding-status="${selected?.session.status ?? ''}" data-coding-native-thread="${String(Boolean(selected?.session.nativeThreadId))}" data-coding-revision="${selected?.revision ?? -1}" data-coding-csrf="${csrf}">${sidebar}<div class="coding-main"><div id="coding-error" class="coding-notice" role="alert"${error ? '' : html ` hidden`}>${noticeHtml(error)}</div><p id="coding-connection" class="coding-meta" role="status" hidden></p>${main}</div></section>`;
|
|
82
82
|
}
|
|
@@ -276,7 +276,7 @@ export function codingWorkspaceScript() {
|
|
|
276
276
|
const area = root.querySelector('#coding-change-content');
|
|
277
277
|
const button = root.querySelector('#coding-refresh-changes'); button.disabled = true;
|
|
278
278
|
try {
|
|
279
|
-
const response = await fetch(base + '/changes', {headers:{accept:'application/json'},credentials:'same-origin',cache:'no-store',signal:requestSignal()});
|
|
279
|
+
const response = await fetch(base + '/changes', {method:'POST',headers:{accept:'application/json'},body:new URLSearchParams({csrf}),credentials:'same-origin',cache:'no-store',signal:requestSignal()});
|
|
280
280
|
if (response.status === 401 || response.redirected) { signedOut(); return; }
|
|
281
281
|
if (!response.ok) throw new Error('Could not load changes. Try Refresh changes.');
|
|
282
282
|
const changes = await response.json();
|
package/dist/coding-workspace.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkoutGit } from './teardown-git.js';
|
|
1
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
3
|
import { mkdirSync, chmodSync, realpathSync } from 'node:fs';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
@@ -399,8 +400,10 @@ export class CodingWorkspace {
|
|
|
399
400
|
}, 50);
|
|
400
401
|
}
|
|
401
402
|
}
|
|
402
|
-
async git(repo, args) {
|
|
403
|
-
|
|
403
|
+
async git(repo, args, project = repo) {
|
|
404
|
+
// The project's repository and the session's checkout are agent-written: the hardened git, against a frozen view of
|
|
405
|
+
// the repository's config and info (teardown-git.ts checkoutGit).
|
|
406
|
+
const result = await checkoutGit(run, 'build', { repo: project })('git', ['--no-optional-locks', ...args], { cwd: repo, timeoutMs: 60_000, maxBuffer: 1_048_576 });
|
|
404
407
|
if (result.code !== 0)
|
|
405
408
|
throw Error('Git could not complete this workspace operation. Check the repository and try again.');
|
|
406
409
|
return result.stdout;
|
|
@@ -519,13 +522,13 @@ export class CodingWorkspace {
|
|
|
519
522
|
}
|
|
520
523
|
async verifyWorktree(session) {
|
|
521
524
|
const [branch, common, original] = await Promise.all([
|
|
522
|
-
this.git(session.worktree, ['symbolic-ref', '--short', 'HEAD']),
|
|
523
|
-
this.git(session.worktree, ['rev-parse', '--path-format=absolute', '--git-common-dir']),
|
|
525
|
+
this.git(session.worktree, ['symbolic-ref', '--short', 'HEAD'], session.repo),
|
|
526
|
+
this.git(session.worktree, ['rev-parse', '--path-format=absolute', '--git-common-dir'], session.repo),
|
|
524
527
|
this.git(session.repo, ['rev-parse', '--path-format=absolute', '--git-common-dir']),
|
|
525
528
|
]);
|
|
526
529
|
if (branch.trim() !== session.branch || realpathSync(common.trim()) !== realpathSync(original.trim()))
|
|
527
530
|
throw Error('This checkout no longer matches the saved project and branch. Restore that checkout before resuming.');
|
|
528
|
-
await this.git(session.worktree, ['merge-base', '--is-ancestor', session.base, 'HEAD']);
|
|
531
|
+
await this.git(session.worktree, ['merge-base', '--is-ancestor', session.base, 'HEAD'], session.repo);
|
|
529
532
|
}
|
|
530
533
|
priorProcessesGone() {
|
|
531
534
|
const row = this.db.prepare('SELECT payload FROM coding_custody WHERE singleton=1').get();
|
|
@@ -729,10 +732,21 @@ export class CodingWorkspace {
|
|
|
729
732
|
}
|
|
730
733
|
async changes(id, actor) {
|
|
731
734
|
const session = this.get(id, actor);
|
|
735
|
+
// The code page reads this on a GET (docs/plans/get-side-effects.md), from a checkout the agent writes: every git
|
|
736
|
+
// here is the hardened checkout git, which refuses a config that could run a command and reads no global config.
|
|
737
|
+
// Read mode: the read flags, and the checkout's config and info/attributes as read once (teardown-git.ts readView),
|
|
738
|
+
// hold even if the agent changes them after checkoutGit read them: no hook, pager, filter or network.
|
|
739
|
+
const safe = checkoutGit(run, 'read');
|
|
740
|
+
const read = async (args) => {
|
|
741
|
+
const result = await safe('git', args, { cwd: session.worktree, timeoutMs: 60_000, maxBuffer: 1_048_576 });
|
|
742
|
+
if (result.code !== 0)
|
|
743
|
+
throw Error('Git could not complete this workspace operation. Check the repository and try again.');
|
|
744
|
+
return result.stdout;
|
|
745
|
+
};
|
|
732
746
|
const [head, status, tracked, untracked] = await Promise.all([
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
747
|
+
read(['rev-parse', 'HEAD']), read(['status', '--short']),
|
|
748
|
+
safe('git', ['diff', '--no-ext-diff', '--no-textconv', session.base, '--'], { cwd: session.worktree, timeoutMs: 60_000, maxBuffer: 1_048_576 }),
|
|
749
|
+
read(['ls-files', '--others', '--exclude-standard', '-z']),
|
|
736
750
|
]);
|
|
737
751
|
let diff = tracked.stdout, truncated = tracked.code !== 0;
|
|
738
752
|
// git's no-index diff describes symlinks themselves; it does not follow a
|
|
@@ -743,7 +757,7 @@ export class CodingWorkspace {
|
|
|
743
757
|
truncated = true;
|
|
744
758
|
break;
|
|
745
759
|
}
|
|
746
|
-
const part = await
|
|
760
|
+
const part = await safe('git', ['diff', '--no-index', '--no-ext-diff', '--no-textconv', '--', '/dev/null', paths[index]], { cwd: session.worktree, timeoutMs: 60_000, maxBuffer: 131_072 });
|
|
747
761
|
diff += part.stdout;
|
|
748
762
|
if (part.code !== 0 && part.code !== 1)
|
|
749
763
|
truncated = true;
|
|
@@ -72,6 +72,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
72
72
|
readonly code: FormContract<"acceptance" | "answers" | "base" | "candidate" | "decision" | "goal" | "model" | "password" | "prompt" | "repo" | "requestId" | "title" | "visual" | `question:${string}`>;
|
|
73
73
|
readonly sweep: FormContract<"confirm" | "snapshot">;
|
|
74
74
|
readonly passwordless: FormContract<"action" | "password">;
|
|
75
|
+
readonly releaseApproval: FormContract<"action" | "password" | "repo">;
|
|
75
76
|
readonly term: FormContract<"password" | "program" | "repo">;
|
|
76
77
|
readonly skillsRevise: FormContract<"feedback" | "nonce" | "repo" | "run">;
|
|
77
78
|
readonly skillsChange: FormContract<"action" | "content" | "files" | "identity" | "method" | "nonce" | "repo" | "restore" | "revision" | "sample" | "sha" | "url">;
|
|
@@ -123,6 +124,9 @@ export declare const CONSOLE_FORMS: {
|
|
|
123
124
|
readonly notifications: FormContract<"digest" | "mode" | "screenshots">;
|
|
124
125
|
readonly notificationsMute: FormContract<"pings" | "repo">;
|
|
125
126
|
readonly telegramDigest: FormContract<"every">;
|
|
127
|
+
readonly pullRequestsCheck: FormContract<"repo">;
|
|
128
|
+
readonly connectionCheck: FormContract<"auth-mode" | "provider" | "return" | "value">;
|
|
129
|
+
readonly modelsRefresh: FormContract<"command" | "fingerprint" | "model" | "nonce" | "provider" | "repo" | "seconds" | "task" | "token">;
|
|
126
130
|
readonly providerKey: FormContract<"auth-mode" | "provider" | "value">;
|
|
127
131
|
readonly telegramToken: FormContract<"token">;
|
|
128
132
|
readonly email: FormContract<"from" | "host" | "imapHost" | "imapPort" | "password" | "port" | "secure" | "user">;
|
|
@@ -149,6 +153,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
149
153
|
readonly leadIdentity: FormContract<"name" | "persona">;
|
|
150
154
|
readonly leadAbout: FormContract<"about">;
|
|
151
155
|
readonly leadPromiseCancel: FormContract<"promise">;
|
|
156
|
+
readonly leadCheck: FormContract<"return">;
|
|
152
157
|
readonly chatConfig: FormContract<"daily-turns" | "forget-key" | "key" | "model" | "off" | "provider" | "return" | "token" | "weekly-usd">;
|
|
153
158
|
readonly leadMint: FormContract<"ceiling-usd" | "follow" | "return" | "token">;
|
|
154
159
|
readonly leadFollow: FormContract<"enabled" | "return">;
|
|
@@ -190,7 +195,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
190
195
|
};
|
|
191
196
|
export type ConsoleFormName = keyof typeof CONSOLE_FORMS;
|
|
192
197
|
/** The console POSTs whose handlers read no field (sign-out not even the shared guard): their contract is the empty form. */
|
|
193
|
-
export declare const BODILESS_POSTS: readonly ["POST /settings/models/<unknown action>", "POST /settings/lead/on", "POST /r/<run>/draft-repair", "POST /ledger/checkpoint", "POST /settings/backups/now", "POST /mode/revoke", "POST /settings/telegram/retry", "POST /providers/<provider>/resume", "POST /logout", "POST /t/<task>/unhold", "POST /t/<task>/stop-carrying", "POST /t/<task>/plan", "POST /t/<task>/retry-review"];
|
|
198
|
+
export declare const BODILESS_POSTS: readonly ["POST /settings/models/<unknown action>", "POST /settings/lead/on", "POST /r/<run>/draft-repair", "POST /ledger/checkpoint", "POST /settings/backups/now", "POST /mode/revoke", "POST /settings/telegram/retry", "POST /providers/<provider>/resume", "POST /logout", "POST /t/<task>/unhold", "POST /t/<task>/stop-carrying", "POST /t/<task>/plan", "POST /t/<task>/retry-review", "POST /settings/updates/check", "POST /settings/storage/check", "POST /projects/github", "POST /code/<session>/ship/preview"];
|
|
194
199
|
/** The field names a console form's handler may read. */
|
|
195
200
|
export type FormFieldOf<N extends ConsoleFormName> = (typeof CONSOLE_FORMS)[N] extends FormContract<infer K> ? K : never;
|
|
196
201
|
/** `GET /ledger?format=json`: the chain's state, a page of entries (newest first) and the cursor for the next. */
|
|
@@ -81,6 +81,7 @@ export const CONSOLE_FORMS = {
|
|
|
81
81
|
code: formContract("POST /code, /code/start, /code/<session>/<act>, /code/<anything else>", ["repo", "password", "title", "model", "prompt", "requestId", "base", "candidate", "goal", "acceptance", "visual", "decision", "answers"], { prefixes: ["question:"] }),
|
|
82
82
|
sweep: formContract("POST /sweeps/<sweep>/cancel-rest", ["confirm", "snapshot"]),
|
|
83
83
|
passwordless: formContract("POST /settings/passwordless", ["action", "password"]),
|
|
84
|
+
releaseApproval: formContract("POST /settings/release-approval", ["repo", "action", "password"]),
|
|
84
85
|
term: formContract("POST /term/start, /term/<terminal>/close, /term/<terminal>/remove", ["repo", "program", "password"]),
|
|
85
86
|
skillsRevise: formContract("POST /settings/skills/revise", ["run", "repo", "feedback", "nonce"]),
|
|
86
87
|
skillsChange: formContract("POST /settings/skills/import, /settings/skills/change", ["repo", "identity", "revision", "method", "content", "files", "url", "action", "sha", "sample", "nonce", "restore"]),
|
|
@@ -132,6 +133,10 @@ export const CONSOLE_FORMS = {
|
|
|
132
133
|
notifications: formContract("POST /settings/notifications", ["mode", "digest", "screenshots"]),
|
|
133
134
|
notificationsMute: formContract("POST /settings/notifications/mute", ["repo", "pings"]),
|
|
134
135
|
telegramDigest: formContract("POST /settings/telegram-digest", ["every"]),
|
|
136
|
+
// Check again sits in the provider-key form and the setup card; Refresh models in the setup form.
|
|
137
|
+
pullRequestsCheck: formContract("POST /settings/pull-requests/check", ["repo"]),
|
|
138
|
+
connectionCheck: formContract("POST /settings/connection/check", ["provider", "auth-mode", "value", "return"]),
|
|
139
|
+
modelsRefresh: formContract("POST /control/models-refresh", ["repo", "nonce", "fingerprint", "token", "provider", "model", "command", "seconds", "task"]),
|
|
135
140
|
providerKey: formContract("POST /settings/provider-key, /settings/provider-key-clear", ["provider", "auth-mode", "value"]),
|
|
136
141
|
telegramToken: formContract("POST /settings/telegram-token", ["token"]),
|
|
137
142
|
email: formContract("POST /settings/email, /settings/email-test, /settings/email-read-test", ["host", "port", "secure", "user", "from", "password", "imapHost", "imapPort"]),
|
|
@@ -158,6 +163,7 @@ export const CONSOLE_FORMS = {
|
|
|
158
163
|
leadIdentity: formContract("POST /settings/lead/identity", ["name", "persona"]),
|
|
159
164
|
leadAbout: formContract("POST /settings/lead/about", ["about"]),
|
|
160
165
|
leadPromiseCancel: formContract("POST /settings/lead/promise/cancel", ["promise"]),
|
|
166
|
+
leadCheck: formContract("POST /lead/check", ["return"]),
|
|
161
167
|
chatConfig: formContract("POST /chat/config", ["return", "token", "off", "forget-key", "provider", "model", "weekly-usd", "daily-turns", "key"]),
|
|
162
168
|
leadMint: formContract("POST /chat/mate/mint", ["return", "ceiling-usd", "token", "follow"]),
|
|
163
169
|
leadFollow: formContract("POST /chat/mate/follow", ["enabled", "return"]),
|
|
@@ -216,6 +222,11 @@ export const BODILESS_POSTS = [
|
|
|
216
222
|
"POST /t/<task>/stop-carrying",
|
|
217
223
|
"POST /t/<task>/plan",
|
|
218
224
|
"POST /t/<task>/retry-review",
|
|
225
|
+
// Check now and its kin (docs/plans/get-side-effects.md): the form token only, no fields read.
|
|
226
|
+
"POST /settings/updates/check",
|
|
227
|
+
"POST /settings/storage/check",
|
|
228
|
+
"POST /projects/github",
|
|
229
|
+
"POST /code/<session>/ship/preview",
|
|
219
230
|
];
|
|
220
231
|
// ---- the JSON API ------------------------------------------------------------------------------------------------
|
|
221
232
|
const ledgerEntrySchema = z.looseObject({
|
package/dist/control-ui.d.ts
CHANGED
|
@@ -6,14 +6,15 @@ import { type ModelChoice } from "./setup-guide.js";
|
|
|
6
6
|
import type { SetupInputs } from "./control-setup.js";
|
|
7
7
|
import { type OpenRouterModels } from "./openrouter-models.js";
|
|
8
8
|
export declare function connectionWords(connection: ProviderConnection): string;
|
|
9
|
-
|
|
9
|
+
/** The last sign-in check (null: none yet). Check again posts: a page never runs a check (docs/plans/get-side-effects.md). */
|
|
10
|
+
export declare function connectionHtml(provider: ProviderId, connection: ProviderConnection | null, task?: string): Html;
|
|
10
11
|
export declare function controlSetupHtml(data: {
|
|
11
12
|
repo: string;
|
|
12
13
|
csrf: string;
|
|
13
14
|
provider: ProviderId;
|
|
14
15
|
inputs: SetupInputs;
|
|
15
16
|
models: ModelChoice[];
|
|
16
|
-
connection: ProviderConnection;
|
|
17
|
+
connection: ProviderConnection | null;
|
|
17
18
|
catalog: OpenRouterModels | null;
|
|
18
19
|
task?: string;
|
|
19
20
|
preparation: {
|
package/dist/control-ui.js
CHANGED
|
@@ -8,15 +8,20 @@ export function connectionWords(connection) {
|
|
|
8
8
|
unverified: "Not verified", "key-present": "API key saved · not verified", "key-works": "API key works", "key-refused": "API key refused", "missing-key": "API key needed",
|
|
9
9
|
}[connection.state];
|
|
10
10
|
}
|
|
11
|
+
/** The last sign-in check (null: none yet). Check again posts: a page never runs a check (docs/plans/get-side-effects.md). */
|
|
11
12
|
export function connectionHtml(provider, connection, task = "") {
|
|
12
13
|
const query = new URLSearchParams({ provider, ...(task ? { task } : {}) });
|
|
13
|
-
|
|
14
|
+
const back = `/control?${query.toString()}`;
|
|
15
|
+
const check = postForm("/settings/connection/check", html `<button type="submit" class="secondary">${connection === null ? "Check now" : "Check again"}</button>`, { attrs: { class: "inline" }, returnTo: back, hidden: { provider } });
|
|
16
|
+
if (connection === null)
|
|
17
|
+
return html `<div class="card assistant-account"><p><strong>${ASSISTANTS[provider].name} · Not checked yet</strong></p>${check}</div>`;
|
|
18
|
+
return html `<div class="card assistant-account"><p><strong>${ASSISTANTS[provider].name} · ${connectionWords(connection)}</strong></p><p class="meta">${[connection.email, connection.plan, connection.method].filter(Boolean).join(" · ")}</p><p class="meta">Checked ${new Date(connection.checkedAt).toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", timeZoneName: "short" })} on the computer running this console. Sign-in does not prove model access or remaining quota.</p><a class="button-link" href="/control/connection?${query.toString()}">${connection.state === "connected" ? "Manage connection" : "Connect account"}</a> ${check}</div>`;
|
|
14
19
|
}
|
|
15
20
|
export function controlSetupHtml(data) {
|
|
16
21
|
const { provider, inputs, task = "" } = data;
|
|
17
22
|
const query = new URLSearchParams({ provider, ...(task ? { task } : {}) });
|
|
18
23
|
const model = data.catalog !== null
|
|
19
|
-
? openRouterPicker(data.catalog, inputs.model || null,
|
|
24
|
+
? openRouterPicker(data.catalog, inputs.model || null, "/control/models-refresh")
|
|
20
25
|
: html `<label>Model<input type="text" name="model" value="${inputs.model}" list="setup-models" required autocomplete="off" placeholder="Exact model id or supported alias"></label><datalist id="setup-models">${data.models.map(one => html `<option value="${one.value}">${one.label}</option>`)}</datalist>`;
|
|
21
26
|
return html `<h1>Project setup</h1><p>${data.repo}</p><p class="meta">Choose the default builder for new tasks and the preparation command for this project.</p><nav class="assistant-picker row" aria-label="Choose provider">${joinHtml(Object.keys(ASSISTANTS).map(id => html `<a class="button-link" href="/control?${new URLSearchParams({ provider: id, ...(task ? { task } : {}) }).toString()}"${id === provider ? html ` aria-current="page"` : ""}>${ASSISTANTS[id].name}</a>`), " ")}</nav>${connectionHtml(provider, data.connection, task)}${postForm("/control/setup-preview", html `${model}<details class="setup-advanced"><summary>Project preparation</summary>${data.preparation === null ? "" : html `<p class="meta">Suggested from ${data.preparation.evidence}: ${data.preparation.label} — <code>${data.preparation.command}</code>. Review before approving.</p>`}<label>Preparation command<textarea name="command" rows="2" maxlength="2000">${inputs.command}</textarea></label><label>Preparation timeout in seconds<input name="seconds" type="number" min="1" max="3600" value="${inputs.seconds}" required></label><p class="meta">Preparation applies to subsequent runs, including already approved tasks. Saving setup does not execute it.</p></details><button type="submit">Review project setup</button>`, { attrs: { class: "card" }, hidden: { repo: data.repo, provider, task } })}<section class="card"><h2>Agent instructions</h2>${data.instructions.message ? html `<p>${data.instructions.message}</p>` : data.instructions.installed ? html `<p>Toolroll instructions are installed.</p>` :
|
|
22
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>`;
|
package/dist/demo.js
CHANGED
|
@@ -1481,7 +1481,7 @@ export function createDemoLead(input) {
|
|
|
1481
1481
|
try {
|
|
1482
1482
|
advance(clock());
|
|
1483
1483
|
}
|
|
1484
|
-
catch { /* the next
|
|
1484
|
+
catch { /* the next beat or background pass advances it again */ }
|
|
1485
1485
|
}, beat * stepMs + 20);
|
|
1486
1486
|
timer.unref();
|
|
1487
1487
|
timers.add(timer);
|
|
@@ -1489,8 +1489,10 @@ export function createDemoLead(input) {
|
|
|
1489
1489
|
void exchange;
|
|
1490
1490
|
};
|
|
1491
1491
|
return {
|
|
1492
|
-
|
|
1493
|
-
|
|
1492
|
+
// Reads only (docs/plans/get-side-effects.md): the beat timers, the console's background pass and the person's own
|
|
1493
|
+
// acts move the story on, never a page visit.
|
|
1494
|
+
exchanges: () => list,
|
|
1495
|
+
version: () => changes,
|
|
1494
1496
|
ask(text, now) {
|
|
1495
1497
|
advance(now);
|
|
1496
1498
|
const asked = said(text);
|
package/dist/deploy-gate.d.ts
CHANGED
|
@@ -14,4 +14,7 @@ export declare function deploymentGate(store: Store, input: {
|
|
|
14
14
|
runId: number;
|
|
15
15
|
evidenceRoot: string;
|
|
16
16
|
now: Date;
|
|
17
|
+
/** Inside `toolroll release` (its own reserved deploy): a release's own automatic approval also stands
|
|
18
|
+
* (docs/plans/auto-approve-green.md). Anywhere else only a person's approval deploys. */
|
|
19
|
+
releaseFlow?: boolean;
|
|
17
20
|
}): DeploymentGate;
|
package/dist/deploy-gate.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* same checks for the deployment script, proven again there with the installed runtime's modules.)
|
|
7
7
|
*/
|
|
8
8
|
import { assignmentOf } from "./assignment.js";
|
|
9
|
-
import { liveReleaseOf, personApprovalOf } from "./scope.js";
|
|
9
|
+
import { liveReleaseOf, personApprovalOf, releaseApprovalOf } from "./scope.js";
|
|
10
10
|
import { verificationEvidence } from "./verification-evidence.js";
|
|
11
11
|
/** Whether run `runId` is a deployable candidate now, read only from the plane's records and this runtime's code. */
|
|
12
12
|
export function deploymentGate(store, input) {
|
|
@@ -33,7 +33,8 @@ export function deploymentGate(store, input) {
|
|
|
33
33
|
const release = liveReleaseOf(store, ref.externalId, "deploy", input.now);
|
|
34
34
|
if (release.applies && !release.ok)
|
|
35
35
|
return refuse(`This release can't deploy: ${release.why}.`);
|
|
36
|
-
const anchorPerson = release.applies && release.ok
|
|
36
|
+
const anchorPerson = release.applies && release.ok
|
|
37
|
+
? (input.releaseFlow === true ? releaseApprovalOf(store, release.authority.anchorTask, input.now) : personApprovalOf(store, release.authority.anchorTask)) : null;
|
|
37
38
|
const person = anchorPerson ?? personApprovalOf(store, ref.externalId);
|
|
38
39
|
if (!person.ok)
|
|
39
40
|
return refuse(`A person has to approve this candidate's scope: ${person.why}.`);
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -92,9 +92,11 @@ export declare function previewDesktopUpdate(stateDir: string, installed: string
|
|
|
92
92
|
}>;
|
|
93
93
|
export declare function prepareDesktopUpdate(stateDir: string, installed: string, candidate: string, label: string, expected: string, hooks?: UpdateHooks): Promise<UpdateJournal>;
|
|
94
94
|
export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
|
|
95
|
+
/** Whether another process holds the lock in `file`, without making the file or changing it: a status read (a page
|
|
96
|
+
* shows it, docs/plans/get-side-effects.md). A file that isn't there yet is held by nobody. */
|
|
97
|
+
export declare function lockHeld(file: string): boolean;
|
|
95
98
|
export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
|
|
96
99
|
export declare function desktopUpdateStatus(stateDir: string): {
|
|
97
|
-
version?: never;
|
|
98
100
|
active: boolean;
|
|
99
101
|
phase: string;
|
|
100
102
|
detail: string;
|
|
@@ -108,6 +110,7 @@ export declare function desktopUpdateStatus(stateDir: string): {
|
|
|
108
110
|
wasRunning?: never;
|
|
109
111
|
backupPath?: never;
|
|
110
112
|
updateId?: never;
|
|
113
|
+
version?: never;
|
|
111
114
|
buildId?: never;
|
|
112
115
|
workDir?: never;
|
|
113
116
|
} | {
|
package/dist/desktop-update.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inReadOnlyRequest } from "./read-only-request.js";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
3
|
import { closeSync, chmodSync, copyFileSync, cpSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
@@ -261,11 +262,14 @@ export function desktopUpdateWaitingOf(databaseFile, inTheWay, stateDirs = [dirn
|
|
|
261
262
|
}
|
|
262
263
|
if (!j || resolve(j.databaseFile) !== resolve(databaseFile))
|
|
263
264
|
continue;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
// A page visit only reads (docs/plans/get-side-effects.md): the console's background pass releases it.
|
|
266
|
+
if (!inReadOnlyRequest()) {
|
|
267
|
+
try {
|
|
268
|
+
if (releaseStalledDesktopUpdate(stateDir))
|
|
269
|
+
j = readUpdateJournal(stateDir);
|
|
270
|
+
}
|
|
271
|
+
catch { /* shown as last saved */ }
|
|
267
272
|
}
|
|
268
|
-
catch { /* shown as last saved */ }
|
|
269
273
|
const version = j.next.version;
|
|
270
274
|
if (j.phase === "draining")
|
|
271
275
|
return updateWaitingWords({ app: true, version, stopped: false, lingering: j.waiting ?? null });
|
|
@@ -394,14 +398,46 @@ export function sqliteLock(file, busyTimeoutMs = 0) {
|
|
|
394
398
|
throw error;
|
|
395
399
|
}
|
|
396
400
|
}
|
|
401
|
+
/** Whether another process holds the lock in `file`, without making the file or changing it: a status read (a page
|
|
402
|
+
* shows it, docs/plans/get-side-effects.md). A file that isn't there yet is held by nobody. */
|
|
403
|
+
export function lockHeld(file) {
|
|
404
|
+
let st;
|
|
405
|
+
try {
|
|
406
|
+
st = lstatSync(file);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
// Not a plain file of ours (a link, or one someone else made): treated as held, so nothing goes ahead on it.
|
|
412
|
+
if (!st.isFile() || (typeof process.getuid === "function" && st.uid !== process.getuid()))
|
|
413
|
+
return true;
|
|
414
|
+
let db;
|
|
415
|
+
// Read-only: it never makes the file. The holder's exclusive lock stops even a read.
|
|
416
|
+
try {
|
|
417
|
+
db = new (sqlite().DatabaseSync)(file, { readOnly: true });
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
db.exec("PRAGMA busy_timeout=0");
|
|
424
|
+
db.prepare("SELECT COUNT(*) FROM sqlite_master").get();
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
catch (error) {
|
|
428
|
+
return String(error).includes("locked") || String(error).includes("busy") || [5, 6].includes(error.errcode ?? 0);
|
|
429
|
+
}
|
|
430
|
+
finally {
|
|
431
|
+
db.close();
|
|
432
|
+
}
|
|
433
|
+
}
|
|
397
434
|
const workerLock = (j, busyTimeoutMs = 0) => sqliteLock(join(j.workDir, "worker.sqlite"), busyTimeoutMs);
|
|
398
435
|
export const installationLock = (j) => sqliteLock(join(dirname(j.workDir), `installation-${digest(j.old.path).slice(0, 24)}.sqlite`));
|
|
399
436
|
export function desktopUpdateStatus(stateDir) {
|
|
400
437
|
const j = readUpdateJournal(stateDir);
|
|
401
438
|
if (!j)
|
|
402
439
|
return { active: false, phase: "none", detail: "No update is in progress.", running: false, canResume: false, canCancel: false };
|
|
403
|
-
const
|
|
404
|
-
lock?.close();
|
|
440
|
+
const running = lockHeld(join(j.workDir, "worker.sqlite"));
|
|
405
441
|
let recovery = null;
|
|
406
442
|
try {
|
|
407
443
|
privateFile(join(j.workDir, "recovery.json"));
|
|
@@ -409,9 +445,7 @@ export function desktopUpdateStatus(stateDir) {
|
|
|
409
445
|
}
|
|
410
446
|
catch { /* A legacy/manual update has no guardian. */ }
|
|
411
447
|
const wantsAutomatic = recovery?.id === j.id && ["armed", "running", "backoff"].includes(recovery.state ?? "") && !terminal(j.phase);
|
|
412
|
-
const
|
|
413
|
-
const automatic = wantsAutomatic && (guardianLock === null || Date.now() - Date.parse(recovery?.updatedAt ?? "") < 45_000);
|
|
414
|
-
guardianLock?.close();
|
|
448
|
+
const automatic = wantsAutomatic && (lockHeld(join(j.workDir, "guardian.sqlite")) || Date.now() - Date.parse(recovery?.updatedAt ?? "") < 45_000);
|
|
415
449
|
const detail = wantsAutomatic && !automatic ? "Automatic recovery has not restarted. Work remains paused; use Retry safely in Update status." : recovery?.state === "attention" || (automatic && (!running || j.phase === "needs-attention")) ? recovery?.detail ?? j.detail : j.detail;
|
|
416
450
|
return { active: !terminal(j.phase), phase: j.phase, detail, error: j.error ?? null, running, automaticRecovery: automatic, recoveryAttempts: recovery?.attempts ?? 0, recoveryRepairs: recovery?.repairs ?? 0, canResume: !running && !automatic && !terminal(j.phase), canCancel: !terminal(j.phase), wasRunning: j.wasRunning && !updateStopRequested(j), backupPath: j.backupPath ?? null, updateId: j.id, version: j.next.version, buildId: j.next.buildId, workDir: j.workDir };
|
|
417
451
|
}
|
package/dist/discover.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { basename } from "node:path";
|
|
21
21
|
import { run } from "./exec.js";
|
|
22
|
+
import { checkoutGit } from "./teardown-git.js";
|
|
22
23
|
import { parseWorktrees, parseBranches } from "./git.js";
|
|
23
24
|
import { findRepos } from "./scan.js";
|
|
24
25
|
/** Enough to keep git busy without turning a laptop fan on. */
|
|
@@ -43,7 +44,11 @@ const BRANCH_FORMAT = "--format=%(refname:short)%09%(upstream:track)%09%(committ
|
|
|
43
44
|
const BRANCH_FORMAT_CHEAP = "--format=%(refname:short)%09%09%(committerdate:iso8601-strict)";
|
|
44
45
|
export async function inspectRepo(path, options = {}) {
|
|
45
46
|
const { runner = run, dirty = false } = options;
|
|
46
|
-
|
|
47
|
+
// Scanned and enrolled projects share their repository with every build, which can write its config, info and hooks
|
|
48
|
+
// (`--dirty`'s status would run a planted fsmonitor or filter): the hardened git, against a frozen view of the
|
|
49
|
+
// repository (teardown-git.ts checkoutGit). An injected runner (tests) is used as given.
|
|
50
|
+
const hardened = runner === run ? checkoutGit(run, "build") : runner;
|
|
51
|
+
const git = (args, timeoutMs = REF_TIMEOUT_MS) => hardened(GIT, [...READ_ONLY, ...args], { cwd: path, timeoutMs });
|
|
47
52
|
const [worktrees, branchRead, head, remote, status] = await Promise.all([
|
|
48
53
|
git(["worktree", "list", "--porcelain"]),
|
|
49
54
|
readBranches(git),
|
package/dist/first-run.d.ts
CHANGED
|
@@ -94,14 +94,17 @@ export declare function firstTaskJourney(read: {
|
|
|
94
94
|
stage: "queued" | "planning" | "needs-you" | "waiting" | "building" | "checking" | "reviewing" | "finished" | "complete" | "failed" | "stopped";
|
|
95
95
|
need?: string | undefined;
|
|
96
96
|
}, approved: boolean, built?: boolean): JourneyStep[];
|
|
97
|
-
/**
|
|
98
|
-
|
|
97
|
+
/** The approved check as the task's status reads it (task-status.ts `approvedCheckStateOf`), for its road: `null` when
|
|
98
|
+
* nothing says (the step is then shown skipped, never done). The road reads this one answer, never a row's words. */
|
|
99
|
+
export type RoadChecks = "passed" | "running" | "failed" | "not-run" | "off" | "unreadable" | null;
|
|
99
100
|
/**
|
|
100
101
|
* Any task's road (the read-only stepper on its page, after ReUI's Stepper): the first task's five steps, told only
|
|
101
102
|
* from recorded facts. You approve is done only when the plan was approved (a rewritten plan waiting on approval
|
|
102
|
-
* stands there again); a task that never needed one shows it skipped. Checks is done only when
|
|
103
|
-
* running,
|
|
104
|
-
*
|
|
103
|
+
* stands there again); a task that never needed one shows it skipped. Checks is done only when the approved check
|
|
104
|
+
* passed; a running, missing or Off check says so, and with no checks fact it is skipped. A failed check, or one that
|
|
105
|
+
* can't be read, stops the road at Checks: Ready isn't done (fail closed), even on a task a person marked complete.
|
|
106
|
+
* A research report has no Checks step: a report stopped there stops at
|
|
107
|
+
* Ready. The step that waits on a person carries `needsYou`.
|
|
105
108
|
*/
|
|
106
109
|
export declare function taskRoad(read: Parameters<typeof firstTaskJourney>[0], approved: boolean, built: boolean, facts: {
|
|
107
110
|
report?: boolean;
|