toolroll 0.9.92 → 0.9.93
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 +19 -0
- package/dist/assignment-status.d.ts +1 -1
- package/dist/assignment-ui.js +2 -2
- package/dist/assignment.js +11 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +30 -30
- package/dist/browser-crew.js +2 -2
- package/dist/browser-workspace.d.ts +3 -5
- package/dist/builder.js +21 -2
- package/dist/check-facts.d.ts +124 -0
- package/dist/check-facts.js +272 -0
- package/dist/check-report.d.ts +5 -0
- package/dist/check-report.js +4 -3
- package/dist/cli.js +5 -0
- package/dist/contracts/console-api.d.ts +2 -2
- package/dist/contracts/console-api.js +2 -2
- package/dist/contracts/release-check.d.ts +25 -1
- package/dist/contracts/release-check.js +19 -0
- package/dist/desktop-update.d.ts +2 -2
- package/dist/desktop-update.js +13 -9
- package/dist/eval-hooks.d.ts +55 -0
- package/dist/eval-hooks.js +227 -4
- package/dist/flaky.d.ts +17 -0
- package/dist/flaky.js +9 -0
- package/dist/needs-you.d.ts +7 -7
- package/dist/needs-you.js +12 -10
- package/dist/operate.js +138 -18
- package/dist/project-delete.js +5 -1
- package/dist/project-knowledge.d.ts +6 -6
- package/dist/push.d.ts +10 -3
- package/dist/push.js +22 -6
- package/dist/release-adapters.js +11 -1
- package/dist/release-approval.d.ts +191 -0
- package/dist/release-approval.js +382 -0
- package/dist/release-auto.js +13 -4
- package/dist/release-card.d.ts +73 -4
- package/dist/release-card.js +168 -23
- package/dist/release-cli.js +9 -1
- package/dist/release.d.ts +30 -0
- package/dist/release.js +42 -3
- package/dist/result-acts.d.ts +3 -3
- package/dist/result-acts.js +1 -1
- package/dist/server/guards.js +1 -0
- package/dist/server/pages-assets.js +1 -1
- package/dist/server/render-release.d.ts +17 -0
- package/dist/server/render-release.js +84 -0
- package/dist/server/render-results.d.ts +0 -2
- package/dist/server/render-results.js +4 -6
- package/dist/server/render-tasks.js +3 -3
- package/dist/server/route-table.d.ts +1 -1
- package/dist/server/route-table.js +7 -1
- package/dist/server/tasks.d.ts +15 -0
- package/dist/server/tasks.js +102 -20
- package/dist/setup-guide.d.ts +2 -2
- package/dist/stop-proof.d.ts +67 -0
- package/dist/stop-proof.js +293 -0
- package/dist/store.d.ts +70 -31
- package/dist/store.js +335 -95
- package/dist/surface.js +1 -1
- package/dist/task-outcome-cli.js +19 -0
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +1 -0
- package/dist/work-index.js +7 -9
- package/dist/work-summary.d.ts +2 -2
- package/package.json +1 -1
|
@@ -707,8 +707,6 @@ export declare function needActionOf(assignment: AssignmentSnapshot | null, stat
|
|
|
707
707
|
rebuildable?: boolean;
|
|
708
708
|
refuted?: boolean;
|
|
709
709
|
}): BrowserNeedAction | null;
|
|
710
|
-
/** A build Toolroll can't check at all: the approver ticks that they checked before confirming. */
|
|
711
|
-
export declare const needsCheck: (assignment: AssignmentSnapshot) => boolean;
|
|
712
710
|
/** The server page's form of the same: the need's action first, Request changes as the quiet second. */
|
|
713
711
|
export declare function resultNeedAction(need: BrowserNeedAction, o: ResultPanelOptions, canRequest: boolean): Html;
|
|
714
712
|
/** The outcome in one bounded sentence (repair 2026-09-14): the handoff's
|
|
@@ -731,7 +731,7 @@ export function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab,
|
|
|
731
731
|
checksRunning,
|
|
732
732
|
blocked,
|
|
733
733
|
canRequest: panel.panel.canRequest && panel.panel.request !== null,
|
|
734
|
-
need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "
|
|
734
|
+
need: need === null || need.accept != null ? null : need.rebuild != null ? "rebuild" : need.confirm !== null ? "recheck-stopped" : null,
|
|
735
735
|
next: nextKind === "revise" || nextKind === "draft-repair" ? nextKind : null,
|
|
736
736
|
unanswered: settled === null ? 0 : unanswered.length,
|
|
737
737
|
notRight: 0,
|
|
@@ -2009,12 +2009,10 @@ export function needActionOf(assignment, status, csrf, returnTo, result) {
|
|
|
2009
2009
|
if (status.need.key === "review-result" || here)
|
|
2010
2010
|
return null;
|
|
2011
2011
|
}
|
|
2012
|
-
const confirm = action?.code === "
|
|
2013
|
-
? { action: `${taskHref(assignment.rootId)}/
|
|
2012
|
+
const confirm = action?.code === "recheck-stopped" && action.target.runId !== null && csrf !== ""
|
|
2013
|
+
? { action: `${taskHref(assignment.rootId)}/recheck-stopped`, run: action.target.runId, returnTo } : null;
|
|
2014
2014
|
return { label: status.need.action.label, href: assignmentActionHref(assignment) ?? taskHref(assignment.rootId), confirm };
|
|
2015
2015
|
}
|
|
2016
|
-
/** A build Toolroll can't check at all: the approver ticks that they checked before confirming. */
|
|
2017
|
-
export const needsCheck = (assignment) => assignment.need != null && "key" in assignment.need && assignment.need.key === "check-stopped";
|
|
2018
2016
|
/** The server page's form of the same: the need's action first, Request changes as the quiet second. */
|
|
2019
2017
|
export function resultNeedAction(need, o, canRequest) {
|
|
2020
2018
|
const control = need.accept != null
|
|
@@ -2022,7 +2020,7 @@ export function resultNeedAction(need, o, canRequest) {
|
|
|
2022
2020
|
: need.rebuild != null
|
|
2023
2021
|
? postForm(need.rebuild.action, html `<button type="submit" data-primary-action data-rebuild style="min-height:44px">${need.label}</button>`, { attrs: { class: "rebuild" } })
|
|
2024
2022
|
: need.confirm !== null
|
|
2025
|
-
? postForm(need.confirm.action, html
|
|
2023
|
+
? postForm(need.confirm.action, html `<button type="submit" data-primary-action style="min-height:44px">${need.label}</button>`, { attrs: { id: "recheck-stopped", class: "recheck-stopped" }, returnTo: need.confirm.returnTo, hidden: { run: need.confirm.run } })
|
|
2026
2024
|
: html `<a class="button-link" href="${need.href ?? "#"}" data-primary-action>${need.label}</a>`;
|
|
2027
2025
|
return html `<div class="result-action" data-result-action="need">${control}${canRequest && o.csrf !== "" ? html `<a class="result-feedback-link" href="#request-changes">Request changes</a>` : ""}</div>`;
|
|
2028
2026
|
}
|
|
@@ -50,7 +50,7 @@ import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SEN
|
|
|
50
50
|
import { steerDestinationWords, steerStandingParts } from "../steer-status.js";
|
|
51
51
|
import { reviewHref, taskChatHref, taskHref } from "./http.js";
|
|
52
52
|
import { coordinatorProposalsSection } from "./render-chat.js";
|
|
53
|
-
import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords,
|
|
53
|
+
import { completionReceiptCard, criterionMatrixHtml, evidenceWords, homePhaseWords, incidentWords, pullRequestCardHtml, reasonWords, receiptStatusOf, repairChainHtml, reportItemsHtml, reviewRetryPanel, runNoun, runOutcomeBadge, semanticCoverageHtml, verificationExplanation, verificationRecovered } from "./render-results.js";
|
|
54
54
|
import {} from "./session.js";
|
|
55
55
|
import { linkWords, originNote, requestName, requestWords } from "../task-source.js";
|
|
56
56
|
export function contractChangeHtml(change) {
|
|
@@ -2408,8 +2408,8 @@ export function taskBodyParts(data) {
|
|
|
2408
2408
|
approve: data.archived != null || sheetInput === null || dependencyChoiceNeeded || data.csrf === "" ? null : approveKitOf(sheetInput, data.approvalFresh ?? null),
|
|
2409
2409
|
approved: scope !== null && approval.approved ? approvedKitOf(scope, approval, data.approvalProof ?? null) : null,
|
|
2410
2410
|
more,
|
|
2411
|
-
|
|
2412
|
-
? { action: `${taskHref(data.assignment.rootId)}/
|
|
2411
|
+
recheckStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "recheck-stopped" && data.assignment.primaryAction.target.runId !== null
|
|
2412
|
+
? { action: `${taskHref(data.assignment.rootId)}/recheck-stopped`, run: data.assignment.primaryAction.target.runId } : null,
|
|
2413
2413
|
rebuild,
|
|
2414
2414
|
// The plan, progress and plan changes are thread entries now; the rest still needs a person here.
|
|
2415
2415
|
lead: [
|
|
@@ -22,7 +22,7 @@ export type Caller = "anonymous" | "service" | "cookie" | "bearer" | "oauth" | "
|
|
|
22
22
|
/** What the caller must hold: nothing, read, act (an approver or an `act` token), or a fresh password/provider check on top. */
|
|
23
23
|
export type RouteScope = "none" | "read" | "act" | "step-up";
|
|
24
24
|
/** Where the request's project comes from. */
|
|
25
|
-
export type ProjectResolver = "none" | "session" | "form" | "form-path" | "task" | "run" | "decision" | "incident" | "flow" | "proposal" | "conversation" | "coding" | "adapter";
|
|
25
|
+
export type ProjectResolver = "none" | "session" | "form" | "form-path" | "task" | "task-ref" | "run" | "decision" | "incident" | "flow" | "proposal" | "conversation" | "coding" | "adapter";
|
|
26
26
|
/**
|
|
27
27
|
* What a project-limited account may do here:
|
|
28
28
|
* - `deny`: instance access only.
|
|
@@ -286,6 +286,10 @@ get("tasks", "welcome", "/welcome", { limited: "collection", ...free });
|
|
|
286
286
|
post("tasks", "welcome.sample", "/welcome/sample", { callers: ["cookie"], project: "none", ...approver("An approver adds projects.", "/welcome") });
|
|
287
287
|
get("tasks", "task.page", `^\\/t\\/${TASK}$`, { project: "task", limited: "resource", ...free, sample: "/t/one" });
|
|
288
288
|
get("tasks", "task.evidence", `^\\/t\\/${TASK}\\/evidence$`, { project: "task", limited: "resource", ...free, sample: "/t/one/evidence" });
|
|
289
|
+
// One-tap release approval (trusted-release-reports.md A6/A7): the sheet, and the push's opaque locator, which only
|
|
290
|
+
// redirects to it once the plane's own state says the task ref is a release in the person's projects.
|
|
291
|
+
get("tasks", "task.release", `^\\/t\\/${TASK}\\/release$`, { project: "task", limited: "resource", ...free, sample: "/t/one/release" });
|
|
292
|
+
get("tasks", "release.locate", `^\\/approve\\/${ID}$`, { project: "task-ref", limited: "resource", ...free, sample: "/approve/1" });
|
|
289
293
|
get("pages", "ledger.export", "/ledger/export", { limited: "unscoped", ...free });
|
|
290
294
|
get("tasks", "runs", "/runs", { limited: "collection", ...free }); // a redirect to /agents now
|
|
291
295
|
get("pages", "menu", "/menu", { limited: "collection", ...free });
|
|
@@ -556,7 +560,9 @@ post("tasks", "task.act.route", `^\\/t\\/${TASK}\\/route$`, { project: "task", l
|
|
|
556
560
|
post("tasks", "task.act.retry-review", `^\\/t\\/${TASK}\\/retry-review$`, { project: "task", limited: "resource", sample: "/t/one/retry-review" });
|
|
557
561
|
post("tasks", "task.act.complete", `^\\/t\\/${TASK}\\/complete$`, { project: "task", limited: "resource", sample: "/t/one/complete", ...approver("Only an approver can mark a result complete.") });
|
|
558
562
|
post("tasks", "task.act.merge", `^\\/t\\/${TASK}\\/merge$`, { project: "task", limited: "resource", sample: "/t/one/merge", ...approver("Only an approver can merge.") });
|
|
559
|
-
|
|
563
|
+
// Check again: Toolroll looks at this computer's processes for a finished build once more (stop-proof.ts). Nobody
|
|
564
|
+
// vouches that a build stopped, so no password: it only asks for the look the worker makes on its own anyway.
|
|
565
|
+
post("tasks", "task.act.recheck-stopped", `^\\/t\\/${TASK}\\/recheck-stopped$`, { project: "task", limited: "resource", sample: "/t/one/recheck-stopped", ...approver("Only an approver can have Toolroll check a build again.") });
|
|
560
566
|
post("tasks", "task.act.stop-carrying", `^\\/t\\/${TASK}\\/stop-carrying$`, { project: "task", limited: "resource", sample: "/t/one/stop-carrying", ...approver("Only an approver can stop carrying a release.") });
|
|
561
567
|
post("tasks", "task.act.stop", `^\\/t\\/${TASK}\\/stop$`, { project: "task", limited: "resource", sample: "/t/one/stop" });
|
|
562
568
|
// Putting one task away and bringing it back (task-tidy.md): a person's act, so only a browser session.
|
package/dist/server/tasks.d.ts
CHANGED
|
@@ -64,3 +64,18 @@ export declare function createAgentSignalHandlers(runtime: ServerRuntime): {
|
|
|
64
64
|
"agents.seen": (ctx: HandlerContext) => Promise<void>;
|
|
65
65
|
"agents.watch": (ctx: HandlerContext) => Promise<void>;
|
|
66
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* One-tap release approval (docs/plans/trusted-release-reports.md, slice A): the sheet (`/t/<id>/release`), the push's
|
|
69
|
+
* opaque locator (`/approve/<task ref>`, a GET that only redirects to the sheet) and the sheet's approval, which the
|
|
70
|
+
* task approval POST hands here once its shared checks (cookie session, CSRF and the browser key at admission, the
|
|
71
|
+
* single-use nonce, a password or a standing proof) passed. The approval itself is the shared card wrapper around the
|
|
72
|
+
* unchanged approve() (release-approval.ts approveReleaseCard).
|
|
73
|
+
*/
|
|
74
|
+
export declare function createReleaseSheetHandlers(runtime: ServerRuntime): {
|
|
75
|
+
routes: {
|
|
76
|
+
"task.release": (ctx: HandlerContext) => Promise<void>;
|
|
77
|
+
"release.locate": (ctx: HandlerContext) => Promise<void>;
|
|
78
|
+
};
|
|
79
|
+
sheetScreen: (ctx: Pick<HandlerContext, "who" | "response" | "project">, taskId: string, problem: string | null, status: number) => void;
|
|
80
|
+
approveFromSheet: (ctx: Pick<HandlerContext, "who" | "response" | "project">, taskId: string, cardDigest: string, token: string) => void;
|
|
81
|
+
};
|
package/dist/server/tasks.js
CHANGED
|
@@ -44,6 +44,7 @@ import { validKeyTail } from "../live-records.js";
|
|
|
44
44
|
import { runNotes } from "../live-notes.js";
|
|
45
45
|
import { latestFinishedAttempt, retryNoteOf } from '../needs-you.js';
|
|
46
46
|
import { REMOTE_MESSAGES } from "../operate-remote.js";
|
|
47
|
+
import { proveRunStopped } from "../stop-proof.js";
|
|
47
48
|
import { aggregateNewNames, observeWorktree, parseBaseTreeSnapshot, PEEK_LIMITS } from "../peek.js";
|
|
48
49
|
import { isTaskSize, PHASES as ROUTE_PHASES } from "../phase-routing.js";
|
|
49
50
|
import { applyModeToNewFiling, authorizePlanUnderMode } from "../plan-auto.js";
|
|
@@ -59,7 +60,7 @@ import { structuredHandoffView, terminalDiffView } from "../result-evidence-read
|
|
|
59
60
|
import { fileAddTestsTask, requestFollowUpChecks } from '../result-follow-ups.js';
|
|
60
61
|
import { commentSourceKey, isRevisionFeedback, parseResultTab, resultReturnTarget } from "../result-review.js";
|
|
61
62
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
62
|
-
import { passwordlessSessionFor, acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, freshnessFor, localApprovalEligible, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, modeFilingCoverage, PLACEHOLDER_RUBRIC, proposeGuarded } from "../scope.js";
|
|
63
|
+
import { passwordlessSessionFor, acceptanceLinesToInput, acceptanceToLines, approvalOf, approve as approveScope, freshnessFor, freshlyApprovable, localApprovalEligible, spendingOnlyOnSuccess, ssoNowAvailable, withFreshness, modeFilingCoverage, PLACEHOLDER_RUBRIC, proposeGuarded } from "../scope.js";
|
|
63
64
|
import { subagentNames as subagentNamesOf } from "../spend.js";
|
|
64
65
|
import { verifiedAuthor } from "../store.js";
|
|
65
66
|
import { runCostWords } from "../summary.js";
|
|
@@ -78,6 +79,10 @@ import { timingFor } from "../latency-timeline.js";
|
|
|
78
79
|
import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, withRefusal } from "./render-results.js";
|
|
79
80
|
import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
|
|
80
81
|
import {} from "./session.js";
|
|
82
|
+
import { approveReleaseCard, releaseApprovalCard, releaseSheetHref, releaseTaskOf } from "../release-approval.js";
|
|
83
|
+
import { releaseSheetPage } from "./render-release.js";
|
|
84
|
+
import { approvalFactsNow } from "./kit.js";
|
|
85
|
+
import { localOwnerRefusal } from "../local-owner.js";
|
|
81
86
|
export function createTasksHandlers(runtime) {
|
|
82
87
|
const { identities, store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, leadPrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
|
|
83
88
|
/** A row admitted the address but its path parameter does not parse: the console's unknown page. */
|
|
@@ -1284,14 +1289,14 @@ export function createTasksHandlers(runtime) {
|
|
|
1284
1289
|
}
|
|
1285
1290
|
return redirect(response, decisionBack);
|
|
1286
1291
|
}
|
|
1287
|
-
async function
|
|
1288
|
-
const { url, who, response,
|
|
1289
|
-
const act = matchTaskPath(url.pathname, "/
|
|
1292
|
+
async function taskRecheckStopped(ctx) {
|
|
1293
|
+
const { url, who, response, posted } = ctx;
|
|
1294
|
+
const act = matchTaskPath(url.pathname, "/recheck-stopped$");
|
|
1290
1295
|
if (act === null)
|
|
1291
1296
|
return noPage(who, response);
|
|
1292
|
-
const body = readForm(posted, CONSOLE_FORMS.
|
|
1293
|
-
//
|
|
1294
|
-
//
|
|
1297
|
+
const body = readForm(posted, CONSOLE_FORMS.recheckStopped);
|
|
1298
|
+
// Check again: Toolroll looks at this computer's processes for the build once more (stop-proof.ts) and settles it
|
|
1299
|
+
// when none of them is the build's. Nobody vouches that a build stopped, so there is nothing to confirm here.
|
|
1295
1300
|
const back = taskHref(act.taskId);
|
|
1296
1301
|
const ref = store.lookupRef(act.taskId);
|
|
1297
1302
|
if (ref === null || !visible(ref.repo))
|
|
@@ -1302,19 +1307,19 @@ export function createTasksHandlers(runtime) {
|
|
|
1302
1307
|
if (run === null || runTask === null || familyOf(runTask)?.root.id !== (familyOf(act.taskId)?.root.id ?? act.taskId)) {
|
|
1303
1308
|
return taskScreen(response, who, act.taskId, "That build isn't part of this task.", 409);
|
|
1304
1309
|
}
|
|
1305
|
-
if (!
|
|
1306
|
-
return taskScreen(response, who, act.taskId, "
|
|
1307
|
-
|
|
1308
|
-
// When Toolroll can't check at all, the approver also says they checked: never a one-click confirmation.
|
|
1309
|
-
if (store.stopQuiescenceFact(run.id)?.kind === "unknown" && body.get("checked") !== "yes") {
|
|
1310
|
-
return taskScreen(response, who, act.taskId, `Make sure nothing from build #${run.id} is running, then tick the box to confirm.`, 409);
|
|
1311
|
-
}
|
|
1312
|
-
const settled = store.settleRunWitnessesByApprover({ runId: run.id, by: who.name, why: "Confirmed in the console that nothing from this build is running." }, now);
|
|
1313
|
-
if (!settled.ok) {
|
|
1314
|
-
return taskScreen(response, who, act.taskId, settled.reason === "alive" || settled.reason === "still-running"
|
|
1315
|
-
? `Something from build #${run.id} may still be running, so it can't be confirmed stopped yet.` : `Build #${run.id} can't be confirmed stopped.`, 409);
|
|
1316
|
-
}
|
|
1310
|
+
if (!store.accountCanAccess(who.name, ref.repo))
|
|
1311
|
+
return taskScreen(response, who, act.taskId, "You can't act on this project's tasks.", 403);
|
|
1312
|
+
const proved = await proveRunStopped(store, run.id, clock);
|
|
1317
1313
|
bustBadge();
|
|
1314
|
+
const left = store.stopQuiescenceFact(run.id);
|
|
1315
|
+
if (left !== null) {
|
|
1316
|
+
const words = proved.outcome === "blocked" ? `Build #${proved.run} ${proved.words}.`
|
|
1317
|
+
: left.kind === "alive" || left.kind === "open" ? `Something from build #${left.run} may still be running. Toolroll stops what it proves is the build's on its own.`
|
|
1318
|
+
: left.kind === "elsewhere" ? `Build #${left.run} ran on another computer; only that computer can check it stopped.`
|
|
1319
|
+
: left.kind === "unknown" ? `Toolroll still can't prove build #${left.run} stopped from this computer.`
|
|
1320
|
+
: `Toolroll is still making sure build #${left.run} stopped. This clears on its own.`;
|
|
1321
|
+
return taskScreen(response, who, act.taskId, words, 409);
|
|
1322
|
+
}
|
|
1318
1323
|
const to = resultReturnTarget(body.get("return"), run.id, runTasks(run.taskRef));
|
|
1319
1324
|
return redirect(response, body.get("return") ? to : back);
|
|
1320
1325
|
}
|
|
@@ -2458,7 +2463,11 @@ export function createTasksHandlers(runtime) {
|
|
|
2458
2463
|
: requestedReturn === null
|
|
2459
2464
|
? taskHref(taskId)
|
|
2460
2465
|
: safeChatReturn(requestedReturn);
|
|
2466
|
+
// The release sheet's Approve release (trusted-release-reports.md A7): the same door, its own page for words.
|
|
2467
|
+
const sheet = body.get("sheet") === "release";
|
|
2461
2468
|
const approvalProblem = (message, status) => {
|
|
2469
|
+
if (sheet)
|
|
2470
|
+
return releaseSheet.sheetScreen({ who, response, project: null }, taskId, message, status);
|
|
2462
2471
|
if (requestedReturn !== null && requestedReturn !== "next") {
|
|
2463
2472
|
return redirect(response, chatReturnWithSaid(approvalBack, message));
|
|
2464
2473
|
}
|
|
@@ -2484,6 +2493,9 @@ export function createTasksHandlers(runtime) {
|
|
|
2484
2493
|
&& !(who.authority === "local-owner" && localApprovalEligible(store, who.name, taskId).ok)) {
|
|
2485
2494
|
return approvalProblem("approval requires your password, typed again", 400);
|
|
2486
2495
|
}
|
|
2496
|
+
// The sheet binds the release card's digest (the nonce was minted for it): the shared card wrapper decides.
|
|
2497
|
+
if (sheet)
|
|
2498
|
+
return releaseSheet.approveFromSheet({ who, response, project: null }, taskId, digest, token);
|
|
2487
2499
|
const scopeRow = store.getScope(taskId);
|
|
2488
2500
|
if (scopeRow === null)
|
|
2489
2501
|
return approvalProblem("this task has no scope to approve", 409);
|
|
@@ -3078,6 +3090,7 @@ export function createTasksHandlers(runtime) {
|
|
|
3078
3090
|
return live;
|
|
3079
3091
|
}
|
|
3080
3092
|
const tidyHandlers = createTidyHandlers(runtime);
|
|
3093
|
+
const releaseSheet = createReleaseSheetHandlers(runtime);
|
|
3081
3094
|
/** The question page, with a refusal in its box when an answer was refused (the box keeps the choice to make). */
|
|
3082
3095
|
function decisionScreen(ctx, id, back, refusal, code, now, draft) {
|
|
3083
3096
|
const { who, response, project } = ctx;
|
|
@@ -3095,6 +3108,7 @@ export function createTasksHandlers(runtime) {
|
|
|
3095
3108
|
}
|
|
3096
3109
|
const registrations = handlersOf("tasks", {
|
|
3097
3110
|
...tidyHandlers,
|
|
3111
|
+
...releaseSheet.routes,
|
|
3098
3112
|
...createAgentSignalHandlers(runtime),
|
|
3099
3113
|
"code.page": showCode("workspace"),
|
|
3100
3114
|
"code.session": showCode("session"),
|
|
@@ -3155,7 +3169,7 @@ export function createTasksHandlers(runtime) {
|
|
|
3155
3169
|
"task.act.retry-review": taskAct("retry-review"),
|
|
3156
3170
|
"task.act.complete": taskAct("complete"),
|
|
3157
3171
|
"task.act.merge": taskMerge,
|
|
3158
|
-
"task.act.
|
|
3172
|
+
"task.act.recheck-stopped": taskRecheckStopped,
|
|
3159
3173
|
"task.act.stop": taskAct("stop"),
|
|
3160
3174
|
"task.act.stop-carrying": taskAct("stop-carrying"),
|
|
3161
3175
|
"task.act.pause": taskAct("pause"),
|
|
@@ -3314,3 +3328,71 @@ export function createAgentSignalHandlers(runtime) {
|
|
|
3314
3328
|
}
|
|
3315
3329
|
return { "agents.seen": agentSeen, "agents.watch": agentWatch };
|
|
3316
3330
|
}
|
|
3331
|
+
/**
|
|
3332
|
+
* One-tap release approval (docs/plans/trusted-release-reports.md, slice A): the sheet (`/t/<id>/release`), the push's
|
|
3333
|
+
* opaque locator (`/approve/<task ref>`, a GET that only redirects to the sheet) and the sheet's approval, which the
|
|
3334
|
+
* task approval POST hands here once its shared checks (cookie session, CSRF and the browser key at admission, the
|
|
3335
|
+
* single-use nonce, a password or a standing proof) passed. The approval itself is the shared card wrapper around the
|
|
3336
|
+
* unchanged approve() (release-approval.ts approveReleaseCard).
|
|
3337
|
+
*/
|
|
3338
|
+
export function createReleaseSheetHandlers(runtime) {
|
|
3339
|
+
const { store, sendScreen, chromeFor, visible, mintApprovalNonce, clock } = runtime;
|
|
3340
|
+
const missing = (who, response) => refuse(response, who, 404, "There's no release waiting for approval at this address.", "/work");
|
|
3341
|
+
/** The sheet, with a problem said where it happened. */
|
|
3342
|
+
function sheetScreen(ctx, taskId, problem, status) {
|
|
3343
|
+
const { who, response, project } = ctx;
|
|
3344
|
+
const ref = store.lookupRef(taskId);
|
|
3345
|
+
if (ref === null || !visible(ref.repo) || !store.accountCanAccess(who.name, ref.repo))
|
|
3346
|
+
return missing(who, response);
|
|
3347
|
+
const built = releaseApprovalCard(store, taskId);
|
|
3348
|
+
if (!built.ok)
|
|
3349
|
+
return missing(who, response);
|
|
3350
|
+
const card = built.card;
|
|
3351
|
+
const open = card.approved === null && card.refusal === null;
|
|
3352
|
+
// This computer's local owner can't approve a release: the full sign-in comes first (scope.ts localApprovalEligible).
|
|
3353
|
+
const local = who.via === "cookie" && who.authority === "local-owner" ? localOwnerRefusal(store.accountOf(who.name)?.passwordSet === true, releaseSheetHref(taskId)) : null;
|
|
3354
|
+
const nonce = open && local === null && who.via === "cookie" && who.role === "approver" ? mintApprovalNonce(who.name, taskId, card.digest) : "";
|
|
3355
|
+
const remembered = (() => {
|
|
3356
|
+
if (!open || who.via !== "cookie")
|
|
3357
|
+
return false;
|
|
3358
|
+
const fresh = withFreshness("approve-standard", () => freshnessFor(who.name, Date.now(), true));
|
|
3359
|
+
if (fresh === null)
|
|
3360
|
+
return false;
|
|
3361
|
+
const gate = store.approvalGate(taskId, who.name, "person");
|
|
3362
|
+
return freshlyApprovable(store, taskId, card.scope, gate.verdict === "seal" && !gate.protectedWork);
|
|
3363
|
+
})();
|
|
3364
|
+
return sendScreen(response, status, releaseSheetPage(chromeFor(project ?? ref.repo, "work"), { card, nonce, facts: approvalFactsNow({ remembered }), local, problem }));
|
|
3365
|
+
}
|
|
3366
|
+
async function showSheet(ctx) {
|
|
3367
|
+
const task = matchTaskPath(ctx.url.pathname, "/release$");
|
|
3368
|
+
if (task === null)
|
|
3369
|
+
return missing(ctx.who, ctx.response);
|
|
3370
|
+
return sheetScreen(ctx, task.taskId, null, 200);
|
|
3371
|
+
}
|
|
3372
|
+
/** The push's locator: proved a release in the person's projects from the plane's own state, then only redirected. */
|
|
3373
|
+
async function locate(ctx) {
|
|
3374
|
+
const { who, response, url } = ctx;
|
|
3375
|
+
const raw = /^\/approve\/([1-9][0-9]{0,14})$/.exec(url.pathname)?.[1];
|
|
3376
|
+
const ref = raw === undefined ? null : store.refForId(Number(raw));
|
|
3377
|
+
if (ref === null || ref.backend !== "built-in" || !visible(ref.repo) || !store.accountCanAccess(who.name, ref.repo) || releaseTaskOf(store, ref.externalId) === null)
|
|
3378
|
+
return missing(who, response);
|
|
3379
|
+
return redirect(response, releaseSheetHref(ref.externalId));
|
|
3380
|
+
}
|
|
3381
|
+
/** The sheet's Approve release, after the approval POST's shared checks. */
|
|
3382
|
+
function approveFromSheet(ctx, taskId, cardDigest, token) {
|
|
3383
|
+
const { who, response } = ctx;
|
|
3384
|
+
const now = clock();
|
|
3385
|
+
const answer = spendingOnlyOnSuccess(() => withFreshness("approve-standard", () => approveReleaseCard(store, { taskId, cardDigest, approveNow: scopeDigest => approveScope(store, taskId, who.name, now, scopeDigest, token) })), result => !result.ok && !(result.reason === "approve" && result.result.reason === "second-approver" && !result.result.already));
|
|
3386
|
+
if (answer.ok)
|
|
3387
|
+
return redirect(response, releaseSheetHref(taskId));
|
|
3388
|
+
if (answer.reason !== "approve")
|
|
3389
|
+
return sheetScreen(ctx, taskId, answer.message, answer.reason === "not-release" ? 404 : 409);
|
|
3390
|
+
const refused = answer.result;
|
|
3391
|
+
if (refused.reason === "second-approver")
|
|
3392
|
+
return sheetScreen(ctx, taskId, gateWords({ verdict: "vote", have: refused.have, need: 2, already: refused.already }), 200);
|
|
3393
|
+
if (refused.reason === "policy")
|
|
3394
|
+
return sheetScreen(ctx, taskId, refused.message, 403);
|
|
3395
|
+
return sheetScreen(ctx, taskId, approveRefusalWords(refused.reason), refused.reason === "changed" || refused.reason === "unrouted" ? 409 : 403);
|
|
3396
|
+
}
|
|
3397
|
+
return { routes: { "task.release": showSheet, "release.locate": locate }, sheetScreen, approveFromSheet };
|
|
3398
|
+
}
|
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;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type StopOptions } from "./run-closeout.js";
|
|
2
|
+
import { type Store } from "./store.js";
|
|
3
|
+
/** How long after a run finished its processes have to go on their own before Toolroll looks. */
|
|
4
|
+
export declare const STOP_PROOF_GRACE_MS = 60000;
|
|
5
|
+
export type StopProofOutcome =
|
|
6
|
+
/** Proven stopped and settled, in the ledger with its evidence. */
|
|
7
|
+
"settled"
|
|
8
|
+
/** Not yet: inside the grace period, a live child is still tracked here, or a leftover was just stopped. */
|
|
9
|
+
| "waiting"
|
|
10
|
+
/** This computer can't prove it (can't look, another computer's process, or something it didn't start is in the
|
|
11
|
+
* folder). Recorded once, in plain words. */
|
|
12
|
+
| "blocked"
|
|
13
|
+
/** Nothing for this proof to do: already stopped, still open, alive on its own record, or a native sandbox. */
|
|
14
|
+
| "skipped";
|
|
15
|
+
export type StopProofResult = {
|
|
16
|
+
run: number;
|
|
17
|
+
outcome: StopProofOutcome;
|
|
18
|
+
words: string;
|
|
19
|
+
stopped?: number;
|
|
20
|
+
};
|
|
21
|
+
export type StopProofOptions = StopOptions & {
|
|
22
|
+
/** How long after the run finished before Toolroll looks (STOP_PROOF_GRACE_MS). `graceMs` is the SIGTERM grace. */
|
|
23
|
+
afterFinishMs?: number;
|
|
24
|
+
/** Which processes have their working directory inside each path; must throw when it can't tell. */
|
|
25
|
+
cwds?: (paths: readonly string[]) => Map<string, number[]>;
|
|
26
|
+
/** Every process's command line, by pid; must throw when it can't tell. */
|
|
27
|
+
commandLines?: () => Map<number, string>;
|
|
28
|
+
/** The live service's own pid (its ancestors are never targets). */
|
|
29
|
+
self?: number;
|
|
30
|
+
/** The worktree note check (worktree.ts); throws when the note can't be read. */
|
|
31
|
+
occupancy?: (path: string) => {
|
|
32
|
+
held: true;
|
|
33
|
+
by: number;
|
|
34
|
+
} | {
|
|
35
|
+
held: false;
|
|
36
|
+
};
|
|
37
|
+
/** Called after processes were stopped: a cached look must be read again. */
|
|
38
|
+
refresh?: () => void;
|
|
39
|
+
};
|
|
40
|
+
/** One pass's shared look at this computer: the process table, folders and command lines are read once for every
|
|
41
|
+
* run the pass checks, and again after anything was stopped. */
|
|
42
|
+
export declare function sharedLook(options?: StopProofOptions): StopProofOptions;
|
|
43
|
+
/** Which processes work inside each path, from the OS: /proc on Linux, lsof on macOS. Unlike a best-effort listing,
|
|
44
|
+
* it throws when the listing can't be read: no answer is never "nobody". */
|
|
45
|
+
export declare function processesInStrict(paths: readonly string[], listing?: readonly {
|
|
46
|
+
pid: number;
|
|
47
|
+
cwd: string;
|
|
48
|
+
}[]): Map<string, number[]>;
|
|
49
|
+
/** Every process's working directory, from the OS; throws when the listing can't be read. */
|
|
50
|
+
export declare function processFolders(): {
|
|
51
|
+
pid: number;
|
|
52
|
+
cwd: string;
|
|
53
|
+
}[];
|
|
54
|
+
/** Every process's command line, by pid. Read to match a folder's path in memory only; never stored or shown. */
|
|
55
|
+
export declare function processCommandLines(): Map<number, string>;
|
|
56
|
+
/** Whether a command line names `path` or something inside it (a whole path, never a longer folder's prefix). */
|
|
57
|
+
export declare function namesPath(line: string, path: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Prove one finished run stopped, on this computer, without asking anyone (see the file's comment). Walks the run's
|
|
60
|
+
* owned runs one unproven stop at a time. Never signals a process that isn't proven the run's.
|
|
61
|
+
*/
|
|
62
|
+
export declare function proveRunStopped(store: Store, runId: number, clock: () => Date, options?: StopProofOptions): Promise<StopProofResult>;
|
|
63
|
+
/** The worker's pass (operate.ts reconcile): every finished run of this computer whose stop is unproven where
|
|
64
|
+
* Toolroll's own look can prove it. A batch a pass, oldest first; a blocked run never holds newer ones back. */
|
|
65
|
+
export declare function proveFinishedRunsStopped(store: Store, clock: () => Date, options?: StopProofOptions & {
|
|
66
|
+
batch?: number;
|
|
67
|
+
}): Promise<StopProofResult[]>;
|