toolroll 0.9.75 → 0.9.77
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 +241 -0
- package/README.md +11 -4
- package/THIRD_PARTY_NOTICES.md +5 -5
- package/dist/agent-fence.js +36 -8
- package/dist/approval-authority.js +6 -2
- package/dist/approval-rules-ui.d.ts +11 -0
- package/dist/approval-rules-ui.js +13 -1
- package/dist/assignment.js +3 -0
- package/dist/batch-checks.js +3 -1
- package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/builder.d.ts +14 -0
- package/dist/builder.js +141 -42
- package/dist/chat-decide.js +4 -0
- package/dist/check-failure.d.ts +92 -0
- package/dist/check-failure.js +267 -0
- package/dist/check-progress.d.ts +28 -1
- package/dist/check-progress.js +105 -6
- package/dist/check-report.d.ts +52 -6
- package/dist/check-report.js +193 -8
- package/dist/checkout-cleanup.js +2 -2
- package/dist/cli.js +8 -5
- package/dist/coding-handoff.d.ts +1 -0
- package/dist/coding-handoff.js +20 -11
- package/dist/coding-workspace.js +7 -5
- package/dist/commit-extract.d.ts +6 -0
- package/dist/commit-extract.js +63 -8
- package/dist/console-chrome.d.ts +41 -0
- package/dist/console-chrome.js +166 -0
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/release-check.d.ts +12 -0
- package/dist/contracts/release-check.js +17 -0
- package/dist/contracts/verification-receipt.d.ts +20 -2
- package/dist/contracts/verification-receipt.js +15 -3
- package/dist/contracts/verification-view.d.ts +8 -0
- package/dist/controller-supervisor.d.ts +8 -0
- package/dist/controller-supervisor.js +33 -1
- package/dist/demo.js +3 -2
- package/dist/deploy-gate.d.ts +3 -0
- package/dist/deploy-gate.js +8 -2
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discover.js +6 -1
- package/dist/dispose.d.ts +3 -0
- package/dist/dispose.js +6 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +10 -2
- package/dist/envelope.js +2 -1
- package/dist/evidence.js +4 -1
- package/dist/exec.d.ts +7 -0
- package/dist/exec.js +7 -1
- package/dist/first-run.js +3 -1
- package/dist/git-floor.d.ts +16 -0
- package/dist/git-floor.js +36 -0
- package/dist/git-incident.d.ts +49 -0
- package/dist/git-incident.js +69 -0
- package/dist/git-views.d.ts +20 -0
- package/dist/git-views.js +120 -0
- package/dist/grant.js +7 -1
- package/dist/guides.js +4 -3
- package/dist/operate.d.ts +13 -0
- package/dist/operate.js +779 -319
- package/dist/planner.js +5 -1
- package/dist/prepared-evidence.d.ts +2 -1
- package/dist/prepared-evidence.js +8 -3
- package/dist/project-knowledge.js +6 -2
- package/dist/project-learning.js +23 -3
- package/dist/proof.js +6 -0
- package/dist/pull-request-flow.js +4 -0
- package/dist/read-executor-worker.js +8 -4
- package/dist/read-executor.d.ts +18 -1
- package/dist/read-executor.js +8 -0
- package/dist/release-adapters.d.ts +71 -2
- package/dist/release-adapters.js +471 -12
- package/dist/release-authority.d.ts +46 -0
- package/dist/release-authority.js +68 -0
- package/dist/release-auto.d.ts +184 -0
- package/dist/release-auto.js +612 -0
- package/dist/release-card.d.ts +15 -3
- package/dist/release-card.js +25 -9
- package/dist/release-ci-hold.d.ts +41 -0
- package/dist/release-ci-hold.js +53 -0
- package/dist/release-cli.js +77 -14
- package/dist/release-coverage.js +5 -2
- package/dist/release-recheck.d.ts +1 -1
- package/dist/release-recheck.js +6 -1
- package/dist/release.d.ts +207 -6
- package/dist/release.js +495 -20
- package/dist/repository-context.js +4 -6
- package/dist/result-follow-ups.js +3 -1
- package/dist/review-context.js +8 -6
- package/dist/runner.js +2 -1
- package/dist/sandbox-runtime.d.ts +8 -0
- package/dist/sandbox-runtime.js +14 -2
- package/dist/scope.d.ts +31 -2
- package/dist/scope.js +51 -4
- package/dist/scout.js +5 -1
- package/dist/secret-mint.d.ts +1 -0
- package/dist/secret-mint.js +14 -0
- package/dist/serve.js +101 -51
- package/dist/server/chat.js +5 -5
- package/dist/server/render-tasks.d.ts +12 -0
- package/dist/server/render-tasks.js +25 -14
- package/dist/server/request-context.d.ts +6 -0
- package/dist/server/route-table.js +3 -0
- package/dist/server/runtime.d.ts +2 -0
- package/dist/server/settings.js +29 -2
- package/dist/server/task-views.js +15 -2
- package/dist/setup-guide.d.ts +2 -2
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +28 -1
- package/dist/store.d.ts +11 -1
- package/dist/store.js +44 -5
- package/dist/task-base.d.ts +4 -0
- package/dist/task-base.js +22 -2
- package/dist/teardown-git.d.ts +90 -13
- package/dist/teardown-git.js +1027 -129
- package/dist/test-infra.d.ts +26 -0
- package/dist/test-infra.js +321 -0
- package/dist/up-processes.d.ts +137 -0
- package/dist/up-processes.js +165 -0
- package/dist/up-role-child.d.ts +1 -0
- package/dist/up-role-child.js +77 -0
- package/dist/verification-evidence.d.ts +1 -0
- package/dist/verification-evidence.js +13 -1
- package/dist/worktree.d.ts +4 -0
- package/dist/worktree.js +13 -7
- package/package.json +2 -1
package/dist/server/chat.js
CHANGED
|
@@ -44,7 +44,7 @@ import {} from "./render-pages.js";
|
|
|
44
44
|
import { completionForm, hardBlockerOf, homePhaseWords, owedAcceptanceOf, resultPanelHtml } from "./render-results.js";
|
|
45
45
|
import { browserCaller, oneLineUa } from "./session.js";
|
|
46
46
|
export function createChatHandlers(runtime) {
|
|
47
|
-
const { firstRunStepsNow, managedRepos, leadWords, store, options, phoneSetup, visible, liveTurns, leadSaid, evidenceRoot, clock, sessions, CHAT_CANDIDATE_TTL_MS, chatFetcher, CHAT_CANDIDATES_PER_APPROVER, chatCeilingDigest, workAccess, familyOf, firstTasks, leadPrincipal, taskChatFocus, chatScopeOf, leadConversationRows, demoLeadHere, sendScreen, chromeFor, teamBrowserReply, team, teamChatProvider, runVisible, runIsLive, resultDetailOf, pullRequestTargetOf, chatEnablement, startLeadConversation, projectFamilyPeek, needsYouBadge, liveRefreshSeconds, chatKeyFor, chatCatalog, chatCatalogKnown, providerHome, mintApprovalNonce, checkLocalAgents, agentSignInCommand, bustBadge, authenticateApprover, revisionDestination, armTaskResume, consumeApprovalNonce } = runtime;
|
|
47
|
+
const { firstRunStepsNow, managedRepos, leadWords, store, options, phoneSetup, visible, liveTurns, leadSaid, evidenceRoot, clock, sessions, CHAT_CANDIDATE_TTL_MS, chatFetcher, CHAT_CANDIDATES_PER_APPROVER, chatCeilingDigest, workAccess, familyOf, firstTasks, leadPrincipal, taskChatFocus, chatScopeOf, leadConversationRows, demoLeadHere, sendScreen, chromeFor, teamBrowserReply, team, teamChatProvider, runVisible, runIsLive, resultDetailOf, pullRequestTargetOf, chatEnablement, sharedIndexPage, startLeadConversation, projectFamilyPeek, needsYouBadge, liveRefreshSeconds, chatKeyFor, chatCatalog, chatCatalogKnown, providerHome, mintApprovalNonce, checkLocalAgents, agentSignInCommand, bustBadge, authenticateApprover, revisionDestination, armTaskResume, consumeApprovalNonce } = runtime;
|
|
48
48
|
// The read-only refresh (package 2): the same JSON status as before —
|
|
49
49
|
// session binding, the send receipt, the live turn — plus a version
|
|
50
50
|
// of the DISPLAYED facts and, only when the caller's version differs,
|
|
@@ -273,7 +273,7 @@ export function createChatHandlers(runtime) {
|
|
|
273
273
|
if (repos.length > 0) {
|
|
274
274
|
try {
|
|
275
275
|
fleetSnapshot = store.chatSnapshot(repos, now);
|
|
276
|
-
const summaries =
|
|
276
|
+
const summaries = sharedIndexPage({ principal: 'operator', repos, includeUnplaced: false, viewer: who.name }, { limit: 100, ...(evidenceRoot === undefined ? {} : { root: evidenceRoot }) }).items;
|
|
277
277
|
fleetSnapshot.assignmentStates = Object.fromEntries(summaries.flatMap(value => [value.rootId, value.activeTaskId].map(id => [id, { state: value.assignmentState, label: value.status.label, detail: value.status.detail }])));
|
|
278
278
|
fleetSnapshot.attentionCount = needsYouBadge(null);
|
|
279
279
|
}
|
|
@@ -1452,9 +1452,9 @@ export function createChatHandlers(runtime) {
|
|
|
1452
1452
|
agent: `${providerName(run.provider)} on ${run.runner}`, phase: homePhaseWords(run), project: run.repo === null ? null : projectName(run.repo), since: run.startedAt,
|
|
1453
1453
|
activity: runActivityOf(store, run, now) };
|
|
1454
1454
|
});
|
|
1455
|
-
|
|
1456
|
-
const
|
|
1457
|
-
const
|
|
1455
|
+
// The three whole views, read at most once per workspace revision (and on a read worker first when one is attached).
|
|
1456
|
+
const view = (name) => sharedIndexPage(access, { view: name, limit: WORK_INDEX_MAX_LIMIT, ...(evidenceRoot === undefined ? {} : { root: evidenceRoot }) });
|
|
1457
|
+
const all = view("all"), needs = view("needs-you"), done = view("completed");
|
|
1458
1458
|
const weekAgo = new Date(now.getTime() - 7 * 86_400_000).toISOString();
|
|
1459
1459
|
const counts = [
|
|
1460
1460
|
{ key: "working", label: "Working now", value: agents.length, href: "/work?view=running" },
|
|
@@ -232,6 +232,11 @@ export declare function agentsStripHtml(view: RouteView | null, taskId: string):
|
|
|
232
232
|
/** A ceremony nonce as stored: the sha256 hex of the value the form carried. */
|
|
233
233
|
export declare function nonceHashOf(value: string): string;
|
|
234
234
|
/** The approve POST's refusal, in words a person can act on. */
|
|
235
|
+
/** A release task's automatic approval, in words (docs/plans/auto-approve-green.md). */
|
|
236
|
+
export declare function autoReleaseWords(auto: {
|
|
237
|
+
kind: "checking" | "approved";
|
|
238
|
+
candidate: string;
|
|
239
|
+
}): string;
|
|
235
240
|
/** " (password remembered from 10:42)" when the approval stood on a remembered password or sign-in; nothing otherwise. */
|
|
236
241
|
export declare function approvalProofWords(proof: {
|
|
237
242
|
method: string;
|
|
@@ -523,6 +528,13 @@ export declare function taskBodyParts(data: {
|
|
|
523
528
|
method: string;
|
|
524
529
|
at: string;
|
|
525
530
|
} | null;
|
|
531
|
+
/** A release task the plane approved (or is checking) itself (docs/plans/auto-approve-green.md): its candidate. */
|
|
532
|
+
autoRelease?: {
|
|
533
|
+
kind: "checking" | "approved";
|
|
534
|
+
candidate: string;
|
|
535
|
+
} | null;
|
|
536
|
+
/** A release task filed while its pull request's CI still runs (a person may approve now; nothing deploys before CI passes). */
|
|
537
|
+
releaseCiRunning?: boolean;
|
|
526
538
|
/** One approval per release (release-once.md): the release this task belongs to, and whether this reader may stop it. */
|
|
527
539
|
release?: ReleaseView | null;
|
|
528
540
|
canStopRelease?: boolean;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Task pages: approval, agents, plans, controls, decisions and the task body. */
|
|
2
2
|
import { networkWords } from "../egress.js";
|
|
3
3
|
import { releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
|
|
4
|
+
import { RELEASE_CI_RUNNING_WORDS } from "../release-ci-hold.js";
|
|
4
5
|
import { writesWords } from "../writes-mode.js";
|
|
5
6
|
import { checkAccessWords } from "../check-access.js";
|
|
6
7
|
import { sandboxTermsWords } from "../project-sandbox.js";
|
|
@@ -746,6 +747,11 @@ export function nonceHashOf(value) {
|
|
|
746
747
|
return createHash("sha256").update(value).digest("hex");
|
|
747
748
|
}
|
|
748
749
|
/** The approve POST's refusal, in words a person can act on. */
|
|
750
|
+
/** A release task's automatic approval, in words (docs/plans/auto-approve-green.md). */
|
|
751
|
+
export function autoReleaseWords(auto) {
|
|
752
|
+
return auto.kind === "approved" ? `Approved automatically: CI and release check passed on ${auto.candidate.slice(0, 7)}`
|
|
753
|
+
: `Checking automatically: approves itself once CI and the release check pass on ${auto.candidate.slice(0, 7)}`;
|
|
754
|
+
}
|
|
749
755
|
/** " (password remembered from 10:42)" when the approval stood on a remembered password or sign-in; nothing otherwise. */
|
|
750
756
|
export function approvalProofWords(proof) {
|
|
751
757
|
if (proof == null)
|
|
@@ -1496,12 +1502,14 @@ export function taskBodyParts(data) {
|
|
|
1496
1502
|
scope.touches.length === 0 ? "" : html `<p class="scope-paths"><strong>Touches</strong> ${scope.touches.join(", ")}</p>`,
|
|
1497
1503
|
html `<p><strong>Quality</strong> ${qualityModeTitle(scope.qualityMode ?? "default")}</p>`,
|
|
1498
1504
|
acceptanceCeremonyHtml(scope.acceptance),
|
|
1499
|
-
scope.releaseTerms == null ? "" : html `<p><strong>Release</strong></p><p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>${scope.releaseTerms.policy.carry ? html `<details><summary>What asks again</summary><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul></details>` : ""}`,
|
|
1500
|
-
approval.approved && data.
|
|
1501
|
-
? html `<p class="meta scope-seal"
|
|
1502
|
-
: approval.approved
|
|
1503
|
-
? html `<p class="meta scope-seal">
|
|
1504
|
-
:
|
|
1505
|
+
scope.releaseTerms == null ? "" : html `<p><strong>Release</strong></p><p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>${data.releaseCiRunning === true ? html `<p class="meta">${RELEASE_CI_RUNNING_WORDS}</p>` : ""}${scope.releaseTerms.policy.carry ? html `<details><summary>What asks again</summary><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul></details>` : ""}`,
|
|
1506
|
+
approval.approved && data.autoRelease != null
|
|
1507
|
+
? html `<p class="meta scope-seal">${autoReleaseWords(data.autoRelease)} · <span class="seal">signs ${shortDigest(scope.digest)}</span></p>`
|
|
1508
|
+
: approval.approved && data.release?.kind === "carried"
|
|
1509
|
+
? html `<p class="meta scope-seal">Carried from ${data.release.approver ?? "a person"}'s release approval · <a href="${taskHref(data.release.anchorTask)}">Release ${data.release.version}</a> · <span class="seal">signs ${shortDigest(scope.digest)}</span></p>`
|
|
1510
|
+
: approval.approved
|
|
1511
|
+
? html `<p class="meta scope-seal">Approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)} · <span class="seal">signs ${shortDigest(scope.digest)}</span><span class="so-sr-only"> — approval binds to this exact wording</span></p>`
|
|
1512
|
+
: html `<p class="meta"><span class="seal">signs ${shortDigest(scope.digest)}</span> — approval binds to this exact wording</p><p class="meta">Not approved${approval.reason === "changed" ? " — approved once, then rewritten" : ""}</p>`,
|
|
1505
1513
|
html `</div>`,
|
|
1506
1514
|
], "\n");
|
|
1507
1515
|
// The scout's report (mate arc §10): title, summary, the document inert,
|
|
@@ -1792,11 +1800,13 @@ export function taskBodyParts(data) {
|
|
|
1792
1800
|
: "";
|
|
1793
1801
|
const scopeRow = scope === null
|
|
1794
1802
|
? prop("scope", "none yet")
|
|
1795
|
-
: approval.approved && data.
|
|
1796
|
-
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> ·
|
|
1797
|
-
: approval.approved
|
|
1798
|
-
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${
|
|
1799
|
-
:
|
|
1803
|
+
: approval.approved && data.autoRelease != null
|
|
1804
|
+
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${autoReleaseWords(data.autoRelease)}`)
|
|
1805
|
+
: approval.approved && data.release?.kind === "carried"
|
|
1806
|
+
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · carried from ${data.release.approver ?? "a person"}'s release approval · <a href="${taskHref(data.release.anchorTask)}">Release ${data.release.version}</a>`)
|
|
1807
|
+
: approval.approved
|
|
1808
|
+
? prop("approved scope", html `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)} · ${whenTime(approval.at)}`)
|
|
1809
|
+
: prop("scope", approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved");
|
|
1800
1810
|
const strikesRow = data.strikes > 0 ? prop("strikes", `${data.strikes} failed attempt(s)`) : "";
|
|
1801
1811
|
const propsCard = html `<div class="card props">${workerRow}${queueRow}${scopeRow}${publishesRow}${economics}${strikesRow}</div>`;
|
|
1802
1812
|
// The same facts for the rebuilt page, row for row.
|
|
@@ -1811,9 +1821,10 @@ export function taskBodyParts(data) {
|
|
|
1811
1821
|
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: "/board?view=order" }] });
|
|
1812
1822
|
}
|
|
1813
1823
|
facts.push(scope === null ? { label: "Scope", parts: ["none yet"] }
|
|
1814
|
-
: approval.approved && data.
|
|
1815
|
-
: approval.approved ? { label: "Approved scope", parts: [
|
|
1816
|
-
: { label: "
|
|
1824
|
+
: approval.approved && data.autoRelease != null ? { label: "Approved scope", parts: [autoReleaseWords(data.autoRelease)] }
|
|
1825
|
+
: approval.approved && data.release?.kind === "carried" ? { label: "Approved scope", parts: [`Carried from ${data.release.approver ?? "a person"}'s release approval · `, { label: `Release ${data.release.version}`, href: taskHref(data.release.anchorTask) }] }
|
|
1826
|
+
: approval.approved ? { label: "Approved scope", parts: [{ seal: `signs ${scope.digest.length <= 12 ? scope.digest : `${scope.digest.slice(0, 12)}…`}` }, ` · ${qualityModeTitle(scope.qualityMode ?? "default")} · approved by ${approval.by}${approvalProofWords(data.approvalProof)}`, ...(approval.at === null ? [] : [" · ", { at: approval.at }])] }
|
|
1827
|
+
: { label: "Scope", parts: [approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved"] });
|
|
1817
1828
|
if (data.release?.kind === "anchor")
|
|
1818
1829
|
facts.push({ label: "Release", parts: [`Approved for release ${data.release.version}`, ...(data.release.landed.length === 0 ? [] : [` · later commits: ${data.release.landed.map(one => one.sha.slice(0, 7)).join(", ")}`]), ...(data.release.carrying ? [] : [` · later commits ask again`])] });
|
|
1819
1830
|
facts.push({ label: "Publishes as", parts: [publishesAs] });
|
|
@@ -20,12 +20,18 @@ export type RequestFacts = {
|
|
|
20
20
|
createdTask?: string;
|
|
21
21
|
browser?: boolean;
|
|
22
22
|
workspaceRead?: boolean;
|
|
23
|
+
/** Receipt reconciliation, task and result reads (`request`, `task`, `result`): work is read afresh, never cached. */
|
|
24
|
+
freshWork?: boolean;
|
|
25
|
+
/** A workspace re-read's If-None-Match: the content validator it holds from its last read. */
|
|
26
|
+
workspaceIfNoneMatch?: string;
|
|
23
27
|
workspaceRequest?: string | null;
|
|
24
28
|
workCounts?: ReturnType<typeof workCountsByProject>;
|
|
25
29
|
workCrew?: {
|
|
26
30
|
project: string | null;
|
|
27
31
|
page: WorkIndexPage;
|
|
28
32
|
};
|
|
33
|
+
/** The chrome this request already read (console-chrome.ts keys): one snapshot per request, however often it asks. */
|
|
34
|
+
chromeReads?: Map<string, unknown>;
|
|
29
35
|
lens?: string | null;
|
|
30
36
|
workspaceValidator?: {
|
|
31
37
|
key: string;
|
|
@@ -303,6 +303,9 @@ post("tasks", "sweep.cancel-rest", "^\\/sweeps\\/[1-9][0-9]{0,9}\\/cancel-rest$"
|
|
|
303
303
|
// (the handler proves it as a credential act); turning it off is one click, so the row itself is not a step-up.
|
|
304
304
|
get("settings", "settings.passwordless", "/settings/passwordless", { ...free, ...operator("An instance operator sets Passwordless.", "/settings") });
|
|
305
305
|
post("settings", "settings.passwordless-send", "/settings/passwordless", { callers: ["cookie"], ...operator("An instance operator sets Passwordless.", "/settings") });
|
|
306
|
+
// Release approval (docs/plans/auto-approve-green.md): an approver of the project, from a browser. Approving releases
|
|
307
|
+
// automatically takes a typed password (the handler proves it as a credential act); asking again is one click.
|
|
308
|
+
post("settings", "settings.release-approval-send", "/settings/release-approval", { callers: ["cookie"], ...approver("An approver sets how releases are approved.", "/settings/approval") });
|
|
306
309
|
post("pages", "ledger.checkpoint", "/ledger/checkpoint", operator("An instance operator makes ledger checkpoints.", "/ledger"));
|
|
307
310
|
const SETTINGS_ACT_ROLES = {
|
|
308
311
|
policy: operator("An instance operator sets the policy.", "/settings/policy"),
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -153,6 +153,8 @@ export interface ServerRuntime {
|
|
|
153
153
|
saturated: boolean;
|
|
154
154
|
};
|
|
155
155
|
liveRefreshSeconds: () => number;
|
|
156
|
+
/** One work index page for an already-admitted reader, read at most once per workspace revision (console-chrome.ts). */
|
|
157
|
+
sharedIndexPage: (access: import("../work-summary.js").WorkSummaryAccess, options: import("../work-index.js").WorkIndexOptions) => import("../work-index.js").WorkIndexPage;
|
|
156
158
|
chatKeyFor: (provider: DirectChatProviderId) => {
|
|
157
159
|
key: string;
|
|
158
160
|
source: "environment" | "stored";
|
package/dist/server/settings.js
CHANGED
|
@@ -39,7 +39,8 @@ import { browserCaller } from "./session.js";
|
|
|
39
39
|
import { DEFAULT_ACCENT, normalHex } from "../accent-colors.js";
|
|
40
40
|
import { resolvePhaseAgent } from "../agentconfig.js";
|
|
41
41
|
import { parseProtectedPaths } from "../approval-policy.js";
|
|
42
|
-
import { approvalRulesHtml } from "../approval-rules-ui.js";
|
|
42
|
+
import { approvalRulesHtml, releaseApprovalHtml } from "../approval-rules-ui.js";
|
|
43
|
+
import { releaseApprovalSetting, setReleaseApproval } from "../release-auto.js";
|
|
43
44
|
import { backupHtml } from "../backup-ui.js";
|
|
44
45
|
import { BACKUP_EVERY_HOURS, backupFolderOf, backupNow, checkBackupFolder, defaultBackupFolder, MAX_KEEP } from "../backup.js";
|
|
45
46
|
import { projectBatchChecks, setProjectBatchChecks } from '../batch-policy.js';
|
|
@@ -366,7 +367,8 @@ ${connectionHtml(asked, connection, task)}${command === null ? "" : html `<p>On
|
|
|
366
367
|
return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
|
|
367
368
|
const selector = projects.length > 1 ? html `<form class="approval-project" method="get" action="/settings/approval"><label>Project<select name="repo">${projects.map(p => html `<option value="${p}"${p === chosen ? " selected" : ""}>${projectName(p)}</option>`)}</select></label><button>Show</button></form>` : "";
|
|
368
369
|
const approvers = chosen === "" ? 0 : store.listApprovers().filter(one => { const account = store.accountOf(one.name); return account !== null && account.revokedAt === null && account.role === "approver" && store.accountCanAccess(one.name, chosen); }).length;
|
|
369
|
-
const
|
|
370
|
+
const release = chosen === "" ? null : releaseApprovalSetting(store, chosen);
|
|
371
|
+
const content = chosen === "" ? html `<p>Add a project to set its approval rules.</p>` : html `${approvalRulesHtml({ repo: chosen, name: projectName(chosen), rules: store.approvalRules(chosen), canChange: who.via === "cookie" && store.isInstanceOperator(who.name), approvers }, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}${releaseApprovalHtml({ repo: chosen, auto: release.auto, setBy: release.setBy, at: release.at, canChange: who.via === "cookie" && who.role === "approver" && !store.isDemo() && store.accountCanAccess(who.name, chosen) })}`;
|
|
370
372
|
return sendScreen(response, 200, screen("Approval rules", html `<p><a href="/settings">Settings</a></p><h1>Approval rules</h1>${selector}${content}`, { chrome: chromeFor(chosen || project, "settings") }));
|
|
371
373
|
}
|
|
372
374
|
// Sprint 8: Settings → Policy, the organisation policy and its history. Anyone signed in reads it; an instance operator changes it.
|
|
@@ -2538,6 +2540,7 @@ ${modeWords(terms)
|
|
|
2538
2540
|
"settings.sign-in": signInPage,
|
|
2539
2541
|
"settings.passwordless": passwordlessPage,
|
|
2540
2542
|
"settings.passwordless-send": passwordlessSave,
|
|
2543
|
+
"settings.release-approval-send": releaseApprovalSave,
|
|
2541
2544
|
"settings.lead": leadPage,
|
|
2542
2545
|
"settings.teams": teamsPage,
|
|
2543
2546
|
"settings.discord": discordPage,
|
|
@@ -2725,5 +2728,29 @@ ${modeWords(terms)
|
|
|
2725
2728
|
return back("problem", "An approver for this project changes its sandbox. Nothing changed.");
|
|
2726
2729
|
return back("said", JSON.stringify(changed.before) === JSON.stringify(changed.after) ? "Nothing changed." : "Saved. Tasks scoped from now on use it; tasks already approved keep what they were approved with.");
|
|
2727
2730
|
}
|
|
2731
|
+
// Release approval (docs/plans/auto-approve-green.md): an approver of the project. Approving automatically is a
|
|
2732
|
+
// credential act (a password typed now, never a session); asking again is one click and takes back automatic seals.
|
|
2733
|
+
async function releaseApprovalSave(ctx) {
|
|
2734
|
+
const { response, now, posted } = ctx;
|
|
2735
|
+
const who = browserCaller(ctx.who);
|
|
2736
|
+
const body = readForm(posted, CONSOLE_FORMS.releaseApproval);
|
|
2737
|
+
const repo = body.get("repo") ?? "";
|
|
2738
|
+
if (!consoleProjects().includes(repo) || !visible(repo))
|
|
2739
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings/approval");
|
|
2740
|
+
const back = sayTo(response, `/settings/approval?repo=${encodeURIComponent(repo)}`);
|
|
2741
|
+
if (who.role !== "approver" || store.isDemo() || !store.accountCanAccess(who.name, repo))
|
|
2742
|
+
return back("problem", "An approver of this project sets how its releases are approved.");
|
|
2743
|
+
const action = body.get("action");
|
|
2744
|
+
if (action === "ask") {
|
|
2745
|
+
const changed = setReleaseApproval(store, repo, false, who.name, now);
|
|
2746
|
+
return back("said", !changed.changed ? "Saved." : `Releases ask you again.${changed.withdrawn.length === 0 ? "" : ` ${changed.withdrawn.length} automatic approval${changed.withdrawn.length === 1 ? " was" : "s were"} taken back.`}`);
|
|
2747
|
+
}
|
|
2748
|
+
if (action !== "auto")
|
|
2749
|
+
return back("problem", "Choose Ask me or Approve automatically when checks pass.");
|
|
2750
|
+
if (!authenticateCredential(who, body.get("password") ?? "", repo).ok)
|
|
2751
|
+
return back("problem", "Enter your Toolroll password to approve releases automatically.");
|
|
2752
|
+
const changed = setReleaseApproval(store, repo, true, who.name, now);
|
|
2753
|
+
return back("said", changed.changed ? "Releases approve themselves once CI and the release check pass." : "Saved.");
|
|
2754
|
+
}
|
|
2728
2755
|
return { registrations, modelsView };
|
|
2729
2756
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { releaseViewOf } from "../release-view.js";
|
|
2
|
+
import { releaseCiRunningOf } from "../release-ci-hold.js";
|
|
2
3
|
import { releaseObligationsOf } from "../release-recheck.js";
|
|
3
4
|
import { approvalProofOf, freshlyApprovable, freshnessFor, withFreshness } from "../scope.js";
|
|
4
5
|
import { liveAttemptOf, steerControlsFor } from "../steer-controls.js";
|
|
5
6
|
import { originOf } from "../task-source.js";
|
|
6
|
-
import { taskBaseOf } from "../task-base.js";
|
|
7
|
+
import { recentGitReads, taskBaseOf } from "../task-base.js";
|
|
7
8
|
import { assignmentPresentationOf, historicalAssessmentReason } from '../assignment-presentation.js';
|
|
8
9
|
import { html, joinHtml } from "../html.js";
|
|
9
10
|
import { assignmentStatusOf, assignmentWithEvidence } from '../assignment-ui.js';
|
|
@@ -53,6 +54,9 @@ import {} from "../store.js";
|
|
|
53
54
|
import { taskControlOf } from "../task-control.js";
|
|
54
55
|
import { lastErrorLineOf, missedRequirementOf, workStatusOf } from "../workspace-ui.js";
|
|
55
56
|
export function createTaskViews(runtime) {
|
|
57
|
+
// The task page's base line reads Git through a frozen view of the project (~0.2-0.5 s on a large repository) and
|
|
58
|
+
// the page renders it twice per view and again on every live refresh: its answers are kept for a moment.
|
|
59
|
+
const baseGit = recentGitReads();
|
|
56
60
|
/** The first task's way to Ready shows until the first Ready result, and stays on the installation's first task. */
|
|
57
61
|
function guided(rootId, now) {
|
|
58
62
|
if (runtime.store.isDemo())
|
|
@@ -491,11 +495,13 @@ export function createTaskViews(runtime) {
|
|
|
491
495
|
return { rows, decisions: decisionsFor(runtime.store, rows), now };
|
|
492
496
|
})(),
|
|
493
497
|
steering: ref === null ? [] : runtime.store.listSteerNotes(ref.id),
|
|
494
|
-
base: taskBaseOf(runtime.store, taskId),
|
|
498
|
+
base: taskBaseOf(runtime.store, taskId, baseGit),
|
|
495
499
|
steeringLive: ref !== null && runtime.store.steeringLiveBuild(ref.id, now) !== null,
|
|
496
500
|
steeringRun: ref === null ? null : runtime.store.steeringLiveBuild(ref.id, now),
|
|
497
501
|
release: ref === null ? null : releaseViewOf(runtime.store, ref.externalId, now, authority => releaseObligationsOf(runtime.store, runtime.evidenceRoot, authority)),
|
|
498
502
|
canStopRelease: who.role === "approver" && who.via === "cookie",
|
|
503
|
+
autoRelease: ref === null ? null : autoReleaseOf(runtime.store.getScope(ref.externalId)),
|
|
504
|
+
releaseCiRunning: ref !== null && releaseCiRunningOf(runtime.store, ref.externalId) !== null,
|
|
499
505
|
approvalProof: (() => {
|
|
500
506
|
// The proof of the approval shown: its approver, its exact sealed digest (approvedDigest === digest when approved).
|
|
501
507
|
const scope = ref === null ? null : runtime.store.getScope(ref.externalId);
|
|
@@ -1139,3 +1145,10 @@ export function createTaskViews(runtime) {
|
|
|
1139
1145
|
}
|
|
1140
1146
|
return { guided, projectFamilyPeek, familiesInView, familyTasksInView, revisionDestination, earlierLiveVersions, familyHistory, freshAssignment, workRowOf, taskListPane, runIsLive, runIsTaskResult, revisionViewOf, taskViewData, routeViewOf, taskChatFocus, pullRequestTargetOf, taskPullRequestOf, failureOf, explainAttempt, runChecksHere, taskScreen, armTaskResume, planViewOf, planContractViewOf, revisionDocOf, revisionLedgerOf, progressOf, resultDetailOf };
|
|
1141
1147
|
}
|
|
1148
|
+
/** A release task the plane approved (or is checking) itself, for the task page (docs/plans/auto-approve-green.md). */
|
|
1149
|
+
function autoReleaseOf(scope) {
|
|
1150
|
+
if (scope === null || scope.approvedDigest !== scope.digest || (scope.candidate ?? null) === null)
|
|
1151
|
+
return null;
|
|
1152
|
+
return scope.approvalBasis === "auto-green" ? { kind: "approved", candidate: scope.candidate }
|
|
1153
|
+
: scope.approvalBasis === "auto-check" ? { kind: "checking", candidate: scope.candidate } : null;
|
|
1154
|
+
}
|
package/dist/setup-guide.d.ts
CHANGED
|
@@ -15,10 +15,10 @@ 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;
|
|
21
|
+
content?: never;
|
|
22
22
|
fingerprint?: never;
|
|
23
23
|
installed?: never;
|
|
24
24
|
} | {
|
|
@@ -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;
|
|
36
|
+
content?: never;
|
|
37
37
|
fingerprint?: never;
|
|
38
38
|
installed?: never;
|
|
39
39
|
} | import("./skills.js").InstallResult;
|
package/dist/storage.d.ts
CHANGED
|
@@ -36,3 +36,8 @@ export declare function storageReport(store: Store, databaseFile: string, checko
|
|
|
36
36
|
lines: StorageLine[];
|
|
37
37
|
};
|
|
38
38
|
export declare function bytesWords(bytes: number): string;
|
|
39
|
+
/** diskBytes without holding the event loop: the same one `du` per 200 paths and the same bounds, awaited. A console
|
|
40
|
+
* measuring its checkouts (tens of GB) used to stall every page for as long as `du` took. */
|
|
41
|
+
export declare function diskBytesAsync(paths: readonly string[], run?: typeof duOnce): Promise<Map<string, number>>;
|
|
42
|
+
/** `du -sk` over a batch, its output whatever it managed (a slow disk or a failure answers what it has, or nothing). */
|
|
43
|
+
declare function duOnce(batch: readonly string[]): Promise<string>;
|
package/dist/storage.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* what slips through: stale test temp folders, leftover processes, extra
|
|
8
8
|
* staged runtimes and dependency installs unused for a week.
|
|
9
9
|
*/
|
|
10
|
-
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
11
11
|
import { lstatSync, readdirSync } from "node:fs";
|
|
12
12
|
import { basename, dirname, join } from "node:path";
|
|
13
13
|
import { DEPS_FOLDER, sharedCopies } from "./shared-deps.js";
|
|
@@ -190,3 +190,30 @@ export function bytesWords(bytes) {
|
|
|
190
190
|
return `${Math.round(bytes / 1024 ** 2)} MB`;
|
|
191
191
|
return `${Math.round(bytes / 1024)} KB`;
|
|
192
192
|
}
|
|
193
|
+
/** diskBytes without holding the event loop: the same one `du` per 200 paths and the same bounds, awaited. A console
|
|
194
|
+
* measuring its checkouts (tens of GB) used to stall every page for as long as `du` took. */
|
|
195
|
+
export async function diskBytesAsync(paths, run = duOnce) {
|
|
196
|
+
const sizes = new Map();
|
|
197
|
+
if (paths.length === 0)
|
|
198
|
+
return sizes;
|
|
199
|
+
if (process.platform !== "win32") {
|
|
200
|
+
for (let at = 0; at < paths.length; at += 200) {
|
|
201
|
+
const batch = paths.slice(at, at + 200);
|
|
202
|
+
for (const line of (await run(batch)).split("\n")) {
|
|
203
|
+
const match = /^(\d+)\t(.+)$/.exec(line);
|
|
204
|
+
if (match !== null && batch.includes(match[2]))
|
|
205
|
+
sizes.set(match[2], Number(match[1]) * 1024);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const path of paths)
|
|
210
|
+
if (!sizes.has(path))
|
|
211
|
+
sizes.set(path, treeBytes(path));
|
|
212
|
+
return sizes;
|
|
213
|
+
}
|
|
214
|
+
/** `du -sk` over a batch, its output whatever it managed (a slow disk or a failure answers what it has, or nothing). */
|
|
215
|
+
function duOnce(batch) {
|
|
216
|
+
return new Promise(resolve => {
|
|
217
|
+
execFile("du", ["-sk", ...batch], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, timeout: 60_000 }, (_error, stdout) => resolve(typeof stdout === "string" ? stdout : ""));
|
|
218
|
+
});
|
|
219
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -2355,7 +2355,7 @@ export declare class Store {
|
|
|
2355
2355
|
* exactly what the signed digest was bound to. Returns false when the
|
|
2356
2356
|
* scope vanished mid-ceremony. */
|
|
2357
2357
|
sealScopeApproval(taskId: string, by: string, now: Date, mutation?: Mutation, basis?: {
|
|
2358
|
-
kind: "mode" | "chat" | "grant" | "schedule";
|
|
2358
|
+
kind: "mode" | "chat" | "grant" | "schedule" | "auto-check" | "auto-green";
|
|
2359
2359
|
modeDigest: string;
|
|
2360
2360
|
/** A chat seal of a deploy gate: the release card it came from (chat-management.md §0a.5). */
|
|
2361
2361
|
card?: "release";
|
|
@@ -7780,3 +7780,13 @@ export type AgentsLiveRun = Run & {
|
|
|
7780
7780
|
* Visibility is the caller's: it filters each by the console's ceiling and the viewer's access.
|
|
7781
7781
|
*/
|
|
7782
7782
|
export declare function agentsLiveRuns(store: Store, endedSince: Date): AgentsLiveRun[];
|
|
7783
|
+
/**
|
|
7784
|
+
* This runner's watches a predecessor left needing recovery (Codex review round 1, finding 2): a watch lease still
|
|
7785
|
+
* live, or one whose holder left a claim open (or a run open under a claim a reaper already released). A replacement
|
|
7786
|
+
* worker takes each over — recovering the holder's incarnation in the same transaction — before it admits new work,
|
|
7787
|
+
* whichever projects its start names (a project being removed when the predecessor died is in none of them).
|
|
7788
|
+
*/
|
|
7789
|
+
export declare function predecessorWatches(store: Store, runner: string, now: Date): {
|
|
7790
|
+
repo: string;
|
|
7791
|
+
owner: string;
|
|
7792
|
+
}[];
|
package/dist/store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mintSecret } from "./secret-mint.js";
|
|
1
2
|
import { WRITES_DEFAULT } from "./writes-mode.js";
|
|
2
3
|
import { checkAccessOf, widerThan } from "./check-access.js";
|
|
3
4
|
import { PROJECT_SANDBOX_DEFAULT, sandboxOf } from "./project-sandbox.js";
|
|
@@ -72,6 +73,7 @@ import { hasDisguisedText, hasForbiddenControls, validateNote } from "./decision
|
|
|
72
73
|
import { parseReviewContext, reviewContextCustodyProblem } from "./review-context.js";
|
|
73
74
|
import { foldReview, manualReviewOnly } from "./proof.js";
|
|
74
75
|
import { liveReleaseOf } from "./scope.js";
|
|
76
|
+
import { releaseCiHoldOf, releaseCiRunningOf } from "./release-ci-hold.js";
|
|
75
77
|
import { APPROVAL_BASES, approvalOf, machineActing, personProof, proofDetailOf, quarantinedAuthor, sealOriginOf, digestOf, canonicalProfileJson, chainFromJson, profileDigestOf, profileFromJson, scopeAuthorityOf, routeParityProblem, parseAcceptanceCriteria, exactAcceptance, exactStringList, exactSafeIntegerOrNull } from "./scope.js";
|
|
76
78
|
import { resolveScopeProfile, resolveRouteCandidates, exactPinOf, routeOfTask, agentChoicesFor } from "./agentconfig.js";
|
|
77
79
|
import { canonicalOverridesJson, canonicalRouteJson, isRiskLevel, isTaskSize, headroomFrom, makesNoPlan, legOf, overridesFromJson, phaseOfRole, recommendRoute, routeDigestOf, routeFromJson, routeProblems, routeStampProblem, ROUTE_ERA, PROVIDER_ID_LIST, } from "./phase-routing.js";
|
|
@@ -6932,6 +6934,10 @@ export class Store {
|
|
|
6932
6934
|
// road it came by.
|
|
6933
6935
|
if (basis === undefined && machineActing())
|
|
6934
6936
|
return false;
|
|
6937
|
+
// A RELEASE WAITING FOR CI (Codex review round 1, finding 4): filed while its pull request's checks still ran,
|
|
6938
|
+
// only the automatic road (which proves CI itself) seals it until the plane read exact-head CI passing.
|
|
6939
|
+
if (basis?.kind !== "auto-check" && basis?.kind !== "auto-green" && releaseCiHoldOf(this, taskId) !== null)
|
|
6940
|
+
return false;
|
|
6935
6941
|
// `carried` is written by the carry transaction alone (carryReleaseApproval), never through a seal.
|
|
6936
6942
|
if (basis?.kind === "carried")
|
|
6937
6943
|
return false;
|
|
@@ -7014,7 +7020,17 @@ export class Store {
|
|
|
7014
7020
|
const deployGate = (typeof working["candidate"] === "string" && working["candidate"] !== "") || working["release_terms_json"] != null;
|
|
7015
7021
|
if (deployGate && basis?.kind === "chat" && !(basis.card === "release" && typeof basis.cardDigest === "string" && /^[0-9a-f]{64}$/.test(basis.cardDigest)))
|
|
7016
7022
|
return false;
|
|
7017
|
-
|
|
7023
|
+
// AUTOMATIC RELEASE SEALS (auto-approve-green.md): only on a release task (a candidate and release terms signed
|
|
7024
|
+
// in), only naming the evidence row the plane wrote for exactly this task, kind, digest and candidate.
|
|
7025
|
+
const automatic = basis?.kind === "auto-check" || basis?.kind === "auto-green";
|
|
7026
|
+
if (automatic) {
|
|
7027
|
+
if (terms.terms === null || typeof working["candidate"] !== "string" || working["candidate"] === "")
|
|
7028
|
+
return false;
|
|
7029
|
+
const evidence = autoEvidenceOf(this, taskId, basis.modeDigest, basis.kind);
|
|
7030
|
+
if (evidence === null || evidence.digest !== String(working["digest"]) || evidence.candidate !== working["candidate"] || evidence.by !== by)
|
|
7031
|
+
return false;
|
|
7032
|
+
}
|
|
7033
|
+
if (terms.terms !== null && basis !== undefined && basis.kind !== "chat" && !automatic)
|
|
7018
7034
|
return false;
|
|
7019
7035
|
if (terms.terms !== null && releaseSealProblem(this, terms.terms) !== null)
|
|
7020
7036
|
return false;
|
|
@@ -7033,7 +7049,8 @@ export class Store {
|
|
|
7033
7049
|
approved_route_json = proposed_route_json
|
|
7034
7050
|
WHERE task_id = ?`)
|
|
7035
7051
|
.run(now.toISOString(), by, basis === undefined ? "password" : basis.kind, basis === undefined ? null : basis.modeDigest, taskId);
|
|
7036
|
-
|
|
7052
|
+
// `auto-check` only lets the check run: it opens no release authority (only `auto-green` and a person's yes do).
|
|
7053
|
+
if (changed.changes > 0 && terms.ok && terms.terms !== null && basis?.kind !== "auto-check") {
|
|
7037
7054
|
if (openReleaseAuthority(this, { taskId, digest: String(working["digest"]), terms: terms.terms, by, now }) === null)
|
|
7038
7055
|
throw new ReleaseSealRefused();
|
|
7039
7056
|
}
|
|
@@ -7066,7 +7083,11 @@ export class Store {
|
|
|
7066
7083
|
? "Approved automatically: its flow runs on its own. A connected worker can take it next."
|
|
7067
7084
|
: basis.kind === "schedule"
|
|
7068
7085
|
? "Approved by its standing order. A connected worker can take it next."
|
|
7069
|
-
:
|
|
7086
|
+
: basis.kind === "auto-check"
|
|
7087
|
+
? "Checking automatically: it approves itself once CI and the release check pass."
|
|
7088
|
+
: basis.kind === "auto-green"
|
|
7089
|
+
? "Approved automatically: CI and the release check passed."
|
|
7090
|
+
: "Approved automatically under the operating mode. A connected worker can take it next.",
|
|
7070
7091
|
link: chatControlHref("task", taskId),
|
|
7071
7092
|
}, now);
|
|
7072
7093
|
}
|
|
@@ -8003,7 +8024,7 @@ export class Store {
|
|
|
8003
8024
|
/** A single-use door into the instance (U2): 128-bit token, sha256
|
|
8004
8025
|
* stored, role pinned at mint, 72 hours. Only the caller's own
|
|
8005
8026
|
* authentication gates this — an approver mints; a viewer cannot. */
|
|
8006
|
-
mintInvite(role, mintedBy, now, token = () =>
|
|
8027
|
+
mintInvite(role, mintedBy, now, token = () => mintSecret(16), projects = null) {
|
|
8007
8028
|
const access = normalizeProjectAccess(projects);
|
|
8008
8029
|
return this.transact(() => {
|
|
8009
8030
|
if (!this.isInstanceOperator(mintedBy))
|
|
@@ -20422,7 +20443,7 @@ export function serializeReleaseTerms(terms) {
|
|
|
20422
20443
|
}
|
|
20423
20444
|
// ---- one approval per release: the authority record (release-once.md, part 1) --------------------------------------
|
|
20424
20445
|
export { LIVE_RELEASE_STATES, belongsToRelease, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseAuthorityOfTask, releaseCarries, releaseCarryTo, releaseOperation, releaseOperations, releaseSealProblem, } from "./release-authority.js";
|
|
20425
|
-
import { LIVE_RELEASE_STATES, belongsToRelease, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseCarryTo, releaseOperation, releaseSealProblem, } from "./release-authority.js";
|
|
20446
|
+
import { LIVE_RELEASE_STATES, autoEvidenceOf, belongsToRelease, newestReleaseGeneration, outstandingReleaseOperations, releaseAuthority, releaseCarryTo, releaseOperation, releaseSealProblem, } from "./release-authority.js";
|
|
20426
20447
|
/**
|
|
20427
20448
|
* Open a release's authority for a person's seal of its terms (inside the seal's transaction): the next generation,
|
|
20428
20449
|
* superseding the live one. False when it can't (releaseSealProblem), which refuses the seal.
|
|
@@ -20492,6 +20513,9 @@ export function reserveRelease(store, input) {
|
|
|
20492
20513
|
const a = releaseAuthority(store, input.authority);
|
|
20493
20514
|
if (a === null)
|
|
20494
20515
|
return { ok: false, why: `No release ${input.authority}.` };
|
|
20516
|
+
// Early approval authorizes the check, never an irreversible operation before the plane confirms exact-head CI.
|
|
20517
|
+
if (releaseCiRunningOf(store, input.taskId) !== null)
|
|
20518
|
+
return { ok: false, why: "This release still waits for GitHub CI. Resume it with `toolroll release`." };
|
|
20495
20519
|
const at = input.now.toISOString();
|
|
20496
20520
|
// Expiry stops every live state; an operation already reserved settles only through reconciliation.
|
|
20497
20521
|
if (a.expiresAt <= at)
|
|
@@ -20533,6 +20557,8 @@ export function runReleaseOperation(store, id, epoch, now, issuer = currentRelea
|
|
|
20533
20557
|
const a = op === null ? null : releaseAuthority(store, op.authority);
|
|
20534
20558
|
if (op === null || a === null || op.state !== "reserved" || op.epoch !== epoch || a.leaseEpoch !== epoch)
|
|
20535
20559
|
return false;
|
|
20560
|
+
if (releaseCiRunningOf(store, op.taskId) !== null)
|
|
20561
|
+
return false;
|
|
20536
20562
|
// Past the expiry nothing new starts; the reserved operation is settled by reconciliation instead.
|
|
20537
20563
|
if (a.expiresAt <= now.toISOString())
|
|
20538
20564
|
return false;
|
|
@@ -20843,3 +20869,16 @@ export function agentsLiveRuns(store, endedSince) {
|
|
|
20843
20869
|
repo: row["live_task_repo"] === null || row["live_task_repo"] === undefined ? null : String(row["live_task_repo"]),
|
|
20844
20870
|
}));
|
|
20845
20871
|
}
|
|
20872
|
+
/**
|
|
20873
|
+
* This runner's watches a predecessor left needing recovery (Codex review round 1, finding 2): a watch lease still
|
|
20874
|
+
* live, or one whose holder left a claim open (or a run open under a claim a reaper already released). A replacement
|
|
20875
|
+
* worker takes each over — recovering the holder's incarnation in the same transaction — before it admits new work,
|
|
20876
|
+
* whichever projects its start names (a project being removed when the predecessor died is in none of them).
|
|
20877
|
+
*/
|
|
20878
|
+
export function predecessorWatches(store, runner, now) {
|
|
20879
|
+
return store.handle.prepare(`SELECT w.repo AS repo, w.owner AS owner FROM watch_lease w
|
|
20880
|
+
WHERE w.runner = ? AND (w.expires_at > ? OR EXISTS (
|
|
20881
|
+
SELECT 1 FROM claim c WHERE c.runner = w.runner AND c.incarnation = w.owner
|
|
20882
|
+
AND (c.released_at IS NULL OR EXISTS (SELECT 1 FROM run r WHERE r.lease_id = c.lease_id AND r.outcome IS NULL))))
|
|
20883
|
+
ORDER BY w.repo`).all(runner, now.toISOString()).map(row => ({ repo: String(row["repo"]), owner: String(row["owner"]) }));
|
|
20884
|
+
}
|
package/dist/task-base.d.ts
CHANGED
|
@@ -33,3 +33,7 @@ export declare function freshBaseFor(store: Store, taskId: string, branch: strin
|
|
|
33
33
|
};
|
|
34
34
|
} | Record<string, never>;
|
|
35
35
|
export declare function taskBaseOf(store: Store, taskId: string, git?: GitRead): TaskBaseView | null;
|
|
36
|
+
/** `git`'s answers kept for `ttlMs` (at most `entries` of them), for a page that shows the base line on every refresh:
|
|
37
|
+
* each read still goes through the same frozen view when it runs; only its answer is reused for a moment. Display only —
|
|
38
|
+
* never for a decision. */
|
|
39
|
+
export declare function recentGitReads(git?: GitRead, ttlMs?: number, entries?: number, now?: () => number): GitRead;
|
package/dist/task-base.js
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* branch's pinned origin, or the newest plan's base — and the behind count is
|
|
5
5
|
* read from Git now. Reads only.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { viewedGitSync } from "./teardown-git.js";
|
|
8
8
|
export const readGit = (cwd, args) => {
|
|
9
9
|
try {
|
|
10
|
-
|
|
10
|
+
// Every git version through a frozen read view, bounded (eval-harness review 1, finding 7): a partial clone, an
|
|
11
|
+
// include or a command-naming key is refused from the config git then runs with.
|
|
12
|
+
const ran = viewedGitSync(cwd, args, { timeoutMs: 5_000, maxBuffer: 65_536 });
|
|
13
|
+
return ran.status === 0 ? ran.stdout.trim() : null;
|
|
11
14
|
}
|
|
12
15
|
catch {
|
|
13
16
|
return null;
|
|
@@ -49,3 +52,20 @@ export function taskBaseOf(store, taskId, git = readGit) {
|
|
|
49
52
|
const view = { base, branch, main: behind === null ? null : main, behind };
|
|
50
53
|
return { ...view, words: `Base ${baseStandingWords(view)}` };
|
|
51
54
|
}
|
|
55
|
+
/** `git`'s answers kept for `ttlMs` (at most `entries` of them), for a page that shows the base line on every refresh:
|
|
56
|
+
* each read still goes through the same frozen view when it runs; only its answer is reused for a moment. Display only —
|
|
57
|
+
* never for a decision. */
|
|
58
|
+
export function recentGitReads(git = readGit, ttlMs = 15_000, entries = 64, now = Date.now) {
|
|
59
|
+
const kept = new Map();
|
|
60
|
+
return (cwd, args) => {
|
|
61
|
+
const key = JSON.stringify([cwd, args]), at = now(), hit = kept.get(key);
|
|
62
|
+
if (hit !== undefined && at - hit.at < ttlMs && at >= hit.at)
|
|
63
|
+
return hit.value;
|
|
64
|
+
const value = git(cwd, args);
|
|
65
|
+
kept.delete(key);
|
|
66
|
+
if (kept.size >= entries)
|
|
67
|
+
kept.delete(kept.keys().next().value);
|
|
68
|
+
kept.set(key, { at, value });
|
|
69
|
+
return value;
|
|
70
|
+
};
|
|
71
|
+
}
|