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
@@ -0,0 +1,4 @@
1
+ import type { Database } from "./store.js";
2
+ /** Reclaim whole free pages on databases created without auto_vacuum. Call only after committing writes.
3
+ * A migration needs at least 1 MiB to justify the I/O; retention reclaims whenever more than 25% is free. */
4
+ export declare function reclaimDatabase(db: Database, reason: "migration" | "retention"): boolean;
@@ -0,0 +1,13 @@
1
+ /** Reclaim whole free pages on databases created without auto_vacuum. Call only after committing writes.
2
+ * A migration needs at least 1 MiB to justify the I/O; retention reclaims whenever more than 25% is free. */
3
+ export function reclaimDatabase(db, reason) {
4
+ const free = Number(db.prepare("PRAGMA freelist_count").get()["freelist_count"]);
5
+ const pages = Number(db.prepare("PRAGMA page_count").get()["page_count"]);
6
+ const size = Number(db.prepare("PRAGMA page_size").get()["page_size"]);
7
+ if (reason === "migration" ? free * size < 1024 * 1024 : free * 4 <= pages)
8
+ return false;
9
+ db.exec("VACUUM");
10
+ // VACUUM writes through WAL too: checkpoint so the main file (and its backups) shrinks now.
11
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
12
+ return true;
13
+ }
package/dist/demo.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * - The database is stamped `demo` BEFORE any row exists — an append-only
9
9
  * installation fact with no unset API. Every spending or external-effect
10
10
  * command (tick, watch, build, publish, reconcile, daemon, bridge,
11
- * outbox deliver, intake) fails closed on the stamp, so a kept sandbox
11
+ * intake) fails closed on the stamp, so a kept sandbox
12
12
  * can never be mistaken for real work by a worker pointed at it later.
13
13
  * The console banner is decoration; the fence is enforcement.
14
14
  * - The seeded history is SYNTHETIC and does not pretend otherwise: runs
package/dist/demo.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * - The database is stamped `demo` BEFORE any row exists — an append-only
9
9
  * installation fact with no unset API. Every spending or external-effect
10
10
  * command (tick, watch, build, publish, reconcile, daemon, bridge,
11
- * outbox deliver, intake) fails closed on the stamp, so a kept sandbox
11
+ * intake) fails closed on the stamp, so a kept sandbox
12
12
  * can never be mistaken for real work by a worker pointed at it later.
13
13
  * The console banner is decoration; the fence is enforcement.
14
14
  * - The seeded history is SYNTHETIC and does not pretend otherwise: runs
@@ -33,8 +33,8 @@ import { addApprover, propose, approve } from "./scope.js";
33
33
  import { legOf, routeDigestOf } from "./phase-routing.js";
34
34
  import { acquire, release } from "./claim.js";
35
35
  import { register } from "./runner.js";
36
- import { approveRoutine, fireRoutine } from "./routine.js";
37
- import { fileTaskProposal, fileRoutineProposal } from "./proposal.js";
36
+ import { createScheduledFlow } from "./flow-schedule.js";
37
+ import { fileTaskProposal } from "./proposal.js";
38
38
  import { storeEvidence, budgetedStatJson, imageDimensions } from "./evidence.js";
39
39
  import { parseProof, adjudicate } from "./proof.js";
40
40
  import { sealVerificationReceipt } from "./verification-evidence.js";
@@ -58,7 +58,7 @@ const DEMO_PROFILE = {
58
58
  function presentedRoute(store, taskRef, role, spend = { provider: "claude", model: null }) {
59
59
  // A task with no scope presents the bare word `legacy` for the exact
60
60
  // pair it spends as (atomic authority closure) — nothing opens unstamped.
61
- const authority = store.routeAuthorityFor(taskRef, role) ?? store.routeAuthorityFor(taskRef, role, null, spend);
61
+ const authority = store.routeAuthorityFor(taskRef, role) ?? store.routeAuthorityFor(taskRef, role, spend);
62
62
  if (authority === null || !authority.ok)
63
63
  throw new Error(`demo seed: ${authority === null ? "no task" : authority.problem}`);
64
64
  return { route: authority.stamp };
@@ -419,11 +419,11 @@ export function seedDemo(store, repos, evidenceRoot, now) {
419
419
  // --- needs-you: an approval waiting -----------------------------------
420
420
  const planned = task("rotate-log-format", "Rotate the request-log format to JSON lines", repos.web, "Switch the request logger to JSON lines so the collector stops parsing free text. Keep the human console formatter for local dev. Migrate the two dashboards that grep the old format.");
421
421
  const plannedRef = store.refFor("built-in", planned).id;
422
- // An explainable route (v47): the operator declared elevated risk and
422
+ // An explainable route (v47): the operator sized the change large and
423
423
  // overrode the reviewer, so the ceremony shows a recommended leg, a
424
424
  // strong-tier leg, and an overridden leg side by side, with the runner's
425
425
  // readiness per provider.
426
- store.editTaskRoute(plannedRef, { by: "demo", authenticate: () => ({ ok: true }), risk: "elevated" }, hoursAgo(3.5));
426
+ store.editTaskRoute(plannedRef, { by: "demo", authenticate: () => ({ ok: true }), size: { size: "large", risky: false } }, hoursAgo(3.5));
427
427
  store.editTaskRoute(plannedRef, { by: "demo", authenticate: () => ({ ok: true }), override: { phase: "review", provider: "claude", model: "opus" } }, hoursAgo(3.2));
428
428
  store.setPlanState(plannedRef, "drafted");
429
429
  const plannerRun = store.startRun({
@@ -948,28 +948,18 @@ export function seedDemo(store, repos, evidenceRoot, now) {
948
948
  store.cancelTask("design-tokens", hoursAgo(10), "the token package was superseded");
949
949
  const held = task("migrate-billing", "Migrate billing exports to the new vendor", repos.api);
950
950
  store.hold(store.refFor("built-in", held).id, "waiting on the vendor sandbox account", null, hoursAgo(12));
951
- // --- a standing order with a track record ------------------------------
952
- const routine = fileRoutineProposal(store, {
953
- name: "nightly-deps",
954
- repo: repos.api,
955
- goal: "Refresh the lockfile within existing ranges, run the suite, summarize anything notable.",
956
- outOfScope: "No major version bumps.",
957
- touches: [],
958
- acceptance: [
959
- { id: "c1", statement: "The full test suite passes against the refreshed lockfile.", how: null, evidence: ["check"] },
960
- ],
961
- requirements: [],
962
- schedule: "daily:03:30",
963
- costCeilingUsd: null,
964
- filedVia: "demo",
951
+ // --- a scheduled flow, waiting for someone to turn its schedule on -----
952
+ const nightly = createScheduledFlow(store, {
953
+ repo: realpathSync(repos.api), name: "Nightly dependency refresh", stem: "nightly-deps", schedule: "daily:03:30", by: "demo",
954
+ terms: {
955
+ goal: "Refresh the lockfile within existing ranges, run the suite, summarize anything notable.",
956
+ outOfScope: "No major version bumps.",
957
+ touches: [], requirements: [], budgetPerRunMicrousd: null, costCeilingUsd: null,
958
+ acceptance: [{ id: "c1", statement: "The full test suite passes against the refreshed lockfile.", how: null, evidence: ["check"] }],
959
+ },
965
960
  }, hoursAgo(70));
966
- if (!routine.ok)
967
- throw new Error(`seed routine: ${routine.reason}`);
968
- approveRoutine(store, routine.id, "demo", hoursAgo(69), routine.digest, token);
969
- // Two nightly slots since approval: one fires, the second records its
970
- // single-flight skip honestly (the first instance is still open).
971
- fireRoutine(store, routine.id, hoursAgo(45));
972
- fireRoutine(store, routine.id, hoursAgo(21));
961
+ if (!nightly.ok)
962
+ throw new Error(`seed scheduled flow: ${nightly.message}`);
973
963
  // --- the outer loop: an opened PR with observed checks ----------------
974
964
  const pub = store.createPublicationIntent({
975
965
  run: doneRun,
@@ -61,7 +61,6 @@ export function activeUpdateWork(db) {
61
61
  runs: count("SELECT count(*) n FROM run WHERE outcome IS NULL"),
62
62
  claims: count("SELECT count(*) n FROM claim WHERE released_at IS NULL"),
63
63
  conversations: count("SELECT count(*) n FROM mate_turn WHERE state IN ('queued','running')") + count("SELECT count(*) n FROM chat_turn WHERE state IN ('queued','running')"),
64
- sessions: count("SELECT count(*) n FROM held_session WHERE ended_at IS NULL"),
65
64
  stopping: count("SELECT count(*) n FROM run_stop WHERE settled_at IS NULL"),
66
65
  ...activeCodingUpdateWork(db),
67
66
  };
@@ -94,13 +94,13 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
94
94
  export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
95
95
  export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
96
96
  export declare function desktopUpdateStatus(stateDir: string): {
97
- error?: never;
98
97
  active: boolean;
99
98
  phase: string;
100
99
  detail: string;
101
100
  running: boolean;
102
101
  canResume: boolean;
103
102
  canCancel: boolean;
103
+ error?: never;
104
104
  automaticRecovery?: never;
105
105
  recoveryAttempts?: never;
106
106
  recoveryRepairs?: never;
@@ -295,8 +295,6 @@ export function plainLingering(fallbackRun, problem) {
295
295
  return { run, on: `Run #${run} finished, but Toolroll can't check whether its folder is still in use`, action: null };
296
296
  if (/may still be running|owned subprocess/.test(problem))
297
297
  return { run, on: `Run #${run} finished, but ${pid ? `its process ${pid}` : "one of its processes"} is still running`, action: null };
298
- if (/held supervisor/.test(problem))
299
- return { run, on: `Run #${run} is still shutting down`, action: null };
300
298
  if (/still open/.test(problem))
301
299
  return { run, on: `Run #${run} is still running`, action: null };
302
300
  return { run, on: `Run #${run} hasn't finished shutting down`, action: null };
@@ -844,7 +842,7 @@ export async function runDesktopUpdate(stateDir, hooks = {}) {
844
842
  if (j.waiting && now().getTime() - Date.parse(j.waiting.since) >= LINGERING_LIMIT_MS)
845
843
  stuck = lingering;
846
844
  else
847
- save(j, "draining", lingering ? `New work is paused. Waiting for run #${lingering.run}: ${lingeringWords(lingering)} No process is being killed.` : `Waiting for current work: ${active.runs} runs, ${active.claims} leases, ${active.conversations} chat requests, ${active.sessions} sessions, ${active.stopping} shutdowns, ${active.coding} coding sessions, ${active.codingDeliveries} unconfirmed messages. Nothing is being cancelled.`);
845
+ save(j, "draining", lingering ? `New work is paused. Waiting for run #${lingering.run}: ${lingeringWords(lingering)} No process is being killed.` : `Waiting for current work: ${active.runs} runs, ${active.claims} leases, ${active.conversations} chat requests, ${active.stopping} shutdowns, ${active.coding} coding sessions, ${active.codingDeliveries} unconfirmed messages. Nothing is being cancelled.`);
848
846
  }
849
847
  finally {
850
848
  checking.close();
@@ -90,7 +90,7 @@ export declare function taskReadinessBlocker(store: Store, taskRef: number, now:
90
90
  * mode still stand. */
91
91
  export declare function scopeApprovedForDispatch(store: Store, taskRef: number, now: Date): boolean;
92
92
  /** The phase the ordinary dispatch loop would choose before it resolves an
93
- * agent. Attended authorization is deliberately outside this ordinary path. */
93
+ * agent. */
94
94
  export declare function dispatchRoleFor(store: Store, taskRef: number, now: Date): "builder" | "planner" | "scout";
95
95
  /** The first requirement this task fails, in words, or null. */
96
96
  export declare function missingCapability(store: Store, taskRef: number, dispatchRepo: string | null, now: Date): string | null;
package/dist/dispatch.js CHANGED
@@ -85,7 +85,7 @@ export function scopeApprovedForDispatch(store, taskRef, now) {
85
85
  return row !== undefined;
86
86
  }
87
87
  /** The phase the ordinary dispatch loop would choose before it resolves an
88
- * agent. Attended authorization is deliberately outside this ordinary path. */
88
+ * agent. */
89
89
  export function dispatchRoleFor(store, taskRef, now) {
90
90
  const ref = store.refForId(taskRef);
91
91
  const approved = scopeApprovedForDispatch(store, taskRef, now);
package/dist/dispose.d.ts CHANGED
@@ -6,10 +6,9 @@ import { type ProofVerdict } from "./proof.js";
6
6
  * Which road is disposing. 'tick' = the unattended loop: full task
7
7
  * completion, strikes, quota, publication. 'standalone' = the one-off
8
8
  * `build` command: run records only — no task state, no strikes, no
9
- * publication (its historical shape). The held road reuses 'tick' —
10
- * an attended session is still the task's real attempt.
9
+ * publication (its historical shape).
11
10
  */
12
- export type DisposePolicy = "tick" | "standalone" | "continuation";
11
+ export type DisposePolicy = "tick" | "standalone";
13
12
  export type DisposeContext = {
14
13
  store: Store;
15
14
  policy: DisposePolicy;
package/dist/dispose.js CHANGED
@@ -5,16 +5,15 @@ import { originalTaskBase, focusedTestCommandSupported } from "./observations.js
5
5
  * operations that actually END an attempt — sealing parks, accepting
6
6
  * completions behind the completion fence, recording failures with their
7
7
  * strikes and holds, opening publication intents — extracted from tick's
8
- * and the standalone build command's finalizers into ONE place, so the
9
- * held-session coordinator can end a run through exactly the machinery
10
- * every other road uses. The two historical roads differ deliberately
8
+ * and the standalone build command's finalizers into ONE place. The two
9
+ * historical roads differ deliberately
11
10
  * (the standalone command completes no task, strikes nothing, publishes
12
11
  * nothing), and the policy record says so explicitly instead of forking
13
12
  * the logic: behavior on both is byte-identical to what the callers
14
13
  * inlined before.
15
14
  */
16
15
  import { createHash } from "node:crypto";
17
- import { completeFenced, finalizeFailureFenced, finalizeHandBackFenced, finalizeMalformedFenced, finalizeParkFenced, interruptIfStopped, release, } from "./claim.js";
16
+ import { interruptIfStopped, release, finalize, } from "./claim.js";
18
17
  import { bodyHashOf, publicationBody } from "./publish.js";
19
18
  import { modeTermsFromJson } from "./modes.js";
20
19
  import { evidenceRoot, readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
@@ -101,8 +100,8 @@ function disposeBuildOutcomeLocked(context, result) {
101
100
  if (result.ok && result.parked !== undefined) {
102
101
  if (leaseId === undefined)
103
102
  return { kind: "park-fenced" };
104
- const sealed = finalizeParkFenced(store, {
105
- leaseId,
103
+ const sealed = finalize(store, leaseId, {
104
+ kind: "park",
106
105
  runId,
107
106
  taskId,
108
107
  decision: result.parked.decision,
@@ -118,57 +117,6 @@ function disposeBuildOutcomeLocked(context, result) {
118
117
  return { kind: "parked", decisionId: sealed.decisionId };
119
118
  }
120
119
  if (result.ok) {
121
- if (policy === "continuation") {
122
- // The taskless success (v4 Q7 + v5 P5): run finished, claim released,
123
- // publication intent under the SAME completion latch the ordinary
124
- // road holds — and the parent task untouched in state, strikes,
125
- // holds, and derived stats. All one transaction.
126
- const sealed = store.transact(() => {
127
- const latchOpen = store.mirrorAllowsCompletion(taskId);
128
- store.finishRun(runId, {
129
- outcome: result.noChange === true ? "no-change" : "built",
130
- ...(result.noChange === true ? { reason: "handoff" } : {}),
131
- committed: result.committed,
132
- now: clock(),
133
- });
134
- if (leaseId !== undefined)
135
- release(store, leaseId, clock());
136
- if (!latchOpen)
137
- return { disowned: true };
138
- if (result.noChange !== true && result.committed) {
139
- const grant = store.publicationGrantFor(repo);
140
- const headSha = store.getRun(runId)?.headRevision ?? null;
141
- if (grant !== null &&
142
- grant.publishOn !== "complete" &&
143
- headSha !== null &&
144
- headWithin(branch, grant.headPrefix) &&
145
- (grant.selector === "all" || origin === "ours")) {
146
- const intentId = store.createPublicationIntent({
147
- run: runId,
148
- taskRef,
149
- githubRepo: grant.githubRepo,
150
- remote: grant.remote,
151
- base: grant.base,
152
- head: branch,
153
- headSha,
154
- bodyHash: "",
155
- draft: grant.draft,
156
- }, clock());
157
- const publication = store.publicationForRun(runId);
158
- if (publication !== null) {
159
- store.handle
160
- .prepare("UPDATE publication SET body_hash = ? WHERE id = ?")
161
- .run(bodyHashOf(publicationBody(store, publication)), intentId);
162
- }
163
- }
164
- }
165
- return { disowned: false };
166
- });
167
- store.clearQuota(runner, provider, model ?? "");
168
- if (sealed.disowned)
169
- return { kind: "disowned" };
170
- return { kind: "built", committed: result.committed, noChange: result.noChange === true };
171
- }
172
120
  if (policy === "standalone") {
173
121
  store.finishRun(runId, {
174
122
  outcome: result.noChange === true ? "no-change" : "built",
@@ -184,7 +132,7 @@ function disposeBuildOutcomeLocked(context, result) {
184
132
  // around the fenced release, the run's outcome, and the publication
185
133
  // intent, so "done" and "this must reach a PR" cannot come apart.
186
134
  const sealed = store.transact(() => {
187
- const fence = completeFenced(store, leaseId, "done", clock());
135
+ const fence = finalize(store, leaseId, { kind: "complete", state: "done", now: clock() });
188
136
  if (!fence.ok)
189
137
  return fence;
190
138
  // The disowned arm (external dispatch, v4 §24): the tracker closed
@@ -264,14 +212,14 @@ function disposeBuildOutcomeLocked(context, result) {
264
212
  // THE ALREADY-SEALED ENDINGS (adaptive execution plans). Every other arm
265
213
  // below ends the attempt itself — releases the claim, writes the run's
266
214
  // outcome, counts what it costs. These two arrive with all of that
267
- // already done: `finalizeRevisionFenced` released the lease, appended the
215
+ // already done: `finalize` (a `revision` ending) released the lease, appended the
268
216
  // ledger row, placed the hold when one was owed, finished the run, and
269
217
  // paged, all inside ONE fenced transaction — the planner road's shape,
270
218
  // where claim.ts owns the ending and dispose only reports it.
271
219
  //
272
220
  // So this branch exists to do NOTHING, deliberately, and it is placed
273
- // above the policy arms so that every road — tick, standalone, held,
274
- // continuation — skips them alike.
221
+ // above the policy arms so that every road — tick and standalone —
222
+ // skips them alike.
275
223
  //
276
224
  // Falling through instead would be wrong twice over. `release()` would
277
225
  // survive it (an already-released lease whose `released_by` is
@@ -284,20 +232,10 @@ function disposeBuildOutcomeLocked(context, result) {
284
232
  if (result.reason === "plan-revised" || result.reason === "plan-revision-blocked") {
285
233
  return { kind: "skipped", reason: result.reason };
286
234
  }
287
- if (policy === "continuation") {
288
- // The taskless failure (v4 Q7): the run says what happened, the claim
289
- // releases — NO strikes, NO holds, NO done→failed demotion; three
290
- // failed continuations still leave the parent exactly as it finished.
291
- const outcome = result.reason === "fenced" ? "refused" : "failed";
292
- store.finishRun(runId, { outcome, reason: result.reason, now: clock() });
293
- if (leaseId !== undefined)
294
- release(store, leaseId, clock());
295
- return { kind: "recorded", outcome };
296
- }
297
235
  if (policy === "standalone") {
298
236
  if (result.reason === "malformed-decision" && leaseId !== undefined) {
299
- const sealed = finalizeMalformedFenced(store, {
300
- leaseId,
237
+ const sealed = finalize(store, leaseId, {
238
+ kind: "malformed",
301
239
  runId,
302
240
  taskId,
303
241
  problems: result.problems ?? [],
@@ -309,19 +247,6 @@ function disposeBuildOutcomeLocked(context, result) {
309
247
  store.finishRun(runId, { outcome, reason: result.reason, now: clock() });
310
248
  return { kind: "recorded", outcome };
311
249
  }
312
- // The attended refusal family (v28 sweep): typed, no strike, release and
313
- // move on — the pre-claim gates make every one a rare race, and the
314
- // invariant arm they used to fall through is for BUGS, not races.
315
- if (result.reason === "attended-only" ||
316
- result.reason === "attended-held" ||
317
- result.reason === "stale-authorization" ||
318
- result.reason === "session-cap" ||
319
- result.reason === "run-held") {
320
- if (leaseId !== undefined)
321
- release(store, leaseId, clock());
322
- store.finishRun(runId, { outcome: "refused", reason: result.reason, now: clock() });
323
- return { kind: "skipped", reason: result.reason };
324
- }
325
250
  if (result.reason === "unapproved" || result.reason === "scope-changed") {
326
251
  // Approval drifted between the prefilter and the builder's own gate.
327
252
  if (leaseId !== undefined)
@@ -337,8 +262,8 @@ function disposeBuildOutcomeLocked(context, result) {
337
262
  if (result.reason === "malformed-decision") {
338
263
  if (leaseId === undefined)
339
264
  return { kind: "malformed", sealed: false };
340
- const sealed = finalizeMalformedFenced(store, {
341
- leaseId,
265
+ const sealed = finalize(store, leaseId, {
266
+ kind: "malformed",
342
267
  runId,
343
268
  taskId,
344
269
  problems: result.problems ?? [],
@@ -349,7 +274,7 @@ function disposeBuildOutcomeLocked(context, result) {
349
274
  // A stop no operator asked for is the SERVICE stopping (the fence above
350
275
  // already settled every task stop): hand the attempt back, never strike it.
351
276
  if (result.reason === "stopped" && policy === "tick" && leaseId !== undefined && store.getRun(runId)?.role === "builder") {
352
- const handed = finalizeHandBackFenced(store, { leaseId, runId, taskId, message: result.message, now: clock() });
277
+ const handed = finalize(store, leaseId, { kind: "hand-back", runId, taskId, message: result.message, now: clock() });
353
278
  return { kind: "handed-back", requeued: handed.requeued };
354
279
  }
355
280
  if (TICK_FAILURE_REASONS.has(result.reason)) {
@@ -376,8 +301,8 @@ function disposeBuildOutcomeLocked(context, result) {
376
301
  store.finishRun(runId, { outcome: "failed", reason: result.reason, now: clock() });
377
302
  return { kind: "failed", failureClass, disposition: null, strikes: null, sealed: false };
378
303
  }
379
- const sealed = finalizeFailureFenced(store, {
380
- leaseId,
304
+ const sealed = finalize(store, leaseId, {
305
+ kind: "failure",
381
306
  runId,
382
307
  taskId,
383
308
  failureClass,
@@ -531,7 +456,7 @@ export function regateTask(store, taskId, now, approval) {
531
456
  return store.transact(() => {
532
457
  const proposed = propose(store, {
533
458
  taskId, goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd,
534
- acceptance: scope.acceptance, qualityMode: scope.qualityMode ?? "default", riskLevel: scope.riskLevel ?? "routine",
459
+ acceptance: scope.acceptance, qualityMode: scope.qualityMode ?? "default",
535
460
  candidate: head, now,
536
461
  });
537
462
  let sealed = false;
package/dist/envelope.js CHANGED
@@ -55,12 +55,9 @@ export const DOCUMENTED_REASONS = [
55
55
  "unapproved", // no person has agreed to the scope yet
56
56
  "reserved", // the task belongs to another worker's queue
57
57
  "external", // a tracker mirror is not dispatchable (see detail)
58
- "contest-open", // a tournament is running; a person picks
59
- "all-lanes-raceable", // every compared agent could hold a real budget — race them instead
60
58
  "provider-unattested", // the provider binary is outside its attested range
61
59
  "daily-runs", // the active mode's daily run cap is spent
62
60
  "daily-dollars", // the active mode's daily measured-spend rail is reached
63
- "steering-pending", // a steering note is waiting; contests refuse
64
61
  "not-ready", // blocked, held, or otherwise not dispatchable
65
62
  "not-leased", // the lease does not exist or already ended
66
63
  "not-yours", // held or reserved by someone else
package/dist/exec.d.ts CHANGED
@@ -226,67 +226,3 @@ export declare function runStreamJsonl(file: string, args: readonly string[], op
226
226
  */
227
227
  export declare function runGeminiStreamJsonl(file: string, args: readonly string[], options?: RunOptions): Promise<ExecResult>;
228
228
  export declare function runClaudeStreamJsonl(file: string, args: readonly string[], options?: RunOptions): Promise<ExecResult>;
229
- /**
230
- * Where the supervisor script lives: a plain-.mjs sibling of this module in
231
- * BOTH layouts (src/ under tsx, dist/ after build — postbuild copies it).
232
- */
233
- export declare function supervisorPath(): string;
234
- /**
235
- * Darwin's sun_path is 104 bytes including the terminator; Linux allows a
236
- * hair more. Refusing at spawn beats failing inside bind() with a raw
237
- * ENAMETOOLONG after custody was already recorded.
238
- */
239
- export declare const HELD_SOCKET_PATH_LIMIT = 103;
240
- export declare function heldSocketPathProblem(path: string): string | null;
241
- export type HeldSessionEvents = {
242
- /** The nth system/init observed (1-based) — acceptance of the nth written turn. */
243
- onTurnInit?: (seq: number) => void;
244
- /** The nth primary-origin result (1-based) — settlement material for the nth turn. */
245
- onTurnResult?: (seq: number, event: Record<string, unknown>) => void;
246
- /** Every parsed stream event, arc-1 isolation rules unchanged. */
247
- onStreamEvent?: (event: Record<string, unknown>) => void;
248
- onSessionId?: (id: string) => void;
249
- /** The supervisor exited: the hold is over, however it ended. */
250
- onExit?: (info: {
251
- code: number | null;
252
- }) => void;
253
- };
254
- export type HeldSessionHandle = {
255
- supervisorPid: number | null;
256
- agentPgid: number | null;
257
- /** One turn as a stream-json line. False = the pipe is gone; nothing was written. */
258
- writeTurn(json: string): boolean;
259
- /** The EOF road: the supervisor fences (EOF → grace → group kill) on its own. */
260
- endInput(): void;
261
- /** SIGTERM the supervisor — the same fence, for expiry/revocation/lapse. */
262
- terminate(): void;
263
- /** Last resort only: SIGKILL the supervisor itself (can orphan the agent). */
264
- killHard(): void;
265
- exited: Promise<{
266
- code: number | null;
267
- }>;
268
- };
269
- export type HeldSessionStart = {
270
- ok: true;
271
- handle: HeldSessionHandle;
272
- } | {
273
- ok: false;
274
- reason: "socket-path" | "spawn-failed" | "ready-timeout";
275
- message: string;
276
- };
277
- /**
278
- * Hold one claude process open across stream-json turns, under a
279
- * supervisor whose parenthood makes the eventual kill provable. Returns
280
- * once the supervisor's control frame settles the spawn either way.
281
- *
282
- * No timers live here: the per-turn wall deadline, liveness lapse, and
283
- * absolute expiry are the coordinator's exact timers (v2 S1d) — this
284
- * transport only moves bytes and counts the protocol's own marks.
285
- */
286
- export declare function startClaudeHeldSession(file: string, args: readonly string[], options: RunOptions & {
287
- socketPath: string;
288
- cookie: string;
289
- graceMs?: number;
290
- events?: HeldSessionEvents;
291
- readyTimeoutMs?: number;
292
- }): Promise<HeldSessionStart>;