toolroll 0.9.63 → 0.9.67
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 +108 -0
- package/README.md +33 -5
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +117 -2
- package/dist/agent-fence.js +439 -12
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +39 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/batch-checks.js +3 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +228 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +235 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +10 -1
- package/dist/builder.js +87 -24
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +197 -15
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +19 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +21 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/console-api.d.ts +13 -4
- package/dist/contracts/console-api.js +13 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +4 -1
- package/dist/doctor.js +1 -1
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/exec.d.ts +78 -0
- package/dist/exec.js +367 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-engine.js +24 -5
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +77 -8
- package/dist/flow-triggers.js +517 -70
- package/dist/flows-cli.js +8 -2
- package/dist/flows-ui.d.ts +0 -2
- package/dist/flows-ui.js +52 -4
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/invoke.d.ts +35 -0
- package/dist/invoke.js +347 -28
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +152 -1
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +269 -0
- package/dist/live-terminal.js +686 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/operate.d.ts +4 -0
- package/dist/operate.js +335 -19
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/project-delete-ui.d.ts +18 -0
- package/dist/project-delete-ui.js +60 -9
- package/dist/project-delete.js +2 -2
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +5 -3
- package/dist/project-tools.js +12 -9
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider.d.ts +25 -2
- package/dist/provider.js +63 -9
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release-adapters.d.ts +6 -0
- package/dist/release-adapters.js +70 -3
- package/dist/release.d.ts +10 -1
- package/dist/release.js +34 -15
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-follow-ups.js +3 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +183 -5
- package/dist/scope.js +256 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +248 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +16 -2
- package/dist/server/chrome.js +63 -11
- package/dist/server/console.css +107 -42
- package/dist/server/flows.js +38 -8
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +32 -4
- package/dist/server/render-pages.js +83 -13
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +111 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +47 -5
- package/dist/server/runtime.d.ts +33 -2
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +142 -10
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +327 -78
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +440 -15
- package/dist/store.js +1072 -52
- package/dist/subagent-desk.js +1 -1
- package/dist/summary.js +3 -0
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/teardown-git.d.ts +22 -0
- package/dist/teardown-git.js +136 -2
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +14 -0
- package/dist/tool-broker.js +67 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/unfenced-test-support.d.ts +2 -0
- package/dist/unfenced-test-support.js +11 -0
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +16 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/writes-mode.d.ts +9 -0
- package/dist/writes-mode.js +8 -0
- package/package.json +15 -2
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BARE `toolroll` IN A REPOSITORY (docs/plans/agents-cockpit.md B1): the two-action start.
|
|
3
|
+
*
|
|
4
|
+
* Inside a git repository, `toolroll` with nothing else (or only --json) either starts the console for it (`up`), or —
|
|
5
|
+
* when this installation's console is already running and PROVES it is (console-instance.ts: a fresh challenge on the
|
|
6
|
+
* discovery socket and on its HTTP listener, answered with the secret in the database this CLI opened itself) —
|
|
7
|
+
* adds the repository through the ordinary project admission and opens that proved console. Under an agent, or with
|
|
8
|
+
* --json, it starts nothing and changes nothing: it says what it would do. Outside a repository, it scans as before
|
|
9
|
+
* (the scan is also `toolroll scan`).
|
|
10
|
+
*/
|
|
11
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
12
|
+
import { proveLiveConsole } from "./console-instance.js";
|
|
13
|
+
import { underAgent } from "./prompt.js";
|
|
14
|
+
import { envelopeJson } from "./envelope.js";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { openStoreReadOnly, readSchemaVersion, SCHEMA_VERSION } from "./store.js";
|
|
17
|
+
const EXIT = { ok: 0, refused: 3 };
|
|
18
|
+
/** Whether argv is a bare `toolroll` (nothing, or only --json). */
|
|
19
|
+
export const isBareInvocation = (argv) => argv.every(one => one === "--json");
|
|
20
|
+
/** The database's schema version by the store's own strict reader, read without changing anything. */
|
|
21
|
+
function schemaOf(file) {
|
|
22
|
+
try {
|
|
23
|
+
// Loaded only here, as the store loads it: the CLI's other commands never pay for (or warn about) it.
|
|
24
|
+
const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
|
|
25
|
+
const db = new DatabaseSync(file, { readOnly: true });
|
|
26
|
+
try {
|
|
27
|
+
const read = readSchemaVersion(db);
|
|
28
|
+
return read.ok ? read : { ok: false };
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
db.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return { ok: false };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Whether the database file is there: "absent" only when the system says so (ENOENT), never for a path it won't show. */
|
|
39
|
+
function databasePresence(file) {
|
|
40
|
+
try {
|
|
41
|
+
lstatSync(file);
|
|
42
|
+
return "present";
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return error.code === "ENOENT" ? "absent" : "unknown";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async function prove(seams) {
|
|
49
|
+
// Read only, never created or migrated, never cleaned up: proving changes nothing. "None" only when nothing of this
|
|
50
|
+
// build can be running on it (no database yet, or one older than this build, which `up` brings up to date); a
|
|
51
|
+
// database that can't be read, is mid-upgrade or is newer than this build can't be checked, so it is refused.
|
|
52
|
+
const none = { ok: false, state: "none", message: "no console is running" };
|
|
53
|
+
const unverifiable = (why) => ({ ok: false, state: "refused", message: `${why}, so a running console can't be checked — run \`toolroll up\` to see what's wrong` });
|
|
54
|
+
const presence = databasePresence(seams.databaseFile);
|
|
55
|
+
if (presence === "absent")
|
|
56
|
+
return none;
|
|
57
|
+
if (presence === "unknown")
|
|
58
|
+
return unverifiable("Toolroll's database can't be reached");
|
|
59
|
+
const schema = schemaOf(seams.databaseFile);
|
|
60
|
+
if (!schema.ok)
|
|
61
|
+
return unverifiable("Toolroll's database can't be read");
|
|
62
|
+
if (schema.version === null || (schema.version > 0 && schema.version < SCHEMA_VERSION))
|
|
63
|
+
return none;
|
|
64
|
+
if (schema.version !== SCHEMA_VERSION)
|
|
65
|
+
return unverifiable("Toolroll's database is being upgraded");
|
|
66
|
+
const store = openStoreReadOnly(seams.databaseFile);
|
|
67
|
+
if (store === null)
|
|
68
|
+
return unverifiable("Toolroll's database can't be read");
|
|
69
|
+
try {
|
|
70
|
+
return await proveLiveConsole(seams.databaseFile, store, seams.build, seams.attach ?? {});
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return unverifiable("the running console couldn't be checked");
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
store.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** null: not this command (outside a repository) — the caller scans as before. */
|
|
80
|
+
export async function runQuickStart(argv, write, seams) {
|
|
81
|
+
const found = await seams.findRepo(seams.cwd);
|
|
82
|
+
if (found === null)
|
|
83
|
+
return null;
|
|
84
|
+
let repo = found;
|
|
85
|
+
try {
|
|
86
|
+
repo = realpathSync(found);
|
|
87
|
+
}
|
|
88
|
+
catch { /* as found */ }
|
|
89
|
+
const json = argv.includes("--json");
|
|
90
|
+
const dry = json || (seams.underAgent ?? underAgent(seams.env));
|
|
91
|
+
const proof = await prove(seams);
|
|
92
|
+
const enrolled = await seams.enrolled(repo);
|
|
93
|
+
const would = proof.ok ? (enrolled ? "open" : "add-and-open") : proof.state === "none" ? "start" : "refuse";
|
|
94
|
+
if (dry) {
|
|
95
|
+
// Nothing starts, nothing is added, nothing opens: what a person would get, in words or one envelope.
|
|
96
|
+
const words = would === "start" ? `Would start Toolroll for ${repo} (toolroll up).`
|
|
97
|
+
: would === "refuse" ? `Would refuse: ${proof.message}.`
|
|
98
|
+
: `Would ${would === "add-and-open" ? `add ${repo} and ` : ""}open the running console at ${proof.url}.`;
|
|
99
|
+
if (json)
|
|
100
|
+
write(envelopeJson({ ok: would !== "refuse", command: "toolroll", repo, would, ...(proof.ok ? { url: proof.url } : { console: proof.state, message: proof.message }), dryRun: true }));
|
|
101
|
+
else
|
|
102
|
+
write(words);
|
|
103
|
+
return would === "refuse" ? EXIT.refused : EXIT.ok;
|
|
104
|
+
}
|
|
105
|
+
if (!proof.ok && proof.state === "refused") {
|
|
106
|
+
write(`Toolroll can't attach: ${proof.message}.`);
|
|
107
|
+
return EXIT.refused;
|
|
108
|
+
}
|
|
109
|
+
if (!proof.ok)
|
|
110
|
+
return seams.startUp();
|
|
111
|
+
if (!enrolled) {
|
|
112
|
+
const admitted = await seams.admit(repo);
|
|
113
|
+
if (admitted !== 0)
|
|
114
|
+
return admitted;
|
|
115
|
+
}
|
|
116
|
+
// Whatever happened since the first proof (reading the registry, adding the project), the console is proved again,
|
|
117
|
+
// on both channels, right before its address is shown or opened — never a listener that took the port meanwhile.
|
|
118
|
+
const again = await prove(seams);
|
|
119
|
+
if (!again.ok || again.url !== proof.url || again.pid !== proof.pid) {
|
|
120
|
+
write(`Toolroll can't attach: ${again.ok ? "the console changed while the project was being added" : again.message} — run \`toolroll up\`.`);
|
|
121
|
+
return EXIT.refused;
|
|
122
|
+
}
|
|
123
|
+
write(`Toolroll is on ${proof.url}`);
|
|
124
|
+
if (seams.terminal)
|
|
125
|
+
await seams.openBrowser(proof.url);
|
|
126
|
+
return EXIT.ok;
|
|
127
|
+
}
|
|
@@ -55,6 +55,12 @@ export type AdapterOptions = {
|
|
|
55
55
|
sleep?: (ms: number) => Promise<void>;
|
|
56
56
|
};
|
|
57
57
|
export declare function releaseAdapters(options: AdapterOptions): ReleaseAdapters;
|
|
58
|
+
/**
|
|
59
|
+
* Whether a workflow's triggers (its top-level `on:` as a name, a list or a map) include a change to the repository.
|
|
60
|
+
* A file that doesn't parse, or whose triggers aren't one of those shapes, counts as running, so the release waits
|
|
61
|
+
* rather than skipping checks it owes.
|
|
62
|
+
*/
|
|
63
|
+
export declare function workflowRunsOnChange(text: string): boolean;
|
|
58
64
|
/** The formula with its first `url` and `sha256` replaced; anything else is left exactly as it was. */
|
|
59
65
|
export declare function bumpFormula(text: string, url: string, sha256: string): string;
|
|
60
66
|
export {};
|
package/dist/release-adapters.js
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { basename, delimiter, dirname, join } from "node:path";
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
|
+
import { parse as parseYaml } from "yaml";
|
|
13
14
|
// Loaded on first use, like the store: a static import prints Node's SQLite warning before the CLI can quiet it.
|
|
14
15
|
const sqlite = () => createRequire(import.meta.url)("node:sqlite");
|
|
15
16
|
import { activeUpdateWork } from "./desktop-update-gate.js";
|
|
@@ -225,7 +226,14 @@ export function releaseAdapters(options) {
|
|
|
225
226
|
return;
|
|
226
227
|
if (current !== head)
|
|
227
228
|
throw new Error(`${branch} moved after merging; its new head was not deleted.`);
|
|
228
|
-
await
|
|
229
|
+
const pushed = await git("push", `--force-with-lease=${ref}:${head}`, "origin", `:${ref}`);
|
|
230
|
+
if (pushed.code === 0)
|
|
231
|
+
return;
|
|
232
|
+
// GitHub may delete a merged branch itself between the look and the push; gone is what was wanted.
|
|
233
|
+
const after = (await must(git("ls-remote", "origin", ref), "git ls-remote")).trim().split(/\s+/)[0] || null;
|
|
234
|
+
if (after === null)
|
|
235
|
+
return;
|
|
236
|
+
throw new Error(`deleting merged branch ${branch} failed: ${pushed.stderr.trim() || `exit ${pushed.code}`}`);
|
|
229
237
|
},
|
|
230
238
|
async tree(sha) { return (await must(git("rev-parse", `${sha}^{tree}`), `git rev-parse ${sha.slice(0, 7)}^{tree}`)).trim(); },
|
|
231
239
|
async fileAt(sha, path) {
|
|
@@ -338,10 +346,10 @@ export function releaseAdapters(options) {
|
|
|
338
346
|
return prs.find(one => one.state === "open") ?? prs.filter(one => one.state === "merged").sort((a, b) => b.number - a.number)[0] ?? null;
|
|
339
347
|
},
|
|
340
348
|
async checks(repo, sha) {
|
|
341
|
-
const out = await must(gh("api", "--paginate", `repos/${repo}/commits/${sha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion}"), "gh api check-runs");
|
|
349
|
+
const out = await must(gh("api", "--paginate", `repos/${repo}/commits/${sha}/check-runs?per_page=100`, "--jq", ".check_runs[] | {id, name, status, conclusion, app: .app.slug}"), "gh api check-runs");
|
|
342
350
|
const runs = out.split("\n").filter(Boolean).map(line => object(JSON.parse(line)));
|
|
343
351
|
// Oldest first, so a rerun's attempt is the last one listed for its name.
|
|
344
|
-
return runs.sort((a, b) => Number(a["id"]) - Number(b["id"])).map((one) => ({ name: String(one["name"]), status: String(one["status"]), conclusion: textOr(one["conclusion"]) }));
|
|
352
|
+
return runs.sort((a, b) => Number(a["id"]) - Number(b["id"])).map((one) => ({ name: String(one["name"]), status: String(one["status"]), conclusion: textOr(one["conclusion"]), app: textOr(one["app"]) }));
|
|
345
353
|
},
|
|
346
354
|
async merge(repo, pr, head) {
|
|
347
355
|
const merged = object(await ghJson("gh api pulls merge", "api", "-X", "PUT", `repos/${repo}/pulls/${pr}/merge`, "-f", "merge_method=squash", "-f", `sha=${head}`));
|
|
@@ -379,6 +387,37 @@ export function releaseAdapters(options) {
|
|
|
379
387
|
},
|
|
380
388
|
homebrew: {
|
|
381
389
|
repo: tap,
|
|
390
|
+
async checked(head) {
|
|
391
|
+
// Only a workflow that runs on pull requests (or pushes) puts checks on the tap's change; apps that open a
|
|
392
|
+
// check suite and never run it don't count. Workflows are read as the change has them and as the tap's default
|
|
393
|
+
// branch has them (pull_request_target runs from there); a file this can't read counts as checking.
|
|
394
|
+
for (const ref of [head, await defaultBranch()]) {
|
|
395
|
+
const listed = await gh("api", `repos/${tap}/contents/.github/workflows?ref=${encodeURIComponent(ref)}`, "--jq", ".[] | select(.type == \"file\") | .path");
|
|
396
|
+
if (listed.code !== 0) {
|
|
397
|
+
if (/Not Found|HTTP 404/.test(listed.stderr + listed.stdout))
|
|
398
|
+
continue;
|
|
399
|
+
throw new Error(`listing ${tap}'s workflows at ${ref}: ${listed.stderr.trim()}`);
|
|
400
|
+
}
|
|
401
|
+
for (const path of listed.stdout.split("\n").filter(Boolean)) {
|
|
402
|
+
if (!/\.ya?ml$/.test(path))
|
|
403
|
+
continue;
|
|
404
|
+
const file = await gh("api", `repos/${tap}/contents/${path}?ref=${encodeURIComponent(ref)}`, "--jq", ".content");
|
|
405
|
+
if (file.code !== 0)
|
|
406
|
+
return true;
|
|
407
|
+
if (workflowRunsOnChange(Buffer.from(file.stdout.replace(/\\n|\s/g, ""), "base64").toString("utf8")))
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return false;
|
|
412
|
+
},
|
|
413
|
+
async statuses(head) {
|
|
414
|
+
// The older commit-status API, which some CI reports through instead of checks; newest first, so reversed.
|
|
415
|
+
const out = await must(gh("api", "--paginate", `repos/${tap}/commits/${head}/statuses?per_page=100`, "--jq", ".[] | {context, state}"), "gh api statuses");
|
|
416
|
+
return out.split("\n").filter(Boolean).map(line => object(JSON.parse(line))).reverse().map((one) => {
|
|
417
|
+
const state = String(one["state"]);
|
|
418
|
+
return { name: `status: ${String(one["context"])}`, status: state === "pending" ? "in_progress" : "completed", conclusion: state === "pending" ? null : state === "success" ? "success" : "failure" };
|
|
419
|
+
});
|
|
420
|
+
},
|
|
382
421
|
async state(version) {
|
|
383
422
|
const branch = await defaultBranch();
|
|
384
423
|
const current = await formulaAt(branch);
|
|
@@ -427,6 +466,34 @@ export function releaseAdapters(options) {
|
|
|
427
466
|
}
|
|
428
467
|
}
|
|
429
468
|
const brewBranch = (version) => `toolroll-${version}`;
|
|
469
|
+
/** The events that put checks on a change. */
|
|
470
|
+
const CHANGE_EVENTS = new Set(["pull_request", "pull_request_target", "push", "merge_group"]);
|
|
471
|
+
/**
|
|
472
|
+
* Whether a workflow's triggers (its top-level `on:` as a name, a list or a map) include a change to the repository.
|
|
473
|
+
* A file that doesn't parse, or whose triggers aren't one of those shapes, counts as running, so the release waits
|
|
474
|
+
* rather than skipping checks it owes.
|
|
475
|
+
*/
|
|
476
|
+
export function workflowRunsOnChange(text) {
|
|
477
|
+
let parsed;
|
|
478
|
+
try {
|
|
479
|
+
parsed = parseYaml(text);
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
485
|
+
return true;
|
|
486
|
+
const workflow = parsed;
|
|
487
|
+
// YAML 1.1 readers take a bare `on` for true; either key names the triggers.
|
|
488
|
+
const on = "on" in workflow ? workflow["on"] : workflow["true"];
|
|
489
|
+
const events = typeof on === "string" ? [on]
|
|
490
|
+
: Array.isArray(on) ? on
|
|
491
|
+
: on !== null && typeof on === "object" ? Object.keys(on)
|
|
492
|
+
: null;
|
|
493
|
+
if (events === null || !events.every(one => typeof one === "string"))
|
|
494
|
+
return true;
|
|
495
|
+
return events.some(one => CHANGE_EVENTS.has(one));
|
|
496
|
+
}
|
|
430
497
|
/** The formula with its first `url` and `sha256` replaced; anything else is left exactly as it was. */
|
|
431
498
|
export function bumpFormula(text, url, sha256) {
|
|
432
499
|
if (!/^\s*url "[^"]*"/m.test(text) || !/^\s*sha256 "[0-9a-f]{64}"/m.test(text))
|
package/dist/release.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* skipped or failed never merges. merge
|
|
18
18
|
* squash-merges with the head pinned to the gated commit, tree proves main's new commit has exactly the gated tree,
|
|
19
19
|
* and only then tag pushes the version tag, whose macOS matrix must pass before npm publication (publish.yml). publish
|
|
20
|
-
* waits for npm, the GitHub release and the macOS checks; homebrew updates the tap's formula and merges it.
|
|
20
|
+
* waits for npm, the GitHub release and the macOS checks; homebrew updates the tap's formula and merges it (after its checks, when the tap runs any).
|
|
21
21
|
*
|
|
22
22
|
* A private journal (mode 0600, written whole then renamed) records what each step is about to do before it does
|
|
23
23
|
* anything outside this machine, and what it saw after. A stop — a time limit, a failure, a crash, Ctrl-C — leaves the
|
|
@@ -38,10 +38,12 @@ export type ReleaseStep = typeof RELEASE_STEPS[number];
|
|
|
38
38
|
type WaitingStep = "approval" | "check" | "quiet" | "deploy" | "ci" | "publish" | "homebrew";
|
|
39
39
|
/** How long each waiting step may take, in minutes, before the release stops there (rerunning continues from it). */
|
|
40
40
|
export declare const RELEASE_LIMITS: Readonly<Record<WaitingStep, number>>;
|
|
41
|
+
/** One check attempt; `app` is the GitHub App that made it (github-actions for a workflow), when known. */
|
|
41
42
|
export type CheckRun = {
|
|
42
43
|
name: string;
|
|
43
44
|
status: string;
|
|
44
45
|
conclusion: string | null;
|
|
46
|
+
app?: string | null;
|
|
45
47
|
};
|
|
46
48
|
export type PullRequest = {
|
|
47
49
|
number: number;
|
|
@@ -195,6 +197,10 @@ export type ReleaseAdapters = {
|
|
|
195
197
|
};
|
|
196
198
|
homebrew: {
|
|
197
199
|
repo: string;
|
|
200
|
+
/** Whether any of the tap's workflows, at the change or its base, runs on a change; if none does, no checks are owed. */
|
|
201
|
+
checked(head: string): Promise<boolean>;
|
|
202
|
+
/** The change's commit statuses, as check runs oldest first. */
|
|
203
|
+
statuses(head: string): Promise<CheckRun[]>;
|
|
198
204
|
state(version: string): Promise<HomebrewState>;
|
|
199
205
|
propose(version: string): Promise<{
|
|
200
206
|
pr: number;
|
|
@@ -226,10 +232,13 @@ export type ReleaseJournal = {
|
|
|
226
232
|
by: string;
|
|
227
233
|
at: string;
|
|
228
234
|
} | null;
|
|
235
|
+
/** The passing result: its run, receipt and checkout, and the commit it built (`head`), which a prepared candidate
|
|
236
|
+
* records as a commit of its own with exactly the candidate's tree (absent in a journal from before that was known). */
|
|
229
237
|
check: {
|
|
230
238
|
run: number;
|
|
231
239
|
receipt: string;
|
|
232
240
|
worktree: string;
|
|
241
|
+
head?: string;
|
|
233
242
|
} | null;
|
|
234
243
|
completion: {
|
|
235
244
|
actor: string;
|
package/dist/release.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* skipped or failed never merges. merge
|
|
18
18
|
* squash-merges with the head pinned to the gated commit, tree proves main's new commit has exactly the gated tree,
|
|
19
19
|
* and only then tag pushes the version tag, whose macOS matrix must pass before npm publication (publish.yml). publish
|
|
20
|
-
* waits for npm, the GitHub release and the macOS checks; homebrew updates the tap's formula and merges it.
|
|
20
|
+
* waits for npm, the GitHub release and the macOS checks; homebrew updates the tap's formula and merges it (after its checks, when the tap runs any).
|
|
21
21
|
*
|
|
22
22
|
* A private journal (mode 0600, written whole then renamed) records what each step is about to do before it does
|
|
23
23
|
* anything outside this machine, and what it saw after. A stop — a time limit, a failure, a crash, Ctrl-C — leaves the
|
|
@@ -213,9 +213,11 @@ export async function runRelease(options, adapters) {
|
|
|
213
213
|
if (gate.result.check !== "passed")
|
|
214
214
|
throw new Stop("check-failed", `The Full release check of ${j.candidate.sha.slice(0, 7)} did not pass (run ${gate.result.run}: ${gate.result.check}). It stays as it is: fix the branch and release the new commit with --new.`);
|
|
215
215
|
};
|
|
216
|
+
/** The commit the passing build recorded: what the deployed runtime is stamped with. */
|
|
217
|
+
const builtHead = () => j.check?.head ?? j.candidate.sha;
|
|
216
218
|
const checked = (gate) => {
|
|
217
219
|
approved(gate);
|
|
218
|
-
if (gate.result?.head !==
|
|
220
|
+
if (gate.result?.head !== builtHead() || gate.result?.receipt !== j.check?.receipt)
|
|
219
221
|
throw new Stop("drift", `Task ${j.task.id}'s result changed since its check passed.`);
|
|
220
222
|
fullCheck(gate);
|
|
221
223
|
};
|
|
@@ -268,12 +270,16 @@ export async function runRelease(options, adapters) {
|
|
|
268
270
|
throw new Stop("check-failed", `The release build of ${j.task.id} failed: ${gate.failed}. Fix it and release the new commit with --new.`);
|
|
269
271
|
if (gate.result === null || gate.result.check === "running" || gate.result.check === "pending")
|
|
270
272
|
return null;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
// The build records a prepared candidate as a commit of its own (its message names the task): it must hold
|
|
274
|
+
// exactly the candidate's tree, or it isn't the release.
|
|
275
|
+
const head = gate.result.head;
|
|
276
|
+
if (head === null || (head !== j.candidate.sha && await adapters.repo.tree(head) !== await adapters.repo.tree(j.candidate.sha))) {
|
|
277
|
+
throw new Stop("drift", `The release result is for ${head?.slice(0, 7) ?? "no commit"}, which doesn't hold exactly ${j.candidate.sha.slice(0, 7)}'s files.`);
|
|
278
|
+
}
|
|
273
279
|
fullCheck(gate);
|
|
274
280
|
if (gate.result.worktree === null)
|
|
275
281
|
throw new Stop("worktree", `Run ${gate.result.run} names no checkout to deploy from.`);
|
|
276
|
-
return { run: gate.result.run, receipt: gate.result.receipt, worktree: gate.result.worktree };
|
|
282
|
+
return { run: gate.result.run, receipt: gate.result.receipt, worktree: gate.result.worktree, head };
|
|
277
283
|
});
|
|
278
284
|
j.check = result;
|
|
279
285
|
say(` run ${result.run} passed on ${j.candidate.sha.slice(0, 7)}`);
|
|
@@ -310,7 +316,7 @@ export async function runRelease(options, adapters) {
|
|
|
310
316
|
await recoverDeploy();
|
|
311
317
|
checked(await adapters.toolroll.gate(j.task.id));
|
|
312
318
|
// A deployment that finished but whose answer was lost: the services already run a runtime of this commit.
|
|
313
|
-
let runtime = await served(adapters,
|
|
319
|
+
let runtime = await served(adapters, builtHead());
|
|
314
320
|
if (runtime === null) {
|
|
315
321
|
await pinned();
|
|
316
322
|
j.deployAttempt = { stage: join(options.stateDir, "staged-upgrades", `browser-${j.candidate.sha.slice(0, 7)}-${randomUUID()}`) };
|
|
@@ -326,7 +332,7 @@ export async function runRelease(options, adapters) {
|
|
|
326
332
|
save();
|
|
327
333
|
}
|
|
328
334
|
const want = runtime;
|
|
329
|
-
await waitFor("deploy", `the console and worker to run the gated commit from ${want}`, async () => (await served(adapters,
|
|
335
|
+
await waitFor("deploy", `the console and worker to run the gated commit from ${want}`, async () => (await served(adapters, builtHead())) === want ? true : null);
|
|
330
336
|
j.deploy = { runtime: want, at: j.deploy?.runtime === want ? j.deploy.at : adapters.now().toISOString() };
|
|
331
337
|
j.deployAttempt = null;
|
|
332
338
|
delete j.intents.deploy;
|
|
@@ -336,7 +342,7 @@ export async function runRelease(options, adapters) {
|
|
|
336
342
|
}
|
|
337
343
|
case "link": {
|
|
338
344
|
const runtime = j.deploy.runtime;
|
|
339
|
-
if (await served(adapters,
|
|
345
|
+
if (await served(adapters, builtHead()) !== runtime)
|
|
340
346
|
throw new Stop("drift", `The console and worker no longer run ${runtime}; the CLI names were not moved.`);
|
|
341
347
|
if (j.links === undefined || j.links === null) {
|
|
342
348
|
j.links = await adapters.toolroll.cliLinks();
|
|
@@ -348,7 +354,7 @@ export async function runRelease(options, adapters) {
|
|
|
348
354
|
catch (error) {
|
|
349
355
|
throw new Stop("link", `The CLI names were not moved to ${runtime} (they point where they did): ${error.message}. The service already runs the new build; rerun to try again.`);
|
|
350
356
|
}
|
|
351
|
-
const everywhere = await deployed(adapters,
|
|
357
|
+
const everywhere = await deployed(adapters, builtHead());
|
|
352
358
|
if (everywhere !== runtime)
|
|
353
359
|
throw new Stop("mixed-runtime", `After relinking, the console, worker and both CLI names don't all run ${runtime} at ${j.candidate.sha.slice(0, 7)}.`);
|
|
354
360
|
say(` toolroll and standing-orders run ${join(runtime, "bin.js")}`);
|
|
@@ -448,15 +454,28 @@ export async function runRelease(options, adapters) {
|
|
|
448
454
|
const proposed = state;
|
|
449
455
|
j.homebrew = { pr: proposed.pr, head: proposed.head, merged: false };
|
|
450
456
|
save();
|
|
457
|
+
// Whatever checks do register are always judged; an empty set passes only for a tap whose workflows never run
|
|
458
|
+
// on a change, and only when it's still empty a poll later.
|
|
459
|
+
const checked = await brew.checked(proposed.head);
|
|
460
|
+
let emptyLooks = 0;
|
|
451
461
|
await waitFor("homebrew", `the Homebrew change's checks (${brew.repo} #${proposed.pr})`, async () => {
|
|
452
|
-
|
|
462
|
+
// Check runs and commit statuses are judged apart, so neither stands in for the other or hides its failure.
|
|
463
|
+
// Two apps may name a check alike; each app's attempts stand for themselves.
|
|
464
|
+
const runs = (await adapters.github.checks(brew.repo, proposed.head)).map(run => ({ ...run, name: `${run.app ?? "unknown app"}: ${run.name}` }));
|
|
465
|
+
const statuses = await brew.statuses(proposed.head);
|
|
466
|
+
const verdicts = [runs, statuses].filter(one => one.length > 0).map(one => checksVerdict(one, [...new Set(one.map(run => run.name))]));
|
|
467
|
+
const failed = verdicts.flatMap(one => one.state === "failed" ? one.failed : []);
|
|
468
|
+
if (failed.length > 0)
|
|
469
|
+
throw new Stop("homebrew-failed", `The Homebrew change's checks failed: ${failed.join(", ")}.`);
|
|
470
|
+
if (verdicts.some(one => one.state === "pending"))
|
|
471
|
+
return null;
|
|
453
472
|
// GitHub registers checks asynchronously; an empty set says nothing about success.
|
|
454
|
-
|
|
473
|
+
// A workflow is owed: only its own run counts, not another app's green check.
|
|
474
|
+
if (checked && !runs.some(run => run.app === "github-actions"))
|
|
455
475
|
return null;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
return verdict.state === "passed" ? true : null;
|
|
476
|
+
if (runs.length === 0 && statuses.length === 0)
|
|
477
|
+
return ++emptyLooks >= 2 ? true : null;
|
|
478
|
+
return true;
|
|
460
479
|
});
|
|
461
480
|
await brew.merge(proposed.pr, proposed.head);
|
|
462
481
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { html, postForm } from "./html.js";
|
|
1
|
+
import { html, postForm, reauth } from "./html.js";
|
|
2
2
|
import { effectiveLimits, limitOverrides, PER_DAY_MAX, PER_MINUTE_MAX, REQUEST_BUDGET_DEFAULTS } from "./request-budget.js";
|
|
3
3
|
const n = (value) => value.toLocaleString("en-US");
|
|
4
4
|
export const REQUEST_LIMITS_CSS = `.credentials .limits{display:grid;gap:14px}.credentials .limits form{display:grid;gap:10px}.credentials .limits label{display:grid;gap:6px}` +
|
|
@@ -14,7 +14,7 @@ export function requestLimitsHtml(db, tokens, now) {
|
|
|
14
14
|
const overrides = limitOverrides(db);
|
|
15
15
|
const everyone = overrides.get("*") ?? { readPerMinute: null, actPerMinute: null, perDay: null };
|
|
16
16
|
const field = (name, label, value, placeholder, max) => html `<label>${label}<input type="number" name="${name}" min="1" max="${max}" step="1" inputmode="numeric" value="${value === null ? "" : value}" placeholder="${placeholder}"></label>`;
|
|
17
|
-
const password = html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label
|
|
17
|
+
const password = html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password"></label>`)}`;
|
|
18
18
|
const buttons = html `<div class="actions"><button name="action" value="save">Save limits</button><button class="secondary" name="action" value="clear">Use defaults</button></div>`;
|
|
19
19
|
const live = tokens.filter(one => one.revokedAt === null && Date.parse(one.expiresAt) > now);
|
|
20
20
|
const installation = postForm("/settings/request-limits", html `\
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Add tests files a small task, unapproved like any other filing, to write
|
|
11
11
|
* tests for that change. */
|
|
12
|
+
import { fencedCheckoutCommands } from "./invoke.js";
|
|
12
13
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
13
14
|
import { tmpdir } from "node:os";
|
|
14
15
|
import { join } from "node:path";
|
|
@@ -201,7 +202,7 @@ export async function runFollowUpCheck(store, root, request, deps = {}) {
|
|
|
201
202
|
let ready = true;
|
|
202
203
|
if (setup !== null) {
|
|
203
204
|
const shell = approvedCommandShell(setup.command);
|
|
204
|
-
const prepared = await runWithIsolatedDatabase(runner, shell.file, shell.args, { cwd: checkout, timeoutMs: setup.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
|
|
205
|
+
const prepared = await runWithIsolatedDatabase(fencedCheckoutCommands(store, target.repo, checkout, runner), shell.file, shell.args, { cwd: checkout, timeoutMs: setup.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
|
|
205
206
|
note("Approved project setup", setup.command, prepared);
|
|
206
207
|
if (prepared.code !== 0 || prepared.timedOut || prepared.notFound) {
|
|
207
208
|
ready = false;
|
|
@@ -216,7 +217,7 @@ export async function runFollowUpCheck(store, root, request, deps = {}) {
|
|
|
216
217
|
}
|
|
217
218
|
else {
|
|
218
219
|
const shell = approvedCommandShell(live.command);
|
|
219
|
-
const ran = await runWithIsolatedDatabase(runner, shell.file, shell.args, { cwd: checkout, timeoutMs: live.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
|
|
220
|
+
const ran = await runWithIsolatedDatabase(fencedCheckoutCommands(store, target.repo, checkout, runner), shell.file, shell.args, { cwd: checkout, timeoutMs: live.timeoutMs, processGroup: true, envAllowlist: SETUP_ENV_ALLOWLIST, omitEnv: SETUP_ENV_DENYLIST });
|
|
220
221
|
note(`${CHECK_LEVEL_WORDS[target.level]} check`, live.command, ran);
|
|
221
222
|
result = ran.timedOut ? { ran: false, failure: "The check ran out of time." } : ran.notFound ? { ran: false, failure: "The check couldn't start." } : { ran: true, exitCode: ran.code };
|
|
222
223
|
}
|
package/dist/result-review.d.ts
CHANGED
|
@@ -149,7 +149,7 @@ export declare const RESULT_FACT_KEYS: readonly ["run", "head", "base", "head-so
|
|
|
149
149
|
export declare function resultFactsFromHtml(html: string): Record<string, string>[];
|
|
150
150
|
/** Where a result form may send its reader back: the review cockpit's
|
|
151
151
|
* own deep link, the chat's result detail, the page of a task named in
|
|
152
|
-
* `tasks` (the run's own), or the run page. Anything else lands on the run
|
|
152
|
+
* `tasks` (the run's own) or its Result tab at this run, or the run page. Anything else lands on the run
|
|
153
153
|
* page. Matched exactly — never a bare open redirect. */
|
|
154
154
|
export declare function resultReturnTarget(raw: string | null | undefined, runId: number, tasks?: readonly string[]): string;
|
|
155
155
|
/** A form's own request token, minted per render — the same shape the
|
package/dist/result-review.js
CHANGED
|
@@ -147,7 +147,7 @@ export function resultFactsFromHtml(html) {
|
|
|
147
147
|
}
|
|
148
148
|
/** Where a result form may send its reader back: the review cockpit's
|
|
149
149
|
* own deep link, the chat's result detail, the page of a task named in
|
|
150
|
-
* `tasks` (the run's own), or the run page. Anything else lands on the run
|
|
150
|
+
* `tasks` (the run's own) or its Result tab at this run, or the run page. Anything else lands on the run
|
|
151
151
|
* page. Matched exactly — never a bare open redirect. */
|
|
152
152
|
export function resultReturnTarget(raw, runId, tasks = []) {
|
|
153
153
|
if (raw !== null && raw !== undefined) {
|
|
@@ -160,6 +160,10 @@ export function resultReturnTarget(raw, runId, tasks = []) {
|
|
|
160
160
|
// The run's own task page, which offered Run checks in place; never another task's.
|
|
161
161
|
if (tasks.some(task => raw === `/t/${encodeURIComponent(task)}`))
|
|
162
162
|
return raw;
|
|
163
|
+
// That task page's Result tab at this run (docs/plans/one-page-task-and-agent.md), its version and view kept.
|
|
164
|
+
const tab = /^\/t\/([A-Za-z0-9._~%-]{1,200})\?(?:version=[A-Za-z0-9._~%-]{1,200}&)?tab=result&run=([0-9]{1,15})(?:§ion=(?:summary|changes|checks))?$/.exec(raw);
|
|
165
|
+
if (tab !== null && Number(tab[2]) === runId && tasks.some(task => tab[1] === encodeURIComponent(task)))
|
|
166
|
+
return raw;
|
|
163
167
|
}
|
|
164
168
|
return `/r/${runId}`;
|
|
165
169
|
}
|
|
@@ -240,6 +244,8 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
240
244
|
var user=panel?panel.getAttribute('data-result-user')||'':'';
|
|
241
245
|
var task=panel?panel.getAttribute('data-result-task')||'':'';
|
|
242
246
|
var run=panel?panel.getAttribute('data-result-run')||'':'';
|
|
247
|
+
// The address parameter that holds the result's own view: 'tab' on its page, 'section' on a task page's Result tab.
|
|
248
|
+
var viewParam=panel&&panel.getAttribute('data-result-param')==='section'?'section':'tab';
|
|
243
249
|
var draftPrefix='standing-orders:review-draft:',scrollPrefix='standing-orders:result-scroll:';
|
|
244
250
|
var day=86400000,storage=true,rememberPosition=null;
|
|
245
251
|
function read(key){try{return JSON.parse(sessionStorage.getItem(key)||'null');}catch(e){return null;}}
|
|
@@ -262,7 +268,7 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
262
268
|
tab.addEventListener('click',function(ev){
|
|
263
269
|
if(ev.metaKey||ev.ctrlKey||ev.shiftKey||ev.altKey||ev.button!==0)return;
|
|
264
270
|
ev.preventDefault();var name=tab.getAttribute('data-result-tab');show(name);
|
|
265
|
-
try{var url=new URL(location.href);url.searchParams.set(
|
|
271
|
+
try{var url=new URL(location.href);url.searchParams.set(viewParam,name);url.hash='';history.replaceState(history.state,'',url.toString());}catch(e){}
|
|
266
272
|
if(rememberPosition)rememberPosition();
|
|
267
273
|
});
|
|
268
274
|
tab.addEventListener('keydown',function(ev){
|
|
@@ -383,7 +389,7 @@ export const RESULT_REVIEW_SCRIPT = String.raw `
|
|
|
383
389
|
// view the URL now names.
|
|
384
390
|
var returnKey=scrollPrefix+user+':'+task+':return';
|
|
385
391
|
function scrollKeyFor(){var url=null;try{url=new URL(location.href);}catch(e){}
|
|
386
|
-
return scrollPrefix+user+':'+task+':'+(url?(url.searchParams.get('result')||run)+':'+(url.searchParams.get(
|
|
392
|
+
return scrollPrefix+user+':'+task+':'+(url?(url.searchParams.get('result')||run)+':'+(url.searchParams.get(viewParam)||'summary'):run);}
|
|
387
393
|
// Once a navigation has begun (pageswap / beforeunload) the document's
|
|
388
394
|
// scroll is no longer the reader's — a cross-document transition can
|
|
389
395
|
// move it to the top while it is captured — so remembering stops.
|
package/dist/retention-ui.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* the next daily sweep would remove. An instance operator's page; changes
|
|
4
4
|
* take a step-up.
|
|
5
5
|
*/
|
|
6
|
-
import { html, postForm } from "./html.js";
|
|
6
|
+
import { html, postForm, reauth } from "./html.js";
|
|
7
7
|
import { bytesWords } from "./storage.js";
|
|
8
8
|
import { RETENTION_KINDS, countWords, periodChoices, periodLabel } from "./retention.js";
|
|
9
9
|
export const RETENTION_CSS = `.retention{max-width:720px;min-width:0}.retention-next{margin:0 0 16px}` +
|
|
@@ -31,5 +31,5 @@ export function retentionHtml(view, notice) {
|
|
|
31
31
|
return html `<div class="kind" data-kind="${kind}"><label class="name" for="keep-${kind}">${label}</label><p class="meta">${detail}</p><select id="keep-${kind}" name="${kind}">${options}</select></div>`;
|
|
32
32
|
});
|
|
33
33
|
const last = view.lastSweep === null ? "" : ` Last sweep ${view.lastSweep.slice(0, 10)}.`;
|
|
34
|
-
return html `<article class="retention">${note}<p class="retention-next" data-retention-next>${nextWords(view)}${last}</p>${postForm("/settings/retention", html `${rows}
|
|
34
|
+
return html `<article class="retention">${note}<p class="retention-next" data-retention-next>${nextWords(view)}${last}</p>${postForm("/settings/retention", html `${rows}${reauth(html `<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<button type="submit">Save</button>`)}<p class="meta">Never removed: the action ledger, and anything a task still needs (unfinished tasks, results Ready for review, anything on hold).</p></article>`;
|
|
35
35
|
}
|
package/dist/retention.js
CHANGED
|
@@ -24,6 +24,10 @@ import { RETENTION_NOTE } from "./evidence.js";
|
|
|
24
24
|
import { COMPLETION_ACTION } from "./result-completion.js";
|
|
25
25
|
import { bytesWords, treeBytes } from "./storage.js";
|
|
26
26
|
import { RETENTION_DAYS, retentionDaysSchema, retentionKindSchema } from "./contracts/retention.js";
|
|
27
|
+
/** A personal lead-queue row the sweep keeps (with its message): one still waiting, running or uncertain, and any of a
|
|
28
|
+
* conversation that is still open — its request id is the tombstone that stops a removed message being sent again. */
|
|
29
|
+
const KEPT_QUEUE_ROW = (message) => `lead_queue lq WHERE lq.message = ${message}
|
|
30
|
+
AND (lq.status IN ('queued', 'running', 'uncertain') OR EXISTS (SELECT 1 FROM lead_session ls WHERE ls.id = lq.session AND ls.ended_at IS NULL))`;
|
|
27
31
|
const RETENTION_TABLE = (name) => `
|
|
28
32
|
CREATE TABLE IF NOT EXISTS ${name} (
|
|
29
33
|
kind TEXT PRIMARY KEY CHECK (kind IN ('evidence', 'checkouts', 'chat', 'notifications')),
|
|
@@ -177,6 +181,7 @@ export function retentionPlan(store, evidenceRoot, now, periods = store.retentio
|
|
|
177
181
|
const rows = db.prepare(`SELECT m.id AS id, length(m.text) + COALESCE(length(m.activity), 0) + 64 AS bytes FROM lead_message m JOIN lead_thread th ON th.id = m.thread
|
|
178
182
|
WHERE m.created_at < ?
|
|
179
183
|
AND NOT EXISTS (SELECT 1 FROM team_message tm WHERE tm.message = m.id AND tm.status IN ('queued', 'running', 'uncertain'))
|
|
184
|
+
AND NOT EXISTS (SELECT 1 FROM ${KEPT_QUEUE_ROW("m.id")})
|
|
180
185
|
AND NOT EXISTS (SELECT 1 FROM mate_turn turn WHERE turn.id = m.turn AND turn.state IN ('queued', 'running'))
|
|
181
186
|
AND NOT (th.scope_kind = 'task' AND NOT EXISTS (SELECT 1 FROM task_ref r WHERE r.external_id = th.scope_key AND ${finishedTask("r")}))
|
|
182
187
|
ORDER BY m.id LIMIT ?`).all(cutoff(now, periods.chat), SWEEP_BATCH + 1);
|
|
@@ -263,14 +268,18 @@ export function sweepRetention(store, evidenceRoot, now, actor = "worker") {
|
|
|
263
268
|
}
|
|
264
269
|
if (periods.chat !== null) {
|
|
265
270
|
const team = db.prepare("DELETE FROM team_message WHERE message = ? AND status NOT IN ('queued', 'running', 'uncertain')");
|
|
266
|
-
const message = db.prepare(
|
|
271
|
+
const message = db.prepare(`DELETE FROM lead_message WHERE id = ? AND NOT EXISTS (SELECT 1 FROM team_message WHERE message = ?)
|
|
272
|
+
AND NOT EXISTS (SELECT 1 FROM ${KEPT_QUEUE_ROW("lead_message.id")})`);
|
|
267
273
|
const asked = db.prepare("SELECT turn FROM lead_message WHERE id = ? AND role = 'assistant' AND turn IS NOT NULL");
|
|
274
|
+
const searchCopy = db.prepare("DELETE FROM memory_search WHERE kind = 'conversation' AND ref = ?");
|
|
268
275
|
// The lead's question goes with the reply that asked it; its buttons then say it expired.
|
|
269
276
|
const ask = db.prepare("DELETE FROM lead_ask WHERE turn = ? AND NOT EXISTS (SELECT 1 FROM lead_message WHERE turn = ? AND role = 'assistant')");
|
|
270
277
|
for (const one of plan.items.chat) {
|
|
271
278
|
team.run(one.id);
|
|
272
279
|
const turn = asked.get(one.id)?.["turn"];
|
|
273
280
|
if (Number(message.run(one.id, one.id).changes) === 1) {
|
|
281
|
+
// Its search copy goes with it (search never shows words the store no longer keeps).
|
|
282
|
+
searchCopy.run(`message:${one.id}`);
|
|
274
283
|
done.chat.count++;
|
|
275
284
|
done.chat.bytes += one.bytes;
|
|
276
285
|
if (turn !== undefined && turn !== null)
|