toolroll 0.9.77 → 0.9.79

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 (77) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/agent-fence.js +18 -2
  3. package/dist/backup-ui.d.ts +9 -1
  4. package/dist/backup-ui.js +28 -3
  5. package/dist/backup.js +4 -3
  6. package/dist/browser/workspace.css +1 -1
  7. package/dist/browser/workspace.js +18 -18
  8. package/dist/browser-workspace.d.ts +2 -0
  9. package/dist/builder.d.ts +4 -3
  10. package/dist/builder.js +14 -8
  11. package/dist/check-failure.js +31 -3
  12. package/dist/check-report.d.ts +18 -3
  13. package/dist/check-report.js +67 -7
  14. package/dist/claim.d.ts +9 -0
  15. package/dist/claim.js +9 -1
  16. package/dist/contracts/console-api.d.ts +1 -1
  17. package/dist/contracts/console-api.js +1 -1
  18. package/dist/contracts/release-terms.d.ts +41 -0
  19. package/dist/contracts/release-terms.js +53 -0
  20. package/dist/controller-service.js +4 -1
  21. package/dist/deploy-rescue.d.ts +41 -0
  22. package/dist/deploy-rescue.js +94 -0
  23. package/dist/dispose.js +4 -3
  24. package/dist/evidence.d.ts +9 -0
  25. package/dist/evidence.js +15 -3
  26. package/dist/exec.d.ts +2 -0
  27. package/dist/exec.js +15 -1
  28. package/dist/git-views.d.ts +1 -1
  29. package/dist/git-views.js +2 -2
  30. package/dist/guides.js +18 -8
  31. package/dist/latency-timeline.d.ts +46 -0
  32. package/dist/latency-timeline.js +120 -0
  33. package/dist/live-backup.d.ts +39 -0
  34. package/dist/live-backup.js +271 -0
  35. package/dist/operate.js +203 -16
  36. package/dist/planner.js +2 -2
  37. package/dist/project-knowledge.d.ts +7 -7
  38. package/dist/release-adapters.d.ts +41 -3
  39. package/dist/release-adapters.js +275 -73
  40. package/dist/release-carry.d.ts +21 -1
  41. package/dist/release-carry.js +22 -0
  42. package/dist/release-cli.d.ts +3 -0
  43. package/dist/release-cli.js +74 -5
  44. package/dist/release-gate.d.ts +5 -0
  45. package/dist/release-gate.js +1 -1
  46. package/dist/release-net.d.ts +90 -0
  47. package/dist/release-net.js +253 -0
  48. package/dist/release-recheck.js +3 -2
  49. package/dist/release.d.ts +40 -5
  50. package/dist/release.js +142 -52
  51. package/dist/runner-process.d.ts +67 -0
  52. package/dist/runner-process.js +219 -0
  53. package/dist/runner.d.ts +48 -6
  54. package/dist/runner.js +71 -8
  55. package/dist/scope.d.ts +19 -1
  56. package/dist/scope.js +19 -2
  57. package/dist/scout.js +2 -2
  58. package/dist/serve.js +11 -0
  59. package/dist/server/http.d.ts +2 -0
  60. package/dist/server/pages.js +7 -3
  61. package/dist/server/remote-hooks.d.ts +1 -1
  62. package/dist/server/remote-hooks.js +5 -3
  63. package/dist/server/render-results.d.ts +2 -0
  64. package/dist/server/render-results.js +1 -0
  65. package/dist/server/request-context.d.ts +2 -0
  66. package/dist/server/settings.js +22 -2
  67. package/dist/server/tasks.js +4 -1
  68. package/dist/storage-sweep.js +1 -1
  69. package/dist/store.d.ts +125 -2
  70. package/dist/store.js +250 -12
  71. package/dist/surface.js +3 -1
  72. package/dist/teardown-git.d.ts +1 -1
  73. package/dist/teardown-git.js +12 -12
  74. package/dist/toolroll-update.js +2 -2
  75. package/dist/up-role-child.js +6 -2
  76. package/dist/workspace-revision.js +3 -1
  77. package/package.json +1 -1
@@ -190,7 +190,7 @@ function nestedRepository(path) {
190
190
  * repository (boundDirs; `binding.repo` when the caller has the project's), is refused; so is a command whose
191
191
  * repository changed under it while git ran.
192
192
  */
193
- async function inView(cwd, binding, work, after = true) {
193
+ async function inView(cwd, binding, work, after = true, lockTimeoutMs, gitFile = "git") {
194
194
  const root = checkoutRootOf(cwd);
195
195
  if (root === null)
196
196
  return NOT_A_REPOSITORY;
@@ -206,7 +206,7 @@ async function inView(cwd, binding, work, after = true) {
206
206
  return await withCheckoutLock(realpathSync(homedir()), key, async () => {
207
207
  let view;
208
208
  try {
209
- view = gitView(root, "write", binding);
209
+ view = gitView(root, "write", binding, gitFile);
210
210
  }
211
211
  catch (error) {
212
212
  return refused(error instanceof Error ? error.message : "the checkout couldn't be read");
@@ -221,7 +221,7 @@ async function inView(cwd, binding, work, after = true) {
221
221
  finally {
222
222
  view.dispose();
223
223
  }
224
- });
224
+ }, lockTimeoutMs);
225
225
  }
226
226
  catch (error) {
227
227
  return refused(error instanceof Error ? error.message : "the checkout is busy");
@@ -234,11 +234,11 @@ async function inView(cwd, binding, work, after = true) {
234
234
  * target is bound to the same repository as the folder it is removed from. A removal without --force runs under the
235
235
  * target's own view (its own config.worktree folded in), so git's last check of whether it is clean reads it as the
236
236
  * target itself would. `binding.repo`: the project's main checkout, from the plane's records, when the caller has it. */
237
- export function teardownGit(runner = run, binding = {}) {
237
+ export function teardownGit(runner = run, binding = {}, lockTimeoutMs, gitFile = "git") {
238
238
  const options = (cwd, env) => ({ cwd, envAllowlist: ENV_ALLOWLIST, env: { ...ENV, ...env }, timeoutMs: WORKTREE_TIMEOUT_MS, maxBuffer: 512 * 1024 * 1024 });
239
239
  const configProblem = async (cwd, env) => {
240
240
  // git-lfs's own filters may stand only beside a .lfsconfig that names no extension (commandSetting's lfsChecked).
241
- if (lfsExtensionIn(env["GIT_WORK_TREE"] ?? cwd))
241
+ if (lfsExtensionIn(env["GIT_WORK_TREE"] ?? cwd, gitFile))
242
242
  return refused("the checkout's .lfsconfig sets a git-lfs key that can run a command");
243
243
  const config = await runner("git", ["--no-optional-locks", "config", "--list", "--no-includes", "--show-scope", "-z"], options(cwd, env));
244
244
  if (config.code !== 0)
@@ -287,8 +287,8 @@ export function teardownGit(runner = run, binding = {}) {
287
287
  // the target's view, inspected above, so it judges the target by the target's own settings.
288
288
  return ran(removal && !forced ? { ...view.env, GIT_COMMON_DIR: targetView.env["GIT_COMMON_DIR"] } : view.env);
289
289
  // A removed target has no git directory left to prove: the folder it was removed from is proven instead.
290
- }, !removal);
291
- });
290
+ }, !removal, lockTimeoutMs, gitFile);
291
+ }, true, lockTimeoutMs, gitFile);
292
292
  }
293
293
  /**
294
294
  * Git on a build's checkout, as the plane runs it outside every fence (staging, committing and inspecting the build's
@@ -364,7 +364,7 @@ function includeIn(path, gitFile = "git") {
364
364
  const text = regularFile(path);
365
365
  if (text === null)
366
366
  return "an unreadable file";
367
- const parsed = spawnSync(gitFile, ["config", "--file", path, "--no-includes", "--name-only", "--list", "-z"], { cwd: "/", encoding: "utf8", timeout: 5_000, env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } });
367
+ const parsed = spawnSync(gitFile, ["config", "--file", path, "--no-includes", "--name-only", "--list", "-z"], { cwd: "/", encoding: "utf8", timeout: 5_000, killSignal: "SIGKILL", env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } });
368
368
  if (parsed.status !== 0)
369
369
  return "an unreadable file";
370
370
  const named = parsed.stdout.split("\0").find(name => /^(include\.path|includeif\..*\.path)$/i.test(name));
@@ -488,7 +488,7 @@ function gitView(checkout, kind, binding = {}, gitFile = "git") {
488
488
  throw new Error("the checkout's git config couldn't be read as a plain file");
489
489
  const configPath = join(view, "config");
490
490
  writeFileSync(configPath, config, { mode: 0o600 });
491
- const plain = { cwd: "/", encoding: "utf8", timeout: 5_000, env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } };
491
+ const plain = { cwd: "/", encoding: "utf8", timeout: 5_000, killSignal: "SIGKILL", env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } };
492
492
  if (/worktreeconfig/i.test(config)) {
493
493
  const flag = spawnSync(gitFile, ["config", "--file", configPath, "--no-includes", "--type=bool", "--get", "extensions.worktreeConfig"], plain);
494
494
  if (flag.status !== 0 && flag.status !== 1)
@@ -1038,7 +1038,7 @@ export function lfsExtensionIn(checkout, gitFile = "git") {
1038
1038
  catch {
1039
1039
  return false;
1040
1040
  }
1041
- const parsed = spawnSync(gitFile, ["config", "--file", path, "--no-includes", "--name-only", "--list", "-z"], { cwd: "/", encoding: "utf8", timeout: 5_000, env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } });
1041
+ const parsed = spawnSync(gitFile, ["config", "--file", path, "--no-includes", "--name-only", "--list", "-z"], { cwd: "/", encoding: "utf8", timeout: 5_000, killSignal: "SIGKILL", env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } });
1042
1042
  return parsed.status !== 0 || parsed.stdout.split("\0").filter(Boolean).some(name => lfsCommand(name) || ["include", "includeif"].includes(keyParts(name).section));
1043
1043
  }
1044
1044
  /** Every command runs against a view of the checkout's repository (gitView) whose config and info are copies the
@@ -1148,7 +1148,7 @@ export function viewedGitSync(cwd, args, options = {}) {
1148
1148
  const view = gitView(cwd, "read", options.binding, gitFile);
1149
1149
  try {
1150
1150
  const env = { PATH: process.env["PATH"] ?? "", HOME: process.env["HOME"] ?? "", LANG: "C", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_TERMINAL_PROMPT: "0", ...view.env };
1151
- const bounded = { cwd, env, encoding: "utf8", timeout: options.timeoutMs ?? 60_000, maxBuffer: options.maxBuffer ?? 1_048_576, stdio: ["ignore", "pipe", "pipe"] };
1151
+ const bounded = { cwd, env, encoding: "utf8", timeout: options.timeoutMs ?? 60_000, killSignal: "SIGKILL", maxBuffer: options.maxBuffer ?? 1_048_576, stdio: ["ignore", "pipe", "pipe"] };
1152
1152
  const own = spawnSync(gitFile, ["--no-optional-locks", "config", "--list", "--show-scope", "-z"], bounded);
1153
1153
  if (own.status !== 0)
1154
1154
  throw new Error("its config couldn't be read");
@@ -1234,7 +1234,7 @@ export function lazyFetchSupported(gitFile = "git") {
1234
1234
  const executable = gitExecutablePath(gitFile);
1235
1235
  let known = lazyFetchByGit.get(executable);
1236
1236
  if (known === undefined) {
1237
- const probe = spawnSync(executable, ["--no-lazy-fetch", "version"], { cwd: "/", encoding: "utf8", timeout: 5_000, stdio: ["ignore", "pipe", "pipe"],
1237
+ const probe = spawnSync(executable, ["--no-lazy-fetch", "version"], { cwd: "/", encoding: "utf8", timeout: 5_000, killSignal: "SIGKILL", stdio: ["ignore", "pipe", "pipe"],
1238
1238
  env: { PATH: process.env["PATH"] ?? "", GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", HOME: "/nonexistent" } });
1239
1239
  known = probe.status === 0;
1240
1240
  lazyFetchByGit.set(executable, known);
@@ -37,7 +37,7 @@ import { removeTerminalUpdateGate } from "./terminal-catalog.js";
37
37
  import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner, removeCodingUpdateGate } from "./coding-update.js";
38
38
  import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
39
39
  import { NAME } from "./names.js";
40
- import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS } from "./store.js";
40
+ import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS } from "./store.js";
41
41
  import { isNewer, REGISTRY } from "./releases.js";
42
42
  import { markNeverIndex } from "./never-index.js";
43
43
  import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
@@ -409,7 +409,7 @@ const HISTORY_RULES = {
409
409
  // v126: a receipt's state, acceptance time and held item, a trigger's credential generation and turned-away count,
410
410
  // and who made a card read none, 0, or none. A table with an earlier rule (flow_trigger's v115 move) keeps it too.
411
411
  // v128: a lead message's pending flag reads waiting exactly for a team queue's unadmitted operator message.
412
- ...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS))
412
+ ...Object.fromEntries(Object.entries(addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS, V125_ADDED_COLUMNS, V126_ADDED_COLUMNS, V128_ADDED_COLUMNS, V129_ADDED_COLUMNS, V134_ADDED_COLUMNS, V135_ADDED_COLUMNS, V138_ADDED_COLUMNS))
413
413
  .map(([table, rule]) => [table, { ...EARLIER_RULES[table], ...rule }])),
414
414
  };
415
415
  /** The tables a migration may remove whole (v115's removed features). */
@@ -39,8 +39,12 @@ const port = {
39
39
  // stop on the second), so a role never acts on SIGINT itself. SIGTERM (the supervisor's stop) drains, once.
40
40
  process.on("SIGINT", () => { });
41
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));
42
+ // The parent gone (its channel closed): drain, as a stop would. The worker's own grace timer bounds it; past a minute
43
+ // with nobody left to stop it, the role stops its work outright and ends, never running on as an orphan.
44
+ process.once("disconnect", () => {
45
+ stop(false);
46
+ setTimeout(() => { stop(true); setTimeout(() => process.exit(1), 5_000).unref(); }, 60_000).unref();
47
+ });
44
48
  const start = await new Promise(resolve => {
45
49
  const timer = setTimeout(() => resolve(null), 60_000);
46
50
  process.once("message", value => { clearTimeout(timer); resolve(value); });
@@ -8,7 +8,9 @@ const OMIT = new Set(['schema_version', 'service_cursor', 'wake', 'notification_
8
8
  // Who is watching an agent right now: renewed by every open page's keep-alive, never news.
9
9
  'agent_view',
10
10
  // A run's How it ran, derived from its own records on first view (run-efficiency.ts): a cache, never news.
11
- 'run_efficiency']);
11
+ 'run_efficiency',
12
+ // v137: lifecycle heartbeats, latency timelines and the continuous copy's record are bookkeeping, never news.
13
+ 'service_lifecycle', 'latency_timeline', 'live_backup']);
12
14
  const QUIET = {
13
15
  runner: ['heartbeat_at'], claim: ['heartbeat_at', 'expires_at'], watch_lease: ['heartbeat_at', 'expires_at'],
14
16
  watch_episode: ['ticks'], provider_readiness: ['observed_at'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.77",
3
+ "version": "0.9.79",
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"