toolroll 0.9.93 → 0.9.94
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 +68 -0
- package/dist/activity-line.d.ts +7 -4
- package/dist/activity-line.js +9 -7
- package/dist/agent-fence.d.ts +4 -0
- package/dist/agent-fence.js +4 -1
- package/dist/approval-authority.d.ts +7 -1
- package/dist/approval-authority.js +11 -2
- package/dist/approval-password.d.ts +31 -0
- package/dist/approval-password.js +26 -0
- package/dist/assignment-ui.js +1 -1
- package/dist/assignment.js +5 -0
- package/dist/boot-identity.d.ts +2 -1
- package/dist/boot-identity.js +6 -4
- package/dist/bounded-spawn.d.ts +57 -0
- package/dist/bounded-spawn.js +220 -0
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-shell.d.ts +1 -1
- package/dist/browser-shell.js +2 -2
- package/dist/browser-workspace.d.ts +45 -2
- package/dist/browser-workspace.js +3 -3
- package/dist/builder-restart.d.ts +21 -0
- package/dist/builder-restart.js +50 -0
- package/dist/builder.d.ts +4 -0
- package/dist/builder.js +53 -24
- package/dist/chat-acceptance.js +1 -1
- package/dist/chat-actions.d.ts +12 -2
- package/dist/chat-actions.js +25 -2
- package/dist/chat-channel.js +11 -0
- package/dist/chat-decide.d.ts +7 -2
- package/dist/chat-decide.js +37 -9
- package/dist/chat-quiet.js +3 -3
- package/dist/chat-voice.d.ts +1 -1
- package/dist/chat-voice.js +2 -2
- package/dist/check-facts.d.ts +3 -1
- package/dist/check-facts.js +57 -11
- package/dist/check-failure.d.ts +22 -3
- package/dist/check-failure.js +57 -13
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +10 -1
- package/dist/check-load.d.ts +85 -0
- package/dist/check-load.js +147 -0
- package/dist/check-only-standing.d.ts +36 -0
- package/dist/check-only-standing.js +39 -0
- package/dist/check-only.d.ts +15 -0
- package/dist/check-only.js +99 -0
- package/dist/check-report.d.ts +2 -0
- package/dist/check-report.js +1 -1
- package/dist/check-stamp.d.ts +46 -0
- package/dist/check-stamp.js +93 -0
- package/dist/checkout-cleanup.d.ts +36 -5
- package/dist/checkout-cleanup.js +166 -14
- package/dist/cli-help.d.ts +4 -1
- package/dist/cli-help.js +47 -30
- package/dist/cli.js +11 -1
- package/dist/code-extract.d.ts +44 -0
- package/dist/code-extract.js +88 -0
- package/dist/code-map-acts.d.ts +24 -0
- package/dist/code-map-acts.js +53 -0
- package/dist/code-map-admission.d.ts +52 -0
- package/dist/code-map-admission.js +295 -0
- package/dist/code-map-build.d.ts +63 -0
- package/dist/code-map-build.js +415 -0
- package/dist/code-map-cli.d.ts +153 -0
- package/dist/code-map-cli.js +103 -0
- package/dist/code-map-main.d.ts +1 -0
- package/dist/code-map-main.js +48 -0
- package/dist/code-map-read.d.ts +106 -0
- package/dist/code-map-read.js +171 -0
- package/dist/code-map-report.d.ts +37 -0
- package/dist/code-map-report.js +95 -0
- package/dist/code-map-shape.d.ts +172 -0
- package/dist/code-map-shape.js +327 -0
- package/dist/code-map-store.d.ts +136 -0
- package/dist/code-map-store.js +233 -0
- package/dist/coding-provider.d.ts +2 -0
- package/dist/coding-provider.js +3 -2
- package/dist/coding-update.js +3 -2
- package/dist/coding-workspace.js +4 -4
- package/dist/console-chrome.js +2 -2
- package/dist/containment.d.ts +5 -0
- package/dist/containment.js +14 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +2 -0
- package/dist/contracts/cli.d.ts +52 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +10 -3
- package/dist/contracts/console-api.js +10 -3
- package/dist/contracts/gateway-tools.d.ts +17 -0
- package/dist/contracts/gateway-tools.js +3 -1
- package/dist/contracts/lead-tools.d.ts +61 -0
- package/dist/contracts/lead-tools.js +10 -1
- package/dist/contracts/project-knowledge.d.ts +38 -5
- package/dist/contracts/project-knowledge.js +11 -5
- package/dist/contracts/project-memory.d.ts +3 -1
- package/dist/contracts/project-memory.js +3 -2
- package/dist/contracts/storage.d.ts +14 -0
- package/dist/contracts/storage.js +3 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/youiyoux-request.d.ts +1 -1
- package/dist/contracts/youiyoux-request.js +1 -1
- package/dist/desktop-access.js +6 -2
- package/dist/dispatch.js +18 -15
- package/dist/dispose.js +8 -2
- package/dist/easy-start.js +2 -3
- package/dist/evals.js +2 -1
- package/dist/fast-start.d.ts +136 -0
- package/dist/fast-start.js +431 -0
- package/dist/fast-terms.d.ts +9 -0
- package/dist/fast-terms.js +23 -0
- package/dist/fenced-check-coverage.d.ts +62 -0
- package/dist/fenced-check-coverage.js +54 -0
- package/dist/first-run.js +1 -1
- package/dist/flaky-ci.d.ts +31 -0
- package/dist/flaky-ci.js +149 -0
- package/dist/flaky-cli.d.ts +60 -0
- package/dist/flaky-cli.js +60 -0
- package/dist/flaky.d.ts +75 -9
- package/dist/flaky.js +255 -0
- package/dist/flow-card-task.js +3 -0
- package/dist/flow-triggers.d.ts +1 -1
- package/dist/flow-triggers.js +1 -1
- package/dist/github-import.d.ts +62 -0
- package/dist/github-import.js +146 -0
- package/dist/guides.js +15 -10
- package/dist/invoke.d.ts +5 -1
- package/dist/invoke.js +49 -14
- package/dist/job-object-helper.ps1 +10 -3
- package/dist/keymap.d.ts +41 -0
- package/dist/keymap.js +51 -0
- package/dist/knowledge-cli.js +1 -1
- package/dist/knowledge-ui.d.ts +21 -4
- package/dist/knowledge-ui.js +75 -7
- package/dist/laptop-background.d.ts +16 -0
- package/dist/laptop-background.js +108 -2
- package/dist/lead-context.js +1 -1
- package/dist/lead-contract.d.ts +1 -1
- package/dist/lead-contract.js +2 -2
- package/dist/lead-progress.js +3 -0
- package/dist/lead-status.d.ts +2 -0
- package/dist/lead-status.js +6 -2
- package/dist/lead-tools.js +51 -2
- package/dist/lead.js +1 -1
- package/dist/lesson-notes.d.ts +81 -0
- package/dist/lesson-notes.js +336 -0
- package/dist/limits-ui.js +1 -1
- package/dist/local-owner.d.ts +1 -1
- package/dist/local-owner.js +11 -0
- package/dist/machine-check.d.ts +16 -0
- package/dist/machine-check.js +26 -0
- package/dist/machine-identity.d.ts +75 -0
- package/dist/machine-identity.js +170 -0
- package/dist/mcp-core.js +11 -1
- package/dist/memory-cli.d.ts +300 -1
- package/dist/memory-cli.js +62 -9
- package/dist/memory-pass.d.ts +16 -0
- package/dist/memory-pass.js +67 -3
- package/dist/memory-tidy.d.ts +181 -0
- package/dist/memory-tidy.js +523 -0
- package/dist/memory-use.d.ts +35 -0
- package/dist/memory-use.js +109 -0
- package/dist/merge-checklist.d.ts +55 -0
- package/dist/merge-checklist.js +182 -0
- package/dist/needs-you.d.ts +27 -8
- package/dist/needs-you.js +96 -39
- package/dist/nightly-github.d.ts +2 -0
- package/dist/nightly-github.js +4 -4
- package/dist/operate-remote-arguments.js +6 -1
- package/dist/operate-remote.js +3 -1
- package/dist/operate.d.ts +6 -1
- package/dist/operate.js +402 -52
- package/dist/phase-routing.js +2 -2
- package/dist/plain-schema.d.ts +23 -0
- package/dist/plain-schema.js +94 -0
- package/dist/plain-words.d.ts +20 -0
- package/dist/plain-words.js +66 -0
- package/dist/planner.js +2 -1
- package/dist/process-liveness.d.ts +2 -0
- package/dist/process-liveness.js +61 -17
- package/dist/project-delete-ui.d.ts +7 -2
- package/dist/project-delete-ui.js +12 -1
- package/dist/project-delete.js +8 -1
- package/dist/project-knowledge.d.ts +18 -11
- package/dist/project-knowledge.js +39 -9
- package/dist/project-learning.d.ts +2 -1
- package/dist/project-learning.js +15 -45
- package/dist/project-memory.d.ts +5 -0
- package/dist/project-memory.js +22 -2
- package/dist/proof.js +30 -5
- package/dist/provider-auth.js +1 -1
- package/dist/publish.js +9 -1
- package/dist/pull-request-flow.js +5 -0
- package/dist/push.d.ts +18 -2
- package/dist/push.js +40 -16
- package/dist/release-adapters.d.ts +9 -0
- package/dist/release-adapters.js +76 -0
- package/dist/release-auto.js +13 -13
- package/dist/release-card.d.ts +1 -2
- package/dist/release-card.js +11 -0
- package/dist/release-carry.d.ts +7 -2
- package/dist/release-carry.js +39 -9
- package/dist/release-cli.d.ts +4 -0
- package/dist/release-cli.js +29 -22
- package/dist/release-coverage.js +1 -1
- package/dist/release-next.d.ts +48 -0
- package/dist/release-next.js +161 -0
- package/dist/release.d.ts +20 -1
- package/dist/release.js +76 -8
- package/dist/remote-destination.d.ts +36 -0
- package/dist/remote-destination.js +217 -0
- package/dist/remote-main.d.ts +48 -0
- package/dist/remote-main.js +84 -5
- package/dist/repository-context.d.ts +3 -2
- package/dist/repository-context.js +112 -70
- package/dist/restart-certification.d.ts +1 -0
- package/dist/restart-certification.js +8 -6
- package/dist/result-acts.d.ts +27 -19
- package/dist/result-acts.js +36 -25
- package/dist/result-follow-ups.d.ts +2 -2
- package/dist/result-follow-ups.js +18 -4
- package/dist/result-review.d.ts +1 -1
- package/dist/route-change-check.d.ts +4 -0
- package/dist/route-change-check.js +13 -0
- package/dist/route-change.d.ts +22 -0
- package/dist/route-change.js +89 -0
- package/dist/route-hooks.js +3 -2
- package/dist/route-results.d.ts +6 -1
- package/dist/route-results.js +14 -5
- package/dist/run-closeout.d.ts +1 -1
- package/dist/run-closeout.js +7 -5
- package/dist/runner-process.js +3 -2
- package/dist/runner.js +2 -1
- package/dist/scope.d.ts +6 -1
- package/dist/scope.js +22 -9
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +24 -18
- package/dist/server/chat.js +14 -12
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +26 -35
- package/dist/server/console.css +23 -0
- package/dist/server/guards.js +3 -0
- package/dist/server/home.d.ts +6 -0
- package/dist/server/home.js +21 -13
- package/dist/server/http.js +1 -1
- package/dist/server/kit.js +1 -1
- package/dist/server/merge-ready-view.d.ts +10 -0
- package/dist/server/merge-ready-view.js +30 -0
- package/dist/server/pages-assets.d.ts +1 -0
- package/dist/server/pages-assets.js +8 -5
- package/dist/server/pages.js +103 -82
- package/dist/server/render-chat.js +2 -2
- package/dist/server/render-code-map.d.ts +30 -0
- package/dist/server/render-code-map.js +137 -0
- package/dist/server/render-flaky.d.ts +20 -0
- package/dist/server/render-flaky.js +59 -0
- package/dist/server/render-history.js +1 -1
- package/dist/server/render-new-task.d.ts +26 -1
- package/dist/server/render-new-task.js +30 -5
- package/dist/server/render-pages.d.ts +1 -175
- package/dist/server/render-pages.js +38 -313
- package/dist/server/render-projects.d.ts +2 -1
- package/dist/server/render-projects.js +9 -7
- package/dist/server/render-release.d.ts +2 -0
- package/dist/server/render-release.js +3 -3
- package/dist/server/render-results.d.ts +2 -0
- package/dist/server/render-results.js +67 -67
- package/dist/server/render-tasks.d.ts +13 -2
- package/dist/server/render-tasks.js +153 -136
- package/dist/server/retired-routes.d.ts +17 -0
- package/dist/server/retired-routes.js +53 -0
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +45 -12
- package/dist/server/settings.js +129 -22
- package/dist/server/task-views.js +61 -26
- package/dist/server/tasks.d.ts +2 -6
- package/dist/server/tasks.js +174 -321
- package/dist/server/welcome.js +1 -1
- package/dist/settings-groups-ui.js +1 -1
- package/dist/setup-guide.d.ts +2 -2
- package/dist/stop-proof.js +6 -4
- package/dist/storage-sweep-process.js +1 -1
- package/dist/storage-sweep.d.ts +5 -3
- package/dist/storage-sweep.js +30 -9
- package/dist/storage-ui.js +28 -17
- package/dist/storage.d.ts +27 -2
- package/dist/storage.js +39 -2
- package/dist/store.d.ts +80 -5
- package/dist/store.js +296 -87
- package/dist/surface.js +45 -4
- package/dist/sweep-quarantine.d.ts +1 -0
- package/dist/sweep-quarantine.js +6 -4
- package/dist/tamper-scan.d.ts +4 -0
- package/dist/tamper-scan.js +90 -0
- package/dist/task-aliases.d.ts +5 -0
- package/dist/task-aliases.js +9 -0
- package/dist/task-control.js +4 -1
- package/dist/task-outcome-cli.d.ts +2 -0
- package/dist/task-outcome-cli.js +17 -17
- package/dist/task-sizing.d.ts +5 -0
- package/dist/task-sizing.js +2 -0
- package/dist/task-source.d.ts +7 -0
- package/dist/task-source.js +14 -0
- package/dist/task-status.d.ts +9 -0
- package/dist/task-status.js +40 -49
- package/dist/teardown-git.d.ts +16 -0
- package/dist/teardown-git.js +63 -5
- package/dist/telegram-progress.js +5 -3
- package/dist/telegram-status.js +6 -4
- package/dist/test-infra.d.ts +8 -0
- package/dist/test-infra.js +18 -0
- package/dist/tidy.js +1 -1
- package/dist/toolroll-update.js +17 -5
- package/dist/tui-contract.d.ts +24 -0
- package/dist/tui-source.js +67 -8
- package/dist/tui.js +51 -51
- package/dist/vanished-retry.d.ts +30 -0
- package/dist/vanished-retry.js +174 -0
- package/dist/work-index.d.ts +2 -0
- package/dist/work-index.js +76 -62
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +28 -22
- package/dist/worktree.d.ts +12 -3
- package/dist/worktree.js +68 -13
- package/package.json +1 -1
- package/dist/server/render-next.d.ts +0 -63
- package/dist/server/render-next.js +0 -109
package/dist/remote-main.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Every doubt fails closed: no recorded remote, a URL that isn't allowed, a failed fetch or a remote without main all
|
|
14
14
|
* read as "not proven", and the task stays for a person.
|
|
15
15
|
*/
|
|
16
|
-
import { mkdirSync, mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
16
|
+
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, rmSync, existsSync } from "node:fs";
|
|
17
17
|
import { isAbsolute, join } from "node:path";
|
|
18
18
|
import { run } from "./exec.js";
|
|
19
19
|
/** The ref a fetched remote main is kept under, inside the private directory only. */
|
|
@@ -35,7 +35,9 @@ export const CREDENTIAL_HELPER = process.platform === "darwin" ? "osxkeychain" :
|
|
|
35
35
|
function hardened(helper, file = "never") {
|
|
36
36
|
return ["-c", "core.hooksPath=/dev/null", "-c", "protocol.allow=never", "-c", "protocol.https.allow=always", "-c", "protocol.ssh.allow=always",
|
|
37
37
|
"-c", `protocol.file.allow=${file}`, "-c", "protocol.ext.allow=never", "-c", "credential.helper=", ...(helper === "" ? [] : ["-c", `credential.helper=${helper}`]),
|
|
38
|
-
"-c", "gc.auto=0", "-c", "maintenance.auto=false", "-c", "fetch.recurseSubmodules=false", "-c", "core.fsmonitor=false"
|
|
38
|
+
"-c", "gc.auto=0", "-c", "maintenance.auto=false", "-c", "fetch.recurseSubmodules=false", "-c", "core.fsmonitor=false",
|
|
39
|
+
// Never to an address the recorded URL doesn't name: a moved repository is a person's choice to record (MOVED).
|
|
40
|
+
"-c", "http.followRedirects=false"];
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
41
43
|
* A URL Toolroll may fetch remote main from: https or ssh (either form). Local paths, file URLs and loopback hosts
|
|
@@ -96,6 +98,14 @@ export function remoteUrlProblem(url) {
|
|
|
96
98
|
const parsed = parseRemoteUrl(url);
|
|
97
99
|
return "why" in parsed ? parsed.why : null;
|
|
98
100
|
}
|
|
101
|
+
/** What every reader of a remote main shows when its address redirects. The target is remote-supplied text: it is never
|
|
102
|
+
* shown, stored or followed, and the new remote is never adopted automatically. */
|
|
103
|
+
export const REMOTE_MOVED = "The repository moved: its address now redirects. Update the project's remote in Toolroll (Settings → Projects), then try again.";
|
|
104
|
+
/** git's words for a refused redirect (http.followRedirects=false), read for the status code alone. */
|
|
105
|
+
export function redirected(stderr) {
|
|
106
|
+
return /returned error: 30[12378]\b/.test(stderr);
|
|
107
|
+
}
|
|
108
|
+
const UNREAD = "Toolroll couldn't read the project's remote main";
|
|
99
109
|
/**
|
|
100
110
|
* One pass's reads of recorded remotes. Every `read` fetches again (the pre-teardown re-check is never older than
|
|
101
111
|
* the teardown); a project's private directory lasts until `close`, so a fetch after the first only brings what
|
|
@@ -145,14 +155,16 @@ export class RemoteMains {
|
|
|
145
155
|
return { ok: false, why: problem };
|
|
146
156
|
const dir = await this.dirFor(repo);
|
|
147
157
|
if (dir === null)
|
|
148
|
-
return { ok: false, why:
|
|
158
|
+
return { ok: false, why: UNREAD };
|
|
149
159
|
const fetched = await this.exec(dir, ["fetch", "--quiet", "--no-tags", "--no-write-fetch-head", "--", url, `+refs/heads/main:${REMOTE_MAIN}`]);
|
|
160
|
+
if (fetched.code !== 0 && redirected(fetched.stderr))
|
|
161
|
+
return { ok: false, why: REMOTE_MOVED, reason: "moved" };
|
|
150
162
|
if (fetched.code !== 0 || !this.clean(dir))
|
|
151
|
-
return { ok: false, why:
|
|
163
|
+
return { ok: false, why: UNREAD };
|
|
152
164
|
const resolved = await this.exec(dir, ["rev-parse", "--verify", "--quiet", `${REMOTE_MAIN}^{commit}`]);
|
|
153
165
|
const main = resolved.stdout.trim();
|
|
154
166
|
if (resolved.code !== 0 || !SHA.test(main))
|
|
155
|
-
return { ok: false, why:
|
|
167
|
+
return { ok: false, why: UNREAD };
|
|
156
168
|
const git = args => this.exec(dir, args);
|
|
157
169
|
const bring = async (from, head) => {
|
|
158
170
|
if (!SHA.test(head))
|
|
@@ -177,3 +189,70 @@ export class RemoteMains {
|
|
|
177
189
|
this.dirs.clear();
|
|
178
190
|
}
|
|
179
191
|
}
|
|
192
|
+
// ---- the code map's own read: pinned, depth 1, bounded (docs/plans/code-map.md §1) ---------------------------------
|
|
193
|
+
const MAP_MAIN = "refs/toolroll/map-main";
|
|
194
|
+
/** What keeps git's own memory down. index-pack still materializes delta bases, so these reduce; they don't bound. */
|
|
195
|
+
const LOW_MEMORY = ["-c", "pack.threads=1", "-c", "core.deltaBaseCacheLimit=32m", "-c", "core.bigFileThreshold=1m"];
|
|
196
|
+
export const MAP_FETCH_LIMITS = { timeoutMs: 120_000, memoryBytes: 1024 ** 3, diskBytes: 1024 ** 3, fileBytes: 512 * 1024 ** 2, maxStderr: 64 * 1024 };
|
|
197
|
+
/** Git in a private directory under the map's rules: the same hardening as `RemoteMains`, plus the pin. */
|
|
198
|
+
export function mapGitArgs(dir, pin, args, options = {}) {
|
|
199
|
+
return ["--git-dir", dir, ...hardened(options.helper ?? CREDENTIAL_HELPER), ...pin.gitArgs, ...(options.extra ?? []), ...args];
|
|
200
|
+
}
|
|
201
|
+
/** The environment for it: exactly the allowed names, the fixed settings, and the pin's. */
|
|
202
|
+
export function mapGitEnv(pin, from = process.env) {
|
|
203
|
+
const env = {};
|
|
204
|
+
for (const name of ENV_ALLOWLIST) {
|
|
205
|
+
const value = from[name];
|
|
206
|
+
if (value !== undefined)
|
|
207
|
+
env[name] = value;
|
|
208
|
+
}
|
|
209
|
+
return { ...env, ...ENV, ...pin.env };
|
|
210
|
+
}
|
|
211
|
+
export function initPrivateDir(dir, bounded) {
|
|
212
|
+
return bounded("git", ["init", "--quiet", "--bare", "--template=", dir], { cwd: dir, env: mapGitEnv({ gitArgs: [], env: {}, proxy: null }), timeoutMs: 30_000, maxStdout: 4096, maxStderr: 4096, label: "code-map-init" });
|
|
213
|
+
}
|
|
214
|
+
/** The commit the recorded remote's main is at, without fetching: null when the remote has no `main`. */
|
|
215
|
+
export async function peekMain(url, dir, pin, bounded, helper) {
|
|
216
|
+
const ran = await bounded("git", mapGitArgs(dir, pin, ["ls-remote", "--", url, "refs/heads/main"], helper === undefined ? {} : { helper }), { cwd: dir, env: mapGitEnv(pin), timeoutMs: 30_000, maxStdout: 64 * 1024, maxStderr: MAP_FETCH_LIMITS.maxStderr, label: "code-map-peek" });
|
|
217
|
+
if (ran.code !== 0 && redirected(ran.stderr))
|
|
218
|
+
return { ok: false, why: REMOTE_MOVED, reason: "moved" };
|
|
219
|
+
if (ran.code !== 0 || ran.killed !== null)
|
|
220
|
+
return { ok: false, why: UNREAD };
|
|
221
|
+
const line = ran.stdout.toString("utf8").split("\n").find(one => one.endsWith("\trefs/heads/main"));
|
|
222
|
+
if (line === undefined)
|
|
223
|
+
return ran.stdout.length === 0 ? { ok: true, commit: null } : { ok: false, why: UNREAD };
|
|
224
|
+
const commit = line.slice(0, line.indexOf("\t"));
|
|
225
|
+
return SHA.test(commit) ? { ok: true, commit } : { ok: false, why: UNREAD };
|
|
226
|
+
}
|
|
227
|
+
/** A depth-1 private folder is clean when it holds only what the fetch made: a `shallow` file naming exactly the fetched
|
|
228
|
+
* commit, and no grafts or alternates. */
|
|
229
|
+
export function shallowClean(dir, commit) {
|
|
230
|
+
if ([join("info", "grafts"), join("objects", "info", "alternates"), join("objects", "info", "http-alternates")].some(name => existsSync(join(dir, name))))
|
|
231
|
+
return false;
|
|
232
|
+
try {
|
|
233
|
+
const held = lstatSync(join(dir, "shallow"));
|
|
234
|
+
if (!held.isFile() || held.size > 128)
|
|
235
|
+
return false;
|
|
236
|
+
return readFileSync(join(dir, "shallow"), "utf8").trim() === commit;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
// A history of one commit needs no shallow boundary, and git writes none.
|
|
240
|
+
return error.code === "ENOENT";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/** Fetch main at depth 1 into `dir`, under the map's limits. `onStart` records the spawn for crash cleanup. */
|
|
244
|
+
export async function readShallowMain(url, dir, pin, bounded, onStart, helper) {
|
|
245
|
+
const fetched = await bounded("git", mapGitArgs(dir, pin, ["fetch", "--quiet", "--depth=1", "--no-tags", "--no-write-fetch-head", "--", url, `+refs/heads/main:${MAP_MAIN}`], { extra: LOW_MEMORY, ...(helper === undefined ? {} : { helper }) }), { cwd: dir, env: mapGitEnv(pin), timeoutMs: MAP_FETCH_LIMITS.timeoutMs, maxStdout: 4096, maxStderr: MAP_FETCH_LIMITS.maxStderr, memoryBytes: MAP_FETCH_LIMITS.memoryBytes,
|
|
246
|
+
diskBytes: MAP_FETCH_LIMITS.diskBytes, fileBytes: MAP_FETCH_LIMITS.fileBytes, folder: dir, label: "code-map-fetch", ...(onStart === undefined ? {} : { onStart }) });
|
|
247
|
+
if (fetched.code !== 0 && redirected(fetched.stderr))
|
|
248
|
+
return { ok: false, why: REMOTE_MOVED, reason: "moved" };
|
|
249
|
+
if (fetched.killed !== null)
|
|
250
|
+
return { ok: false, why: `the fetch went past its ${fetched.killed} limit` };
|
|
251
|
+
if (fetched.code !== 0)
|
|
252
|
+
return { ok: false, why: UNREAD };
|
|
253
|
+
const resolved = await bounded("git", mapGitArgs(dir, pin, ["rev-parse", "--verify", "--quiet", `${MAP_MAIN}^{commit}`]), { cwd: dir, env: mapGitEnv(pin), timeoutMs: 30_000, maxStdout: 4096, maxStderr: 4096, label: "code-map-rev" });
|
|
254
|
+
const commit = resolved.stdout.toString("utf8").trim();
|
|
255
|
+
if (resolved.code !== 0 || !SHA.test(commit) || !shallowClean(dir, commit))
|
|
256
|
+
return { ok: false, why: UNREAD };
|
|
257
|
+
return { ok: true, commit, enforced: fetched.enforced };
|
|
258
|
+
}
|
|
@@ -3,7 +3,8 @@ export type RepositoryContextRequest = {
|
|
|
3
3
|
query: string;
|
|
4
4
|
mode?: 'search' | 'impact';
|
|
5
5
|
audience?: 'lead' | 'crew';
|
|
6
|
-
/**
|
|
6
|
+
/** The admitted project from Toolroll's records: repo itself, or the project a crew worktree belongs to. Every read is
|
|
7
|
+
* bound to its git folder. */
|
|
7
8
|
project?: string;
|
|
8
9
|
baseRevision?: string;
|
|
9
10
|
cacheRoot?: string;
|
|
@@ -16,7 +17,7 @@ export type RepositoryCheckout = {
|
|
|
16
17
|
head: string;
|
|
17
18
|
baseRevision: string;
|
|
18
19
|
sourceHash: string;
|
|
19
|
-
source: 'working-tree';
|
|
20
|
+
source: 'working-tree' | 'base-commit';
|
|
20
21
|
};
|
|
21
22
|
export type RepositoryRelationship = {
|
|
22
23
|
from: string;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/** Optional repository context. Callers admit project access; this module grants no authority.
|
|
2
2
|
* The index is disposable. Every read hashes the current bounded tracked source set;
|
|
3
3
|
* a missing/changed/unreadable index falls back to ordinary text search, never a task gate. */
|
|
4
|
-
import { checkoutGitSync } from './teardown-git.js';
|
|
4
|
+
import { checkoutGitSync, viewedGitSync } from './teardown-git.js';
|
|
5
5
|
import { createHash, randomUUID } from 'node:crypto';
|
|
6
6
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
8
8
|
import { scanForSecrets } from './evidence.js';
|
|
9
|
+
import { compilerOptionsOf, extractFile, resolveAll } from './code-extract.js';
|
|
9
10
|
const VERSION = 1;
|
|
10
11
|
const MAX_FILES = 1200, MAX_FILE_BYTES = 256_000, MAX_SOURCE_BYTES = 12_000_000;
|
|
11
12
|
const CODE = /\.(?:[cm]?[jt]sx?)$/i;
|
|
@@ -15,31 +16,23 @@ const sha = (value) => createHash('sha256').update(value).digest('hex');
|
|
|
15
16
|
// Read-only and bounded, and always the hardened checkout git (a crew worktree is agent-written, and a page read or a
|
|
16
17
|
// lead's refresh reads it alike): no filter, hook, program or network, its config and info frozen in a view of the
|
|
17
18
|
// plane's own before they're inspected (teardown-git.ts checkoutGitSync).
|
|
18
|
-
|
|
19
|
+
// Bound to the project's record, so a pointer or commondir can't turn the read to another repository.
|
|
20
|
+
const git = (repo, args, binding) => checkoutGitSync(repo, args, { timeoutMs: 10_000, maxBuffer: 2_000_000, binding }).trimEnd();
|
|
19
21
|
const inside = (root, path) => { const r = relative(root, path); return r !== '..' && !r.startsWith('..' + sep) && !isAbsolute(r); };
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
if (!/^[a-f0-9]{40,64}$/.test(baseRevision) || git(repo, ['rev-parse', '--verify', `${baseRevision}^{commit}`]) !== baseRevision)
|
|
29
|
-
throw Error('The requested context base is unavailable.');
|
|
30
|
-
if (request.audience === 'crew' && baseRevision !== head)
|
|
31
|
-
throw Error('Crew context must start at its exact task base.');
|
|
32
|
-
const gitDir = realpathSync(git(repo, ['rev-parse', '--absolute-git-dir']));
|
|
33
|
-
if (request.project && realpathSync(git(repo, ['rev-parse', '--path-format=absolute', '--git-common-dir'])) !== realpathSync(git(request.project, ['rev-parse', '--path-format=absolute', '--git-common-dir'])))
|
|
34
|
-
throw Error('The worktree belongs to a different project.');
|
|
35
|
-
const files = git(repo, ['ls-files', '-z', '--cached']).split('\0').filter(Boolean).sort();
|
|
36
|
-
const sources = [], warnings = [];
|
|
22
|
+
/** Whether a tracked path is one the index reads, by its name alone. */
|
|
23
|
+
const skippedPath = (file) => !TEXT.test(file) || /(?:^|\/)(?:node_modules|vendor|dist|build|coverage|\.git)(?:\/|$)/.test(file);
|
|
24
|
+
const unsafePath = (file) => file.split('/').some(p => p === '..') || /[\x00-\x1f]/.test(file);
|
|
25
|
+
const unreadableText = (data, text) => data.length > MAX_FILE_BYTES || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f\ufffd]/u.test(text) || scanForSecrets(text).length > 0;
|
|
26
|
+
/** The working tree's tracked files, for a lead or a person looking at the project's own checkout. */
|
|
27
|
+
function workingTreeSources(repo, binding) {
|
|
28
|
+
const files = git(repo, ['ls-files', '-z', '--cached'], binding).split('\0').filter(Boolean).sort();
|
|
29
|
+
const sources = [];
|
|
37
30
|
let omitted = 0, omittedBytes = 0, bytes = 0;
|
|
38
31
|
for (const file of [...new Set(files)]) {
|
|
39
|
-
if (
|
|
32
|
+
if (skippedPath(file))
|
|
40
33
|
continue;
|
|
41
34
|
const path = resolve(repo, file);
|
|
42
|
-
if (!inside(repo, path) ||
|
|
35
|
+
if (!inside(repo, path) || unsafePath(file)) {
|
|
43
36
|
omitted++;
|
|
44
37
|
continue;
|
|
45
38
|
}
|
|
@@ -57,7 +50,7 @@ function capture(request) {
|
|
|
57
50
|
}
|
|
58
51
|
const data = readFileSync(path);
|
|
59
52
|
const text = data.toString('utf8');
|
|
60
|
-
if (data
|
|
53
|
+
if (unreadableText(data, text)) {
|
|
61
54
|
omitted++;
|
|
62
55
|
omittedBytes += data.length;
|
|
63
56
|
continue;
|
|
@@ -69,13 +62,91 @@ function capture(request) {
|
|
|
69
62
|
omitted++;
|
|
70
63
|
}
|
|
71
64
|
}
|
|
72
|
-
|
|
65
|
+
return { sources, omitted, omittedBytes };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* A crew's sources: the task base's tree and raw blobs, never the working tree an agent writes. A dirty tracked file,
|
|
69
|
+
* a symlink swapped in or a replace ref is not read; no filter runs on a raw blob.
|
|
70
|
+
*/
|
|
71
|
+
function baseCommitSources(repo, base, binding) {
|
|
72
|
+
const read = (args, maxBuffer, input) => {
|
|
73
|
+
const ran = viewedGitSync(repo, ['--no-replace-objects', ...args], { timeoutMs: 10_000, maxBuffer, binding, buffer: true, ...(input === undefined ? {} : { input }) });
|
|
74
|
+
if (ran.status !== 0)
|
|
75
|
+
throw Error('The task base could not be read.');
|
|
76
|
+
return ran.stdout;
|
|
77
|
+
};
|
|
78
|
+
const wanted = [];
|
|
79
|
+
let omitted = 0, omittedBytes = 0, bytes = 0;
|
|
80
|
+
for (const entry of read(['ls-tree', '-r', '-l', '-z', base], 16_000_000).toString('utf8').split('\0')) {
|
|
81
|
+
const match = /^(\d{6}) (\w+) ([0-9a-f]{40,64}) +(\d+|-)\t(.+)$/s.exec(entry);
|
|
82
|
+
if (!match)
|
|
83
|
+
continue;
|
|
84
|
+
const [, mode, kind, oid, sized, file] = match;
|
|
85
|
+
if (skippedPath(file))
|
|
86
|
+
continue;
|
|
87
|
+
const size = sized === '-' ? 0 : Number(sized);
|
|
88
|
+
if (unsafePath(file) || kind !== 'blob' || (mode !== '100644' && mode !== '100755')) {
|
|
89
|
+
omitted++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (wanted.length >= MAX_FILES || size > MAX_FILE_BYTES || bytes + size > MAX_SOURCE_BYTES) {
|
|
93
|
+
omitted++;
|
|
94
|
+
omittedBytes += size;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
wanted.push({ file, oid });
|
|
98
|
+
bytes += size;
|
|
99
|
+
}
|
|
100
|
+
wanted.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : 0));
|
|
101
|
+
const sources = [];
|
|
102
|
+
if (wanted.length === 0)
|
|
103
|
+
return { sources, omitted, omittedBytes };
|
|
104
|
+
const out = read(['cat-file', '--batch'], MAX_SOURCE_BYTES + wanted.length * 128 + 4096, Buffer.from(wanted.map(w => w.oid).join('\n') + '\n'));
|
|
105
|
+
let at = 0;
|
|
106
|
+
for (const { file, oid } of wanted) {
|
|
107
|
+
const end = out.indexOf(0x0a, at);
|
|
108
|
+
const header = end < 0 ? null : /^([0-9a-f]{40,64}) blob (\d+)$/.exec(out.subarray(at, end).toString('latin1'));
|
|
109
|
+
if (end < 0 || header === null || header[1] !== oid)
|
|
110
|
+
throw Error('The task base could not be read.');
|
|
111
|
+
const size = Number(header[2]);
|
|
112
|
+
const data = out.subarray(end + 1, end + 1 + size);
|
|
113
|
+
at = end + 1 + size + 1;
|
|
114
|
+
const text = data.toString('utf8');
|
|
115
|
+
if (unreadableText(data, text)) {
|
|
116
|
+
omitted++;
|
|
117
|
+
omittedBytes += data.length;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
sources.push({ file, text, sha256: sha(data) });
|
|
121
|
+
}
|
|
122
|
+
return { sources, omitted, omittedBytes };
|
|
123
|
+
}
|
|
124
|
+
function capture(request) {
|
|
125
|
+
const repo = realpathSync(request.repo);
|
|
126
|
+
if (repo !== resolve(request.repo))
|
|
127
|
+
throw Error('The project location changed.');
|
|
128
|
+
// The project's record, never the checkout's own pointer files: the admitted project, or the one a crew worktree
|
|
129
|
+
// belongs to. Only a person's local look at an unrecorded folder has none.
|
|
130
|
+
const binding = { repo: request.project ?? null };
|
|
131
|
+
if (realpathSync(git(repo, ['rev-parse', '--show-toplevel'], binding)) !== repo)
|
|
132
|
+
throw Error('Choose the project checkout root.');
|
|
133
|
+
const head = git(repo, ['rev-parse', '--verify', 'HEAD'], binding);
|
|
134
|
+
const baseRevision = request.baseRevision ?? head;
|
|
135
|
+
if (!/^[a-f0-9]{40,64}$/.test(baseRevision) || git(repo, ['rev-parse', '--verify', `${baseRevision}^{commit}`], binding) !== baseRevision)
|
|
136
|
+
throw Error('The requested context base is unavailable.');
|
|
137
|
+
if (request.audience === 'crew' && baseRevision !== head)
|
|
138
|
+
throw Error('Crew context must start at its exact task base.');
|
|
139
|
+
const gitDir = realpathSync(git(repo, ['rev-parse', '--absolute-git-dir'], binding));
|
|
140
|
+
const crew = request.audience === 'crew';
|
|
141
|
+
const { sources, omitted, omittedBytes } = crew ? baseCommitSources(repo, baseRevision, binding) : workingTreeSources(repo, binding);
|
|
142
|
+
if (git(repo, ['rev-parse', '--verify', 'HEAD'], binding) !== head)
|
|
73
143
|
throw Error('The checkout changed while context was being read.');
|
|
144
|
+
const warnings = [];
|
|
74
145
|
if (omitted)
|
|
75
146
|
warnings.push(`${omitted} source files were omitted because they were unavailable, excluded, sensitive or beyond the read limits.`);
|
|
76
147
|
warnings.push('Tracked files only. Static imports do not establish every runtime dependency or sufficient test coverage.');
|
|
77
148
|
const sourceHash = sha(JSON.stringify(sources.map(({ file, sha256 }) => [file, sha256])));
|
|
78
|
-
return { checkout: { repo, gitDir, head, baseRevision, sourceHash, source: 'working-tree' }, sources, omitted, omittedBytes, warnings };
|
|
149
|
+
return { checkout: { repo, gitDir, head, baseRevision, sourceHash, source: crew ? 'base-commit' : 'working-tree' }, sources, omitted, omittedBytes, warnings };
|
|
79
150
|
}
|
|
80
151
|
function cacheFile(root, checkout) {
|
|
81
152
|
const absolute = resolve(root);
|
|
@@ -135,62 +206,33 @@ async function buildIndex(snapshot) {
|
|
|
135
206
|
dir = dirname(dir);
|
|
136
207
|
}
|
|
137
208
|
}
|
|
138
|
-
let options = { module: compiler.ModuleKind.NodeNext, moduleResolution: compiler.ModuleResolutionKind.NodeNext, allowJs: true, resolveJsonModule: true };
|
|
139
209
|
const config = byPath.get(join(snapshot.checkout.repo, 'tsconfig.json'));
|
|
210
|
+
const { options, problem } = compilerOptionsOf(compiler, join(snapshot.checkout.repo, 'tsconfig.json'), config?.text, snapshot.checkout.repo);
|
|
140
211
|
const warnings = [];
|
|
141
|
-
if (
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (parsed.config.extends)
|
|
147
|
-
warnings.push('Inherited tsconfig settings are not indexed; unresolved aliases remain omitted.');
|
|
148
|
-
}
|
|
149
|
-
else
|
|
150
|
-
warnings.push('TypeScript configuration could not be read; relative imports still work.');
|
|
151
|
-
}
|
|
212
|
+
if (problem === 'extends')
|
|
213
|
+
warnings.push('Inherited tsconfig settings are not indexed; unresolved aliases remain omitted.');
|
|
214
|
+
else if (problem === 'unreadable')
|
|
215
|
+
warnings.push('TypeScript configuration could not be read; relative imports still work.');
|
|
216
|
+
const resolveOne = resolveAll(compiler, snapshot.checkout.repo, byPath, directories, options, resolve);
|
|
152
217
|
const nodes = [], relationships = [];
|
|
153
218
|
let unresolved = 0, parseFailures = 0;
|
|
154
|
-
const host = {
|
|
155
|
-
fileExists: file => byPath.has(resolve(file)),
|
|
156
|
-
readFile: file => byPath.get(resolve(file))?.text,
|
|
157
|
-
directoryExists: dir => directories.has(resolve(dir)),
|
|
158
|
-
getCurrentDirectory: () => snapshot.checkout.repo,
|
|
159
|
-
realpath: file => resolve(file),
|
|
160
|
-
};
|
|
161
219
|
for (const source of snapshot.sources.filter(s => CODE.test(s.file))) {
|
|
162
220
|
const fileName = resolve(snapshot.checkout.repo, source.file);
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
221
|
+
const read = extractFile(compiler, fileName, source.text);
|
|
222
|
+
if (read.parseFailed)
|
|
165
223
|
parseFailures++;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
224
|
+
for (const one of read.declarations)
|
|
225
|
+
if (nodes.length < MAX_NODES)
|
|
226
|
+
nodes.push({ file: source.file, name: one.name, line: one.line, endLine: one.endLine });
|
|
227
|
+
for (const one of read.specifiers) {
|
|
228
|
+
const resolved = resolveOne(one.specifier, fileName);
|
|
229
|
+
if (resolved === null) {
|
|
170
230
|
unresolved++;
|
|
171
|
-
|
|
231
|
+
continue;
|
|
172
232
|
}
|
|
173
233
|
if (relationships.length < MAX_EDGES)
|
|
174
|
-
relationships.push({ from: source.file, to: byPath.get(
|
|
175
|
-
}
|
|
176
|
-
const visit = (node) => {
|
|
177
|
-
if (nodes.length < MAX_NODES && (compiler.isFunctionDeclaration(node) || compiler.isClassDeclaration(node) || compiler.isInterfaceDeclaration(node) || compiler.isTypeAliasDeclaration(node) || compiler.isEnumDeclaration(node) || compiler.isMethodDeclaration(node) || compiler.isVariableDeclaration(node))) {
|
|
178
|
-
const name = node.name;
|
|
179
|
-
if (name && compiler.isIdentifier(name))
|
|
180
|
-
nodes.push({ file: source.file, name: name.text, line: line(node.getStart(tree)), endLine: line(node.end) });
|
|
181
|
-
}
|
|
182
|
-
if (compiler.isImportDeclaration(node) && compiler.isStringLiteral(node.moduleSpecifier))
|
|
183
|
-
relationship(node.moduleSpecifier.text, 'imports', node.getStart(tree));
|
|
184
|
-
if (compiler.isExportDeclaration(node) && node.moduleSpecifier && compiler.isStringLiteral(node.moduleSpecifier))
|
|
185
|
-
relationship(node.moduleSpecifier.text, 're-exports', node.getStart(tree));
|
|
186
|
-
if (compiler.isCallExpression(node) && node.arguments.length === 1 && compiler.isStringLiteral(node.arguments[0])) {
|
|
187
|
-
if (node.expression.kind === compiler.SyntaxKind.ImportKeyword)
|
|
188
|
-
relationship(node.arguments[0].text, 'dynamic-import', node.getStart(tree));
|
|
189
|
-
// require may be shadowed; do not present a guessed call as a proven relationship.
|
|
190
|
-
}
|
|
191
|
-
compiler.forEachChild(node, visit);
|
|
192
|
-
};
|
|
193
|
-
visit(tree);
|
|
234
|
+
relationships.push({ from: source.file, to: byPath.get(resolved).file, kind: one.kind, file: source.file, line: one.line, provenance: 'explicit' });
|
|
235
|
+
}
|
|
194
236
|
}
|
|
195
237
|
if (unresolved)
|
|
196
238
|
warnings.push(`${unresolved} external or unresolved imports were omitted.`);
|
|
@@ -25,6 +25,7 @@ import { dirname, join } from "node:path";
|
|
|
25
25
|
import { fileURLToPath } from "node:url";
|
|
26
26
|
import { existsSync, readFileSync, realpathSync, readdirSync, statSync } from "node:fs";
|
|
27
27
|
import { bootIdentity, provenDeadByBootChange } from "./boot-identity.js";
|
|
28
|
+
import { sameMachineHost } from "./machine-identity.js";
|
|
28
29
|
import { containmentStatus, currentContainment } from "./containment.js";
|
|
29
30
|
import { daemonStatus } from "./daemon.js";
|
|
30
31
|
import { isAlive } from "./runner.js";
|
|
@@ -70,8 +71,8 @@ export async function recordRestartBaseline(inputs) {
|
|
|
70
71
|
.map(row => ({ run: Number(row["run"]), taskRef: Number(row["task_ref"]), requestedAt: String(row["requested_at"]), problem: store.stopQuiescenceProblem(Number(row["run"])) }));
|
|
71
72
|
const openRuns = raw.prepare("SELECT id, task_ref, role, runner, lease_id FROM run WHERE outcome IS NULL ORDER BY id").all()
|
|
72
73
|
.map(row => ({ id: Number(row["id"]), taskRef: Number(row["task_ref"]), role: String(row["role"]), runner: String(row["runner"]), leaseId: String(row["lease_id"]) }));
|
|
73
|
-
const witnesses = raw.prepare("SELECT run, pid, host, boot_id, containment, exited_at FROM run_process WHERE exited_at IS NULL ORDER BY id").all()
|
|
74
|
-
.map(row => ({ run: Number(row["run"]), pid: row["pid"] === null ? null : Number(row["pid"]), host: String(row["host"]), bootId: row["boot_id"] === null ? null : String(row["boot_id"]), containment: row["containment"] === null ? null : String(row["containment"]), open: true }));
|
|
74
|
+
const witnesses = raw.prepare("SELECT run, pid, host, boot_id, containment, exited_at, machine_id FROM run_process WHERE exited_at IS NULL ORDER BY id").all()
|
|
75
|
+
.map(row => ({ run: Number(row["run"]), pid: row["pid"] === null ? null : Number(row["pid"]), host: String(row["host"]), bootId: row["boot_id"] === null ? null : String(row["boot_id"]), containment: row["containment"] === null ? null : String(row["containment"]), open: true, machineId: row["machine_id"] == null ? null : String(row["machine_id"]) }));
|
|
75
76
|
return {
|
|
76
77
|
version: 2,
|
|
77
78
|
runnerName: inputs.runnerName ?? null,
|
|
@@ -123,7 +124,8 @@ export async function verifyRestartRecovery(inputs) {
|
|
|
123
124
|
const sameDatabase = baseline.version === 2 && baseline.database.identity === current.database.identity;
|
|
124
125
|
checks.push({ name: "database", ok: sameDatabase, detail: sameDatabase ? "same database file identity" : "database identity differs or baseline predates identity verification; no recovery mutations performed" });
|
|
125
126
|
// 1. Boot identity, as the OS reports it.
|
|
126
|
-
|
|
127
|
+
// This machine under any of its own names (machine-identity.ts): a reboot onto another network can move the hostname.
|
|
128
|
+
const sameHost = sameMachineHost(baseline.host, current.host);
|
|
127
129
|
const bootChanged = !sameHost || baseline.boot.id === null || current.boot.id === null
|
|
128
130
|
? null
|
|
129
131
|
: provenDeadByBootChange({ host: current.host, bootId: baseline.boot.id }, { host: current.host, bootId: current.boot.id });
|
|
@@ -153,7 +155,7 @@ export async function verifyRestartRecovery(inputs) {
|
|
|
153
155
|
}
|
|
154
156
|
const baselineBeats = new Map(baseline.database.runners.map(one => [one.name, one.heartbeatAt]));
|
|
155
157
|
const liveRunners = current.database.runners.filter(one => !one.retired);
|
|
156
|
-
const fresh = liveRunners.filter(one => one.host
|
|
158
|
+
const fresh = liveRunners.filter(one => sameMachineHost(one.host, current.host) && one.alive && (baseline.runnerName === null || one.name === baseline.runnerName) && baselineBeats.has(one.name) && one.heartbeatAt > baselineBeats.get(one.name));
|
|
157
159
|
checks.push({
|
|
158
160
|
name: "heartbeat",
|
|
159
161
|
ok: fresh.length > 0,
|
|
@@ -187,8 +189,8 @@ export async function verifyRestartRecovery(inputs) {
|
|
|
187
189
|
? `every run open at the baseline has ended (${baseline.database.openRuns.map(run => `#${run.id}`).join(", ")})`
|
|
188
190
|
: `still open from before: ${staleOpen.map(run => `#${run.id} (${run.role}, ${run.runner})`).join(", ")} — recovery has not run, or its lease is still live`,
|
|
189
191
|
});
|
|
190
|
-
const oldWitnesses = baseline.database.witnesses.filter(one => one.bootId !== null && current.boot.id !== null && one.host
|
|
191
|
-
const survivors = oldWitnesses.filter(one => !provenDeadByBootChange({ host: one.host, bootId: one.bootId }, { host:
|
|
192
|
+
const oldWitnesses = baseline.database.witnesses.filter(one => one.bootId !== null && current.boot.id !== null && sameMachineHost(one.host, current.host, one.machineId));
|
|
193
|
+
const survivors = oldWitnesses.filter(one => !provenDeadByBootChange({ host: one.host, bootId: one.bootId }, { host: one.host, bootId: current.boot.id }));
|
|
192
194
|
checks.push({
|
|
193
195
|
name: "old-boot-custody",
|
|
194
196
|
ok: bootChanged !== true || survivors.length === 0,
|
package/dist/result-acts.d.ts
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
/** The result page's acts (2026-10-02), chosen in one place:
|
|
2
|
-
* act, the one that resolves the result's state,
|
|
3
|
-
* most one outline act beside it. A result that can't be accepted says why
|
|
4
|
-
* in
|
|
1
|
+
/** The result page's acts (2026-10-02; docs/plans/plain-toolroll.md P1.3), chosen in one place: three verbs, each a
|
|
2
|
+
* relabel over the post it has always made. Exactly one ink act at most, the one that resolves the result's state,
|
|
3
|
+
* and never navigation; at most one outline act beside it. A result that can't be accepted says why in one line
|
|
4
|
+
* before them. Run checks stays in the Checks row, and a person's own checks in the Checks list: neither is an act here.
|
|
5
5
|
*
|
|
6
|
-
* everything met → Accept
|
|
7
|
-
* a "You check this one" unanswered → the
|
|
8
|
-
* a check you marked Not right →
|
|
9
|
-
* checks queued or running → Checks running
|
|
10
|
-
* checks didn't run, a check exists → Run checks, then Accept without checks
|
|
6
|
+
* everything met → Accept
|
|
7
|
+
* a "You check this one" unanswered → (the check itself, above), then Accept without your check
|
|
8
|
+
* a check you marked Not right → Send back, then Accept anyway
|
|
9
|
+
* checks queued or running → (Checks running, in its row), then Accept without checks
|
|
10
|
+
* checks didn't run, a check exists → (Run checks, in its row), then Accept without checks
|
|
11
11
|
* report and changes disagree,
|
|
12
|
-
* or the proof is refuted →
|
|
12
|
+
* or the proof is refuted → Send back, then Accept (as allowed);
|
|
13
13
|
* never Accept as ink, even with no form here
|
|
14
|
-
* a Needs you form resolves it → that form (Build again
|
|
15
|
-
* saved notes or a failing PR →
|
|
16
|
-
* a failed build → Retry (its note starts with what to change)
|
|
17
|
-
*
|
|
14
|
+
* a Needs you form resolves it → that form (Retry: Build again; Check again)
|
|
15
|
+
* saved notes or a failing PR → Send back (the revision from the notes, or the repair of the PR)
|
|
16
|
+
* a failed build → Retry (its note starts with what to change); never Accept: a failed build
|
|
17
|
+
* is not a result to accept
|
|
18
18
|
* a failed task's delivered result → Retry, then Accept anyway (in outline, with a reason) where it may be
|
|
19
|
-
* accepted
|
|
19
|
+
* accepted
|
|
20
20
|
*
|
|
21
|
-
* Accepting finishes the task in one request
|
|
22
|
-
*
|
|
23
|
-
* which act is ink; the page recomputes it as the person answers each check. */
|
|
21
|
+
* Accepting finishes the task in one request: the person's acceptance, when one is owed, and the completion
|
|
22
|
+
* together. This only decides which act is ink; the page recomputes it as the person answers each check. */
|
|
24
23
|
import type { ProofVerdict } from "./proof.js";
|
|
25
24
|
/** The words over the reason field an Accept of a refuted result requires. */
|
|
26
25
|
export declare const ACCEPT_NEEDS_REASON = "Accepting needs a reason";
|
|
@@ -74,12 +73,21 @@ export type ResultActs = {
|
|
|
74
73
|
/** The one line before the acts: why it can't be accepted yet. */
|
|
75
74
|
line: string | null;
|
|
76
75
|
};
|
|
76
|
+
/** The three verbs, and the posts each one makes (docs/plans/plain-toolroll.md P1.3): Accept is today's Accept (and
|
|
77
|
+
* finish), or Accept anyway with a reason; Send back is today's request for changes and its revision from saved notes
|
|
78
|
+
* for an unpublished result, or the repair of a published one; Retry is today's Retry, or Build again. Check again
|
|
79
|
+
* keeps its own words (its act stands on the build, not on the result). */
|
|
80
|
+
export type ResultVerb = "Accept" | "Send back" | "Retry";
|
|
81
|
+
export declare const RESULT_VERB: Readonly<Record<ResultActKind, ResultVerb | null>>;
|
|
82
|
+
/** What Send back does, said on its own confirm line: the rule is fixed by whether the result is already published. */
|
|
83
|
+
export declare const SEND_BACK_UNPUBLISHED = "Sends it back with your notes. The same task builds again once you approve the revision.";
|
|
84
|
+
export declare const SEND_BACK_PUBLISHED = "It's already published, so this files a fix as a new version for you to approve.";
|
|
77
85
|
export declare function resultActsOf(facts: ResultActFacts): ResultActs;
|
|
78
86
|
/** The words a result's Accept wears and the one line before it, given the
|
|
79
87
|
* server's words for everything but the person's own checks (`base`) and how
|
|
80
88
|
* those stand on this page: an unanswered check names itself; a check marked
|
|
81
89
|
* Not right says so; otherwise the base words stand. */
|
|
82
|
-
export type AcceptLabel = "Accept
|
|
90
|
+
export type AcceptLabel = "Accept" | "Accept without checks" | "Accept without your check" | "Accept anyway";
|
|
83
91
|
export declare function acceptWithChecksOf(base: {
|
|
84
92
|
label: AcceptLabel;
|
|
85
93
|
ready: boolean;
|
package/dist/result-acts.js
CHANGED
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
/** The result page's acts (2026-10-02), chosen in one place:
|
|
2
|
-
* act, the one that resolves the result's state,
|
|
3
|
-
* most one outline act beside it. A result that can't be accepted says why
|
|
4
|
-
* in
|
|
1
|
+
/** The result page's acts (2026-10-02; docs/plans/plain-toolroll.md P1.3), chosen in one place: three verbs, each a
|
|
2
|
+
* relabel over the post it has always made. Exactly one ink act at most, the one that resolves the result's state,
|
|
3
|
+
* and never navigation; at most one outline act beside it. A result that can't be accepted says why in one line
|
|
4
|
+
* before them. Run checks stays in the Checks row, and a person's own checks in the Checks list: neither is an act here.
|
|
5
5
|
*
|
|
6
|
-
* everything met → Accept
|
|
7
|
-
* a "You check this one" unanswered → the
|
|
8
|
-
* a check you marked Not right →
|
|
9
|
-
* checks queued or running → Checks running
|
|
10
|
-
* checks didn't run, a check exists → Run checks, then Accept without checks
|
|
6
|
+
* everything met → Accept
|
|
7
|
+
* a "You check this one" unanswered → (the check itself, above), then Accept without your check
|
|
8
|
+
* a check you marked Not right → Send back, then Accept anyway
|
|
9
|
+
* checks queued or running → (Checks running, in its row), then Accept without checks
|
|
10
|
+
* checks didn't run, a check exists → (Run checks, in its row), then Accept without checks
|
|
11
11
|
* report and changes disagree,
|
|
12
|
-
* or the proof is refuted →
|
|
12
|
+
* or the proof is refuted → Send back, then Accept (as allowed);
|
|
13
13
|
* never Accept as ink, even with no form here
|
|
14
|
-
* a Needs you form resolves it → that form (Build again
|
|
15
|
-
* saved notes or a failing PR →
|
|
16
|
-
* a failed build → Retry (its note starts with what to change)
|
|
17
|
-
*
|
|
14
|
+
* a Needs you form resolves it → that form (Retry: Build again; Check again)
|
|
15
|
+
* saved notes or a failing PR → Send back (the revision from the notes, or the repair of the PR)
|
|
16
|
+
* a failed build → Retry (its note starts with what to change); never Accept: a failed build
|
|
17
|
+
* is not a result to accept
|
|
18
18
|
* a failed task's delivered result → Retry, then Accept anyway (in outline, with a reason) where it may be
|
|
19
|
-
* accepted
|
|
19
|
+
* accepted
|
|
20
20
|
*
|
|
21
|
-
* Accepting finishes the task in one request
|
|
22
|
-
*
|
|
23
|
-
* which act is ink; the page recomputes it as the person answers each check. */
|
|
21
|
+
* Accepting finishes the task in one request: the person's acceptance, when one is owed, and the completion
|
|
22
|
+
* together. This only decides which act is ink; the page recomputes it as the person answers each check. */
|
|
24
23
|
/** The words over the reason field an Accept of a refuted result requires. */
|
|
25
24
|
export const ACCEPT_NEEDS_REASON = "Accepting needs a reason";
|
|
26
25
|
export const FAILED_CHECK = /^the repository's approved verification command exited (-?[0-9]+)/;
|
|
@@ -42,34 +41,46 @@ export function cantAcceptYetOf(verdict, reasons, accepted) {
|
|
|
42
41
|
? "Can't accept yet: the project's check failed on these changes."
|
|
43
42
|
: ACCEPT_NEEDS_REASON;
|
|
44
43
|
}
|
|
44
|
+
export const RESULT_VERB = {
|
|
45
|
+
accept: "Accept", "accept-anyway": "Accept",
|
|
46
|
+
"request-changes": "Send back", revise: "Send back", "draft-repair": "Send back",
|
|
47
|
+
retry: "Retry", rebuild: "Retry",
|
|
48
|
+
"recheck-stopped": null,
|
|
49
|
+
// Never in the act row: Run checks lives in the Checks row, and a person's own checks in the Checks list.
|
|
50
|
+
"next-check": null, "checks-running": null, "run-checks": null,
|
|
51
|
+
};
|
|
52
|
+
/** What Send back does, said on its own confirm line: the rule is fixed by whether the result is already published. */
|
|
53
|
+
export const SEND_BACK_UNPUBLISHED = "Sends it back with your notes. The same task builds again once you approve the revision.";
|
|
54
|
+
export const SEND_BACK_PUBLISHED = "It's already published, so this files a fix as a new version for you to approve.";
|
|
45
55
|
export function resultActsOf(facts) {
|
|
46
56
|
const request = facts.canRequest ? "request-changes" : null;
|
|
47
57
|
const accept = facts.accept === null ? null : "accept";
|
|
48
58
|
if (facts.failed != null) {
|
|
49
|
-
const checks = facts.runChecks && !facts.checksRunning ? "run-checks" : facts.checksRunning ? "checks-running" : null;
|
|
50
59
|
const anyway = facts.failed.acceptAnyway === true ? "accept-anyway" : null;
|
|
51
|
-
return facts.failed.retry ? { primary: "retry", secondary: anyway
|
|
60
|
+
return facts.failed.retry ? { primary: "retry", secondary: anyway, line: null } : { primary: null, secondary: anyway, line: null };
|
|
52
61
|
}
|
|
53
62
|
if (facts.need !== null)
|
|
54
63
|
return { primary: facts.need, secondary: request, line: null };
|
|
64
|
+
// Saved notes or a failing pull request: Send back is ink, and Accept the quiet other way.
|
|
55
65
|
if (facts.next !== null)
|
|
56
|
-
return { primary: facts.next, secondary: accept
|
|
66
|
+
return { primary: facts.next, secondary: accept, line: null };
|
|
57
67
|
// Can't be accepted yet: Accept is never the ink act, even when the feedback form isn't here.
|
|
58
68
|
if (facts.blocked !== null)
|
|
59
69
|
return { primary: request, secondary: accept, line: facts.blocked };
|
|
60
|
-
// The person's own checks come first
|
|
70
|
+
// The person's own checks come first, in the Checks list above: nothing here is ink until each is answered.
|
|
61
71
|
if (accept !== null && (facts.unanswered ?? 0) > 0)
|
|
62
|
-
return { primary:
|
|
72
|
+
return { primary: null, secondary: accept, line: null };
|
|
63
73
|
if (accept !== null && (facts.notRight ?? 0) > 0)
|
|
64
74
|
return { primary: request, secondary: accept, line: null };
|
|
75
|
+
// Checks running, or not run: their row says so and offers Run checks; Accept waits in outline.
|
|
65
76
|
if (facts.checksRunning)
|
|
66
|
-
return { primary:
|
|
77
|
+
return { primary: null, secondary: accept ?? request, line: null };
|
|
67
78
|
if (facts.accept === null)
|
|
68
79
|
return { primary: null, secondary: request, line: null };
|
|
69
80
|
if (facts.accept.ready)
|
|
70
81
|
return { primary: "accept", secondary: request, line: null };
|
|
71
82
|
if (facts.runChecks)
|
|
72
|
-
return { primary:
|
|
83
|
+
return { primary: null, secondary: "accept", line: null };
|
|
73
84
|
return { primary: "accept", secondary: request, line: null };
|
|
74
85
|
}
|
|
75
86
|
export function acceptWithChecksOf(base, checks) {
|