toolroll 0.9.92 → 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 (341) hide show
  1. package/CHANGELOG.md +87 -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-status.d.ts +1 -1
  11. package/dist/assignment-ui.js +3 -3
  12. package/dist/assignment.js +16 -3
  13. package/dist/boot-identity.d.ts +2 -1
  14. package/dist/boot-identity.js +6 -4
  15. package/dist/bounded-spawn.d.ts +57 -0
  16. package/dist/bounded-spawn.js +220 -0
  17. package/dist/browser/terminal.js +42 -42
  18. package/dist/browser/workspace.css +2 -2
  19. package/dist/browser/workspace.js +26 -26
  20. package/dist/browser-crew.js +2 -2
  21. package/dist/browser-shell.d.ts +1 -1
  22. package/dist/browser-shell.js +2 -2
  23. package/dist/browser-workspace.d.ts +48 -7
  24. package/dist/browser-workspace.js +3 -3
  25. package/dist/builder-restart.d.ts +21 -0
  26. package/dist/builder-restart.js +50 -0
  27. package/dist/builder.d.ts +4 -0
  28. package/dist/builder.js +73 -25
  29. package/dist/chat-acceptance.js +1 -1
  30. package/dist/chat-actions.d.ts +12 -2
  31. package/dist/chat-actions.js +25 -2
  32. package/dist/chat-channel.js +11 -0
  33. package/dist/chat-decide.d.ts +7 -2
  34. package/dist/chat-decide.js +37 -9
  35. package/dist/chat-quiet.js +3 -3
  36. package/dist/chat-voice.d.ts +1 -1
  37. package/dist/chat-voice.js +2 -2
  38. package/dist/check-facts.d.ts +126 -0
  39. package/dist/check-facts.js +318 -0
  40. package/dist/check-failure.d.ts +22 -3
  41. package/dist/check-failure.js +57 -13
  42. package/dist/check-levels-ui.d.ts +4 -0
  43. package/dist/check-levels-ui.js +10 -1
  44. package/dist/check-load.d.ts +85 -0
  45. package/dist/check-load.js +147 -0
  46. package/dist/check-only-standing.d.ts +36 -0
  47. package/dist/check-only-standing.js +39 -0
  48. package/dist/check-only.d.ts +15 -0
  49. package/dist/check-only.js +99 -0
  50. package/dist/check-report.d.ts +7 -0
  51. package/dist/check-report.js +4 -3
  52. package/dist/check-stamp.d.ts +46 -0
  53. package/dist/check-stamp.js +93 -0
  54. package/dist/checkout-cleanup.d.ts +36 -5
  55. package/dist/checkout-cleanup.js +166 -14
  56. package/dist/cli-help.d.ts +4 -1
  57. package/dist/cli-help.js +47 -30
  58. package/dist/cli.js +16 -1
  59. package/dist/code-extract.d.ts +44 -0
  60. package/dist/code-extract.js +88 -0
  61. package/dist/code-map-acts.d.ts +24 -0
  62. package/dist/code-map-acts.js +53 -0
  63. package/dist/code-map-admission.d.ts +52 -0
  64. package/dist/code-map-admission.js +295 -0
  65. package/dist/code-map-build.d.ts +63 -0
  66. package/dist/code-map-build.js +415 -0
  67. package/dist/code-map-cli.d.ts +153 -0
  68. package/dist/code-map-cli.js +103 -0
  69. package/dist/code-map-main.d.ts +1 -0
  70. package/dist/code-map-main.js +48 -0
  71. package/dist/code-map-read.d.ts +106 -0
  72. package/dist/code-map-read.js +171 -0
  73. package/dist/code-map-report.d.ts +37 -0
  74. package/dist/code-map-report.js +95 -0
  75. package/dist/code-map-shape.d.ts +172 -0
  76. package/dist/code-map-shape.js +327 -0
  77. package/dist/code-map-store.d.ts +136 -0
  78. package/dist/code-map-store.js +233 -0
  79. package/dist/coding-provider.d.ts +2 -0
  80. package/dist/coding-provider.js +3 -2
  81. package/dist/coding-update.js +3 -2
  82. package/dist/coding-workspace.js +4 -4
  83. package/dist/console-chrome.js +2 -2
  84. package/dist/containment.d.ts +5 -0
  85. package/dist/containment.js +14 -1
  86. package/dist/contracts/chat-actions.d.ts +9 -0
  87. package/dist/contracts/chat-actions.js +2 -0
  88. package/dist/contracts/cli.d.ts +52 -0
  89. package/dist/contracts/cli.js +4 -0
  90. package/dist/contracts/console-api.d.ts +12 -5
  91. package/dist/contracts/console-api.js +12 -5
  92. package/dist/contracts/gateway-tools.d.ts +17 -0
  93. package/dist/contracts/gateway-tools.js +3 -1
  94. package/dist/contracts/lead-tools.d.ts +61 -0
  95. package/dist/contracts/lead-tools.js +10 -1
  96. package/dist/contracts/project-knowledge.d.ts +38 -5
  97. package/dist/contracts/project-knowledge.js +11 -5
  98. package/dist/contracts/project-memory.d.ts +3 -1
  99. package/dist/contracts/project-memory.js +3 -2
  100. package/dist/contracts/release-check.d.ts +25 -1
  101. package/dist/contracts/release-check.js +19 -0
  102. package/dist/contracts/storage.d.ts +14 -0
  103. package/dist/contracts/storage.js +3 -1
  104. package/dist/contracts/store-json.d.ts +2 -0
  105. package/dist/contracts/youiyoux-request.d.ts +1 -1
  106. package/dist/contracts/youiyoux-request.js +1 -1
  107. package/dist/desktop-access.js +6 -2
  108. package/dist/desktop-update.d.ts +2 -2
  109. package/dist/desktop-update.js +13 -9
  110. package/dist/dispatch.js +18 -15
  111. package/dist/dispose.js +8 -2
  112. package/dist/easy-start.js +2 -3
  113. package/dist/eval-hooks.d.ts +55 -0
  114. package/dist/eval-hooks.js +227 -4
  115. package/dist/evals.js +2 -1
  116. package/dist/fast-start.d.ts +136 -0
  117. package/dist/fast-start.js +431 -0
  118. package/dist/fast-terms.d.ts +9 -0
  119. package/dist/fast-terms.js +23 -0
  120. package/dist/fenced-check-coverage.d.ts +62 -0
  121. package/dist/fenced-check-coverage.js +54 -0
  122. package/dist/first-run.js +1 -1
  123. package/dist/flaky-ci.d.ts +31 -0
  124. package/dist/flaky-ci.js +149 -0
  125. package/dist/flaky-cli.d.ts +60 -0
  126. package/dist/flaky-cli.js +60 -0
  127. package/dist/flaky.d.ts +83 -0
  128. package/dist/flaky.js +264 -0
  129. package/dist/flow-card-task.js +3 -0
  130. package/dist/flow-triggers.d.ts +1 -1
  131. package/dist/flow-triggers.js +1 -1
  132. package/dist/github-import.d.ts +62 -0
  133. package/dist/github-import.js +146 -0
  134. package/dist/guides.js +15 -10
  135. package/dist/invoke.d.ts +5 -1
  136. package/dist/invoke.js +49 -14
  137. package/dist/job-object-helper.ps1 +10 -3
  138. package/dist/keymap.d.ts +41 -0
  139. package/dist/keymap.js +51 -0
  140. package/dist/knowledge-cli.js +1 -1
  141. package/dist/knowledge-ui.d.ts +21 -4
  142. package/dist/knowledge-ui.js +75 -7
  143. package/dist/laptop-background.d.ts +16 -0
  144. package/dist/laptop-background.js +108 -2
  145. package/dist/lead-context.js +1 -1
  146. package/dist/lead-contract.d.ts +1 -1
  147. package/dist/lead-contract.js +2 -2
  148. package/dist/lead-progress.js +3 -0
  149. package/dist/lead-status.d.ts +2 -0
  150. package/dist/lead-status.js +6 -2
  151. package/dist/lead-tools.js +51 -2
  152. package/dist/lead.js +1 -1
  153. package/dist/lesson-notes.d.ts +81 -0
  154. package/dist/lesson-notes.js +336 -0
  155. package/dist/limits-ui.js +1 -1
  156. package/dist/local-owner.d.ts +1 -1
  157. package/dist/local-owner.js +11 -0
  158. package/dist/machine-check.d.ts +16 -0
  159. package/dist/machine-check.js +26 -0
  160. package/dist/machine-identity.d.ts +75 -0
  161. package/dist/machine-identity.js +170 -0
  162. package/dist/mcp-core.js +11 -1
  163. package/dist/memory-cli.d.ts +300 -1
  164. package/dist/memory-cli.js +62 -9
  165. package/dist/memory-pass.d.ts +16 -0
  166. package/dist/memory-pass.js +67 -3
  167. package/dist/memory-tidy.d.ts +181 -0
  168. package/dist/memory-tidy.js +523 -0
  169. package/dist/memory-use.d.ts +35 -0
  170. package/dist/memory-use.js +109 -0
  171. package/dist/merge-checklist.d.ts +55 -0
  172. package/dist/merge-checklist.js +182 -0
  173. package/dist/needs-you.d.ts +32 -13
  174. package/dist/needs-you.js +108 -49
  175. package/dist/nightly-github.d.ts +2 -0
  176. package/dist/nightly-github.js +4 -4
  177. package/dist/operate-remote-arguments.js +6 -1
  178. package/dist/operate-remote.js +3 -1
  179. package/dist/operate.d.ts +6 -1
  180. package/dist/operate.js +540 -70
  181. package/dist/phase-routing.js +2 -2
  182. package/dist/plain-schema.d.ts +23 -0
  183. package/dist/plain-schema.js +94 -0
  184. package/dist/plain-words.d.ts +20 -0
  185. package/dist/plain-words.js +66 -0
  186. package/dist/planner.js +2 -1
  187. package/dist/process-liveness.d.ts +2 -0
  188. package/dist/process-liveness.js +61 -17
  189. package/dist/project-delete-ui.d.ts +7 -2
  190. package/dist/project-delete-ui.js +12 -1
  191. package/dist/project-delete.js +13 -2
  192. package/dist/project-knowledge.d.ts +18 -11
  193. package/dist/project-knowledge.js +39 -9
  194. package/dist/project-learning.d.ts +2 -1
  195. package/dist/project-learning.js +15 -45
  196. package/dist/project-memory.d.ts +5 -0
  197. package/dist/project-memory.js +22 -2
  198. package/dist/proof.js +30 -5
  199. package/dist/provider-auth.js +1 -1
  200. package/dist/publish.js +9 -1
  201. package/dist/pull-request-flow.js +5 -0
  202. package/dist/push.d.ts +26 -3
  203. package/dist/push.js +55 -15
  204. package/dist/release-adapters.d.ts +9 -0
  205. package/dist/release-adapters.js +87 -1
  206. package/dist/release-approval.d.ts +191 -0
  207. package/dist/release-approval.js +382 -0
  208. package/dist/release-auto.js +26 -17
  209. package/dist/release-card.d.ts +74 -6
  210. package/dist/release-card.js +179 -23
  211. package/dist/release-carry.d.ts +7 -2
  212. package/dist/release-carry.js +39 -9
  213. package/dist/release-cli.d.ts +4 -0
  214. package/dist/release-cli.js +38 -23
  215. package/dist/release-coverage.js +1 -1
  216. package/dist/release-next.d.ts +48 -0
  217. package/dist/release-next.js +161 -0
  218. package/dist/release.d.ts +50 -1
  219. package/dist/release.js +118 -11
  220. package/dist/remote-destination.d.ts +36 -0
  221. package/dist/remote-destination.js +217 -0
  222. package/dist/remote-main.d.ts +48 -0
  223. package/dist/remote-main.js +84 -5
  224. package/dist/repository-context.d.ts +3 -2
  225. package/dist/repository-context.js +112 -70
  226. package/dist/restart-certification.d.ts +1 -0
  227. package/dist/restart-certification.js +8 -6
  228. package/dist/result-acts.d.ts +29 -21
  229. package/dist/result-acts.js +36 -25
  230. package/dist/result-follow-ups.d.ts +2 -2
  231. package/dist/result-follow-ups.js +18 -4
  232. package/dist/result-review.d.ts +1 -1
  233. package/dist/route-change-check.d.ts +4 -0
  234. package/dist/route-change-check.js +13 -0
  235. package/dist/route-change.d.ts +22 -0
  236. package/dist/route-change.js +89 -0
  237. package/dist/route-hooks.js +3 -2
  238. package/dist/route-results.d.ts +6 -1
  239. package/dist/route-results.js +14 -5
  240. package/dist/run-closeout.d.ts +1 -1
  241. package/dist/run-closeout.js +7 -5
  242. package/dist/runner-process.js +3 -2
  243. package/dist/runner.js +2 -1
  244. package/dist/scope.d.ts +6 -1
  245. package/dist/scope.js +22 -9
  246. package/dist/serve.d.ts +1 -1
  247. package/dist/serve.js +24 -18
  248. package/dist/server/chat.js +14 -12
  249. package/dist/server/chrome.d.ts +1 -1
  250. package/dist/server/chrome.js +26 -35
  251. package/dist/server/console.css +23 -0
  252. package/dist/server/guards.js +4 -0
  253. package/dist/server/home.d.ts +6 -0
  254. package/dist/server/home.js +21 -13
  255. package/dist/server/http.js +1 -1
  256. package/dist/server/kit.js +1 -1
  257. package/dist/server/merge-ready-view.d.ts +10 -0
  258. package/dist/server/merge-ready-view.js +30 -0
  259. package/dist/server/pages-assets.d.ts +1 -0
  260. package/dist/server/pages-assets.js +8 -5
  261. package/dist/server/pages.js +103 -82
  262. package/dist/server/render-chat.js +2 -2
  263. package/dist/server/render-code-map.d.ts +30 -0
  264. package/dist/server/render-code-map.js +137 -0
  265. package/dist/server/render-flaky.d.ts +20 -0
  266. package/dist/server/render-flaky.js +59 -0
  267. package/dist/server/render-history.js +1 -1
  268. package/dist/server/render-new-task.d.ts +26 -1
  269. package/dist/server/render-new-task.js +30 -5
  270. package/dist/server/render-pages.d.ts +1 -175
  271. package/dist/server/render-pages.js +38 -313
  272. package/dist/server/render-projects.d.ts +2 -1
  273. package/dist/server/render-projects.js +9 -7
  274. package/dist/server/render-release.d.ts +19 -0
  275. package/dist/server/render-release.js +84 -0
  276. package/dist/server/render-results.d.ts +2 -2
  277. package/dist/server/render-results.js +71 -73
  278. package/dist/server/render-tasks.d.ts +13 -2
  279. package/dist/server/render-tasks.js +156 -139
  280. package/dist/server/retired-routes.d.ts +17 -0
  281. package/dist/server/retired-routes.js +53 -0
  282. package/dist/server/route-table.d.ts +4 -1
  283. package/dist/server/route-table.js +52 -13
  284. package/dist/server/settings.js +129 -22
  285. package/dist/server/task-views.js +61 -26
  286. package/dist/server/tasks.d.ts +17 -6
  287. package/dist/server/tasks.js +273 -338
  288. package/dist/server/welcome.js +1 -1
  289. package/dist/settings-groups-ui.js +1 -1
  290. package/dist/setup-guide.d.ts +2 -2
  291. package/dist/stop-proof.d.ts +67 -0
  292. package/dist/stop-proof.js +295 -0
  293. package/dist/storage-sweep-process.js +1 -1
  294. package/dist/storage-sweep.d.ts +5 -3
  295. package/dist/storage-sweep.js +30 -9
  296. package/dist/storage-ui.js +28 -17
  297. package/dist/storage.d.ts +27 -2
  298. package/dist/storage.js +39 -2
  299. package/dist/store.d.ts +148 -34
  300. package/dist/store.js +626 -177
  301. package/dist/surface.js +46 -5
  302. package/dist/sweep-quarantine.d.ts +1 -0
  303. package/dist/sweep-quarantine.js +6 -4
  304. package/dist/tamper-scan.d.ts +4 -0
  305. package/dist/tamper-scan.js +90 -0
  306. package/dist/task-aliases.d.ts +5 -0
  307. package/dist/task-aliases.js +9 -0
  308. package/dist/task-control.js +4 -1
  309. package/dist/task-outcome-cli.d.ts +2 -0
  310. package/dist/task-outcome-cli.js +36 -17
  311. package/dist/task-sizing.d.ts +5 -0
  312. package/dist/task-sizing.js +2 -0
  313. package/dist/task-source.d.ts +7 -0
  314. package/dist/task-source.js +14 -0
  315. package/dist/task-status.d.ts +9 -0
  316. package/dist/task-status.js +40 -49
  317. package/dist/teardown-git.d.ts +16 -0
  318. package/dist/teardown-git.js +63 -5
  319. package/dist/telegram-progress.js +5 -3
  320. package/dist/telegram-status.js +6 -4
  321. package/dist/test-infra.d.ts +8 -0
  322. package/dist/test-infra.js +18 -0
  323. package/dist/tidy.js +1 -1
  324. package/dist/toolroll-update-ui.js +2 -2
  325. package/dist/toolroll-update.js +18 -5
  326. package/dist/tui-contract.d.ts +24 -0
  327. package/dist/tui-source.js +67 -8
  328. package/dist/tui.js +51 -51
  329. package/dist/vanished-retry.d.ts +30 -0
  330. package/dist/vanished-retry.js +174 -0
  331. package/dist/work-index.d.ts +2 -0
  332. package/dist/work-index.js +82 -70
  333. package/dist/work-summary.d.ts +2 -2
  334. package/dist/work-summary.js +2 -0
  335. package/dist/workspace-ui.d.ts +4 -2
  336. package/dist/workspace-ui.js +28 -22
  337. package/dist/worktree.d.ts +12 -3
  338. package/dist/worktree.js +68 -13
  339. package/package.json +1 -1
  340. package/dist/server/render-next.d.ts +0 -63
  341. package/dist/server/render-next.js +0 -109
package/dist/invoke.js CHANGED
@@ -27,7 +27,7 @@ import { attestProvider } from "./attest.js";
27
27
  import { run as runCommand, runOwnerTag, streamsInput } from "./exec.js";
28
28
  import { spawnSync } from "node:child_process";
29
29
  import { currentContainment } from "./containment.js";
30
- import { recordRunBillingBasis, recordRunCostUnknown, recordRunModelReported } from "./store.js";
30
+ import { recordCheckStamp, recordRunBillingBasis, recordRunCostUnknown, recordRunModelReported } from "./store.js";
31
31
  import { witnessedRunner } from "./process-custody.js";
32
32
  import { underStopWatch } from "./task-control.js";
33
33
  import { childDatabaseEnv, isolatedChildDatabase, removeChildDatabase as removeAgentDatabase } from "./child-database.js";
@@ -39,10 +39,12 @@ import { dirname, join } from "node:path";
39
39
  import { poolRootOf } from "./worktree.js";
40
40
  import { checkLaunchRoot, signInGrants, signInIsItself } from "./agent-fence.js";
41
41
  import { localModeOn } from "./local-owner.js";
42
+ import { CHECK_STAMP_VERSION, EVERYWHERE, unfencedStamp } from "./check-stamp.js";
42
43
  import { CHECK_ACCESS_DEFAULT, checkAccessHere, localModeCap, narrowestAccess, narrowestNetwork, signInsAvailable } from "./check-access.js";
43
44
  import { readDenyPath } from "./project-sandbox.js";
44
45
  import { sandboxRuntimeWords } from "./sandbox-runtime.js";
45
46
  import { canonicalJson } from "./canonical-json.js";
47
+ import { deliveredFile } from "./code-map-admission.js";
46
48
  import { buildCacheEnv, buildCacheFolder, grantFolder, writeAllowlist, WRITES_DEFAULT, CheckoutBindingError, trustedWorktreeBinding, checkoutGitEnv, HARDENED_GIT_ARGS, codexFenceArgv, fenceFor, fencedGitEnv, LEGACY_SANDBOX, launchSandboxOf, linuxFenceAvailable, macosFenceAvailable, NO_GRANTS, sandboxedSpawn, serverGrantsOf, runFolders, taskBoundary } from "./agent-fence.js";
47
49
  import { proxyEnv, providerOnlyNetwork, repositoryEcosystems, resolveNetwork, startEgressProxy } from "./egress.js";
48
50
  import { claudeConfigDir, claudeDefaults, codexDefaults, codexHomeDir, isolatedCodexHome } from "./provider-home.js";
@@ -960,6 +962,8 @@ function runFence(store, runId, provider, keyHome, ownTools, writes = WRITES_DEF
960
962
  // checkout was recorded in (a --pool elsewhere), so another project's worktree, present at launch, is out of reach. Native wrappers also cover later creations.
961
963
  const launch = fenceFor(provider === "command" ? "command" : "build", { databaseFile, worktree, ...home, boundary, binding, runFolders: folders, poolRoots,
962
964
  extra: allowed?.deny ?? [], linked: linkedFor(store, worktree), writes: allowed === null ? null : { writes: allowed.writes, readOnly: allowed.readOnly }, policy: sandbox.policy, provider,
965
+ // This launch's code map file: exactly that file, read-only, inside the denied state folder. Commands don't get it.
966
+ ...(provider === "command" ? {} : { exactFile: deliveredFile(store, runId) }),
963
967
  // The sealed read denies, and whatever the project lists now too (adding a deny only narrows).
964
968
  readDeny: [...new Set([...sandbox.readDeny, ...(row === null ? [] : store.projectSandbox(row.repo).readDeny)])].map(one => readDenyPath(one, keyHome ?? homedir())), machLookup: sandbox.machLookup });
965
969
  return {
@@ -992,6 +996,7 @@ export function fencedCommands(store, runId, writes, inner, available = () => ma
992
996
  if (!available()) {
993
997
  if (store.unfencedAgentsAllowed()) {
994
998
  store.stampRunCheckAccess(runId, "unsandboxed");
999
+ stampLaunch(store, [runId], unfencedStamp());
995
1000
  return inner(file, args, options);
996
1001
  }
997
1002
  return { code: 1, stdout: "", stderr: "This machine can't confine the build's commands (no macOS sandbox, no working bubblewrap), so they didn't run. Install bubblewrap, socat and ripgrep (Linux), or an instance operator allows unconfined agents with `toolroll config unconfined-agents allow`.\n", timedOut: false, notFound: false };
@@ -1002,13 +1007,17 @@ export function fencedCommands(store, runId, writes, inner, available = () => ma
1002
1007
  mkdirSync(temp, { mode: 0o700 });
1003
1008
  // Not sandboxed (check-access.ts): the project's checks are trusted by the whole instance, as before 0.9.66. Never
1004
1009
  // while local mode is on (easy-start C5): then they run in the fence, sandboxed.
1005
- if (access === "unsandboxed" && !localModeOn(store))
1010
+ if (access === "unsandboxed" && !localModeOn(store)) {
1011
+ stampLaunch(store, [runId], unfencedStamp());
1006
1012
  return await inner(file, args, { ...options, env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp } });
1013
+ }
1007
1014
  const fenced = runFence(store, runId, "command", undefined, own, writes, sandbox);
1008
1015
  if (fenced.problem !== undefined)
1009
1016
  return { code: 1, stdout: "", stderr: `${fenced.problem}\n`, timedOut: false, notFound: false };
1010
- if (fenced.fence.length === 0)
1017
+ if (fenced.fence.length === 0) {
1018
+ stampLaunch(store, [runId], unfencedStamp());
1011
1019
  return inner(file, args, options);
1020
+ }
1012
1021
  // A self-hosted check's own database (child-database.ts) is the command's to write too.
1013
1022
  const database = options?.env?.["TOOLROLL_DB"];
1014
1023
  const checkData = database === undefined || fenced.grants.writes === undefined ? null : grantFolder(dirname(database), false);
@@ -1020,12 +1029,14 @@ export function fencedCommands(store, runId, writes, inner, available = () => ma
1020
1029
  if (signIns !== null && "problem" in signIns)
1021
1030
  return { code: 1, stdout: "", stderr: `${signIns.problem}\n`, timedOut: false, notFound: false };
1022
1031
  try {
1032
+ // Each sign-in file looked at again right before the launch: one no longer itself isn't granted.
1033
+ // Never the keychain: the sign-ins kept there come in the environment, read outside (keychainSignIns).
1034
+ const given = signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network };
1035
+ stampLaunch(store, [runId], fencedStamp(signIns === null ? "sandboxed" : "sign-ins", fenced.fence, given, signIns === null ? undefined : network));
1023
1036
  return await inner(file, args, {
1024
1037
  ...options,
1025
1038
  fence: fenced.fence,
1026
- // Each sign-in file looked at again right before the launch: one no longer itself isn't granted.
1027
- // Never the keychain: the sign-ins kept there come in the environment, read outside (keychainSignIns).
1028
- fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network },
1039
+ fenceGrants: given,
1029
1040
  confinement: "required",
1030
1041
  env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp, ...(fenced.cache === undefined || fenced.grants.writes === undefined ? {} : buildCacheEnv(fenced.cache)), ...(signIns?.env ?? {}) },
1031
1042
  ...(signIns === null ? {} : { omitEnv: [...(options?.omitEnv ?? []), ...SIGN_INS_OMITTED_ENV] }),
@@ -1046,26 +1057,32 @@ export function fencedCommands(store, runId, writes, inner, available = () => ma
1046
1057
  * project's writes setting, only that checkout, its Git data, its own temp folder, the project's caches and its check
1047
1058
  * database writable. Where nothing can confine them, they run only when an instance operator allowed unconfined agents.
1048
1059
  */
1049
- export function fencedCheckoutCommands(store, repo, checkout, inner, available = () => macosFenceAvailable() || linuxFenceAvailable(), access = CHECK_ACCESS_DEFAULT, network, sandbox = LEGACY_SANDBOX) {
1060
+ export function fencedCheckoutCommands(store, repo, checkout, inner, available = () => macosFenceAvailable() || linuxFenceAvailable(), access = CHECK_ACCESS_DEFAULT, network, sandbox = LEGACY_SANDBOX, stampRuns = []) {
1050
1061
  if (inner !== runCommand)
1051
1062
  return inner;
1052
1063
  return async (file, args, options) => {
1053
1064
  if (!available()) {
1054
- if (store.unfencedAgentsAllowed())
1065
+ if (store.unfencedAgentsAllowed()) {
1066
+ stampLaunch(store, stampRuns, unfencedStamp());
1055
1067
  return inner(file, args, options);
1068
+ }
1056
1069
  return { code: 1, stdout: "", stderr: "This machine can't confine the project's checks (no macOS sandbox, no working bubblewrap), so they didn't run. Install bubblewrap, socat and ripgrep (Linux), or an instance operator allows unconfined agents with `toolroll config unconfined-agents allow`.\n", timedOut: false, notFound: false };
1057
1070
  }
1058
1071
  const databaseFile = store.databaseFile();
1059
- if (databaseFile === null)
1072
+ if (databaseFile === null) {
1073
+ stampLaunch(store, stampRuns, unfencedStamp());
1060
1074
  return inner(file, args, options);
1075
+ }
1061
1076
  const own = realpathSync(mkdtempSync(join(checkLaunchRoot(), "so-cmd-")));
1062
1077
  try {
1063
1078
  const temp = join(own, "tmp");
1064
1079
  mkdirSync(temp, { mode: 0o700 });
1065
1080
  // Not sandboxed (check-access.ts): the project's checks are trusted by the whole instance, as before 0.9.66. Never
1066
1081
  // while local mode is on (easy-start C5): then they run in the fence, sandboxed.
1067
- if (access === "unsandboxed" && !localModeOn(store))
1082
+ if (access === "unsandboxed" && !localModeOn(store)) {
1083
+ stampLaunch(store, stampRuns, unfencedStamp());
1068
1084
  return await inner(file, args, { ...options, env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp } });
1085
+ }
1069
1086
  let binding;
1070
1087
  try {
1071
1088
  binding = trustedWorktreeBinding(repo, checkout);
@@ -1091,12 +1108,14 @@ export function fencedCheckoutCommands(store, repo, checkout, inner, available =
1091
1108
  if (signIns !== null && "problem" in signIns)
1092
1109
  return { code: 1, stdout: "", stderr: `${signIns.problem}\n`, timedOut: false, notFound: false };
1093
1110
  try {
1111
+ // Each sign-in file looked at again right before the launch: one no longer itself isn't granted.
1112
+ // Never the keychain: the sign-ins kept there come in the environment, read outside (keychainSignIns).
1113
+ const given = signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network };
1114
+ stampLaunch(store, stampRuns, fencedStamp(signIns === null ? "sandboxed" : "sign-ins", launch.fence, given, signIns === null ? undefined : network));
1094
1115
  return await inner(file, args, {
1095
1116
  ...options,
1096
1117
  fence: launch.fence,
1097
- // Each sign-in file looked at again right before the launch: one no longer itself isn't granted.
1098
- // Never the keychain: the sign-ins kept there come in the environment, read outside (keychainSignIns).
1099
- fenceGrants: signIns === null ? grants : { ...grants, signIns: { ...signIns.grants, read: signIns.grants.read.filter(path => signInIsItself(realpathSync(homedir()), path)) }, network: signIns.network },
1118
+ fenceGrants: given,
1100
1119
  confinement: launch.confinement,
1101
1120
  env: { ...(options?.env ?? {}), TMPDIR: temp, TMP: temp, TEMP: temp, ...(allowed === null ? {} : buildCacheEnv(cache)), ...(signIns?.env ?? {}) },
1102
1121
  ...(signIns === null ? {} : { omitEnv: [...(options?.omitEnv ?? []), ...SIGN_INS_OMITTED_ENV] }),
@@ -1284,7 +1303,7 @@ export function checkoutRunner(store, repo, checkout, inner, runIds, ran, platfo
1284
1303
  return (file, args, options) => {
1285
1304
  const launch = checkoutLaunchOf(store, repo, runIds, platform);
1286
1305
  ran(actualCheckAccess(store, launch.access));
1287
- return fencedCheckoutCommands(store, repo, checkout, inner, undefined, launch.access, launch.network, checkoutSandboxOf(store, repo, runIds))(file, args, options);
1306
+ return fencedCheckoutCommands(store, repo, checkout, inner, undefined, launch.access, launch.network, checkoutSandboxOf(store, repo, runIds), runIds)(file, args, options);
1288
1307
  };
1289
1308
  }
1290
1309
  /**
@@ -1308,6 +1327,22 @@ export function checkoutSandboxOf(store, repo, runIds) {
1308
1327
  machLookup: common(one => one.machLookup, live.machLookup),
1309
1328
  };
1310
1329
  }
1330
+ /** A fenced launch's stamp: the level it runs at and exactly what it was given (check-stamp.ts), read from the grants the
1331
+ * launch itself takes, never from what was asked for. */
1332
+ export function fencedStamp(level, fence, grants, network) {
1333
+ const writes = grants.writes;
1334
+ return {
1335
+ version: CHECK_STAMP_VERSION, level, policyDigest: fenceDigest(fence, grants),
1336
+ writable: writes === undefined ? [EVERYWHERE] : [...new Set([...writes.subpaths, ...writes.prefixes.map(one => `${one.path}${one.suffix}`)])].sort(),
1337
+ readable: [...new Set([...grants.readable, ...(grants.signIns?.read ?? [])])].sort(),
1338
+ network: network === undefined ? { mode: "machine" } : { mode: network.mode, hosts: [...network.hosts].sort() },
1339
+ };
1340
+ }
1341
+ function stampLaunch(store, runs, stamp) {
1342
+ const now = new Date();
1343
+ for (const runId of runs)
1344
+ recordCheckStamp(store, runId, stamp, now);
1345
+ }
1311
1346
  /** The digest of a launch's whole fence (its denied paths and grants, canonical), as stamped on its run (C11). */
1312
1347
  export function fenceDigest(fence, grants) {
1313
1348
  return createHash("sha256").update(`toolroll:fence:${canonicalJson({ fence: [...fence], grants: grants })}`).digest("hex").slice(0, 32);
@@ -25,7 +25,9 @@ param(
25
25
  # a space or a percent sign the controller passed.
26
26
  [Parameter(Position = 1)] [string] $Encoded,
27
27
  [string] $JobName,
28
- [string] $QueryName
28
+ [string] $QueryName,
29
+ # A whole-job memory limit in bytes (the code map's bounded reads); 0 is none.
30
+ [long] $MemoryLimit = 0
29
31
  )
30
32
 
31
33
  $ErrorActionPreference = "Stop"
@@ -68,6 +70,7 @@ public static class SoJob {
68
70
  public const int JobObjectBasicAccountingInformation = 1;
69
71
  public const int JobObjectExtendedLimitInformation = 9;
70
72
  public const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000;
73
+ public const uint JOB_OBJECT_LIMIT_JOB_MEMORY = 0x200;
71
74
  public const uint CREATE_SUSPENDED = 0x4;
72
75
  public const uint CREATE_UNICODE_ENVIRONMENT = 0x400;
73
76
  public const int STARTF_USESTDHANDLES = 0x100;
@@ -111,7 +114,7 @@ public static class SoJob {
111
114
  catch { return "unknown"; }
112
115
  finally { CloseHandle(job); }
113
116
  }
114
- public static IntPtr MakeJob(string name) {
117
+ public static IntPtr MakeJob(string name, long memoryLimit) {
115
118
  IntPtr job = CreateJobObjectW(IntPtr.Zero, name);
116
119
  int createdError = Marshal.GetLastWin32Error();
117
120
  if (job != IntPtr.Zero && createdError == 183) { CloseHandle(job); throw new Exception("job name already exists"); }
@@ -119,6 +122,10 @@ public static class SoJob {
119
122
  var info = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
120
123
  // Kill on close; NO breakaway flags, so nothing inside may leave the job.
121
124
  info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
125
+ if (memoryLimit > 0) {
126
+ info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_JOB_MEMORY;
127
+ info.JobMemoryLimit = new UIntPtr((ulong)memoryLimit);
128
+ }
122
129
  int size = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
123
130
  IntPtr buffer = Marshal.AllocHGlobal(size);
124
131
  try {
@@ -177,7 +184,7 @@ try {
177
184
  [string[]]$targetArguments = ConvertFrom-Json -InputObject ([System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Encoded)))
178
185
  if ($targetArguments.Count -lt 1) { throw "the target argv is empty" }
179
186
  # Admission arrives only after the controller durably records this helper.
180
- $job = [SoJob]::MakeJob($JobName)
187
+ $job = [SoJob]::MakeJob($JobName, $MemoryLimit)
181
188
  Send "ready"
182
189
  $admission = $reader.ReadLineAsync()
183
190
  if (-not $admission.Wait(15000) -or $admission.Result -cne "go") { throw "job admission refused" }
@@ -0,0 +1,41 @@
1
+ /** One keymap for the console and the terminal UI (docs/plans/plain-toolroll.md P1.7). A shortcut only ever presses a
2
+ * control the page already rendered, the same press a person's click makes, so any password field or confirm it opens
3
+ * still appears; with no such control on the page the key does nothing. Shortcuts sleep in text fields, with a
4
+ * modifier held, and while the shortcuts list is open. Navigation keys go to a page and post nothing. */
5
+ /** What a key does in the console. `press`: the first visible, enabled control matching these, in order. */
6
+ export type WebShortcut = {
7
+ kind: "go";
8
+ href: string;
9
+ } | {
10
+ kind: "press";
11
+ selectors: readonly string[];
12
+ } | {
13
+ kind: "search";
14
+ } | {
15
+ kind: "help";
16
+ };
17
+ /** What a key does in the terminal UI, or null with the reason it has none there (a named gap). */
18
+ export type TuiShortcut = "new" | "ink" | "send-back" | "retry" | "list" | "help";
19
+ export type Shortcut = {
20
+ keys: string;
21
+ says: string;
22
+ web: WebShortcut;
23
+ tui: TuiShortcut | {
24
+ none: string;
25
+ };
26
+ };
27
+ /** The page's ink act: Approve (the approve control itself, password popover and all) or Accept. */
28
+ export declare const INK_ACT_SELECTORS: readonly ["[data-ink-approve]", '[data-ink-act="accept"]', "[data-accept-result][data-primary-action]"];
29
+ /** Send back: the control that opens the Send back form (never its submit: the note comes first). */
30
+ export declare const SEND_BACK_SELECTORS: readonly ['[data-act="request-changes"]', '[data-act="revise"]', 'a.result-feedback-link[href="#request-changes"]'];
31
+ /** Retry: the result's Retry, or the failed box's own Retry form. */
32
+ export declare const RETRY_SELECTORS: readonly ['[data-act="retry"]', 'form[data-retry] button[type="submit"]', 'form[data-rebuild] button[type="submit"]'];
33
+ export declare const SHORTCUTS: readonly Shortcut[];
34
+ /** The keys typed, as the help list shows them: "g h" reads "g then h". */
35
+ export declare const keyWords: (keys: string) => string;
36
+ /**
37
+ * The console's key routing, as an inline script (it runs inside chromeScript's closure, which defines `show` for the
38
+ * jump-to palette, `toggleHelp`, `helpOpen`, `close` and `go`). Generated from SHORTCUTS, so the table is the only
39
+ * place a key is defined.
40
+ */
41
+ export declare function webKeymapScript(): string;
package/dist/keymap.js ADDED
@@ -0,0 +1,51 @@
1
+ /** One keymap for the console and the terminal UI (docs/plans/plain-toolroll.md P1.7). A shortcut only ever presses a
2
+ * control the page already rendered, the same press a person's click makes, so any password field or confirm it opens
3
+ * still appears; with no such control on the page the key does nothing. Shortcuts sleep in text fields, with a
4
+ * modifier held, and while the shortcuts list is open. Navigation keys go to a page and post nothing. */
5
+ /** The page's ink act: Approve (the approve control itself, password popover and all) or Accept. */
6
+ export const INK_ACT_SELECTORS = ["[data-ink-approve]", '[data-ink-act="accept"]', "[data-accept-result][data-primary-action]"];
7
+ /** Send back: the control that opens the Send back form (never its submit: the note comes first). */
8
+ export const SEND_BACK_SELECTORS = ['[data-act="request-changes"]', '[data-act="revise"]', 'a.result-feedback-link[href="#request-changes"]'];
9
+ /** Retry: the result's Retry, or the failed box's own Retry form. */
10
+ export const RETRY_SELECTORS = ['[data-act="retry"]', 'form[data-retry] button[type="submit"]', 'form[data-rebuild] button[type="submit"]'];
11
+ export const SHORTCUTS = [
12
+ { keys: "n", says: "new task", web: { kind: "go", href: "/tasks/new" }, tui: "new" },
13
+ { keys: "a", says: "Approve or Accept (presses the button on the page)", web: { kind: "press", selectors: INK_ACT_SELECTORS }, tui: "ink" },
14
+ { keys: "b", says: "Send back (opens its note)", web: { kind: "press", selectors: SEND_BACK_SELECTORS }, tui: "send-back" },
15
+ { keys: "r", says: "Retry (presses the button on the page)", web: { kind: "press", selectors: RETRY_SELECTORS }, tui: "retry" },
16
+ { keys: "/", says: "search", web: { kind: "search" }, tui: { none: "The terminal has no search; press w to open the console." } },
17
+ { keys: "g h", says: "go Home", web: { kind: "go", href: "/" }, tui: "list" },
18
+ { keys: "g t", says: "go to Tasks", web: { kind: "go", href: "/tasks" }, tui: "list" },
19
+ { keys: "?", says: "these shortcuts", web: { kind: "help" }, tui: "help" },
20
+ ];
21
+ /** The keys typed, as the help list shows them: "g h" reads "g then h". */
22
+ export const keyWords = (keys) => keys.split(" ").join(" then ");
23
+ /**
24
+ * The console's key routing, as an inline script (it runs inside chromeScript's closure, which defines `show` for the
25
+ * jump-to palette, `toggleHelp`, `helpOpen`, `close` and `go`). Generated from SHORTCUTS, so the table is the only
26
+ * place a key is defined.
27
+ */
28
+ export function webKeymapScript() {
29
+ const single = {}, after = {};
30
+ for (const one of SHORTCUTS) {
31
+ const parts = one.keys.split(" ");
32
+ if (parts.length === 1)
33
+ single[parts[0]] = one.web;
34
+ else if (parts[0] === "g")
35
+ after[parts[1]] = one.web;
36
+ }
37
+ return `var KEYS=${JSON.stringify(single)},GKEYS=${JSON.stringify(after)};` +
38
+ // A control a person could press right now: rendered, shown, and not disabled.
39
+ `function pressable(el){if(!el||el.disabled||el.getAttribute("aria-disabled")==="true")return false;` +
40
+ `if(el.closest("[hidden],[inert]"))return false;var r=el.getClientRects();return r.length>0;}` +
41
+ // Press it exactly as a click would; a submit whose form isn't complete (an empty password, a required reason)
42
+ // shows the browser's own prompt for it instead of posting.
43
+ `function press(selectors){for(var s=0;s<selectors.length;s++){var all=document.querySelectorAll(selectors[s]);` +
44
+ `for(var i=0;i<all.length;i++){var el=all[i];if(!pressable(el))continue;` +
45
+ `var form=el.form||null;var submits=el.tagName.toLowerCase()==="button"&&(el.type==="submit"||el.getAttribute("type")===null)&&form;` +
46
+ `if(submits&&!form.checkValidity()){form.reportValidity();return true;}` +
47
+ `el.click();return true;}}return false;}` +
48
+ `function search(){if(document.querySelector("[data-workspace-shell]")){window.dispatchEvent(new CustomEvent("so:open-search",{detail:document.activeElement}));return;}show();}` +
49
+ `function run(act){if(act.kind==="go"){go(act.href);return true;}if(act.kind==="press")return press(act.selectors);` +
50
+ `if(act.kind==="search"){search();return true;}if(act.kind==="help"){toggleHelp();return true;}return false;}`;
51
+ }
@@ -112,7 +112,7 @@ export async function runKnowledgeCommand(positional, options, context) {
112
112
  admittedToken = token;
113
113
  }
114
114
  const cacheRoot = context.evidenceRoot ? join(dirname(context.evidenceRoot), 'repository-context') : namedPath(join(homedir(), '.cache'), ['repository-context']);
115
- const result = await repositoryContext({ repo: resolve(repo), query, mode: spec.action === 'impact' ? 'impact' : 'search', refresh: spec.action === 'refresh', cacheRoot, ...(typeof base === 'string' ? { baseRevision: base } : {}) });
115
+ const result = await repositoryContext({ repo: resolve(repo), ...(admittedToken ? { project: resolve(repo) } : {}), query, mode: spec.action === 'impact' ? 'impact' : 'search', refresh: spec.action === 'refresh', cacheRoot, ...(typeof base === 'string' ? { baseRevision: base } : {}) });
116
116
  if (admittedToken) {
117
117
  const current = authenticateCoordinator(context.store, admittedToken);
118
118
  if (!current.ok)
@@ -1,17 +1,18 @@
1
1
  import { type KnowledgeDraft, type KnowledgeSelection, type KnowledgeView } from './project-knowledge.js';
2
2
  import { type Html } from './html.js';
3
+ import { type MemoryUse } from './memory-use.js';
3
4
  /** A section as a frame (after ReUI's Frame, MIT): its heading on the raised well, its body one paper panel. The
4
5
  * cards inside become hairline-divided rows of that panel, never cards in a card. */
5
6
  export declare function framedSection(heading: string, body: Html | string, extra?: string): Html;
6
7
  export declare const KNOWLEDGE_CSS = ".knowledge{max-width:780px;min-width:0;overflow-wrap:anywhere}.knowledge .card{padding:20px;min-width:0;margin:16px 0}.knowledge h2{margin:0 0 12px}.knowledge form{display:grid;gap:12px;margin:0}.knowledge label{display:grid;gap:6px}.knowledge textarea,.knowledge input,.knowledge select{box-sizing:border-box;width:100%;min-width:0;max-width:100%}.knowledge textarea{resize:vertical;min-height:110px}.knowledge button,.knowledge summary,.knowledge .button-link{min-height:44px}.knowledge button{justify-self:start;white-space:nowrap}.knowledge summary{padding:12px 0;cursor:pointer}.knowledge pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}.knowledge .knowledge-actions{display:flex;gap:12px;flex-wrap:wrap}.knowledge .meta{font-size:.86rem}.knowledge .knowledge-source{display:block}.knowledge details+details{border-top:1px solid var(--so-line)}.memory-stats{display:flex;flex-wrap:wrap;gap:.4rem}.memory-stats span+span::before{content:\"\u00B7\";margin-right:.4rem}.knowledge.frame{margin:0 0 16px}.knowledge.frame>.frame-head h2,.knowledge.frame>summary.frame-head{margin:0;font-size:13px;font-weight:600;line-height:1.4}.knowledge.frame>summary.frame-head{min-height:44px;padding:4px 10px}.knowledge.frame>.frame-panel{min-width:0}.knowledge.frame>.frame-panel>:first-child{margin-top:0}.knowledge.frame>.frame-panel>:last-child{margin-bottom:0}.knowledge .frame-panel>details.card,.knowledge .frame-panel .learning .card{margin:0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.knowledge .frame-panel>details.card+details.card,.knowledge .frame-panel .learning .card+.card{border-top:1px solid var(--so-line);padding-top:4px}.knowledge.frame>summary.frame-head{justify-content:flex-start}.knowledge form.field-row{grid-template-columns:minmax(0,1fr) auto;align-items:end}.knowledge form.field-row>label,.knowledge form.field-row :is(input,select,button){margin:0}@media(max-width:600px){.knowledge .card{padding:16px}.knowledge h2{font-size:1.1rem}}";
7
- export declare function knowledgeHtml(view: KnowledgeView, csrf: string, canManage: boolean, draft?: KnowledgeDraft, error?: string): Html;
8
+ export declare function knowledgeHtml(view: KnowledgeView, csrf: string, canManage: boolean, draft?: KnowledgeDraft, error?: string, use?: ReadonlyMap<string, MemoryUse>): Html;
8
9
  export declare function knowledgeContextHtml(snapshot: KnowledgeSelection | null): Html | '';
9
10
  import type { Decision, MemoryHit } from './project-memory.js';
10
- export declare const MEMORY_INTRO = "What the lead and crew read before they work. Every change keeps its history.";
11
+ export declare const MEMORY_INTRO = "What the lead and crew read before they work. Every change keeps its history. Last included is counted in runs.";
11
12
  export declare function decisionsHtml(repo: string, decisions: Decision[], csrf: string, canManage: boolean, draft?: {
12
13
  claim?: string;
13
14
  why?: string;
14
- }, error?: string): Html;
15
+ }, error?: string, use?: ReadonlyMap<string, MemoryUse>, afterwards?: (decision: Decision) => string | null): Html;
15
16
  export declare function memorySearchHtml(repo: string, query: string, hits: MemoryHit[] | null): Html;
16
17
  import type { MemoryProposal } from './memory-pass.js';
17
18
  export declare function proposalsHtml(repo: string, proposals: MemoryProposal[], status: {
@@ -25,4 +26,20 @@ export declare function proposalsHtml(repo: string, proposals: MemoryProposal[],
25
26
  };
26
27
  openGaps: number;
27
28
  pending: number;
28
- } | null, csrf: string, canManage: boolean): Html;
29
+ } | null, csrf: string, canManage: boolean, who?: string): Html;
30
+ import type { TidyProposal } from './memory-tidy.js';
31
+ export declare function cleanUpHtml(repo: string, proposals: readonly TidyProposal[], manage: boolean): Html;
32
+ /** On a saved version's page: the references that aren't in current knowledge, each back as a new version. */
33
+ export declare function referenceRestoreHtml(repo: string, revision: number, references: readonly {
34
+ id: string;
35
+ title: string;
36
+ }[], manage: boolean): Html | '';
37
+ /** Retired decisions, undoable merges and disabled lessons, each with its way back. */
38
+ export declare function restoreHtml(repo: string, retired: readonly Decision[], merges: readonly {
39
+ merge: number;
40
+ successor: number;
41
+ predecessors: number[];
42
+ }[], lessons: readonly {
43
+ id: number;
44
+ observation: string;
45
+ }[], manage: boolean): Html | '';
@@ -1,6 +1,7 @@
1
1
  import { STALE_KNOWLEDGE } from './project-knowledge.js';
2
2
  import { html, joinHtml, postForm } from './html.js';
3
- import { acts, btn, codeLine, disclose, formRow, input, note, notice, settingsLink, settingsRecord, settingsSection, textarea } from './server/kit.js';
3
+ import { lastIncludedWords } from './memory-use.js';
4
+ import { acts, btn, codeLine, disclose, formRow, input, note, notice, radios, settingsLink, settingsRecord, settingsSection, textarea } from './server/kit.js';
4
5
  // Hidden fields as they always were: a missing value is an empty field.
5
6
  const fields = (data) => Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v ?? '')]));
6
7
  /** A section as a frame (after ReUI's Frame, MIT): its heading on the raised well, its body one paper panel. The
@@ -9,7 +10,7 @@ export function framedSection(heading, body, extra = '') {
9
10
  return html `<section class="${`knowledge frame${extra ? ` ${extra}` : ''}`}"><div class="frame-head"><h2>${heading}</h2></div><div class="frame-panel">${body}</div></section>`;
10
11
  }
11
12
  export const KNOWLEDGE_CSS = `.knowledge{max-width:780px;min-width:0;overflow-wrap:anywhere}.knowledge .card{padding:20px;min-width:0;margin:16px 0}.knowledge h2{margin:0 0 12px}.knowledge form{display:grid;gap:12px;margin:0}.knowledge label{display:grid;gap:6px}.knowledge textarea,.knowledge input,.knowledge select{box-sizing:border-box;width:100%;min-width:0;max-width:100%}.knowledge textarea{resize:vertical;min-height:110px}.knowledge button,.knowledge summary,.knowledge .button-link{min-height:44px}.knowledge button{justify-self:start;white-space:nowrap}.knowledge summary{padding:12px 0;cursor:pointer}.knowledge pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}.knowledge .knowledge-actions{display:flex;gap:12px;flex-wrap:wrap}.knowledge .meta{font-size:.86rem}.knowledge .knowledge-source{display:block}.knowledge details+details{border-top:1px solid var(--so-line)}.memory-stats{display:flex;flex-wrap:wrap;gap:.4rem}.memory-stats span+span::before{content:"·";margin-right:.4rem}.knowledge.frame{margin:0 0 16px}.knowledge.frame>.frame-head h2,.knowledge.frame>summary.frame-head{margin:0;font-size:13px;font-weight:600;line-height:1.4}.knowledge.frame>summary.frame-head{min-height:44px;padding:4px 10px}.knowledge.frame>.frame-panel{min-width:0}.knowledge.frame>.frame-panel>:first-child{margin-top:0}.knowledge.frame>.frame-panel>:last-child{margin-bottom:0}.knowledge .frame-panel>details.card,.knowledge .frame-panel .learning .card{margin:0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.knowledge .frame-panel>details.card+details.card,.knowledge .frame-panel .learning .card+.card{border-top:1px solid var(--so-line);padding-top:4px}.knowledge.frame>summary.frame-head{justify-content:flex-start}.knowledge form.field-row{grid-template-columns:minmax(0,1fr) auto;align-items:end}.knowledge form.field-row>label,.knowledge form.field-row :is(input,select,button){margin:0}@media(max-width:600px){.knowledge .card{padding:16px}.knowledge h2{font-size:1.1rem}}`;
12
- export function knowledgeHtml(view, csrf, canManage, draft, error) {
13
+ export function knowledgeHtml(view, csrf, canManage, draft, error, use = new Map()) {
13
14
  const manage = canManage && !!csrf;
14
15
  const base = { repo: view.repo, identity: view.identity, revision: view.revision };
15
16
  const form = (action, body, extra = {}) => postForm('/settings/knowledge/change', body, { hidden: fields({ ...base, action, ...extra }) });
@@ -22,6 +23,7 @@ export function knowledgeHtml(view, csrf, canManage, draft, error) {
22
23
  const refs = view.knowledge.references.map(ref => settingsRecord({
23
24
  title: ref.title,
24
25
  sub: ref.path ? html `<span class="k-mono">${ref.path} · ${ref.sourceRevision?.slice(0, 7)}</span>` : '',
26
+ when: lastIncludedWords(use.get(`reference:${ref.id}`)),
25
27
  more: joinHtml([
26
28
  disclose('Read reference', html `<pre class="k-scode">${ref.content}</pre>`, draft?.id === ref.id),
27
29
  manage ? disclose('Edit reference', joinHtml([
@@ -59,14 +61,16 @@ export function knowledgeContextHtml(snapshot) {
59
61
  return '';
60
62
  return html `<details class="knowledge result-knowledge"><summary>Context used</summary><p class="meta">Project knowledge · version ${snapshot.revision}${snapshot.inheritedFrom === null ? '' : ` · from run #${snapshot.inheritedFrom}`}</p>${snapshot.instructions ? html `<details><summary>Instructions</summary><pre>${snapshot.instructions}</pre></details>` : html `<p>No project instructions were supplied.</p>`}${snapshot.references.map(r => html `<details><summary>${r.title}</summary>${r.path ? html `<p class="meta">${r.path} · ${r.sourceRevision}</p>` : ''}<pre>${r.content}</pre></details>`)}${snapshot.omitted.length ? html `<details><summary>Not included (${snapshot.omitted.length})</summary><ul>${snapshot.omitted.map(r => html `<li>${r.title} — ${r.reason}</li>`)}</ul></details>` : ''}</details>`;
61
63
  }
62
- export const MEMORY_INTRO = 'What the lead and crew read before they work. Every change keeps its history.';
63
- export function decisionsHtml(repo, decisions, csrf, canManage, draft, error) {
64
+ export const MEMORY_INTRO = 'What the lead and crew read before they work. Every change keeps its history. Last included is counted in runs.';
65
+ export function decisionsHtml(repo, decisions, csrf, canManage, draft, error, use = new Map(), afterwards = () => null) {
64
66
  const manage = canManage && !!csrf;
65
67
  const base = { repo };
66
68
  const form = (action, body, extra = {}) => postForm('/settings/knowledge/decision', body, { hidden: fields({ ...base, action, ...extra }) });
67
69
  const rows = decisions.map(d => settingsRecord({
68
70
  title: d.claim, sub: d.why, when: html `<span class="k-num">#${d.id}</span> · ${d.decidedBy} · ${d.decidedAt.slice(0, 10)}`, attrs: { id: `decision-${d.id}` },
69
71
  more: joinHtml([
72
+ note(lastIncludedWords(use.get(`decision:${d.id}`))),
73
+ afterwards(d) === null ? '' : note(afterwards(d)),
70
74
  note(`${d.sourceKind}${d.sourceRef ? ` · ${d.sourceRef}` : ''}${d.supersedes === null ? '' : ` · replaces #${d.supersedes}`}`),
71
75
  manage ? disclose('Retire', form('retire', joinHtml([
72
76
  formRow('Why it no longer holds', null, input({ id: `decision-${d.id}-reason`, name: 'reason', attrs: { maxlength: 500, required: true, 'aria-label': 'Why it no longer holds' } }), 'wide'),
@@ -89,14 +93,30 @@ export function memorySearchHtml(repo, query, hits) {
89
93
  results,
90
94
  ]) });
91
95
  }
92
- export function proposalsHtml(repo, proposals, status, csrf, canManage) {
96
+ import { NOTE_SURFACE } from './lesson-notes.js';
97
+ export function proposalsHtml(repo, proposals, status, csrf, canManage, who = '') {
93
98
  const manage = canManage && !!csrf;
94
99
  const line = status === null ? '' : joinHtml([
95
100
  note(`${status.sessions.analyzed}/${status.sessions.total} sessions read${status.sessions.failed ? ` · ${status.sessions.failed} failed` : ''} · ${status.openGaps} open gap${status.openGaps === 1 ? '' : 's'} · Instructions ${status.instructionBytes}/${status.budgetBytes} bytes`),
96
101
  disclose('Read new sessions', joinHtml([note('Run this on this computer:'), codeLine(`toolroll memory propose --repo ${repo}`)])),
97
102
  ]);
98
103
  const decide = (p, decision, label) => postForm('/settings/knowledge/proposal', btn(label, { sm: true, variant: decision === 'accept' ? 'outline' : 'ghost', type: 'submit' }), { hidden: fields({ repo, proposal: p.id, decision }) });
99
- const cards = proposals.map(p => settingsRecord({
104
+ // A lesson note: the facts, the failure line as output, and the person's own words in an edit box that starts as a draft.
105
+ const lessonNote = (p) => settingsRecord({
106
+ title: p.title, sub: html `<span class="k-num">#${p.id}</span> · Lesson note`,
107
+ more: joinHtml([
108
+ note(p.rationale),
109
+ ...p.evidence.map(ev => html `<pre class="k-scode">${ev.quote}</pre>`),
110
+ manage ? joinHtml([
111
+ postForm('/settings/knowledge/note', joinHtml([
112
+ formRow('Note', 'Say it in your own words before accepting.', textarea({ id: `note-${p.id}`, name: 'claim', rows: 3, value: p.afterText, attrs: { maxlength: 240, required: true, 'aria-label': 'Note' } }), 'top'),
113
+ acts(btn(`Accept as ${who}`, { sm: true, type: 'submit' })),
114
+ ]), { hidden: fields({ repo, proposal: p.id, decision: 'accept' }) }),
115
+ postForm('/settings/knowledge/note', btn('Reject', { sm: true, variant: 'ghost', type: 'submit' }), { hidden: fields({ repo, proposal: p.id, decision: 'reject' }) }),
116
+ ]) : '',
117
+ ]),
118
+ });
119
+ const cards = proposals.map(p => p.surface === NOTE_SURFACE ? lessonNote(p) : settingsRecord({
100
120
  title: p.title, sub: html `<span class="k-num">#${p.id}</span> · ${p.kind} · ${p.sessions} sessions`,
101
121
  action: manage ? html `${decide(p, 'accept', 'Accept')}${decide(p, 'reject', 'Reject')}` : '',
102
122
  more: joinHtml([
@@ -106,5 +126,53 @@ export function proposalsHtml(repo, proposals, status, csrf, canManage) {
106
126
  html `<ul class="k-note">${p.evidence.map(ev => html `<li>“${ev.quote.slice(0, 200)}” · ${ev.session.split(':')[0] ?? ''}</li>`)}</ul>`,
107
127
  ]),
108
128
  }));
109
- return settingsSection({ id: 'proposals', title: 'Proposed from sessions', body: joinHtml([line, cards.length ? joinHtml(cards) : note('Nothing to review. Suggestions appear when the same mistake shows up in two sessions.')]) });
129
+ return settingsSection({ id: 'proposals', title: 'Proposed changes', body: joinHtml([line, cards.length ? joinHtml(cards) : note('Nothing to review. Suggestions appear when the same mistake shows up in two sessions.')]) });
130
+ }
131
+ const CLEANUP_TITLES = { retire: 'Retire unused', merge: 'Merge duplicates', contradiction: 'Contradiction', 'note-failing': 'Note keeps failing', restore: 'Restore', unmerge: 'Undo merge' };
132
+ const cleanupItem = (p, item) => item.kind === 'decision' ? `Decision #${item.id} · version ${item.revision} · ${item.status}${p.titles[`decision:${item.id}`] ? ` · ${p.titles[`decision:${item.id}`]}` : ''}`
133
+ : item.kind === 'lesson' ? `Lesson #${item.id} · version ${item.version}` : `Reference · knowledge version ${item.revision}`;
134
+ /** Radio choices for a proposal with no default: nothing is pre-selected. */
135
+ const choices = (name, options) => radios({ name, legend: 'Choose one', current: '', items: options.map(([value, label]) => ({ value, label })) });
136
+ export function cleanUpHtml(repo, proposals, manage) {
137
+ const form = (body, hidden) => postForm('/settings/knowledge/tidy', body, { hidden: fields({ repo, ...hidden }) });
138
+ const accept = (p) => {
139
+ const tail = Array.isArray(p.evidence['tail']) ? p.evidence['tail'] : [];
140
+ const decisions = p.items.flatMap(item => item.kind === 'decision' ? [item.id] : []);
141
+ const extra = p.kind === 'contradiction' ? joinHtml([choices('choice', [...decisions.map(id => [`keep:${id}`, `Keep #${id}, retire the other`]), ['both', 'Keep both'], ['new', 'Write a new one']]),
142
+ formRow('New decision', null, input({ id: `tidy-${p.id}-claim`, name: 'claim', attrs: { maxlength: 240, 'aria-label': 'New decision' } }), 'wide'), formRow('Why', null, input({ id: `tidy-${p.id}-why`, name: 'why', attrs: { maxlength: 500, 'aria-label': 'Why' } }), 'wide')])
143
+ : p.kind === 'note-failing' ? joinHtml([choices('choice', [['rewrite', 'Rewrite it'], ['retire', 'Retire it']]),
144
+ formRow('New note', null, input({ id: `tidy-${p.id}-claim`, name: 'claim', attrs: { maxlength: 240, 'aria-label': 'New note' } }), 'wide'), formRow('Why', null, input({ id: `tidy-${p.id}-why`, name: 'why', attrs: { maxlength: 500, 'aria-label': 'Why' } }), 'wide')])
145
+ : (p.kind === 'restore' || p.kind === 'unmerge') && tail.length > 0 ? choices('choice', [['retire', `Retire ${tail.map(id => `#${id}`).join(', ')}`], ['keep', 'Keep both active']])
146
+ : p.kind === 'merge' ? formRow('Merged decision', null, input({ id: `tidy-${p.id}-claim`, name: 'claim', value: p.afterText ?? '', attrs: { maxlength: 240, 'aria-label': 'Merged decision' } }), 'wide') : '';
147
+ return form(joinHtml([extra, acts(btn('Accept', { sm: true, type: 'submit' }))]), { action: 'accept', proposal: p.id });
148
+ };
149
+ const rows = proposals.map(p => settingsRecord({
150
+ title: CLEANUP_TITLES[p.kind], sub: p.rationale,
151
+ more: joinHtml([
152
+ html `<ul class="k-note">${p.items.map(item => html `<li>${cleanupItem(p, item)}</li>`)}</ul>`,
153
+ manage ? joinHtml([accept(p), form(btn('Reject', { sm: true, variant: 'ghost', type: 'submit' }), { action: 'reject', proposal: p.id })]) : '',
154
+ ]),
155
+ }));
156
+ return settingsSection({ id: 'clean-up', title: 'Clean up', body: joinHtml([
157
+ rows.length ? joinHtml(rows) : note('Nothing to clean up.'),
158
+ manage ? form(acts(btn('Clean up', { sm: true, type: 'submit' })), { action: 'propose' }) : '',
159
+ ]) });
160
+ }
161
+ /** On a saved version's page: the references that aren't in current knowledge, each back as a new version. */
162
+ export function referenceRestoreHtml(repo, revision, references, manage) {
163
+ if (!manage || references.length === 0)
164
+ return '';
165
+ return settingsSection({ id: 'reference-restore', title: 'References no longer in use', body: joinHtml(references.map(ref => settingsRecord({ title: ref.title,
166
+ action: postForm('/settings/knowledge/tidy', btn('Restore', { sm: true, variant: 'ghost', type: 'submit' }), { hidden: fields({ repo, action: 'reference', target: ref.id, revision }) }) }))) });
167
+ }
168
+ /** Retired decisions, undoable merges and disabled lessons, each with its way back. */
169
+ export function restoreHtml(repo, retired, merges, lessons, manage) {
170
+ if (retired.length + merges.length + lessons.length === 0)
171
+ return '';
172
+ const form = (label, action, target) => manage ? postForm('/settings/knowledge/tidy', btn(label, { sm: true, variant: 'ghost', type: 'submit' }), { hidden: fields({ repo, action, target }) }) : '';
173
+ return html `<details class="knowledge frame"><summary class="frame-head">Retired</summary><div class="frame-panel">${joinHtml([
174
+ ...merges.map(m => settingsRecord({ title: `Merge of ${m.predecessors.map(id => `#${id}`).join(', ')} into #${m.successor}`, action: form('Undo merge', 'unmerge', m.merge) })),
175
+ ...retired.map(d => settingsRecord({ title: d.claim, sub: html `<span class="k-num">#${d.id}</span> · ${d.status}`, action: form('Restore', 'restore', d.id) })),
176
+ ...lessons.map(l => settingsRecord({ title: l.observation, sub: 'Lesson', action: form('Use again', 'readopt', l.id) })),
177
+ ])}</div></details>`;
110
178
  }
@@ -51,6 +51,8 @@ export type LaptopSeams = {
51
51
  host?: string;
52
52
  healthTimeoutMs?: number;
53
53
  exitTimeoutMs?: number;
54
+ /** The build an installed unit would start, from its entry point (default: the version in that package's package.json). */
55
+ buildAt?: (entry: string) => string | null;
54
56
  /** The foreground `up` this invoker started (the terminal UI's child): its controller's pid, and how to start it again. */
55
57
  foreground?: {
56
58
  controllerPid: number;
@@ -168,4 +170,18 @@ export declare function backgroundInstalled(seams?: LaptopSeams): boolean;
168
170
  * `up` beside it — and prove the console it runs live. Our own label only.
169
171
  */
170
172
  export declare function startInstalledService(store: Store, databaseFile: string, seams?: LaptopSeams): Promise<AttachProof>;
173
+ export type BuilderRestart = {
174
+ restarted: true;
175
+ } | {
176
+ restarted: false;
177
+ reason: "unsupported" | "not-installed" | "not-wanted" | "paused" | "running" | "unproven" | "disabled" | "identity" | "attempts" | "failed";
178
+ message?: string;
179
+ };
180
+ /**
181
+ * Start our own service again, by Toolroll alone, only when a person's recorded wish is to run, nothing is stopping or
182
+ * updating, the old process is proved gone, what would start is what ran, and it isn't over its attempts. Every other
183
+ * answer leaves the need with one label, "Start the builder". This never enables a disabled label, which is what sets it
184
+ * apart from `startService`: a person's own yes.
185
+ */
186
+ export declare function restartOwnBuilder(store: Store, databaseFile: string, seams?: LaptopSeams, now?: Date): Promise<BuilderRestart>;
171
187
  export {};