tumwater 0.4.0 → 0.4.2

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 (291) hide show
  1. package/README.md +4 -6
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-backend.js +5 -2
  4. package/dist/src/agent/backend-traits.js +21 -0
  5. package/dist/src/agent/claude/claude-backend.js +1 -1
  6. package/dist/src/agent/claude/claude-stream.js +181 -182
  7. package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
  8. package/dist/src/agent/claude/claude-write-guard.js +5 -4
  9. package/dist/src/agent/codex/codex-backend.js +1 -1
  10. package/dist/src/agent/codex/codex-rollout.js +48 -45
  11. package/dist/src/agent/codex/codex-stream.js +269 -215
  12. package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
  13. package/dist/src/agent/codex/codex-write-guard.js +3 -3
  14. package/dist/src/agent/image-input.js +18 -87
  15. package/dist/src/agent/notes-shim.js +2 -2
  16. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  17. package/dist/src/agent/opencode/opencode-catalog.js +2 -2
  18. package/dist/src/agent/opencode/opencode-config.js +2 -2
  19. package/dist/src/agent/opencode/opencode-stream.js +101 -92
  20. package/dist/src/agent/run-agent.js +76 -382
  21. package/dist/src/agent/run-settle.js +25 -15
  22. package/dist/src/agent/run-spawn.js +226 -0
  23. package/dist/src/agent/run-stream.js +185 -0
  24. package/dist/src/agent/session-ledger.js +16 -0
  25. package/dist/src/agent/write-guard.js +5 -3
  26. package/dist/src/backlog/backlog-archive.js +86 -170
  27. package/dist/src/backlog/backlog-cli-view.js +89 -70
  28. package/dist/src/backlog/backlog-eligibility.js +51 -59
  29. package/dist/src/backlog/backlog-file-bugs.js +133 -0
  30. package/dist/src/backlog/backlog-index-data.js +159 -0
  31. package/dist/src/backlog/backlog-index.js +7 -5
  32. package/dist/src/backlog/backlog-md.js +39 -28
  33. package/dist/src/backlog/backlog-release.js +21 -36
  34. package/dist/src/backlog/backlog-render.js +1 -1
  35. package/dist/src/backlog/backlog-structure.js +14 -145
  36. package/dist/src/backlog/backlog-write.js +31 -18
  37. package/dist/src/backlog/backlog.js +14 -4
  38. package/dist/src/backlog/question-answer.js +33 -15
  39. package/dist/src/baseline/main-baseline.js +71 -26
  40. package/dist/src/baseline/main-guardrails.js +76 -15
  41. package/dist/src/baseline/main-red.js +45 -94
  42. package/dist/src/baseline/main-watch.js +9 -3
  43. package/dist/src/brief/brief.js +40 -0
  44. package/dist/src/budget/budget-fallback-view.js +50 -8
  45. package/dist/src/budget/budget-wiring.js +15 -11
  46. package/dist/src/budget/budget.js +3 -2
  47. package/dist/src/build/build-check-events.js +4 -2
  48. package/dist/src/build/build-check-report.js +2 -1
  49. package/dist/src/build/build-check-scoped.js +70 -41
  50. package/dist/src/change/change-data.js +4 -2
  51. package/dist/src/cli/cli-args.js +9 -0
  52. package/dist/src/cli/cli-command-args.js +199 -98
  53. package/dist/src/cli/cli-flag-specs.js +6 -4
  54. package/dist/src/cli/cli-marker-commands.js +2 -2
  55. package/dist/src/cli/cli-query-commands.js +41 -6
  56. package/dist/src/cli/cli-review-commands.js +2 -1
  57. package/dist/src/cli/cli-role-command.js +46 -92
  58. package/dist/src/cli/cli-start.js +123 -47
  59. package/dist/src/cli/follow-view.js +3 -1
  60. package/dist/src/cli/help.js +19 -12
  61. package/dist/src/cli/log-commands.js +128 -107
  62. package/dist/src/cli/question-commands.js +82 -63
  63. package/dist/src/cli.js +396 -343
  64. package/dist/src/collections.js +46 -9
  65. package/dist/src/config/config-fallbacks.js +0 -4
  66. package/dist/src/config/config-live.js +179 -147
  67. package/dist/src/config/config-request.js +176 -51
  68. package/dist/src/config/config-validation-roles.js +271 -0
  69. package/dist/src/config/config-validation.js +10 -205
  70. package/dist/src/config/config-views.js +2 -7
  71. package/dist/src/config/config-write.js +205 -175
  72. package/dist/src/config/config.js +2 -1
  73. package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
  74. package/dist/src/doctor/doctor-backlog-plans.js +24 -25
  75. package/dist/src/doctor/doctor-backlog.js +34 -42
  76. package/dist/src/doctor/doctor-check-names.js +4 -2
  77. package/dist/src/doctor/doctor-checks.js +16 -4
  78. package/dist/src/doctor/doctor-claude-checks.js +2 -2
  79. package/dist/src/doctor/doctor-codex-checks.js +9 -20
  80. package/dist/src/doctor/doctor-contention-checks.js +52 -19
  81. package/dist/src/doctor/doctor-holds.js +22 -20
  82. package/dist/src/doctor/doctor-model-checks.js +8 -184
  83. package/dist/src/doctor/doctor-notify.js +2 -5
  84. package/dist/src/doctor/doctor-opencode-checks.js +90 -41
  85. package/dist/src/doctor/doctor-principles.js +26 -0
  86. package/dist/src/doctor/doctor-project-checks.js +69 -19
  87. package/dist/src/doctor/doctor-role-notes.js +33 -0
  88. package/dist/src/doctor/doctor.js +13 -6
  89. package/dist/src/doctor/probe.js +1 -1
  90. package/dist/src/events/event-format.js +307 -162
  91. package/dist/src/events/event-labels.js +0 -1
  92. package/dist/src/events/event-legacy.js +1 -1
  93. package/dist/src/events/event-window.js +1 -1
  94. package/dist/src/events/events.js +15 -2
  95. package/dist/src/events/notify.js +1 -1
  96. package/dist/src/failure/failure-cluster.js +4 -3
  97. package/dist/src/failure/failure-data.js +126 -94
  98. package/dist/src/failure/failure-render.js +58 -17
  99. package/dist/src/failure/failure-state-change.js +308 -27
  100. package/dist/src/failure/time-spend.js +139 -106
  101. package/dist/src/files/files.js +7 -0
  102. package/dist/src/files/json-object.js +15 -2
  103. package/dist/src/gates/budget-gates.js +90 -68
  104. package/dist/src/gates/gate-polls.js +92 -61
  105. package/dist/src/gates/role-cap-gates.js +1 -1
  106. package/dist/src/gates/startup-gate.js +28 -28
  107. package/dist/src/gates/streak-gate.js +1 -1
  108. package/dist/src/git/git.js +1 -1
  109. package/dist/src/git/slot-salvage.js +9 -9
  110. package/dist/src/git/slots-state.js +12 -3
  111. package/dist/src/git/worktree-pool-startup.js +127 -0
  112. package/dist/src/git/worktree-pool.js +1 -120
  113. package/dist/src/gui/gui-admin-endpoints.js +3 -4
  114. package/dist/src/gui/gui-args.js +1 -1
  115. package/dist/src/gui/gui-backlog-commands.js +20 -11
  116. package/dist/src/gui/gui-backlog-endpoints.js +2 -8
  117. package/dist/src/gui/gui-config-commands.js +51 -76
  118. package/dist/src/gui/gui-endpoint-commands.js +4 -2
  119. package/dist/src/gui/gui-endpoint-common.js +4 -3
  120. package/dist/src/gui/gui-history-endpoints.js +36 -27
  121. package/dist/src/gui/gui-operator-commands.js +43 -17
  122. package/dist/src/gui/gui-prompt-commands.js +27 -21
  123. package/dist/src/gui/gui-quarantine-commands.js +30 -0
  124. package/dist/src/gui/gui-report-endpoints.js +13 -9
  125. package/dist/src/gui/gui-server.js +5 -3
  126. package/dist/src/gui/gui-team-commands.js +1 -1
  127. package/dist/src/history/history-data.js +7 -1
  128. package/dist/src/history/history-render.js +15 -10
  129. package/dist/src/history/history.js +103 -68
  130. package/dist/src/inbox/inbox-submit.js +1 -5
  131. package/dist/src/inbox/pending-prompt.js +3 -2
  132. package/dist/src/inbox/prompt-commands.js +194 -161
  133. package/dist/src/init/init.js +104 -25
  134. package/dist/src/merge/merge-core.js +1 -1
  135. package/dist/src/merge/merge-drain.js +1 -1
  136. package/dist/src/merge/merge-git.js +5 -5
  137. package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
  138. package/dist/src/merge/merge-queue.js +2 -1
  139. package/dist/src/merge/merge-to-main.js +1 -1
  140. package/dist/src/merge/merge-verify.js +3 -2
  141. package/dist/src/operator/operator-commands.js +84 -52
  142. package/dist/src/operator/operator-intent.js +26 -3
  143. package/dist/src/operator/retire.js +2 -1
  144. package/dist/src/operator/revert.js +18 -12
  145. package/dist/src/orchestrator/orchestrator-poll.js +2 -2
  146. package/dist/src/orchestrator/orchestrator.js +1 -1
  147. package/dist/src/orchestrator/retention.js +6 -5
  148. package/dist/src/paths.js +10 -10
  149. package/dist/src/pi/pi-bin.js +2 -2
  150. package/dist/src/pi/pi-models.js +9 -0
  151. package/dist/src/pi/pi-stream.js +63 -41
  152. package/dist/src/pi/pi-watchdogs.js +84 -49
  153. package/dist/src/pi-extension/context-shake.js +55 -31
  154. package/dist/src/pi-extension/full-output.js +2 -3
  155. package/dist/src/process/process.js +24 -1
  156. package/dist/src/process/run-marker.js +2 -2
  157. package/dist/src/process/supervisor.js +24 -5
  158. package/dist/src/prompt/gate-notes.js +41 -16
  159. package/dist/src/prompt/notes-backlog.js +22 -29
  160. package/dist/src/prompt/principles.js +11 -0
  161. package/dist/src/prompt/prompt-followup.js +4 -12
  162. package/dist/src/prompt/prompt.js +15 -12
  163. package/dist/src/redeploy/redeploy-probes.js +11 -1
  164. package/dist/src/redeploy/redeployer.js +54 -7
  165. package/dist/src/redeploy/self-reload.js +7 -2
  166. package/dist/src/report/report-data.js +224 -320
  167. package/dist/src/report/report-fold.js +23 -231
  168. package/dist/src/report/report-main-red.js +70 -3
  169. package/dist/src/report/report-render.js +181 -48
  170. package/dist/src/report/report-since-data.js +218 -0
  171. package/dist/src/report/report-usage-fold.js +288 -0
  172. package/dist/src/report/report.js +2 -1
  173. package/dist/src/review/known-flakes.js +22 -3
  174. package/dist/src/review/post-merge-review-accept.js +3 -2
  175. package/dist/src/review/post-merge-review.js +16 -14
  176. package/dist/src/review/quarantine-render.js +29 -11
  177. package/dist/src/review/review-precheck.js +2 -1
  178. package/dist/src/roles/role-catalog.js +30 -29
  179. package/dist/src/roles/role-guidance.js +26 -43
  180. package/dist/src/roles/role-render.js +19 -0
  181. package/dist/src/roles/role-view.js +19 -5
  182. package/dist/src/roles/roles.js +2 -22
  183. package/dist/src/run/ci-parity.js +5 -6
  184. package/dist/src/run/feedback-filings.js +10 -1
  185. package/dist/src/run/qa-coverage.js +4 -4
  186. package/dist/src/run/run-apply.js +14 -2
  187. package/dist/src/run/run-detail-data.js +50 -10
  188. package/dist/src/run/run-detail-render.js +32 -1
  189. package/dist/src/run/run-detail.js +44 -22
  190. package/dist/src/run/run-dismiss.js +43 -0
  191. package/dist/src/run/run-finalize.js +75 -46
  192. package/dist/src/run/run-prompt.js +271 -187
  193. package/dist/src/run/run-resume.js +32 -31
  194. package/dist/src/run/run-stage.js +116 -64
  195. package/dist/src/run/run-timing.js +18 -4
  196. package/dist/src/run/run-verdict.js +119 -83
  197. package/dist/src/run/stage-backlog-checks.js +4 -31
  198. package/dist/src/run/stage-check.js +4 -12
  199. package/dist/src/run/stage-checks-shared.js +15 -0
  200. package/dist/src/run/stage-name-checks.js +3 -13
  201. package/dist/src/run/stage-path-checks.js +26 -29
  202. package/dist/src/runner/leftover.js +13 -4
  203. package/dist/src/runner/revision.js +2 -2
  204. package/dist/src/runner/runner-fallback.js +1 -1
  205. package/dist/src/runner/runner-outcome.js +0 -5
  206. package/dist/src/runner/runner-pi.js +72 -49
  207. package/dist/src/runner/runner-run.js +19 -102
  208. package/dist/src/runner/runner-state-load.js +0 -10
  209. package/dist/src/runner/runner.js +1 -32
  210. package/dist/src/scheduling/claims.js +4 -11
  211. package/dist/src/status/status-data.js +140 -91
  212. package/dist/src/status/status-polls.js +41 -5
  213. package/dist/src/status/status-role-notes.js +53 -0
  214. package/dist/src/status/status-roles.js +10 -37
  215. package/dist/src/text/datetime.js +8 -1
  216. package/dist/src/text/format.js +13 -1
  217. package/dist/src/text/phrases.js +16 -2
  218. package/dist/src/text/text.js +12 -0
  219. package/dist/src/ui/alert-feeds.js +115 -0
  220. package/dist/src/ui/badges.js +3 -4
  221. package/dist/src/ui/gui/gui-client-boot.js +78 -57
  222. package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
  223. package/dist/src/ui/gui/gui-client-drawer.js +18 -2
  224. package/dist/src/ui/gui/gui-client-feed.js +3 -8
  225. package/dist/src/ui/gui/gui-client-history.js +16 -16
  226. package/dist/src/ui/gui/gui-client-model.js +9 -9
  227. package/dist/src/ui/gui/gui-client-operator.js +6 -10
  228. package/dist/src/ui/gui/gui-client-pending.js +4 -4
  229. package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
  230. package/dist/src/ui/gui/gui-client-report.js +70 -11
  231. package/dist/src/ui/gui/gui-client-roles.js +52 -12
  232. package/dist/src/ui/gui/gui-client-settings.js +31 -26
  233. package/dist/src/ui/gui/gui-client-team.js +28 -2
  234. package/dist/src/ui/gui/gui-client.js +33 -4
  235. package/dist/src/ui/gui/gui-icons.js +7 -1
  236. package/dist/src/ui/progress-data.js +2 -2
  237. package/dist/src/ui/run-progress-model.js +2 -2
  238. package/dist/src/ui/status-model.js +26 -16
  239. package/dist/src/ui/status-payload.js +149 -326
  240. package/dist/src/ui/status-render.js +131 -80
  241. package/dist/src/ui/team-alert-backlog.js +236 -0
  242. package/dist/src/ui/team-alert-shared.js +92 -0
  243. package/dist/src/ui/team-alert-status.js +326 -0
  244. package/dist/src/ui/team-alert-tail.js +238 -0
  245. package/dist/src/ui/team-alerts.js +16 -984
  246. package/dist/src/ui/transcript-tail.js +137 -94
  247. package/dist/src/ui/tui/tui-backlog.js +1 -1
  248. package/dist/src/ui/tui/tui-editor-answer.js +54 -44
  249. package/dist/src/ui/tui/tui-editor-attach.js +74 -63
  250. package/dist/src/ui/tui/tui-editor-backends.js +62 -56
  251. package/dist/src/ui/tui/tui-editor-brief.js +64 -54
  252. package/dist/src/ui/tui/tui-editor-budget.js +106 -89
  253. package/dist/src/ui/tui/tui-editor-config.js +2 -4
  254. package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
  255. package/dist/src/ui/tui/tui-editor-entry.js +171 -153
  256. package/dist/src/ui/tui/tui-editor-instances.js +2 -1
  257. package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
  258. package/dist/src/ui/tui/tui-editor-interval.js +39 -112
  259. package/dist/src/ui/tui/tui-editor-notes.js +28 -84
  260. package/dist/src/ui/tui/tui-editor-pause.js +210 -183
  261. package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
  262. package/dist/src/ui/tui/tui-editor-queued.js +74 -64
  263. package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
  264. package/dist/src/ui/tui/tui-editor-release.js +85 -0
  265. package/dist/src/ui/tui/tui-editor-retire.js +57 -46
  266. package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
  267. package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
  268. package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
  269. package/dist/src/ui/tui/tui-editor-search.js +70 -58
  270. package/dist/src/ui/tui/tui-editor-session.js +3 -0
  271. package/dist/src/ui/tui/tui-editors.js +4 -1
  272. package/dist/src/ui/tui/tui-frame.js +7 -3
  273. package/dist/src/ui/tui/tui-history.js +12 -10
  274. package/dist/src/ui/tui/tui-keys.js +6 -1
  275. package/dist/src/ui/tui/tui-pane-activity.js +62 -50
  276. package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
  277. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  278. package/dist/src/ui/tui/tui-pane-health.js +45 -26
  279. package/dist/src/ui/tui/tui-pane-history.js +296 -264
  280. package/dist/src/ui/tui/tui-pane-report.js +74 -57
  281. package/dist/src/ui/tui/tui-pane-session.js +6 -2
  282. package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
  283. package/dist/src/ui/tui/tui-pane.js +201 -174
  284. package/dist/src/ui/tui/tui-panes.js +211 -148
  285. package/dist/src/ui/tui/tui.js +4 -4
  286. package/dist/src/verdict/fix-claim.js +18 -6
  287. package/dist/src/verdict/refusal.js +32 -7
  288. package/package.json +1 -1
  289. package/dist/src/doctor/image-probe.js +0 -257
  290. /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
  291. /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
@@ -1,6 +1,4 @@
1
- import { MODEL_TIERS, BACKOFF_KEYS, BOOTSTRAP_KEYS, CHECK_KEYS, CUSTOM_ROLE_KEYS, CUSTOM_NAME_RE, CUSTOM_TASK_MAX_CHARS, REVIEW_KEYS, ROLE_ENTRY_KEYS, TOP_LEVEL_KEYS, ROLE_INSTRUCTIONS_MAX_CHARS, } from "./config-schema.js";
2
- import { DIRECTOR_ROLE, allRoleIds } from "../roles/roles.js";
3
- import { INSTANCE_ROLES, baseRoleOf } from "../roles/runner-ids.js";
1
+ import { MODEL_TIERS, BACKOFF_KEYS, BOOTSTRAP_KEYS, CHECK_KEYS, REVIEW_KEYS, TOP_LEVEL_KEYS, } from "./config-schema.js";
4
2
  import { isHarnessCodexConfigKey } from "../agent/codex/codex-args.js";
5
3
  import { isJsonObject } from "../files/json-object.js";
6
4
  import { expandHomeTilde } from "../files/files.js";
@@ -10,11 +8,11 @@ import { isNonBlankString, tooLongMessage } from "../text/text.js";
10
8
  import { PREFILL_REASON } from "../text/phrases.js";
11
9
  import { parseQuietHours } from "../scheduling/quiet-hours.js";
12
10
  import { isBackendId } from "./model-selector.js";
13
- import { TUMWATER_SOURCE } from "../events/events.js";
14
- import { AT_LEAST_ONE, checkBooleanField, checkKnownKeys, checkKnownRoleId, checkModelTripleField, checkNumberField, checkObjectSection, codexConfigKey, checkStringArrayField, checkStringField, NON_NEGATIVE, NON_NEGATIVE_OR_DISABLED, DOLLAR_CAP, MAX_DURATION_SECONDS, POSITIVE, POSITIVE_INTEGER, show, typeName, } from "./config-field-checks.js";
11
+ import { AT_LEAST_ONE, checkBooleanField, checkKnownKeys, checkModelTripleField, checkNumberField, checkObjectSection, codexConfigKey, checkStringArrayField, checkStringField, NON_NEGATIVE, NON_NEGATIVE_OR_DISABLED, DOLLAR_CAP, MAX_DURATION_SECONDS, POSITIVE, POSITIVE_INTEGER, show, typeName, } from "./config-field-checks.js";
15
12
  import { checkEntryBackend, checkRetiredSelector, checkSelectorHalves, } from "./config-selector-checks.js";
16
13
  import { validateAgentsBlock } from "./config-validation-agents.js";
17
14
  import { validateFallbackBlock, validateModelBlock, } from "./config-validation-model.js";
15
+ import { validateRolesAndCrossField } from "./config-validation-roles.js";
18
16
  /** Schema validation for tumwater.json: validateConfig, the gate every load and save passes
19
17
  * through (config.ts's load/save, config-write.ts's budget editing and
20
18
  * config-request.ts's director config requests), judged against the key allow-lists and
@@ -38,33 +36,6 @@ const DURATION_OR_DISABLED = {
38
36
  ok: (n) => n >= 0 && n <= MAX_DURATION_SECONDS,
39
37
  what: `a number of 0 or more, at most ${MAX_DURATION_SECONDS} (0 disables)`,
40
38
  };
41
- /** A per-role instance count (`roles.feature.instances`): an integer from 1 to 8. Bounded so
42
- * a typo like `instances: 800` cannot spawn hundreds of runners, and integer so
43
- * `feature-1.5` — which no role id can name — is refused. */
44
- const INSTANCE_COUNT = {
45
- ok: (n) => Number.isInteger(n) && n >= 1 && n <= 8,
46
- what: "an integer from 1 to 8",
47
- };
48
- /** Validate one per-role map section (`maxDailyCostUsdPerRole`, `quietHoursPerRole`): the
49
- * value must be an object mapping role ids to entries, every key must name a known role
50
- * (checkKnownRoleId's guard — a typo'd id would silently no-op the section), and each entry
51
- * goes through `checkValue` with its role id. `expectation` is the value description the
52
- * type-error message quotes. One home for the section shape the two dotted per-role keys
53
- * share, so a third per-role map reuses it instead of re-copying the loop. */
54
- function checkPerRoleMap(r, key, expectation, customNames, problems, checkValue) {
55
- if (!(key in r))
56
- return;
57
- const map = r[key];
58
- if (!isJsonObject(map)) {
59
- problems.push(`${key} must be an object mapping role ids to ${expectation} (got ${show(map)})`);
60
- return;
61
- }
62
- for (const [id, value] of Object.entries(map)) {
63
- if (!checkKnownRoleId(key, id, customNames, problems))
64
- continue;
65
- checkValue(id, value);
66
- }
67
- }
68
39
  /** Validate tumwater.json values, so a typo fails fast with an actionable message instead
69
40
  * of misbehaving at runtime — e.g. a non-numeric runTimeoutSeconds becomes NaN and kills
70
41
  * every pi run instantly, a non-numeric logMaxBytes rotates the event log on every write, an
@@ -319,180 +290,14 @@ export function validateConfig(raw, label = "tumwater.json", rawFile = false) {
319
290
  tierBackend,
320
291
  eachTierBackend,
321
292
  });
322
- // Custom roles (plans/user-defined-loops.md): names become worktree dirs and git refs, so
323
- // they are validated strictly — a colliding name would silently shadow a built-in's prompt
324
- // (roleById wins in runPrompt), validation makes that impossible instead of subtle. The
325
- // collected names feed the roles cross-check below: an id under `roles` is valid when it is
326
- // in the catalog OR listed as a customRoles name.
327
- const customNames = new Set();
328
- if ("customRoles" in r) {
329
- const cl = r.customRoles;
330
- if (!Array.isArray(cl)) {
331
- problems.push(`customRoles must be an array of { name, task } entries (got ${show(cl)})`);
332
- }
333
- else {
334
- cl.forEach((entry, i) => {
335
- const where = `customRoles[${i}]`;
336
- if (!isJsonObject(entry)) {
337
- problems.push(`${where} must be an object with keys name and task (got ${show(entry)})`);
338
- return;
339
- }
340
- checkKnownKeys(entry, CUSTOM_ROLE_KEYS, where, problems);
341
- const name = entry.name;
342
- if (typeof name !== "string") {
343
- problems.push(`${where}.name must be a string matching /^[a-z0-9][a-z0-9_-]{0,31}$/ (got ${show(name)})`);
344
- }
345
- else if (!CUSTOM_NAME_RE.test(name)) {
346
- problems.push(`${where}.name "${name}" is not a valid role name: start with a lowercase letter or digit, then at most 31 more of [a-z0-9_-]`);
347
- }
348
- else if (name === TUMWATER_SOURCE) {
349
- problems.push(`${where}.name "${name}" is reserved: events tumwater writes about itself carry that source, so a role's events would be indistinguishable from them — pick another name`);
350
- }
351
- else if (allRoleIds().includes(name)) {
352
- problems.push(`${where}.name "${name}" collides with a built-in role id — pick another name`);
353
- }
354
- else if (baseRoleOf(name) !== name) {
355
- problems.push(`${where}.name "${name}" would shadow the ${baseRoleOf(name)} role's instance ids — pick another name`);
356
- }
357
- else if (customNames.has(name)) {
358
- problems.push(`${where}.name "${name}" is duplicated in customRoles — names must be unique`);
359
- }
360
- else {
361
- customNames.add(name);
362
- }
363
- const task = entry.task;
364
- // A whitespace-only task is as inert as an empty one — it rides into every one of
365
- // this role's run prefills as blank text, so the role has nothing to do and runs
366
- // no_change forever. Reject both shapes with the same message; the model-triple
367
- // checkString rule uses the same trim-based emptiness test.
368
- if (!isNonBlankString(task)) {
369
- problems.push(`${where}.task must be a non-empty string (got ${show(task)})`);
370
- }
371
- else {
372
- checkPromptLength(`${where}.task`, task, CUSTOM_TASK_MAX_CHARS);
373
- }
374
- });
375
- }
376
- }
377
- if ("roles" in r) {
378
- const roles = r.roles;
379
- if (!isJsonObject(roles)) {
380
- problems.push(`roles must be an object mapping role ids to settings (got ${show(roles)})`);
381
- }
382
- else {
383
- for (const [id, rc] of Object.entries(roles)) {
384
- // An id outside the catalog and customRoles cannot work: runPrompt has no prompt for
385
- // it and the role would error every run forever (checkKnownRoleId).
386
- if (!checkKnownRoleId("roles", id, customNames, problems))
387
- continue;
388
- if (!isJsonObject(rc)) {
389
- problems.push(`roles.${id} must be an object (got ${show(rc)})`);
390
- continue;
391
- }
392
- const o = rc;
393
- checkKnownKeys(o, ROLE_ENTRY_KEYS, `roles.${id}`, problems);
394
- // Empty is a deliberate "no extra instructions". A non-empty value rides into every
395
- // one of this role's run prefills, so it is capped like a custom role's task.
396
- checkString(o, `roles.${id}.`, "instructions");
397
- checkPromptLength(`roles.${id}.instructions`, o.instructions, ROLE_INSTRUCTIONS_MAX_CHARS);
398
- checkModelTripleEntry(o, `roles.${id}.`, `roles.${id}`);
399
- checkBoolean(o, `roles.${id}.`, "enabled");
400
- checkNumber(o, `roles.${id}.`, "minRunIntervalSeconds", DURATION_NON_NEGATIVE);
401
- // Only feature and bugfix may split (src/roles/runner-ids.ts INSTANCE_ROLES); an
402
- // instances count under any other role would never spawn a runner, so reject it
403
- // rather than let the operator believe it did something.
404
- if ("instances" in o && !INSTANCE_ROLES.has(id)) {
405
- problems.push(`roles.${id}.instances is only valid for ${[...INSTANCE_ROLES].join(" and ")} (got ${show(o.instances)})`);
406
- }
407
- else {
408
- checkNumber(o, `roles.${id}.`, "instances", INSTANCE_COUNT);
409
- }
410
- }
411
- }
412
- }
413
- // review.afterMerge names roles, so a typo'd id would silently keep that role reviewed before
414
- // merging; the director is refused because its changes always merge on a review first.
415
- if (isJsonObject(r.review) && Array.isArray(r.review.afterMerge)) {
416
- r.review.afterMerge.forEach((id, i) => {
417
- if (typeof id !== "string" || id.trim() === "")
418
- return; // Already reported by checkStringArray.
419
- if (id === DIRECTOR_ROLE)
420
- problems.push(`review.afterMerge[${i}] must not be "${DIRECTOR_ROLE}" — its changes are always reviewed before merging`);
421
- else
422
- checkKnownRoleId("review.afterMerge", id, customNames, problems);
423
- });
424
- }
425
- // Per-role daily cost caps (src/gates/role-cap-gates.ts): each key must name a known role —
426
- // built-in or customRoles — because a typo'd id would silently no-op the cap, the exact
427
- // silent-ignore class the `roles.<id>` check exists to prevent (the customNames set above
428
- // is fully collected by this point, so a custom role can be capped). Each value is a
429
- // spend threshold with the NON_NEGATIVE_OR_DISABLED semantics: 0 disables that role's cap.
430
- checkPerRoleMap(r, "maxDailyCostUsdPerRole", "USD caps", customNames, problems, (id) => {
431
- // Same MAX_SAFE_INTEGER bound as maxDailyCostUsd's DOLLAR_CAP rule (BUGS.md
432
- // 2026-10-02): a finite-but-unrepresentable per-role cap is an effectively
433
- // uncapped budget for that role — DOLLAR_CAP is that rule, so the predicate
434
- // and its wording have one home. checkPerRoleMap narrowed the map, so the cast
435
- // is sound and checkNumberField sees the entry's value typed as before.
436
- checkNumberField(problems, r.maxDailyCostUsdPerRole, "maxDailyCostUsdPerRole.", id, DOLLAR_CAP);
437
- });
438
- // Per-role quiet hours (src/scheduling/quiet-hours.ts): the same known-role gate as the
439
- // caps — a typo'd role id would silently no-op the window. Each value must parse as a
440
- // quiet-hours window (an empty string means off for that role, the same disablement the
441
- // team-wide key takes), and the message names the key and the offending id/value verbatim.
442
- checkPerRoleMap(r, "quietHoursPerRole", '"HH:MM-HH:MM" windows', customNames, problems, (id, value) => {
443
- if (typeof value !== "string") {
444
- problems.push(`quietHoursPerRole.${id} must be a "HH:MM-HH:MM" string like "23:00-07:00" (an empty string means off) (got ${show(value)})`);
445
- return;
446
- }
447
- if (value.trim() === "")
448
- return; // empty = off for that role
449
- const parsed = parseQuietHours(value, `quietHoursPerRole.${id}`);
450
- if (!parsed.ok)
451
- problems.push(parsed.error);
293
+ validateRolesAndCrossField(r, problems, {
294
+ checkString,
295
+ checkBoolean,
296
+ checkNumber,
297
+ checkPromptLength,
298
+ checkModelTripleEntry,
299
+ checkWindowReserve,
452
300
  });
453
- // Per-provider concurrency caps (plans/provider-concurrency.md §1): a map from provider
454
- // key to a positive integer. Unlike the per-role maps above, an unknown provider name is
455
- // NOT a validation error — providers live in pi's models.json, which validation does not
456
- // read, and a fallback's provider may only come into use later (doctor warns in part 5/5).
457
- // Each value must be a positive integer under a non-empty key.
458
- if (r.maxConcurrentPerProvider !== undefined) {
459
- const map = r.maxConcurrentPerProvider;
460
- if (!isJsonObject(map)) {
461
- problems.push(`maxConcurrentPerProvider must be an object mapping provider names to concurrency caps (got ${show(map)})`);
462
- }
463
- else {
464
- for (const [provider, value] of Object.entries(map)) {
465
- if (provider.trim() === "") {
466
- problems.push(`maxConcurrentPerProvider.<empty> — a provider key must not be empty`);
467
- continue;
468
- }
469
- if (typeof value !== "number" || !Number.isFinite(value) || !POSITIVE_INTEGER.ok(value)) {
470
- problems.push(`maxConcurrentPerProvider.${provider} must be ${POSITIVE_INTEGER.what} (got ${show(value)})`);
471
- }
472
- }
473
- }
474
- }
475
- // The subscription-window reserve: a map from window type to a fraction, 0 < f <= 1.
476
- if (r.claudeWindowReserve !== undefined)
477
- checkWindowReserve("claudeWindowReserve", r.claudeWindowReserve);
478
- // The reserve's two spellings are mutually exclusive (plans/codex-backend.md part 2b/9): the
479
- // nested key is the current one; `claudeWindowReserve` is kept as claude's older spelling. A
480
- // file holding both is ambiguous about which drives the window, so it is rejected naming both.
481
- if (r.claudeWindowReserve !== undefined &&
482
- isJsonObject(r.backends) &&
483
- isJsonObject(r.backends.claude) &&
484
- r.backends.claude.windowReserve !== undefined)
485
- problems.push("claudeWindowReserve and backends.claude.windowReserve set the same value — keep one (backends.claude.windowReserve is the current spelling; claudeWindowReserve is its old name)");
486
- // Cross-field (judged on the merged config, where both sides are always present; on a raw
487
- // file only when the file itself names both): scheduleBackoff clamps every idle wait — the
488
- // first included — to min(initialSeconds, maxSeconds), so a smaller max silently discards
489
- // the configured initial wait. Name both values so one edit fixes the pair.
490
- const backoff = r.idleBackoff;
491
- if (isJsonObject(backoff) &&
492
- typeof backoff.initialSeconds === "number" &&
493
- typeof backoff.maxSeconds === "number" &&
494
- backoff.maxSeconds < backoff.initialSeconds)
495
- problems.push(`idleBackoff.maxSeconds (${show(backoff.maxSeconds)}) must be ≥ idleBackoff.initialSeconds (${show(backoff.initialSeconds)}) — every idle wait is clamped to the smaller, so the smaller max would silently shorten the configured first wait`);
496
301
  if (problems.length > 0) {
497
302
  throw new Error(`invalid ${label}:\n - ${problems.join("\n - ")}`);
498
303
  }
@@ -109,12 +109,7 @@ export function tierModel(config, tier) {
109
109
  * key it overrides would.
110
110
  *
111
111
  * `tier` is the seam's effective tier, already resolved by the caller from any tier-name
112
- * override — so `o.model` here is always a selector string, never a tier reference.
113
- *
114
- * Exported so imageRerouteTarget (src/agent/image-input.ts) can probe a peer tier's
115
- * top-level model (`withModelOverrides(config, tier, {})`, the same shape resolverConfig
116
- * builds strong with) and reshape a tier's fallback pair into a runnable config — the
117
- * tier-config builder the plan names. */
112
+ * override — so `o.model` here is always a selector string, never a tier reference. */
118
113
  export function withModelOverrides(config, tier, o) {
119
114
  // The top-level model resolves at the seam's tier: a string form is the default tier's
120
115
  // selector; a map form takes the tier's entry, else `default`'s.
@@ -204,7 +199,7 @@ export function configForRole(config, role) {
204
199
  }
205
200
  /** The config a role's runs SCHEDULE from: configForRole, except that while PLANS.md still
206
201
  * has planning room (planBacklogOpen — below the plan role's eligible-plan target, or holding
207
- * a plan that needs review/needs replan) the plan role schedules with a ZERO min gap, so a
202
+ * a plan that needs review) the plan role schedules with a ZERO min gap, so a
208
203
  * productive plan run reruns on the next poll and an unproductive one drops to the idle
209
204
  * backoff. The plan role's own machine signal is `planBacklogFull` (src/scheduling/claims.ts):
210
205
  * a fresh project's empty backlog is not full, so this subsumes the old placeholder-status