toolroll 0.9.74 → 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 +31 -0
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +27 -8
- package/dist/browser/workspace.js +27 -27
- package/dist/browser-workspace.d.ts +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 +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/desktop-update.d.ts +3 -0
- package/dist/desktop-update.js +43 -9
- package/dist/first-run.d.ts +8 -5
- package/dist/first-run.js +39 -6
- 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/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 +2 -0
- package/dist/project-learning.js +44 -6
- package/dist/project-tools.js +19 -1
- 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/repository-context.js +6 -1
- package/dist/result-follow-ups.js +20 -1
- package/dist/result-review.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.js +7 -4
- package/dist/server/route-table.js +14 -1
- package/dist/server/runtime.d.ts +16 -1
- package/dist/server/settings.js +82 -29
- package/dist/server/task-views.js +0 -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 +3 -0
- package/dist/store.js +33 -9
- package/dist/subagent-work.js +3 -0
- 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
|
@@ -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';
|
|
@@ -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/desktop-update.d.ts
CHANGED
|
@@ -92,6 +92,9 @@ 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
100
|
version?: never;
|
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/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;
|
package/dist/first-run.js
CHANGED
|
@@ -144,31 +144,64 @@ export function firstTaskJourney(read, approved, built = false) {
|
|
|
144
144
|
/**
|
|
145
145
|
* Any task's road (the read-only stepper on its page, after ReUI's Stepper): the first task's five steps, told only
|
|
146
146
|
* from recorded facts. You approve is done only when the plan was approved (a rewritten plan waiting on approval
|
|
147
|
-
* stands there again); a task that never needed one shows it skipped. Checks is done only when
|
|
148
|
-
* running,
|
|
149
|
-
*
|
|
147
|
+
* stands there again); a task that never needed one shows it skipped. Checks is done only when the approved check
|
|
148
|
+
* passed; a running, missing or Off check says so, and with no checks fact it is skipped. A failed check, or one that
|
|
149
|
+
* can't be read, stops the road at Checks: Ready isn't done (fail closed), even on a task a person marked complete.
|
|
150
|
+
* A research report has no Checks step: a report stopped there stops at
|
|
151
|
+
* Ready. The step that waits on a person carries `needsYou`.
|
|
150
152
|
*/
|
|
151
153
|
export function taskRoad(read, approved, built, facts) {
|
|
152
154
|
const replan = read.stage === "needs-you" && read.need === "replan";
|
|
153
155
|
const plainApproved = approved && !replan;
|
|
154
156
|
let steps = firstTaskJourney(replan ? { stage: "needs-you", need: "approval" } : read, plainApproved, built);
|
|
157
|
+
const complete = read.stage === "complete";
|
|
158
|
+
// A finished build that waits on something other than its check (a hold, a decision) stops after Checks, at Ready,
|
|
159
|
+
// when its check passed, didn't run or is Off: the road never calls a passing check stuck.
|
|
160
|
+
let waitsAtReady = false;
|
|
155
161
|
steps = steps.map(step => {
|
|
156
162
|
if (step.key === "approve" && step.state === "done" && !plainApproved)
|
|
157
163
|
return { ...step, state: "skipped" };
|
|
164
|
+
if (step.key === "checks" && step.state === "stuck" && built) {
|
|
165
|
+
switch (facts.checks ?? null) {
|
|
166
|
+
case "passed":
|
|
167
|
+
waitsAtReady = true;
|
|
168
|
+
return { ...step, state: "done" };
|
|
169
|
+
case "not-run":
|
|
170
|
+
waitsAtReady = true;
|
|
171
|
+
return { ...step, state: "skipped", label: "Checks not run" };
|
|
172
|
+
case "off":
|
|
173
|
+
waitsAtReady = true;
|
|
174
|
+
return { ...step, state: "skipped", label: "Checks at release" };
|
|
175
|
+
case "running": return { ...step, state: "current", label: "Checks running" };
|
|
176
|
+
case "failed": return { ...step, label: "Checks failed" };
|
|
177
|
+
case "unreadable": return { ...step, label: "Checks unreadable" };
|
|
178
|
+
default: return step;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
158
181
|
if (step.key !== "checks" || (step.state !== "done" && step.state !== "current"))
|
|
159
182
|
return step;
|
|
160
|
-
// At Checks now: only a recorded failure or a running check changes how it reads.
|
|
183
|
+
// At Checks now: only a recorded failure, an unreadable check or a running check changes how it reads.
|
|
161
184
|
if (step.state === "current")
|
|
162
|
-
return facts.checks === "failed" ? { ...step, state: "stuck", label: "Checks failed" }
|
|
185
|
+
return facts.checks === "failed" ? { ...step, state: "stuck", label: "Checks failed" }
|
|
186
|
+
: facts.checks === "unreadable" ? { ...step, state: "stuck", label: "Checks unreadable" }
|
|
187
|
+
: facts.checks === "running" ? { ...step, label: "Checks running" } : step;
|
|
163
188
|
switch (facts.checks ?? null) {
|
|
164
189
|
case "passed": return step;
|
|
165
190
|
case "running": return { ...step, state: "current", label: "Checks running" };
|
|
166
191
|
case "failed": return { ...step, state: "stuck", label: "Checks failed" };
|
|
167
192
|
case "not-run": return { ...step, state: "skipped", label: "Checks not run" };
|
|
168
|
-
case "
|
|
193
|
+
case "off": return { ...step, state: "skipped", label: "Checks at release" };
|
|
194
|
+
case "unreadable": return { ...step, state: "stuck", label: "Checks unreadable", ...(complete ? {} : { needsYou: true }) };
|
|
169
195
|
default: return { ...step, state: "skipped" };
|
|
170
196
|
}
|
|
171
197
|
});
|
|
198
|
+
if (waitsAtReady)
|
|
199
|
+
steps = steps.map(step => step.key === "ready" ? { ...step, state: "stuck" } : step);
|
|
200
|
+
// A road stopped at Checks (failed, or unreadable) has not reached Ready.
|
|
201
|
+
const at = steps.find(step => step.key === "checks");
|
|
202
|
+
if (facts.report !== true && at !== undefined && at.state === "stuck") {
|
|
203
|
+
steps = steps.map(step => step.key === "ready" && step.state === "done" ? { ...step, state: "todo" } : step);
|
|
204
|
+
}
|
|
172
205
|
if (facts.report === true) {
|
|
173
206
|
const checks = steps.find(step => step.key === "checks");
|
|
174
207
|
if (checks !== undefined && (checks.state === "current" || checks.state === "stuck")) {
|
package/dist/flow-card-task.js
CHANGED
|
@@ -32,8 +32,9 @@ export function flowCardTaskLine(store, taskId, repo, now, secrets = []) {
|
|
|
32
32
|
// The canvas reads no evidence files. Keep the machine's recorded check visible
|
|
33
33
|
// when the full receipt has no artifact root from which to read its check.
|
|
34
34
|
const check = assignment.receipt === null ? null : store.runCheckFor(assignment.receipt.runId);
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
// With neither, nothing about the check was read here: the line says nothing about it, never a pass and never an ask.
|
|
36
|
+
if (facts.checks?.status === "unavailable")
|
|
37
|
+
facts.checks = check === null ? null : { ...facts.checks, status: check.status, exitCode: check.exitCode };
|
|
37
38
|
const status = taskStatusOf(facts);
|
|
38
39
|
if (status.headline === "Needs you") {
|
|
39
40
|
const fallback = status.need?.action.label ?? "Open the task";
|
|
@@ -99,6 +99,8 @@ export declare function endUnattendedGrants(store: Store, filter: {
|
|
|
99
99
|
/** The live grant for a flow, or null. One past its expiry is closed here, durably (like activeMode). A grant whose
|
|
100
100
|
* signer is no longer an active approver, or whose flow moved on, covers nothing. */
|
|
101
101
|
export declare function activeUnattendedGrant(store: Store, flowId: number, now: Date): UnattendedGrant | null;
|
|
102
|
+
/** Close every grant past its expiry (the console's background pass). Returns how many ended. */
|
|
103
|
+
export declare function closeExpiredUnattendedGrants(store: Store, now: Date): number;
|
|
102
104
|
/** sha256 over a trigger's identity and canonical settings: what a grant freezes for each schedule it covers. */
|
|
103
105
|
export declare function triggerDigestOf(trigger: Pick<FlowTriggerRow, "id" | "flow" | "kind" | "configJson">): string | null;
|
|
104
106
|
/** Asynchronous preparation, never called from filing transactions or page rendering. */
|
package/dist/flow-unattended.js
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* This module owns the terms, digest, words and transactions; the flow engine
|
|
25
25
|
* asks for coverage inside its filing transaction, and plan-auto.ts seals.
|
|
26
26
|
*/
|
|
27
|
+
import { inReadOnlyRequest } from "./read-only-request.js";
|
|
27
28
|
import { spawn } from "node:child_process";
|
|
28
29
|
import { createHash } from "node:crypto";
|
|
29
30
|
import { lstat, readFile, realpath, stat } from "node:fs/promises";
|
|
@@ -231,11 +232,21 @@ export function activeUnattendedGrant(store, flowId, now) {
|
|
|
231
232
|
if (row === undefined)
|
|
232
233
|
return null;
|
|
233
234
|
if (Date.parse(String(row["absolute_expiry"])) <= now.getTime()) {
|
|
234
|
-
|
|
235
|
+
// A page visit only reads (docs/plans/get-side-effects.md): the background pass or the next path that acts closes it.
|
|
236
|
+
if (!inReadOnlyRequest())
|
|
237
|
+
store.transact(() => closeGrant(store, row, "the-clock", "expired", now));
|
|
235
238
|
return null;
|
|
236
239
|
}
|
|
237
240
|
return readGrant(row);
|
|
238
241
|
}
|
|
242
|
+
/** Close every grant past its expiry (the console's background pass). Returns how many ended. */
|
|
243
|
+
export function closeExpiredUnattendedGrants(store, now) {
|
|
244
|
+
const rows = store.handle.prepare("SELECT * FROM flow_unattended_grant WHERE revoked_at IS NULL").all()
|
|
245
|
+
.filter(row => Date.parse(String(row["absolute_expiry"])) <= now.getTime());
|
|
246
|
+
for (const row of rows)
|
|
247
|
+
store.transact(() => closeGrant(store, row, "the-clock", "expired", now));
|
|
248
|
+
return rows.length;
|
|
249
|
+
}
|
|
239
250
|
/** Why the grant can't cover this flow right now (the same checks signing makes), or null. */
|
|
240
251
|
function coverageProblem(store, flow, owner, workRepos) {
|
|
241
252
|
if (flow.state !== "active")
|
package/dist/integrations.d.ts
CHANGED
|
@@ -114,14 +114,15 @@ export declare function secretsNote(list: readonly Integration[], secrets: Secre
|
|
|
114
114
|
* files only, or why they can't be used (secretsNote). */
|
|
115
115
|
export declare function renderIntegrations(list: readonly Integration[], secrets?: SecretsKept | null): string[];
|
|
116
116
|
export type IntegrationMonitor = {
|
|
117
|
-
/** The list now,
|
|
117
|
+
/** The list now, from the last answers saved. It only reads: a page calls it on every render (docs/plans/get-side-effects.md). */
|
|
118
118
|
list(): Integration[];
|
|
119
|
-
/** Check now and wait (Send test, or the first answer in a test). */
|
|
119
|
+
/** Check now and wait (Send test, Check now, or the first answer in a test). */
|
|
120
120
|
check(keys?: readonly string[]): Promise<Integration[]>;
|
|
121
|
+
/** The background pass: start a check when the last one is older than the cache. Never called from a page. */
|
|
122
|
+
refreshIfDue(): void;
|
|
121
123
|
/** Whether a background check is running. */
|
|
122
124
|
readonly checking: boolean;
|
|
123
125
|
};
|
|
124
|
-
/** Checks in the background with a short cache, as the first-run suggestions are: a render never waits on a check. */
|
|
125
126
|
export declare function createIntegrationMonitor(io: () => IntegrationIo, options?: {
|
|
126
127
|
cacheMs?: number;
|
|
127
128
|
now?: () => number;
|