toolroll 0.9.6 → 0.9.8
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 +64 -1
- package/dist/actor.d.ts +21 -0
- package/dist/actor.js +48 -0
- package/dist/assignment-adapters.d.ts +2 -0
- package/dist/assignment-adapters.js +2 -0
- package/dist/assignment-ui.js +3 -1
- package/dist/assignment.d.ts +15 -0
- package/dist/assignment.js +80 -16
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +28 -24
- package/dist/browser-workspace.d.ts +117 -2
- package/dist/build-review.d.ts +38 -0
- package/dist/build-review.js +341 -0
- package/dist/builder.d.ts +1 -0
- package/dist/builder.js +33 -13
- package/dist/chat-actions.js +2 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-quiet.js +11 -1
- package/dist/chat-task-actions.d.ts +13 -0
- package/dist/chat-task-actions.js +19 -1
- package/dist/check-levels-ui.d.ts +26 -0
- package/dist/check-levels-ui.js +84 -0
- package/dist/check-levels.d.ts +67 -0
- package/dist/check-levels.js +171 -0
- package/dist/claim.d.ts +22 -0
- package/dist/claim.js +43 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -0
- package/dist/demo-chat.js +4 -1
- package/dist/dispatch.js +9 -3
- package/dist/dispose.d.ts +12 -3
- package/dist/dispose.js +19 -5
- package/dist/exec.d.ts +5 -0
- package/dist/exec.js +30 -4
- package/dist/export.js +3 -2
- package/dist/first-run.d.ts +34 -0
- package/dist/first-run.js +37 -1
- package/dist/flow-gallery-ui.d.ts +30 -0
- package/dist/flow-gallery-ui.js +85 -0
- package/dist/flow-gallery.d.ts +111 -0
- package/dist/flow-gallery.js +445 -0
- package/dist/flow-share.d.ts +102 -0
- package/dist/flow-share.js +614 -0
- package/dist/flow-starters.d.ts +2 -0
- package/dist/flow-starters.js +1 -1
- package/dist/flow-steps.js +7 -0
- package/dist/flow-triggers.js +2 -0
- package/dist/flows-cli.d.ts +56 -0
- package/dist/flows-cli.js +109 -3
- package/dist/flows-ui.d.ts +15 -1
- package/dist/flows-ui.js +31 -6
- package/dist/guides.js +27 -2
- package/dist/invoke.js +5 -0
- package/dist/lead-context.js +6 -2
- package/dist/mate-doors.d.ts +0 -15
- package/dist/mate-doors.js +5 -1
- package/dist/mate-tools.js +9 -4
- package/dist/models-ui.js +1 -1
- package/dist/modes.d.ts +3 -1
- package/dist/modes.js +3 -1
- package/dist/operate.d.ts +4 -1
- package/dist/operate.js +711 -98
- package/dist/process-custody.js +8 -0
- package/dist/project-concurrency.d.ts +62 -0
- package/dist/project-concurrency.js +116 -0
- package/dist/project-delete-ui.d.ts +7 -0
- package/dist/project-delete-ui.js +19 -2
- package/dist/project-delete.js +4 -0
- package/dist/provider.js +16 -0
- package/dist/pull-request-flow.d.ts +9 -0
- package/dist/pull-request-flow.js +21 -3
- package/dist/result-follow-ups.d.ts +101 -0
- package/dist/result-follow-ups.js +333 -0
- package/dist/retention-ui.d.ts +3 -1
- package/dist/retention-ui.js +4 -4
- package/dist/retention.d.ts +13 -6
- package/dist/retention.js +50 -12
- package/dist/review-switch.d.ts +41 -0
- package/dist/review-switch.js +88 -0
- package/dist/reviewer.d.ts +24 -2
- package/dist/reviewer.js +49 -1
- package/dist/scope.js +11 -0
- package/dist/serve.d.ts +211 -2
- package/dist/serve.js +1038 -145
- package/dist/store.d.ts +112 -4
- package/dist/store.js +447 -27
- package/dist/surface.js +36 -9
- package/dist/tailnet.d.ts +11 -0
- package/dist/tailnet.js +47 -0
- package/dist/task-outcome-cli.js +2 -2
- package/dist/task-status.d.ts +13 -3
- package/dist/task-status.js +38 -16
- package/dist/team-cli.js +3 -0
- package/dist/telegram-status.js +2 -2
- package/dist/verification-evidence.js +21 -1
- package/dist/webhooks.js +5 -0
- package/dist/work-index.js +39 -6
- package/dist/work-summary.d.ts +0 -4
- package/dist/work-summary.js +4 -1
- package/dist/workspace-ui.d.ts +6 -1
- package/dist/workspace-ui.js +14 -11
- package/package.json +1 -1
package/dist/chat-actions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared, exact-state actions. Models may prepare; only the existing human
|
|
2
2
|
* confirmation door executes. Protected actions use a one-use review receipt. */
|
|
3
|
+
import { withActor } from "./actor.js";
|
|
3
4
|
import { publicChatText } from "./chat-display.js";
|
|
4
5
|
import { gateWords } from "./approval-policy.js";
|
|
5
6
|
import { createHash, randomBytes } from "node:crypto";
|
|
@@ -1102,7 +1103,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
|
|
|
1102
1103
|
throw Error(completed.message);
|
|
1103
1104
|
}
|
|
1104
1105
|
else if (payload.operation === "task_cancel") {
|
|
1105
|
-
const result = store.cancelTask(task, now);
|
|
1106
|
+
const result = withActor({ account: who.name, lead: false }, () => store.cancelTask(task, now));
|
|
1106
1107
|
if (!result.ok)
|
|
1107
1108
|
throw Error(`Task was not cancelled: ${result.reason}.`);
|
|
1108
1109
|
}
|
package/dist/chat-delivery.js
CHANGED
|
@@ -627,6 +627,8 @@ export async function planChatNotifications(options) {
|
|
|
627
627
|
const personal = notification.recipient !== null;
|
|
628
628
|
if (notification.createdAt >= binding.created &&
|
|
629
629
|
notification.resolvedAt === null &&
|
|
630
|
+
// Pings follow responsibility: the lead's work, this person's own act and a muted project stay in the console.
|
|
631
|
+
store.pingAllowed(notification, binding.approver) &&
|
|
630
632
|
// A flow decision for "anyone who approves" reaches every approver who can see the project.
|
|
631
633
|
// A notification addressed to this person reaches them whatever project
|
|
632
634
|
// their channel follows (a sign-in pause is the installation's, v108).
|
package/dist/chat-quiet.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { assignmentOf } from "./assignment.js";
|
|
8
8
|
import { chatControlHref } from "./chat-controls.js";
|
|
9
9
|
import { telegramProgressCard } from "./telegram-progress.js";
|
|
10
|
-
import { assignmentStatusFacts, headlineEmoji, taskStatusOf } from "./task-status.js";
|
|
10
|
+
import { assignmentStatusFacts, headlineEmoji, replacedWords, taskStatusOf } from "./task-status.js";
|
|
11
11
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
12
12
|
import { isLifecycleNotification } from "./store.js";
|
|
13
13
|
/** What always makes a new message, even in quiet mode: a question or approval waiting, a failure or other
|
|
@@ -55,6 +55,16 @@ function taskLine(store, taskRef, now, root) {
|
|
|
55
55
|
return null;
|
|
56
56
|
const task = store.getTask(ref.externalId);
|
|
57
57
|
const title = phoneText(task?.title ?? ref.externalId, 88);
|
|
58
|
+
// Replaced, never "Cancelled": the card names its successor and links to it.
|
|
59
|
+
const successor = task?.state === "cancelled" ? store.replacementOf(ref.externalId) : null;
|
|
60
|
+
if (successor !== null) {
|
|
61
|
+
const heading = `${headlineEmoji("Stopped")} ${replacedWords(successor)}`;
|
|
62
|
+
const text = [title, heading, "", `${projectLabel(ref.repo)} · ${ref.externalId}`].join("\n");
|
|
63
|
+
return { title, icon: headlineEmoji("Stopped"), status: replacedWords(successor), project: ref.repo, view: {
|
|
64
|
+
text, entities: [{ type: "bold", offset: 0, length: title.length }, { type: "bold", offset: title.length + 1, length: heading.length }],
|
|
65
|
+
link: { label: `Open ${phoneText(successor, 40)}`, path: chatControlHref("task", successor) },
|
|
66
|
+
} };
|
|
67
|
+
}
|
|
58
68
|
const run = cardRun(store, taskRef);
|
|
59
69
|
if (run !== null) {
|
|
60
70
|
const view = telegramProgressCard(store, run, ref.externalId, ref.repo, now, root);
|
|
@@ -26,6 +26,18 @@ export declare const CHAT_TASK_ACTIONS: {
|
|
|
26
26
|
readonly label: "Remove dependency";
|
|
27
27
|
readonly detail: "Stops waiting for the selected task. Other requirements still apply.";
|
|
28
28
|
};
|
|
29
|
+
readonly run_checks: {
|
|
30
|
+
readonly label: "Run checks";
|
|
31
|
+
readonly detail: "Runs the project's quick check on this result's exact commit. A pass upgrades its status; a failure stays visible.";
|
|
32
|
+
};
|
|
33
|
+
readonly run_full_checks: {
|
|
34
|
+
readonly label: "Run full checks";
|
|
35
|
+
readonly detail: "Runs the project's full check on this result's exact commit. A pass upgrades its status; a failure stays visible.";
|
|
36
|
+
};
|
|
37
|
+
readonly add_tests: {
|
|
38
|
+
readonly label: "Add tests";
|
|
39
|
+
readonly detail: "Files a small task to write tests for this change. Nothing runs until it is approved.";
|
|
40
|
+
};
|
|
29
41
|
};
|
|
30
42
|
export declare function isChatTaskAction(value: unknown): value is keyof typeof CHAT_TASK_ACTIONS;
|
|
31
43
|
/** Bind cards to a specific execution and its current state, not the family's moving pointer. */
|
|
@@ -34,6 +46,7 @@ export declare function chatTaskStamp(store: Store, who: VerifiedApprover, task:
|
|
|
34
46
|
export declare function chatTaskRun(store: Store, task: string, operation: "stop" | "resume", now: Date): number | null;
|
|
35
47
|
export declare function applyChatTaskAction(store: Store, who: VerifiedApprover, payload: Record<string, unknown>, now: Date, web: boolean, controls?: Pick<StopRequest, "held" | "deferSignal"> & {
|
|
36
48
|
via?: StopRequest["via"];
|
|
49
|
+
evidenceRoot?: string;
|
|
37
50
|
}): {
|
|
38
51
|
ok: true;
|
|
39
52
|
taskId: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { verifiedAuthor } from "./store.js";
|
|
2
2
|
import { requestTaskStop, taskControlOf } from "./task-control.js";
|
|
3
3
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
4
|
+
import { fileAddTestsTask, requestFollowUpChecks } from "./result-follow-ups.js";
|
|
4
5
|
export const CHAT_TASK_ACTIONS = {
|
|
5
6
|
// Build #1604 feedback: one short consequence that still says saved work
|
|
6
7
|
// remains, other tasks continue, and the pause waits for acknowledgement.
|
|
@@ -10,6 +11,10 @@ export const CHAT_TASK_ACTIONS = {
|
|
|
10
11
|
plan: { label: "Plan first", detail: "Requests a plan. Building still requires approval." },
|
|
11
12
|
wait_for: { label: "Add dependency", detail: "Waits for the selected task to finish." },
|
|
12
13
|
stop_waiting: { label: "Remove dependency", detail: "Stops waiting for the selected task. Other requirements still apply." },
|
|
14
|
+
// Follow-ups on a result (result-follow-ups.ts): the same actions as the result page.
|
|
15
|
+
run_checks: { label: "Run checks", detail: "Runs the project's quick check on this result's exact commit. A pass upgrades its status; a failure stays visible." },
|
|
16
|
+
run_full_checks: { label: "Run full checks", detail: "Runs the project's full check on this result's exact commit. A pass upgrades its status; a failure stays visible." },
|
|
17
|
+
add_tests: { label: "Add tests", detail: "Files a small task to write tests for this change. Nothing runs until it is approved." },
|
|
13
18
|
};
|
|
14
19
|
export function isChatTaskAction(value) {
|
|
15
20
|
return typeof value === "string" && Object.hasOwn(CHAT_TASK_ACTIONS, value);
|
|
@@ -52,7 +57,7 @@ export function applyChatTaskAction(store, who, payload, now, web, controls = {}
|
|
|
52
57
|
// Resume remains the existing nonce + password ceremony. A proposal only opens it.
|
|
53
58
|
if (operation === "resume")
|
|
54
59
|
return { ok: true, taskId: task, said: `Resume review requested for ${task}. Complete the password confirmation on the task to resume.` };
|
|
55
|
-
const { via, ...rest } = controls;
|
|
60
|
+
const { via, evidenceRoot: _root, ...rest } = controls;
|
|
56
61
|
const result = requestTaskStop(store, { taskId: task, runId: run, by: verifiedAuthor(who.name), via: via ?? (web ? "web" : "cli"), ...rest }, now);
|
|
57
62
|
return result.ok ? { ok: true, taskId: task, said: `Stop requested for ${task}, run #${run}.` }
|
|
58
63
|
: { ok: false, message: result.detail };
|
|
@@ -61,6 +66,19 @@ export function applyChatTaskAction(store, who, payload, now, web, controls = {}
|
|
|
61
66
|
const result = store.requeueTask(task, who.name, now);
|
|
62
67
|
return result.ok ? { ok: true, taskId: task, said: "Task queued again." } : { ok: false, message: result.reason === "claimed" ? "This task is already running." : result.reason === "not-stalled" ? "This task does not need another attempt." : "That task is no longer available." };
|
|
63
68
|
}
|
|
69
|
+
if (operation === "run_checks" || operation === "run_full_checks" || operation === "add_tests") {
|
|
70
|
+
const result = store.runsFor(ref.id).find(one => one.role === "builder" && one.finishedAt !== null && one.headRevision !== null) ?? null;
|
|
71
|
+
if (result === null)
|
|
72
|
+
return { ok: false, message: "This task has no finished result yet." };
|
|
73
|
+
if (operation === "add_tests") {
|
|
74
|
+
const filed = fileAddTestsTask(store, controls.evidenceRoot ?? null, { runId: result.id, actor: who.name, filedVia: web ? "console" : "cli", admittedRepos: who.repos }, now);
|
|
75
|
+
return filed.ok ? { ok: true, taskId: filed.id, said: filed.existing ? `Tests are already filed as ${filed.id}.` : `Filed ${filed.id} to add tests. Review and approve it to start.` }
|
|
76
|
+
: { ok: false, message: filed.message };
|
|
77
|
+
}
|
|
78
|
+
const asked = requestFollowUpChecks(store, { runId: result.id, level: operation === "run_full_checks" ? "full" : "quick", actor: who.name }, now);
|
|
79
|
+
return asked.ok ? { ok: true, taskId: task, said: `${asked.level === "full" ? "Full" : "Quick"} checks ${asked.existing ? "are already" : "are"} queued on this result's commit.` }
|
|
80
|
+
: { ok: false, message: asked.message };
|
|
81
|
+
}
|
|
64
82
|
if (operation === "plan") {
|
|
65
83
|
const result = store.requestPlan(ref.id, now);
|
|
66
84
|
if (!result.ok)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Settings → Projects → Checks: how much checks after each build, in plain
|
|
2
|
+
* words. One choice (Quick, Full, Off) and the quick command beside the full
|
|
3
|
+
* one. Every change takes the password and lands in the ledger; approving a
|
|
4
|
+
* command restates exactly what will run, unattended, before the yes. */
|
|
5
|
+
import { type CheckLevel } from "./check-levels.js";
|
|
6
|
+
import type { VerifyCommand } from "./store.js";
|
|
7
|
+
export type CheckSettingsView = {
|
|
8
|
+
repo: string;
|
|
9
|
+
name: string;
|
|
10
|
+
csrf: string;
|
|
11
|
+
canChange: boolean;
|
|
12
|
+
level: CheckLevel;
|
|
13
|
+
full: VerifyCommand | null;
|
|
14
|
+
quick: VerifyCommand | null;
|
|
15
|
+
/** A starting point for the quick command, from the project's scripts. */
|
|
16
|
+
suggestion: string | null;
|
|
17
|
+
/** The project's automatic review switch (review-switch.ts), beside its checks. */
|
|
18
|
+
review?: {
|
|
19
|
+
on: boolean;
|
|
20
|
+
source: "project" | "hands-off" | "default";
|
|
21
|
+
};
|
|
22
|
+
said: string | null;
|
|
23
|
+
problem: string | null;
|
|
24
|
+
};
|
|
25
|
+
export declare function checkSettingsHtml(view: CheckSettingsView): string;
|
|
26
|
+
export declare const CHECK_SETTINGS_CSS = "\n .check-settings { display: grid; gap: .6rem; max-width: 40rem; }\n .check-settings p, .check-settings h2 { margin: 0; }\n .check-settings h2 { font-size: 1rem; }\n .check-settings fieldset { border: 0; margin: 0; padding: 0; display: grid; gap: .25rem; }\n .check-settings legend { font-weight: 600; margin-bottom: .25rem; }\n .check-settings label { display: grid; gap: .25rem; }\n .check-settings .check-choice { display: flex; align-items: flex-start; gap: .6rem; min-height: 44px; padding: .35rem 0; cursor: pointer; }\n .check-settings .check-choice > span { display: grid; gap: .1rem; }\n .check-settings .check-choice input { margin-top: .2rem; }\n .check-settings input[type=password], .check-settings input[type=number], .check-settings textarea { min-height: 44px; box-sizing: border-box; width: 100%; }\n .check-settings button { min-height: 44px; justify-self: start; }\n .check-settings .check-command { overflow-wrap: anywhere; }\n .check-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n @media (max-width: 480px) { .check-settings button { justify-self: stretch; } }\n .follow-ups .follow-up-list { margin: .4rem 0; padding-left: 1.1rem; display: grid; gap: .25rem; }\n .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: flex; flex-wrap: wrap; gap: .5rem; margin: .5rem 0 0; }\n .follow-ups .follow-up-act { margin: 0; }\n .follow-ups button { min-height: 44px; }\n @media (max-width: 480px) { .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: grid; } .follow-ups .follow-up-act button { width: 100%; } }\n";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** Settings → Projects → Checks: how much checks after each build, in plain
|
|
2
|
+
* words. One choice (Quick, Full, Off) and the quick command beside the full
|
|
3
|
+
* one. Every change takes the password and lands in the ledger; approving a
|
|
4
|
+
* command restates exactly what will run, unattended, before the yes. */
|
|
5
|
+
import { CHECK_LEVELS, CHECK_LEVEL_HINTS, CHECK_LEVEL_WORDS } from "./check-levels.js";
|
|
6
|
+
const escape = (text) => text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
7
|
+
export function checkSettingsHtml(view) {
|
|
8
|
+
const hidden = (act) => `<input type="hidden" name="csrf" value="${escape(view.csrf)}"><input type="hidden" name="repo" value="${escape(view.repo)}"><input type="hidden" name="act" value="${act}">`;
|
|
9
|
+
const password = `<label>Password<input type="password" name="password" autocomplete="current-password" required></label>`;
|
|
10
|
+
const notes = [
|
|
11
|
+
view.said === null ? "" : `<p class="notice" role="status">${escape(view.said)}</p>`,
|
|
12
|
+
view.problem === null ? "" : `<p class="problem" role="alert">${escape(view.problem)}</p>`,
|
|
13
|
+
].join("");
|
|
14
|
+
const head = `<p class="meta">${escape(view.name)} · <span class="mono">${escape(view.repo)}</span></p>${notes}`;
|
|
15
|
+
const command = (one) => `<p class="mono check-command">${escape(one.command)}</p><p class="meta">Up to ${Math.round(one.timeoutMs / 1000)}s · approved by ${escape(one.approvedBy)}</p>`;
|
|
16
|
+
// Quick with no quick command runs the full check; say so where the choice is made.
|
|
17
|
+
const fallsBack = view.level === "quick" && view.quick === null;
|
|
18
|
+
const current = `<p><strong>${CHECK_LEVEL_WORDS[view.level]}</strong> · ${escape(CHECK_LEVEL_HINTS[view.level])}</p>` +
|
|
19
|
+
(fallsBack ? `<p class="meta">No quick command yet, so builds run the full check.</p>` : "") +
|
|
20
|
+
(view.level !== "full" && view.full !== null ? `<p class="meta">The full check still runs when a pull request opens, and Merge waits for it.</p>` : "");
|
|
21
|
+
if (!view.canChange) {
|
|
22
|
+
return head + `<section class="card check-settings">${current}</section>` + reviewHtml(view, "") +
|
|
23
|
+
`<section class="card check-settings"><h2>Quick check</h2>${view.quick === null ? `<p class="meta">None yet.</p>` : command(view.quick)}</section>` +
|
|
24
|
+
`<section class="card check-settings"><h2>Full check</h2>${view.full === null ? `<p class="meta">None yet.</p>` : command(view.full)}</section>`;
|
|
25
|
+
}
|
|
26
|
+
const choices = CHECK_LEVELS.map(level => `<label class="check-choice"><input type="radio" name="level" value="${level}"${level === view.level ? " checked" : ""}>` +
|
|
27
|
+
`<span><strong>${CHECK_LEVEL_WORDS[level]}</strong><span class="meta">${escape(CHECK_LEVEL_HINTS[level])}</span></span></label>`).join("");
|
|
28
|
+
const levelForm = `<form method="post" action="/settings/checks" class="card check-settings">${hidden("level")}` +
|
|
29
|
+
`<fieldset><legend>After each build</legend>${choices}</fieldset>` +
|
|
30
|
+
(fallsBack ? `<p class="meta">No quick command yet, so Quick runs the full check.</p>` : "") +
|
|
31
|
+
`<p class="meta">A task can choose its own checks when it's filed. With Quick or Off, the full check runs when a pull request opens, and Merge waits for it unless you merge anyway.</p>` +
|
|
32
|
+
`${password}<button type="submit">Save</button></form>`;
|
|
33
|
+
const quickTerms = `<ul class="check-terms"><li>Runs unattended right after every Quick build, in that build's checkout.</li><li>Plain environment: no credentials.</li><li>A failure shows on the result; nothing is blocked.</li></ul>`;
|
|
34
|
+
const quickForm = (value, submit) => `<form method="post" action="/settings/checks" class="check-settings">${hidden("quick")}` +
|
|
35
|
+
`<label>Command<textarea name="command" rows="2" maxlength="2000" spellcheck="false" class="mono" required>${escape(value)}</textarea></label>` +
|
|
36
|
+
`<label>Time limit (seconds)<input type="number" name="timeout" min="1" max="3600" value="${view.quick === null ? 180 : Math.round(view.quick.timeoutMs / 1000)}" required></label>` +
|
|
37
|
+
`${quickTerms}${password}<button type="submit">${submit}</button></form>`;
|
|
38
|
+
const quick = view.quick === null
|
|
39
|
+
? `<section class="card check-settings"><h2>Quick check</h2>` +
|
|
40
|
+
(view.suggestion === null ? `<p class="meta">Typecheck and the tests near the change, ideally under 2 minutes.</p>` : `<p class="meta">Suggested from the project's scripts. Edit it before approving.</p>`) +
|
|
41
|
+
quickForm(view.suggestion ?? "", "Approve quick check") + `</section>`
|
|
42
|
+
: `<section class="card check-settings"><h2>Quick check</h2>${command(view.quick)}` +
|
|
43
|
+
`<details class="settings-more"><summary>Change quick check</summary>${quickForm(view.quick.command, "Approve change")}</details>` +
|
|
44
|
+
`<details class="settings-more"><summary>Remove quick check</summary><form method="post" action="/settings/checks" class="check-settings">${hidden("quick-clear")}` +
|
|
45
|
+
`<p class="meta">Quick builds run the full check instead.</p>${password}<button type="submit" class="danger">Remove</button></form></details></section>`;
|
|
46
|
+
const full = `<details class="settings-more"><summary>Full check</summary><section class="card check-settings">` +
|
|
47
|
+
(view.full === null ? `<p class="meta">None yet. Approve one with <span class="mono">toolroll verify set --repo … --command "…"</span>.</p>` : command(view.full)) + `</section></details>`;
|
|
48
|
+
return head + levelForm + reviewHtml(view, password) + quick + full;
|
|
49
|
+
}
|
|
50
|
+
/** Automatic review: one state, one sentence, and its one change behind a disclosure (the password). */
|
|
51
|
+
function reviewHtml(view, password) {
|
|
52
|
+
const review = view.review;
|
|
53
|
+
if (review === undefined)
|
|
54
|
+
return "";
|
|
55
|
+
const state = review.on ? (review.source === "hands-off" ? "On while hands-off lasts" : "On") : "Off";
|
|
56
|
+
const act = review.on ? "Turn off" : "Turn on";
|
|
57
|
+
const change = password === "" ? ""
|
|
58
|
+
: `<details class="settings-more"><summary>${act}</summary><form method="post" action="/settings/checks" class="check-settings">` +
|
|
59
|
+
`<input type="hidden" name="csrf" value="${escape(view.csrf)}"><input type="hidden" name="repo" value="${escape(view.repo)}"><input type="hidden" name="act" value="review">` +
|
|
60
|
+
`<input type="hidden" name="on" value="${review.on ? "0" : "1"}">${password}<button type="submit">${act}</button></form></details>`;
|
|
61
|
+
return `<section class="card check-settings" data-review-switch="${review.on ? "on" : "off"}"><h2>Automatic review</h2>` +
|
|
62
|
+
`<p><strong>${state}</strong> · One read-only review after each build passes its checks. Only high findings send it back, once.</p>${change}</section>`;
|
|
63
|
+
}
|
|
64
|
+
export const CHECK_SETTINGS_CSS = `
|
|
65
|
+
.check-settings { display: grid; gap: .6rem; max-width: 40rem; }
|
|
66
|
+
.check-settings p, .check-settings h2 { margin: 0; }
|
|
67
|
+
.check-settings h2 { font-size: 1rem; }
|
|
68
|
+
.check-settings fieldset { border: 0; margin: 0; padding: 0; display: grid; gap: .25rem; }
|
|
69
|
+
.check-settings legend { font-weight: 600; margin-bottom: .25rem; }
|
|
70
|
+
.check-settings label { display: grid; gap: .25rem; }
|
|
71
|
+
.check-settings .check-choice { display: flex; align-items: flex-start; gap: .6rem; min-height: 44px; padding: .35rem 0; cursor: pointer; }
|
|
72
|
+
.check-settings .check-choice > span { display: grid; gap: .1rem; }
|
|
73
|
+
.check-settings .check-choice input { margin-top: .2rem; }
|
|
74
|
+
.check-settings input[type=password], .check-settings input[type=number], .check-settings textarea { min-height: 44px; box-sizing: border-box; width: 100%; }
|
|
75
|
+
.check-settings button { min-height: 44px; justify-self: start; }
|
|
76
|
+
.check-settings .check-command { overflow-wrap: anywhere; }
|
|
77
|
+
.check-terms { margin: 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
|
|
78
|
+
@media (max-width: 480px) { .check-settings button { justify-self: stretch; } }
|
|
79
|
+
.follow-ups .follow-up-list { margin: .4rem 0; padding-left: 1.1rem; display: grid; gap: .25rem; }
|
|
80
|
+
.follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: flex; flex-wrap: wrap; gap: .5rem; margin: .5rem 0 0; }
|
|
81
|
+
.follow-ups .follow-up-act { margin: 0; }
|
|
82
|
+
.follow-ups button { min-height: 44px; }
|
|
83
|
+
@media (max-width: 480px) { .follow-ups .follow-up-acts, .follow-ups .follow-up-act { display: grid; } .follow-ups .follow-up-act button { width: 100%; } }
|
|
84
|
+
`;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Store, VerifyCommand } from "./store.js";
|
|
2
|
+
export declare const CHECK_LEVELS: readonly ["quick", "full", "off"];
|
|
3
|
+
export type CheckLevel = (typeof CHECK_LEVELS)[number];
|
|
4
|
+
export declare const isCheckLevel: (value: unknown) => value is CheckLevel;
|
|
5
|
+
export declare const CHECK_LEVEL_WORDS: Readonly<Record<CheckLevel, string>>;
|
|
6
|
+
export declare const CHECK_LEVEL_HINTS: Readonly<Record<CheckLevel, string>>;
|
|
7
|
+
/** Ledger actions. The ledger is append-only, so the newest entry is the setting. */
|
|
8
|
+
export declare const PROJECT_LEVEL_ACTION = "check level changed";
|
|
9
|
+
export declare const TASK_LEVEL_ACTION = "task checks chosen";
|
|
10
|
+
export declare const RUN_LEVEL_ACTION = "checks used";
|
|
11
|
+
/** The quick command's grant lives beside the full one under its own key. */
|
|
12
|
+
export declare const quickVerifyKey: (repo: string) => string;
|
|
13
|
+
export declare function liveQuickCommand(store: Store, repo: string): VerifyCommand | null;
|
|
14
|
+
/** Ledger reads go through the repo index: `repo = ?` (or IS NULL for unplaced work). */
|
|
15
|
+
export declare const repoClause: (repo: string | null) => [string, string[]];
|
|
16
|
+
export declare function repoOfRun(store: Store, runId: number): string | null;
|
|
17
|
+
export type ProjectCheckLevel = {
|
|
18
|
+
level: CheckLevel;
|
|
19
|
+
setBy: string | null;
|
|
20
|
+
at: string | null;
|
|
21
|
+
};
|
|
22
|
+
/** A project's level. With nothing recorded it is Full: a project that existed
|
|
23
|
+
* before levels keeps exactly the check it had. New projects are stamped Quick
|
|
24
|
+
* when they are added (stampNewProjectLevel). */
|
|
25
|
+
export declare function projectCheckLevel(store: Store, repo: string): ProjectCheckLevel;
|
|
26
|
+
/** An approver's act: set the project's level. The ledger keeps before → after. */
|
|
27
|
+
export declare function setProjectCheckLevel(store: Store, repo: string, level: CheckLevel, by: string, now: Date): {
|
|
28
|
+
changed: boolean;
|
|
29
|
+
before: CheckLevel;
|
|
30
|
+
};
|
|
31
|
+
/** A project added from now on starts on Quick. One that already has a level,
|
|
32
|
+
* or an approved check from before levels existed, is left exactly as it is. */
|
|
33
|
+
export declare function stampNewProjectLevel(store: Store, repo: string, now: Date): boolean;
|
|
34
|
+
/** A task's own choice, if it made one; a revision inherits the task it revises. */
|
|
35
|
+
export declare function taskCheckLevel(store: Store, taskId: string): CheckLevel | null;
|
|
36
|
+
/** A filing choice: this task's checks, overriding the project. Fixed once its
|
|
37
|
+
* plan is approved, so what the approver saw is what runs. */
|
|
38
|
+
export declare function setTaskCheckLevel(store: Store, taskId: string, level: CheckLevel, by: string, now: Date): {
|
|
39
|
+
ok: true;
|
|
40
|
+
} | {
|
|
41
|
+
ok: false;
|
|
42
|
+
message: string;
|
|
43
|
+
};
|
|
44
|
+
export type EffectiveCheckLevel = {
|
|
45
|
+
level: CheckLevel;
|
|
46
|
+
from: "task" | "project";
|
|
47
|
+
};
|
|
48
|
+
export declare function effectiveCheckLevel(store: Store, repo: string | null, taskId: string | null): EffectiveCheckLevel;
|
|
49
|
+
/** The command a level runs. Quick without its own command runs the full one
|
|
50
|
+
* (and says so: the level that ran is Full). Off runs nothing. */
|
|
51
|
+
export declare function checkCommandFor(store: Store, repo: string, level: CheckLevel): {
|
|
52
|
+
level: CheckLevel;
|
|
53
|
+
command: VerifyCommand | null;
|
|
54
|
+
};
|
|
55
|
+
/** The builder records which level a run used, so its result says it honestly. */
|
|
56
|
+
export declare function recordRunCheckLevel(store: Store, run: {
|
|
57
|
+
id: number;
|
|
58
|
+
taskId: string;
|
|
59
|
+
repo: string | null;
|
|
60
|
+
}, level: CheckLevel, from: EffectiveCheckLevel["from"], now: Date): void;
|
|
61
|
+
/** The level a finished run's checks used; null for a run from before levels. */
|
|
62
|
+
export declare function runCheckLevel(store: Store, runId: number): CheckLevel | null;
|
|
63
|
+
/** A starting point for the quick command, from the project's own scripts.
|
|
64
|
+
* It is only a suggestion: a person approves the exact command. */
|
|
65
|
+
export declare function suggestQuickCommand(repo: string): string | null;
|
|
66
|
+
/** "skip the tests", "run the full checks": a filing request in chat, as a level. */
|
|
67
|
+
export declare function checkLevelFromWords(text: string): CheckLevel | null;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/** Check levels: how much of the project check runs after a build.
|
|
2
|
+
*
|
|
3
|
+
* - Quick: a fast check while work is in progress (typecheck plus the tests
|
|
4
|
+
* near the change). New projects start here.
|
|
5
|
+
* - Full: the project's approved check, as before. Projects that already
|
|
6
|
+
* had a check keep it.
|
|
7
|
+
* - Off: no check. The result reads Ready for review, with "Checks: Off for this project" underneath.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here needs a new table. A project's level and a task's choice are
|
|
10
|
+
* append-only ledger entries (the newest one wins, and the ledger already
|
|
11
|
+
* says who changed what, when). The quick command is a second approved
|
|
12
|
+
* verification grant with its own digest, kept under a `quick:` key beside
|
|
13
|
+
* the full one, so approving it is the same ceremony as `verify set`. */
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { approvalOf } from "./scope.js";
|
|
17
|
+
export const CHECK_LEVELS = ["quick", "full", "off"];
|
|
18
|
+
export const isCheckLevel = (value) => typeof value === "string" && CHECK_LEVELS.includes(value);
|
|
19
|
+
export const CHECK_LEVEL_WORDS = { quick: "Quick", full: "Full", off: "Off" };
|
|
20
|
+
export const CHECK_LEVEL_HINTS = {
|
|
21
|
+
quick: "A fast check while building: typecheck and the tests near the change.",
|
|
22
|
+
full: "The project's full check after every build.",
|
|
23
|
+
off: "No check. Results are Ready for review, and say no check ran.",
|
|
24
|
+
};
|
|
25
|
+
/** Ledger actions. The ledger is append-only, so the newest entry is the setting. */
|
|
26
|
+
export const PROJECT_LEVEL_ACTION = "check level changed";
|
|
27
|
+
export const TASK_LEVEL_ACTION = "task checks chosen";
|
|
28
|
+
export const RUN_LEVEL_ACTION = "checks used";
|
|
29
|
+
/** The quick command's grant lives beside the full one under its own key. */
|
|
30
|
+
export const quickVerifyKey = (repo) => `quick:${repo}`;
|
|
31
|
+
export function liveQuickCommand(store, repo) {
|
|
32
|
+
return store.liveVerifyCommand(quickVerifyKey(repo));
|
|
33
|
+
}
|
|
34
|
+
/** Ledger reads go through the repo index: `repo = ?` (or IS NULL for unplaced work). */
|
|
35
|
+
export const repoClause = (repo) => repo === null ? ["repo IS NULL", []] : ["repo = ?", [repo]];
|
|
36
|
+
export function repoOfRun(store, runId) {
|
|
37
|
+
const run = store.getRun(runId);
|
|
38
|
+
return run === null ? null : store.refById(run.taskRef)?.repo ?? null;
|
|
39
|
+
}
|
|
40
|
+
function newest(store, sql, ...params) {
|
|
41
|
+
const row = store.handle.prepare(sql).get(...params);
|
|
42
|
+
return row === undefined ? null : { actor: String(row["actor"]), at: String(row["at"]), outcome: String(row["outcome"]) };
|
|
43
|
+
}
|
|
44
|
+
/** A project's level. With nothing recorded it is Full: a project that existed
|
|
45
|
+
* before levels keeps exactly the check it had. New projects are stamped Quick
|
|
46
|
+
* when they are added (stampNewProjectLevel). */
|
|
47
|
+
export function projectCheckLevel(store, repo) {
|
|
48
|
+
const row = newest(store, "SELECT actor, at, outcome FROM action_ledger WHERE repo = ? AND task_id IS NULL AND action = ? ORDER BY id DESC LIMIT 1", repo, PROJECT_LEVEL_ACTION);
|
|
49
|
+
if (row === null || !isCheckLevel(row.outcome))
|
|
50
|
+
return { level: "full", setBy: null, at: null };
|
|
51
|
+
return { level: row.outcome, setBy: row.actor, at: row.at };
|
|
52
|
+
}
|
|
53
|
+
/** An approver's act: set the project's level. The ledger keeps before → after. */
|
|
54
|
+
export function setProjectCheckLevel(store, repo, level, by, now) {
|
|
55
|
+
const before = projectCheckLevel(store, repo);
|
|
56
|
+
if (before.level === level && before.setBy !== null)
|
|
57
|
+
return { changed: false, before: before.level };
|
|
58
|
+
store.recordAction({ at: now.toISOString(), actor: by, repo, taskId: null, runId: null, action: PROJECT_LEVEL_ACTION, outcome: level, source: "policy",
|
|
59
|
+
detail: `${CHECK_LEVEL_WORDS[before.level]} → ${CHECK_LEVEL_WORDS[level]}` });
|
|
60
|
+
return { changed: before.level !== level, before: before.level };
|
|
61
|
+
}
|
|
62
|
+
/** A project added from now on starts on Quick. One that already has a level,
|
|
63
|
+
* or an approved check from before levels existed, is left exactly as it is. */
|
|
64
|
+
export function stampNewProjectLevel(store, repo, now) {
|
|
65
|
+
const known = newest(store, "SELECT actor, at, outcome FROM action_ledger WHERE repo = ? AND task_id IS NULL AND action = ? LIMIT 1", repo, PROJECT_LEVEL_ACTION);
|
|
66
|
+
if (known !== null)
|
|
67
|
+
return false;
|
|
68
|
+
const hadCheck = store.handle.prepare("SELECT 1 FROM verify_command WHERE repo = ? LIMIT 1").get(repo) !== undefined;
|
|
69
|
+
if (hadCheck)
|
|
70
|
+
return false;
|
|
71
|
+
store.recordAction({ at: now.toISOString(), actor: "system", repo, taskId: null, runId: null, action: PROJECT_LEVEL_ACTION, outcome: "quick", source: "policy",
|
|
72
|
+
detail: "New project → Quick" });
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/** A task's own choice, if it made one; a revision inherits the task it revises. */
|
|
76
|
+
export function taskCheckLevel(store, taskId) {
|
|
77
|
+
let id = taskId;
|
|
78
|
+
for (let depth = 0; id !== null && depth < 64; depth++) {
|
|
79
|
+
const ref = store.lookupRef(id);
|
|
80
|
+
const [where, params] = repoClause(ref?.repo ?? null);
|
|
81
|
+
const row = newest(store, `SELECT actor, at, outcome FROM action_ledger WHERE ${where} AND task_id = ? AND action = ? ORDER BY id DESC LIMIT 1`, ...params, id, TASK_LEVEL_ACTION);
|
|
82
|
+
if (row !== null && isCheckLevel(row.outcome))
|
|
83
|
+
return row.outcome;
|
|
84
|
+
id = ref?.revisionOf ?? null;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
/** A filing choice: this task's checks, overriding the project. Fixed once its
|
|
89
|
+
* plan is approved, so what the approver saw is what runs. */
|
|
90
|
+
export function setTaskCheckLevel(store, taskId, level, by, now) {
|
|
91
|
+
const ref = store.lookupRef(taskId);
|
|
92
|
+
if (ref === null)
|
|
93
|
+
return { ok: false, message: `No task ${taskId}.` };
|
|
94
|
+
if (approvalOf(store.getScope(taskId)).approved) {
|
|
95
|
+
return { ok: false, message: "This task's plan is already approved, so its checks are fixed. File a new task to change them." };
|
|
96
|
+
}
|
|
97
|
+
if (taskCheckLevel(store, taskId) === level)
|
|
98
|
+
return { ok: true };
|
|
99
|
+
store.recordAction({ at: now.toISOString(), actor: by, repo: ref.repo, taskId, runId: null, action: TASK_LEVEL_ACTION, outcome: level, source: "request",
|
|
100
|
+
detail: `Checks for this task: ${CHECK_LEVEL_WORDS[level]}` });
|
|
101
|
+
return { ok: true };
|
|
102
|
+
}
|
|
103
|
+
export function effectiveCheckLevel(store, repo, taskId) {
|
|
104
|
+
const own = taskId === null ? null : taskCheckLevel(store, taskId);
|
|
105
|
+
if (own !== null)
|
|
106
|
+
return { level: own, from: "task" };
|
|
107
|
+
return { level: repo === null ? "full" : projectCheckLevel(store, repo).level, from: "project" };
|
|
108
|
+
}
|
|
109
|
+
/** The command a level runs. Quick without its own command runs the full one
|
|
110
|
+
* (and says so: the level that ran is Full). Off runs nothing. */
|
|
111
|
+
export function checkCommandFor(store, repo, level) {
|
|
112
|
+
if (level === "off")
|
|
113
|
+
return { level: "off", command: null };
|
|
114
|
+
if (level === "quick") {
|
|
115
|
+
const quick = liveQuickCommand(store, repo);
|
|
116
|
+
if (quick !== null)
|
|
117
|
+
return { level: "quick", command: quick };
|
|
118
|
+
}
|
|
119
|
+
return { level: "full", command: store.liveVerifyCommand(repo) };
|
|
120
|
+
}
|
|
121
|
+
/** The builder records which level a run used, so its result says it honestly. */
|
|
122
|
+
export function recordRunCheckLevel(store, run, level, from, now) {
|
|
123
|
+
store.recordAction({ at: now.toISOString(), actor: "system", repo: run.repo, taskId: run.taskId, runId: run.id, action: RUN_LEVEL_ACTION, outcome: level, source: "work",
|
|
124
|
+
detail: `${CHECK_LEVEL_WORDS[level]} checks (${from === "task" ? "this task's choice" : "project setting"})` });
|
|
125
|
+
}
|
|
126
|
+
/** The level a finished run's checks used; null for a run from before levels. */
|
|
127
|
+
export function runCheckLevel(store, runId) {
|
|
128
|
+
const [where, params] = repoClause(repoOfRun(store, runId));
|
|
129
|
+
const row = newest(store, `SELECT actor, at, outcome FROM action_ledger WHERE ${where} AND run_id = ? AND action = ? ORDER BY id DESC LIMIT 1`, ...params, runId, RUN_LEVEL_ACTION);
|
|
130
|
+
return row !== null && isCheckLevel(row.outcome) ? row.outcome : null;
|
|
131
|
+
}
|
|
132
|
+
/** A starting point for the quick command, from the project's own scripts.
|
|
133
|
+
* It is only a suggestion: a person approves the exact command. */
|
|
134
|
+
export function suggestQuickCommand(repo) {
|
|
135
|
+
const file = join(repo, "package.json");
|
|
136
|
+
if (!existsSync(file))
|
|
137
|
+
return null;
|
|
138
|
+
let scripts = {};
|
|
139
|
+
let deps = {};
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
142
|
+
scripts = parsed.scripts ?? {};
|
|
143
|
+
deps = { ...(parsed.dependencies ?? {}), ...(parsed.devDependencies ?? {}) };
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
const has = (name) => typeof scripts[name] === "string";
|
|
149
|
+
const runner = existsSync(join(repo, "pnpm-lock.yaml")) ? "pnpm" : existsSync(join(repo, "yarn.lock")) ? "yarn" : "npm";
|
|
150
|
+
const run = (name) => runner === "npm" ? `npm run ${name}` : `${runner} ${name}`;
|
|
151
|
+
const exec = runner === "npm" ? "npx" : `${runner} exec`;
|
|
152
|
+
const typecheck = ["typecheck", "type-check", "check-types", "tsc"].find(has) ?? null;
|
|
153
|
+
const changed = "$(git diff --name-only HEAD~1)";
|
|
154
|
+
const tests = "vitest" in deps ? `${exec} vitest related --run ${changed}`
|
|
155
|
+
: "jest" in deps ? `${exec} jest --findRelatedTests --passWithNoTests ${changed}` : null;
|
|
156
|
+
const parts = [typecheck === null ? null : run(typecheck), tests].filter((one) => one !== null);
|
|
157
|
+
if (parts.length > 0)
|
|
158
|
+
return parts.join(" && ");
|
|
159
|
+
return has("lint") ? run("lint") : null;
|
|
160
|
+
}
|
|
161
|
+
/** "skip the tests", "run the full checks": a filing request in chat, as a level. */
|
|
162
|
+
export function checkLevelFromWords(text) {
|
|
163
|
+
const said = text.toLowerCase().replace(/[’']/g, "'");
|
|
164
|
+
if (/\b(skip|without|no|don't run|do not run|turn off|switch off)\b[^.!?\n]{0,20}\b(the )?(tests|checks)\b/.test(said) || /\b(tests|checks) off\b/.test(said))
|
|
165
|
+
return "off";
|
|
166
|
+
if (/\b(full|all the|every|complete)\s+(test|check)s?\b/.test(said))
|
|
167
|
+
return "full";
|
|
168
|
+
if (/\bquick\s+(test|check)s?\b/.test(said))
|
|
169
|
+
return "quick";
|
|
170
|
+
return null;
|
|
171
|
+
}
|
package/dist/claim.d.ts
CHANGED
|
@@ -411,6 +411,28 @@ export declare function finalizeInterruptedFenced(store: Store, args: {
|
|
|
411
411
|
committed?: boolean;
|
|
412
412
|
now: Date;
|
|
413
413
|
}): InterruptSeal;
|
|
414
|
+
/**
|
|
415
|
+
* Hand an attempt back because the SERVICE stopped (its builder is
|
|
416
|
+
* stopping or restarting), not because anyone stopped the task: one fenced
|
|
417
|
+
* transaction releases the claim as `interrupted`, ends the run (and every
|
|
418
|
+
* open run it owns) as `failed` / `interrupted` — the words dead-runner
|
|
419
|
+
* recovery writes, so the next attempt inherits the preserved work in the
|
|
420
|
+
* same checkout exactly as after a crash — and returns the task to the
|
|
421
|
+
* queue with no strike, no backoff and no hold, so the next builder pass
|
|
422
|
+
* resumes it. A lease the world moved past still ends the run, touching no
|
|
423
|
+
* task state.
|
|
424
|
+
*/
|
|
425
|
+
export declare function finalizeHandBackFenced(store: Store, args: {
|
|
426
|
+
leaseId: string;
|
|
427
|
+
runId: number;
|
|
428
|
+
taskId: string;
|
|
429
|
+
message: string;
|
|
430
|
+
now: Date;
|
|
431
|
+
}): {
|
|
432
|
+
ok: true;
|
|
433
|
+
requeued: boolean;
|
|
434
|
+
fenced: boolean;
|
|
435
|
+
};
|
|
414
436
|
/**
|
|
415
437
|
* THE STOP FENCE at settlement (v52): when a stop applies to the run —
|
|
416
438
|
* its own or an owning ancestor's — seal the attempt as interrupted and
|
package/dist/claim.js
CHANGED
|
@@ -1003,6 +1003,49 @@ export function finalizeInterruptedFenced(store, args) {
|
|
|
1003
1003
|
return { ok: true, stopRun, requeued: Number(requeued.changes) > 0, fenced: false };
|
|
1004
1004
|
});
|
|
1005
1005
|
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Hand an attempt back because the SERVICE stopped (its builder is
|
|
1008
|
+
* stopping or restarting), not because anyone stopped the task: one fenced
|
|
1009
|
+
* transaction releases the claim as `interrupted`, ends the run (and every
|
|
1010
|
+
* open run it owns) as `failed` / `interrupted` — the words dead-runner
|
|
1011
|
+
* recovery writes, so the next attempt inherits the preserved work in the
|
|
1012
|
+
* same checkout exactly as after a crash — and returns the task to the
|
|
1013
|
+
* queue with no strike, no backoff and no hold, so the next builder pass
|
|
1014
|
+
* resumes it. A lease the world moved past still ends the run, touching no
|
|
1015
|
+
* task state.
|
|
1016
|
+
*/
|
|
1017
|
+
export function finalizeHandBackFenced(store, args) {
|
|
1018
|
+
const { leaseId, runId, taskId, now } = args;
|
|
1019
|
+
const db = store.handle;
|
|
1020
|
+
return inTransaction(store, () => {
|
|
1021
|
+
const run = store.getRun(runId);
|
|
1022
|
+
if (run === null || run.leaseId !== leaseId || run.outcome !== null) {
|
|
1023
|
+
throw new Error(`run ${runId} is not ${leaseId}'s open attempt — a hand-back seals exactly one`);
|
|
1024
|
+
}
|
|
1025
|
+
const { changes } = db
|
|
1026
|
+
.prepare(`UPDATE claim SET released_at = ?, released_by = 'interrupted'
|
|
1027
|
+
WHERE lease_id = ? AND released_at IS NULL AND ${NOT_SUPERSEDED}`)
|
|
1028
|
+
.run(now.toISOString(), leaseId);
|
|
1029
|
+
const fenced = Number(changes) === 0;
|
|
1030
|
+
if (args.message.trim() !== "")
|
|
1031
|
+
store.recordOutcomeFacts(runId, { handoff: args.message });
|
|
1032
|
+
for (const owned of store.ownedRunsOf(runId)) {
|
|
1033
|
+
if (owned === runId)
|
|
1034
|
+
continue;
|
|
1035
|
+
const child = store.getRun(owned);
|
|
1036
|
+
if (child !== null && child.outcome === null)
|
|
1037
|
+
store.finishRun(owned, { outcome: "failed", reason: "interrupted", now });
|
|
1038
|
+
}
|
|
1039
|
+
store.finishRun(runId, { outcome: "failed", reason: "interrupted", now });
|
|
1040
|
+
if (fenced)
|
|
1041
|
+
return { ok: true, requeued: false, fenced: true };
|
|
1042
|
+
const requeued = db
|
|
1043
|
+
.prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'")
|
|
1044
|
+
.run(now.toISOString(), taskId);
|
|
1045
|
+
store.bumpWake();
|
|
1046
|
+
return { ok: true, requeued: Number(requeued.changes) > 0, fenced: false };
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1006
1049
|
/**
|
|
1007
1050
|
* THE STOP FENCE at settlement (v52): when a stop applies to the run —
|
|
1008
1051
|
* its own or an owning ancestor's — seal the attempt as interrupted and
|
package/dist/cli.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ export type ParseResult = {
|
|
|
39
39
|
error: string;
|
|
40
40
|
};
|
|
41
41
|
type Write = (line: string) => void;
|
|
42
|
-
export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll [path...] report what is in flight\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> connect one (no path: the repo you are in)\n toolroll repos remove <path>\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll session native coding sessions through the running service\n toolroll connect save a private connection to your central service\n toolroll lead named leads on the connected service\n toolroll conversation shared and private conversations on that service\n toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to the mate: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
|
|
42
|
+
export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll [path...] report what is in flight\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> connect one (no path: the repo you are in)\n toolroll repos remove <path>\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll session native coding sessions through the running service\n toolroll connect save a private connection to your central service\n toolroll lead named leads on the connected service\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll conversation shared and private conversations on that service\n toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to the mate: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
|
|
43
43
|
export declare function parseArgs(argv: readonly string[]): ParseResult;
|
|
44
44
|
/** The commands that operate the queue rather than report on the world. */
|
|
45
45
|
/** Every verb the CLI routes OUTSIDE runOperate. Exported (with
|