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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Started only by `toolroll up` (up-processes.ts): one of its two roles, the console or the worker, in a process of
|
|
3
|
+
* its own. It takes its whole start from the parent over the IPC channel (never from arguments or the environment, so
|
|
4
|
+
* the runner token is in no process listing), and stops when the parent says so or goes away.
|
|
5
|
+
*/
|
|
6
|
+
import { parentMessage, startMessage } from "./up-processes.js";
|
|
7
|
+
if (process.send === undefined || !process.connected) {
|
|
8
|
+
process.stderr.write("This process is started by `toolroll up`; run `toolroll up` instead.\n");
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
// node:sqlite still says it is experimental; the CLI hushes exactly that notice, and so does a role.
|
|
12
|
+
const emitWarning = process.emitWarning.bind(process);
|
|
13
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
14
|
+
const type = typeof rest[0] === "string" ? rest[0] : rest[0]?.type;
|
|
15
|
+
if (type === "ExperimentalWarning" && (warning instanceof Error ? warning.message : String(warning)).includes("SQLite"))
|
|
16
|
+
return;
|
|
17
|
+
emitWarning(warning, ...rest);
|
|
18
|
+
});
|
|
19
|
+
const handlers = [];
|
|
20
|
+
const stops = [];
|
|
21
|
+
let stopRequested = false;
|
|
22
|
+
const stop = (hard) => {
|
|
23
|
+
stopRequested = true;
|
|
24
|
+
for (const one of stops)
|
|
25
|
+
one(hard);
|
|
26
|
+
};
|
|
27
|
+
const port = {
|
|
28
|
+
send: (message) => { if (process.connected) {
|
|
29
|
+
try {
|
|
30
|
+
process.send?.(message);
|
|
31
|
+
}
|
|
32
|
+
catch { /* the parent is gone; its loss stops this role */ }
|
|
33
|
+
} },
|
|
34
|
+
onMessage: handler => { handlers.push(handler); },
|
|
35
|
+
onStop: handler => { stops.push(handler); if (stopRequested)
|
|
36
|
+
handler(false); },
|
|
37
|
+
};
|
|
38
|
+
// A terminal's Ctrl-C reaches the whole process group: the parent decides what a signal means (drain, then a hard
|
|
39
|
+
// stop on the second), so a role never acts on SIGINT itself. SIGTERM (the supervisor's stop) drains, once.
|
|
40
|
+
process.on("SIGINT", () => { });
|
|
41
|
+
process.on("SIGTERM", () => stop(false));
|
|
42
|
+
// The parent gone (its channel closed): drain, as a stop would. The worker's own grace timer bounds it.
|
|
43
|
+
process.once("disconnect", () => stop(false));
|
|
44
|
+
const start = await new Promise(resolve => {
|
|
45
|
+
const timer = setTimeout(() => resolve(null), 60_000);
|
|
46
|
+
process.once("message", value => { clearTimeout(timer); resolve(value); });
|
|
47
|
+
});
|
|
48
|
+
const checked = startMessage(start);
|
|
49
|
+
// The role named on the command line (for a process listing) and the one started must be the same.
|
|
50
|
+
if (checked === null || checked.role !== process.argv[2]) {
|
|
51
|
+
process.stderr.write("The start message from `toolroll up` was not understood.\n");
|
|
52
|
+
process.exit(2);
|
|
53
|
+
}
|
|
54
|
+
process.on("message", value => {
|
|
55
|
+
const message = parentMessage(value);
|
|
56
|
+
if (message === null)
|
|
57
|
+
return;
|
|
58
|
+
if (message.type === "hard-stop") {
|
|
59
|
+
stop(true);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
for (const one of handlers)
|
|
63
|
+
one(message);
|
|
64
|
+
});
|
|
65
|
+
const { runUpRole } = await import("./operate.js");
|
|
66
|
+
let code = 1;
|
|
67
|
+
try {
|
|
68
|
+
code = await runUpRole(checked, port);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
port.send({ type: "fatal", detail: error instanceof Error ? error.message : String(error) });
|
|
72
|
+
}
|
|
73
|
+
// Everything this role owned is closed; leave promptly even if a stray handle lingers.
|
|
74
|
+
if (process.connected)
|
|
75
|
+
process.disconnect?.();
|
|
76
|
+
setTimeout(() => process.exit(code), 200).unref();
|
|
77
|
+
process.exitCode = code;
|
|
@@ -29,7 +29,10 @@ export function effectiveCheckFacts(raw, facts) {
|
|
|
29
29
|
if (facts === null || coveragePasses(facts.coverage))
|
|
30
30
|
return { result: raw, facts };
|
|
31
31
|
const exitCode = raw.configured && raw.ran ? raw.exitCode : null;
|
|
32
|
-
|
|
32
|
+
// A check that stopped before it could report keeps its own failure (it timed out, Toolroll stopped it): that is why.
|
|
33
|
+
if (facts.coverage === "no-result" && raw.configured && !raw.ran)
|
|
34
|
+
return { result: raw, facts };
|
|
35
|
+
return { result: { configured: true, ran: false, attemptFailed: true, failure: facts.coverage === "no-result" ? "no-result" : "coverage-incomplete" }, facts: { ...facts, check: { ...facts.check, exitCode } } };
|
|
33
36
|
}
|
|
34
37
|
export function sealVerificationReceipt(store, root, runId, head, command, result, now, reusedFrom, release) {
|
|
35
38
|
const source = store.getRun(runId);
|
|
@@ -142,6 +145,15 @@ export function verificationEvidence(store, root, runId) {
|
|
|
142
145
|
const sha = (capture) => { const found = artifacts.filter(a => a.kind === "structured-output" && a.capture === capture); return found.length === 1 ? found[0].sha256 : null; };
|
|
143
146
|
if (sha(CHECK_REPORT_CAPTURE) !== checked.check.report || sha(CHECK_INVENTORY_CAPTURE) !== checked.check.inventory)
|
|
144
147
|
return fail("The release check's sealed report or inventory changed.");
|
|
148
|
+
// Their bytes too, not only their records (eval-harness review 1, finding 3): a report or inventory the receipt
|
|
149
|
+
// names that is missing, cut short or changed on disk no longer verifies.
|
|
150
|
+
for (const capture of [CHECK_REPORT_CAPTURE, CHECK_INVENTORY_CAPTURE]) {
|
|
151
|
+
const one = artifacts.find(a => a.kind === "structured-output" && a.capture === capture);
|
|
152
|
+
if (one === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
if (one.truncated || one.redacted || one.captureStatus !== "ok" || !readVerifiedArtifact(root, one).ok)
|
|
155
|
+
return fail("The release check's sealed report or inventory no longer verifies.");
|
|
156
|
+
}
|
|
145
157
|
if ((checked.coverage === "composed") !== (checked.composed !== undefined))
|
|
146
158
|
return fail("The release check's coverage and its links disagree.");
|
|
147
159
|
// A check that didn't cover what it owed can never read as a pass.
|
package/dist/worktree.d.ts
CHANGED
|
@@ -152,6 +152,10 @@ export declare class WorktreePool {
|
|
|
152
152
|
private readonly store;
|
|
153
153
|
private readonly options;
|
|
154
154
|
private readonly runner;
|
|
155
|
+
/** The pool's own git in a project's repository and its checkouts (making, moving and reading them): hardened against
|
|
156
|
+
* a frozen view of the repository's config and info (teardown-git.ts checkoutGit), which agents can write. An injected
|
|
157
|
+
* runner (tests' stubbed git) is used as given. */
|
|
158
|
+
private readonly hardened;
|
|
155
159
|
/** Carried to the independent storage sweep process. */
|
|
156
160
|
get root(): string;
|
|
157
161
|
constructor(store: Store, options: PoolOptions);
|
package/dist/worktree.js
CHANGED
|
@@ -32,7 +32,7 @@ import { HANDOFF_PREFIX, MAILBOX_SUFFIX, looksLikeProtocolFile, readMailbox } fr
|
|
|
32
32
|
import { parseHandoff } from "./decision.js";
|
|
33
33
|
import { diskBytes } from "./storage.js";
|
|
34
34
|
import { markNeverIndex } from "./never-index.js";
|
|
35
|
-
import { teardownGit } from "./teardown-git.js";
|
|
35
|
+
import { checkoutGit, teardownGit } from "./teardown-git.js";
|
|
36
36
|
/**
|
|
37
37
|
* The note a lease leaves in the checkout, naming the process holding it.
|
|
38
38
|
*
|
|
@@ -268,12 +268,18 @@ export class WorktreePool {
|
|
|
268
268
|
store;
|
|
269
269
|
options;
|
|
270
270
|
runner;
|
|
271
|
+
/** The pool's own git in a project's repository and its checkouts (making, moving and reading them): hardened against
|
|
272
|
+
* a frozen view of the repository's config and info (teardown-git.ts checkoutGit), which agents can write. An injected
|
|
273
|
+
* runner (tests' stubbed git) is used as given. */
|
|
274
|
+
hardened;
|
|
271
275
|
/** Carried to the independent storage sweep process. */
|
|
272
276
|
get root() { return this.options.root; }
|
|
273
277
|
constructor(store, options) {
|
|
274
278
|
this.store = store;
|
|
275
279
|
this.options = options;
|
|
276
280
|
this.runner = options.runner ?? run;
|
|
281
|
+
// Bound to the project's repository from the pool's own request or record (teardown-git.ts GitBinding).
|
|
282
|
+
this.hardened = repo => this.runner === run ? checkoutGit(run, "build", { repo }) : this.runner;
|
|
277
283
|
}
|
|
278
284
|
/**
|
|
279
285
|
* Take a working copy for this branch.
|
|
@@ -475,7 +481,7 @@ export class WorktreePool {
|
|
|
475
481
|
// checkout between the inspection and the move.
|
|
476
482
|
let base;
|
|
477
483
|
if (request.freshBase !== undefined && request.taskRef !== undefined) {
|
|
478
|
-
const fresh = await this.freshen(path, request.branch, request.taskRef, request.freshBase, request.now);
|
|
484
|
+
const fresh = await this.freshen(request.repo, path, request.branch, request.taskRef, request.freshBase, request.now);
|
|
479
485
|
if (!fresh.ok)
|
|
480
486
|
return { ok: false, reason: "git", message: fresh.message };
|
|
481
487
|
base = fresh.standing;
|
|
@@ -524,9 +530,9 @@ export class WorktreePool {
|
|
|
524
530
|
* untracked. The move is `git reset --keep`, which refuses rather than overwrite anything; HEAD and the tree are read
|
|
525
531
|
* again right before and after it, and any surprise fails the lease with the checkout left for a person.
|
|
526
532
|
*/
|
|
527
|
-
async freshen(path, branch, taskRef, fresh, now) {
|
|
533
|
+
async freshen(repo, path, branch, taskRef, fresh, now) {
|
|
528
534
|
const read = async (args) => {
|
|
529
|
-
const out = await this.
|
|
535
|
+
const out = await this.hardened(repo)(GIT, [...READ_ONLY, ...args], { cwd: path, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
530
536
|
return out.code === 0 ? out.stdout.trim() : null;
|
|
531
537
|
};
|
|
532
538
|
const main = await read(["rev-parse", "--verify", "--quiet", `${fresh.main}^{commit}`]);
|
|
@@ -544,14 +550,14 @@ export class WorktreePool {
|
|
|
544
550
|
return stay("not on its branch");
|
|
545
551
|
if (fresh.recordedBase !== null && fresh.recordedBase !== head)
|
|
546
552
|
return stay("has commits");
|
|
547
|
-
const contained = await this.
|
|
553
|
+
const contained = await this.hardened(repo)(GIT, [...READ_ONLY, "merge-base", "--is-ancestor", head, main], { cwd: path, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
548
554
|
if (contained.code !== 0)
|
|
549
555
|
return stay("has commits");
|
|
550
556
|
if (await this.isDirty(path) !== false)
|
|
551
557
|
return stay("has changes");
|
|
552
558
|
// Recorded before git moves it: a move that then fails leaves the next attempt's own base as the branch's origin.
|
|
553
559
|
this.store.recordBranchBaseMove(taskRef, branch, head, main, now);
|
|
554
|
-
const moved = await this.
|
|
560
|
+
const moved = await this.hardened(repo)(GIT, ["reset", "--keep", main], { cwd: path, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
555
561
|
const after = await read(["rev-parse", "--verify", "--quiet", "HEAD^{commit}"]);
|
|
556
562
|
const branchNow = await read(["rev-parse", "--verify", "--quiet", `refs/heads/${branch}^{commit}`]);
|
|
557
563
|
if (moved.code !== 0 || after !== main || branchNow !== main || await this.isDirty(path) !== false) {
|
|
@@ -1052,7 +1058,7 @@ export class WorktreePool {
|
|
|
1052
1058
|
/** All disposal and inspection paths share the hardened teardown boundary. */
|
|
1053
1059
|
teardown = (cwd, args) => teardownGit(this.runner)(cwd, args);
|
|
1054
1060
|
git(cwd, args, prefix = []) {
|
|
1055
|
-
return this.
|
|
1061
|
+
return this.hardened(cwd)(GIT, [...prefix, ...args], { cwd, timeoutMs: WORKTREE_TIMEOUT_MS });
|
|
1056
1062
|
}
|
|
1057
1063
|
}
|
|
1058
1064
|
function firstLine(text) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.77",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"test:slow": "vitest run .slow.test",
|
|
12
12
|
"test:all": "vitest run",
|
|
13
13
|
"test:attacks": "vitest run --config test/attacks/vitest.config.ts",
|
|
14
|
+
"test:git": "vitest run test/attacks/checkout-git- test/attacks/fence-git-views test/attacks/repository-context-git test/attacks/discover-dirty src/teardown-git.test.ts src/worktree.slow.test.ts",
|
|
14
15
|
"check:quick": "node scripts/quick-check.mjs",
|
|
15
16
|
"dev": "tsx src/cli.ts",
|
|
16
17
|
"typecheck": "tsc --noEmit && tsc -p tsconfig.browser.json",
|