@agentwhy/cli 0.0.0-dev

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 (400) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +181 -0
  4. package/THIRD_PARTY_NOTICES.md +31 -0
  5. package/dist/adapter/claude-code/contract/assumptions.js +208 -0
  6. package/dist/adapter/claude-code/contract/denials.js +15 -0
  7. package/dist/adapter/claude-code/contract/entry-points.js +15 -0
  8. package/dist/adapter/claude-code/contract/fields.js +73 -0
  9. package/dist/adapter/claude-code/contract/hooks.js +96 -0
  10. package/dist/adapter/claude-code/contract/identifiers.js +15 -0
  11. package/dist/adapter/claude-code/contract/layout.js +29 -0
  12. package/dist/adapter/claude-code/contract/line-types.js +26 -0
  13. package/dist/adapter/claude-code/contract/projects.js +23 -0
  14. package/dist/adapter/claude-code/contract/recognition.js +14 -0
  15. package/dist/adapter/claude-code/contract/session-title.js +20 -0
  16. package/dist/adapter/claude-code/contract/settings.js +27 -0
  17. package/dist/adapter/claude-code/contract/task-notifications.js +33 -0
  18. package/dist/adapter/claude-code/contract/tools.js +37 -0
  19. package/dist/adapter/claude-code/contract/version.js +51 -0
  20. package/dist/adapter/claude-code/discovery/claude-code-project-catalogue.js +129 -0
  21. package/dist/adapter/claude-code/discovery/claude-code-recognition.js +46 -0
  22. package/dist/adapter/claude-code/discovery/claude-code-session-catalogue.js +100 -0
  23. package/dist/adapter/claude-code/discovery/claude-code-session-discovery.js +61 -0
  24. package/dist/adapter/claude-code/discovery/claude-code-session-titles.js +28 -0
  25. package/dist/adapter/claude-code/discovery/discovered-session.js +1 -0
  26. package/dist/adapter/claude-code/discovery/session-entries.js +41 -0
  27. package/dist/adapter/claude-code/discovery/transcript-tail.js +81 -0
  28. package/dist/adapter/claude-code/events/claude-code-session-source.js +197 -0
  29. package/dist/adapter/claude-code/events/delegation-index.js +15 -0
  30. package/dist/adapter/claude-code/events/transcript-scan.js +179 -0
  31. package/dist/adapter/claude-code/hooks/hook-output.js +42 -0
  32. package/dist/adapter/claude-code/hooks/pre-tool-use-input.js +23 -0
  33. package/dist/adapter/claude-code/hooks/stop-input.js +25 -0
  34. package/dist/adapter/claude-code/hooks/subagent-stop-input.js +26 -0
  35. package/dist/adapter/claude-code/policy/deny-rules.js +46 -0
  36. package/dist/adapter/claude-code/probe/claude-code-probe.js +86 -0
  37. package/dist/adapter/claude-code/probe/collectors/agent-tool-input-key-collector.js +24 -0
  38. package/dist/adapter/claude-code/probe/collectors/denial-kind-collector.js +20 -0
  39. package/dist/adapter/claude-code/probe/collectors/denied-call-collector.js +11 -0
  40. package/dist/adapter/claude-code/probe/collectors/identifier-collector.js +19 -0
  41. package/dist/adapter/claude-code/probe/collectors/line-collector.js +1 -0
  42. package/dist/adapter/claude-code/probe/collectors/line-type-collector.js +29 -0
  43. package/dist/adapter/claude-code/probe/collectors/sidechain-collector.js +20 -0
  44. package/dist/adapter/claude-code/probe/collectors/task-notification-collector.js +51 -0
  45. package/dist/adapter/claude-code/probe/collectors/tool-result-reference-collector.js +31 -0
  46. package/dist/adapter/claude-code/probe/collectors/tool-use-collector.js +24 -0
  47. package/dist/adapter/claude-code/probe/collectors/tool-version-collector.js +14 -0
  48. package/dist/adapter/claude-code/probe/collectors/top-level-key-collector.js +14 -0
  49. package/dist/adapter/claude-code/probe/collectors/unknown-line-type-key-collector.js +24 -0
  50. package/dist/adapter/claude-code/probe/collectors/working-directory-collector.js +28 -0
  51. package/dist/adapter/claude-code/probe/doctor-report.js +1 -0
  52. package/dist/adapter/claude-code/probe/meta-tally.js +42 -0
  53. package/dist/adapter/claude-code/probe/session-probe.js +1 -0
  54. package/dist/adapter/claude-code/probe/session-tally.js +41 -0
  55. package/dist/adapter/claude-code/probe/transcript-line.js +30 -0
  56. package/dist/adapter/claude-code/probe/transcript-tally.js +74 -0
  57. package/dist/adapter/claude-code/settings/deny-entries.js +65 -0
  58. package/dist/adapter/claude-code/settings/hook-entries.js +303 -0
  59. package/dist/adapter/codex/contract/actions.js +69 -0
  60. package/dist/adapter/codex/contract/assumptions.js +55 -0
  61. package/dist/adapter/codex/contract/capabilities.js +24 -0
  62. package/dist/adapter/codex/contract/delegations.js +30 -0
  63. package/dist/adapter/codex/contract/deliveries.js +25 -0
  64. package/dist/adapter/codex/contract/envelope.js +33 -0
  65. package/dist/adapter/codex/contract/hook-refusals.js +14 -0
  66. package/dist/adapter/codex/contract/hooks.js +46 -0
  67. package/dist/adapter/codex/contract/messages.js +47 -0
  68. package/dist/adapter/codex/contract/permissions.js +27 -0
  69. package/dist/adapter/codex/contract/reviews.js +13 -0
  70. package/dist/adapter/codex/contract/session.js +31 -0
  71. package/dist/adapter/codex/contract/turns.js +23 -0
  72. package/dist/adapter/codex/contract/version.js +15 -0
  73. package/dist/adapter/codex/discovery/codex-conversations.js +44 -0
  74. package/dist/adapter/codex/discovery/codex-project-catalogue.js +49 -0
  75. package/dist/adapter/codex/discovery/codex-recognition.js +20 -0
  76. package/dist/adapter/codex/discovery/codex-session-catalogue.js +28 -0
  77. package/dist/adapter/codex/discovery/codex-session-discovery.js +115 -0
  78. package/dist/adapter/codex/discovery/codex-session-index.js +21 -0
  79. package/dist/adapter/codex/discovery/codex-session-titles.js +135 -0
  80. package/dist/adapter/codex/discovery/session-header.js +86 -0
  81. package/dist/adapter/codex/discovery/session-owners.js +11 -0
  82. package/dist/adapter/codex/discovery/session-roots.js +49 -0
  83. package/dist/adapter/codex/events/action-items.js +138 -0
  84. package/dist/adapter/codex/events/capability-records.js +26 -0
  85. package/dist/adapter/codex/events/codex-session-source.js +121 -0
  86. package/dist/adapter/codex/events/hook-refusals.js +27 -0
  87. package/dist/adapter/codex/events/rollout-scan.js +614 -0
  88. package/dist/adapter/codex/events/turn-permissions.js +60 -0
  89. package/dist/adapter/codex/hooks/pre-tool-use-input.js +24 -0
  90. package/dist/adapter/codex/hooks/stop-refusals.js +57 -0
  91. package/dist/adapter/codex/probe/codex-doctor-report.js +1 -0
  92. package/dist/adapter/codex/probe/codex-probe.js +268 -0
  93. package/dist/adapter/codex/settings/codex-hooks.js +83 -0
  94. package/dist/cli/cli-command.js +1 -0
  95. package/dist/cli/command-router.js +42 -0
  96. package/dist/cli/commands/check-cli-command.js +128 -0
  97. package/dist/cli/commands/codex-stop-cli-command.js +27 -0
  98. package/dist/cli/commands/doctor-arguments.js +32 -0
  99. package/dist/cli/commands/doctor-cli-command.js +31 -0
  100. package/dist/cli/commands/doctor-usage.js +14 -0
  101. package/dist/cli/commands/init-cli-command.js +123 -0
  102. package/dist/cli/commands/menu-cli-command.js +58 -0
  103. package/dist/cli/commands/notify-cli-command.js +158 -0
  104. package/dist/cli/commands/refuse-cli-command.js +79 -0
  105. package/dist/cli/commands/report-arguments.js +62 -0
  106. package/dist/cli/commands/report-cli-command.js +36 -0
  107. package/dist/cli/commands/report-usage.js +41 -0
  108. package/dist/cli/commands/sessions-cli-command.js +184 -0
  109. package/dist/cli/commands/start-cli-command.js +83 -0
  110. package/dist/cli/commands/watch-cli-command.js +142 -0
  111. package/dist/cli/exit-codes.js +13 -0
  112. package/dist/cli/project-switches.js +46 -0
  113. package/dist/cli.js +80 -0
  114. package/dist/composition-root.js +411 -0
  115. package/dist/core/access/command-line.js +454 -0
  116. package/dist/core/access/listing.js +58 -0
  117. package/dist/core/access/path-shape.js +59 -0
  118. package/dist/core/access/path-tokens.js +37 -0
  119. package/dist/core/access/protected-access.js +310 -0
  120. package/dist/core/access/protected-values.js +102 -0
  121. package/dist/core/access/recorded-effect.js +67 -0
  122. package/dist/core/access/returns.js +173 -0
  123. package/dist/core/access/search-output.js +34 -0
  124. package/dist/core/access/search-reach.js +268 -0
  125. package/dist/core/access/uses.js +143 -0
  126. package/dist/core/access/words-before.js +52 -0
  127. package/dist/core/agent.js +1 -0
  128. package/dist/core/capability.js +11 -0
  129. package/dist/core/combined-project-catalogue.js +36 -0
  130. package/dist/core/combined-session-catalogue.js +25 -0
  131. package/dist/core/completeness.js +10 -0
  132. package/dist/core/context.js +1 -0
  133. package/dist/core/correlation/correlate.js +329 -0
  134. package/dist/core/delegation.js +1 -0
  135. package/dist/core/detector/prompt-signals.js +78 -0
  136. package/dist/core/entry-point.js +1 -0
  137. package/dist/core/event.js +1 -0
  138. package/dist/core/evidence.js +9 -0
  139. package/dist/core/late-message.js +32 -0
  140. package/dist/core/message.js +1 -0
  141. package/dist/core/nearest-project.js +28 -0
  142. package/dist/core/policy/default-policy.js +32 -0
  143. package/dist/core/policy/glob.js +56 -0
  144. package/dist/core/policy/parse-policy.js +70 -0
  145. package/dist/core/policy/policy.js +20 -0
  146. package/dist/core/policy/resolve-policy.js +16 -0
  147. package/dist/core/project-catalogue.js +1 -0
  148. package/dist/core/project-root.js +133 -0
  149. package/dist/core/provider-titles.js +10 -0
  150. package/dist/core/redaction/entropy.js +46 -0
  151. package/dist/core/redaction/redacted.js +4 -0
  152. package/dist/core/redaction/redactor.js +339 -0
  153. package/dist/core/redaction/secret-patterns.js +22 -0
  154. package/dist/core/redaction/value-shapes.js +81 -0
  155. package/dist/core/redaction/value-trace.js +110 -0
  156. package/dist/core/review.js +1 -0
  157. package/dist/core/session-catalogue.js +19 -0
  158. package/dist/core/session-filter.js +42 -0
  159. package/dist/core/session-format.js +27 -0
  160. package/dist/core/session-model.js +1 -0
  161. package/dist/core/session-reader.js +22 -0
  162. package/dist/core/session-records.js +1 -0
  163. package/dist/core/session-source.js +1 -0
  164. package/dist/core/session-titles.js +21 -0
  165. package/dist/core/turn.js +1 -0
  166. package/dist/doctor/codex-doctor.js +14 -0
  167. package/dist/doctor/doctor-use-case.js +1 -0
  168. package/dist/doctor/format-selecting-doctor.js +28 -0
  169. package/dist/doctor/render/attention/attention-rule.js +1 -0
  170. package/dist/doctor/render/attention/doctor-attention-rules.js +28 -0
  171. package/dist/doctor/render/attention/rules/damaged-meta-files-rule.js +8 -0
  172. package/dist/doctor/render/attention/rules/damaged-transcripts-rule.js +9 -0
  173. package/dist/doctor/render/attention/rules/incomplete-subagent-pairs-rule.js +6 -0
  174. package/dist/doctor/render/attention/rules/missing-main-transcript-rule.js +5 -0
  175. package/dist/doctor/render/attention/rules/missing-spilled-results-rule.js +6 -0
  176. package/dist/doctor/render/attention/rules/multiple-working-directories-rule.js +15 -0
  177. package/dist/doctor/render/attention/rules/unknown-denial-kinds-rule.js +5 -0
  178. package/dist/doctor/render/attention/rules/unknown-line-types-rule.js +6 -0
  179. package/dist/doctor/render/attention/rules/unrecognised-entries-rule.js +6 -0
  180. package/dist/doctor/render/attention/rules/unusable-directories-rule.js +12 -0
  181. package/dist/doctor/render/attention/transcript-scopes.js +6 -0
  182. package/dist/doctor/render/codex-text-doctor-renderer.js +97 -0
  183. package/dist/doctor/render/doctor-renderers.js +1 -0
  184. package/dist/doctor/render/json-doctor-renderer.js +7 -0
  185. package/dist/doctor/render/output-format.js +1 -0
  186. package/dist/doctor/render/text-doctor-renderer.js +124 -0
  187. package/dist/doctor/session-doctor.js +16 -0
  188. package/dist/infrastructure/clack-asker.js +24 -0
  189. package/dist/infrastructure/clack-chooser.js +40 -0
  190. package/dist/infrastructure/clack-multi-chooser.js +31 -0
  191. package/dist/infrastructure/file-alert-store.js +120 -0
  192. package/dist/infrastructure/file-checked-store.js +55 -0
  193. package/dist/infrastructure/file-mark-store.js +67 -0
  194. package/dist/infrastructure/file-onboarding-store.js +93 -0
  195. package/dist/infrastructure/folder-window.js +26 -0
  196. package/dist/infrastructure/node-agentwhy-invocation.js +99 -0
  197. package/dist/infrastructure/node-browser.js +40 -0
  198. package/dist/infrastructure/node-file-system.js +111 -0
  199. package/dist/infrastructure/node-local-server.js +115 -0
  200. package/dist/infrastructure/node-text-input.js +26 -0
  201. package/dist/infrastructure/os-notifier.js +21 -0
  202. package/dist/infrastructure/osascript-folder-chooser.js +53 -0
  203. package/dist/infrastructure/powershell-folder-chooser.js +78 -0
  204. package/dist/infrastructure/real-directory.js +18 -0
  205. package/dist/infrastructure/stream-printer.js +11 -0
  206. package/dist/infrastructure/terminal-banner.js +19 -0
  207. package/dist/ports/agentwhy-invocation.js +1 -0
  208. package/dist/ports/alert-store.js +1 -0
  209. package/dist/ports/asker.js +1 -0
  210. package/dist/ports/browser.js +1 -0
  211. package/dist/ports/checked-store.js +1 -0
  212. package/dist/ports/chooser.js +1 -0
  213. package/dist/ports/directory-reader.js +1 -0
  214. package/dist/ports/file-access-error.js +11 -0
  215. package/dist/ports/file-reader.js +1 -0
  216. package/dist/ports/file-tail-reader.js +1 -0
  217. package/dist/ports/file-writer.js +1 -0
  218. package/dist/ports/folder-chooser.js +1 -0
  219. package/dist/ports/local-server.js +1 -0
  220. package/dist/ports/mark-store.js +10 -0
  221. package/dist/ports/multi-chooser.js +1 -0
  222. package/dist/ports/notifier.js +1 -0
  223. package/dist/ports/onboarding-store.js +1 -0
  224. package/dist/ports/printer.js +1 -0
  225. package/dist/ports/text-input.js +1 -0
  226. package/dist/refuse/command-reach.js +135 -0
  227. package/dist/refuse/command-refusal.js +88 -0
  228. package/dist/refuse/render/codex-stop-words.js +21 -0
  229. package/dist/refuse/render/refusal-words.js +34 -0
  230. package/dist/report/build-report.js +1037 -0
  231. package/dist/report/check/actions-digest.js +73 -0
  232. package/dist/report/check/check-lines.js +83 -0
  233. package/dist/report/check/mark-request.js +90 -0
  234. package/dist/report/check/marks.js +84 -0
  235. package/dist/report/check/render/text-digest-renderer.js +332 -0
  236. package/dist/report/check/session-actions.js +90 -0
  237. package/dist/report/check/session-check.js +91 -0
  238. package/dist/report/choose-policy.js +61 -0
  239. package/dist/report/flow-reads.js +18 -0
  240. package/dist/report/private-files/tell-lists.js +84 -0
  241. package/dist/report/project-rules.js +33 -0
  242. package/dist/report/render/home-relative.js +18 -0
  243. package/dist/report/render/html-head.js +26 -0
  244. package/dist/report/render/html-report-components.js +48 -0
  245. package/dist/report/render/html-report-styles.js +500 -0
  246. package/dist/report/render/html-sidebar.js +42 -0
  247. package/dist/report/render/path-tail.js +14 -0
  248. package/dist/report/render/report-copy.js +1177 -0
  249. package/dist/report/render/report-page/advanced-view.js +167 -0
  250. package/dist/report/render/report-page/file-story.js +114 -0
  251. package/dist/report/render/report-page/files-view.js +381 -0
  252. package/dist/report/render/report-page/files.js +77 -0
  253. package/dist/report/render/report-page/fix-wizard-script.js +269 -0
  254. package/dist/report/render/report-page/fix-wizard.js +310 -0
  255. package/dist/report/render/report-page/helpers-view.js +383 -0
  256. package/dist/report/render/report-page/helpers.js +51 -0
  257. package/dist/report/render/report-page/item-names.js +63 -0
  258. package/dist/report/render/report-page/protect-patterns.js +13 -0
  259. package/dist/report/render/report-page/providers.js +208 -0
  260. package/dist/report/render/report-page/record-view.js +367 -0
  261. package/dist/report/render/report-page/report-page-renderer.js +129 -0
  262. package/dist/report/render/report-page/report-views.js +31 -0
  263. package/dist/report/render/report-page/story-window.js +356 -0
  264. package/dist/report/render/report-page/times.js +25 -0
  265. package/dist/report/render/report-page/to-do-view.js +299 -0
  266. package/dist/report/render/report-page/to-do.js +152 -0
  267. package/dist/report/render/report-page.js +1 -0
  268. package/dist/report/render/session-view.js +250 -0
  269. package/dist/report/render/text-report-renderer.js +981 -0
  270. package/dist/report/render/ui/add-file-popup.js +122 -0
  271. package/dist/report/render/ui/app-sidebar.js +78 -0
  272. package/dist/report/render/ui/ask-panel.js +80 -0
  273. package/dist/report/render/ui/avatar.js +19 -0
  274. package/dist/report/render/ui/backdrop.js +29 -0
  275. package/dist/report/render/ui/brand-mark.js +4 -0
  276. package/dist/report/render/ui/button.js +67 -0
  277. package/dist/report/render/ui/callout.js +20 -0
  278. package/dist/report/render/ui/chat-example.js +17 -0
  279. package/dist/report/render/ui/checklist.js +50 -0
  280. package/dist/report/render/ui/confirm-dialog.js +34 -0
  281. package/dist/report/render/ui/data-table.js +49 -0
  282. package/dist/report/render/ui/drawer.js +25 -0
  283. package/dist/report/render/ui/file-chip.js +37 -0
  284. package/dist/report/render/ui/fold-line.js +22 -0
  285. package/dist/report/render/ui/guide-card.js +25 -0
  286. package/dist/report/render/ui/hero.js +37 -0
  287. package/dist/report/render/ui/labelled-select.js +10 -0
  288. package/dist/report/render/ui/live-script.js +222 -0
  289. package/dist/report/render/ui/local-date.js +58 -0
  290. package/dist/report/render/ui/mode-icon.js +6 -0
  291. package/dist/report/render/ui/page-shell.js +101 -0
  292. package/dist/report/render/ui/pill-tabs.js +47 -0
  293. package/dist/report/render/ui/popup.js +77 -0
  294. package/dist/report/render/ui/progress.js +35 -0
  295. package/dist/report/render/ui/project-list.js +284 -0
  296. package/dist/report/render/ui/scope-choice.js +19 -0
  297. package/dist/report/render/ui/stats.js +21 -0
  298. package/dist/report/render/ui/status-icon.js +30 -0
  299. package/dist/report/render/ui/status-look.js +21 -0
  300. package/dist/report/render/ui/switch.js +11 -0
  301. package/dist/report/render/ui/tag.js +19 -0
  302. package/dist/report/render/ui/task-list.js +56 -0
  303. package/dist/report/render/ui/tokens.js +46 -0
  304. package/dist/report/render/ui/update-notice.js +146 -0
  305. package/dist/report/render/ui/words/app-words.js +163 -0
  306. package/dist/report/render/ui/words/conversations-words.js +457 -0
  307. package/dist/report/render/ui/words/month-words.js +59 -0
  308. package/dist/report/render/ui/words/onboarding-words.js +446 -0
  309. package/dist/report/render/ui/words/projects-words.js +217 -0
  310. package/dist/report/render/ui/words/report-words.js +2099 -0
  311. package/dist/report/render/ui/words/settings-words.js +611 -0
  312. package/dist/report/render/ui/words/to-fix-words.js +287 -0
  313. package/dist/report/report-model.js +2 -0
  314. package/dist/report/report-use-case.js +1 -0
  315. package/dist/report/rule-names.js +20 -0
  316. package/dist/report/session-report.js +163 -0
  317. package/dist/report/start/app-nav.js +37 -0
  318. package/dist/report/start/conversations/calendar-view.js +104 -0
  319. package/dist/report/start/conversations/conversation-columns.js +187 -0
  320. package/dist/report/start/conversations/conversations-renderer.js +54 -0
  321. package/dist/report/start/conversations/empty-week.js +120 -0
  322. package/dist/report/start/conversations/files-window.js +166 -0
  323. package/dist/report/start/conversations/period-section.js +280 -0
  324. package/dist/report/start/conversations/periods-script.js +440 -0
  325. package/dist/report/start/conversations/periods.js +110 -0
  326. package/dist/report/start/conversations/week-view.js +30 -0
  327. package/dist/report/start/conversations/weeks.js +17 -0
  328. package/dist/report/start/month/month-renderer.js +50 -0
  329. package/dist/report/start/month/month-view.js +135 -0
  330. package/dist/report/start/month/months.js +17 -0
  331. package/dist/report/start/onboarding/done.js +171 -0
  332. package/dist/report/start/onboarding/finish-onboarding.js +93 -0
  333. package/dist/report/start/onboarding/intro.js +100 -0
  334. package/dist/report/start/onboarding/onboarding-changes.js +40 -0
  335. package/dist/report/start/onboarding/onboarding-renderer.js +195 -0
  336. package/dist/report/start/onboarding/onboarding-script.js +400 -0
  337. package/dist/report/start/onboarding/onboarding-view.js +98 -0
  338. package/dist/report/start/onboarding/project-step.js +210 -0
  339. package/dist/report/start/onboarding/steps.js +239 -0
  340. package/dist/report/start/onboarding/welcome.js +66 -0
  341. package/dist/report/start/page-notice.js +10 -0
  342. package/dist/report/start/projects/index-projects.js +43 -0
  343. package/dist/report/start/projects/moved-page.js +82 -0
  344. package/dist/report/start/projects/project-switch.js +138 -0
  345. package/dist/report/start/projects/projects-window.js +67 -0
  346. package/dist/report/start/projects/temporary-space.js +18 -0
  347. package/dist/report/start/render/air-datepicker-vendor.js +27 -0
  348. package/dist/report/start/render/session-status.js +41 -0
  349. package/dist/report/start/render/start-words.js +84 -0
  350. package/dist/report/start/repository.js +28 -0
  351. package/dist/report/start/serve/index-handler.js +372 -0
  352. package/dist/report/start/serve/onboarding-request.js +50 -0
  353. package/dist/report/start/serve/settings-request.js +1 -0
  354. package/dist/report/start/serve/settings-setup.js +193 -0
  355. package/dist/report/start/session-index.js +1 -0
  356. package/dist/report/start/session-start.js +974 -0
  357. package/dist/report/start/settings/alerts-tab.js +104 -0
  358. package/dist/report/start/settings/files-tab.js +169 -0
  359. package/dist/report/start/settings/general-tab.js +71 -0
  360. package/dist/report/start/settings/settings-renderer.js +254 -0
  361. package/dist/report/start/settings/settings-script.js +161 -0
  362. package/dist/report/start/settings/settings-view.js +240 -0
  363. package/dist/report/start/settings/settings-windows.js +334 -0
  364. package/dist/report/start/settings-files.js +43 -0
  365. package/dist/report/start/to-fix/done-list.js +45 -0
  366. package/dist/report/start/to-fix/file-window.js +110 -0
  367. package/dist/report/start/to-fix/to-fix-list.js +70 -0
  368. package/dist/report/start/to-fix/to-fix-renderer.js +237 -0
  369. package/dist/report/start/to-fix/to-fix-script.js +94 -0
  370. package/dist/report/start/to-fix/to-fix-view.js +124 -0
  371. package/dist/report/watch/agent-alert.js +72 -0
  372. package/dist/report/watch/notice-choices.js +35 -0
  373. package/dist/report/watch/notice-settings.js +65 -0
  374. package/dist/report/watch/preferences.js +100 -0
  375. package/dist/report/watch/render/instruction-words.js +54 -0
  376. package/dist/report/watch/render/notice-words.js +182 -0
  377. package/dist/report/watch/subagent-watch.js +345 -0
  378. package/dist/report/watch/watch-notice.js +1 -0
  379. package/dist/setup/behind.js +18 -0
  380. package/dist/setup/codex-mirror.js +171 -0
  381. package/dist/setup/not-a-project.js +19 -0
  382. package/dist/setup/project-setup.js +644 -0
  383. package/dist/setup/protected-patterns.js +30 -0
  384. package/dist/shared/colour.js +23 -0
  385. package/dist/shared/compare.js +4 -0
  386. package/dist/shared/content-version.js +13 -0
  387. package/dist/shared/counter.js +11 -0
  388. package/dist/shared/file-stamp.js +4 -0
  389. package/dist/shared/json.js +13 -0
  390. package/dist/shared/label.js +13 -0
  391. package/dist/shared/package-name.js +6 -0
  392. package/dist/shared/plain-invocation.js +44 -0
  393. package/dist/shared/plain-version.js +21 -0
  394. package/dist/shared/printable.js +25 -0
  395. package/dist/shared/renderer.js +1 -0
  396. package/dist/shared/sentence.js +20 -0
  397. package/dist/shared/terminal-logo.js +41 -0
  398. package/dist/shared/text-table.js +15 -0
  399. package/dist/shared/wrap.js +29 -0
  400. package/package.json +70 -0
@@ -0,0 +1,372 @@
1
+ import { ALERT_THRESHOLDS } from '../../watch/agent-alert.js';
2
+ import { CLEAN_MODES, NOTICE_CHANNELS, NOTICE_LANGS, SAID_AS } from '../../watch/notice-choices.js';
3
+ import { isMarkResult, MARK_RESULTS } from '../../../ports/mark-store.js';
4
+ import { onboardingChoices } from './onboarding-request.js';
5
+ import { LANGS } from '../../render/report-copy.js';
6
+ /** The one other value a page may send; anything else is the window, which asks nothing of the run it starts. */
7
+ function switchFrom(value) {
8
+ return value === 'step' ? 'step' : 'window';
9
+ }
10
+ const TEXT = 'text/plain; charset=utf-8';
11
+ const JSON_TYPE = 'application/json; charset=utf-8';
12
+ const VERSION_ROUTE = 'api/version/';
13
+ /**
14
+ * A page with its version on its root element (L2), and for the index how many conversations it was sent with, so the
15
+ * pill can say how many are new (L8). A version is base 36 and a count a number: neither needs escaping.
16
+ */
17
+ function withVersion(html, version) {
18
+ const count = version.conversations === undefined ? '' : ' data-conversations="' + version.conversations + '"';
19
+ return html.replace(/<html /, '<html data-version="' + version.version + '"' + count + ' ');
20
+ }
21
+ /**
22
+ * The page's server as a function of a request (`worth-running-every-day` R50-R53): a name it was not given, a path
23
+ * without the token, a file this run did not write, a write from another origin - each gets nothing. What is left is
24
+ * reading the pages and the two writes a person can make from them.
25
+ */
26
+ export function indexHandler(dependencies) {
27
+ const { origin, token } = dependencies;
28
+ const host = origin.replace(/^http:\/\//, '');
29
+ const prefix = `/${token}/`;
30
+ return async (request) => {
31
+ // A name that merely resolves to this machine is not the address the page was opened on (DNS rebinding).
32
+ if (request.headers.host !== host)
33
+ return { status: 403, type: TEXT, body: 'Forbidden.' };
34
+ if (!request.path.startsWith(prefix)) {
35
+ const other = /^\/([^/]+)\/(.*)$/.exec(request.path);
36
+ const past = other === null ? undefined : dependencies.pastRun?.(other[1] ?? '');
37
+ return past === undefined ? notFound() : pastRunAnswer(past, other?.[2] ?? '', request, dependencies, prefix);
38
+ }
39
+ const rest = request.path.slice(prefix.length);
40
+ if (request.method === 'GET') {
41
+ // live-pages L3: a page asks whether its file is still the version it was sent. The refresh ran before this
42
+ // handler was reached (the server runs it for every page and every version asked for); this only answers.
43
+ if (rest.startsWith(VERSION_ROUTE)) {
44
+ // Empty is the page opened at the served root: the index, as a `GET` of the root is.
45
+ const asked = rest.slice(VERSION_ROUTE.length) || 'index.html';
46
+ const version = dependencies.files.has(asked) ? dependencies.versionOf?.(asked) : undefined;
47
+ return version === undefined ? notFound() : { status: 200, type: JSON_TYPE, body: JSON.stringify(version) };
48
+ }
49
+ const name = rest === '' ? 'index.html' : rest;
50
+ if (!dependencies.files.has(name))
51
+ return notFound();
52
+ const body = await dependencies.read(name);
53
+ // L2: the page carries the version it was sent at, added as it is sent, so the file on disk - and the version
54
+ // hashed from it - is exactly what the renderer wrote.
55
+ const version = dependencies.versionOf?.(name);
56
+ return { status: 200, type: 'text/html; charset=utf-8', body: version === undefined ? body : withVersion(body, version) };
57
+ }
58
+ const routes = [
59
+ 'api/mark',
60
+ 'api/unmark',
61
+ ...(dependencies.settings === undefined ? [] : ['api/settings']),
62
+ ...(dependencies.notify === undefined ? [] : ['api/notify']),
63
+ ...(dependencies.include === undefined ? [] : ['api/include']),
64
+ ...(dependencies.onboarding === undefined ? [] : ['api/onboarding']),
65
+ ...(dependencies.switchProject === undefined ? [] : ['api/switch-project']),
66
+ ...(dependencies.chooseFolder === undefined ? [] : ['api/choose-folder']),
67
+ ];
68
+ if (request.method !== 'POST' || !routes.includes(rest))
69
+ return notFound();
70
+ if (request.headers.origin !== origin)
71
+ return { status: 403, type: TEXT, body: 'Forbidden.' };
72
+ if (!(request.headers['content-type'] ?? '').startsWith('application/json'))
73
+ return { status: 415, type: TEXT, body: 'JSON only.' };
74
+ let body;
75
+ try {
76
+ body = JSON.parse(request.body);
77
+ }
78
+ catch {
79
+ return reply(400, 'The request was not JSON.');
80
+ }
81
+ if (body === null || typeof body !== 'object')
82
+ return reply(400, 'The request was not an object.');
83
+ const fields = body;
84
+ if (rest === 'api/notify') {
85
+ const change = noticeChange(fields);
86
+ if (typeof change === 'string')
87
+ return reply(400, change);
88
+ const answer = await dependencies.notify(change);
89
+ if (!answer.written)
90
+ return reply(500, answer.said);
91
+ await dependencies.rerender();
92
+ return { status: 200, type: JSON_TYPE, body: JSON.stringify({ ok: true, message: answer.said }) };
93
+ }
94
+ if (rest === 'api/choose-folder') {
95
+ const lang = LANGS.find((one) => one === fields.lang) ?? 'en';
96
+ const answer = await dependencies.chooseFolder(lang, switchFrom(fields.from));
97
+ if (answer.kind === 'failed')
98
+ return reply(422, answer.reason);
99
+ // A folder switched to is this run's end, as a switch by id is (V18): the answer is sent whole first.
100
+ return {
101
+ status: 200,
102
+ type: JSON_TYPE,
103
+ body: JSON.stringify({ ok: true, ...answer }),
104
+ ...(answer.kind === 'switched' && dependencies.switched !== undefined ? { after: dependencies.switched } : {}),
105
+ };
106
+ }
107
+ if (rest === 'api/switch-project') {
108
+ if (typeof fields.id !== 'string' || fields.id === '' || fields.id.length > 1024)
109
+ return reply(400, 'A project is named.');
110
+ const answer = await dependencies.switchProject(fields.id, switchFrom(fields.from));
111
+ if ('failed' in answer)
112
+ return reply(422, answer.failed);
113
+ // The answer is sent whole before this run's server closes: closing first would cut it.
114
+ return { status: 200, type: JSON_TYPE, body: JSON.stringify({ ok: true, url: answer.url }), ...(dependencies.switched === undefined ? {} : { after: dependencies.switched }) };
115
+ }
116
+ if (rest === 'api/include') {
117
+ // F55: one name, and nothing else; whether it may be written is the run's to say, which lists what it left out.
118
+ if (typeof fields.name !== 'string' || fields.name === '' || fields.name.length > 200)
119
+ return reply(400, 'A conversation is named.');
120
+ const answer = await dependencies.include(fields.name);
121
+ if (!answer.written)
122
+ return reply(422, answer.said);
123
+ await dependencies.rerender();
124
+ return { status: 200, type: JSON_TYPE, body: JSON.stringify({ ok: true, message: answer.said }) };
125
+ }
126
+ if (rest === 'api/onboarding') {
127
+ const choices = onboardingChoices(fields);
128
+ if (typeof choices === 'string')
129
+ return reply(400, choices);
130
+ const answer = await dependencies.onboarding(choices);
131
+ // A name that cannot be written refuses the whole request: nothing was written, nothing recorded (R59).
132
+ if (answer.outcome === 'refused')
133
+ return reply(422, answer.message);
134
+ // Handled, whatever each change answered: the page names the ones that failed (W20), and every page is read again.
135
+ await dependencies.rerender();
136
+ return { status: 200, type: JSON_TYPE, body: JSON.stringify({ ok: true, results: answer.results, recorded: answer.recorded, ...(answer.codex === undefined ? {} : { codex: answer.codex }) }) };
137
+ }
138
+ if (rest === 'api/settings') {
139
+ const change = settingsChange(fields);
140
+ if (typeof change === 'string')
141
+ return reply(400, change);
142
+ // R58: the page's confirm step is the consent, so the same setup a terminal runs is run with `yes`.
143
+ const answer = await dependencies.settings(change);
144
+ // R59: a refusal says why and writes nothing; R60: a write is followed by the page being read again.
145
+ if (answer.outcome !== 'written' && answer.outcome !== 'unchanged') {
146
+ return reply(answer.outcome === 'refused' ? 422 : 500, answer.output.trim());
147
+ }
148
+ await dependencies.rerender();
149
+ return { status: 200, type: JSON_TYPE, body: JSON.stringify({ ok: true, message: answer.output.trim() }) };
150
+ }
151
+ if (typeof fields.path !== 'string' || fields.path === '')
152
+ return reply(400, 'A path is required.');
153
+ let answer;
154
+ if (rest === 'api/mark') {
155
+ const result = fields.result;
156
+ if (!isMarkResult(result))
157
+ return reply(400, `The result is ${MARK_RESULTS.join(', ')}.`);
158
+ if (fields.note !== undefined && typeof fields.note !== 'string')
159
+ return reply(400, 'A note is text.');
160
+ answer = await dependencies.mark({
161
+ path: fields.path,
162
+ result,
163
+ ...(typeof fields.note === 'string' && fields.note.trim() !== '' ? { note: fields.note } : {}),
164
+ });
165
+ }
166
+ else {
167
+ answer = await dependencies.unmark(fields.path);
168
+ }
169
+ if (answer.outcome !== 'marked')
170
+ return reply(answer.outcome === 'mark-refused' ? 422 : 500, answer.output.trim());
171
+ await dependencies.rerender();
172
+ return { status: 200, type: JSON_TYPE, body: JSON.stringify({ ok: true, message: answer.output.trim() }) };
173
+ };
174
+ }
175
+ /**
176
+ * R57: one change per request, in the shape `--watch`/`--refuse`/`--protect`/`--unprotect` already give the command
177
+ * line, plus the change of a pattern, which is those last two in order. A body this does not recognise is answered
178
+ * as the bad request it is, and nothing is written.
179
+ */
180
+ function settingsChange(fields) {
181
+ // Which of the project's two settings files the change is to. Absent is the local one, which is what `init`
182
+ // writes when `--shared` is not given; anything else is a body this server does not recognise.
183
+ if (fields.where !== undefined && fields.where !== 'local' && fields.where !== 'shared') {
184
+ return 'The file is local or shared.';
185
+ }
186
+ const where = fields.where === 'shared' ? { where: 'shared' } : {};
187
+ if (fields.change === 'hooks') {
188
+ if (!Array.isArray(fields.hooks))
189
+ return 'The hooks are a list.';
190
+ const hooks = fields.hooks.filter((hook) => hook === 'watch' || hook === 'refuse');
191
+ if (hooks.length !== fields.hooks.length)
192
+ return 'A hook is watch or refuse.';
193
+ if (hooks.length === 0)
194
+ return 'A hook is required.';
195
+ if (typeof fields.on !== 'boolean')
196
+ return 'The direction is true or false.';
197
+ return { change: 'hooks', hooks, on: fields.on, ...where };
198
+ }
199
+ // The built-in list, written into the project. The patterns are named by the page rather than taken on trust
200
+ // from a body: a request that could name any pattern at all would be a way to write rules nobody chose.
201
+ if (fields.change === 'adopt') {
202
+ if (fields.where !== 'local' && fields.where !== 'shared')
203
+ return 'A change names the file it writes.';
204
+ if (!Array.isArray(fields.patterns) || fields.patterns.length === 0)
205
+ return 'The patterns are a list.';
206
+ const patterns = fields.patterns.filter((pattern) => typeof pattern === 'string' && pattern.trim() !== '');
207
+ if (patterns.length !== fields.patterns.length)
208
+ return 'A pattern is text.';
209
+ return { change: 'adopt', patterns: patterns.map((pattern) => pattern.trim()), where: fields.where };
210
+ }
211
+ // F57: one row's switch. The patterns and the rules are the page's, named as `adopt` names its patterns.
212
+ if (fields.change === 'mode') {
213
+ if (fields.where !== 'local' && fields.where !== 'shared')
214
+ return 'A change names the file it writes.';
215
+ if (fields.to !== 'block' && fields.to !== 'tell' && fields.to !== 'none')
216
+ return 'The mode is block, tell or none.';
217
+ if (!Array.isArray(fields.patterns) || fields.patterns.length === 0 || !Array.isArray(fields.rules))
218
+ return 'The patterns and the rules are lists.';
219
+ const patterns = fields.patterns.filter((pattern) => typeof pattern === 'string' && pattern.trim() !== '');
220
+ const rules = fields.rules.filter((rule) => typeof rule === 'string' && rule.trim() !== '');
221
+ if (patterns.length !== fields.patterns.length || rules.length !== fields.rules.length)
222
+ return 'A pattern is text.';
223
+ return { change: 'mode', to: fields.to, patterns: patterns.map((pattern) => pattern.trim()), rules: rules.map((rule) => rule.trim()), where: fields.where };
224
+ }
225
+ // General (F56): the rules and the hooks the page names, into one file. As with `adopt`, the page names them.
226
+ if (fields.change === 'scope') {
227
+ if (fields.where !== 'local' && fields.where !== 'shared')
228
+ return 'A change names the file it writes.';
229
+ if (!Array.isArray(fields.patterns) || !Array.isArray(fields.hooks))
230
+ return 'The patterns and the hooks are lists.';
231
+ const patterns = fields.patterns.filter((pattern) => typeof pattern === 'string' && pattern.trim() !== '');
232
+ if (patterns.length !== fields.patterns.length)
233
+ return 'A pattern is text.';
234
+ const hooks = fields.hooks.filter((hook) => hook === 'watch' || hook === 'refuse');
235
+ if (hooks.length !== fields.hooks.length)
236
+ return 'A hook is watch or refuse.';
237
+ if (patterns.length === 0 && hooks.length === 0)
238
+ return 'A change sets something.';
239
+ return { change: 'scope', patterns: patterns.map((pattern) => pattern.trim()), hooks, where: fields.where };
240
+ }
241
+ // F59: both hooks out of each file named, with the rules the page names for it. A file named with no rules still has
242
+ // its hooks taken out; a body that names no file at all asks for nothing.
243
+ if (fields.change === 'uninstall') {
244
+ const named = fields.rules;
245
+ if (typeof named !== 'object' || named === null || Array.isArray(named))
246
+ return 'The rules are listed by file.';
247
+ const rules = {};
248
+ for (const [file, list] of Object.entries(named)) {
249
+ if (file !== 'local' && file !== 'shared')
250
+ return 'The file is local or shared.';
251
+ if (!Array.isArray(list))
252
+ return 'The rules are a list.';
253
+ const patterns = list.filter((pattern) => typeof pattern === 'string' && pattern.trim() !== '');
254
+ if (patterns.length !== list.length)
255
+ return 'A pattern is text.';
256
+ rules[file] = patterns.map((pattern) => pattern.trim());
257
+ }
258
+ if (Object.keys(rules).length === 0)
259
+ return 'An uninstall names the files it takes agentwhy out of.';
260
+ return { change: 'uninstall', rules };
261
+ }
262
+ // The same rule, in the other file. `where` is the destination and is required here: a move with no file named
263
+ // is not a move. A hook is moved by a `hooks` change instead, which names the file it is to run from.
264
+ if (fields.change === 'move') {
265
+ if (fields.where !== 'local' && fields.where !== 'shared')
266
+ return 'A move names the file it goes to.';
267
+ const to = fields.where;
268
+ if (typeof fields.pattern !== 'string' || fields.pattern.trim() === '')
269
+ return 'A pattern is required.';
270
+ if (fields.from !== 'local' && fields.from !== 'shared')
271
+ return 'A move names the file it comes from.';
272
+ return { change: 'move', pattern: fields.pattern.trim(), from: fields.from, where: to };
273
+ }
274
+ // U7: the update notice names nothing; what it pins, and to what, the setup reads for itself.
275
+ if (fields.change === 'update')
276
+ return { change: 'update' };
277
+ if (fields.change === 'codex')
278
+ return { change: 'codex' };
279
+ if (fields.change === 'protect' || fields.change === 'unprotect' || fields.change === 'edit') {
280
+ if (typeof fields.pattern !== 'string' || fields.pattern.trim() === '')
281
+ return 'A pattern is required.';
282
+ if (fields.change !== 'edit')
283
+ return { change: fields.change, pattern: fields.pattern.trim(), ...where };
284
+ if (typeof fields.from !== 'string' || fields.from.trim() === '')
285
+ return 'The pattern being changed is required.';
286
+ return { change: 'edit', from: fields.from.trim(), pattern: fields.pattern.trim(), ...where };
287
+ }
288
+ return 'The change is hooks, protect, unprotect, edit, move, adopt, scope, mode, uninstall or update.';
289
+ }
290
+ /**
291
+ * R26a: the scope, and the answers to set - or that they are to be taken out. Nothing else about a person or a
292
+ * project goes in the body, and a value this version does not know is a bad request rather than a choice half
293
+ * written: someone believing they are told more than they are is the failure this whole feature exists against.
294
+ */
295
+ function noticeChange(fields) {
296
+ if (fields.scope !== 'project' && fields.scope !== 'everywhere')
297
+ return 'The scope is project or everywhere.';
298
+ const scope = fields.scope;
299
+ const on = oneOf(fields.on, ALERT_THRESHOLDS);
300
+ const clean = oneOf(fields.clean, CLEAN_MODES);
301
+ const say = oneOf(fields.say, SAID_AS);
302
+ const notify = channelsIn(fields.notify);
303
+ const lang = oneOf(fields.lang, NOTICE_LANGS);
304
+ if (on === 'unknown')
305
+ return `A level is ${ALERT_THRESHOLDS.join(', ')}.`;
306
+ if (clean === 'unknown')
307
+ return `A quiet turn is ${CLEAN_MODES.join(', ')}.`;
308
+ if (say === 'unknown')
309
+ return `A value is said as ${SAID_AS.join(' or ')}.`;
310
+ if (notify === 'unknown')
311
+ return `A channel is ${NOTICE_CHANNELS.join(', ')}.`;
312
+ if (lang === 'unknown')
313
+ return `A language is ${NOTICE_LANGS.join(', ')}.`;
314
+ const choices = {
315
+ ...(on === undefined ? {} : { on }),
316
+ ...(clean === undefined ? {} : { clean }),
317
+ ...(say === undefined ? {} : { say }),
318
+ ...(notify === undefined ? {} : { notify }),
319
+ ...(lang === undefined ? {} : { lang }),
320
+ };
321
+ // Mirrors the CLI's `--reset`: it takes answers out, and is refused rather than guessed at where a choice came
322
+ // with it. Checked after the fields are read, so a choice this version does not know is still the error said -
323
+ // and checked before `choices` is dropped, so a choice given alongside `reset` is never silently lost.
324
+ if (fields.reset === true) {
325
+ return Object.keys(choices).length > 0
326
+ ? 'A reset takes answers out; it cannot be given alongside a choice.'
327
+ : { scope, choices: {}, reset: true };
328
+ }
329
+ return Object.keys(choices).length === 0 ? 'A change sets something, or resets.' : { scope, choices };
330
+ }
331
+ function oneOf(value, known) {
332
+ if (value === undefined)
333
+ return undefined;
334
+ return known.find((candidate) => candidate === value) ?? 'unknown';
335
+ }
336
+ function channelsIn(value) {
337
+ if (value === undefined)
338
+ return undefined;
339
+ if (!Array.isArray(value) || value.length === 0)
340
+ return 'unknown';
341
+ const channels = value.map((name) => NOTICE_CHANNELS.find((channel) => channel === name));
342
+ return channels.includes(undefined) ? 'unknown' : [...new Set(channels)];
343
+ }
344
+ /**
345
+ * The pages of a project shown earlier in this process (V14, amended). The project shown again since: its pages are sent
346
+ * to this run's address. Another: a page is "This page was for my-app", a page open in a tab that asks whether it
347
+ * changed is told it has gone for good (410, so it says so at once, L13), and the only thing it may ask is the way back -
348
+ * from its own origin, as every write. Nothing else is read or written from it (V17).
349
+ */
350
+ async function pastRunAnswer(past, rest, request, dependencies, prefix) {
351
+ if (past.same)
352
+ return request.method === 'GET' ? { status: 302, type: TEXT, body: '', location: prefix + rest } : notFound();
353
+ if (request.method === 'GET') {
354
+ if (rest.startsWith('api/'))
355
+ return { status: 410, type: JSON_TYPE, body: JSON.stringify({ gone: true }) };
356
+ return { status: 200, type: 'text/html; charset=utf-8', body: await past.page() };
357
+ }
358
+ if (request.method !== 'POST' || rest !== 'api/switch-back')
359
+ return notFound();
360
+ if (request.headers.origin !== dependencies.origin)
361
+ return { status: 403, type: TEXT, body: 'Forbidden.' };
362
+ const answer = await past.back();
363
+ if ('failed' in answer)
364
+ return reply(422, answer.failed);
365
+ return { status: 200, type: JSON_TYPE, body: JSON.stringify({ ok: true, url: answer.url }), ...(dependencies.switched === undefined ? {} : { after: dependencies.switched }) };
366
+ }
367
+ function notFound() {
368
+ return { status: 404, type: TEXT, body: 'Not found.' };
369
+ }
370
+ function reply(status, message) {
371
+ return { status, type: JSON_TYPE, body: JSON.stringify({ ok: false, message }) };
372
+ }
@@ -0,0 +1,50 @@
1
+ /** W15: a person adds a few files while setting up; more than this in one request is not a page that was used. */
2
+ export const MOST_NAMES = 20;
3
+ const FIELDS = ['scope', 'watch', 'protect', 'tell', 'modes', 'stopped', 'fine'];
4
+ /**
5
+ * The body of `api/onboarding` (`.ai/specs/2026-09-24-onboarding.md` W15): exactly the three steps' answers, and
6
+ * nothing else. A body this does not recognise is a bad request, and nothing is written - a request that could carry
7
+ * more than the page offers would be a way to write what nobody chose. Whether a name can be written is not decided
8
+ * here: `finishOnboarding` refuses the whole request where one cannot (R46, R59).
9
+ */
10
+ export function onboardingChoices(fields) {
11
+ const unknown = Object.keys(fields).filter((key) => !FIELDS.includes(key));
12
+ if (unknown.length > 0)
13
+ return 'The request holds ' + FIELDS.join(', ') + ', and nothing else.';
14
+ if (fields.scope !== 'local' && fields.scope !== 'shared')
15
+ return 'The scope is local or shared.';
16
+ if (typeof fields.watch !== 'boolean' || typeof fields.stopped !== 'boolean' || typeof fields.fine !== 'boolean') {
17
+ return 'watch, stopped and fine are true or false.';
18
+ }
19
+ const protect = names(fields.protect);
20
+ const tell = names(fields.tell);
21
+ if (typeof protect === 'string')
22
+ return protect;
23
+ if (typeof tell === 'string')
24
+ return tell;
25
+ if (protect.length + tell.length > MOST_NAMES)
26
+ return `At most ${MOST_NAMES} names are added at once.`;
27
+ // W12a: a row's mode by its key. The key only finds a row the server reads again; it writes nothing of its own.
28
+ if (fields.modes === null || typeof fields.modes !== 'object' || Array.isArray(fields.modes))
29
+ return 'The modes are an object.';
30
+ const modes = Object.entries(fields.modes);
31
+ if (modes.length > MOST_NAMES || modes.some(([key, mode]) => key.length > 200 || (mode !== 'block' && mode !== 'tell')))
32
+ return 'A mode is block or tell.';
33
+ return {
34
+ scope: fields.scope,
35
+ watch: fields.watch,
36
+ protect,
37
+ tell,
38
+ modes: Object.fromEntries(modes),
39
+ stopped: fields.stopped,
40
+ fine: fields.fine,
41
+ };
42
+ }
43
+ function names(value) {
44
+ if (!Array.isArray(value))
45
+ return 'The names are a list.';
46
+ const kept = value.filter((pattern) => typeof pattern === 'string' && pattern.trim() !== '');
47
+ if (kept.length !== value.length)
48
+ return 'A name is text.';
49
+ return kept.map((pattern) => pattern.trim());
50
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,193 @@
1
+ import { patternsOf } from '../../../setup/protected-patterns.js';
2
+ /**
3
+ * One change from the served Settings view, as the `SetupOptions` the equivalent `init` flags would build
4
+ * (`worth-running-every-day` R58). Nothing new decides what is written: this only chooses the flags, and the
5
+ * setup a terminal runs does the rest.
6
+ *
7
+ * `yes` is given because the page's own confirm step (R56) is the consent R7 asks for, off a terminal; the setup
8
+ * is built with `interactive: false`, so no question is ever opened.
9
+ */
10
+ export async function settingsChangeToSetup(setup, change) {
11
+ // A pattern `init` would not write is refused here, before anything runs (R46, R59). `init` itself only says "Not
12
+ // written" and goes on with the rest - an answer that reads as success, and a page that reloads on success would
13
+ // show the list unchanged with no reason at all.
14
+ const refused = patternsOf(patternsIn(change)).refused;
15
+ if (refused.length > 0) {
16
+ return { outcome: 'refused', output: refused.map(({ pattern, reason }) => `"${pattern}" was not written: ${reason}.`).join('\n') };
17
+ }
18
+ // U7: `init --update`, the same run a terminal makes; the setup reads both files and the version itself.
19
+ if (change.change === 'update') {
20
+ const result = await setup.run({ ...base(undefined), update: true });
21
+ return { outcome: result.outcome, output: result.output };
22
+ }
23
+ // CK5: `init --codex`; the setup reads what Claude Code's `refuse` runs, and writes Codex's to match.
24
+ if (change.change === 'codex') {
25
+ const result = await setup.run({ ...base(undefined), codex: true });
26
+ return { outcome: result.outcome, output: result.output };
27
+ }
28
+ if (change.change === 'adopt') {
29
+ // One run: `init --protect … --refuse`, keeping every other hook as it runs. A private file is kept from the
30
+ // agent's searches as well as from being opened (F38, amended 2026-09-24): `refuse` is what stops a `grep -r`
31
+ // that the deny rule never sees, and it is installed with the rules rather than asked for on a tab of its own.
32
+ const result = await setup.run({ ...base(change.where), hooks: ['refuse'], keep: true, protect: [...change.patterns] });
33
+ return { outcome: result.outcome, output: result.output };
34
+ }
35
+ if (change.change === 'scope')
36
+ return scope(setup, change);
37
+ if (change.change === 'uninstall')
38
+ return uninstall(setup, change);
39
+ if (change.change === 'mode')
40
+ return { outcome: 'refused', output: 'This page cannot change what is told: agentwhy has no told lists to write here.' };
41
+ if (change.change === 'move')
42
+ return move(setup, change);
43
+ if (change.change === 'edit')
44
+ return edit(setup, change.from, change.pattern, change.where);
45
+ // An add is what the Add window confirmed: "your AI won't be able to open it or search through it" (F38).
46
+ const result = await setup.run(change.change === 'protect' ? { ...optionsFor(change), hooks: ['refuse'], keep: true } : optionsFor(change));
47
+ return { outcome: result.outcome, output: result.output };
48
+ }
49
+ /**
50
+ * Changing a pattern: the old rule out, then the new one in. `init` has no flag that rewrites a rule in place, so
51
+ * this is the two runs a terminal would do, in that order. The first failing stops the second, so a change that
52
+ * cannot take the old rule out never leaves both patterns protected; the page then says what went wrong and the
53
+ * file is as it was.
54
+ */
55
+ async function edit(setup, from, pattern, where) {
56
+ const out = await setup.run(optionsFor({ change: 'unprotect', pattern: from, ...(where === undefined ? {} : { where }) }));
57
+ if (out.outcome !== 'written' && out.outcome !== 'unchanged')
58
+ return { outcome: out.outcome, output: out.output };
59
+ // The new rule goes back into the file the old one came out of, so changing a rule never moves it between the
60
+ // file that is this machine's and the one the project shares.
61
+ const back = await setup.run(optionsFor({ change: 'protect', pattern, ...(where === undefined ? {} : { where }) }));
62
+ return { outcome: back.outcome, output: [out.output.trim(), back.output.trim()].filter((line) => line !== '').join('\n') };
63
+ }
64
+ /**
65
+ * The same rule, in the other file: the deny rule out of the file that holds it, then in to the other, in that
66
+ * order, because nothing removes a deny rule except `--remove --unprotect`. The first failing stops the second,
67
+ * so a move that cannot take the old rule out never leaves the same rule in both files.
68
+ */
69
+ async function move(setup, change) {
70
+ const out = await setup.run(optionsFor({ change: 'unprotect', pattern: change.pattern, where: change.from }));
71
+ if (out.outcome !== 'written' && out.outcome !== 'unchanged')
72
+ return { outcome: out.outcome, output: out.output };
73
+ const back = await setup.run(optionsFor({ change: 'protect', pattern: change.pattern, where: change.where }));
74
+ return { outcome: back.outcome, output: [out.output.trim(), back.output.trim()].filter((line) => line !== '').join('\n') };
75
+ }
76
+ /**
77
+ * General (F56): the rules first, each moved as `move` moves one, then the hooks, installed in the file they are to run
78
+ * from - which takes them out of the other (R4g) and points them at the rules now there. The first failure stops the
79
+ * rest and is said: what was moved before it stays moved, and every rule is in one file or the other, never neither.
80
+ */
81
+ async function scope(setup, change) {
82
+ const from = change.where === 'shared' ? 'local' : 'shared';
83
+ const said = [];
84
+ for (const pattern of change.patterns) {
85
+ const moved = await move(setup, { change: 'move', pattern, from, where: change.where });
86
+ said.push(moved.output.trim());
87
+ if (moved.outcome !== 'written' && moved.outcome !== 'unchanged')
88
+ return { outcome: moved.outcome, output: said.filter(Boolean).join('\n') };
89
+ }
90
+ if (change.hooks.length > 0) {
91
+ const hooks = await setup.run({ ...base(change.where), hooks: change.hooks, keep: true });
92
+ said.push(hooks.output.trim());
93
+ if (hooks.outcome !== 'written' && hooks.outcome !== 'unchanged')
94
+ return { outcome: hooks.outcome, output: said.filter(Boolean).join('\n') };
95
+ }
96
+ return { outcome: said.length === 0 ? 'unchanged' : 'written', output: said.filter(Boolean).join('\n') };
97
+ }
98
+ /**
99
+ * F59: `init --remove --watch --refuse --unprotect …` for each file the page named, the local one first. The first
100
+ * failure stops the rest and is said: a file already done stays done, and the page, read again, shows what is left.
101
+ */
102
+ async function uninstall(setup, change) {
103
+ const said = [];
104
+ let written = false;
105
+ for (const file of ['local', 'shared']) {
106
+ const rules = change.rules[file];
107
+ if (rules === undefined)
108
+ continue;
109
+ const out = await setup.run({ ...base(file), remove: true, hooks: ['watch', 'refuse'], unprotect: [...rules] });
110
+ said.push(out.output.trim());
111
+ if (out.outcome !== 'written' && out.outcome !== 'unchanged')
112
+ return { outcome: out.outcome, output: said.filter(Boolean).join('\n') };
113
+ written ||= out.outcome === 'written';
114
+ }
115
+ return { outcome: written ? 'written' : 'unchanged', output: said.filter(Boolean).join('\n') };
116
+ }
117
+ /**
118
+ * Whether a change only takes something out: the one change the setup makes where no project is (`which-project.md`
119
+ * V8), since what an earlier agentwhy wrote there has to be removable. Every other change writes something, and an edit,
120
+ * a move, General's scope and a switch to Block or Tell me take something out first - refused only at the step that
121
+ * writes, each left the file neither protected nor told. Found by a review.
122
+ */
123
+ export function onlyTakesOut(change) {
124
+ return change.change === 'uninstall' || change.change === 'unprotect' || (change.change === 'hooks' && !change.on);
125
+ }
126
+ /** The patterns a change writes: the ones it protects, and what an edited rule becomes. */
127
+ function patternsIn(change) {
128
+ if (change.change === 'adopt' || change.change === 'scope' || change.change === 'mode')
129
+ return change.patterns;
130
+ if (change.change === 'protect' || change.change === 'edit')
131
+ return [change.pattern];
132
+ return [];
133
+ }
134
+ /** What every change here has in common: it writes one named file, and the page's confirm step is the consent. */
135
+ function base(where) {
136
+ // Where the change says nothing, it is the file `init` writes when `--shared` is not given: this page's own
137
+ // default, and the one a pattern typed into the add field goes to. No `invoke`: the setup settles the command a
138
+ // hook runs, for this page as for `init` (`a-hook-runs-what-you-ran.md` J1).
139
+ return { protect: [], remove: false, yes: true, target: where ?? 'local' };
140
+ }
141
+ function optionsFor(change) {
142
+ const base_ = base(change.where);
143
+ if (change.change === 'hooks') {
144
+ // On installs the hook named and leaves the other as the file has it (`keep`: `init --watch` alone would take a
145
+ // running `refuse` out, R4b); `init --remove --watch` takes out that one hook and leaves every deny rule. Turning
146
+ // a switch on or off is one of those two and nothing else.
147
+ return change.on
148
+ ? { ...base_, hooks: change.hooks, keep: true }
149
+ : { ...base_, remove: true, hooks: change.hooks };
150
+ }
151
+ // Both name no hook, so both name the empty set of them. Left unsaid, `init` off a terminal installs `watch`
152
+ // and `--remove` takes every hook out - neither of which the confirm step offered, and R41 says a change
153
+ // touches only what it names. An add names `refuse` itself, in `settingsChangeToSetup`; an edit or a move of a rule
154
+ // that is already there does not: it stays the block it was, and where that was not whole, its row says so and
155
+ // **Finish blocking** closes it (`block-means-blocked` K3, K7).
156
+ return change.change === 'protect'
157
+ ? { ...base_, hooks: [], protect: [change.pattern] }
158
+ : { ...base_, remove: true, hooks: [], unprotect: [change.pattern] };
159
+ }
160
+ /**
161
+ * F57's switch, where the told lists can be written. To `tell`: every deny rule named comes out of both files - a rule
162
+ * left in either would still stop the agent - then the patterns go on the list General points at. To `block`: the
163
+ * patterns come off both lists first, then are protected as an add is, `refuse` with them. The first failure stops the
164
+ * rest and is said; a file is never left both off the rules and off the lists.
165
+ */
166
+ export async function modeChange(setup, lists, change) {
167
+ const refused = patternsOf(change.patterns).refused;
168
+ if (refused.length > 0)
169
+ return { outcome: 'refused', output: refused.map(({ pattern, reason }) => `"${pattern}" was not written: ${reason}.`).join('\n') };
170
+ const said = [];
171
+ const failed = (outcome) => outcome !== 'written' && outcome !== 'unchanged';
172
+ if (change.to === 'tell') {
173
+ for (const file of ['local', 'shared']) {
174
+ if (change.rules.length === 0)
175
+ break;
176
+ const out = await setup.run({ ...base(file), remove: true, hooks: [], unprotect: [...change.rules] });
177
+ said.push(out.output.trim());
178
+ if (failed(out.outcome))
179
+ return { outcome: out.outcome, output: said.filter(Boolean).join('\n') };
180
+ }
181
+ if (!(await lists.change(change.where, change.patterns, [])))
182
+ return { outcome: 'unwritable', output: 'The list of files to tell you about could not be written.' };
183
+ return { outcome: 'written', output: said.filter(Boolean).join('\n') };
184
+ }
185
+ for (const file of ['local', 'shared']) {
186
+ if (!(await lists.change(file, [], change.patterns)))
187
+ return { outcome: 'unwritable', output: 'The list of files to tell you about could not be written.' };
188
+ }
189
+ if (change.to === 'none')
190
+ return { outcome: 'written', output: '' };
191
+ const back = await setup.run({ ...base(change.where), hooks: ['refuse'], keep: true, protect: [...change.patterns] });
192
+ return { outcome: back.outcome, output: back.output };
193
+ }
@@ -0,0 +1 @@
1
+ export {};