toolroll 0.9.93 → 0.9.94

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 (328) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/activity-line.d.ts +7 -4
  3. package/dist/activity-line.js +9 -7
  4. package/dist/agent-fence.d.ts +4 -0
  5. package/dist/agent-fence.js +4 -1
  6. package/dist/approval-authority.d.ts +7 -1
  7. package/dist/approval-authority.js +11 -2
  8. package/dist/approval-password.d.ts +31 -0
  9. package/dist/approval-password.js +26 -0
  10. package/dist/assignment-ui.js +1 -1
  11. package/dist/assignment.js +5 -0
  12. package/dist/boot-identity.d.ts +2 -1
  13. package/dist/boot-identity.js +6 -4
  14. package/dist/bounded-spawn.d.ts +57 -0
  15. package/dist/bounded-spawn.js +220 -0
  16. package/dist/browser/terminal.js +42 -42
  17. package/dist/browser/workspace.css +1 -1
  18. package/dist/browser/workspace.js +32 -32
  19. package/dist/browser-shell.d.ts +1 -1
  20. package/dist/browser-shell.js +2 -2
  21. package/dist/browser-workspace.d.ts +45 -2
  22. package/dist/browser-workspace.js +3 -3
  23. package/dist/builder-restart.d.ts +21 -0
  24. package/dist/builder-restart.js +50 -0
  25. package/dist/builder.d.ts +4 -0
  26. package/dist/builder.js +53 -24
  27. package/dist/chat-acceptance.js +1 -1
  28. package/dist/chat-actions.d.ts +12 -2
  29. package/dist/chat-actions.js +25 -2
  30. package/dist/chat-channel.js +11 -0
  31. package/dist/chat-decide.d.ts +7 -2
  32. package/dist/chat-decide.js +37 -9
  33. package/dist/chat-quiet.js +3 -3
  34. package/dist/chat-voice.d.ts +1 -1
  35. package/dist/chat-voice.js +2 -2
  36. package/dist/check-facts.d.ts +3 -1
  37. package/dist/check-facts.js +57 -11
  38. package/dist/check-failure.d.ts +22 -3
  39. package/dist/check-failure.js +57 -13
  40. package/dist/check-levels-ui.d.ts +4 -0
  41. package/dist/check-levels-ui.js +10 -1
  42. package/dist/check-load.d.ts +85 -0
  43. package/dist/check-load.js +147 -0
  44. package/dist/check-only-standing.d.ts +36 -0
  45. package/dist/check-only-standing.js +39 -0
  46. package/dist/check-only.d.ts +15 -0
  47. package/dist/check-only.js +99 -0
  48. package/dist/check-report.d.ts +2 -0
  49. package/dist/check-report.js +1 -1
  50. package/dist/check-stamp.d.ts +46 -0
  51. package/dist/check-stamp.js +93 -0
  52. package/dist/checkout-cleanup.d.ts +36 -5
  53. package/dist/checkout-cleanup.js +166 -14
  54. package/dist/cli-help.d.ts +4 -1
  55. package/dist/cli-help.js +47 -30
  56. package/dist/cli.js +11 -1
  57. package/dist/code-extract.d.ts +44 -0
  58. package/dist/code-extract.js +88 -0
  59. package/dist/code-map-acts.d.ts +24 -0
  60. package/dist/code-map-acts.js +53 -0
  61. package/dist/code-map-admission.d.ts +52 -0
  62. package/dist/code-map-admission.js +295 -0
  63. package/dist/code-map-build.d.ts +63 -0
  64. package/dist/code-map-build.js +415 -0
  65. package/dist/code-map-cli.d.ts +153 -0
  66. package/dist/code-map-cli.js +103 -0
  67. package/dist/code-map-main.d.ts +1 -0
  68. package/dist/code-map-main.js +48 -0
  69. package/dist/code-map-read.d.ts +106 -0
  70. package/dist/code-map-read.js +171 -0
  71. package/dist/code-map-report.d.ts +37 -0
  72. package/dist/code-map-report.js +95 -0
  73. package/dist/code-map-shape.d.ts +172 -0
  74. package/dist/code-map-shape.js +327 -0
  75. package/dist/code-map-store.d.ts +136 -0
  76. package/dist/code-map-store.js +233 -0
  77. package/dist/coding-provider.d.ts +2 -0
  78. package/dist/coding-provider.js +3 -2
  79. package/dist/coding-update.js +3 -2
  80. package/dist/coding-workspace.js +4 -4
  81. package/dist/console-chrome.js +2 -2
  82. package/dist/containment.d.ts +5 -0
  83. package/dist/containment.js +14 -1
  84. package/dist/contracts/chat-actions.d.ts +9 -0
  85. package/dist/contracts/chat-actions.js +2 -0
  86. package/dist/contracts/cli.d.ts +52 -0
  87. package/dist/contracts/cli.js +4 -0
  88. package/dist/contracts/console-api.d.ts +10 -3
  89. package/dist/contracts/console-api.js +10 -3
  90. package/dist/contracts/gateway-tools.d.ts +17 -0
  91. package/dist/contracts/gateway-tools.js +3 -1
  92. package/dist/contracts/lead-tools.d.ts +61 -0
  93. package/dist/contracts/lead-tools.js +10 -1
  94. package/dist/contracts/project-knowledge.d.ts +38 -5
  95. package/dist/contracts/project-knowledge.js +11 -5
  96. package/dist/contracts/project-memory.d.ts +3 -1
  97. package/dist/contracts/project-memory.js +3 -2
  98. package/dist/contracts/storage.d.ts +14 -0
  99. package/dist/contracts/storage.js +3 -1
  100. package/dist/contracts/store-json.d.ts +2 -0
  101. package/dist/contracts/youiyoux-request.d.ts +1 -1
  102. package/dist/contracts/youiyoux-request.js +1 -1
  103. package/dist/desktop-access.js +6 -2
  104. package/dist/dispatch.js +18 -15
  105. package/dist/dispose.js +8 -2
  106. package/dist/easy-start.js +2 -3
  107. package/dist/evals.js +2 -1
  108. package/dist/fast-start.d.ts +136 -0
  109. package/dist/fast-start.js +431 -0
  110. package/dist/fast-terms.d.ts +9 -0
  111. package/dist/fast-terms.js +23 -0
  112. package/dist/fenced-check-coverage.d.ts +62 -0
  113. package/dist/fenced-check-coverage.js +54 -0
  114. package/dist/first-run.js +1 -1
  115. package/dist/flaky-ci.d.ts +31 -0
  116. package/dist/flaky-ci.js +149 -0
  117. package/dist/flaky-cli.d.ts +60 -0
  118. package/dist/flaky-cli.js +60 -0
  119. package/dist/flaky.d.ts +75 -9
  120. package/dist/flaky.js +255 -0
  121. package/dist/flow-card-task.js +3 -0
  122. package/dist/flow-triggers.d.ts +1 -1
  123. package/dist/flow-triggers.js +1 -1
  124. package/dist/github-import.d.ts +62 -0
  125. package/dist/github-import.js +146 -0
  126. package/dist/guides.js +15 -10
  127. package/dist/invoke.d.ts +5 -1
  128. package/dist/invoke.js +49 -14
  129. package/dist/job-object-helper.ps1 +10 -3
  130. package/dist/keymap.d.ts +41 -0
  131. package/dist/keymap.js +51 -0
  132. package/dist/knowledge-cli.js +1 -1
  133. package/dist/knowledge-ui.d.ts +21 -4
  134. package/dist/knowledge-ui.js +75 -7
  135. package/dist/laptop-background.d.ts +16 -0
  136. package/dist/laptop-background.js +108 -2
  137. package/dist/lead-context.js +1 -1
  138. package/dist/lead-contract.d.ts +1 -1
  139. package/dist/lead-contract.js +2 -2
  140. package/dist/lead-progress.js +3 -0
  141. package/dist/lead-status.d.ts +2 -0
  142. package/dist/lead-status.js +6 -2
  143. package/dist/lead-tools.js +51 -2
  144. package/dist/lead.js +1 -1
  145. package/dist/lesson-notes.d.ts +81 -0
  146. package/dist/lesson-notes.js +336 -0
  147. package/dist/limits-ui.js +1 -1
  148. package/dist/local-owner.d.ts +1 -1
  149. package/dist/local-owner.js +11 -0
  150. package/dist/machine-check.d.ts +16 -0
  151. package/dist/machine-check.js +26 -0
  152. package/dist/machine-identity.d.ts +75 -0
  153. package/dist/machine-identity.js +170 -0
  154. package/dist/mcp-core.js +11 -1
  155. package/dist/memory-cli.d.ts +300 -1
  156. package/dist/memory-cli.js +62 -9
  157. package/dist/memory-pass.d.ts +16 -0
  158. package/dist/memory-pass.js +67 -3
  159. package/dist/memory-tidy.d.ts +181 -0
  160. package/dist/memory-tidy.js +523 -0
  161. package/dist/memory-use.d.ts +35 -0
  162. package/dist/memory-use.js +109 -0
  163. package/dist/merge-checklist.d.ts +55 -0
  164. package/dist/merge-checklist.js +182 -0
  165. package/dist/needs-you.d.ts +27 -8
  166. package/dist/needs-you.js +96 -39
  167. package/dist/nightly-github.d.ts +2 -0
  168. package/dist/nightly-github.js +4 -4
  169. package/dist/operate-remote-arguments.js +6 -1
  170. package/dist/operate-remote.js +3 -1
  171. package/dist/operate.d.ts +6 -1
  172. package/dist/operate.js +402 -52
  173. package/dist/phase-routing.js +2 -2
  174. package/dist/plain-schema.d.ts +23 -0
  175. package/dist/plain-schema.js +94 -0
  176. package/dist/plain-words.d.ts +20 -0
  177. package/dist/plain-words.js +66 -0
  178. package/dist/planner.js +2 -1
  179. package/dist/process-liveness.d.ts +2 -0
  180. package/dist/process-liveness.js +61 -17
  181. package/dist/project-delete-ui.d.ts +7 -2
  182. package/dist/project-delete-ui.js +12 -1
  183. package/dist/project-delete.js +8 -1
  184. package/dist/project-knowledge.d.ts +18 -11
  185. package/dist/project-knowledge.js +39 -9
  186. package/dist/project-learning.d.ts +2 -1
  187. package/dist/project-learning.js +15 -45
  188. package/dist/project-memory.d.ts +5 -0
  189. package/dist/project-memory.js +22 -2
  190. package/dist/proof.js +30 -5
  191. package/dist/provider-auth.js +1 -1
  192. package/dist/publish.js +9 -1
  193. package/dist/pull-request-flow.js +5 -0
  194. package/dist/push.d.ts +18 -2
  195. package/dist/push.js +40 -16
  196. package/dist/release-adapters.d.ts +9 -0
  197. package/dist/release-adapters.js +76 -0
  198. package/dist/release-auto.js +13 -13
  199. package/dist/release-card.d.ts +1 -2
  200. package/dist/release-card.js +11 -0
  201. package/dist/release-carry.d.ts +7 -2
  202. package/dist/release-carry.js +39 -9
  203. package/dist/release-cli.d.ts +4 -0
  204. package/dist/release-cli.js +29 -22
  205. package/dist/release-coverage.js +1 -1
  206. package/dist/release-next.d.ts +48 -0
  207. package/dist/release-next.js +161 -0
  208. package/dist/release.d.ts +20 -1
  209. package/dist/release.js +76 -8
  210. package/dist/remote-destination.d.ts +36 -0
  211. package/dist/remote-destination.js +217 -0
  212. package/dist/remote-main.d.ts +48 -0
  213. package/dist/remote-main.js +84 -5
  214. package/dist/repository-context.d.ts +3 -2
  215. package/dist/repository-context.js +112 -70
  216. package/dist/restart-certification.d.ts +1 -0
  217. package/dist/restart-certification.js +8 -6
  218. package/dist/result-acts.d.ts +27 -19
  219. package/dist/result-acts.js +36 -25
  220. package/dist/result-follow-ups.d.ts +2 -2
  221. package/dist/result-follow-ups.js +18 -4
  222. package/dist/result-review.d.ts +1 -1
  223. package/dist/route-change-check.d.ts +4 -0
  224. package/dist/route-change-check.js +13 -0
  225. package/dist/route-change.d.ts +22 -0
  226. package/dist/route-change.js +89 -0
  227. package/dist/route-hooks.js +3 -2
  228. package/dist/route-results.d.ts +6 -1
  229. package/dist/route-results.js +14 -5
  230. package/dist/run-closeout.d.ts +1 -1
  231. package/dist/run-closeout.js +7 -5
  232. package/dist/runner-process.js +3 -2
  233. package/dist/runner.js +2 -1
  234. package/dist/scope.d.ts +6 -1
  235. package/dist/scope.js +22 -9
  236. package/dist/serve.d.ts +1 -1
  237. package/dist/serve.js +24 -18
  238. package/dist/server/chat.js +14 -12
  239. package/dist/server/chrome.d.ts +1 -1
  240. package/dist/server/chrome.js +26 -35
  241. package/dist/server/console.css +23 -0
  242. package/dist/server/guards.js +3 -0
  243. package/dist/server/home.d.ts +6 -0
  244. package/dist/server/home.js +21 -13
  245. package/dist/server/http.js +1 -1
  246. package/dist/server/kit.js +1 -1
  247. package/dist/server/merge-ready-view.d.ts +10 -0
  248. package/dist/server/merge-ready-view.js +30 -0
  249. package/dist/server/pages-assets.d.ts +1 -0
  250. package/dist/server/pages-assets.js +8 -5
  251. package/dist/server/pages.js +103 -82
  252. package/dist/server/render-chat.js +2 -2
  253. package/dist/server/render-code-map.d.ts +30 -0
  254. package/dist/server/render-code-map.js +137 -0
  255. package/dist/server/render-flaky.d.ts +20 -0
  256. package/dist/server/render-flaky.js +59 -0
  257. package/dist/server/render-history.js +1 -1
  258. package/dist/server/render-new-task.d.ts +26 -1
  259. package/dist/server/render-new-task.js +30 -5
  260. package/dist/server/render-pages.d.ts +1 -175
  261. package/dist/server/render-pages.js +38 -313
  262. package/dist/server/render-projects.d.ts +2 -1
  263. package/dist/server/render-projects.js +9 -7
  264. package/dist/server/render-release.d.ts +2 -0
  265. package/dist/server/render-release.js +3 -3
  266. package/dist/server/render-results.d.ts +2 -0
  267. package/dist/server/render-results.js +67 -67
  268. package/dist/server/render-tasks.d.ts +13 -2
  269. package/dist/server/render-tasks.js +153 -136
  270. package/dist/server/retired-routes.d.ts +17 -0
  271. package/dist/server/retired-routes.js +53 -0
  272. package/dist/server/route-table.d.ts +3 -0
  273. package/dist/server/route-table.js +45 -12
  274. package/dist/server/settings.js +129 -22
  275. package/dist/server/task-views.js +61 -26
  276. package/dist/server/tasks.d.ts +2 -6
  277. package/dist/server/tasks.js +174 -321
  278. package/dist/server/welcome.js +1 -1
  279. package/dist/settings-groups-ui.js +1 -1
  280. package/dist/setup-guide.d.ts +2 -2
  281. package/dist/stop-proof.js +6 -4
  282. package/dist/storage-sweep-process.js +1 -1
  283. package/dist/storage-sweep.d.ts +5 -3
  284. package/dist/storage-sweep.js +30 -9
  285. package/dist/storage-ui.js +28 -17
  286. package/dist/storage.d.ts +27 -2
  287. package/dist/storage.js +39 -2
  288. package/dist/store.d.ts +80 -5
  289. package/dist/store.js +296 -87
  290. package/dist/surface.js +45 -4
  291. package/dist/sweep-quarantine.d.ts +1 -0
  292. package/dist/sweep-quarantine.js +6 -4
  293. package/dist/tamper-scan.d.ts +4 -0
  294. package/dist/tamper-scan.js +90 -0
  295. package/dist/task-aliases.d.ts +5 -0
  296. package/dist/task-aliases.js +9 -0
  297. package/dist/task-control.js +4 -1
  298. package/dist/task-outcome-cli.d.ts +2 -0
  299. package/dist/task-outcome-cli.js +17 -17
  300. package/dist/task-sizing.d.ts +5 -0
  301. package/dist/task-sizing.js +2 -0
  302. package/dist/task-source.d.ts +7 -0
  303. package/dist/task-source.js +14 -0
  304. package/dist/task-status.d.ts +9 -0
  305. package/dist/task-status.js +40 -49
  306. package/dist/teardown-git.d.ts +16 -0
  307. package/dist/teardown-git.js +63 -5
  308. package/dist/telegram-progress.js +5 -3
  309. package/dist/telegram-status.js +6 -4
  310. package/dist/test-infra.d.ts +8 -0
  311. package/dist/test-infra.js +18 -0
  312. package/dist/tidy.js +1 -1
  313. package/dist/toolroll-update.js +17 -5
  314. package/dist/tui-contract.d.ts +24 -0
  315. package/dist/tui-source.js +67 -8
  316. package/dist/tui.js +51 -51
  317. package/dist/vanished-retry.d.ts +30 -0
  318. package/dist/vanished-retry.js +174 -0
  319. package/dist/work-index.d.ts +2 -0
  320. package/dist/work-index.js +76 -62
  321. package/dist/work-summary.js +2 -0
  322. package/dist/workspace-ui.d.ts +4 -2
  323. package/dist/workspace-ui.js +28 -22
  324. package/dist/worktree.d.ts +12 -3
  325. package/dist/worktree.js +68 -13
  326. package/package.json +1 -1
  327. package/dist/server/render-next.d.ts +0 -63
  328. package/dist/server/render-next.js +0 -109
@@ -0,0 +1,136 @@
1
+ import { type ApprovalPasswordFacts } from "./approval-password.js";
2
+ import { type TaskSizing } from "./phase-routing.js";
3
+ import { type ApproveResult, type Scope } from "./scope.js";
4
+ import { type Store } from "./store.js";
5
+ /** The constants, in one place, versioned together in every marker. */
6
+ export declare const FAST_START_RULE = "small-v1";
7
+ export declare const FAST_WINDOW_MS: number;
8
+ export declare const FAST_MAX_FILES = 3;
9
+ export declare const FAST_MAX_LINES = 150;
10
+ /** How long a rendered form may still be pressed. */
11
+ export declare const FAST_PREVIEW_MS: number;
12
+ /** Which person surface is filing. A chat, a flow, a token or an agent has no entry here. */
13
+ export type FastVia = "console" | "terminal-ui" | "cli";
14
+ export type FastRefusal = "setting" | "size" | "classifier" | "plan" | "quality" | "rules" | "protected" | "password" | "release" | "trial" | "chain" | "monthly" | "policy" | "agents" | "machine" | "mode";
15
+ export declare const fastRefusalWords: (reason: FastRefusal) => string;
16
+ /** Small tasks start right away: a project's own choice, else on for single-approver installs and off once there's more
17
+ * than one approver. */
18
+ export declare function fastStartOn(store: Store, repo: string): boolean;
19
+ /** A person's logged settings act. */
20
+ export declare function setFastStart(store: Store, repo: string, on: boolean, by: string, now: Date): boolean;
21
+ /** Filed this way, the plan's words are the person's words, and they inspect the result. */
22
+ export declare const fastAcceptance: (title: string) => {
23
+ id: string;
24
+ statement: string;
25
+ how: null;
26
+ evidence: ["manual-review"];
27
+ }[];
28
+ export type FastInput = {
29
+ actor: string;
30
+ repo: string;
31
+ title: string;
32
+ permissionMode?: "auto" | "bypassPermissions" | undefined;
33
+ };
34
+ export type PasswordFacts = Pick<ApprovalPasswordFacts, "passwordless" | "localOwner" | "freshAt" | "returnTo">;
35
+ export declare function fastStartEligible(store: Store, input: FastInput & {
36
+ taskId: string;
37
+ sizing: TaskSizing;
38
+ password: PasswordFacts;
39
+ }, now: Date): {
40
+ ok: true;
41
+ scope: Scope;
42
+ } | {
43
+ ok: false;
44
+ reason: FastRefusal;
45
+ };
46
+ export type FastPreview = {
47
+ eligible: true;
48
+ preview: string;
49
+ digest: string;
50
+ scope: Scope;
51
+ taskId: string;
52
+ } | {
53
+ eligible: false;
54
+ reason: FastRefusal;
55
+ };
56
+ /**
57
+ * What the form renders: the classifier asked (at most its budget), then the same proposal the plan step would seal, in a
58
+ * transaction that's rolled back. The filing's words and the classifier's answer are kept (fast_preview), so Build
59
+ * files exactly this, whatever is posted.
60
+ */
61
+ export declare function prepareFastStart(store: Store, input: FastInput & {
62
+ via: FastVia;
63
+ password: PasswordFacts;
64
+ }, now: Date): Promise<FastPreview>;
65
+ export type FastBuild = {
66
+ ok: true;
67
+ taskId: string;
68
+ } | {
69
+ ok: false;
70
+ reason: "gone" | "changed" | FastRefusal;
71
+ message: string;
72
+ };
73
+ /** Approve inside the filing: `approveNow` is the surface's own call to approve() (web: its freshness and re-check). */
74
+ export type FastApprove = (taskId: string, sawDigest: string, fast: {
75
+ rule: string;
76
+ at: string;
77
+ }) => ApproveResult;
78
+ /**
79
+ * Build: file and approve in one transaction. Every check is made here, on the rows written here; anything refused
80
+ * rolls the filing back, so a refusal leaves nothing behind.
81
+ */
82
+ export declare function fastStartBuild(store: Store, input: {
83
+ preview: string;
84
+ digest: string;
85
+ actor: string;
86
+ via: FastVia;
87
+ password: PasswordFacts;
88
+ approveNow: FastApprove;
89
+ }, now: Date): FastBuild;
90
+ /**
91
+ * A fast start that didn't start in time: its seal is cleared, in a logged transaction, and the task shows the ordinary
92
+ * Approve plan. A task with a build running or a result to read is left to that.
93
+ */
94
+ export declare function expireFastStarts(store: Store, now: Date): number;
95
+ export type VerifiedChange = {
96
+ ok: true;
97
+ base: string;
98
+ head: string;
99
+ files: {
100
+ path: string;
101
+ added: number;
102
+ removed: number;
103
+ }[];
104
+ lines: number;
105
+ } | {
106
+ ok: false;
107
+ problem: string;
108
+ };
109
+ /**
110
+ * The whole change a result carries, verified: every version of the task (any ancestry problem refuses), from the base in
111
+ * Toolroll's own records to the head on the run record, every contributing build's saved diff present, and every file
112
+ * countable through the hardened git. Null-like answers refuse: nothing here is ever a guess.
113
+ */
114
+ export declare function verifiedChange(store: Store, taskId: string, runId: number): VerifiedChange;
115
+ /** Whether any version of this task started fast. An unreadable history counts: it fails closed below. */
116
+ export declare function fastStarted(store: Store, taskId: string): boolean;
117
+ /** The one sentence over every over-the-caps refusal; the road on is the approval of the expanded commit (keepGrownChange). */
118
+ export declare const GREW_WORDS = "This grew beyond a small change. Approve it to keep it.";
119
+ /** Why this exact result can't go further as a small change, or null. */
120
+ export declare function fastChangeProblem(store: Store, taskId: string, runId: number): string | null;
121
+ /** Completion: a person only, and only a change the measure accepts. */
122
+ export declare function fastCompletionProblem(store: Store, taskId: string, runId: number, person: string | null): string | null;
123
+ /** Publication and merge: nothing leaves until a person accepted this exact build, and the measure still accepts it. */
124
+ export declare function fastPublishProblem(store: Store, taskId: string, runId: number): string | null;
125
+ /**
126
+ * "Approve it to keep it": a plan for the actual expanded commit, proposed the way a rerun proposes one (dispose.ts
127
+ * regateTask): the same terms with `candidate` set to the exact head. It seals nothing; the person approves it on the
128
+ * ordinary ceremony, Toolroll checks that commit out with no agent, and only that attempt's result goes further.
129
+ */
130
+ export declare function keepGrownChange(store: Store, taskId: string, now: Date): {
131
+ ok: true;
132
+ digest: string;
133
+ } | {
134
+ ok: false;
135
+ message: string;
136
+ };
@@ -0,0 +1,431 @@
1
+ /**
2
+ * The fast path: file and approve in one step (docs/plans/plain-toolroll.md P2.4).
3
+ *
4
+ * For an eligible small task a person starts themselves, the New task form shows the exact terms a plan approval would
5
+ * seal, and pressing Build is that person's approval of exactly those terms: approve()'s own checks on the digest they
6
+ * saw, with the same proofs, inside the filing's transaction. Nothing is narrowed or widened. The seal is the ordinary
7
+ * person basis; the marker (the proof row's `fastStart`, `task_ref.fast_start_at`) only adds restrictions:
8
+ *
9
+ * - **the start window**: the first build starts within 30 minutes, or the seal is cleared in a logged transaction and
10
+ * the task shows the ordinary Approve plan (approval-authority.ts fastStartStandsSql, expireFastStarts);
11
+ * - **the Accept backstop**: a machine can't complete it, the project's grant doesn't push it until a person accepted it,
12
+ * and completion, publication and merge all read one verified measure of the whole change (verifiedChange).
13
+ *
14
+ * Only a person's own surfaces reach here (the browser, the terminal UI as the local owner, interactive `task new`).
15
+ * Every machine is refused in the writer from trusted context. `task add` never comes this way.
16
+ */
17
+ import { randomBytes } from "node:crypto";
18
+ import { approvalPasswordField } from "./approval-password.js";
19
+ import { isProtectedWork, protectedChanges } from "./approval-policy.js";
20
+ import { originalTaskBase } from "./observations.js";
21
+ import { legOf, makesNoPlan, routeFromJson } from "./phase-routing.js";
22
+ import { levelOfProfile } from "./policy.js";
23
+ import { fileTaskProposal } from "./proposal.js";
24
+ import { releaseCiHoldOf } from "./release-ci-hold.js";
25
+ import { COMPLETION_ACTION, COMPLETION_BY_PERSON } from "./result-completion.js";
26
+ import { deployGateScope, machineActing, propose, releaseCheckScope } from "./scope.js";
27
+ import { BUILT_IN } from "./store.js";
28
+ import { classifyTask, filingSizer, heuristicSizing } from "./task-sizing.js";
29
+ import { issueImported } from "./task-source.js";
30
+ import { titleWithin } from "./task-text.js";
31
+ import { viewedGitSync } from "./teardown-git.js";
32
+ /** The constants, in one place, versioned together in every marker. */
33
+ export const FAST_START_RULE = "small-v1";
34
+ export const FAST_WINDOW_MS = 30 * 60_000;
35
+ export const FAST_MAX_FILES = 3;
36
+ export const FAST_MAX_LINES = 150;
37
+ /** How long a rendered form may still be pressed. */
38
+ export const FAST_PREVIEW_MS = 15 * 60_000;
39
+ const FAST_WORDS = {
40
+ setting: "Small tasks don't start right away in this project.",
41
+ size: "This looks bigger than a small change.",
42
+ classifier: "Toolroll couldn't confirm this is a small change.",
43
+ plan: "This needs a plan first.",
44
+ quality: "Strict checks need a plan first.",
45
+ rules: "This project's approval rules need a plan first.",
46
+ protected: "This may reach protected work.",
47
+ password: "Approving this takes your password.",
48
+ release: "Releases need a plan first.",
49
+ trial: "A cheaper-model trial is approved on its own.",
50
+ chain: "A fallback chain needs a plan first.",
51
+ monthly: "A monthly limit is reached.",
52
+ policy: "Your organisation's policy needs a plan first.",
53
+ agents: "Choose an agent first.",
54
+ machine: "Only a person starts work this way.",
55
+ mode: "This project's mode approves it.",
56
+ };
57
+ export const fastRefusalWords = (reason) => FAST_WORDS[reason];
58
+ // ---- the project setting ---------------------------------------------------------------------------------------------
59
+ /** Small tasks start right away: a project's own choice, else on for single-approver installs and off once there's more
60
+ * than one approver. */
61
+ export function fastStartOn(store, repo) {
62
+ const saved = store.handle.prepare("SELECT fast_start FROM project WHERE path = ?").get(repo)?.["fast_start"];
63
+ if (saved === 0 || saved === 1)
64
+ return saved === 1;
65
+ const approvers = Number(store.handle.prepare("SELECT COUNT(*) AS n FROM approver WHERE revoked_at IS NULL AND role = 'approver'").get()?.["n"] ?? 0);
66
+ return approvers === 1;
67
+ }
68
+ /** A person's logged settings act. */
69
+ export function setFastStart(store, repo, on, by, now) {
70
+ return store.transact(() => {
71
+ const written = store.handle.prepare("UPDATE project SET fast_start = ? WHERE path = ?").run(on ? 1 : 0, repo);
72
+ if (Number(written.changes) === 0)
73
+ return false;
74
+ store.recordAction({ at: now.toISOString(), actor: by, repo, taskId: null, runId: null, action: "small tasks start right away", outcome: on ? "on" : "off", source: "policy", detail: null });
75
+ return true;
76
+ });
77
+ }
78
+ // ---- eligibility: one function, read by the form and again inside the transaction ------------------------------------
79
+ /** Filed this way, the plan's words are the person's words, and they inspect the result. */
80
+ export const fastAcceptance = (title) => [{ id: "c1", statement: title, how: null, evidence: ["manual-review"] }];
81
+ export function fastStartEligible(store, input, now) {
82
+ const no = (reason) => ({ ok: false, reason });
83
+ if (machineActing())
84
+ return no("machine");
85
+ if (!fastStartOn(store, input.repo))
86
+ return no("setting");
87
+ const account = store.accountOf(input.actor);
88
+ if (account === null || account.revokedAt !== null || account.role !== "approver" || !store.accountCanAccess(input.actor, input.repo))
89
+ return no("rules");
90
+ // The description decides "small"; the classifier can only confirm it, and silence is no.
91
+ if (heuristicSizing({ title: input.title, goal: input.title }).size !== "small")
92
+ return no("size");
93
+ if (input.sizing.source !== "classifier")
94
+ return no("classifier");
95
+ if (input.sizing.size !== "small" || input.sizing.risky)
96
+ return no("size");
97
+ const scope = store.getScope(input.taskId);
98
+ if (scope === null)
99
+ return no("plan");
100
+ if (issueImported(store, input.taskId))
101
+ return no("plan");
102
+ if (scope.profileState === "unresolved" || scope.profile == null)
103
+ return no("agents");
104
+ const route = routeFromJson(scope.proposedRouteJson ?? null);
105
+ if (route === null)
106
+ return no("agents");
107
+ if (!makesNoPlan(input.sizing, route.risk))
108
+ return no("plan");
109
+ if ((scope.qualityMode ?? "default") !== "default")
110
+ return no("quality");
111
+ if (scope.proposedChainJson != null)
112
+ return no("chain");
113
+ const rules = store.approvalRules(input.repo);
114
+ if (rules.notRequester)
115
+ return no("rules");
116
+ if (isProtectedWork(rules, scope.touches))
117
+ return no("protected");
118
+ const gate = store.approvalGate(input.taskId, input.actor, "person");
119
+ if (gate.verdict !== "seal" || gate.protectedWork)
120
+ return no("rules");
121
+ if (deployGateScope(scope) || releaseCheckScope(scope) || releaseCiHoldOf(store, input.taskId) !== null)
122
+ return no("release");
123
+ if (legOf(route, "build").selection?.kind === "trial")
124
+ return no("trial");
125
+ // Anything approving would take a typed password for: escalated terms, wider check access, sandbox widenings.
126
+ const profile = scope.profile;
127
+ if (levelOfProfile(scope.profile) === "escalated" || profile["checkAccess"] !== undefined || profile["machLookup"] !== undefined)
128
+ return no("password");
129
+ const field = approvalPasswordField(store, { name: input.actor, taskId: input.taskId, scope, ...input.password });
130
+ if (field.field === "required" || field.field === "local-refused")
131
+ return no("password");
132
+ const ref = store.lookupRef(input.taskId);
133
+ if (ref === null || store.budgetGate(now)({ ...store.budgetSubject(ref.id), agents: store.agentsFor([scope.profile.provider]) }).over !== null)
134
+ return no("monthly");
135
+ if (store.scopePolicyRefusal(input.taskId) !== null)
136
+ return no("policy");
137
+ return { ok: true, scope };
138
+ }
139
+ /** File the task, inside the caller's transaction, exactly as Build will. */
140
+ function fileIt(store, id, input, now) {
141
+ const filed = fileTaskProposal(store, {
142
+ id, title: titleWithin(input.title), repo: input.repo, goal: input.title, acceptance: fastAcceptance(input.title), planning: "skip", sizing: input.sizing,
143
+ ...(input.permissionMode === undefined ? {} : { permissionMode: input.permissionMode }),
144
+ filedVia: input.via === "console" ? "console" : input.via === "terminal-ui" ? "tui" : "cli", filedBy: { name: input.actor, kind: "person" },
145
+ }, now);
146
+ return filed.ok ? { ok: true } : { ok: false, message: filed.message };
147
+ }
148
+ class RolledBack extends Error {
149
+ answer;
150
+ constructor(answer) {
151
+ super("fast start preview");
152
+ this.answer = answer;
153
+ }
154
+ }
155
+ /** Run `body` in a transaction that never commits: a preview writes nothing. */
156
+ function dryRun(store, body) {
157
+ try {
158
+ store.transact(() => { throw new RolledBack(body()); });
159
+ }
160
+ catch (error) {
161
+ if (error instanceof RolledBack)
162
+ return error.answer;
163
+ throw error;
164
+ }
165
+ throw new Error("unreachable");
166
+ }
167
+ /**
168
+ * What the form renders: the classifier asked (at most its budget), then the same proposal the plan step would seal, in a
169
+ * transaction that's rolled back. The filing's words and the classifier's answer are kept (fast_preview), so Build
170
+ * files exactly this, whatever is posted.
171
+ */
172
+ export async function prepareFastStart(store, input, now) {
173
+ if (machineActing())
174
+ return { eligible: false, reason: "machine" };
175
+ if (!fastStartOn(store, input.repo))
176
+ return { eligible: false, reason: "setting" };
177
+ if (heuristicSizing({ title: input.title, goal: input.title }).size !== "small")
178
+ return { eligible: false, reason: "size" };
179
+ // A mode that auto-approves filings comes first: its own road, its own seal.
180
+ const mode = store.activeMode(input.repo, now);
181
+ if (mode !== null && /"autoApproveFiling":true/.test(mode.termsJson))
182
+ return { eligible: false, reason: "mode" };
183
+ const sizer = filingSizer();
184
+ const sizing = await classifyTask({ title: input.title, goal: input.title }, sizer.sizer, sizer.budget);
185
+ const taskId = `task-${randomBytes(8).toString("hex")}`;
186
+ const read = dryRun(store, () => {
187
+ const filed = fileIt(store, taskId, { ...input, sizing }, now);
188
+ if (!filed.ok)
189
+ return { ok: false, reason: "plan" };
190
+ return fastStartEligible(store, { ...input, taskId, sizing }, now);
191
+ });
192
+ if (!read.ok)
193
+ return { eligible: false, reason: read.reason };
194
+ const preview = randomBytes(16).toString("hex");
195
+ store.handle.prepare(`INSERT INTO fast_preview (id, actor, via, task_id, repo, title, permission, sizing_json, digest, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
196
+ .run(preview, input.actor, input.via, taskId, input.repo, input.title, input.permissionMode ?? null, JSON.stringify(sizing), read.scope.digest, now.toISOString(), new Date(now.getTime() + FAST_PREVIEW_MS).toISOString());
197
+ return { eligible: true, preview, digest: read.scope.digest, scope: read.scope, taskId };
198
+ }
199
+ /**
200
+ * Build: file and approve in one transaction. Every check is made here, on the rows written here; anything refused
201
+ * rolls the filing back, so a refusal leaves nothing behind.
202
+ */
203
+ export function fastStartBuild(store, input, now) {
204
+ if (machineActing())
205
+ return { ok: false, reason: "machine", message: FAST_WORDS.machine };
206
+ class Refused extends Error {
207
+ answer;
208
+ constructor(answer) {
209
+ super("fast start refused");
210
+ this.answer = answer;
211
+ }
212
+ }
213
+ const refuse = (answer) => { throw new Refused(answer); };
214
+ try {
215
+ return store.transact(() => {
216
+ const row = store.handle.prepare("SELECT * FROM fast_preview WHERE id = ? AND consumed_at IS NULL AND expires_at > ?").get(input.preview, now.toISOString());
217
+ if (row === undefined || row["actor"] !== input.actor || row["via"] !== input.via)
218
+ return refuse({ ok: false, reason: "gone", message: "This form has expired. Start again." });
219
+ const sizing = JSON.parse(String(row["sizing_json"]));
220
+ const taskId = String(row["task_id"]);
221
+ const filing = { actor: input.actor, repo: String(row["repo"]), title: String(row["title"]), ...(row["permission"] === null ? {} : { permissionMode: row["permission"] }) };
222
+ const filed = fileIt(store, taskId, { ...filing, via: input.via, sizing }, now);
223
+ if (!filed.ok)
224
+ return refuse({ ok: false, reason: "changed", message: filed.message });
225
+ const eligible = fastStartEligible(store, { ...filing, taskId, sizing, password: input.password }, now);
226
+ if (!eligible.ok)
227
+ return refuse({ ok: false, reason: eligible.reason, message: FAST_WORDS[eligible.reason] });
228
+ const at = now.toISOString();
229
+ // approve() compares the digest the person saw with the plan just filed: changed terms refuse "changed".
230
+ const approved = input.approveNow(taskId, input.digest, { rule: FAST_START_RULE, at });
231
+ if (!approved.ok)
232
+ return refuse({ ok: false, reason: approved.reason === "changed" ? "changed" : "password", message: approved.reason === "changed" ? "This changed since you read it. Read it again." : FAST_WORDS.password });
233
+ store.handle.prepare("UPDATE fast_preview SET consumed_at = ? WHERE id = ?").run(at, input.preview);
234
+ store.handle.prepare("UPDATE task_ref SET fast_start_at = ?, fast_start_until = ? WHERE backend = ? AND external_id = ?").run(at, new Date(now.getTime() + FAST_WINDOW_MS).toISOString(), BUILT_IN, taskId);
235
+ store.bumpWake();
236
+ return { ok: true, taskId };
237
+ });
238
+ }
239
+ catch (error) {
240
+ if (error instanceof Refused)
241
+ return error.answer;
242
+ throw error;
243
+ }
244
+ }
245
+ // ---- the start window ------------------------------------------------------------------------------------------------
246
+ /**
247
+ * A fast start that didn't start in time: its seal is cleared, in a logged transaction, and the task shows the ordinary
248
+ * Approve plan. A task with a build running or a result to read is left to that.
249
+ */
250
+ export function expireFastStarts(store, now) {
251
+ const stamp = now.toISOString();
252
+ // Forms nobody pressed hold a person's words: they go once they can't be pressed.
253
+ store.handle.prepare("DELETE FROM fast_preview WHERE expires_at <= ?").run(stamp);
254
+ const late = store.handle.prepare(`SELECT r.id AS ref, r.external_id AS task, r.repo AS repo FROM task_ref r JOIN task t ON t.id = r.external_id
255
+ WHERE r.backend = ? AND r.fast_start_until IS NOT NULL AND r.fast_start_until <= ? AND t.state = 'queued'`).all(BUILT_IN, stamp);
256
+ let cleared = 0;
257
+ for (const row of late) {
258
+ const ref = Number(row["ref"]), task = String(row["task"]);
259
+ store.transact(() => {
260
+ if (store.currentLiveLease(ref, now) !== null)
261
+ return;
262
+ store.handle.prepare(`UPDATE task_scope SET approved_at = NULL, approved_by = NULL, approved_digest = NULL, approved_profile_json = NULL, approved_route_json = NULL, approval_basis = NULL WHERE task_id = ?`).run(task);
263
+ store.handle.prepare("UPDATE task_ref SET fast_start_until = NULL WHERE id = ?").run(ref);
264
+ store.recordAction({ at: stamp, actor: "system", repo: row["repo"] === null ? null : String(row["repo"]), taskId: task, runId: null, action: "fast start expired", outcome: "seal cleared", source: "work", detail: `no build started within ${FAST_WINDOW_MS / 60_000} minutes` });
265
+ cleared++;
266
+ });
267
+ }
268
+ if (cleared > 0)
269
+ store.bumpWake();
270
+ return cleared;
271
+ }
272
+ // ---- the Accept backstop ---------------------------------------------------------------------------------------------
273
+ /** Files a small change has no business touching, by name, wherever they sit. */
274
+ const HEAVY_FILE = /(?:^|\/)(?:package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.ya?ml|bun\.lockb?|Cargo\.lock|Gemfile\.lock|poetry\.lock|Pipfile\.lock|composer\.lock|go\.sum|uv\.lock|package\.json|Cargo\.toml|Gemfile|go\.mod|pyproject\.toml|requirements[^/]*\.txt|composer\.json|schema\.(?:sql|prisma|rb|graphql)|structure\.sql)$|(?:^|\/)(?:migrations?|migrate|db\/migrate)\//i;
275
+ const SHA = /^[0-9a-f]{40}$/;
276
+ /** `ls-tree -r -z`: path to mode and object, read from the commit, so a gitlink can't hide from a diff setting. */
277
+ function treeOf(repo, commit) {
278
+ const out = new Map();
279
+ const listed = viewedGitSync(repo, ["ls-tree", "-r", "-z", "--full-tree", commit], { timeoutMs: 30_000, maxBuffer: 64 * 1024 * 1024 });
280
+ if (listed.status !== 0)
281
+ throw new Error("the commit couldn't be read");
282
+ for (const entry of listed.stdout.split("\0")) {
283
+ if (entry === "")
284
+ continue;
285
+ const tab = entry.indexOf("\t");
286
+ if (tab < 0)
287
+ throw new Error("a tree entry couldn't be read");
288
+ const [mode = "", type = "", object = ""] = entry.slice(0, tab).split(" ");
289
+ out.set(entry.slice(tab + 1), `${mode} ${type} ${object}`);
290
+ }
291
+ return out;
292
+ }
293
+ /**
294
+ * The whole change a result carries, verified: every version of the task (any ancestry problem refuses), from the base in
295
+ * Toolroll's own records to the head on the run record, every contributing build's saved diff present, and every file
296
+ * countable through the hardened git. Null-like answers refuse: nothing here is ever a guess.
297
+ */
298
+ export function verifiedChange(store, taskId, runId) {
299
+ const no = (problem) => ({ ok: false, problem });
300
+ const ancestry = store.revisionAncestryStatus(taskId);
301
+ if (ancestry.problem !== null)
302
+ return no("Its history can't be read whole.");
303
+ const run = store.getRun(runId);
304
+ const ref = store.lookupRef(taskId);
305
+ if (run === null || ref === null || ref.repo === null || run.taskRef !== ref.id || run.headRevision === null || !SHA.test(run.headRevision))
306
+ return no("Its result has no recorded commit.");
307
+ const base = originalTaskBase(store, taskId);
308
+ if (base === null || !SHA.test(base))
309
+ return no("Its base commit isn't on record.");
310
+ for (const id of ancestry.chain.length === 0 ? [taskId] : ancestry.chain) {
311
+ const member = store.lookupRef(id);
312
+ if (member === null)
313
+ return no("Its history can't be read whole.");
314
+ for (const one of store.runsFor(member.id)) {
315
+ if ((one.role !== "builder" && one.role !== "repair") || one.outcome !== "built")
316
+ continue;
317
+ if (!store.artifactsFor(one.id).some(artifact => artifact.kind === "diff-stat" || artifact.kind === "terminal-diff"))
318
+ return no(`Build #${one.id} has no saved diff.`);
319
+ }
320
+ }
321
+ try {
322
+ const before = treeOf(ref.repo, base), after = treeOf(ref.repo, run.headRevision);
323
+ const changed = new Set();
324
+ for (const [path, entry] of after)
325
+ if (before.get(path) !== entry)
326
+ changed.add(path);
327
+ for (const path of before.keys())
328
+ if (!after.has(path))
329
+ changed.add(path);
330
+ for (const path of changed) {
331
+ const entry = after.get(path) ?? before.get(path) ?? "";
332
+ if (!entry.startsWith("100"))
333
+ return no(`${path} can't be counted.`);
334
+ }
335
+ const counted = viewedGitSync(ref.repo, ["diff-tree", "-r", "-z", "--numstat", "--no-renames", "--no-ext-diff", "--no-textconv", base, run.headRevision], { timeoutMs: 30_000, maxBuffer: 16 * 1024 * 1024 });
336
+ if (counted.status !== 0)
337
+ return no("The change couldn't be counted.");
338
+ const files = [];
339
+ for (const entry of counted.stdout.split("\0")) {
340
+ if (entry === "")
341
+ continue;
342
+ const [added = "", removed = "", ...rest] = entry.split("\t");
343
+ const path = rest.join("\t");
344
+ if (!/^\d+$/.test(added) || !/^\d+$/.test(removed))
345
+ return no(`${path} can't be counted.`);
346
+ files.push({ path, added: Number(added), removed: Number(removed) });
347
+ }
348
+ if (files.length !== changed.size || files.some(one => !changed.has(one.path)))
349
+ return no("The change couldn't be counted whole.");
350
+ return { ok: true, base, head: run.headRevision, files, lines: files.reduce((sum, one) => sum + one.added + one.removed, 0) };
351
+ }
352
+ catch {
353
+ return no("The change couldn't be read.");
354
+ }
355
+ }
356
+ /** Whether any version of this task started fast. An unreadable history counts: it fails closed below. */
357
+ export function fastStarted(store, taskId) {
358
+ const ancestry = store.revisionAncestryStatus(taskId);
359
+ if (ancestry.problem !== null) {
360
+ const own = store.handle.prepare("SELECT 1 AS hit FROM task_ref WHERE backend = ? AND external_id = ? AND fast_start_at IS NOT NULL").get(BUILT_IN, taskId);
361
+ return own !== undefined;
362
+ }
363
+ const chain = ancestry.chain.length === 0 ? [taskId] : ancestry.chain;
364
+ return store.handle.prepare(`SELECT 1 AS hit FROM task_ref WHERE backend = ? AND fast_start_at IS NOT NULL AND external_id IN (SELECT value FROM json_each(?))`).get(BUILT_IN, JSON.stringify(chain)) !== undefined;
365
+ }
366
+ /** The one sentence over every over-the-caps refusal; the road on is the approval of the expanded commit (keepGrownChange). */
367
+ export const GREW_WORDS = "This grew beyond a small change. Approve it to keep it.";
368
+ function grownApproved(store, taskId, runId) {
369
+ const scope = store.getScope(taskId), run = store.getRun(runId);
370
+ return scope !== null && run !== null && scope.candidate != null && scope.candidate === run.headRevision && scope.approvedDigest === scope.digest && run.scopeDigest === scope.digest
371
+ && (scope.approvalBasis === "password" || scope.approvalBasis === "chat");
372
+ }
373
+ /** Why this exact result can't go further as a small change, or null. */
374
+ export function fastChangeProblem(store, taskId, runId) {
375
+ if (!fastStarted(store, taskId))
376
+ return null;
377
+ if (grownApproved(store, taskId, runId))
378
+ return null;
379
+ const change = verifiedChange(store, taskId, runId);
380
+ if (!change.ok)
381
+ return GREW_WORDS;
382
+ if (change.files.length > FAST_MAX_FILES || change.lines > FAST_MAX_LINES)
383
+ return GREW_WORDS;
384
+ const ref = store.lookupRef(taskId);
385
+ const paths = change.files.map(one => one.path);
386
+ if (paths.some(path => HEAVY_FILE.test(path)))
387
+ return GREW_WORDS;
388
+ if (ref?.repo != null && protectedChanges(store.approvalRules(ref.repo), paths).length > 0)
389
+ return GREW_WORDS;
390
+ return null;
391
+ }
392
+ /** Completion: a person only, and only a change the measure accepts. */
393
+ export function fastCompletionProblem(store, taskId, runId, person) {
394
+ if (!fastStarted(store, taskId))
395
+ return null;
396
+ if (person === null)
397
+ return "A person accepts this one.";
398
+ return fastChangeProblem(store, taskId, runId);
399
+ }
400
+ /** Publication and merge: nothing leaves until a person accepted this exact build, and the measure still accepts it. */
401
+ export function fastPublishProblem(store, taskId, runId) {
402
+ if (!fastStarted(store, taskId))
403
+ return null;
404
+ const accepted = store.handle.prepare("SELECT 1 AS hit FROM action_ledger WHERE run_id = ? AND action = ? AND detail = ? LIMIT 1").get(runId, COMPLETION_ACTION, COMPLETION_BY_PERSON);
405
+ if (accepted === undefined)
406
+ return "It waits for a person to accept it.";
407
+ return fastChangeProblem(store, taskId, runId);
408
+ }
409
+ /**
410
+ * "Approve it to keep it": a plan for the actual expanded commit, proposed the way a rerun proposes one (dispose.ts
411
+ * regateTask): the same terms with `candidate` set to the exact head. It seals nothing; the person approves it on the
412
+ * ordinary ceremony, Toolroll checks that commit out with no agent, and only that attempt's result goes further.
413
+ */
414
+ export function keepGrownChange(store, taskId, now) {
415
+ if (machineActing())
416
+ return { ok: false, message: FAST_WORDS.machine };
417
+ return store.transact(() => {
418
+ const ref = store.lookupRef(taskId);
419
+ const scope = store.getScope(taskId);
420
+ const last = ref === null ? null : store.lastBuilderRun(ref.id);
421
+ if (ref === null || scope === null || last === null || last.headRevision === null)
422
+ return { ok: false, message: "This task has no result to keep." };
423
+ if (!fastStarted(store, taskId) || fastChangeProblem(store, taskId, last.id) === null)
424
+ return { ok: false, message: "This change doesn't need a new approval." };
425
+ if (store.hasLiveClaim(ref.id, now))
426
+ return { ok: false, message: "A build is running on it." };
427
+ const proposed = propose(store, { taskId, goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, budgetMicrousd: scope.budgetMicrousd,
428
+ acceptance: scope.acceptance, qualityMode: scope.qualityMode ?? "default", candidate: last.headRevision, now });
429
+ return { ok: true, digest: proposed.digest };
430
+ });
431
+ }
@@ -0,0 +1,9 @@
1
+ /** A fast start's terms, the words a yes agrees to (scope.ts describeScope, the lines every plan approval reads), plus the
2
+ * project tools its seal will carry. The same lines on the New task form, in the terminal UI and in `task new`. The
3
+ * plan version and approval state rows say nothing yet: nothing is approved. */
4
+ import { type Scope } from "./scope.js";
5
+ import type { Store } from "./store.js";
6
+ export declare function fastTermsLines(store: Store, scope: Scope, repo: string): {
7
+ label: string;
8
+ words: string;
9
+ }[];
@@ -0,0 +1,23 @@
1
+ /** A fast start's terms, the words a yes agrees to (scope.ts describeScope, the lines every plan approval reads), plus the
2
+ * project tools its seal will carry. The same lines on the New task form, in the terminal UI and in `task new`. The
3
+ * plan version and approval state rows say nothing yet: nothing is approved. */
4
+ import { describeScope } from "./scope.js";
5
+ const LABELS = { goal: "Goal", "not this": "Not this", files: "Files", budget: "Budget", network: "Network", writes: "Writes", reads: "Reads", checks: "Checks", services: "Services" };
6
+ export function fastTermsLines(store, scope, repo) {
7
+ const lines = [];
8
+ for (const raw of describeScope(scope)) {
9
+ const line = raw.trimEnd();
10
+ const named = /^ {2}(\S+(?: \S+)?) {2,}(.*)$/.exec(line);
11
+ if (named !== null && (named[1] === "version" || named[1] === "approved"))
12
+ continue;
13
+ if (named !== null)
14
+ lines.push({ label: LABELS[named[1]] ?? `${named[1][0].toUpperCase()}${named[1].slice(1)}`, words: named[2] });
15
+ else if (lines.length > 0)
16
+ lines[lines.length - 1].words += `\n${line.trim()}`;
17
+ else
18
+ lines.push({ label: "Plan", words: line.trim() });
19
+ }
20
+ const tools = store.projectTools(repo).map(one => one.name);
21
+ lines.push({ label: "Project tools", words: tools.length === 0 ? "None" : tools.join(", ") });
22
+ return lines;
23
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * What a sandboxed Full check must show for what it couldn't run (docs/plans/trusted-release-reports.md, H3). Inside a
3
+ * fence some cases skip: a test that makes a sandbox, or a keychain. A skipped case counts as covered only if the SAME
4
+ * case passed in the macOS CI of that exact commit, or it is on the short reviewed list below. Being in the same file
5
+ * isn't enough: a file is in the macOS lane even when its case is skipped there (Codex round 1, finding 6).
6
+ *
7
+ * This reads nothing on an unsandboxed check, which skips nothing for this reason: Toolroll's own releases, today. It
8
+ * guards a release only once a person has narrowed that project's check access, and scripts/fenced-run.mjs has measured
9
+ * it first. Reports are the candidate's own words (R1): this keeps an accident from passing unseen, not a hostile test.
10
+ *
11
+ * The fallback: when a sandboxed check can't cover everything, the release scope seals the wider level the measurement
12
+ * shows it needs. That is an operator's widening, with a password, and a new approval whose card says so: nothing here
13
+ * widens anything, and a release can always go out at the level a person approves.
14
+ */
15
+ import type { CheckAccess } from "./check-access.js";
16
+ import type { CheckReport } from "./contracts/release-check.js";
17
+ /** Cases that may stay skipped with no CI case behind them, each with why. Product code, reviewed like it: never a file a
18
+ * release's commits could carry. Empty until a measured run gives a reason. */
19
+ export declare const FENCED_SKIP_EXEMPTIONS: readonly {
20
+ identity: string;
21
+ why: string;
22
+ }[];
23
+ export declare const unitCaseIdentity: (file: string, name: string) => string;
24
+ type VitestAssertion = {
25
+ title?: unknown;
26
+ ancestorTitles?: unknown;
27
+ status?: unknown;
28
+ };
29
+ type VitestReport = {
30
+ testResults?: {
31
+ name?: unknown;
32
+ assertionResults?: VitestAssertion[];
33
+ }[];
34
+ };
35
+ /**
36
+ * Every case in the macOS CI's reports, by the identity the release check's own reporter gives it (test/release-reporter.ts:
37
+ * `suite > test`; Vitest's JSON `fullName` joins with spaces, so it can't be matched as it stands). A name twice in a file
38
+ * passes only if every one of them did. `root` is the checkout the reports name files under.
39
+ */
40
+ export declare function macosCases(reports: readonly VitestReport[], root: string): Map<string, boolean>;
41
+ /** Every case a check's report says it skipped: unit cases and journeys alike. */
42
+ export declare function skippedCases(report: CheckReport): string[];
43
+ export type SkipCoverage = {
44
+ covered: string[];
45
+ exempt: string[];
46
+ uncovered: string[];
47
+ };
48
+ /** Each skipped case: passed in that commit's macOS CI, reviewed as exempt, or not covered. */
49
+ export declare function skipCoverage(skipped: readonly string[], macos: ReadonlyMap<string, boolean>, exemptions?: readonly {
50
+ identity: string;
51
+ }[]): SkipCoverage;
52
+ /** A measurement's verdict on one level (scripts/fenced-run.mjs): whether the check passed there, and what it left uncovered. */
53
+ export type LevelMeasurement = {
54
+ level: CheckAccess;
55
+ passed: boolean;
56
+ uncovered: number;
57
+ };
58
+ /** The narrowest level a measured check fully covers. No level covered: `unsandboxed`, as today, with a person's yes. */
59
+ export declare function measuredLevel(measurements: readonly LevelMeasurement[]): CheckAccess;
60
+ /** Why a sandboxed check can't stand for this release, in words a person can act on. */
61
+ export declare function uncoveredWords(uncovered: readonly string[]): string;
62
+ export {};