toolroll 0.9.51 → 0.9.53

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 (164) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +55 -80
  3. package/dist/agentconfig.d.ts +11 -36
  4. package/dist/agentconfig.js +3 -62
  5. package/dist/approval-policy.d.ts +1 -1
  6. package/dist/approval-policy.js +1 -1
  7. package/dist/backup.d.ts +8 -0
  8. package/dist/backup.js +10 -1
  9. package/dist/board.d.ts +3 -19
  10. package/dist/board.js +4 -42
  11. package/dist/browser/workspace.css +1 -1
  12. package/dist/browser/workspace.js +18 -18
  13. package/dist/browser-workspace.d.ts +3 -1
  14. package/dist/builder.d.ts +9 -40
  15. package/dist/builder.js +50 -257
  16. package/dist/chat-actions.js +3 -14
  17. package/dist/chat-controls.d.ts +0 -4
  18. package/dist/chat-controls.js +0 -1
  19. package/dist/chat-decide.js +1 -3
  20. package/dist/chat-delivery.d.ts +0 -2
  21. package/dist/chat-delivery.js +0 -1
  22. package/dist/chat-quiet.js +2 -2
  23. package/dist/chat-task-actions.d.ts +1 -1
  24. package/dist/chat-task-actions.js +0 -2
  25. package/dist/claim.d.ts +79 -244
  26. package/dist/claim.js +688 -1023
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +10 -2
  29. package/dist/contracts/cli.d.ts +0 -13
  30. package/dist/contracts/cli.js +0 -2
  31. package/dist/contracts/console-api.d.ts +5 -10
  32. package/dist/contracts/console-api.js +4 -10
  33. package/dist/contracts/flow.d.ts +50 -0
  34. package/dist/contracts/flow.js +26 -1
  35. package/dist/contracts/lead-tools.d.ts +0 -13
  36. package/dist/contracts/lead-tools.js +3 -3
  37. package/dist/contracts/store-json.d.ts +1 -25
  38. package/dist/contracts/store-json.js +3 -13
  39. package/dist/converse.d.ts +1 -11
  40. package/dist/converse.js +6 -38
  41. package/dist/coordinator.d.ts +0 -1
  42. package/dist/coordinator.js +3 -5
  43. package/dist/database-reclaim.d.ts +4 -0
  44. package/dist/database-reclaim.js +13 -0
  45. package/dist/demo.d.ts +1 -1
  46. package/dist/demo.js +17 -27
  47. package/dist/desktop-update-gate.js +0 -1
  48. package/dist/desktop-update.d.ts +1 -1
  49. package/dist/desktop-update.js +1 -3
  50. package/dist/dispatch.d.ts +1 -1
  51. package/dist/dispatch.js +1 -1
  52. package/dist/dispose.d.ts +2 -3
  53. package/dist/dispose.js +17 -92
  54. package/dist/envelope.js +0 -3
  55. package/dist/exec.d.ts +0 -64
  56. package/dist/exec.js +3 -356
  57. package/dist/exhaustion.d.ts +22 -76
  58. package/dist/exhaustion.js +14 -140
  59. package/dist/export.js +4 -4
  60. package/dist/flow-engine.js +1 -1
  61. package/dist/flow-schedule.d.ts +154 -0
  62. package/dist/flow-schedule.js +321 -0
  63. package/dist/flow-share.js +1 -1
  64. package/dist/flow-triggers.d.ts +6 -1
  65. package/dist/flow-triggers.js +65 -10
  66. package/dist/flows-cli.js +8 -4
  67. package/dist/flows-ui.js +2 -0
  68. package/dist/flows.js +1 -1
  69. package/dist/guides.js +9 -29
  70. package/dist/invoke.d.ts +1 -24
  71. package/dist/invoke.js +17 -259
  72. package/dist/lead-status.js +2 -4
  73. package/dist/lead-voice.js +1 -1
  74. package/dist/live-bus.d.ts +3 -3
  75. package/dist/live-bus.js +3 -3
  76. package/dist/mate-doors.d.ts +1 -2
  77. package/dist/mate-doors.js +12 -25
  78. package/dist/mate-tools.d.ts +2 -2
  79. package/dist/mate-tools.js +10 -22
  80. package/dist/modes.d.ts +6 -7
  81. package/dist/modes.js +7 -13
  82. package/dist/monitoring.js +32 -8
  83. package/dist/notification-delivery.d.ts +19 -0
  84. package/dist/notification-delivery.js +37 -0
  85. package/dist/operate-remote-arguments.js +2 -6
  86. package/dist/operate-remote.js +6 -7
  87. package/dist/operate.d.ts +0 -6
  88. package/dist/operate.js +283 -2378
  89. package/dist/phase-routing.d.ts +13 -23
  90. package/dist/phase-routing.js +13 -35
  91. package/dist/plan-auto.js +2 -2
  92. package/dist/planner-source.js +2 -1
  93. package/dist/policy.d.ts +1 -4
  94. package/dist/policy.js +0 -8
  95. package/dist/process-recovery-eligibility.js +2 -3
  96. package/dist/process-recovery-settlement.js +1 -0
  97. package/dist/project-delete.js +3 -15
  98. package/dist/proposal.d.ts +4 -24
  99. package/dist/proposal.js +3 -88
  100. package/dist/provider-auth.d.ts +3 -3
  101. package/dist/provider-auth.js +4 -6
  102. package/dist/provider.d.ts +11 -25
  103. package/dist/provider.js +4 -36
  104. package/dist/push.js +1 -1
  105. package/dist/recipe-ui.js +6 -6
  106. package/dist/recipes.d.ts +3 -3
  107. package/dist/recipes.js +27 -23
  108. package/dist/retention.js +6 -5
  109. package/dist/run-closeout.d.ts +1 -1
  110. package/dist/run-closeout.js +1 -4
  111. package/dist/scope.d.ts +11 -65
  112. package/dist/scope.js +17 -82
  113. package/dist/serve.js +16 -25
  114. package/dist/server/chat.js +7 -30
  115. package/dist/server/flows.js +11 -10
  116. package/dist/server/guards.js +0 -1
  117. package/dist/server/pages-assets.js +1 -1
  118. package/dist/server/pages.js +0 -2
  119. package/dist/server/people-tokens.js +0 -4
  120. package/dist/server/route-table.d.ts +1 -1
  121. package/dist/server/route-table.js +2 -17
  122. package/dist/server/settings.js +4 -9
  123. package/dist/server/shared.d.ts +29 -212
  124. package/dist/server/shared.js +73 -780
  125. package/dist/server/task-views.js +9 -76
  126. package/dist/server/tasks.d.ts +1 -50
  127. package/dist/server/tasks.js +36 -946
  128. package/dist/setup-guide.d.ts +2 -2
  129. package/dist/slack-chat.d.ts +0 -2
  130. package/dist/slack-chat.js +0 -1
  131. package/dist/store.d.ts +142 -1382
  132. package/dist/store.js +900 -4083
  133. package/dist/surface.js +10 -25
  134. package/dist/task-composer.d.ts +1 -1
  135. package/dist/task-composer.js +1 -1
  136. package/dist/task-control.d.ts +3 -10
  137. package/dist/task-control.js +2 -7
  138. package/dist/teammate-desk.js +1 -1
  139. package/dist/teammates-ui.js +1 -1
  140. package/dist/telegram-mate.d.ts +0 -3
  141. package/dist/telegram-mate.js +0 -1
  142. package/dist/templates.d.ts +11 -7
  143. package/dist/templates.js +12 -9
  144. package/dist/toolroll-update.d.ts +24 -3
  145. package/dist/toolroll-update.js +159 -16
  146. package/dist/webhooks.d.ts +10 -22
  147. package/dist/webhooks.js +43 -81
  148. package/dist/work-index.js +3 -7
  149. package/dist/workspace-revision.d.ts +28 -1
  150. package/dist/workspace-revision.js +276 -49
  151. package/dist/workspace-ui.js +1 -1
  152. package/package.json +1 -3
  153. package/scripts/recovery-canary.mjs +3 -3
  154. package/dist/contest.d.ts +0 -318
  155. package/dist/contest.js +0 -753
  156. package/dist/held.d.ts +0 -150
  157. package/dist/held.js +0 -799
  158. package/dist/liveness.d.ts +0 -30
  159. package/dist/liveness.js +0 -42
  160. package/dist/pricing.d.ts +0 -45
  161. package/dist/pricing.js +0 -77
  162. package/dist/routine.d.ts +0 -228
  163. package/dist/routine.js +0 -764
  164. package/dist/supervisor.mjs +0 -297
@@ -227,6 +227,7 @@ export type BrowserSettingsView = {
227
227
  mode: 'subscription' | 'api-key';
228
228
  set: boolean;
229
229
  }[] | null;
230
+ legacyWebhookWarning?: string;
230
231
  services: {
231
232
  configured: string[];
232
233
  channel: string | null;
@@ -739,7 +740,6 @@ export type BrowserResultView = {
739
740
  } | null;
740
741
  noRun: string | null;
741
742
  panel: BrowserResultPanel | null;
742
- contest: string;
743
743
  notes: {
744
744
  author: string;
745
745
  at: string;
@@ -1015,6 +1015,8 @@ export type BrowserFlowTrigger = {
1015
1015
  checkable: boolean;
1016
1016
  /** A button shared as a public form (its link works). */
1017
1017
  shared: boolean;
1018
+ /** A schedule's standing order (v115): the terms each run files, in words — shown on demand. */
1019
+ order?: string[];
1018
1020
  };
1019
1021
  /** A flow's canvas: zones, cards, and what this person may change. */
1020
1022
  export type BrowserFlowView = {
package/dist/builder.d.ts CHANGED
@@ -28,30 +28,7 @@ export declare function approvedCommandShell(command: string, platform?: NodeJS.
28
28
  * must not turn an ordinary product failure into environment recovery.
29
29
  */
30
30
  export declare function verificationExecutableMissing(result: ExecResult, platform?: NodeJS.Platform): boolean;
31
- /** The attended dispatch (Parity II Phase 2): the authorization is the
32
- * authority, the coordinator takes ownership at the spawn point, and the
33
- * builder returns `held` without settling. */
34
- export type AttendedDispatch = {
35
- authorization: import("./store.js").AttendedAuthorization;
36
- coordinator: import("./held.js").HeldSessionCoordinator;
37
- upIncarnation: string;
38
- socketDir: string;
39
- releaseWorktree: (path: string) => Promise<unknown>;
40
- dispose: {
41
- repo: string;
42
- origin: string;
43
- provider: string;
44
- model: string | null;
45
- policy?: import("./dispose.js").DisposePolicy;
46
- };
47
- starter?: import("./exec.js").HeldSessionStart extends never ? never : typeof import("./exec.js").startClaudeHeldSession;
48
- graceMs?: number;
49
- /** v28: the operator's session cap, enforced in the custody transaction. */
50
- maxHeldSessions?: number;
51
- onDisposed?: import("./held.js").HeldLaunchArgs["onDisposed"];
52
- };
53
31
  export type BuildRequest = {
54
- attended?: AttendedDispatch;
55
32
  taskId: string;
56
33
  taskRef: number;
57
34
  runner: string;
@@ -70,10 +47,6 @@ export type BuildRequest = {
70
47
  * back to the unauthenticated touch, exactly as before.
71
48
  */
72
49
  runnerToken?: string;
73
- /** A tournament contestant's OWN approved profile (v24): under the joint
74
- * race approval, this — not the scope's single snapshot — is what the
75
- * dispatch proof holds the invocation to. */
76
- contestProfile?: ExecutionProfile;
77
50
  /**
78
51
  * Real time, read repeatedly. `now` is one instant and a build is not: a
79
52
  * lease heartbeated with the timestamp the build started at is a lease that
@@ -91,8 +64,8 @@ export type BuildRequest = {
91
64
  * gateway refuses a paid call whose run is missing or already finished.
92
65
  */
93
66
  runId: number;
94
- /** Claude's native dollar cap for this attempt (tournaments): the
95
- * harness stops itself at this figure. Absent = uncapped, as today. */
67
+ /** Claude's native dollar cap for this attempt: the harness stops
68
+ * itself at this figure. Absent = uncapped. */
96
69
  maxBudgetUsd?: number;
97
70
  /** Fires with the provider's process-group id the moment it exists —
98
71
  * the worker-process ledger records it (v14). */
@@ -136,7 +109,7 @@ export type BuildRequest = {
136
109
  /**
137
110
  * What the agent handed over when it parked: the validated decision and the
138
111
  * evidence rows already written for it. The caller seals it with
139
- * `finalizeParkFenced` — nothing here has touched the claim or the task.
112
+ * `finalize` (a `park` ending) — nothing here has touched the claim or the task.
140
113
  */
141
114
  export type ParkPackage = {
142
115
  decision: ParsedDecision;
@@ -148,9 +121,6 @@ export type BuildResult = {
148
121
  committed: boolean;
149
122
  /** The agent said no-change and the tree proves it: done, nothing to publish. */
150
123
  noChange?: boolean;
151
- /** The session is HELD (attended road): the coordinator owns run,
152
- * lease, and worktree from here — the caller settles NOTHING. */
153
- held?: true;
154
124
  branch: string;
155
125
  summary: string;
156
126
  } | {
@@ -165,9 +135,9 @@ export type BuildResult = {
165
135
  };
166
136
  export type BuildRefusal = "skills-unavailable"
167
137
  /** Sprint 8: the organisation policy stops this provider, model or permission level. */
168
- | "policy" | "unapproved" | "scope-changed" | "stale-approval" | "mode-ended" | "stale-authorization" | "attended-only" | "session-cap" | "attended-held" | "attended-unsupported" | "run-held" | "spawn-failed" | "capability" | "no-claim" | "not-yours" | "not-leased" | "protected-branch" | "wrong-branch" | "moved-branch" | "fenced" | "agent" | "agent-reported" | "no-op"
138
+ | "policy" | "unapproved" | "scope-changed" | "stale-approval" | "mode-ended" | "capability" | "no-claim" | "not-yours" | "not-leased" | "protected-branch" | "wrong-branch" | "moved-branch" | "fenced" | "agent" | "agent-reported" | "no-op"
169
139
  /** Run 2085: the agent stopped before its handoff; its work was kept for the retry. */
170
- | "no-handoff" | "moved-head" | "timeout" | "git" | "commit-failure" | "malformed-decision" | "provider-init" | "setup" | "revision-brief" | "external" | "stopped" | "plan-revised" | "plan-revision-blocked" | "provider-unattested" | "provider-protocol" | "chain-credential" | "chain-custody" | "runner-custody" | "auth-mode" | "route-authority" | "containment";
140
+ | "no-handoff" | "moved-head" | "timeout" | "git" | "commit-failure" | "malformed-decision" | "provider-init" | "setup" | "revision-brief" | "external" | "stopped" | "plan-revised" | "plan-revision-blocked" | "provider-unattested" | "provider-protocol" | "runner-custody" | "auth-mode" | "route-authority" | "containment";
171
141
  /** Long enough for real work; short enough that a stuck build ends the same night. */
172
142
  export declare const DEFAULT_BUILD_TIMEOUT_MS: number;
173
143
  export declare const DEFAULT_MAX_TURNS = 40;
@@ -208,7 +178,7 @@ export declare const SETUP_ENV_DENYLIST: readonly string[];
208
178
  export declare function redactSecretText(text: string): string;
209
179
  /** The same blanking with no length cap, for a log that keeps its length (a flow script's output). */
210
180
  export declare function redactSecretAssignments(text: string): string;
211
- export declare function proveApprovedProfile(scope: Scope | null, contestProfile: ExecutionProfile | null, given: {
181
+ export declare function proveApprovedProfile(scope: Scope | null, given: {
212
182
  provider: string;
213
183
  model: string | undefined;
214
184
  maxTurns: number | undefined;
@@ -236,10 +206,9 @@ export declare function proveApprovedProfile(scope: Scope | null, contestProfile
236
206
  */
237
207
  export declare function build(store: Store, request: BuildRequest): Promise<BuildResult>;
238
208
  /**
239
- * Everything the post-provider settlement closes over (Parity II Phase 2,
240
- * v4 Q2 / v6 W8): an explicit record, so the held road's coordinator can
241
- * run THE SAME settlement the one-shot road runs — one state machine,
242
- * never a paraphrase. `fenced()` reads the pulse's live flag: settlement
209
+ * Everything the post-provider settlement closes over: an explicit record,
210
+ * so the prepared-candidate road runs THE SAME settlement the agent road
211
+ * runs — one state machine, never a paraphrase. `fenced()` reads the pulse's live flag: settlement
243
212
  * decisions are about NOW, not about the moment of capture.
244
213
  */
245
214
  /** The prepared candidate must be a commit this repository holds and must
package/dist/builder.js CHANGED
@@ -47,10 +47,10 @@ import { stopRequestedFor, stopWords, underStopWatch } from "./task-control.js";
47
47
  import { witnessedRunner } from "./process-custody.js";
48
48
  import { runWithIsolatedDatabase } from "./child-database.js";
49
49
  import { recordWorktreeProcess, saveWorkPatch } from "./worktree.js";
50
- import { approvalOf, digestOf, profileDigestOf, chainDigestOf, entryDigestOf, routeParityProblem, profileFromJson } from "./scope.js";
50
+ import { approvalOf, digestOf, profileDigestOf, routeParityProblem } from "./scope.js";
51
51
  import { legOf, routeDigestOf, routeFromJson } from "./phase-routing.js";
52
52
  import { execFileSync } from "node:child_process";
53
- import { currentClaim, finalizeRevisionFenced, heartbeat, SYNC_MAX_AGE_MS } from "./claim.js";
53
+ import { currentClaim, heartbeat, SYNC_MAX_AGE_MS, finalize } from "./claim.js";
54
54
  import { missingCapability } from "./dispatch.js";
55
55
  import { heartbeat as runnerHeartbeat } from "./runner.js";
56
56
  import { MARKER as LEASE_MARKER } from "./worktree.js";
@@ -173,7 +173,7 @@ export function redactSecretAssignments(text) {
173
173
  }
174
174
  /**
175
175
  * The last-mile profile proof (v24, foundations findings 6/17): given the
176
- * scope (or a contestant's own race-approved profile), verify the approval
176
+ * scope (or a profile pinned beside it), verify the approval
177
177
  * record and hold the invocation to EXACTLY the sealed terms. Returns the
178
178
  * effective parameters — the snapshot's values — so an unset request field
179
179
  * can never float, and refuses divergence in words.
@@ -214,13 +214,13 @@ function providerVersionOf(provider) {
214
214
  PROVIDER_VERSIONS.set(provider, version);
215
215
  return version;
216
216
  }
217
- export function proveApprovedProfile(scope, contestProfile, given) {
217
+ export function proveApprovedProfile(scope, given) {
218
218
  // The raw terms first (raw authority repair): a scope whose stored terms
219
219
  // do not read back exactly proves nothing — not the filtered reading.
220
- if (contestProfile === null && scope !== null && scope.termsProblem != null) {
220
+ if (scope !== null && scope.termsProblem != null) {
221
221
  return { ok: false, message: `the scope's stored terms cannot be read exactly (${scope.termsProblem}) — re-file the scope and approve it again (stale-approval)` };
222
222
  }
223
- const snapshot = contestProfile ?? scope?.approvedProfile ?? null;
223
+ const snapshot = scope?.approvedProfile ?? null;
224
224
  if (snapshot === null) {
225
225
  return {
226
226
  ok: false,
@@ -231,7 +231,7 @@ export function proveApprovedProfile(scope, contestProfile, given) {
231
231
  // the column is bookkeeping, the recomputation is the proof. Grandfathered
232
232
  // v1 approvals rederive without the profile (their signed bytes) and are
233
233
  // held to the snapshot pinned at migration.
234
- if (contestProfile === null && scope !== null) {
234
+ if (scope !== null) {
235
235
  const rederived = (scope.digestVersion ?? 1) >= 2
236
236
  ? digestOf({
237
237
  goal: scope.goal,
@@ -310,15 +310,8 @@ export function proveApprovedProfile(scope, contestProfile, given) {
310
310
  export async function build(store, request) {
311
311
  const { taskId, taskRef, runner, worktree, branch, now, permissionMode = "auto", skipPermissions = false, provider = "claude", model, maxTurns = DEFAULT_MAX_TURNS, timeoutMs = DEFAULT_BUILD_TIMEOUT_MS, agent, git = run, } = request;
312
312
  const scope = store.getScope(taskId);
313
- // The run row, for the chain-entry binding (E3d): a run created by the
314
- // cycle roads carries chain_cycle/chain_index/entry_digest/auth_mode, and
315
- // the dispatch proof below re-derives every one of them.
316
- const chainRun = store.getRun(request.runId);
317
313
  const approval = approvalOf(scope);
318
- const attended = request.attended !== undefined && request.attended.authorization.taskRef === taskRef
319
- ? request.attended
320
- : undefined;
321
- if (!approval.approved && attended === undefined) {
314
+ if (!approval.approved) {
322
315
  return approval.reason === "changed"
323
316
  ? {
324
317
  ok: false,
@@ -335,7 +328,7 @@ export async function build(store, request) {
335
328
  // finding 2): a mode-sealed approval re-proves its signature still
336
329
  // stands here too — the claim roads already refuse, and this covers a
337
330
  // custom driver calling build directly with a stale claim.
338
- if (approval.approved && attended === undefined && !store.modeApprovalLive(taskRef, request.now)) {
331
+ if (!store.modeApprovalLive(taskRef, request.now)) {
339
332
  return {
340
333
  ok: false,
341
334
  reason: "mode-ended",
@@ -347,111 +340,24 @@ export async function build(store, request) {
347
340
  // permissions, limits — with the approved digest REDERIVED from the live
348
341
  // fields plus the snapshot, never trusted as a column. The profile is
349
342
  // the authority: request fields left unset take its values; request
350
- // fields that DIVERGE refuse, typed, naming what moved. A contestant
351
- // proves against its own race-approved profile.
343
+ // fields that DIVERGE refuse, typed, naming what moved.
352
344
  let effective;
353
- if (attended !== undefined) {
354
- // The attended road: the authorization's PINNED profile is the authority
355
- // (ruling 12); the final byte-compare against these values happens in
356
- // the coordinator's proof transaction, at the actual HEAD.
357
- let pinnedJson = null;
358
- try {
359
- const terms = JSON.parse(attended.authorization.termsJson);
360
- pinnedJson = typeof terms.profileJson === "string" ? terms.profileJson : null;
361
- }
362
- catch {
363
- pinnedJson = null;
364
- }
365
- const pinned = profileFromJson(pinnedJson);
366
- if (pinned === null) {
367
- return { ok: false, reason: "stale-authorization", message: `${taskId}: the authorization's pinned profile cannot be rehydrated` };
368
- }
369
- effective = {
370
- model: pinned.model,
371
- maxTurns: pinned.provider === "claude" ? pinned.maxTurns : request.maxTurns,
372
- timeoutMs: pinned.timeoutSeconds * 1000,
373
- skipPermissions: profileWantsSkip(pinned),
374
- profile: pinned,
375
- };
376
- }
377
- else if (chainRun !== null && chainRun.chainCycle != null) {
378
- // THE CHAIN-ENTRY DISPATCH PROOF (E3d, review findings 5/6): a run bound
379
- // to a fallback-chain entry proves against the IMMUTABLE approved chain,
380
- // never the single-profile snapshot. Everything is RE-DERIVED here, none
381
- // of it trusted from the caller: the chain approval must still stand
382
- // (approvedChainOf proves digest freshness), the LIVE cycle must be this
383
- // run's cycle — same id, open, cursor at this run's index, this run as
384
- // its tail, digest matching the approved chain — and the run's pinned
385
- // entry digest + auth mode must equal the approved entry at that index.
386
- // Only then does the entry's WHOLE profile (and nothing else) run.
387
- const chain = store.approvedChainOf(taskId);
388
- if (chain === null) {
389
- return { ok: false, reason: "stale-approval", message: `${taskId}: the chain approval no longer stands — re-approve (stale-approval)` };
390
- }
391
- // The run must belong to THE TASK being built (finding 7 + verify R7):
392
- // a chain digest excludes scope text, so two tasks can share one. The
393
- // task_ref equality alone is not enough — the caller supplies BOTH
394
- // taskRef and taskId, so the pairing itself is re-derived from the
395
- // store: the ref row's own external id must name exactly the task whose
396
- // scope authorizes this build.
397
- const chainOwner = store.refForId(taskRef);
398
- if (chainRun.taskRef !== taskRef ||
399
- chainOwner === null ||
400
- chainOwner.backend !== "built-in" ||
401
- chainOwner.externalId !== taskId) {
402
- return { ok: false, reason: "stale-approval", message: `${taskId}: this run belongs to a different task than its dispatch claims (stale-approval)` };
403
- }
404
- const cycle = store.fallbackCycleFor(taskRef);
405
- if (cycle === null ||
406
- cycle.id !== chainRun.chainCycle ||
407
- cycle.state !== "open" ||
408
- cycle.cursor !== chainRun.chainIndex ||
409
- cycle.tailRun !== request.runId ||
410
- cycle.chainDigest !== chainDigestOf(chain)) {
411
- return { ok: false, reason: "stale-approval", message: `${taskId}: this run is not the live custody of its fallback cycle — nothing spends outside the cycle (stale-approval)` };
412
- }
413
- const entry = chain[chainRun.chainIndex ?? -1];
414
- if (entry === undefined || entryDigestOf(entry) !== chainRun.entryDigest || entry.authMode !== chainRun.authMode) {
415
- return { ok: false, reason: "stale-approval", message: `${taskId}: the run's pinned entry does not match the approved chain at its index (stale-approval)` };
416
- }
417
- const proof = proveApprovedProfile(scope, entry.profile, {
418
- provider,
419
- model: request.model,
420
- maxTurns: request.maxTurns,
421
- timeoutMs: request.timeoutMs,
422
- skipPermissions,
423
- });
424
- if (!proof.ok) {
425
- return { ok: false, reason: "stale-approval", message: `${taskId}: ${proof.message}` };
426
- }
427
- effective = proof.effective;
428
- }
429
- else {
430
- // A CHAIN approval dispatches ONLY through its cycle (finding 6): an
431
- // ordinary (non-contest) run on a chain scope that carries no cycle
432
- // binding must never fall through to the single-profile proof — that
433
- // proof cannot verify a chain digest, and a run outside the cycle would
434
- // spend outside its custody.
435
- if (request.contestProfile === undefined && scope?.approvalKind === "chain") {
436
- return { ok: false, reason: "stale-approval", message: `${taskId}: a chain approval dispatches only through its fallback cycle — this run carries no cycle binding (stale-approval)` };
437
- }
438
- const proof = proveApprovedProfile(scope, request.contestProfile ?? null, {
439
- provider,
440
- model: request.model,
441
- maxTurns: request.maxTurns,
442
- timeoutMs: request.timeoutMs,
443
- skipPermissions,
444
- });
445
- if (!proof.ok) {
446
- return { ok: false, reason: "stale-approval", message: `${taskId}: ${proof.message}` };
447
- }
448
- effective = proof.effective;
345
+ const dispatchProof = proveApprovedProfile(scope, {
346
+ provider,
347
+ model: request.model,
348
+ maxTurns: request.maxTurns,
349
+ timeoutMs: request.timeoutMs,
350
+ skipPermissions,
351
+ });
352
+ if (!dispatchProof.ok) {
353
+ return { ok: false, reason: "stale-approval", message: `${taskId}: ${dispatchProof.message}` };
449
354
  }
355
+ effective = dispatchProof.effective;
450
356
  // The dispatch stamps (finding 21's order): written the moment the proof
451
357
  // passes, before anything provider-shaped happens — the run row then says
452
358
  // exactly which sealed terms this invocation was held to, and warm
453
359
  // resume below can match on them honestly.
454
- const provenScopeDigest = request.contestProfile !== undefined || attended !== undefined ? (scope?.digest ?? "") : (scope?.approvedDigest ?? "");
360
+ const provenScopeDigest = scope?.approvedDigest ?? "";
455
361
  const provenProfileDigest = profileDigestOf(effective.profile);
456
362
  store.stampRun(request.runId, {
457
363
  scopeDigest: provenScopeDigest,
@@ -461,8 +367,7 @@ export async function build(store, request) {
461
367
  : {}),
462
368
  });
463
369
  // ROUTE PROVENANCE (v47), written once at admission: every run — a
464
- // routed dispatch, a fallback admission, a contest lane, an attended
465
- // session, a pre-routing row's build — was stamped inside its own
370
+ // routed dispatch, a pre-routing row's build — was stamped inside its own
466
371
  // insert (v48 integrity: there is no late stamp). What is about to
467
372
  // spend must BE that stamp — the same provider and exact model — or
468
373
  // execution is refused before any spawn; a run that carries none was
@@ -470,7 +375,7 @@ export async function build(store, request) {
470
375
  // it.
471
376
  {
472
377
  const existing = store.runRoute(request.runId);
473
- const sealed = request.contestProfile !== undefined || attended !== undefined ? null : store.sealedRouteOf(taskId);
378
+ const sealed = store.sealedRouteOf(taskId);
474
379
  if (existing === null) {
475
380
  return {
476
381
  ok: false,
@@ -506,14 +411,10 @@ export async function build(store, request) {
506
411
  };
507
412
  }
508
413
  }
509
- // THE ORGANISATION POLICY (sprint 8), the last look before money on every build road — the tick, a fallback entry,
510
- // a race lane, an attended session: a provider or model it doesn't allow never spawns; terms above its permission
511
- // ceiling run lowered (unattended work, said on the run) or, for an attended session the person signed at exactly
512
- // these terms, are refused. The sealed terms and their stamps are untouched: only what this invocation runs with.
414
+ // THE ORGANISATION POLICY (sprint 8), the last look before money on every build road — the tick, a
415
+ // direct build: a provider or model it doesn't allow never spawns; terms above its permission ceiling run lowered
416
+ // (said on the run). The sealed terms and their stamps are untouched: only what this invocation runs with.
513
417
  {
514
- const attendedRefused = attended === undefined ? null : store.attendedPolicyRefusal(effective.profile);
515
- if (attendedRefused !== null)
516
- return { ok: false, reason: "policy", message: `${taskId}: ${attendedRefused}` };
517
418
  const verdict = store.runPolicy(effective.profile);
518
419
  if (!verdict.ok)
519
420
  return { ok: false, reason: "policy", message: `${taskId}: ${verdict.message}` };
@@ -624,8 +525,6 @@ export async function build(store, request) {
624
525
  try {
625
526
  codingHandoff = readCodingHandoff(store, taskId);
626
527
  if (codingHandoff !== null) {
627
- if (request.attended !== undefined)
628
- throw Error("This saved coding result must use its prepared review task.");
629
528
  const head = await git(GIT, ["--no-optional-locks", "rev-parse", "HEAD"], { cwd: worktree });
630
529
  const actual = await git(GIT, ["--no-optional-locks", "symbolic-ref", "--short", "HEAD"], { cwd: worktree });
631
530
  if (head.code !== 0 || actual.code !== 0 || actual.stdout.trim() !== branch)
@@ -737,7 +636,7 @@ export async function build(store, request) {
737
636
  }
738
637
  return null;
739
638
  };
740
- if (preparedCandidate === null || attended !== undefined) {
639
+ if (preparedCandidate === null) {
741
640
  const setupFailure = await runApprovedSetup();
742
641
  if (setupFailure !== null)
743
642
  return setupFailure;
@@ -1161,13 +1060,6 @@ export async function build(store, request) {
1161
1060
  milestones.length === 0 || planRevisionHash === null
1162
1061
  ? null
1163
1062
  : { revision: planRevisionNumber, hash: planRevisionHash, milestones, progress, proposal }, scope === null ? [] : flowGoalCuts(store, scope.taskId, scope.goal)) + `\nCanonical signed rubric: ${rubric}. Its statement fields are exact; evidence requirements are separate fields. Do not edit this input. The lead or user reads these criteria directly; no restatement is needed.\n` + sharedDepsBrief(store, worktree);
1164
- // THE HELD BRANCH (Phase 2, v2 S0d + v6 W8): ownership transfers to the
1165
- // coordinator at the spawn point. Everything build() armed that its
1166
- // finally would have cleared is torn down or handed over HERE — the pulse
1167
- // interval dies (the coordinator heartbeats from now on; no doubled
1168
- // writers) and the live-log handle rides the capture (the live window
1169
- // stays streaming across the whole hold). build() returns WITHOUT
1170
- // settling: the run, the lease, and the worktree are the coordinator's.
1171
1063
  // THE PREPARED-CANDIDATE ROAD (v69): the scope names a commit, proved
1172
1064
  // above before anything moved. The machine brings the worktree to its exact
1173
1065
  // tree — uncommitted, as an agent would leave it — writes the handoff
@@ -1176,7 +1068,7 @@ export async function build(store, request) {
1176
1068
  // gate. No provider is spawned; the lease heartbeat keeps
1177
1069
  // running until settlement returns, exactly as it does around a provider.
1178
1070
  const prepared = scope?.candidate ?? null;
1179
- if (prepared !== null && attended === undefined) {
1071
+ if (prepared !== null) {
1180
1072
  try {
1181
1073
  if (stopRequestedFor(store, request.runId, request.shouldStop))
1182
1074
  return { ok: false, reason: "stopped", message: stopWords(store, request.runId, worktree, "The attempt was stopped before the prepared candidate was loaded.") };
@@ -1252,54 +1144,6 @@ export async function build(store, request) {
1252
1144
  liveLog?.close();
1253
1145
  }
1254
1146
  }
1255
- if (attended !== undefined) {
1256
- if (pulseTimer !== undefined)
1257
- clearInterval(pulseTimer);
1258
- // The follow-up is NEW INSTRUCTION inside the signed terms (v2 S3c):
1259
- // it rides the brief in an OPERATOR fence, after the scope text —
1260
- // operator speech, exactly like turns, never widening scope.
1261
- const followup = attended.authorization.followup;
1262
- const heldBrief = followup === null || followup === undefined
1263
- ? briefText
1264
- : `${briefText}\n\n=== OPERATOR FOLLOW-UP (this session continues finished attempt #${attended.authorization.parentRun ?? "?"}) ===\n${followup}\n=== END OPERATOR FOLLOW-UP ===`;
1265
- const captured = {
1266
- store, request, agent, git, worktree, branch, baseRevision, taskId, taskRef,
1267
- runner, provider, scope, effective, answers, timeoutMs, root, mailbox, done, proof, rubric,
1268
- clock, fenced: () => fencedMidBuild,
1269
- plan: { proposal, revision: planRevisionNumber, authority, progress: progressState },
1270
- };
1271
- const launched = await attended.coordinator.launch({
1272
- store,
1273
- captured,
1274
- authorization: attended.authorization,
1275
- runId: request.runId,
1276
- leaseId: request.leaseId ?? "unclaimed",
1277
- runner,
1278
- ...(request.runnerToken === undefined ? {} : { runnerToken: request.runnerToken }),
1279
- upIncarnation: attended.upIncarnation,
1280
- brief: heldBrief,
1281
- cwd: worktree,
1282
- socketDir: attended.socketDir,
1283
- releaseWorktree: attended.releaseWorktree,
1284
- liveLog,
1285
- omitEnv: AGENT_ENV_DENYLIST,
1286
- dispose: attended.dispose,
1287
- clock,
1288
- ...(attended.starter === undefined ? {} : { starter: attended.starter }),
1289
- ...(attended.graceMs === undefined ? {} : { graceMs: attended.graceMs }),
1290
- ...(attended.maxHeldSessions === undefined ? {} : { maxHeldSessions: attended.maxHeldSessions }),
1291
- ...(attended.onDisposed === undefined ? {} : { onDisposed: attended.onDisposed }),
1292
- });
1293
- if (!launched.ok) {
1294
- liveLog?.close();
1295
- return {
1296
- ok: false,
1297
- reason: launched.reason ?? "attended-only",
1298
- message: launched.message,
1299
- };
1300
- }
1301
- return { ok: true, held: true, committed: false, branch, summary: "the session is held — the operator is watching" };
1302
- }
1303
1147
  let invoked;
1304
1148
  try {
1305
1149
  invoked = await invokeAgent(store, request.runId,
@@ -1387,10 +1231,9 @@ export async function build(store, request) {
1387
1231
  }
1388
1232
  }
1389
1233
  /**
1390
- * Everything the post-provider settlement closes over (Parity II Phase 2,
1391
- * v4 Q2 / v6 W8): an explicit record, so the held road's coordinator can
1392
- * run THE SAME settlement the one-shot road runs — one state machine,
1393
- * never a paraphrase. `fenced()` reads the pulse's live flag: settlement
1234
+ * Everything the post-provider settlement closes over: an explicit record,
1235
+ * so the prepared-candidate road runs THE SAME settlement the agent road
1236
+ * runs — one state machine, never a paraphrase. `fenced()` reads the pulse's live flag: settlement
1394
1237
  * decisions are about NOW, not about the moment of capture.
1395
1238
  */
1396
1239
  /** The prepared candidate must be a commit this repository holds and must
@@ -1472,10 +1315,9 @@ function ingestProgress(store, state, now) {
1472
1315
  * The post-provider state machine, extracted verbatim from build(): the
1473
1316
  * timeout/init/agent classification, the synchronous fence re-proof, park
1474
1317
  * ingestion (with its repair turns), the branch and HEAD laws, handoff
1475
- * validation, evidence capture, and the commit. build() calls it inline —
1476
- * behavior byte-identical — and a held session's coordinator calls it
1477
- * when the stream reaches a terminal handoff. Only this pair of callers:
1478
- * a run completes through THIS function or not at all.
1318
+ * validation, evidence capture, and the commit. build() calls it for the
1319
+ * agent road and the prepared-candidate road: a run completes through THIS
1320
+ * function or not at all.
1479
1321
  */
1480
1322
  /** The handoff's route line (v47): the run's stamped provenance, or nothing
1481
1323
  * for a run that opened before routes existed. */
@@ -1565,7 +1407,7 @@ export async function settleProviderOutcome(captured, result) {
1565
1407
  // before anything commits: a park never commits — whatever work is in
1566
1408
  // progress stays in the worktree, preserved for the resume — and this
1567
1409
  // function only assembles the package. Sealing it against the lease is
1568
- // `finalizeParkFenced`, one transaction, in the caller's hands.
1410
+ // `finalize` (a `park` ending), one transaction, in the caller's hands.
1569
1411
  store.setRunPhase(request.runId, "validating-handoff");
1570
1412
  if (result.sessionId !== null) {
1571
1413
  store.stampRun(request.runId, { sessionId: result.sessionId });
@@ -1817,7 +1659,7 @@ export async function settleProviderOutcome(captured, result) {
1817
1659
  // result is that commit, sealed below against the task's original base.
1818
1660
  const made = finishesKeptWork
1819
1661
  ? await keptWorkResult(captured, baseRevision, handoff.conclusion)
1820
- : await commit(git, worktree, branch, taskId, scope, handoff.conclusion, request.attended === undefined ? scope?.candidate ?? null : null, words => store.addRunNote(request.runId, "Toolroll", words, clock()));
1662
+ : await commit(git, worktree, branch, taskId, scope, handoff.conclusion, scope?.candidate ?? null, words => store.addRunNote(request.runId, "Toolroll", words, clock()));
1821
1663
  if (made.ok && made.parked === undefined && made.committed) {
1822
1664
  const newHead = await git(GIT, ["--no-optional-locks", "rev-parse", "HEAD"], { cwd: worktree });
1823
1665
  if (newHead.code === 0) {
@@ -1984,8 +1826,8 @@ function settleRevisionProposal(captured, binding) {
1984
1826
  deliverable: store.refForId(taskRef)?.deliverable ?? "branch",
1985
1827
  };
1986
1828
  const classification = classifyRevisionAuthority(binding.authority, now);
1987
- const sealed = finalizeRevisionFenced(store, {
1988
- leaseId: request.leaseId,
1829
+ const sealed = finalize(store, request.leaseId, {
1830
+ kind: "revision",
1989
1831
  runId: request.runId,
1990
1832
  taskId,
1991
1833
  taskRef,
@@ -2567,8 +2409,8 @@ async function startsFromKeptWork(captured, baseRevision, pinnedBase) {
2567
2409
  }
2568
2410
  /** The accepted result of an attempt that finished kept work with nothing left to change. An exact prepared candidate must still match it. */
2569
2411
  async function keptWorkResult(captured, head, summary) {
2570
- const { git, worktree, branch, request, scope } = captured;
2571
- const candidate = request.attended === undefined ? scope?.candidate ?? null : null;
2412
+ const { git, worktree, branch, scope } = captured;
2413
+ const candidate = scope?.candidate ?? null;
2572
2414
  if (candidate !== null) {
2573
2415
  const exact = await git(GIT, ["--no-optional-locks", "diff", "--quiet", candidate, head, "--"], { cwd: worktree });
2574
2416
  if (exact.code !== 0)
@@ -2615,8 +2457,7 @@ async function resumeUnhandedWork(captured, sessionId) {
2615
2457
  const waiting = () => !existsSync(join(worktree, done)) &&
2616
2458
  !existsSync(join(worktree, mailbox)) &&
2617
2459
  (captured.plan === undefined || !existsSync(join(worktree, captured.plan.proposal)));
2618
- // A watched (attended) session has an operator; nothing resumes it headless.
2619
- if (request.attended !== undefined || !waiting() || !canResumeSession(captured, sessionId))
2460
+ if (!waiting() || !canResumeSession(captured, sessionId))
2620
2461
  return null;
2621
2462
  const changes = await unhandedChanges(git, worktree);
2622
2463
  if (changes === null || changes.length === 0)
@@ -2705,7 +2546,7 @@ async function keepUnhandedWork(captured, sessionId) {
2705
2546
  if (!saved.ok) {
2706
2547
  return { ok: false, reason: "no-handoff", message: `${NO_HANDOFF_WORDS} Its changes stay uncommitted in ${worktree}; ${saved.message}.` };
2707
2548
  }
2708
- if (request.attended === undefined && !canResumeSession(captured, sessionId) && scope !== null && !stopRequestedFor(store, request.runId, request.shouldStop)) {
2549
+ if (!canResumeSession(captured, sessionId) && scope !== null && !stopRequestedFor(store, request.runId, request.shouldStop)) {
2709
2550
  const made = await commit(git, worktree, branch, taskId, scope, WIP_COMMIT_WORDS);
2710
2551
  if (made.ok && "committed" in made && made.committed) {
2711
2552
  return { ok: false, reason: "no-handoff", message: `${NO_HANDOFF_WORDS} It was saved as a work-in-progress commit on ${branch}.` };
@@ -2811,13 +2652,10 @@ async function ingestPark(args) {
2811
2652
  // approval froze. The repair model comes from the sealed profile
2812
2653
  // ("inherit" = the build's exact model); a disagreement with the sealed
2813
2654
  // route, or an unreadable route on a routed row, refuses the repair in
2814
- // words rather than mending under an agent nobody approved. Provenance
2815
- // follows the parent: an approved fallback entry's repair stays
2816
- // `fallback`; a legacy parent's repair stays `legacy`. Admission (v48)
2817
- // then proves the stamp again inside the run's own transaction — a
2818
- // fallback repair against the approved chain entry's exact repair model
2819
- // under the parent's route digest, the chain binding inherited verbatim
2820
- // below — so no repair turn can open under a lineage nobody approved.
2655
+ // words rather than mending under an agent nobody approved. A legacy
2656
+ // parent's repair stays `legacy`. Admission (v48) then proves the stamp
2657
+ // again inside the run's own transaction, so no repair turn can open
2658
+ // under a lineage nobody approved.
2821
2659
  const repairModel = repairModelOf(args.profile, request);
2822
2660
  for (let turn = 0; turn < REPAIR_TURNS && (resumableRepair ? sessionId !== undefined : true); turn++) {
2823
2661
  // The lease is re-proved around every repair turn: extended going in,
@@ -2828,7 +2666,7 @@ async function ingestPark(args) {
2828
2666
  return { fenced: true };
2829
2667
  }
2830
2668
  // v105: a repair turn spends too — a budget used up since the build began stops further turns (billed to a key).
2831
- // It bills as the attempt it mends did (a pinned chain entry's key included).
2669
+ // It bills as the attempt it mends did.
2832
2670
  if (store.budgetGate(clock())({ ...store.budgetSubject(request.taskRef), agents: [{ provider: repairProvider, billing: store.runBilling(request.runId) ?? store.agentsFor([repairProvider])[0].billing }] }).over !== null)
2833
2671
  break;
2834
2672
  // Sprint 8: nor does a repair run on a provider or model the organisation policy doesn't allow.
@@ -2838,11 +2676,6 @@ async function ingestPark(args) {
2838
2676
  if (!admitted.ok)
2839
2677
  return admitted;
2840
2678
  const repairRun = admitted.runId;
2841
- // A repair turn inherits its parent's chain binding VERBATIM (Codex E3d
2842
- // review, finding 2) — inside its own admission (v48 authority repair), so the pinned
2843
- // entry, auth mode included, follows the custody from the first byte
2844
- // of the row and the mending turn spends under exactly the credential
2845
- // the operator approved for this entry.
2846
2679
  const spoken = await invokeAgent(store, repairRun, { provider: repairProvider, model: repairModel }, {
2847
2680
  phase: "repair",
2848
2681
  brief: resumableRepair
@@ -2936,8 +2769,8 @@ function admitProtocolRepair(store, request, profile, sessionId, clock) {
2936
2769
  const repairScope = store.getScope(request.taskId);
2937
2770
  const repairSealed = repairScope !== null && repairScope.routeEra != null ? store.sealedRouteOf(request.taskId) : null;
2938
2771
  const repairModel = repairModelOf(args.profile, request);
2939
- let repairChosen = parentRoute?.chosen === "fallback" ? "fallback" : "legacy";
2940
- if (repairSealed !== null && parentRoute?.chosen !== "fallback") {
2772
+ let repairChosen = "legacy";
2773
+ if (repairSealed !== null) {
2941
2774
  if (!repairSealed.ok) {
2942
2775
  return { ok: false, problems: [{ reason: "route-unreadable", message: `the repair cannot run: ${repairSealed.detail}` }] };
2943
2776
  }
@@ -2961,47 +2794,7 @@ function admitProtocolRepair(store, request, profile, sessionId, clock) {
2961
2794
  };
2962
2795
  let repairRun;
2963
2796
  try {
2964
- if (repairChosen === "fallback") {
2965
- // A repair turn under an approved FALLBACK entry is admitted by the
2966
- // one fallback road (v48 integrity): every fact the parent's
2967
- // binding states — cycle, index, digest, auth mode, provider, the
2968
- // exact repair model, the sealed-profile mirror — is presented
2969
- // and re-proved against the approved chain and the live cycle,
2970
- // with the parent as the live tail, before any row exists.
2971
- const parentRun = store.getRun(runId);
2972
- const chain = store.approvedChainOf(request.taskId);
2973
- const mirror = repairScope?.approvedProfile ?? null;
2974
- const entry = parentRun !== null && parentRun.chainIndex != null && chain !== null ? chain[parentRun.chainIndex] : undefined;
2975
- if (parentRun === null || parentRun.chainCycle == null || parentRun.chainIndex == null || parentRun.entryDigest == null || parentRun.authMode == null || chain === null || mirror === null || entry === undefined || repairModel === null) {
2976
- return { ok: false, problems: [{ reason: "route-unreadable", message: `the repair cannot run: run #${runId}'s fallback binding cannot be restated against the approved chain — nothing mends outside the cycle` }] };
2977
- }
2978
- const admitted = store.admitFallback({
2979
- kind: "repair",
2980
- parentRun: runId,
2981
- cycleId: parentRun.chainCycle,
2982
- expectCursor: parentRun.chainIndex,
2983
- expectTail: runId,
2984
- entryDigest: parentRun.entryDigest,
2985
- authMode: parentRun.authMode,
2986
- repairModel: entry.profile.repairModel === "inherit" ? entry.profile.model : entry.profile.repairModel,
2987
- approved: { chainDigest: chainDigestOf(chain), profile: mirror },
2988
- run: {
2989
- taskRef: request.taskRef,
2990
- leaseId: request.leaseId ?? "unclaimed",
2991
- runner: request.runner,
2992
- branch: request.branch,
2993
- worktree,
2994
- provider: repairProvider,
2995
- model: repairModel,
2996
- ...(resumableRepair && sessionId !== undefined ? { sessionId } : {}),
2997
- },
2998
- route: repairStamp,
2999
- }, clock());
3000
- if (!admitted.ok)
3001
- return { ok: false, problems: [{ reason: "route-mismatch", message: `the repair cannot run: ${admitted.problem}` }] };
3002
- repairRun = admitted.runId;
3003
- }
3004
- else {
2797
+ {
3005
2798
  // THE REPAIR ADMISSION (atomic authority closure): the turn mends
3006
2799
  // exactly this live build attempt under its own runner and lease —
3007
2800
  // proved in the store, value-shaped, zero rows on refusal.