toolroll 0.9.73 → 0.9.75
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 +63 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-turns.js +5 -0
- package/dist/agents-live.d.ts +31 -0
- package/dist/agents-live.js +43 -0
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +27 -8
- package/dist/browser/THIRD_PARTY_NOTICES.txt +40 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -30
- package/dist/browser-workspace.d.ts +7 -1
- package/dist/builder.js +1 -1
- package/dist/chat-acceptance.d.ts +4 -1
- package/dist/chat-acceptance.js +13 -3
- package/dist/chat-continuity.js +10 -0
- package/dist/chat-quiet.js +3 -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 +16 -4
- package/dist/contracts/console-api.d.ts +5 -1
- package/dist/contracts/console-api.js +10 -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.js +5 -2
- package/dist/desktop-update.d.ts +4 -1
- package/dist/desktop-update.js +43 -9
- package/dist/dispose.js +35 -2
- package/dist/exec.js +13 -16
- package/dist/first-run.d.ts +20 -1
- package/dist/first-run.js +71 -0
- 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/integrations.d.ts +4 -3
- package/dist/integrations.js +5 -6
- package/dist/invoke.d.ts +13 -0
- package/dist/invoke.js +21 -12
- package/dist/knowledge-ui.d.ts +4 -1
- package/dist/knowledge-ui.js +10 -5
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-follow.js +3 -1
- package/dist/lead-progress.d.ts +4 -1
- package/dist/lead-progress.js +3 -1
- package/dist/lead.js +4 -0
- package/dist/live-bus.d.ts +2 -0
- package/dist/live-bus.js +1 -0
- package/dist/live-children-read.d.ts +52 -0
- package/dist/live-children-read.js +333 -0
- package/dist/live-children.d.ts +73 -0
- package/dist/live-children.js +318 -0
- package/dist/live-detail.d.ts +4 -3
- package/dist/live-detail.js +9 -5
- package/dist/live-input.js +7 -8
- package/dist/live-main-read.d.ts +19 -0
- package/dist/live-main-read.js +91 -0
- package/dist/live-open.d.ts +28 -0
- package/dist/live-open.js +125 -0
- package/dist/live-records.d.ts +10 -1
- package/dist/live-records.js +16 -5
- package/dist/live.d.ts +54 -22
- package/dist/live.js +218 -69
- package/dist/openrouter-models.d.ts +5 -2
- package/dist/openrouter-models.js +7 -4
- package/dist/operate.js +5 -2
- package/dist/peek-cli.d.ts +10 -3
- package/dist/peek-cli.js +17 -8
- package/dist/planner.js +1 -1
- package/dist/project-learning.js +44 -6
- package/dist/project-tools.js +19 -1
- package/dist/provider-limits.d.ts +3 -1
- package/dist/provider-limits.js +5 -2
- package/dist/provider.js +6 -12
- 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.js +4 -0
- package/dist/release.d.ts +3 -0
- package/dist/release.js +3 -0
- package/dist/repository-context-ui.js +2 -1
- package/dist/repository-context.js +6 -1
- package/dist/result-follow-ups.js +20 -1
- package/dist/result-review.js +5 -1
- package/dist/run-efficiency.d.ts +3 -3
- package/dist/run-efficiency.js +20 -8
- package/dist/scout.d.ts +3 -0
- package/dist/scout.js +5 -3
- package/dist/serve.js +196 -29
- package/dist/server/agents-live-filed.d.ts +17 -0
- package/dist/server/agents-live-filed.js +97 -0
- package/dist/server/agents-live-lead.d.ts +47 -0
- package/dist/server/agents-live-lead.js +116 -0
- package/dist/server/agents-live-room.d.ts +23 -0
- package/dist/server/agents-live-room.js +91 -0
- package/dist/server/chat.js +115 -53
- package/dist/server/chrome.js +2 -2
- package/dist/server/console.css +68 -40
- 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/live.d.ts +4 -0
- package/dist/server/live.js +42 -2
- package/dist/server/pages.js +98 -4
- package/dist/server/people-tokens.js +7 -4
- package/dist/server/render-chat.d.ts +15 -1
- package/dist/server/render-chat.js +14 -7
- package/dist/server/render-pages.d.ts +10 -5
- package/dist/server/render-pages.js +83 -74
- 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.js +13 -3
- package/dist/server/route-table.js +19 -2
- package/dist/server/runtime.d.ts +17 -2
- package/dist/server/settings.js +85 -32
- package/dist/server/task-views.js +1 -2
- package/dist/server/tasks.js +70 -43
- 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 +34 -1
- package/dist/store.js +108 -11
- package/dist/stream-scope.d.ts +18 -0
- package/dist/stream-scope.js +28 -0
- package/dist/subagent-work.js +3 -0
- package/dist/subscription-chat.js +5 -3
- package/dist/task-status.d.ts +28 -1
- package/dist/task-status.js +93 -11
- package/dist/teardown-git.d.ts +27 -1
- package/dist/teardown-git.js +185 -39
- package/dist/telegram-progress.js +4 -4
- package/dist/telegram-status.js +8 -3
- 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/package.json +1 -1
|
@@ -211,6 +211,10 @@ export type BrowserTasksView = {
|
|
|
211
211
|
newTask: BrowserLink;
|
|
212
212
|
/** v105: subscription windows and monthly budgets, one tile each (an instance operator's; null when there are none). */
|
|
213
213
|
limits: BrowserLimits | null;
|
|
214
|
+
/** The stored-state filter (Any state, Queued, Running, Done, Failed, Cancelled), each a real link starting on page one. */
|
|
215
|
+
states?: (BrowserLink & {
|
|
216
|
+
active: boolean;
|
|
217
|
+
})[];
|
|
214
218
|
};
|
|
215
219
|
/** One limit: a plan's usage window ("Claude · 5-hour, 48%") or a monthly budget ("shop · Budget, $4.20 of $10"). */
|
|
216
220
|
export type BrowserTaskGroup = WorkIndexGroup;
|
|
@@ -272,7 +276,7 @@ export type BrowserSettingsView = {
|
|
|
272
276
|
connection: {
|
|
273
277
|
words: string;
|
|
274
278
|
facts: string;
|
|
275
|
-
|
|
279
|
+
checkAction: string;
|
|
276
280
|
} | null;
|
|
277
281
|
usage: string;
|
|
278
282
|
envName: string;
|
|
@@ -1445,6 +1449,8 @@ export type BrowserAgentsView = {
|
|
|
1445
1449
|
selected: BrowserAgentDetail | null;
|
|
1446
1450
|
/** Shown when nothing is on the board. */
|
|
1447
1451
|
empty: string | null;
|
|
1452
|
+
/** The Live grid (?view=live): every running agent's live output, watching only. */
|
|
1453
|
+
live?: boolean;
|
|
1448
1454
|
};
|
|
1449
1455
|
export type BrowserView = BrowserTasksView | BrowserSettingsView | BrowserTaskView | BrowserProjectsView | BrowserResultView | BrowserFlowView | BrowserAgentsView;
|
|
1450
1456
|
export type BrowserWorkspace = {
|
package/dist/builder.js
CHANGED
|
@@ -1240,7 +1240,7 @@ export async function build(store, request) {
|
|
|
1240
1240
|
// Every streaming transport emits events now (peek); a file that cannot
|
|
1241
1241
|
// open is a null, and a null never costs a build.
|
|
1242
1242
|
// What the agent did last rides the same stream into one row (live tasks).
|
|
1243
|
-
const liveLog = withAgentTurns(withRunActivity(openLiveLog(root, request.runId, liveLogContext(store, taskRef, worktree)), store, request.runId, clock), (state, at) => { if (attemptKey !== null)
|
|
1243
|
+
const liveLog = withAgentTurns(withRunActivity(openLiveLog(root, request.runId, liveLogContext(store, request.runId, taskRef, worktree, clock())), store, request.runId, clock), (state, at) => { if (attemptKey !== null)
|
|
1244
1244
|
recordAgentTurn(store, { run: request.runId, attemptKey, state, at }); }, clock, (open, at) => { if (attemptKey !== null)
|
|
1245
1245
|
noteOpenTool(store, request.runId, attemptKey, open, at); });
|
|
1246
1246
|
// The retry base (steering fix for run 1465's proof): the SAME pinned
|
|
@@ -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/chat-continuity.js
CHANGED
|
@@ -125,6 +125,8 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
125
125
|
if(got===null)return;
|
|
126
126
|
var data=got.data||{};
|
|
127
127
|
if(data.ok===true){
|
|
128
|
+
// A conversation shown before it existed (session 0) is started by its first send: that is this one.
|
|
129
|
+
if(session==='0'&&typeof data.session==='number'&&data.session>0)adopt(String(data.session));
|
|
128
130
|
if(String(data.session)!==session){changed();return;}
|
|
129
131
|
say(data.queued===true?'Queued. It goes to the lead after the reply in progress.':'Sent. Waiting for the reply…');later(300);return;
|
|
130
132
|
}
|
|
@@ -273,6 +275,9 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
273
275
|
}
|
|
274
276
|
return true;
|
|
275
277
|
}
|
|
278
|
+
// The conversation this page's first send started: its binding and its draft move to it (a reload restores from there).
|
|
279
|
+
function adopt(started){session=started;document.querySelectorAll('form[action="/chat"] [name="request-session"]').forEach(function(bound){bound.value=started;});
|
|
280
|
+
try{sessionStorage.removeItem(key);}catch(e){}key='standing-orders:chat-draft:'+session+':'+task;save();}
|
|
276
281
|
function later(ms){if(stale)return;clearTimeout(timer);timer=setTimeout(check,ms);}
|
|
277
282
|
async function check(){
|
|
278
283
|
if(polling||stale)return;
|
|
@@ -289,6 +294,11 @@ export const CHAT_CONTINUITY_SCRIPT = String.raw `
|
|
|
289
294
|
if(!r.ok)throw new Error('connection');
|
|
290
295
|
var data=await r.json();
|
|
291
296
|
if(!valid(data))throw new Error('response');
|
|
297
|
+
// Not started yet (session 0): only this send's own receipt in the started conversation proves it is the one.
|
|
298
|
+
if(session==='0'&&typeof data.session==='number'&&data.session>0&&asked&&data.received===true)adopt(String(data.session));
|
|
299
|
+
// A poll that lands while the first send is on its way decides nothing: that send's answer (or the poll it
|
|
300
|
+
// asks for afterwards, with its request) says whether the started conversation is this page's.
|
|
301
|
+
if(session==='0'&&sending&&data.session!==null&&String(data.session)!=='0'){later(1000);return;}
|
|
292
302
|
if(data.session===null||String(data.session)!==session){changed();return;}
|
|
293
303
|
// A late answer for another lens never lands here; the poll goes on.
|
|
294
304
|
if(data.task!==task){later(5000);return;}
|
package/dist/chat-quiet.js
CHANGED
|
@@ -101,7 +101,7 @@ export function quietCardView(store, taskRefs, now, root, viewer) {
|
|
|
101
101
|
const heading = `${lines.length} tasks`;
|
|
102
102
|
const projects = [...new Set(lines.map(one => projectLabel(one.project)))];
|
|
103
103
|
const text = [heading, ...lines.map(one => `${one.icon} ${phoneText(one.title, 60)} · ${one.status}`), "", projects.join(", ")].join("\n");
|
|
104
|
-
return { text, entities: [{ type: "bold", offset: 0, length: heading.length }], link: { label: "Open tasks", path: "/
|
|
104
|
+
return { text, entities: [{ type: "bold", offset: 0, length: heading.length }], link: { label: "Open tasks", path: "/work" } };
|
|
105
105
|
}
|
|
106
106
|
/** Facts that carry their own controls or must stand alone (a security alert, a decision, a flow card, a
|
|
107
107
|
* screenshot, an acceptance packet) are never folded into a batch. */
|
|
@@ -176,7 +176,7 @@ export function finishedView(store, batch, now, root, viewer) {
|
|
|
176
176
|
return null;
|
|
177
177
|
const text = batchLine(lines.map(one => one.fact));
|
|
178
178
|
if (lines.length > 1)
|
|
179
|
-
return { text, entities: [], link: { label: "Open", path: "/
|
|
179
|
+
return { text, entities: [], link: { label: "Open", path: "/work" } };
|
|
180
180
|
const target = lines[0].target ?? null;
|
|
181
181
|
return { text, entities: [], link: lines[0].link, ...(lines[0].also.length === 0 ? {} : { also: lines[0].also }), ...(target === null ? {} : { target }) };
|
|
182
182
|
}
|
|
@@ -242,7 +242,7 @@ export function enqueueEveningDigests(store, now, root) {
|
|
|
242
242
|
if (body === null)
|
|
243
243
|
return;
|
|
244
244
|
if (store.enqueueNotification({ dedupeKey: `digest:evening:${person.account}:${day}`, kind: "evening-digest", subject: "Evening digest", body,
|
|
245
|
-
link: "/
|
|
245
|
+
link: "/work", source: { installation: true }, recipient: person.account }, now))
|
|
246
246
|
queued++;
|
|
247
247
|
});
|
|
248
248
|
}
|
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';
|
|
@@ -729,10 +730,21 @@ export class CodingWorkspace {
|
|
|
729
730
|
}
|
|
730
731
|
async changes(id, actor) {
|
|
731
732
|
const session = this.get(id, actor);
|
|
733
|
+
// The code page reads this on a GET (docs/plans/get-side-effects.md), from a checkout the agent writes: every git
|
|
734
|
+
// here is the hardened checkout git, which refuses a config that could run a command and reads no global config.
|
|
735
|
+
// Read mode: the read flags, and the checkout's config and info/attributes as read once (teardown-git.ts readView),
|
|
736
|
+
// hold even if the agent changes them after checkoutGit read them: no hook, pager, filter or network.
|
|
737
|
+
const safe = checkoutGit(run, 'read');
|
|
738
|
+
const read = async (args) => {
|
|
739
|
+
const result = await safe('git', args, { cwd: session.worktree, timeoutMs: 60_000, maxBuffer: 1_048_576 });
|
|
740
|
+
if (result.code !== 0)
|
|
741
|
+
throw Error('Git could not complete this workspace operation. Check the repository and try again.');
|
|
742
|
+
return result.stdout;
|
|
743
|
+
};
|
|
732
744
|
const [head, status, tracked, untracked] = await Promise.all([
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
745
|
+
read(['rev-parse', 'HEAD']), read(['status', '--short']),
|
|
746
|
+
safe('git', ['diff', '--no-ext-diff', '--no-textconv', session.base, '--'], { cwd: session.worktree, timeoutMs: 60_000, maxBuffer: 1_048_576 }),
|
|
747
|
+
read(['ls-files', '--others', '--exclude-standard', '-z']),
|
|
736
748
|
]);
|
|
737
749
|
let diff = tracked.stdout, truncated = tracked.code !== 0;
|
|
738
750
|
// git's no-index diff describes symlinks themselves; it does not follow a
|
|
@@ -743,7 +755,7 @@ export class CodingWorkspace {
|
|
|
743
755
|
truncated = true;
|
|
744
756
|
break;
|
|
745
757
|
}
|
|
746
|
-
const part = await
|
|
758
|
+
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
759
|
diff += part.stdout;
|
|
748
760
|
if (part.code !== 0 && part.code !== 1)
|
|
749
761
|
truncated = true;
|
|
@@ -123,6 +123,9 @@ export declare const CONSOLE_FORMS: {
|
|
|
123
123
|
readonly notifications: FormContract<"digest" | "mode" | "screenshots">;
|
|
124
124
|
readonly notificationsMute: FormContract<"pings" | "repo">;
|
|
125
125
|
readonly telegramDigest: FormContract<"every">;
|
|
126
|
+
readonly pullRequestsCheck: FormContract<"repo">;
|
|
127
|
+
readonly connectionCheck: FormContract<"auth-mode" | "provider" | "return" | "value">;
|
|
128
|
+
readonly modelsRefresh: FormContract<"command" | "fingerprint" | "model" | "nonce" | "provider" | "repo" | "seconds" | "task" | "token">;
|
|
126
129
|
readonly providerKey: FormContract<"auth-mode" | "provider" | "value">;
|
|
127
130
|
readonly telegramToken: FormContract<"token">;
|
|
128
131
|
readonly email: FormContract<"from" | "host" | "imapHost" | "imapPort" | "password" | "port" | "secure" | "user">;
|
|
@@ -149,6 +152,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
149
152
|
readonly leadIdentity: FormContract<"name" | "persona">;
|
|
150
153
|
readonly leadAbout: FormContract<"about">;
|
|
151
154
|
readonly leadPromiseCancel: FormContract<"promise">;
|
|
155
|
+
readonly leadCheck: FormContract<"return">;
|
|
152
156
|
readonly chatConfig: FormContract<"daily-turns" | "forget-key" | "key" | "model" | "off" | "provider" | "return" | "token" | "weekly-usd">;
|
|
153
157
|
readonly leadMint: FormContract<"ceiling-usd" | "follow" | "return" | "token">;
|
|
154
158
|
readonly leadFollow: FormContract<"enabled" | "return">;
|
|
@@ -190,7 +194,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
190
194
|
};
|
|
191
195
|
export type ConsoleFormName = keyof typeof CONSOLE_FORMS;
|
|
192
196
|
/** 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"];
|
|
197
|
+
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
198
|
/** The field names a console form's handler may read. */
|
|
195
199
|
export type FormFieldOf<N extends ConsoleFormName> = (typeof CONSOLE_FORMS)[N] extends FormContract<infer K> ? K : never;
|
|
196
200
|
/** `GET /ledger?format=json`: the chain's state, a page of entries (newest first) and the cursor for the next. */
|
|
@@ -132,6 +132,10 @@ export const CONSOLE_FORMS = {
|
|
|
132
132
|
notifications: formContract("POST /settings/notifications", ["mode", "digest", "screenshots"]),
|
|
133
133
|
notificationsMute: formContract("POST /settings/notifications/mute", ["repo", "pings"]),
|
|
134
134
|
telegramDigest: formContract("POST /settings/telegram-digest", ["every"]),
|
|
135
|
+
// Check again sits in the provider-key form and the setup card; Refresh models in the setup form.
|
|
136
|
+
pullRequestsCheck: formContract("POST /settings/pull-requests/check", ["repo"]),
|
|
137
|
+
connectionCheck: formContract("POST /settings/connection/check", ["provider", "auth-mode", "value", "return"]),
|
|
138
|
+
modelsRefresh: formContract("POST /control/models-refresh", ["repo", "nonce", "fingerprint", "token", "provider", "model", "command", "seconds", "task"]),
|
|
135
139
|
providerKey: formContract("POST /settings/provider-key, /settings/provider-key-clear", ["provider", "auth-mode", "value"]),
|
|
136
140
|
telegramToken: formContract("POST /settings/telegram-token", ["token"]),
|
|
137
141
|
email: formContract("POST /settings/email, /settings/email-test, /settings/email-read-test", ["host", "port", "secure", "user", "from", "password", "imapHost", "imapPort"]),
|
|
@@ -158,6 +162,7 @@ export const CONSOLE_FORMS = {
|
|
|
158
162
|
leadIdentity: formContract("POST /settings/lead/identity", ["name", "persona"]),
|
|
159
163
|
leadAbout: formContract("POST /settings/lead/about", ["about"]),
|
|
160
164
|
leadPromiseCancel: formContract("POST /settings/lead/promise/cancel", ["promise"]),
|
|
165
|
+
leadCheck: formContract("POST /lead/check", ["return"]),
|
|
161
166
|
chatConfig: formContract("POST /chat/config", ["return", "token", "off", "forget-key", "provider", "model", "weekly-usd", "daily-turns", "key"]),
|
|
162
167
|
leadMint: formContract("POST /chat/mate/mint", ["return", "ceiling-usd", "token", "follow"]),
|
|
163
168
|
leadFollow: formContract("POST /chat/mate/follow", ["enabled", "return"]),
|
|
@@ -216,6 +221,11 @@ export const BODILESS_POSTS = [
|
|
|
216
221
|
"POST /t/<task>/stop-carrying",
|
|
217
222
|
"POST /t/<task>/plan",
|
|
218
223
|
"POST /t/<task>/retry-review",
|
|
224
|
+
// Check now and its kin (docs/plans/get-side-effects.md): the form token only, no fields read.
|
|
225
|
+
"POST /settings/updates/check",
|
|
226
|
+
"POST /settings/storage/check",
|
|
227
|
+
"POST /projects/github",
|
|
228
|
+
"POST /code/<session>/ship/preview",
|
|
219
229
|
];
|
|
220
230
|
// ---- the JSON API ------------------------------------------------------------------------------------------------
|
|
221
231
|
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.js
CHANGED
|
@@ -18,8 +18,11 @@ export function deploymentGate(store, input) {
|
|
|
18
18
|
if (run === null || ref === null || head === null)
|
|
19
19
|
return refuse(`Run ${input.runId} is not a built candidate.`);
|
|
20
20
|
// `task regate` reruns the approved check on the last built commit and records no-change: a candidate only for that commit.
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// A prepared candidate's build records a commit of its own holding exactly the candidate's tree; its rerun under the same
|
|
22
|
+
// signed scope is the same check of that commit (the digest is held to the approved one below).
|
|
23
|
+
const built = store.runsFor(run.taskRef).find(one => one.id < input.runId && one.role === "builder" && one.outcome === "built");
|
|
24
|
+
const regated = run.outcome === "no-change" && built?.headRevision === head &&
|
|
25
|
+
(scope?.candidate === head || ((scope?.candidate ?? null) !== null && built.scopeDigest === run.scopeDigest));
|
|
23
26
|
if (run.role !== "builder" || !(run.outcome === "built" || regated))
|
|
24
27
|
return refuse(`Run ${input.runId} is not a built candidate at ${head.slice(0, 7)}.`);
|
|
25
28
|
if (scope === null || scope.approvedDigest !== scope.digest || scope.digest !== run.scopeDigest)
|
package/dist/desktop-update.d.ts
CHANGED
|
@@ -92,8 +92,12 @@ 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): {
|
|
100
|
+
version?: never;
|
|
97
101
|
active: boolean;
|
|
98
102
|
phase: string;
|
|
99
103
|
detail: string;
|
|
@@ -107,7 +111,6 @@ export declare function desktopUpdateStatus(stateDir: string): {
|
|
|
107
111
|
wasRunning?: never;
|
|
108
112
|
backupPath?: never;
|
|
109
113
|
updateId?: never;
|
|
110
|
-
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/dispose.js
CHANGED
|
@@ -23,7 +23,7 @@ import { nightlyTaskRef, releaseTaskRef } from "./store.js";
|
|
|
23
23
|
import {} from "./proof.js";
|
|
24
24
|
import { verificationEvidence, failedVerificationEvidence } from "./verification-evidence.js";
|
|
25
25
|
import { classifyGateFailure, describeGateFailure, gateFailureSummary } from "./gate-failure.js";
|
|
26
|
-
import { propose, approve } from "./scope.js";
|
|
26
|
+
import { propose, approve, liveReleaseOf } from "./scope.js";
|
|
27
27
|
import { headWithin } from "./names.js";
|
|
28
28
|
/** The reasons tick classifies as the attempt itself breaking. */
|
|
29
29
|
const TICK_FAILURE_REASONS = new Set([
|
|
@@ -456,6 +456,35 @@ export function regateTask(store, taskId, now, approval) {
|
|
|
456
456
|
return { ok: false, reason: "no-attempt", message: `${taskId} has no scope` };
|
|
457
457
|
const head = last.headRevision;
|
|
458
458
|
const actor = approval.kind === "operator" ? approval.name : `mode ${approval.name}`;
|
|
459
|
+
// A prepared candidate (a release gate, the nightly check) already names the approved commit, and the builder loads
|
|
460
|
+
// exactly its tree on every attempt. The rerun keeps that scope and its approval as they are: nothing is proposed or
|
|
461
|
+
// sealed again, so a regate can never swap the approved commit, its release terms or the person's yes for others
|
|
462
|
+
// (2026-10-27: a regate rewrote release-0.9.73's candidate to its own build commit and the release refused it).
|
|
463
|
+
if ((scope.candidate ?? null) !== null || (scope.releaseTerms ?? null) !== null) {
|
|
464
|
+
const candidate = scope.candidate ?? null;
|
|
465
|
+
if (candidate === null || scope.approvedDigest !== scope.digest || last.scopeDigest !== scope.digest) {
|
|
466
|
+
return { ok: false, reason: "not-approved", message: `${taskId}'s approved commit or scope changed since attempt #${last.id}, or its approval no longer stands; approve its scope again instead` };
|
|
467
|
+
}
|
|
468
|
+
// A release's task runs again only while its one approval still stands for it (the dispatch would refuse it anyway).
|
|
469
|
+
const release = liveReleaseOf(store, taskId, "dispatch", now);
|
|
470
|
+
if (release.applies && !release.ok)
|
|
471
|
+
return { ok: false, reason: "not-approved", message: `${taskId} can't run again: ${release.why}` };
|
|
472
|
+
// Any other seal stands only while its authority does (an expired mode, a revoked approver): the queue would never
|
|
473
|
+
// dispatch it, so the regate says so instead of queueing a rerun that can't start.
|
|
474
|
+
if (!store.modeApprovalLive(ref.id, now))
|
|
475
|
+
return { ok: false, reason: "not-approved", message: `${taskId}'s approval no longer stands; approve its scope again instead` };
|
|
476
|
+
return store.transact(() => {
|
|
477
|
+
if (store.getTask(taskId)?.state !== "queued") {
|
|
478
|
+
const queued = store.requeueTask(taskId, actor, now);
|
|
479
|
+
if (!queued.ok) {
|
|
480
|
+
const why = { claimed: "claimed", "accepted-result": "accepted-result", published: "published" };
|
|
481
|
+
return { ok: false, reason: why[queued.reason] ?? "not-rejected", message: `${taskId} could not be queued again: ${queued.reason}` };
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
store.addRunNote(last.id, "Toolroll", `The check will run again on the approved commit ${candidate.slice(0, 7)}, under the same approval, as a new attempt, ${approval.kind === "operator" ? `on ${approval.name}'s say-so` : "automatically once, because the failure was outside this change"}.`, now);
|
|
485
|
+
return { ok: true, run: last.id, head: candidate };
|
|
486
|
+
});
|
|
487
|
+
}
|
|
459
488
|
return store.transact(() => {
|
|
460
489
|
const proposed = propose(store, {
|
|
461
490
|
taskId, goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd,
|
|
@@ -520,7 +549,11 @@ export function maybeTriggerRepair(store, repo, evidenceRoot, sourceRunId, verdi
|
|
|
520
549
|
// to a person. No repair draft, no third pass, on either road.
|
|
521
550
|
{
|
|
522
551
|
const rerunTask = store.refById(run.taskRef)?.externalId ?? "";
|
|
523
|
-
|
|
552
|
+
// A prepared candidate's attempts each record their own commit of the candidate's tree, so a rerun under the same
|
|
553
|
+
// approved scope is the same commit's check even when its head isn't the candidate itself.
|
|
554
|
+
const rerunScope = store.getScope(rerunTask);
|
|
555
|
+
const sameCommit = rerunScope !== null && (rerunScope.candidate === run.headRevision || ((rerunScope.candidate ?? null) !== null && rerunScope.digest === run.scopeDigest));
|
|
556
|
+
if (run.headRevision && sameCommit && store.builderAttemptsAt(run.taskRef, run.headRevision) >= 2) {
|
|
524
557
|
const contradicted = cause === "review";
|
|
525
558
|
store.enqueueNotification({ source: { run: sourceRunId }, dedupeKey: `regate-failed:${sourceRunId}`, kind: "repair-evidence", pushClass: "attention",
|
|
526
559
|
subject: contradicted ? "The rerun on the same commit was contradicted by its review" : "The project check failed again on the same commit",
|