toolroll 0.9.76 → 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.
Files changed (102) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/README.md +11 -4
  3. package/THIRD_PARTY_NOTICES.md +5 -5
  4. package/dist/agent-fence.js +28 -8
  5. package/dist/assignment.js +3 -0
  6. package/dist/browser/THIRD_PARTY_NOTICES.txt +5 -5
  7. package/dist/browser/workspace.css +2 -2
  8. package/dist/browser/workspace.js +31 -31
  9. package/dist/builder.d.ts +14 -0
  10. package/dist/builder.js +140 -41
  11. package/dist/chat-decide.js +4 -0
  12. package/dist/check-failure.d.ts +92 -0
  13. package/dist/check-failure.js +267 -0
  14. package/dist/check-progress.d.ts +28 -1
  15. package/dist/check-progress.js +105 -6
  16. package/dist/check-report.d.ts +52 -6
  17. package/dist/check-report.js +193 -8
  18. package/dist/checkout-cleanup.js +2 -2
  19. package/dist/cli.js +8 -5
  20. package/dist/coding-handoff.d.ts +1 -0
  21. package/dist/coding-handoff.js +20 -11
  22. package/dist/commit-extract.d.ts +6 -0
  23. package/dist/commit-extract.js +63 -8
  24. package/dist/console-chrome.d.ts +41 -0
  25. package/dist/console-chrome.js +166 -0
  26. package/dist/contracts/release-check.d.ts +12 -0
  27. package/dist/contracts/release-check.js +17 -0
  28. package/dist/contracts/verification-receipt.d.ts +20 -2
  29. package/dist/contracts/verification-receipt.js +15 -3
  30. package/dist/contracts/verification-view.d.ts +8 -0
  31. package/dist/controller-supervisor.d.ts +8 -0
  32. package/dist/controller-supervisor.js +33 -1
  33. package/dist/demo.js +3 -2
  34. package/dist/deploy-gate.js +5 -0
  35. package/dist/dispose.d.ts +3 -0
  36. package/dist/dispose.js +6 -3
  37. package/dist/doctor.d.ts +7 -0
  38. package/dist/doctor.js +10 -2
  39. package/dist/envelope.js +2 -1
  40. package/dist/evidence.js +4 -1
  41. package/dist/exec.d.ts +7 -0
  42. package/dist/exec.js +7 -1
  43. package/dist/git-floor.d.ts +16 -0
  44. package/dist/git-floor.js +36 -0
  45. package/dist/grant.js +5 -1
  46. package/dist/guides.js +4 -3
  47. package/dist/operate.d.ts +13 -0
  48. package/dist/operate.js +773 -319
  49. package/dist/prepared-evidence.d.ts +2 -1
  50. package/dist/prepared-evidence.js +8 -3
  51. package/dist/project-knowledge.js +6 -2
  52. package/dist/project-learning.js +2 -2
  53. package/dist/proof.js +6 -0
  54. package/dist/read-executor-worker.js +8 -4
  55. package/dist/read-executor.d.ts +18 -1
  56. package/dist/read-executor.js +8 -0
  57. package/dist/release-adapters.d.ts +48 -1
  58. package/dist/release-adapters.js +343 -10
  59. package/dist/release-authority.js +6 -2
  60. package/dist/release-auto.d.ts +38 -7
  61. package/dist/release-auto.js +131 -15
  62. package/dist/release-card.d.ts +15 -3
  63. package/dist/release-card.js +25 -9
  64. package/dist/release-ci-hold.d.ts +41 -0
  65. package/dist/release-ci-hold.js +53 -0
  66. package/dist/release-cli.js +21 -12
  67. package/dist/release-coverage.js +5 -2
  68. package/dist/release-recheck.d.ts +1 -1
  69. package/dist/release-recheck.js +6 -1
  70. package/dist/release.d.ts +108 -11
  71. package/dist/release.js +294 -39
  72. package/dist/review-context.js +8 -6
  73. package/dist/runner.js +2 -1
  74. package/dist/sandbox-runtime.d.ts +8 -0
  75. package/dist/sandbox-runtime.js +14 -2
  76. package/dist/scope.js +7 -1
  77. package/dist/secret-mint.d.ts +1 -0
  78. package/dist/secret-mint.js +14 -0
  79. package/dist/serve.js +101 -51
  80. package/dist/server/chat.js +5 -5
  81. package/dist/server/render-tasks.d.ts +2 -0
  82. package/dist/server/render-tasks.js +2 -1
  83. package/dist/server/request-context.d.ts +6 -0
  84. package/dist/server/runtime.d.ts +2 -0
  85. package/dist/server/task-views.js +7 -2
  86. package/dist/setup-guide.d.ts +2 -2
  87. package/dist/storage.d.ts +5 -0
  88. package/dist/storage.js +28 -1
  89. package/dist/store.d.ts +10 -0
  90. package/dist/store.js +25 -1
  91. package/dist/task-base.d.ts +4 -0
  92. package/dist/task-base.js +22 -2
  93. package/dist/teardown-git.d.ts +52 -3
  94. package/dist/teardown-git.js +143 -31
  95. package/dist/test-infra.js +1 -1
  96. package/dist/up-processes.d.ts +137 -0
  97. package/dist/up-processes.js +165 -0
  98. package/dist/up-role-child.d.ts +1 -0
  99. package/dist/up-role-child.js +77 -0
  100. package/dist/verification-evidence.d.ts +1 -0
  101. package/dist/verification-evidence.js +13 -1
  102. package/package.json +2 -1
package/dist/doctor.js CHANGED
@@ -21,6 +21,7 @@ import { join } from "node:path";
21
21
  import { daemonStatus, installedServiceDefinition, planDaemon } from "./daemon.js";
22
22
  import { scanForSecrets } from "./evidence.js";
23
23
  import { envelopeJson } from "./envelope.js";
24
+ import { GIT_FLOOR_WORDS, installedGitFloorProblem } from "./git-floor.js";
24
25
  import { run as runCommand } from "./exec.js";
25
26
  import { OWN_KEY_ENV } from "./keys.js";
26
27
  import { ALL_CREDENTIAL_ENV } from "./provider.js";
@@ -123,7 +124,8 @@ function reinstallCommand(args) {
123
124
  const flag = equal < 0 ? arg : arg.slice(0, equal);
124
125
  const value = equal < 0 ? args[index + 1] : arg.slice(equal + 1);
125
126
  const known = freeform.has(flag) || numeric.has(flag) || Object.hasOwn(enums, flag);
126
- if (equal < 0 && (known || CREDENTIAL_FLAG.test(flag)) && value !== undefined && !value.startsWith("-"))
127
+ // A credential's value is the next argument even when it starts with "-" (random base64url can).
128
+ if (equal < 0 && value !== undefined && (CREDENTIAL_FLAG.test(flag) || (known && !value.startsWith("-"))))
127
129
  index += 1;
128
130
  if (flag === "--token") {
129
131
  kept.push("--token-file", "<set-again>");
@@ -386,7 +388,9 @@ export async function auditServiceEnvironment(deps = {}) {
386
388
  /** The report as the owner reads it: one state, then each finding with its steps; detail after. */
387
389
  export function doctorLines(report) {
388
390
  const lines = [];
389
- if (report.ok)
391
+ if (report.git?.problem != null)
392
+ lines.push(`Git: ${report.git.problem}`, "");
393
+ if (report.findings.length === 0 && report.unreadable.length === 0)
390
394
  lines.push("No secrets found in Toolroll's service. A build can't read a key from it.");
391
395
  else if (report.findings.length > 0)
392
396
  lines.push(`Toolroll's service holds ${report.findings.length === 1 ? "a secret" : "secrets"} a build could read. Nothing was changed; follow these steps.`);
@@ -422,6 +426,10 @@ export async function runDoctorCommand(argv, write, deps = {}) {
422
426
  }
423
427
  const report = await auditServiceEnvironment(deps);
424
428
  report.sandbox = { engine: SANDBOX_RUNTIME.name, version: SANDBOX_RUNTIME.version, problem: sandboxRuntimeProblem() };
429
+ // A git older than the floor: named here too, and the console and worker won't start on it.
430
+ report.git = { floor: GIT_FLOOR_WORDS, problem: deps.gitFloorProblem === undefined ? installedGitFloorProblem() : deps.gitFloorProblem };
431
+ if (report.git.problem !== null)
432
+ report.ok = false;
425
433
  if (json)
426
434
  write(envelopeJson({ command: "doctor", ...report }));
427
435
  else
package/dist/envelope.js CHANGED
@@ -74,7 +74,8 @@ export const DOCUMENTED_REASONS = [
74
74
  "drift", // the branch, pull request or result moved from the gated commit
75
75
  "check-failed", // the release check failed; it stays visible and nothing reruns it
76
76
  "ci-failed", // a required pull-request check failed or is missing — nothing merged
77
- "base-moved", // main gained a commit the release branch lacks — nothing merged
77
+ "git-too-old", // the installed git is older than Toolroll's floor (git-floor.ts) — serve, watch and up don't start
78
+ "base-moved", // the release branch lacks main's head — nothing filed (gate) or merged
78
79
  "tree-mismatch", // main's merge commit has a different tree than the gated commit — nothing tagged
79
80
  "conflict", // a task or tag exists for a different commit
80
81
  "mixed-runtime", // the console, worker and CLI names don't run one build
package/dist/evidence.js CHANGED
@@ -23,6 +23,7 @@ import { parseReport, REPORT_LIMITS } from "./scout-report.js";
23
23
  import { parseProof, PROOF_LIMITS } from "./proof.js";
24
24
  import { HANDOFF_VERSION, readHandoffArtifact } from "./contracts/handoff.js";
25
25
  import { encodeBaseTreeSnapshot, parseBaseTreeSnapshot } from "./peek.js";
26
+ import { noLazyFetch } from "./teardown-git.js";
26
27
  /** The park mailbox pattern. Each run's actual name carries a nonce; see `mailboxName`. */
27
28
  export const MAILBOX_PREFIX = "STANDING-ORDERS-PARK-";
28
29
  /** The terminal handoff: how every non-parking attempt says how it ended. */
@@ -929,7 +930,9 @@ export async function captureBaseTree(store, git, repoRoot, repo, base, root, ru
929
930
  }
930
931
  }
931
932
  async function captureBaseTreeInner(store, git, repoRoot, repo, base, root, runId, now) {
932
- const args = ["--no-lazy-fetch", "--no-replace-objects", "--no-optional-locks", "ls-tree", "-r", "-l", "-z", base];
933
+ const args = [...noLazyFetch(), "-c", "protocol.allow=never", "--no-replace-objects", "--no-optional-locks", "ls-tree", "-r", "-l", "-z", base];
934
+ // `git` is the builder's hardened checkout git (a frozen view): on a git without --no-lazy-fetch it refuses a partial
935
+ // clone from the same config it runs with (teardown-git.ts lazyFetchSetting).
933
936
  const listed = await git("git", args, { cwd: repoRoot, envAllowlist: ["PATH", "HOME"] });
934
937
  const command = `git ls-tree -r -l -z ${base} (exit ${listed.code})`;
935
938
  const refuse = () => {
package/dist/exec.d.ts CHANGED
@@ -28,6 +28,13 @@ export type ExecResult = {
28
28
  * returned — a transport exit alone never proves that.
29
29
  */
30
30
  containment?: ContainmentOutcome;
31
+ /** The signal that ended the process, when one did (its `code` then reads 1, as it always has): kept apart so a
32
+ * killed command is never mistaken for one that exited 1 on its own (release check 3261). Only the buffered
33
+ * process-group transport says. */
34
+ signal?: NodeJS.Signals | null;
35
+ /** This process (the plane) ended it with its hard stop (terminateLiveProviders: a worker shutting down), not
36
+ * anything the command or another process did. */
37
+ stoppedByPlane?: boolean;
31
38
  };
32
39
  export type ContainmentOutcome = {
33
40
  refused: string;
package/dist/exec.js CHANGED
@@ -21,6 +21,8 @@ import { primaryResultEvent, topLevelEvent } from "./stream-scope.js";
21
21
  export const CONTAINMENT_REFUSED_CODE = 126;
22
22
  /** Live provider children, for the deterministic stop. Registered only when `processGroup` was set. */
23
23
  const liveProviders = new Set();
24
+ /** Children this process's own hard stop ended: their results say so (`stoppedByPlane`). */
25
+ const hardStopped = new WeakSet();
24
26
  /** Live children by owner tag (v52): the exact-attempt stop's handle. A
25
27
  * child is registered here beside liveProviders when its options named an
26
28
  * owner, and dropped the moment it closes. */
@@ -370,6 +372,7 @@ function killGroup(child) {
370
372
  export function terminateLiveProviders() {
371
373
  let terminated = 0;
372
374
  for (const child of liveProviders) {
375
+ hardStopped.add(child);
373
376
  killGroup(child);
374
377
  terminated += 1;
375
378
  }
@@ -677,6 +680,7 @@ function runBufferedGroup(file, args, bag) {
677
680
  });
678
681
  let settled = false;
679
682
  let transient = false;
683
+ let endSignal = null;
680
684
  const finish = (code) => {
681
685
  if (settled)
682
686
  return;
@@ -697,6 +701,8 @@ function runBufferedGroup(file, args, bag) {
697
701
  timedOut: timedOut && !overflowed,
698
702
  notFound,
699
703
  ...containmentOf(container, empty, attachFailure),
704
+ ...(endSignal === null ? {} : { signal: endSignal }),
705
+ ...(hardStopped.has(child) ? { stoppedByPlane: true } : {}),
700
706
  },
701
707
  transient,
702
708
  });
@@ -709,7 +715,7 @@ function runBufferedGroup(file, args, bag) {
709
715
  stderr = String(error);
710
716
  finish(null);
711
717
  });
712
- child.on("close", code => finish(code));
718
+ child.on("close", (code, signal) => { endSignal = code === null ? signal ?? null : null; finish(code); });
713
719
  if (bag.stdin !== undefined && child.stdin !== null) {
714
720
  child.stdin.on("error", () => {
715
721
  inputFailed = true;
@@ -0,0 +1,16 @@
1
+ export declare const GIT_FLOOR: {
2
+ readonly major: 2;
3
+ readonly minor: 41;
4
+ };
5
+ export declare const GIT_FLOOR_WORDS: string;
6
+ /** The version `git --version` names ("git version 2.50.1 (Apple Git-155)" → 2.50.1), or null when it names none. */
7
+ export declare function parseGitVersion(text: string): {
8
+ major: number;
9
+ minor: number;
10
+ words: string;
11
+ } | null;
12
+ /** Why `version` (`git --version`'s answer) is below the floor, in one sentence, or null when it isn't (or can't be read). */
13
+ export declare function gitFloorWords(versionText: string): string | null;
14
+ /** The installed git's floor problem, asked once per process with no system or global config; null when git is new
15
+ * enough, or when no version could be read (a missing git is reported where it is used). */
16
+ export declare function installedGitFloorProblem(): string | null;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The oldest git Toolroll runs on (README, Requirements): the global `git --attr-source` option every hardened read
3
+ * takes arrived in git 2.41 (others in use are older: `worktree list -z` 2.36, GIT_CONFIG_COUNT and `--path-format`
4
+ * 2.31). On git older than 2.45, which lacks `--no-lazy-fetch`, partial clones are refused for protected reads
5
+ * (teardown-git.ts lazyFetchSupported), every protected read runs through a frozen view and refuses every transport,
6
+ * and nothing leans on GIT_NO_LAZY_FETCH, which 2.41.0 ignores. The CI git lanes test this floor's oldest release
7
+ * exactly, 2.41.0, and 2.47 (.github/workflows/ci.yml, scripts/ci-git.sh).
8
+ */
9
+ import { spawnSync } from "node:child_process";
10
+ export const GIT_FLOOR = { major: 2, minor: 41 };
11
+ export const GIT_FLOOR_WORDS = `${GIT_FLOOR.major}.${GIT_FLOOR.minor}`;
12
+ /** The version `git --version` names ("git version 2.50.1 (Apple Git-155)" → 2.50.1), or null when it names none. */
13
+ export function parseGitVersion(text) {
14
+ const found = /^git version (\d+)\.(\d+)(?:\.(\d+))?/.exec(text.trim());
15
+ return found === null ? null : { major: Number(found[1]), minor: Number(found[2]), words: `${found[1]}.${found[2]}${found[3] === undefined ? "" : `.${found[3]}`}` };
16
+ }
17
+ /** Why `version` (`git --version`'s answer) is below the floor, in one sentence, or null when it isn't (or can't be read). */
18
+ export function gitFloorWords(versionText) {
19
+ const version = parseGitVersion(versionText);
20
+ if (version === null)
21
+ return null;
22
+ if (version.major > GIT_FLOOR.major || (version.major === GIT_FLOOR.major && version.minor >= GIT_FLOOR.minor))
23
+ return null;
24
+ return `Toolroll needs git ${GIT_FLOOR_WORDS} or newer, and this machine's git is ${version.words}. Install a newer git (and put it first on PATH), then start Toolroll again.`;
25
+ }
26
+ let probed;
27
+ /** The installed git's floor problem, asked once per process with no system or global config; null when git is new
28
+ * enough, or when no version could be read (a missing git is reported where it is used). */
29
+ export function installedGitFloorProblem() {
30
+ if (probed === undefined) {
31
+ const answer = spawnSync("git", ["--version"], { cwd: "/", encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "ignore"],
32
+ env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } });
33
+ probed = answer.status === 0 ? gitFloorWords(answer.stdout ?? "") : null;
34
+ }
35
+ return probed;
36
+ }
package/dist/grant.js CHANGED
@@ -30,6 +30,7 @@
30
30
  * the same reason an unknown lease is: silence is not consent.
31
31
  */
32
32
  import { run } from "./exec.js";
33
+ import { lazyFetchSafeGit, noLazyFetch } from "./teardown-git.js";
33
34
  export const MUTATION_CLASSES = [
34
35
  "create",
35
36
  "transition",
@@ -175,12 +176,15 @@ async function observedByGit(repo, paths, runner) {
175
176
  const local = paths.filter(path => !isRemoteRef(path));
176
177
  if (local.length === 0)
177
178
  return false;
179
+ // On a git without --no-lazy-fetch, through a frozen read view (teardown-git.ts lazyFetchSafeGit): a partial clone is
180
+ // refused there, and a refusal reads as "not ignored", so observed.
181
+ const git = lazyFetchSafeGit(runner);
178
182
  for (const path of local) {
179
183
  const asked = [path, `${normalise(path)}/`];
180
184
  const ignored = await Promise.all(asked.map(async (candidate) => {
181
185
  // check-ignore reads the index, so a planted fsmonitor would run: off (and hooks, lazy fetch) on the command line,
182
186
  // which wins over any config the repository's builds can write.
183
- const result = await runner("git", ["--no-optional-locks", "--no-lazy-fetch", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "check-ignore", "-q", candidate], {
187
+ const result = await git("git", ["--no-optional-locks", ...noLazyFetch(), "-c", "protocol.allow=never", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "check-ignore", "-q", candidate], {
184
188
  cwd: repo,
185
189
  });
186
190
  return result.code === 0;
package/dist/guides.js CHANGED
@@ -510,9 +510,10 @@ instead of making them twice.
510
510
  rerunning the failed job on GitHub, run the same command again.
511
511
  - \`publish-failed\` / \`homebrew-failed\`: fix the failed workflow before
512
512
  resuming. A failed tag check cannot finish publication or update Homebrew.
513
- - \`base-moved\`: main gained a commit the branch doesn't have, so the
514
- merge wouldn't be the checked tree. Nothing was merged: update the
515
- branch and release it with \`--new\`.
513
+ - \`base-moved\`: the branch is behind main (it doesn't contain main's
514
+ head), so the merge wouldn't be the tree CI tested. Checked at the gate
515
+ (nothing filed) and again right before merging (nothing merged): update
516
+ the branch, push, and release it with \`--new\`.
516
517
  - \`tree-mismatch\`: main changed what was checked. Nothing was tagged;
517
518
  the deployed build stays.
518
519
  - \`timeout\`: rerun to keep waiting; \`--limit <step>=<minutes>\`
package/dist/operate.d.ts CHANGED
@@ -6,6 +6,7 @@ import type { StepIo } from "./flow-steps.js";
6
6
  import type { FetchLike } from "./flow-share.js";
7
7
  import { type Store } from "./store.js";
8
8
  import { type DispatchAdapter } from "./sync.js";
9
+ import { type RolePort, type UpRoleStart } from "./up-processes.js";
9
10
  import { type TelegramTransport } from "./telegram.js";
10
11
  import { type LeadCliSeams } from "./lead-cli.js";
11
12
  import { type Principal, type RemoteSource } from "./operate-remote.js";
@@ -89,6 +90,8 @@ export type OperateOptions = {
89
90
  };
90
91
  /** Injected by tests: the classifier that sizes filed tasks (none by default under test). */
91
92
  filingSizer?: Sizer;
93
+ /** Set only by up-role-child.ts: this process is one role of a `toolroll up` (the console or the worker). */
94
+ upRole?: UpRoleHost;
92
95
  };
93
96
  export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection" | "pullRequests">>;
94
97
  export declare const OPERATE_HELP: string;
@@ -117,6 +120,8 @@ export declare const KEYS_ACTIONS: readonly ["status", "set", "clear", "verify",
117
120
  * tests): every value-taking flag any verb reads, and every boolean.
118
121
  * A --flag in neither set is a typo, refused by name.
119
122
  */
123
+ /** Flags whose value is a secret someone types or pastes: taken as the next argument even when it starts with "--". */
124
+ export declare const SECRET_VALUE_FLAGS: ReadonlySet<string>;
120
125
  export declare const OPERATE_VALUE_FLAGS: ReadonlySet<string>;
121
126
  export declare const OPERATE_BOOLEAN_FLAGS: ReadonlySet<string>;
122
127
  export declare function parseOperateArgs(argv: readonly string[], ownValues?: ReadonlySet<string>): Args | {
@@ -168,3 +173,11 @@ export declare function readUpConsoleUrl(dir: string): string | null;
168
173
  export declare const PROPOSALS_ACTIONS: readonly ["list", "confirm", "dismiss"];
169
174
  /** The value flags a verb reads under a global switch's name, for parseOperateArgs. */
170
175
  export declare function ownValueFlags(command: string, argv: readonly string[]): ReadonlySet<string> | undefined;
176
+ /** What up-role-child.ts hands runOperate: the role's start, and its channel to the `up` that started it. */
177
+ export type UpRoleHost = {
178
+ start: UpRoleStart;
179
+ port: RolePort;
180
+ };
181
+ /** A role child's whole run (up-role-child.ts): the ordinary command prelude (containment, secrets, limits), then the
182
+ * role itself. Only a process started by `up` gets here; no command line selects a role. */
183
+ export declare function runUpRole(start: UpRoleStart, port: RolePort): Promise<number>;