toolroll 0.9.98 → 0.9.100
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 +16 -0
- package/dist/acp-check.js +4 -2
- package/dist/agent-state.js +2 -1
- package/dist/agents-board.js +3 -2
- package/dist/agents-live.js +3 -2
- package/dist/approval-authority.d.ts +2 -1
- package/dist/approval-authority.js +2 -1
- package/dist/chat-ask.js +4 -2
- package/dist/chat-core.js +4 -2
- package/dist/chat-decide.js +1 -2
- package/dist/chat-delivery-state.d.ts +29 -6
- package/dist/chat-delivery-state.js +43 -13
- package/dist/chat-delivery.js +12 -5
- package/dist/chat-flow.js +15 -13
- package/dist/chat-live.js +5 -2
- package/dist/chat-message-states.d.ts +75 -0
- package/dist/chat-message-states.js +74 -0
- package/dist/chat-messages.d.ts +10 -4
- package/dist/chat-messages.js +12 -7
- package/dist/check-failure.js +2 -1
- package/dist/check-report.js +1 -1
- package/dist/checkout-cleanup.js +2 -1
- package/dist/claim.d.ts +40 -2
- package/dist/claim.js +92 -19
- package/dist/contracts/chat-tables.js +3 -0
- package/dist/coordinator.js +3 -3
- package/dist/demo.js +7 -2
- package/dist/deploy-phase-data.json +21 -0
- package/dist/deploy-phase.d.ts +37 -0
- package/dist/deploy-phase.js +64 -0
- package/dist/deploy-rescue.d.ts +2 -1
- package/dist/deploy-rescue.js +6 -7
- package/dist/discord-chat.js +2 -3
- package/dist/dispatch.js +2 -1
- package/dist/dispose.d.ts +4 -1
- package/dist/dispose.js +2 -1
- package/dist/fallback.js +3 -2
- package/dist/fast-start.js +2 -2
- package/dist/monitoring.js +2 -1
- package/dist/operate.js +75 -36
- package/dist/plane-review.js +2 -1
- package/dist/project-knowledge.d.ts +3 -3
- package/dist/release-adapters.js +30 -14
- package/dist/release-authority.d.ts +9 -0
- package/dist/release-authority.js +13 -0
- package/dist/release-auto.js +2 -3
- package/dist/release-gate.d.ts +3 -3
- package/dist/release-gate.js +8 -5
- package/dist/release-next.d.ts +3 -3
- package/dist/release-next.js +13 -16
- package/dist/release-view.js +2 -1
- package/dist/release.js +18 -30
- package/dist/result-shots.js +1 -1
- package/dist/route-results.js +2 -1
- package/dist/run-ending.d.ts +21 -0
- package/dist/run-ending.js +19 -0
- package/dist/scope.js +2 -2
- package/dist/server/render-results.js +3 -2
- package/dist/server/render-tasks.js +3 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.js +2 -3
- package/dist/staged-runtimes.js +2 -1
- package/dist/store.d.ts +61 -2
- package/dist/store.js +127 -77
- package/dist/subagent-question.js +8 -6
- package/dist/surface.js +2 -2
- package/dist/task-moves.d.ts +90 -0
- package/dist/task-moves.js +68 -0
- package/dist/teams-chat.js +2 -2
- package/dist/telegram-flow.d.ts +1 -1
- package/dist/telegram-flow.js +1 -1
- package/dist/telegram-lead.d.ts +1 -2
- package/dist/telegram-lead.js +18 -23
- package/dist/telegram-progress.js +2 -1
- package/dist/telegram.d.ts +0 -1
- package/dist/telegram.js +6 -7
- package/dist/vanished-retry.js +3 -2
- package/dist/workspace-ui.js +2 -1
- package/dist/worktree.js +3 -2
- package/package.json +1 -1
package/dist/release-adapters.js
CHANGED
|
@@ -18,7 +18,8 @@ import { callLabel, readOnlyCall, retryCall, retryingExec, writeWithRecheck } fr
|
|
|
18
18
|
const sqlite = () => createRequire(import.meta.url)("node:sqlite");
|
|
19
19
|
import { PROTECTION_MINIMUM_CHECKS, PROTECTION_REPORT_ONLY_CHECKS } from "./release.js";
|
|
20
20
|
import { activeUpdateWork } from "./desktop-update-gate.js";
|
|
21
|
-
import { BEFORE_SWAP,
|
|
21
|
+
import { BEFORE_SWAP, deployActionOf, stoppedWhereNothingChanged } from "./deploy-phase.js";
|
|
22
|
+
import { gateOwner } from "./release-gate.js";
|
|
22
23
|
import { findCliLinks, switchCliLinks } from "./release-links.js";
|
|
23
24
|
import { databasePath, finishReleaseOperation, requireFreshReleaseApproval, openStore, releaseAuthorityOfTask, releaseCarries, releaseOperations, renewReleaseLease, reserveRelease, runReleaseOperation, Store, recordReleaseStep, releaseIssuerGone, takeReleaseLease, } from "./store.js";
|
|
24
25
|
import { acquireDeploymentLock } from "./deploy-lock.js";
|
|
@@ -62,8 +63,6 @@ const ownCli = () => {
|
|
|
62
63
|
const bin = join(dirname(fileURLToPath(import.meta.url)), "bin.js");
|
|
63
64
|
return existsSync(bin) ? [process.execPath, bin] : ["toolroll"];
|
|
64
65
|
};
|
|
65
|
-
/** Journal phases the deployment's finish accepts (scripts/deploy-phases.mjs finish): the new service runs, unproven. */
|
|
66
|
-
const FINISHES_FORWARD = Object.freeze(["started", "healthy"]);
|
|
67
66
|
const cleanEnv = (extra = {}) => {
|
|
68
67
|
const env = { ...process.env, ...extra };
|
|
69
68
|
delete env["NODE_OPTIONS"];
|
|
@@ -580,23 +579,40 @@ export function releaseAdapters(options) {
|
|
|
580
579
|
const running = (options.processes ?? processes)().filter(one => one.command.includes("deploy-browser.mjs") && one.command.includes(stage));
|
|
581
580
|
if (running.length > 0)
|
|
582
581
|
return { ok: false, message: `the deployment is still running (process ${running.map(one => one.pid).join(", ")}); let it finish, then rerun` };
|
|
583
|
-
// The new service started (deploy-
|
|
584
|
-
//
|
|
582
|
+
// The new service was started (the phase table's "forward" rows, deploy-phase.ts): the release resumes it
|
|
583
|
+
// forward with the deployment's own finish (health, both consoles, leases, then the pause lifted).
|
|
585
584
|
// Finishing deploys, so the candidate is proven first, as for a deploy.
|
|
586
|
-
const
|
|
585
|
+
const journalPhase = () => { try {
|
|
587
586
|
return object(JSON.parse(readFileSync(join(stage, "deployment.json"), "utf8")))["phase"];
|
|
588
587
|
}
|
|
589
588
|
catch {
|
|
590
589
|
return undefined;
|
|
591
|
-
} }
|
|
592
|
-
|
|
590
|
+
} };
|
|
591
|
+
const phase = journalPhase();
|
|
592
|
+
if (deployActionOf(phase) === "forward") {
|
|
593
593
|
const proven = proveCandidate(runId);
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
594
|
+
let why;
|
|
595
|
+
if (proven.ok) {
|
|
596
|
+
const finished = await deployScript(worktree, stage, runId, proven, ["--yes", "--phase", "finish"]);
|
|
597
|
+
if (finished.code === 0)
|
|
598
|
+
return { ok: true };
|
|
599
|
+
why = `${tail(finished)} (the deployment journal is at ${String(phase)}; finishing it forward stopped; ${join(stage, "deployment.json")})`;
|
|
600
|
+
}
|
|
601
|
+
else
|
|
602
|
+
why = proven.message;
|
|
603
|
+
// Once the new service was recorded as started, a journal alone never rolls it back: the release stops here.
|
|
604
|
+
// Still at "starting" (killed while waiting for the new service; finish only looks, and it did not find it
|
|
605
|
+
// running): the deployment's own recovery puts the previous service back, and only when it proves the new
|
|
606
|
+
// one gone. That needs no approval of the candidate, exactly as below.
|
|
607
|
+
if (journalPhase() !== "starting")
|
|
608
|
+
return stopped(stage, why);
|
|
609
|
+
const recorded = recordedCommit(runId);
|
|
610
|
+
if (recorded === null)
|
|
611
|
+
return stopped(stage, why);
|
|
612
|
+
const back = await deployScript(worktree, stage, runId, recorded, ["--phase", "recover"]);
|
|
613
|
+
if (back.code === 0)
|
|
598
614
|
return { ok: true };
|
|
599
|
-
return stopped(stage, `${
|
|
615
|
+
return stopped(stage, `${why} The previous service was not put back either: ${tail(back)}`);
|
|
600
616
|
}
|
|
601
617
|
// Recovery needs no approval, but it runs only the recorded commit's own script, extracted by this runtime.
|
|
602
618
|
const recorded = recordedCommit(runId);
|
|
@@ -642,7 +658,7 @@ export function releaseAdapters(options) {
|
|
|
642
658
|
catch {
|
|
643
659
|
return false;
|
|
644
660
|
}
|
|
645
|
-
return phase
|
|
661
|
+
return stoppedWhereNothingChanged(phase);
|
|
646
662
|
},
|
|
647
663
|
async cliLinks() { return findCliLinks(); },
|
|
648
664
|
async switchLinks(links, bin, version) {
|
|
@@ -7,6 +7,15 @@ import { type ReleasePolicy, type ReleaseTerms } from "./contracts/release-terms
|
|
|
7
7
|
import type { Store } from "./store.js";
|
|
8
8
|
export type ReleaseState = "open" | "deploying" | "merge-reserved" | "merged" | "revoked" | "expired" | "superseded";
|
|
9
9
|
export declare const LIVE_RELEASE_STATES: readonly ReleaseState[];
|
|
10
|
+
/**
|
|
11
|
+
* Whether an authority's approval has run out at `now`. Nothing writes the `expired` state: expiry is worked out from
|
|
12
|
+
* `expires_at` here, and every reader that asks whether an authority is still good asks through this (or
|
|
13
|
+
* releaseStateAt). The one SQL reader, approval-authority.ts releaseStandsSql, states the same rule in SQL.
|
|
14
|
+
*/
|
|
15
|
+
export declare function releaseExpiredAt(a: Pick<ReleaseAuthority, "state" | "expiresAt">, now: Date): boolean;
|
|
16
|
+
/** An authority's state as read at `now`: a live one (open, deploying, merge-reserved) past its time reads as `expired`;
|
|
17
|
+
* an ended one keeps the state it ended in. */
|
|
18
|
+
export declare function releaseStateAt(a: Pick<ReleaseAuthority, "state" | "expiresAt">, now: Date): ReleaseState;
|
|
10
19
|
/** One generation of a release a person approved: the single source of truth for it. */
|
|
11
20
|
export type ReleaseAuthority = {
|
|
12
21
|
id: number;
|
|
@@ -5,6 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { readReleasePolicy } from "./contracts/release-terms.js";
|
|
7
7
|
export const LIVE_RELEASE_STATES = ["open", "deploying", "merge-reserved"];
|
|
8
|
+
/**
|
|
9
|
+
* Whether an authority's approval has run out at `now`. Nothing writes the `expired` state: expiry is worked out from
|
|
10
|
+
* `expires_at` here, and every reader that asks whether an authority is still good asks through this (or
|
|
11
|
+
* releaseStateAt). The one SQL reader, approval-authority.ts releaseStandsSql, states the same rule in SQL.
|
|
12
|
+
*/
|
|
13
|
+
export function releaseExpiredAt(a, now) {
|
|
14
|
+
return a.state === "expired" || a.expiresAt <= now.toISOString();
|
|
15
|
+
}
|
|
16
|
+
/** An authority's state as read at `now`: a live one (open, deploying, merge-reserved) past its time reads as `expired`;
|
|
17
|
+
* an ended one keeps the state it ended in. */
|
|
18
|
+
export function releaseStateAt(a, now) {
|
|
19
|
+
return LIVE_RELEASE_STATES.includes(a.state) && releaseExpiredAt(a, now) ? "expired" : a.state;
|
|
20
|
+
}
|
|
8
21
|
function readReleaseAuthority(row) {
|
|
9
22
|
const text = (key) => row[key] == null ? null : String(row[key]);
|
|
10
23
|
let policy = null;
|
package/dist/release-auto.js
CHANGED
|
@@ -26,7 +26,7 @@ import { releaseCheckStanding } from "./check-failure.js";
|
|
|
26
26
|
import { CARRY_RULES, rulePath } from "./release-carry.js";
|
|
27
27
|
import { AUTO_EVIDENCE_ACTION, AUTO_WITHDRAWN_ACTION, LIVE_RELEASE_STATES, RELEASE_APPROVAL_ACTION, releaseAuthorityOfTask, releaseAutoApproves, releaseApprovalSetting } from "./release-authority.js";
|
|
28
28
|
import { autoReleaseApprovalOf, personApprovalOf } from "./scope.js";
|
|
29
|
-
import { releaseOperations, revokeRelease } from "./store.js";
|
|
29
|
+
import { releaseOperations, revokeRelease, withdrawApproval } from "./store.js";
|
|
30
30
|
import { RELEASE_CI_HELD_ACTION, RELEASE_CI_PASSED_ACTION, RELEASE_CI_RUNNING_WORDS, releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
31
31
|
import { verificationEvidence } from "./verification-evidence.js";
|
|
32
32
|
import { nightlyDebtHold } from "./nightly-github.js";
|
|
@@ -87,8 +87,7 @@ export function setReleaseApproval(store, repo, auto, by, now) {
|
|
|
87
87
|
* marked withdrawn, so no later reading — the setting turned on again included — lets it stand. */
|
|
88
88
|
function unseal(store, taskId, now) {
|
|
89
89
|
withdraw(store, taskId, now);
|
|
90
|
-
store
|
|
91
|
-
approved_route_json = NULL, approval_basis = NULL, mode_digest = NULL WHERE task_id = ? AND approval_basis IN ('auto-check', 'auto-green')`).run(taskId);
|
|
90
|
+
withdrawApproval(store, { where: "WHERE task_id = ? AND approval_basis IN ('auto-check', 'auto-green')", args: [taskId] }, { basisAfter: null, clearRoute: true, clearModeDigest: true });
|
|
92
91
|
}
|
|
93
92
|
/** Mark a task's automatic seal's evidence withdrawn for good (its seal row may stay while an issued merge reconciles). */
|
|
94
93
|
function withdraw(store, taskId, now) {
|
package/dist/release-gate.d.ts
CHANGED
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
* Anything missing, damaged or contradicting refuses and leaves the pause as it is. Without --yes it only shows the
|
|
19
19
|
* proof; with --yes it lifts that one pause (the owner is checked again inside the same write) and journals it.
|
|
20
20
|
*/
|
|
21
|
+
import { BEFORE_SWAP } from "./deploy-phase.js";
|
|
21
22
|
import type { Database } from "./store.js";
|
|
22
|
-
/** The journal phases a deployment passes before it stops the old service (deploy-
|
|
23
|
-
export
|
|
23
|
+
/** The journal phases a deployment passes before it stops the old service (deploy-phase.ts, the one table). */
|
|
24
|
+
export { BEFORE_SWAP };
|
|
24
25
|
export type GateReleaseWorld = {
|
|
25
26
|
/** The live database file. */
|
|
26
27
|
database: string;
|
|
@@ -81,4 +82,3 @@ export type GateReleaseOutcome = {
|
|
|
81
82
|
};
|
|
82
83
|
/** Prove, then (with `yes`) lift exactly that pause and journal the release in the deployment's own journal. */
|
|
83
84
|
export declare function releaseGate(id: string, world: GateReleaseWorld, yes: boolean): GateReleaseOutcome;
|
|
84
|
-
export {};
|
package/dist/release-gate.js
CHANGED
|
@@ -23,10 +23,11 @@ import { join } from "node:path";
|
|
|
23
23
|
import { durableJson } from "./desktop-update.js";
|
|
24
24
|
import { removeUpdateGate, updateGateOwned } from "./desktop-update-gate.js";
|
|
25
25
|
import { acquireDeploymentLock } from "./deploy-lock.js";
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
import { BEFORE_SWAP, RELEASED } from "./deploy-phase.js";
|
|
27
|
+
/** The journal phases a deployment passes before it stops the old service (deploy-phase.ts, the one table). */
|
|
28
|
+
export { BEFORE_SWAP };
|
|
28
29
|
/** Journal fields that only the swap and later phases write. */
|
|
29
|
-
const SWAP_FIELDS = ["stoppingService", "codingBackupBeforeSwap", "migration", "newService", "deployedAt"];
|
|
30
|
+
const SWAP_FIELDS = ["stoppingService", "codingBackupBeforeSwap", "migration", "newService", "deployedAt", "launchd", "newServiceStop"];
|
|
30
31
|
const GATE_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
31
32
|
const refuse = (reason, message) => ({ ok: false, reason, message: `${message} The update pause was left as it is.` });
|
|
32
33
|
/** The owner id written into the live pause's triggers, when there is one pause and it names one id. */
|
|
@@ -121,7 +122,9 @@ export function proveBeforeSwap(id, world) {
|
|
|
121
122
|
if (staged.length > 0)
|
|
122
123
|
return refuse("after-swap", `Process ${staged.map(one => one.pid).join(", ")} runs from the staged runtime ${r.nextRuntime}.`);
|
|
123
124
|
const supervisor = processes.find(one => one.command.includes(`${r.priorRuntime}/controller-service.js`));
|
|
124
|
-
|
|
125
|
+
// The database as a whole argument: an `up` on a path that only begins with this database's is another service's.
|
|
126
|
+
const upHere = `${r.priorRuntime}/cli.js up --db ${world.database}`;
|
|
127
|
+
const worker = processes.find(one => one.command.split(upHere).slice(1).some(rest => rest === "" || /^\s/.test(rest)));
|
|
125
128
|
if (supervisor === undefined || worker === undefined)
|
|
126
129
|
return refuse("conflict", `The service is not running from ${r.priorRuntime}, so it can't be shown that the swap never began.`);
|
|
127
130
|
proof.push(`The service runs from ${r.priorRuntime} (process ${supervisor.pid}); nothing runs from the staged runtime, and no deployment is running.`);
|
|
@@ -163,7 +166,7 @@ export function releaseGate(id, world, yes) {
|
|
|
163
166
|
// The owner is checked again within removeUpdateGate's short write transaction.
|
|
164
167
|
removeUpdateGate(db, id);
|
|
165
168
|
durableJson(journalFile, {
|
|
166
|
-
...fresh.journal, phase:
|
|
169
|
+
...fresh.journal, phase: RELEASED, updatedAt: world.now().toISOString(),
|
|
167
170
|
releasedBeforeSwap: { at: world.now().toISOString(), by: "toolroll release --release-gate", from: fresh.journal.phase, proof: fresh.proof },
|
|
168
171
|
});
|
|
169
172
|
return { ok: true, released: true, id, stage: fresh.stage, proof: fresh.proof };
|
package/dist/release-next.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deployActionOf, type DeployAction } from "./deploy-phase.js";
|
|
1
2
|
import { type RescueWorld } from "./deploy-rescue.js";
|
|
2
3
|
import { type GateReleaseWorld } from "./release-gate.js";
|
|
3
4
|
import { type ReleaseJournal } from "./release.js";
|
|
@@ -29,9 +30,8 @@ export type SettleWorld = {
|
|
|
29
30
|
rescue: RescueWorld;
|
|
30
31
|
gate: GateReleaseWorld;
|
|
31
32
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
export declare function deployActionOf(phase: unknown): DeployAction;
|
|
33
|
+
/** The one action a deployment's journal calls for: a row of the one phase table (deploy-phase.ts). */
|
|
34
|
+
export { deployActionOf, type DeployAction };
|
|
35
35
|
/** Settle what a stopped deployment left, saying what was done. A refusal stops the release before it does anything. */
|
|
36
36
|
export declare function settleDeployment(world: SettleWorld, say: (line: string) => void): Promise<{
|
|
37
37
|
ok: true;
|
package/dist/release-next.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
import { closeSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, writeSync } from "node:fs";
|
|
12
12
|
import { hostname } from "node:os";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
14
|
+
import { deployActionOf } from "./deploy-phase.js";
|
|
15
|
+
import { rescueDeployment } from "./deploy-rescue.js";
|
|
16
|
+
import { releaseGate } from "./release-gate.js";
|
|
16
17
|
import { journalFile, readJournal } from "./release.js";
|
|
17
18
|
import { autoPublishHeld, PUBLISH_ALLOWED_ACTION } from "./release-auto.js";
|
|
18
19
|
import { sameMachineHost } from "./machine-identity.js";
|
|
@@ -78,18 +79,8 @@ export async function takeReleaseRun(stateDir, branch, say, seams = {}) {
|
|
|
78
79
|
await sleep(seams.pollMs ?? 5_000);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
|
-
/** The one action a deployment's journal calls for. */
|
|
82
|
-
export
|
|
83
|
-
if (typeof phase !== "string")
|
|
84
|
-
return "refuse";
|
|
85
|
-
if (phase === "released" || phase === "deployed")
|
|
86
|
-
return "none";
|
|
87
|
-
if (BEFORE_SWAP.includes(phase))
|
|
88
|
-
return "lift-pause";
|
|
89
|
-
if (FORWARD_ONLY.includes(phase))
|
|
90
|
-
return "forward";
|
|
91
|
-
return "roll-back";
|
|
92
|
-
}
|
|
82
|
+
/** The one action a deployment's journal calls for: a row of the one phase table (deploy-phase.ts). */
|
|
83
|
+
export { deployActionOf };
|
|
93
84
|
/** Settle what a stopped deployment left, saying what was done. A refusal stops the release before it does anything. */
|
|
94
85
|
export async function settleDeployment(world, say) {
|
|
95
86
|
const open = world.rescue.journals().filter(one => deployActionOf(one.journal?.["phase"]) !== "none");
|
|
@@ -99,8 +90,14 @@ export async function settleDeployment(world, say) {
|
|
|
99
90
|
return { ok: false, message: `${open.length} deployments are unfinished: ${open.map(one => one.stage).join(", ")}. Nothing was changed.` };
|
|
100
91
|
const { stage, journal } = open[0];
|
|
101
92
|
const action = deployActionOf(journal?.["phase"]);
|
|
102
|
-
if (action === "refuse")
|
|
103
|
-
|
|
93
|
+
if (action === "refuse") {
|
|
94
|
+
// A word this version's table does not hold (a newer candidate's, or damage) is never acted on by itself: the
|
|
95
|
+
// deployment's own recovery knows its own phases, and the person starts it.
|
|
96
|
+
const phase = journal?.["phase"];
|
|
97
|
+
return typeof phase === "string"
|
|
98
|
+
? { ok: false, message: `The deployment in ${stage} stopped at a step this version doesn't know (${/^[a-z-]{1,40}$/.test(phase) ? phase : "not a plain word"}). Nothing was changed. Its own recovery can decide: \`toolroll release --recover-deploy --yes\`.` }
|
|
99
|
+
: { ok: false, message: `The deployment journal in ${stage} can't be read. Nothing was changed.` };
|
|
100
|
+
}
|
|
104
101
|
if (action === "forward") {
|
|
105
102
|
say("Finishing the deployment that was under way.");
|
|
106
103
|
return { ok: true };
|
package/dist/release-view.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { liveReleaseOf } from "./scope.js";
|
|
7
7
|
import { nightlyReleaseLines } from "./nightly-github.js";
|
|
8
8
|
import { releaseAuthorityOfTask, releaseCarries } from "./store.js";
|
|
9
|
+
import { releaseStateAt } from "./release-authority.js";
|
|
9
10
|
export function releaseViewOf(store, taskId, now, obligations = () => []) {
|
|
10
11
|
const found = releaseAuthorityOfTask(store, taskId);
|
|
11
12
|
if (found === null)
|
|
@@ -18,7 +19,7 @@ export function releaseViewOf(store, taskId, now, obligations = () => []) {
|
|
|
18
19
|
return {
|
|
19
20
|
kind: found.carry === null ? "anchor" : "carried",
|
|
20
21
|
authority: a.id, version: a.version, pr: a.pr, state: a.state,
|
|
21
|
-
carrying: a
|
|
22
|
+
carrying: releaseStateAt(a, now) === "open" && live.applies && live.ok, why: live.applies && !live.ok ? live.why : null,
|
|
22
23
|
approver: anchor?.approvedBy ?? null, anchorTask: a.anchorTask, tipTask: a.tipTask, expiresAt: a.expiresAt,
|
|
23
24
|
landed: releaseCarries(store, a.id).flatMap(carry => carry.commits.map(commit => ({ sha: commit.sha, task: carry.taskId, files: commit.files }))),
|
|
24
25
|
obligations: obligations(a.id),
|
package/dist/release.js
CHANGED
|
@@ -667,6 +667,17 @@ export async function runRelease(options, adapters) {
|
|
|
667
667
|
return head !== null && head !== j.candidate.sha ? head : null;
|
|
668
668
|
});
|
|
669
669
|
};
|
|
670
|
+
/**
|
|
671
|
+
* Move the release back to an earlier step: write the fields the caller names (and only those), drop the named steps'
|
|
672
|
+
* intents, set the step, and save the journal. Every step-back goes through here.
|
|
673
|
+
*/
|
|
674
|
+
const rewindTo = (step, fields, dropIntents = []) => {
|
|
675
|
+
Object.assign(j, fields);
|
|
676
|
+
for (const one of dropIntents)
|
|
677
|
+
delete j.intents[one];
|
|
678
|
+
j.step = step;
|
|
679
|
+
save();
|
|
680
|
+
};
|
|
670
681
|
const restart = async (head, updated = false) => {
|
|
671
682
|
// The step this commit was at ends here: the release goes on with the new commit (and, filed again, a new task).
|
|
672
683
|
mark(j.step, "failed", `${j.branch} moved to ${head.slice(0, 7)}`);
|
|
@@ -678,16 +689,10 @@ export async function runRelease(options, adapters) {
|
|
|
678
689
|
const carried = await rel.carry(j.authority.id, head, randomUUID(), epoch);
|
|
679
690
|
if (carried.ok) {
|
|
680
691
|
const view = await rel.authority(carried.taskId);
|
|
692
|
+
// The candidate is written before the gate is read, as it always was: a gate that fails leaves it moved.
|
|
681
693
|
j.candidate = { sha: head, tree };
|
|
682
|
-
|
|
683
|
-
j.
|
|
684
|
-
j.completion = null;
|
|
685
|
-
j.landed = view?.landed ?? j.landed ?? [];
|
|
686
|
-
delete j.intents.check;
|
|
687
|
-
delete j.intents.complete;
|
|
688
|
-
j.step = "check";
|
|
689
|
-
j.stopped = null;
|
|
690
|
-
save();
|
|
694
|
+
const task = { id: carried.taskId, scopeDigest: (await adapters.toolroll.gate(carried.taskId)).scopeDigest };
|
|
695
|
+
rewindTo("check", { task, check: null, completion: null, landed: view?.landed ?? j.landed ?? [], stopped: null }, ["check", "complete"]);
|
|
691
696
|
say(` carried the approval to ${head.slice(0, 7)} (task ${carried.taskId})`);
|
|
692
697
|
return;
|
|
693
698
|
}
|
|
@@ -695,30 +700,16 @@ export async function runRelease(options, adapters) {
|
|
|
695
700
|
}
|
|
696
701
|
else
|
|
697
702
|
say(` ${j.branch} moved to ${head.slice(0, 7)}: filing it for approval`);
|
|
698
|
-
j.candidate = { sha: head, tree };
|
|
699
|
-
j.task = { id: gateTaskId(j.packageVersion, head), scopeDigest: null };
|
|
700
|
-
j.approval = null;
|
|
701
|
-
j.check = null;
|
|
702
|
-
j.completion = null;
|
|
703
|
-
j.authority = null;
|
|
704
|
-
j.auto = null;
|
|
705
703
|
epoch = null;
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
delete j.intents.complete;
|
|
709
|
-
j.step = "gate";
|
|
710
|
-
j.stopped = null;
|
|
711
|
-
save();
|
|
704
|
+
rewindTo("gate", { candidate: { sha: head, tree }, task: { id: gateTaskId(j.packageVersion, head), scopeDigest: null },
|
|
705
|
+
approval: null, check: null, completion: null, authority: null, auto: null, stopped: null }, ["gate", "check", "complete"]);
|
|
712
706
|
};
|
|
713
707
|
/**
|
|
714
708
|
* The automatic road ended without an approval: the plane takes its `auto-check` back (the task asks a person, its
|
|
715
709
|
* passing check kept) and tells the owner; the release waits at approval for a person, as without the setting.
|
|
716
710
|
*/
|
|
717
711
|
const autoRefused = async (why) => {
|
|
718
|
-
|
|
719
|
-
j.check = null;
|
|
720
|
-
j.step = "approval";
|
|
721
|
-
save();
|
|
712
|
+
rewindTo("approval", { auto: { state: "refused", why }, check: null });
|
|
722
713
|
await rel?.auto?.giveUp(j.task.id, why);
|
|
723
714
|
throw new Stop("approval", `Not approved automatically: ${why}. A person approves task ${j.task.id} (\`toolroll task approve ${j.task.id} --digest ${j.task.scopeDigest}\`, or the console), then rerun \`${resume}\`.`);
|
|
724
715
|
};
|
|
@@ -1053,10 +1044,7 @@ export async function runRelease(options, adapters) {
|
|
|
1053
1044
|
}
|
|
1054
1045
|
catch (error) {
|
|
1055
1046
|
if (error instanceof Stop && error.reason === "ci-failed") {
|
|
1056
|
-
|
|
1057
|
-
j.check = null;
|
|
1058
|
-
j.step = "approval";
|
|
1059
|
-
save();
|
|
1047
|
+
rewindTo("approval", { auto: { state: "refused", why: error.message }, check: null });
|
|
1060
1048
|
await rel?.auto?.giveUp(j.task.id, error.message.replace(/\.$/, ""));
|
|
1061
1049
|
}
|
|
1062
1050
|
throw error;
|
package/dist/result-shots.js
CHANGED
|
@@ -169,6 +169,6 @@ export function refuseResultShots(state, store, part, content, app, now) {
|
|
|
169
169
|
const title = chatTitle(store, content.image?.taskId ?? content.task ?? "");
|
|
170
170
|
const line = { text: `Screenshots for ${title} weren't sent: the ${app} app isn't allowed to upload files here.`,
|
|
171
171
|
...(content.task === undefined ? {} : { task: content.task }), ...(content.run === undefined ? {} : { run: content.run }) };
|
|
172
|
-
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE provider=:provider AND id=?").run(savedChatPart(line), now.toISOString(), part.id);
|
|
172
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL,problem=NULL,file=NULL,uploaded=0,uncertain=0,created=? WHERE provider=:provider AND id=? AND state<>'dropped'").run(savedChatPart(line), now.toISOString(), part.id);
|
|
173
173
|
state.prepare("UPDATE chat_part SET state='dropped',problem='Uploads are not allowed here' WHERE provider=:provider AND event=? AND id<>? AND state='pending'").run(part.event, part.id);
|
|
174
174
|
}
|
package/dist/route-results.js
CHANGED
|
@@ -14,6 +14,7 @@ import { INELIGIBLE_REASONS, REASON_WORDS, selectionSchema, } from "./contracts/
|
|
|
14
14
|
import { changedFilesOf, COMPLETION_ACTION } from "./result-completion.js";
|
|
15
15
|
import { runAccounting } from "./store.js";
|
|
16
16
|
import { INFRASTRUCTURE_TERMINALS, saveResultRecord } from "./route-store.js";
|
|
17
|
+
import { wasInterrupted } from "./run-ending.js";
|
|
17
18
|
// The execution class and the cohort snapshot live with their writer (route-store.ts), which store.ts imports: kept
|
|
18
19
|
// light so the store's module graph doesn't reach the evaluator's readers.
|
|
19
20
|
export { cohortOf, EXEC_CLASS_OF_REASON, execClassOf } from "./route-store.js";
|
|
@@ -49,7 +50,7 @@ function classOfRun(run, kept) {
|
|
|
49
50
|
return kept;
|
|
50
51
|
if (run.terminalClass != null && INFRASTRUCTURE_TERMINALS.has(run.terminalClass))
|
|
51
52
|
return "infrastructure";
|
|
52
|
-
if ((run
|
|
53
|
+
if (wasInterrupted(run) && run.providerStartedAt === null)
|
|
53
54
|
return "infrastructure";
|
|
54
55
|
return "model";
|
|
55
56
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a run ended, asked one way.
|
|
3
|
+
*
|
|
4
|
+
* "Interrupted" is stored in two spellings. Every ending written today is the outcome `failed` with the reason
|
|
5
|
+
* `interrupted`. Rows written between schema 25 and 114 carry the outcome `interrupted` itself (a held session cut
|
|
6
|
+
* down mid-flight) and still read. A check that knows only one spelling treats the same event two ways, so every check
|
|
7
|
+
* asks here.
|
|
8
|
+
*/
|
|
9
|
+
export declare function wasInterrupted(run: {
|
|
10
|
+
outcome: string | null;
|
|
11
|
+
reason?: string | null;
|
|
12
|
+
}): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* The spelling written today, and only it. The roads that hand an interrupted attempt's draft to the next attempt
|
|
15
|
+
* (salvage in the checkout, the recovery grant, the check a run never reached) ask this one: they were built for the
|
|
16
|
+
* runs that write it, and reading the old spelling there would widen what they recover.
|
|
17
|
+
*/
|
|
18
|
+
export declare function endedInterrupted(run: {
|
|
19
|
+
outcome: unknown;
|
|
20
|
+
reason?: unknown;
|
|
21
|
+
}): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a run ended, asked one way.
|
|
3
|
+
*
|
|
4
|
+
* "Interrupted" is stored in two spellings. Every ending written today is the outcome `failed` with the reason
|
|
5
|
+
* `interrupted`. Rows written between schema 25 and 114 carry the outcome `interrupted` itself (a held session cut
|
|
6
|
+
* down mid-flight) and still read. A check that knows only one spelling treats the same event two ways, so every check
|
|
7
|
+
* asks here.
|
|
8
|
+
*/
|
|
9
|
+
export function wasInterrupted(run) {
|
|
10
|
+
return run.outcome === "interrupted" || (run.outcome === "failed" && run.reason === "interrupted");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The spelling written today, and only it. The roads that hand an interrupted attempt's draft to the next attempt
|
|
14
|
+
* (salvage in the checkout, the recovery grant, the check a run never reached) ask this one: they were built for the
|
|
15
|
+
* runs that write it, and reading the old spelling there would widen what they recover.
|
|
16
|
+
*/
|
|
17
|
+
export function endedInterrupted(run) {
|
|
18
|
+
return run.outcome === "failed" && run.reason === "interrupted";
|
|
19
|
+
}
|
package/dist/scope.js
CHANGED
|
@@ -1443,7 +1443,7 @@ import { permissionPlainWords } from "./chat-terms.js";
|
|
|
1443
1443
|
import { limitRecognised } from "./exhaustion.js";
|
|
1444
1444
|
import { SUBSCRIPTION_CAPABLE } from "./keys.js";
|
|
1445
1445
|
import { releaseCheckPurposeDigest, releaseTermsDigest } from "./contracts/release-terms.js";
|
|
1446
|
-
import { CHECK_ACCESS_WIDTH, autoEvidenceOf, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseAutoApproves, releaseSealProblem } from "./release-authority.js";
|
|
1446
|
+
import { CHECK_ACCESS_WIDTH, autoEvidenceOf, carriesUnsandboxed, newestReleaseGeneration, releaseAuthorityOfTask, releaseAutoApproves, releaseSealProblem, releaseStateAt } from "./release-authority.js";
|
|
1447
1447
|
/** Whether the command running now is a machine's: the lead (its token) or a remote API run. Trusted context only. */
|
|
1448
1448
|
export const machineActing = () => currentActor()?.lead === true || activeRemote() !== null;
|
|
1449
1449
|
/** A proof row's detail, either form: the legacy text (`<at> · generation <n> · digest <d>`) or the versioned record
|
|
@@ -1558,7 +1558,7 @@ export function liveReleaseOf(store, taskId, action, now = new Date()) {
|
|
|
1558
1558
|
if (a.state === "revoked")
|
|
1559
1559
|
return refuse(`${a.endedBy ?? "an approver"} stopped carrying commits for this release`);
|
|
1560
1560
|
// Expiry stops every live state (security review): an operation already reserved finishes only through reconciliation.
|
|
1561
|
-
if (
|
|
1561
|
+
if (releaseStateAt(a, now) === "expired")
|
|
1562
1562
|
return refuse("the release approval expired");
|
|
1563
1563
|
if (a.state === "merged")
|
|
1564
1564
|
return refuse("the release was already merged");
|
|
@@ -2,6 +2,7 @@ import { assignmentPresentationOf, historicalAssessmentReason, shortenedMaterial
|
|
|
2
2
|
import { assignmentActionHref, assignmentStatusOf, assignmentWithEvidence } from "../assignment-ui.js";
|
|
3
3
|
import { assignmentChecksForRun, owedAcceptance } from "../assignment.js";
|
|
4
4
|
import { releaseCoverageOf } from "../release-coverage.js";
|
|
5
|
+
import { wasInterrupted } from "../run-ending.js";
|
|
5
6
|
import {} from "../browser-workspace.js";
|
|
6
7
|
import { CHECK_LEVEL_WORDS, liveQuickCommand } from "../check-levels.js";
|
|
7
8
|
import { LIMITS } from "../decision.js";
|
|
@@ -942,7 +943,7 @@ export function runOutcomeBadge(run, live) {
|
|
|
942
943
|
if (!live && run.role === "planner" && run.reason === "plan-drafted") {
|
|
943
944
|
return html `<span class="badge">Planned</span>`;
|
|
944
945
|
}
|
|
945
|
-
if (!live && run
|
|
946
|
+
if (!live && wasInterrupted(run))
|
|
946
947
|
return html `<span class="badge">Interrupted</span>`;
|
|
947
948
|
return live
|
|
948
949
|
? html `<span class="badge badge-running">Running</span>`
|
|
@@ -2080,7 +2081,7 @@ export function runOutcomeChip(run, live) {
|
|
|
2080
2081
|
return chip("Running", "live");
|
|
2081
2082
|
if (run.role === "planner" && run.reason === "plan-drafted")
|
|
2082
2083
|
return chip("Planned", "done");
|
|
2083
|
-
if (run
|
|
2084
|
+
if (wasInterrupted(run))
|
|
2084
2085
|
return chip("Interrupted");
|
|
2085
2086
|
const word = sentenceCase(run.outcome ?? "never finished");
|
|
2086
2087
|
return chip(word, run.outcome === "built" || run.outcome === "no-change" ? "done" : run.outcome === "failed" || run.outcome === "refused" ? "failed" : "");
|
|
@@ -3,6 +3,7 @@ import { archivedWords } from "../tidy.js";
|
|
|
3
3
|
import { btn } from "./kit.js";
|
|
4
4
|
import { whenUtc } from "../when-html.js";
|
|
5
5
|
import { networkWords } from "../egress.js";
|
|
6
|
+
import { wasInterrupted } from "../run-ending.js";
|
|
6
7
|
import { chainTermsLines, liveChainFromJson, releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
|
|
7
8
|
import { SUBSCRIPTION_CAPABLE } from "../keys.js";
|
|
8
9
|
import { RELEASE_CI_RUNNING_WORDS } from "../release-ci-hold.js";
|
|
@@ -1862,7 +1863,7 @@ export function taskBodyParts(data) {
|
|
|
1862
1863
|
const workerRow = liveRun !== undefined
|
|
1863
1864
|
? prop("builder", html `${liveRun.runner} · <a href="/r/${liveRun.id}">build #${liveRun.id}</a> running`)
|
|
1864
1865
|
: data.runs[0] !== undefined
|
|
1865
|
-
? prop("last attempt", html `<a href="/r/${data.runs[0].id}">${runNoun(data.runs[0])} #${data.runs[0].id}</a> · ${data.runs[0].role === "planner" && data.runs[0].reason === "plan-drafted" ? "planned" : data.runs[0]
|
|
1866
|
+
? prop("last attempt", html `<a href="/r/${data.runs[0].id}">${runNoun(data.runs[0])} #${data.runs[0].id}</a> · ${data.runs[0].role === "planner" && data.runs[0].reason === "plan-drafted" ? "planned" : wasInterrupted(data.runs[0]) ? "interrupted" : data.runs[0].id === liveHistoryRunId ? "running" : data.runs[0].outcome ?? "never finished"} · ${data.runs[0].runner}`)
|
|
1866
1867
|
: "";
|
|
1867
1868
|
const queueRow = data.position !== null && data.position !== undefined && task.state === "queued"
|
|
1868
1869
|
? prop("queue", html `${data.position.position} of ${data.position.total}${data.position.column === null ? " in the shared queue" : html ` in ${data.position.column}'s queue`} · <a href="/tasks?view=order">reorder</a>`)
|
|
@@ -1885,7 +1886,7 @@ export function taskBodyParts(data) {
|
|
|
1885
1886
|
facts.push({ label: "Builder", parts: [`${liveRun.runner} · `, { label: `build #${liveRun.id}`, href: `/r/${liveRun.id}` }, " running"] });
|
|
1886
1887
|
else if (lastRun !== undefined)
|
|
1887
1888
|
facts.push({ label: "Last attempt", parts: [{ label: `${runNoun(lastRun)} #${lastRun.id}`, href: `/r/${lastRun.id}` },
|
|
1888
|
-
` · ${lastRun.role === "planner" && lastRun.reason === "plan-drafted" ? "planned" : lastRun
|
|
1889
|
+
` · ${lastRun.role === "planner" && lastRun.reason === "plan-drafted" ? "planned" : wasInterrupted(lastRun) ? "interrupted" : lastRun.id === liveHistoryRunId ? "running" : lastRun.outcome ?? "never finished"} · ${lastRun.runner}`] });
|
|
1889
1890
|
if (data.position !== null && data.position !== undefined && task.state === "queued") {
|
|
1890
1891
|
facts.push({ label: "Queue", parts: [`${data.position.position} of ${data.position.total}${data.position.column === null ? " in the shared queue" : ` in ${data.position.column}'s queue`} · `, { label: "Reorder", href: "/tasks?view=order" }] });
|
|
1891
1892
|
}
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,12 +15,12 @@ export declare function detectPreparation(repo: string): {
|
|
|
15
15
|
} | null;
|
|
16
16
|
/** Browser installation may write only this known file inside its project. */
|
|
17
17
|
export declare function previewProjectInstructions(repo: string): {
|
|
18
|
-
content?: never;
|
|
19
18
|
ok: false;
|
|
20
19
|
message: string;
|
|
21
20
|
plan?: never;
|
|
22
21
|
fingerprint?: never;
|
|
23
22
|
installed?: never;
|
|
23
|
+
content?: never;
|
|
24
24
|
} | {
|
|
25
25
|
message?: never;
|
|
26
26
|
ok: true;
|
|
@@ -30,10 +30,10 @@ export declare function previewProjectInstructions(repo: string): {
|
|
|
30
30
|
installed: boolean;
|
|
31
31
|
};
|
|
32
32
|
export declare function addProjectInstructions(repo: string, fingerprint: string): {
|
|
33
|
-
content?: never;
|
|
34
33
|
ok: false;
|
|
35
34
|
message: string;
|
|
36
35
|
plan?: never;
|
|
37
36
|
fingerprint?: never;
|
|
38
37
|
installed?: never;
|
|
38
|
+
content?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
package/dist/slack-chat.js
CHANGED
|
@@ -11,7 +11,7 @@ import { processChatEvent, planChatNotifications, channelAccess, planRoomMessage
|
|
|
11
11
|
import { proposalPreview, proposalLink, proposalOutcomeText, armedCardText, armedYesLabel, } from "./chat-channel.js";
|
|
12
12
|
import { LEAD_MESSAGE_MAX_CHARS } from "./lead.js";
|
|
13
13
|
import { renderReply } from "./reply-shape.js";
|
|
14
|
-
import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
|
|
14
|
+
import { ChatDeliveryError, ChatState, chatHash, chatRetryAt, partContent } from "./chat-delivery-state.js";
|
|
15
15
|
import { readChatPart } from "./contracts/chat-content.js";
|
|
16
16
|
import { readSlackBlockActions, readSlackButton, SLACK_LINK_ACTION } from "./contracts/slack-callback.js";
|
|
17
17
|
import { object, slackId, slackTs, slackMember, SlackError, uploadSlackBytes, } from "./slack-api.js";
|
|
@@ -444,8 +444,7 @@ export async function deliverSlackPart(options) {
|
|
|
444
444
|
? error
|
|
445
445
|
: new SlackError("Slack delivery is waiting to retry", 15_000, true);
|
|
446
446
|
state.setProblem(identity.installation, options.owner, problem.message);
|
|
447
|
-
const until =
|
|
448
|
-
Math.max(problem.retryMs, [5000, 15000, 60000, 300000][Math.min(row.attempts, 3)])).toISOString();
|
|
447
|
+
const until = chatRetryAt(nowOf(options), problem.retryMs, row.attempts);
|
|
449
448
|
state.partFailed(row.id, { problem: problem.message, until, uncertain: problem.uncertain });
|
|
450
449
|
if (problem.code === "ratelimited")
|
|
451
450
|
state.deferUntil(identity.installation, until);
|
package/dist/staged-runtimes.js
CHANGED
|
@@ -12,6 +12,7 @@ import { lstatSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSyn
|
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
15
|
+
import { isDeployed } from "./deploy-phase.js";
|
|
15
16
|
import { quarantine, quarantineEntries, claimQuarantine, releaseQuarantine, restoreQuarantine, deleteQuarantineSync } from "./sweep-quarantine.js";
|
|
16
17
|
import { processCensus } from "./run-closeout.js";
|
|
17
18
|
const DAY_MS = 86_400_000;
|
|
@@ -84,7 +85,7 @@ function readStage(path, name, fallbackAt) {
|
|
|
84
85
|
at = 0;
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
|
-
const finished = deploy !== null ? deploy["phase"]
|
|
88
|
+
const finished = deploy !== null ? isDeployed(deploy["phase"]) : update !== null ? typeof update["finishedAt"] === "string" : false;
|
|
88
89
|
const from = update?.["from"];
|
|
89
90
|
const prior = typeof deploy?.["priorRuntime"] === "string" ? deploy["priorRuntime"] : typeof from?.dist === "string" ? from.dist : null;
|
|
90
91
|
let keptAside = false;
|