toolroll 0.9.41 → 0.9.43

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 (89) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/chat-actions.js +1 -1
  3. package/dist/chat-approval.d.ts +45 -0
  4. package/dist/chat-approval.js +81 -0
  5. package/dist/chat-channel.js +5 -0
  6. package/dist/chat-decide.d.ts +18 -6
  7. package/dist/chat-decide.js +54 -20
  8. package/dist/chat-flow.js +2 -2
  9. package/dist/cli.js +4 -2
  10. package/dist/coding-types.d.ts +2 -24
  11. package/dist/coding-workspace.js +12 -11
  12. package/dist/contracts/cli.d.ts +736 -0
  13. package/dist/contracts/cli.js +223 -0
  14. package/dist/contracts/coding-activity.d.ts +64 -0
  15. package/dist/contracts/coding-activity.js +58 -0
  16. package/dist/contracts/console-api.d.ts +484 -0
  17. package/dist/contracts/console-api.js +274 -0
  18. package/dist/contracts/diff-stat.d.ts +58 -0
  19. package/dist/contracts/diff-stat.js +59 -0
  20. package/dist/contracts/integration-metadata.d.ts +99 -0
  21. package/dist/contracts/integration-metadata.js +101 -0
  22. package/dist/contracts/lead-tools.d.ts +11 -0
  23. package/dist/contracts/lead-tools.js +2 -0
  24. package/dist/contracts/model-catalog.d.ts +21 -0
  25. package/dist/contracts/model-catalog.js +14 -0
  26. package/dist/contracts/native-census.d.ts +42 -0
  27. package/dist/contracts/native-census.js +36 -0
  28. package/dist/contracts/observation-request.d.ts +37 -0
  29. package/dist/contracts/observation-request.js +25 -0
  30. package/dist/contracts/provider.d.ts +30 -0
  31. package/dist/contracts/provider.js +15 -0
  32. package/dist/contracts/recipes.d.ts +166 -0
  33. package/dist/contracts/recipes.js +52 -0
  34. package/dist/contracts/retention.d.ts +42 -0
  35. package/dist/contracts/retention.js +24 -0
  36. package/dist/contracts/storage.d.ts +23 -0
  37. package/dist/contracts/storage.js +10 -0
  38. package/dist/contracts/store-json.d.ts +315 -0
  39. package/dist/contracts/store-json.js +196 -0
  40. package/dist/contracts/trigger-payloads.d.ts +369 -0
  41. package/dist/contracts/trigger-payloads.js +144 -0
  42. package/dist/contracts/update-journal.d.ts +254 -0
  43. package/dist/contracts/update-journal.js +155 -0
  44. package/dist/contracts/verification-view.d.ts +93 -0
  45. package/dist/contracts/verification-view.js +43 -0
  46. package/dist/control-setup.d.ts +1 -1
  47. package/dist/desktop-update.d.ts +7 -40
  48. package/dist/desktop-update.js +36 -8
  49. package/dist/evidence.d.ts +3 -19
  50. package/dist/flow-engine.d.ts +2 -0
  51. package/dist/flow-engine.js +10 -2
  52. package/dist/flow-triggers.js +61 -60
  53. package/dist/flows-cli.d.ts +28 -0
  54. package/dist/flows-cli.js +43 -1
  55. package/dist/guides.js +2 -0
  56. package/dist/mate-contract.js +1 -1
  57. package/dist/mate-tools.js +43 -1
  58. package/dist/mcp-connect.js +37 -22
  59. package/dist/model-catalog.js +8 -6
  60. package/dist/observations.d.ts +2 -6
  61. package/dist/observations.js +13 -9
  62. package/dist/operate.js +55 -4
  63. package/dist/process-recovery-eligibility.d.ts +1 -1
  64. package/dist/process-recovery-native.d.ts +2 -13
  65. package/dist/process-recovery-native.js +8 -5
  66. package/dist/project-tools.d.ts +6 -24
  67. package/dist/project-tools.js +22 -18
  68. package/dist/provider.d.ts +3 -8
  69. package/dist/provider.js +15 -11
  70. package/dist/recipes.d.ts +3 -28
  71. package/dist/recipes.js +79 -52
  72. package/dist/retention.d.ts +4 -5
  73. package/dist/retention.js +5 -4
  74. package/dist/scope.d.ts +4 -2
  75. package/dist/serve.js +193 -30
  76. package/dist/storage.d.ts +2 -1
  77. package/dist/storage.js +12 -9
  78. package/dist/store.d.ts +11 -16
  79. package/dist/store.js +122 -160
  80. package/dist/surface.d.ts +6 -54
  81. package/dist/surface.js +10 -2
  82. package/dist/teammate-work.js +1 -1
  83. package/dist/telegram-flow.d.ts +41 -1
  84. package/dist/telegram-flow.js +96 -12
  85. package/dist/telegram.js +27 -3
  86. package/dist/toolroll-update.d.ts +8 -99
  87. package/dist/toolroll-update.js +28 -18
  88. package/dist/verification-evidence.js +27 -12
  89. package/package.json +1 -1
@@ -37,6 +37,7 @@ import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurab
37
37
  import { NAME } from "./names.js";
38
38
  import { isNewer, REGISTRY } from "./releases.js";
39
39
  import { markNeverIndex } from "./never-index.js";
40
+ import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
40
41
  /** Loaded on first use (as backup.ts and store.ts do), so modules that only import this one (the console,
41
42
  * and tests that load it in a browser-like environment) never need `node:sqlite` itself. */
42
43
  function sqlite() {
@@ -48,7 +49,7 @@ export const PROVENANCE_ISSUER = "https://token.actions.githubusercontent.com";
48
49
  /** How long a watch's `toolroll up` may take to exit once launchd stops it (controller-supervisor's shutdown). */
49
50
  const UP_EXIT_MS = 45_000;
50
51
  /** The steps a person sees, in order. */
51
- export const UPDATE_STEPS = ["verifying", "draining", "backing-up", "rehearsing", "switching", "restarting", "health"];
52
+ export const UPDATE_STEPS = RUNTIME_UPDATE_STEPS;
52
53
  export const STEP_WORDS = {
53
54
  verifying: "Verify the package", draining: "Let running work finish", "backing-up": "Stop and back up",
54
55
  rehearsing: "Rehearse the migration", switching: "Switch to the new version", restarting: "Restart", health: "Health check",
@@ -58,7 +59,7 @@ export const KEEP_RUNTIMES = 2;
58
59
  /** The one-off launchd job the console starts the updater as. */
59
60
  export const UPDATE_JOB_LABEL = "com.toolroll.update";
60
61
  const TERMINAL = ["complete", "restored", "refused", "cancelled"];
61
- export const runtimeUpdateTerminal = (phase) => TERMINAL.includes(phase);
62
+ export const runtimeUpdateTerminal = (phase) => TERMINAL.some(known => known === phase);
62
63
  const journalFile = (stateDir) => join(stateDir, "toolroll-update.json");
63
64
  /** The last completed update, kept apart from the journal: what `--rollback` returns from. */
64
65
  const lastUpdateFile = (stateDir) => join(stateDir, "toolroll-update.last.json");
@@ -73,8 +74,10 @@ function markStarting(j, state, now) {
73
74
  }
74
75
  function updaterStarting(j, now) {
75
76
  try {
76
- const mark = JSON.parse(readFileSync(startingFile(j), "utf8"));
77
- const age = now.getTime() - Date.parse(mark.at ?? "");
77
+ const mark = updaterStartingOf(JSON.parse(readFileSync(startingFile(j), "utf8")));
78
+ if (mark === null)
79
+ return false;
80
+ const age = now.getTime() - mark.at;
78
81
  return mark.id === j.id && age >= 0 && age < UPDATER_START_MS;
79
82
  }
80
83
  catch {
@@ -93,14 +96,17 @@ export function readRuntimeUpdate(stateDir, file = journalFile(stateDir)) {
93
96
  const stat = lstatSync(file);
94
97
  if (!stat.isFile() || stat.isSymbolicLink())
95
98
  throw Error("The saved update record is not a regular file. Nothing was changed.");
96
- const j = JSON.parse(readFileSync(file, "utf8"));
99
+ const read = readRuntimeUpdateJournal(JSON.parse(readFileSync(file, "utf8")));
100
+ if (!read.ok)
101
+ throw Error(`The saved update record is invalid. Preserve it and its backups; nothing was changed. (${read.issues.map(issue => issue.line).join("; ")})`);
102
+ const j = read.value;
97
103
  const real = (path) => { try {
98
104
  return realpathSync(path);
99
105
  }
100
106
  catch {
101
107
  return resolve(path);
102
108
  } };
103
- if (j.version !== 1 || !/^[a-f0-9-]{36}$/.test(j.id) || typeof j.stateDir !== "string" || real(j.stateDir) !== real(stateDir) || !isAbsolute(j.stageDir ?? "") || real(dirname(j.stageDir)) !== real(join(stateDir, "staged-upgrades")))
109
+ if (!/^[a-f0-9-]{36}$/.test(j.id) || typeof j.stateDir !== "string" || real(j.stateDir) !== real(stateDir) || !isAbsolute(j.stageDir ?? "") || real(dirname(j.stageDir)) !== real(join(stateDir, "staged-upgrades")))
104
110
  throw Error("The saved update record is invalid. Preserve it and its backups; nothing was changed.");
105
111
  return j;
106
112
  }
@@ -121,13 +127,16 @@ function keepCompletedUpdate(stateDir) {
121
127
  return;
122
128
  durableJson(lastUpdateFile(stateDir), saved);
123
129
  }
130
+ /** A null phase saves progress without changing the saved phase, including one a newer release wrote. */
124
131
  function save(j, phase, detail, now) {
125
- if (j.phase !== phase)
126
- j.steps.push({ phase, at: now.toISOString() });
127
- j.phase = phase;
132
+ if (phase !== null) {
133
+ if (j.phase !== phase)
134
+ j.steps.push({ phase, at: now.toISOString() });
135
+ j.phase = phase;
136
+ }
128
137
  j.detail = detail;
129
138
  j.updatedAt = now.toISOString();
130
- if (runtimeUpdateTerminal(phase))
139
+ if (runtimeUpdateTerminal(j.phase))
131
140
  j.finishedAt = now.toISOString();
132
141
  durableJson(join(j.stageDir, "update.json"), j);
133
142
  durableJson(journalFile(j.stateDir), j);
@@ -518,7 +527,7 @@ async function stopServices(j, system, main, watches) {
518
527
  }
519
528
  if (watching.length > 0)
520
529
  j.watches = watching;
521
- save(j, j.phase, j.detail, system.now());
530
+ save(j, null, j.detail, system.now());
522
531
  for (const unit of [main, ...loadedWatches(j, watches)])
523
532
  if (unit)
524
533
  await system.stopService(unit);
@@ -781,7 +790,7 @@ export function requestRuntimeUpdateCancel(stateDir, now = new Date(), seams = {
781
790
  if (!j || runtimeUpdateTerminal(j.phase))
782
791
  return "No update is in progress.";
783
792
  const tooLate = (at) => `The update to ${at.to.version} is past the point it can be cancelled (${at.phase}); it will finish or restore on its own.`;
784
- if (!CANCELLABLE.includes(j.phase))
793
+ if (!CANCELLABLE.some(phase => phase === j.phase))
785
794
  return tooLate(j);
786
795
  // No updater is running to see the request: cancel here, so the admission pause does not outlive it.
787
796
  const lock = sqliteLock(join(j.stageDir, "worker.sqlite"));
@@ -797,7 +806,7 @@ export function requestRuntimeUpdateCancel(stateDir, now = new Date(), seams = {
797
806
  return "No update is in progress.";
798
807
  if (held.id !== j.id)
799
808
  return "A different update was saved while cancelling. Nothing was cancelled.";
800
- if (!CANCELLABLE.includes(held.phase))
809
+ if (!CANCELLABLE.some(phase => phase === held.phase))
801
810
  return tooLate(held);
802
811
  durableJson(cancelFile(held), { id: held.id, action: "cancel" });
803
812
  try {
@@ -840,7 +849,7 @@ async function driveRuntimeUpdate(j, system) {
840
849
  }
841
850
  // The lock now says an updater is here; the starting mark has done its job.
842
851
  rmSync(startingFile(j), { force: true });
843
- const at = (phase) => UPDATE_STEPS.indexOf(phase);
852
+ const at = (phase) => UPDATE_STEPS.findIndex(step => step === phase);
844
853
  const step = (phase, detail) => { save(j, phase, detail, system.now()); system.checkpoint?.(phase); };
845
854
  const noun = j.kind === "update" ? "update" : "rollback";
846
855
  const verb = j.kind === "update" ? `${j.from.version} → ${j.to.version}` : `${j.from.version} → ${j.to.version} (back)`;
@@ -1137,12 +1146,13 @@ async function drain(j, system) {
1137
1146
  * the rollback-* records are not this updater's to remove. */
1138
1147
  export function pruneRuntimes(stateDir, keep) {
1139
1148
  const root = join(stateDir, "staged-upgrades");
1149
+ // A partial or older stage file still says when it started; one that is not JSON (or is null) falls back to the mtime.
1140
1150
  const started = (dir) => { try {
1141
- return String(JSON.parse(readFileSync(join(dir, "update.json"), "utf8")).startedAt ?? "");
1151
+ const at = stagedStartedAt(JSON.parse(readFileSync(join(dir, "update.json"), "utf8")));
1152
+ if (at !== null)
1153
+ return at;
1142
1154
  }
1143
- catch {
1144
- return statSync(dir).mtime.toISOString();
1145
- } };
1155
+ catch { /* below */ } return statSync(dir).mtime.toISOString(); };
1146
1156
  const releases = readdirSync(root, { withFileTypes: true }).filter(entry => entry.isDirectory() && entry.name.startsWith("release-"))
1147
1157
  .map(entry => join(root, entry.name)).sort((a, b) => started(b).localeCompare(started(a)));
1148
1158
  const kept = new Set(releases.filter(dir => keep.some(dist => dist !== "" && dist.startsWith(dir + sep))));
@@ -8,6 +8,8 @@ import { adjudicate } from "./proof.js";
8
8
  import { parseReviewContext, reviewContextCustodyProblem } from "./review-context.js";
9
9
  import { readVerifiedArtifact, storeEvidence, scanForSecrets, redactSecretLines } from "./evidence.js";
10
10
  import { readVerificationReceipt } from "./contracts/verification-receipt.js";
11
+ import { LEGACY_GATE_SOURCE, readVerificationView } from "./contracts/verification-view.js";
12
+ import { readCandidateEndpoints, savedInventoryPaths } from "./contracts/diff-stat.js";
11
13
  import { liveQuickCommand, quickVerifyKey, runCheckLevel } from "./check-levels.js";
12
14
  export const VERIFICATION_RECEIPT_CAPTURE = "machine verification receipt v1";
13
15
  export const REVIEW_GATE_NAME = "REVIEW-VERIFICATION.json";
@@ -97,7 +99,10 @@ export function verificationEvidence(store, root, runId) {
97
99
  const original = verificationEvidence(store, root, reused.run);
98
100
  if (!original.ok || !original.bytes || original.digest !== reused.digest)
99
101
  return fail("The original passing gate no longer verifies.");
100
- const previous = JSON.parse(original.bytes);
102
+ const previousRead = readVerificationView(original.bytes);
103
+ if (!previousRead.ok)
104
+ return fail("The original passing gate no longer verifies.");
105
+ const previous = previousRead.value;
101
106
  const originalLog = store.getArtifact(previous.log.artifactId);
102
107
  if (previous.head !== source.headRevision || !("exitCode" in previous.result) || previous.result.exitCode !== 0 || JSON.stringify(previous.command) !== JSON.stringify(command) ||
103
108
  JSON.stringify(previous.result) !== JSON.stringify(receipt.result) || originalLog?.sha256 !== log.sha256 ||
@@ -123,13 +128,17 @@ export function verificationEvidence(store, root, runId) {
123
128
  const readStat = readVerifiedArtifact(root, stat[0]);
124
129
  if (!readStat.ok)
125
130
  return fail("Legacy candidate inventory no longer verifies.");
126
- let endpoints;
131
+ let inventory;
127
132
  try {
128
- endpoints = JSON.parse(readStat.content.toString("utf8"));
133
+ inventory = JSON.parse(readStat.content.toString("utf8"));
129
134
  }
130
135
  catch {
131
136
  return fail("Legacy candidate inventory is unreadable.");
132
137
  }
138
+ const endpointsRead = readCandidateEndpoints(inventory);
139
+ if (!endpointsRead.ok)
140
+ return fail("Legacy verification candidate endpoints do not match.");
141
+ const endpoints = endpointsRead.value;
133
142
  if (!source.headRevision || endpoints.head !== source.headRevision || endpoints.base !== source.baseRevision || endpoints.filesTruncated !== false)
134
143
  return fail("Legacy verification candidate endpoints do not match.");
135
144
  const text = read.content.toString("utf8");
@@ -139,7 +148,8 @@ export function verificationEvidence(store, root, runId) {
139
148
  return fail("The original log lacks an unambiguous machine gate receipt; historical output was not reconstructed.");
140
149
  if (verified && exitCode !== 0)
141
150
  return fail("The passing machine verdict disagrees with the original failed check.");
142
- receipt = { version: 1, source: "legacy machine log header", run: runId, head: source.headRevision, base: source.baseRevision, scopeDigest: source.scopeDigest, command, result: { configured: true, ran: true, exitCode }, log: binding(log), candidate: binding(stat[0]) };
151
+ const view = { version: 1, source: LEGACY_GATE_SOURCE, run: runId, head: source.headRevision, base: source.baseRevision, scopeDigest: source.scopeDigest, command, result: { configured: true, ran: true, exitCode }, log: binding(log), candidate: binding(stat[0]) };
152
+ receipt = view;
143
153
  }
144
154
  // The view preserves shortened/redacted status and does not claim the omitted
145
155
  // output exists. Its fingerprint is re-proved before every turn and ingestion.
@@ -173,8 +183,10 @@ export function failedVerificationEvidence(store, root, runId) {
173
183
  return { kind: "unavailable", problem: verified.problem };
174
184
  if (verified.bytes === null)
175
185
  return { kind: "none" };
176
- const receipt = JSON.parse(verified.bytes);
177
- const result = receipt.result;
186
+ const view = readVerificationView(verified.bytes);
187
+ if (!view.ok)
188
+ return { kind: "unavailable", problem: "The verification receipt cannot be read." };
189
+ const receipt = view.value, result = receipt.result;
178
190
  if (!result.configured || (result.ran ? result.exitCode === 0 :
179
191
  result.failure !== "timed-out" && result.failure !== "retry-timed-out"))
180
192
  return { kind: "none" };
@@ -223,15 +235,15 @@ export function assessmentFromSavedEvidence(store, root, runId) {
223
235
  if (!gate.ok || gate.bytes === null)
224
236
  return null;
225
237
  try {
226
- const inventory = JSON.parse(stat), receipt = JSON.parse(gate.bytes);
238
+ const view = readVerificationView(gate.bytes);
227
239
  const base = source.branch ? store.firstBuilderBase(source.taskRef, source.branch) ?? source.baseRevision : source.baseRevision;
228
- if (inventory.schema !== 1 || inventory.head !== source.headRevision || inventory.base !== base || inventory.filesTruncated !== false ||
229
- !Array.isArray(inventory.files) || inventory.fileCount !== inventory.files.length || !inventory.files.every((one) => typeof one?.path === "string") ||
230
- new Set(inventory.files.map((one) => one.path)).size !== inventory.files.length)
240
+ const paths = savedInventoryPaths(stat, source.headRevision, base);
241
+ if (!view.ok || paths === null)
231
242
  return null;
243
+ const receipt = view.value;
232
244
  return adjudicate({ directAssessment: true, proofArtifactPresent: false, proofParse: null,
233
245
  handoffPresent: true, terminalDiffPresent: true, terminalDiffCaptureStatus: "ok",
234
- diffStat: { captured: true, truncated: false, paths: new Set(inventory.files.map((one) => one.path)) },
246
+ diffStat: { captured: true, truncated: false, paths },
235
247
  verifyCommand: receipt.result, verificationCommand: receipt.command.command, screenshots: [],
236
248
  approvedCriteria: scope.acceptance, reviewContext: parsedContext.inventory.coverage });
237
249
  }
@@ -255,7 +267,10 @@ export function reuseObservationVerification(store, root, runId, now) {
255
267
  const original = verificationEvidence(store, root, brief.sourceRun);
256
268
  if (!original.ok || !original.bytes || original.digest !== brief.gateDigest)
257
269
  throw Error("The original passing gate is unavailable or changed.");
258
- const receipt = JSON.parse(original.bytes), log = store.getArtifact(receipt.log.artifactId);
270
+ const view = readVerificationView(original.bytes);
271
+ if (!view.ok)
272
+ throw Error("The original passing gate is unavailable or changed.");
273
+ const receipt = view.value, log = store.getArtifact(receipt.log.artifactId);
259
274
  if (!log || receipt.head !== brief.head || !("ran" in receipt.result) || receipt.result.ran !== true || receipt.result.exitCode !== 0)
260
275
  throw Error("The source gate did not pass this candidate.");
261
276
  const read = readVerifiedArtifact(root, log);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.41",
3
+ "version": "0.9.43",
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"