@bongos/core 1.21.112 → 1.21.114

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 (265) hide show
  1. package/.bongos-core.json +616 -186
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +1 -1
  4. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  5. package/.claude/skills/builder-ship/SKILL.md +1 -0
  6. package/GOVERNMENT.md +128 -11
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +28 -0
  9. package/clients/bongos-client/index.cjs +28 -0
  10. package/clients/bongos-client/index.d.ts +45 -2
  11. package/clients/bongos-client/index.mjs +28 -0
  12. package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
  13. package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
  14. package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
  15. package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
  16. package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
  17. package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
  18. package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
  19. package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
  20. package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
  21. package/docs/adr/README.md +10 -1
  22. package/docs/api/openapi.json +989 -87
  23. package/docs/api-reference.md +18 -4
  24. package/docs/copy-inventory.md +343 -262
  25. package/docs/copy-registry.json +1223 -426
  26. package/docs/file-map.md +2 -1
  27. package/docs/module-api-changelog.md +4 -0
  28. package/docs/packs/engineer.md +2 -1
  29. package/docs/page-inventory.json +91 -6
  30. package/docs/page-readings.json +5017 -4458
  31. package/modules/builder-settings/law-locks.js +202 -0
  32. package/modules/builder-settings/routes/settings.js +66 -6
  33. package/modules/builder-settings/settings.js +7 -0
  34. package/modules/discord/ship-broadcast.js +22 -13
  35. package/modules/government/asks-db.js +54 -0
  36. package/modules/government/asks.js +269 -0
  37. package/modules/government/board-idea-reads.js +68 -0
  38. package/modules/government/board.js +21 -57
  39. package/modules/government/build-rights.js +148 -0
  40. package/modules/government/buildouts.js +160 -0
  41. package/modules/government/counts.js +81 -0
  42. package/modules/government/db.js +242 -0
  43. package/modules/government/decision-board.js +234 -8
  44. package/modules/government/decision-rules.js +2 -0
  45. package/modules/government/docket.js +114 -6
  46. package/modules/government/law-board.js +131 -0
  47. package/modules/government/laws.js +241 -0
  48. package/modules/government/leads.js +170 -0
  49. package/modules/government/migrations/government_027_laws.sql +143 -0
  50. package/modules/government/migrations/government_028_asks.sql +57 -0
  51. package/modules/government/migrations/government_029_leads.sql +135 -0
  52. package/modules/government/migrations/government_030_buildouts.sql +111 -0
  53. package/modules/government/migrations/government_031_treasury.sql +107 -0
  54. package/modules/government/module.json +2 -1
  55. package/modules/government/pollers/treasury-borrow.js +62 -0
  56. package/modules/government/positions.js +107 -0
  57. package/modules/government/presets.js +149 -0
  58. package/modules/government/routes/government.js +425 -2
  59. package/modules/government/routing.js +154 -0
  60. package/modules/government/treasury-db.js +166 -0
  61. package/modules/government/treasury.js +241 -0
  62. package/modules/hall-ui/public/atlas.html +2 -2
  63. package/modules/hall-ui/public/blockers.html +2 -2
  64. package/modules/hall-ui/public/board-room.html +2 -2
  65. package/modules/hall-ui/public/board-room.js +2 -0
  66. package/modules/hall-ui/public/city-draw.js +604 -11
  67. package/modules/hall-ui/public/city.css +157 -1
  68. package/modules/hall-ui/public/city.html +37 -7
  69. package/modules/hall-ui/public/city.js +441 -21
  70. package/modules/hall-ui/public/city.states.json +110 -5
  71. package/modules/hall-ui/public/collab.html +2 -2
  72. package/modules/hall-ui/public/commands.html +2 -2
  73. package/modules/hall-ui/public/copy-desk.html +2 -2
  74. package/modules/hall-ui/public/craft-mode.js +112 -29
  75. package/modules/hall-ui/public/deploy.html +2 -2
  76. package/modules/hall-ui/public/diagram-viewer.css +12 -0
  77. package/modules/hall-ui/public/diagram-viewer.js +4 -0
  78. package/modules/hall-ui/public/diagrams.html +2 -2
  79. package/modules/hall-ui/public/drachmae.html +2 -2
  80. package/modules/hall-ui/public/fleet.html +2 -2
  81. package/modules/hall-ui/public/gate.html +2 -2
  82. package/modules/hall-ui/public/goal-map.html +2 -2
  83. package/modules/hall-ui/public/goals.html +2 -2
  84. package/modules/hall-ui/public/gov-feeds.js +141 -0
  85. package/modules/hall-ui/public/gov-living.js +1002 -0
  86. package/modules/hall-ui/public/gov-positions.js +97 -0
  87. package/modules/hall-ui/public/government.html +2 -2
  88. package/modules/hall-ui/public/hall-render.js +3 -0
  89. package/modules/hall-ui/public/idea.html +2 -2
  90. package/modules/hall-ui/public/ideas.html +2 -2
  91. package/modules/hall-ui/public/index.html +3 -3
  92. package/modules/hall-ui/public/modules.html +2 -2
  93. package/modules/hall-ui/public/my-agents.css +334 -0
  94. package/modules/hall-ui/public/my-agents.html +72 -0
  95. package/modules/hall-ui/public/my-agents.js +603 -0
  96. package/modules/hall-ui/public/my-agents.states.json +37 -0
  97. package/modules/hall-ui/public/primer.html +2 -2
  98. package/modules/hall-ui/public/profile-connections.js +40 -28
  99. package/modules/hall-ui/public/profile.html +2 -2
  100. package/modules/hall-ui/public/project-settings.html +2 -2
  101. package/modules/hall-ui/public/ranks.html +2 -2
  102. package/modules/hall-ui/public/roadmap.html +2 -2
  103. package/modules/hall-ui/public/room.css +8 -0
  104. package/modules/hall-ui/public/roster.html +2 -2
  105. package/modules/hall-ui/public/sessions.html +2 -2
  106. package/modules/hall-ui/public/settings.html +2 -2
  107. package/modules/hall-ui/public/shell.js +116 -16
  108. package/modules/hall-ui/public/street-card.js +18 -1
  109. package/modules/hall-ui/public/street-deck.css +299 -0
  110. package/modules/hall-ui/public/street-deck.js +1145 -0
  111. package/modules/hall-ui/public/street-depth.js +712 -0
  112. package/modules/hall-ui/public/street-desk.js +5 -2
  113. package/modules/hall-ui/public/street-draw.js +109 -16
  114. package/modules/hall-ui/public/street-inside.js +137 -22
  115. package/modules/hall-ui/public/street-office.js +705 -0
  116. package/modules/hall-ui/public/street.css +161 -11
  117. package/modules/hall-ui/public/street.html +41 -11
  118. package/modules/hall-ui/public/street.js +78 -66
  119. package/modules/hall-ui/public/street.states.json +129 -21
  120. package/modules/hall-ui/public/studio.html +2 -2
  121. package/modules/hall-ui/public/style.css +5 -1
  122. package/modules/hall-ui/public/task.html +2 -2
  123. package/modules/hall-ui/public/thinking.html +2 -2
  124. package/modules/hall-ui/public/tweak-editor.html +2 -2
  125. package/modules/hall-ui/public/watch.html +2 -2
  126. package/modules/hall-ui/public/watch.js +14 -1
  127. package/modules/hall-ui/public/work.html +2 -2
  128. package/modules/hall-ui/records/living-city.md +57 -0
  129. package/modules/hall-ui/records/my-agents.md +21 -0
  130. package/modules/hall-ui/records/oversight-family.md +1 -0
  131. package/modules/hall-ui/records/profile-connections.md +10 -0
  132. package/modules/hall-ui/records/reading-rooms.md +2 -0
  133. package/modules/ideas/routes/blockers.js +92 -3
  134. package/modules/lifecycle/module.json +2 -1
  135. package/modules/lifecycle/moving.js +17 -1
  136. package/modules/lifecycle/routes/moving.js +6 -0
  137. package/modules/lifecycle/routes/project-settings.js +23 -0
  138. package/modules/onboarding/approval-broadcast.js +8 -0
  139. package/modules/onboarding/routes/access-requests.js +16 -6
  140. package/modules/platform-identity/connections.js +13 -2
  141. package/modules/platform-identity/routes/connections.js +7 -1
  142. package/modules/public-landing/public/account-private.states.json +2 -2
  143. package/modules/public-landing/public/account.html +65 -5
  144. package/modules/public-landing/public/account.states.json +3 -3
  145. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  146. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  147. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
  148. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  149. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  150. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
  151. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  152. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  153. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
  154. package/modules/public-landing/public/assets/world.css +4 -4
  155. package/modules/public-landing/public/connections-empty.states.json +14 -0
  156. package/modules/public-landing/public/connections-inactive.states.json +14 -0
  157. package/modules/public-landing/public/connections.html +450 -0
  158. package/modules/public-landing/public/connections.probes.json +414 -0
  159. package/modules/public-landing/public/connections.states.json +17 -0
  160. package/modules/public-landing/public/contact.html +2 -2
  161. package/modules/public-landing/public/guild.html +2 -2
  162. package/modules/public-landing/public/index.html +2 -2
  163. package/modules/public-landing/public/invitations.html +2 -2
  164. package/modules/public-landing/public/privacy.html +2 -2
  165. package/modules/public-landing/public/projects.html +2 -2
  166. package/modules/public-landing/public/projects.probes.json +6 -2
  167. package/modules/public-landing/public/terms.html +2 -2
  168. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  169. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  170. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  171. package/modules/ui-design/kit/serve.js +95 -0
  172. package/package-lock.json +2 -2
  173. package/package.json +1 -1
  174. package/release-notes.json +128 -0
  175. package/scripts/discord/README.md +8 -5
  176. package/scripts/discord/post.js +12 -1
  177. package/scripts/gds/artifact-audit.js +20 -3
  178. package/scripts/gds/ask.js +164 -0
  179. package/scripts/gds/autobongos-run.js +20 -5
  180. package/scripts/gds/autobongos-tweak-lane.js +262 -0
  181. package/scripts/gds/autobongos-verify.js +21 -2
  182. package/scripts/gds/claim.js +2 -0
  183. package/scripts/gds/government-walk.js +638 -4
  184. package/scripts/gds/pulse.js +9 -6
  185. package/scripts/gds/ship-escalation.js +22 -3
  186. package/scripts/gds/ship-finish.js +5 -4
  187. package/scripts/gds/ship-merge-gate.js +18 -6
  188. package/scripts/gds/tweak-halt.js +1 -0
  189. package/scripts/hall-preview/README.md +41 -1
  190. package/scripts/hall-preview/pref-writes.js +140 -0
  191. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  192. package/scripts/hall-preview/scrub-identity.js +366 -7
  193. package/scripts/hall-preview/server.js +27 -1
  194. package/scripts/render-diagrams.sh +4 -2
  195. package/src/bongos/auth-idp.js +6 -1
  196. package/src/bongos/project-settings.js +41 -0
  197. package/src/bongos/route-rank-check.js +27 -0
  198. package/src/bongos/routes/me.js +68 -7
  199. package/src/bongos/serve-internal.js +5 -1
  200. package/src/module-api.js +7 -1
  201. package/src/platform-server.js +10 -0
  202. package/tests/application_lifecycle.mjs +47 -5
  203. package/tests/autobongos_tweak_lane.mjs +208 -0
  204. package/tests/conductor_main_e2e.mjs +21 -0
  205. package/tests/connections_api.mjs +18 -0
  206. package/tests/craft_mode_nav.mjs +5 -3
  207. package/tests/craft_mode_tallies.mjs +52 -16
  208. package/tests/discord_approvals.mjs +11 -0
  209. package/tests/discord_channels.mjs +20 -0
  210. package/tests/fixtures/gov-seated.mjs +64 -0
  211. package/tests/fixtures/government-ask-cards.json +1284 -0
  212. package/tests/government_act_alone_buildings.mjs +1 -0
  213. package/tests/government_agent_asks.mjs +322 -0
  214. package/tests/government_asks.mjs +408 -0
  215. package/tests/government_build_rights.mjs +253 -0
  216. package/tests/government_buildouts.mjs +382 -0
  217. package/tests/government_counts.mjs +143 -0
  218. package/tests/government_decision_rules.mjs +5 -1
  219. package/tests/government_laws.mjs +488 -0
  220. package/tests/government_laws_session.mjs +321 -0
  221. package/tests/government_leads.mjs +375 -0
  222. package/tests/government_positions.mjs +127 -0
  223. package/tests/government_presets.mjs +215 -0
  224. package/tests/government_routes.mjs +8 -1
  225. package/tests/government_routing.mjs +118 -0
  226. package/tests/government_seed.mjs +5 -0
  227. package/tests/government_treasury.mjs +394 -0
  228. package/tests/governor_docket.mjs +26 -2
  229. package/tests/hall_buildouts.mjs +208 -0
  230. package/tests/hall_city.mjs +14 -6
  231. package/tests/hall_city_living.mjs +617 -0
  232. package/tests/hall_gov_counts.mjs +336 -0
  233. package/tests/hall_gov_living.mjs +636 -0
  234. package/tests/hall_gov_real_site.mjs +655 -0
  235. package/tests/hall_matter_card.mjs +6 -1
  236. package/tests/hall_my_agents.mjs +481 -0
  237. package/tests/hall_nav.mjs +107 -4
  238. package/tests/hall_page_gate_map.mjs +4 -0
  239. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  240. package/tests/hall_preview_scrub.mjs +59 -1
  241. package/tests/hall_preview_security.mjs +169 -0
  242. package/tests/hall_reading_world.mjs +44 -0
  243. package/tests/hall_roster_world.mjs +6 -2
  244. package/tests/hall_street.mjs +18 -2
  245. package/tests/hall_street_amend.mjs +1 -1
  246. package/tests/hall_street_deck.mjs +609 -0
  247. package/tests/hall_street_depth.mjs +559 -0
  248. package/tests/hall_street_desk.mjs +1 -1
  249. package/tests/hall_street_inside.mjs +29 -5
  250. package/tests/hall_tweak_editor.mjs +4 -1
  251. package/tests/hub_account_page.mjs +94 -3
  252. package/tests/hub_connections_page.mjs +507 -0
  253. package/tests/hub_invite_badge.mjs +1 -1
  254. package/tests/idp_device_exchange.mjs +18 -0
  255. package/tests/landing_legal_pages.mjs +8 -5
  256. package/tests/landing_page.mjs +23 -4
  257. package/tests/nav_permission_atoms.mjs +4 -1
  258. package/tests/profile_connections_ui.mjs +88 -25
  259. package/tests/projects_hub.mjs +15 -0
  260. package/tests/render_check.mjs +2 -2
  261. package/tests/ship_broadcast_routing.mjs +57 -16
  262. package/tests/ship_broadcast_server.mjs +40 -7
  263. package/tests/ship_merge_gate.mjs +19 -0
  264. package/tests/ui_design_kit.mjs +2 -2
  265. package/tests/watch_applications_queue.mjs +23 -0
@@ -0,0 +1,262 @@
1
+ // scripts/gds/autobongos-tweak-lane.js — the Autobongos tweak lane: pick the
2
+ // oldest submitted page round and hand it to a worker (task 1004679 / BV2.TW19c,
3
+ // ADR 0371 D1, D2, D4, D6, D7).
4
+ //
5
+ // WHY. An artist who presses submit with nobody at a terminal waits forever. The
6
+ // runner already wakes on a cadence, holds a fence and ships through the normal
7
+ // grade, so the lane is a step INSIDE its iteration (D1), before ordinary work,
8
+ // fed by the same derived state the studio shows (D2: open_round.state ===
9
+ // 'submitted', oldest task id first). Everything a person's /tweak does still
10
+ // happens; the worker runs `/tweak --page <id> --unattended` (task 1004678).
11
+ //
12
+ // THE RULES, each its own refusal so a run-log row can say which one held:
13
+ // lane_off tweak.autopickup is off (D3) — the lane is silent
14
+ // lane_at_cap this month's marked claims reached the cap (D5)
15
+ // lane_rank_too_low the runner's builder is below Metic (D4) — reported ONCE
16
+ // round_waits_for_person this runner already applied the round twice (D6.2)
17
+ // round_failed_unchanged the last apply failed and the round has not moved (D6.3)
18
+ // page_unknown / protected_path / needs_host_access the fence over the
19
+ // PAGE'S files (D7) — the setting replaces only the goal
20
+ // allowlist, so decideTask is asked with the round's own goal
21
+ //
22
+ // The server holds the cap, the per-round limit, the artist gate and the file
23
+ // rule again (tasks 1004677, 1004678). This file is the runner not ASKING for
24
+ // what would be refused, so a quiet machine does not spend a worker learning it.
25
+
26
+ 'use strict';
27
+
28
+ const fs = require('node:fs');
29
+ const path = require('node:path');
30
+ const fence = require('../../modules/autonomy/fence');
31
+
32
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
33
+ const LANE_RANK_FLOOR = 'metic';
34
+ // D6.2, the same number the server refuses a third marked claim at.
35
+ const MAX_AUTO_APPLIES_PER_ROUND = 2;
36
+ const MAX_ROUNDS_READ_PER_PASS = 10;
37
+ // The runner's own tree names (autobongos-run.js worktreeName). Restated rather
38
+ // than required: requiring the runner here would be a cycle.
39
+ const RUNNER_TREE_RE = /^autobongos-\d+-[0-9a-f]{6}$/;
40
+ // The fence refusals that are about the goal allowlist and nothing else: the lane
41
+ // runs past them (D3). The kill switch, a paused builder and the grader stop do not.
42
+ const GOAL_ONLY_CODES = new Set(['goal_not_allowlisted', 'no_allowlist', 'no_builder_scope']);
43
+
44
+ // ---- pure ------------------------------------------------------------------
45
+
46
+ // The lane as a whole: may it try at all this pass?
47
+ function decideLane({ settings, rank, state = {} }) {
48
+ if (!settings || typeof settings !== 'object') return { go: false, code: 'lane_unreadable', quiet: true, reason: 'could not read the tweak auto-pickup setting' };
49
+ if (settings.mode !== 'on') return { go: false, code: 'lane_off', quiet: true, reason: 'tweak auto-pickup is off for this project' };
50
+ const remaining = Number.isFinite(Number(settings.remaining_this_month))
51
+ ? Number(settings.remaining_this_month) : Number(settings.cap) - Number(settings.used_this_month);
52
+ if (!(remaining > 0)) return { go: false, code: 'lane_at_cap', reason: `this month's ${settings.cap} automatic page tweaks are used up — rounds wait for a person` };
53
+ const { RANK_TIER } = require('../../src/module-api'); // the one rank map (lazy: only when the lane is on)
54
+ if (!(RANK_TIER[rank] >= RANK_TIER[LANE_RANK_FLOOR])) {
55
+ const once = !state.laneRankReported;
56
+ state.laneRankReported = true;
57
+ return { go: false, code: 'lane_rank_too_low', quiet: !once, reason: `the runner's builder is ${rank || 'of unknown rank'}; applying a page tweak needs Metic or higher` };
58
+ }
59
+ return { go: true };
60
+ }
61
+
62
+ // The submitted rounds, oldest first (D2).
63
+ function submittedRounds(pagesBody) {
64
+ const out = [];
65
+ for (const p of (pagesBody && pagesBody.pages) || []) {
66
+ const r = p && p.open_round;
67
+ if (r && r.state === 'submitted' && /^[1-9][0-9]*$/.test(String(r.task_id))) out.push({ pageId: p.id, taskId: String(r.task_id) });
68
+ }
69
+ return out.sort((a, b) => Number(a.taskId) - Number(b.taskId));
70
+ }
71
+
72
+ // How many times THIS runner applied the round: its claims from a runner tree.
73
+ function autoAppliesByRunner(claims) {
74
+ return (claims || []).filter((c) => c && RUNNER_TREE_RE.test(String(c.worktree_name || ''))).length;
75
+ }
76
+
77
+ function pageFiles(inventory, pageId) {
78
+ const page = ((inventory && inventory.pages) || []).find((p) => p && p.id === pageId);
79
+ return page ? [...(page.files || []), ...(page.states_files || [])] : null;
80
+ }
81
+
82
+ // One round: may the lane take it?
83
+ function decideRound({ task, claims, files, failedAt, matchProtected }) {
84
+ if (autoAppliesByRunner(claims) >= MAX_AUTO_APPLIES_PER_ROUND) {
85
+ return { go: false, code: 'round_waits_for_person', reason: `applied automatically ${MAX_AUTO_APPLIES_PER_ROUND} times already — waiting for a person` };
86
+ }
87
+ if (failedAt !== undefined && failedAt === task.updated_at) {
88
+ return { go: false, code: 'round_failed_unchanged', reason: 'the last automatic apply failed and the round has not changed since' };
89
+ }
90
+ if (!files || !files.length) return { go: false, code: 'page_unknown', reason: 'the page inventory does not list this page\'s files' };
91
+ return fence.decideTask({
92
+ task: { ...task, touches: files },
93
+ goals: task.goal_id == null ? [] : [task.goal_id],
94
+ protectedHits: matchProtected(files),
95
+ });
96
+ }
97
+
98
+ function buildTweakPrompt({ task, pageId }) {
99
+ return [
100
+ `You are an unattended Autobongos worker applying ONE page tweak: task ${task.id}, page ${pageId}.`,
101
+ 'The round is already claimed in this worktree with the auto_pickup mark.',
102
+ `Read modules/copy-desk/skills/tweak/SKILL.md and follow its "Unattended mode" for: /tweak --page ${pageId} --unattended`,
103
+ 'Never ask a question. On any halt, run the tweak-halt.js line the skill names and stop.',
104
+ 'Never pass --skip-grade, --force or --allow-empty. Your finish line is WAITING FOR THE ARTIST.',
105
+ ].join('\n');
106
+ }
107
+
108
+ // After the worker: what the LEDGER says happened to the round. A held pass at
109
+ // completed is the success; a released round is a halt (no blocker: the round
110
+ // already carries its note, and D6.3 keeps the lane off it until it moves).
111
+ function tweakOutcome({ task, grade }) {
112
+ const status = task && task.status;
113
+ if (status === 'completed' && grade && grade.passed === true) return { shape: 'waiting_for_artist', ok: true };
114
+ if (status === 'ready' || status === 'backlog') return { shape: 'tweak_halted', ok: false, failed: true };
115
+ return { shape: 'other', ok: false };
116
+ }
117
+
118
+ // ---- reads -----------------------------------------------------------------
119
+
120
+ function readInventory(root = REPO_ROOT) {
121
+ return JSON.parse(fs.readFileSync(path.join(root, 'docs', 'page-inventory.json'), 'utf8'));
122
+ }
123
+
124
+ async function readRound(api, taskId) {
125
+ const r = await api.tasks.getTasksId({ id: Number(taskId), query: { include: 'claims,grade' } });
126
+ if (!r || !r.ok || !r.data || !r.data.task) return null;
127
+ return { task: r.data.task, claims: r.data.claims || [], grade: r.data.grade || null };
128
+ }
129
+
130
+ // pickRound — the lane's one call per iteration. Returns { round } to work,
131
+ // or { none, code, reason, quiet?, skipped[] }.
132
+ async function pickRound(deps) {
133
+ const { api, state, matchProtected } = deps;
134
+ let settings = null;
135
+ try { const r = await api.projectSettings.getProjectSettingsTweakAutopickup(); if (r && r.ok) settings = r.data; } catch (_) { /* unreadable */ }
136
+ let rank = deps.rank;
137
+ if (rank === undefined) {
138
+ try { const me = await api.me.getMe(); rank = me && me.ok ? me.data.builder?.rank : null; } catch (_) { rank = null; }
139
+ }
140
+ const lane = decideLane({ settings, rank, state });
141
+ if (!lane.go) return { none: true, ...lane, skipped: [] };
142
+
143
+ let pages = null;
144
+ try { const r = await api.copyDesk.getCopyDeskPages(); if (r && r.ok) pages = r.data; } catch (_) { /* unreadable */ }
145
+ if (!pages) return { none: true, code: 'lane_unreadable', reason: 'could not read the studio\'s page list', skipped: [] };
146
+ let inventory = null;
147
+ try { inventory = (deps.readInventory || readInventory)(); } catch (_) { inventory = null; }
148
+
149
+ const skipped = [];
150
+ const failed = state.tweakFailed || new Map();
151
+ // Bounded: a backlog of rounds the lane must skip costs at most this many reads a pass.
152
+ for (const { pageId, taskId } of submittedRounds(pages).slice(0, MAX_ROUNDS_READ_PER_PASS)) {
153
+ const round = await (deps.readRound || readRound)(api, taskId);
154
+ if (!round) { skipped.push({ id: taskId, page_id: pageId, code: 'round_unreadable' }); continue; }
155
+ const files = pageFiles(inventory, pageId);
156
+ const d = decideRound({ task: round.task, claims: round.claims, files, failedAt: failed.get(taskId), matchProtected });
157
+ if (!d.go) { skipped.push({ id: taskId, page_id: pageId, code: d.code }); continue; }
158
+ return { round: { task: round.task, pageId, files } };
159
+ }
160
+ return { none: true, code: 'lane_empty', quiet: true, reason: 'no submitted page round the lane may take', skipped };
161
+ }
162
+
163
+ // Is the lane's switch on? Asked only when the fence refused on goals alone, so a
164
+ // runner with no allowlist and the lane off reports "fenced" exactly as before.
165
+ // Unreadable answers no.
166
+ async function laneSwitchOn(api) {
167
+ try {
168
+ const client = api || await (require('./cli-lib').cliClient)();
169
+ const r = await client.projectSettings.getProjectSettingsTweakAutopickup();
170
+ return !!(r && r.ok && r.data && r.data.mode === 'on');
171
+ } catch (_) { return false; }
172
+ }
173
+
174
+ // ---- the iteration step ----------------------------------------------------
175
+
176
+ // The codes a lane refusal is logged under. A quiet one (off, nothing to do, the
177
+ // rank refusal after its first report) writes no row, so an idle lane costs the
178
+ // run log nothing.
179
+ function laneRow(got) {
180
+ return { event: 'tweak_lane', code: got.code, reason: got.reason, skipped: got.skipped || [] };
181
+ }
182
+
183
+ // workRound — tree, marked claim, one worker, then the ledger. `d` carries the
184
+ // runner's own helpers (runCmd, spawnWork, worktreeName, log, api) so this file
185
+ // adds a lane, not a second way to run a worker.
186
+ async function workRound({ round, opts, state }, d) {
187
+ const { task, pageId } = round;
188
+ const id = String(task.id);
189
+ const node = process.execPath;
190
+ const failedAt = async () => {
191
+ const after = await (d.readRound || readRound)(d.api, id).catch(() => null);
192
+ if (!state.tweakFailed) state.tweakFailed = new Map();
193
+ state.tweakFailed.set(id, after && after.task ? after.task.updated_at : task.updated_at);
194
+ return after;
195
+ };
196
+ const name = d.worktreeName(task.id);
197
+ const made = await d.runCmd(node, [path.join('scripts', 'gds', 'worktree.js'), 'add', name, '--base', 'origin/main']);
198
+ if (!made.ok) return d.log({ event: 'worktree_failed', lane: 'tweak', task_id: task.id, page_id: pageId, worktree: name });
199
+ const treePath = path.join(REPO_ROOT, '.claude', 'worktrees', name);
200
+ const claimed = await d.runCmd(node, [path.join(REPO_ROOT, 'scripts', 'gds', 'claim.js'), id, '--auto-pickup'], { cwd: treePath });
201
+ if (!claimed.ok) {
202
+ await d.runCmd(node, [path.join('scripts', 'gds', 'worktree.js'), 'remove', name]);
203
+ await failedAt();
204
+ return d.log({ event: 'claim_failed', lane: 'tweak', task_id: task.id, page_id: pageId, worktree: name, reason: String(claimed.stderr || claimed.stdout || '').slice(0, 400) });
205
+ }
206
+ const started = Date.now();
207
+ // The grade cap (task 1004536) holds here as on ordinary work: the run's own
208
+ // grade ledger, read after the worker, never the worker's word.
209
+ const gradeLedger = d.gradeLedgerPath ? d.gradeLedgerPath(task.id) : null;
210
+ const capCheck = d.gradeCapReader ? await d.gradeCapReader({ api: d.api, taskId: task.id, ledger: gradeLedger }) : null;
211
+ const res = await d.spawnWork({ task, model: opts.model, timeoutMs: opts.timeoutMs, cwd: treePath, gradeLedger, capCheck, prompt: buildTweakPrompt({ task, pageId }), deps: d.spawnDeps || {} });
212
+ const grades = capCheck ? await capCheck() : null;
213
+ if (gradeLedger) { try { fs.rmSync(gradeLedger, { force: true }); } catch (_) { /* its counts travel in the row */ } }
214
+ let after = await (d.readRound || readRound)(d.api, id).catch(() => null);
215
+ const capped = !!(grades && grades.exhausted);
216
+ if (after && after.task && (after.task.status === 'active' || (capped && after.task.status === 'completed' && !(after.grade && after.grade.passed === true)))) {
217
+ // Left held, or the grade cap reached: release with the fixed note (ADR 0371 D8).
218
+ await d.runCmd(node, [path.join(REPO_ROOT, 'scripts', 'gds', 'tweak-halt.js'), id, '--code', capped ? 'grade_failed' : 'worker_stopped'], { cwd: treePath });
219
+ after = await (d.readRound || readRound)(d.api, id).catch(() => null);
220
+ }
221
+ const out = after ? tweakOutcome(after) : { shape: 'unreadable', ok: false };
222
+ if (!out.ok) await failedAt();
223
+ return d.log({
224
+ event: 'worked', lane: 'tweak', task_id: task.id, title: task.title, page_id: pageId, worktree: name,
225
+ outcome: out.shape, ledger_status: after && after.task ? after.task.status : null,
226
+ ...(grades && (grades.graded || grades.outages) ? { grade_rounds: grades.graded, grade_outages: grades.outages, grade_cap_reached: capped } : {}),
227
+ session_id: (res && res.sessionId) || null, duration_s: Math.round((Date.now() - started) / 1000),
228
+ // A read that fails stops the run, as an unreadable ledger does for ordinary work.
229
+ ...(out.shape === 'unreadable' ? { event: 'verify_halt' } : {}),
230
+ });
231
+ }
232
+
233
+ // tweakLaneStep — the runner's call. Returns a run-log row when the lane did
234
+ // something this pass (worked, refused a claim, dry run), else null and the
235
+ // iteration goes on to ordinary work.
236
+ async function tweakLaneStep({ opts, state }, d) {
237
+ const got = await (d.pickRound || pickRound)({ api: d.api, state, matchProtected: d.matchProtected, readInventory: d.readInventory, readRound: d.readRound, rank: d.rank });
238
+ if (got.none) {
239
+ if (!got.quiet) d.log(laneRow(got));
240
+ return null;
241
+ }
242
+ if (opts.dryRun) return d.log({ event: 'dry_run', lane: 'tweak', task_id: got.round.task.id, page_id: got.round.pageId });
243
+ return workRound({ round: got.round, opts, state }, d);
244
+ }
245
+
246
+ module.exports = {
247
+ GOAL_ONLY_CODES,
248
+ laneSwitchOn,
249
+ tweakLaneStep,
250
+ workRound,
251
+ MAX_AUTO_APPLIES_PER_ROUND,
252
+ LANE_RANK_FLOOR,
253
+ decideLane,
254
+ submittedRounds,
255
+ autoAppliesByRunner,
256
+ pageFiles,
257
+ decideRound,
258
+ buildTweakPrompt,
259
+ tweakOutcome,
260
+ readRound,
261
+ pickRound,
262
+ };
@@ -39,6 +39,7 @@ const path = require('node:path');
39
39
  const { execFile } = require('node:child_process');
40
40
 
41
41
  const { classifyStrand, WATCHED } = require('./strand-watch.js');
42
+ const { postWithAsk } = require('./ask.js');
42
43
 
43
44
  // failureReason — what a failed child process said, fit for one run-log field
44
45
  // (task 1004180).
@@ -264,6 +265,23 @@ function blockerFor(v, { taskId, taskTitle = null, worker = null } = {}) {
264
265
  body_md: lines.join('\n'),
265
266
  source: 'autobongos',
266
267
  source_ref: `task-${taskId}-${v.shape}`,
268
+ ask: blockerAsk(v, { taskId }),
269
+ };
270
+ }
271
+
272
+ // The card a person answers (task 1004704): the shape as one decidable question.
273
+ // fileBlocker sends it beside the row and files without it if it is refused.
274
+ function blockerAsk(v, { taskId }) {
275
+ const shape = String((v && v.shape) || 'unfinished').replace(/_/g, ' ');
276
+ return {
277
+ question: `Requeue task ${taskId}, which came back ${shape}?`,
278
+ options: [
279
+ { key: 'requeue', label: 'Requeue it', result: 'The blocker closes and a runner may pick it up again.', act: 'go ahead' },
280
+ { key: 'keep', label: 'Keep it out', result: 'It stays out of the pool until someone looks.', act: 'not now' },
281
+ ],
282
+ recommend: { key: 'keep', why: 'Read the run log first; a second run may fail the same way.' },
283
+ waits_on: 'lead:automation',
284
+ if_nobody_answers: 'The task stays out of the pool.',
267
285
  };
268
286
  }
269
287
 
@@ -351,7 +369,8 @@ async function fetchTask(api, taskId) {
351
369
  async function fileBlocker(api, row, { taskId, link }) {
352
370
  let created = null;
353
371
  try {
354
- const r = await api.blockers.postBlockers({ body: row });
372
+ const { ask, ...body } = row;
373
+ const r = await postWithAsk((b) => api.blockers.postBlockers({ body: b }), body, ask);
355
374
  if (!r || !r.ok) return { filed: false, reason: `POST /blockers → ${r ? r.status : 'no response'}`, linked: false };
356
375
  if (r.data && r.data.skipped) return { filed: false, duplicate: true, reason: 'this task already carries a blocker of this shape', linked: false };
357
376
  created = r.data && r.data.blocker;
@@ -594,6 +613,6 @@ if (require.main === module) {
594
613
  }
595
614
 
596
615
  module.exports = {
597
- classifyLedger, blockerFor, probeArtifact, fileBlocker, verifyShip, failureReason,
616
+ classifyLedger, blockerFor, blockerAsk, probeArtifact, fileBlocker, verifyShip, failureReason,
598
617
  LAND_GRACE_MS, namesTask, pendingLandIds, recheckLands,
599
618
  };
@@ -606,6 +606,8 @@ async function main() {
606
606
  worktree_name: worktree,
607
607
  head_sha: headSha,
608
608
  enforce_touches: enforceTouches,
609
+ // --auto-pickup (task 1004679, ADR 0371 D5): the Autobongos tweak lane's mark.
610
+ ...(args.includes('--auto-pickup') ? { auto_pickup: true } : {}),
609
611
  },
610
612
  });
611
613
  let r = await postClaim();