@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,124 @@
1
+ import { countsTable, table } from '../../shared/text-table.js';
2
+ const SECTION_INDENT = 2;
3
+ const SUBSECTION_INDENT = 4;
4
+ const DETAIL_INDENT = 6;
5
+ const NOTHING_TO_REPORT = 'none';
6
+ export class TextDoctorRenderer {
7
+ #attentionRules;
8
+ constructor(attentionRules) {
9
+ this.#attentionRules = attentionRules;
10
+ }
11
+ render(report) {
12
+ return [
13
+ `agentwhy doctor · session ${report.sessionId}`,
14
+ `format contract v${report.contractVersion}, verified against Claude Code ${report.verifiedAgainst}`,
15
+ '',
16
+ 'Needs attention',
17
+ ...this.#attention(report).map((item) => `${' '.repeat(SECTION_INDENT)}${item}`),
18
+ '',
19
+ ...sourcesSection(report),
20
+ '',
21
+ 'Claude Code versions (lines)',
22
+ ...countsTable(report.toolVersions, SECTION_INDENT),
23
+ '',
24
+ 'Working directories (cwd)',
25
+ ...table([
26
+ ['distinct', String(report.workingDirectories.distinct)],
27
+ ['invalid', String(report.workingDirectories.invalid)],
28
+ ], SECTION_INDENT),
29
+ '',
30
+ ...transcriptSection('Main session', report.main),
31
+ '',
32
+ ...transcriptSection('Subagents', report.subagents),
33
+ '',
34
+ ...metaSection(report),
35
+ '',
36
+ 'Agent tool input keys',
37
+ ...countsTable(report.agentToolInputKeys, SECTION_INDENT),
38
+ '',
39
+ 'Denials by toolDenialKind',
40
+ ...countsTable(report.denials.byKind, SECTION_INDENT),
41
+ '',
42
+ 'Spilled result references',
43
+ ...table([
44
+ ['referenced', String(report.toolResultReferences.referenced)],
45
+ ['missing', String(report.toolResultReferences.missing)],
46
+ ], SECTION_INDENT),
47
+ '',
48
+ 'Task notifications delivered',
49
+ ...table([
50
+ ['naming a delegation', String(report.taskNotifications.namingADelegation)],
51
+ ['naming another call', String(report.taskNotifications.namingAnotherCall)],
52
+ ['naming no call here', String(report.taskNotifications.namingNoCall)],
53
+ ['naming nothing', String(report.taskNotifications.withoutCallId)],
54
+ ], SECTION_INDENT),
55
+ '',
56
+ ].join('\n');
57
+ }
58
+ #attention(report) {
59
+ const findings = this.#attentionRules.flatMap((rule) => rule.findings(report));
60
+ return findings.length > 0 ? findings : [NOTHING_TO_REPORT];
61
+ }
62
+ }
63
+ function sourcesSection({ sources }) {
64
+ const files = sources.subagentFiles;
65
+ return [
66
+ 'Sources',
67
+ ...table([
68
+ ['main transcript', sources.mainTranscript],
69
+ ['subagents directory', sources.subagentsDirectory],
70
+ ['tool-results directory', sources.toolResultsDirectory],
71
+ ['subagent files', `total ${files.total} · with transcript ${files.withTranscript} · with meta ${files.withMeta}`],
72
+ ['spilled result files', String(sources.toolResultFiles)],
73
+ ['unrecognised entries', String(sources.unrecognisedEntries)],
74
+ ], SECTION_INDENT),
75
+ ];
76
+ }
77
+ function transcriptSection(title, stats) {
78
+ const { sidechain } = stats;
79
+ return [
80
+ title,
81
+ ...table([
82
+ ['transcript files', `${stats.files} (${stats.unreadableFiles} unreadable)`],
83
+ ['lines', `${stats.lines} (${stats.unparsableLines} unparsable)`],
84
+ [
85
+ 'isSidechain',
86
+ `true ${sidechain.true} · false ${sidechain.false} · absent ${sidechain.absent} · invalid ${sidechain.invalid}`,
87
+ ],
88
+ ['Agent calls', String(stats.agentToolUses)],
89
+ ['denied calls', String(stats.deniedCalls)],
90
+ ['unique toolu_ ids', String(stats.uniqueToolUseIds)],
91
+ ['unique UUIDs', String(stats.uniqueUuids)],
92
+ ], SECTION_INDENT),
93
+ ' line types',
94
+ ...countsTable(stats.lineTypes, SUBSECTION_INDENT),
95
+ ' system subtypes',
96
+ ...countsTable(stats.systemSubtypes, SUBSECTION_INDENT),
97
+ ' tools',
98
+ ...countsTable(stats.tools, SUBSECTION_INDENT),
99
+ ' top-level keys',
100
+ ...countsTable(stats.topLevelKeys, SUBSECTION_INDENT),
101
+ ' keys on unknown line types',
102
+ ...keysByTypeLines(stats.unknownLineTypeKeys),
103
+ ];
104
+ }
105
+ function keysByTypeLines(keysByType) {
106
+ const entries = Object.entries(keysByType);
107
+ if (entries.length === 0)
108
+ return countsTable({}, SUBSECTION_INDENT);
109
+ return entries.flatMap(([type, keys]) => [`${' '.repeat(SUBSECTION_INDENT)}${type}`, ...countsTable(keys, DETAIL_INDENT)]);
110
+ }
111
+ function metaSection({ meta }) {
112
+ return [
113
+ 'meta.json',
114
+ ...table([['files', `${meta.files} (${meta.unparsableFiles} unparsable, ${meta.unreadableFiles} unreadable)`]], SECTION_INDENT),
115
+ ' agentType',
116
+ ...countsTable(meta.agentType, SUBSECTION_INDENT),
117
+ ' spawnDepth (files per depth)',
118
+ ...countsTable(meta.spawnDepth, SUBSECTION_INDENT),
119
+ ' requestShape',
120
+ ...countsTable(meta.requestShape, SUBSECTION_INDENT),
121
+ ' keys',
122
+ ...countsTable(meta.keys, SUBSECTION_INDENT),
123
+ ];
124
+ }
@@ -0,0 +1,16 @@
1
+ /** Discovers a session, measures it and renders the report. Returns the result; writing it is the shell's job. */
2
+ export class SessionDoctor {
3
+ #dependencies;
4
+ constructor(dependencies) {
5
+ this.#dependencies = dependencies;
6
+ }
7
+ async run(options) {
8
+ const { discovery, probe, renderers } = this.#dependencies;
9
+ const session = await discovery.discover(options.input);
10
+ const report = await probe.probe(session);
11
+ return {
12
+ outcome: session.mainTranscript.present ? 'complete' : 'main-transcript-missing',
13
+ output: renderers[options.format].render(report),
14
+ };
15
+ }
16
+ }
@@ -0,0 +1,24 @@
1
+ import { isCancel, text } from '@clack/prompts';
2
+ /** One line of text, through `@clack/prompts`. A blank answer comes back as an empty string, never as a cancellation. */
3
+ export class ClackAsker {
4
+ #input;
5
+ #output;
6
+ #banner;
7
+ /** The banner is shared with the command's other prompts, so the mark is drawn once above the first of them. */
8
+ constructor(input, output, banner) {
9
+ this.#input = input;
10
+ this.#output = output;
11
+ this.#banner = banner;
12
+ }
13
+ async ask(question, placeholder) {
14
+ this.#banner?.drawOn(this.#output);
15
+ const answer = await text({
16
+ message: question,
17
+ ...(placeholder === undefined ? {} : { placeholder }),
18
+ defaultValue: '',
19
+ input: this.#input,
20
+ output: this.#output,
21
+ });
22
+ return isCancel(answer) ? undefined : answer;
23
+ }
24
+ }
@@ -0,0 +1,40 @@
1
+ import { autocomplete, isCancel } from '@clack/prompts';
2
+ /** Rows shown at once. The rest are reached by moving, or found by typing. */
3
+ const VISIBLE = 10;
4
+ /**
5
+ * Chooses through `@clack/prompts`: a list that narrows as a person types, so a session is found by a word of its
6
+ * title, its date or its id rather than by counting rows. It is the project's only runtime dependency, and this is
7
+ * the only module allowed to import it (`2026-09-13-conventions.md`, Runtime dependencies).
8
+ *
9
+ * **Replaced a hand-written chooser on 2026-09-15**, after someone at a real terminal could not choose with it. The
10
+ * failure did not reproduce in a pseudo-terminal, so this swaps in a widely used implementation; it does not fix a
11
+ * cause that was found.
12
+ */
13
+ export class ClackChooser {
14
+ #input;
15
+ #output;
16
+ #banner;
17
+ /** `banner` is drawn above the first question of a command: it decides for itself whether it has been drawn already. */
18
+ constructor(input, output, banner) {
19
+ this.#input = input;
20
+ this.#output = output;
21
+ this.#banner = banner;
22
+ }
23
+ async choose(heading, choices) {
24
+ if (choices.length === 0)
25
+ return undefined;
26
+ this.#banner?.drawOn(this.#output);
27
+ const chosen = await autocomplete({
28
+ message: heading,
29
+ options: choices.map((choice, position) => ({ value: position, label: choice.label, hint: choice.detail })),
30
+ // The default filter also matches the value, which here is a row number: typing "2" would match row 2
31
+ // whatever it says. What a person searches by is what they can see.
32
+ filter: (search, option) => `${option.label ?? ''} ${option.hint ?? ''}`.toLowerCase().includes(search.toLowerCase()),
33
+ maxItems: VISIBLE,
34
+ input: this.#input,
35
+ output: this.#output,
36
+ });
37
+ // Escape and Ctrl-C cancel. Enter on a search that matches nothing resolves with no value, which is not a choice.
38
+ return isCancel(chosen) || chosen === undefined ? undefined : chosen;
39
+ }
40
+ }
@@ -0,0 +1,31 @@
1
+ import { isCancel, multiselect } from '@clack/prompts';
2
+ /**
3
+ * A list with boxes, through `@clack/prompts` - the package the session chooser already uses, so no new dependency
4
+ * (`2026-09-13-conventions.md`, Runtime dependencies). Answering with nothing ticked is an answer, not a cancellation:
5
+ * "install none of this" is a thing a person may mean.
6
+ */
7
+ export class ClackMultiChooser {
8
+ #input;
9
+ #output;
10
+ #banner;
11
+ /** The banner is shared with the command's other prompts, so the mark is drawn once above the first of them. */
12
+ constructor(input, output, banner) {
13
+ this.#input = input;
14
+ this.#output = output;
15
+ this.#banner = banner;
16
+ }
17
+ async chooseMany(heading, choices) {
18
+ this.#banner?.drawOn(this.#output);
19
+ if (choices.length === 0)
20
+ return [];
21
+ const chosen = await multiselect({
22
+ message: heading,
23
+ options: choices.map((choice, position) => ({ value: position, label: choice.label, hint: choice.detail })),
24
+ initialValues: choices.flatMap((choice, position) => (choice.selected ? [position] : [])),
25
+ required: false,
26
+ input: this.#input,
27
+ output: this.#output,
28
+ });
29
+ return isCancel(chosen) ? undefined : [...chosen].sort((a, b) => a - b);
30
+ }
31
+ }
@@ -0,0 +1,120 @@
1
+ import { lstat, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ /** One directory of its own, so pruning never looks at a file this tool did not write. */
4
+ const DIRECTORY = 'agentwhy-alerts';
5
+ /** A session nobody finished leaves a file behind. After this it is swept, whether its turn ever ended or not. */
6
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
7
+ /** Everything else in a session id is replaced, so no id can name a path of its own. */
8
+ const UNSAFE = /[^A-Za-z0-9._-]/g;
9
+ /**
10
+ * The alerts of a session, in the system's temporary directory (`specs/2026-09-16-a-notice-in-the-conversation.md`
11
+ * R7): written when `SubagentStop` finds one, read and cleared when `Stop` says it, and swept by age so an
12
+ * abandoned session leaves nothing behind.
13
+ *
14
+ * Nothing here throws. A hook cannot afford it: on `SubagentStop` an error becomes the agent's next instruction, and
15
+ * on `Stop` it holds the turn open. A store that could not write is a notice that was not said - and `check` still
16
+ * finds the same thing in the records.
17
+ */
18
+ export class FileAlertStore {
19
+ #directory;
20
+ #user;
21
+ #now;
22
+ constructor(temporaryDirectory, user, now = Date.now) {
23
+ this.#directory = join(temporaryDirectory, DIRECTORY);
24
+ this.#user = user;
25
+ this.#now = now;
26
+ }
27
+ /**
28
+ * Whether the directory this run would use is this person's own. A mode on `mkdir` settles how a directory is
29
+ * created and says nothing about one that was there already - and on Linux the system's temporary directory is
30
+ * one `/tmp` shared by every account, where this directory's fixed name is a name another account can take
31
+ * first. A symlink left under it would send every write below into whatever directory its author chose.
32
+ *
33
+ * So: not a link, this person's own, and not writable by anyone else. Anything short of that and there is no
34
+ * store, which is a state this class already has an answer for - see the note above.
35
+ */
36
+ async #mine() {
37
+ try {
38
+ const entry = await lstat(this.#directory);
39
+ const owner = this.#user === undefined || entry.uid === this.#user;
40
+ return entry.isDirectory() && owner && (entry.mode & 0o022) === 0;
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ async remember(sessionId, alert) {
47
+ try {
48
+ // The owner alone, and the directory first: the system's temporary directory is one `/tmp` shared by every
49
+ // account on a Linux machine, and this directory's name is fixed, so it is a name another account can reach
50
+ // for. A mode of its own keeps what is written here - which protected file an agent reached, and in whose
51
+ // session - out of every other account's reading.
52
+ await mkdir(this.#directory, { recursive: true, mode: 0o700 });
53
+ if (!(await this.#mine()))
54
+ return;
55
+ await this.#prune();
56
+ const kept = (await this.#read(sessionId)).filter((remembered) => remembered.agentId !== alert.agentId);
57
+ await writeFile(this.#fileFor(sessionId), JSON.stringify([...kept, alert]), { encoding: 'utf8', mode: 0o600 });
58
+ }
59
+ catch {
60
+ // See the note above: a store that could not write says nothing, and nothing is a state this tool has words for.
61
+ }
62
+ }
63
+ async take(sessionId) {
64
+ const remembered = await this.#read(sessionId);
65
+ try {
66
+ await rm(this.#fileFor(sessionId), { force: true });
67
+ }
68
+ catch {
69
+ // Said once is said: if the file survives, the next turn repeats a notice. That is better than losing one.
70
+ }
71
+ return remembered;
72
+ }
73
+ /** A session id is not a file name until this makes it one: no separator, no `..`, no length worth worrying about. */
74
+ #fileFor(sessionId) {
75
+ const safe = sessionId.replace(UNSAFE, '-').slice(0, 80);
76
+ return join(this.#directory, `${safe === '' ? 'session' : safe}.json`);
77
+ }
78
+ async #read(sessionId) {
79
+ if (!(await this.#mine()))
80
+ return [];
81
+ try {
82
+ const parsed = JSON.parse(await readFile(this.#fileFor(sessionId), 'utf8'));
83
+ return Array.isArray(parsed) ? parsed.filter(isRemembered) : [];
84
+ }
85
+ catch {
86
+ // A missing file is the common case; a half-written one is the same answer, because nothing here is authoritative.
87
+ return [];
88
+ }
89
+ }
90
+ async #prune() {
91
+ const oldest = this.#now() - MAX_AGE_MS;
92
+ for (const name of await readdir(this.#directory)) {
93
+ if (!name.endsWith('.json'))
94
+ continue;
95
+ const file = join(this.#directory, name);
96
+ try {
97
+ if ((await stat(file)).mtimeMs < oldest)
98
+ await rm(file, { force: true });
99
+ }
100
+ catch {
101
+ // A file that vanished between the listing and the check is a file this does not have to remove.
102
+ }
103
+ }
104
+ }
105
+ }
106
+ /** Read from a file this process wrote, and still checked: a shape that does not hold is a record that is not there. */
107
+ function isRemembered(value) {
108
+ if (value === null || typeof value !== 'object')
109
+ return false;
110
+ const record = value;
111
+ return typeof record.agentId === 'string' && typeof record.level === 'string' && typeof record.words === 'string' &&
112
+ (record.counts === undefined || areCounts(record.counts));
113
+ }
114
+ /** Counts that are not two whole numbers are counts this did not write: the record is read as one without them. */
115
+ function areCounts(value) {
116
+ if (value === null || typeof value !== 'object')
117
+ return false;
118
+ const counts = value;
119
+ return Number.isInteger(counts.values) && Number.isInteger(counts.reached);
120
+ }
@@ -0,0 +1,55 @@
1
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ /** A session id as Claude Code names a transcript: nothing that could be a path or carry anything else. */
4
+ const SESSION_ID = /^[A-Za-z0-9_-]{1,200}$/;
5
+ /**
6
+ * The conversations a person asked to be checked, as JSON lines in their home directory beside their marks
7
+ * (`for-people-who-build-with-ai.md` F55): outside every repository, readable by that person only, appended to.
8
+ */
9
+ export class FileCheckedStore {
10
+ #file;
11
+ constructor(file) {
12
+ this.#file = file;
13
+ }
14
+ async read() {
15
+ let text;
16
+ try {
17
+ text = await readFile(this.#file, 'utf8');
18
+ }
19
+ catch (error) {
20
+ // Never asked is the ordinary answer; anything else is a record that exists and was not read.
21
+ const missing = error instanceof Error && 'code' in error && error.code === 'ENOENT';
22
+ return { ids: new Set(), failed: !missing };
23
+ }
24
+ const ids = new Set();
25
+ for (const line of text.split('\n')) {
26
+ if (line.trim() === '')
27
+ continue;
28
+ try {
29
+ const parsed = JSON.parse(line);
30
+ // Read from a file a person could have edited: a line that does not hold is skipped.
31
+ if (parsed !== null && typeof parsed === 'object') {
32
+ const record = parsed;
33
+ if (record.kind === 'checked' && typeof record.id === 'string' && SESSION_ID.test(record.id))
34
+ ids.add(record.id);
35
+ }
36
+ }
37
+ catch {
38
+ // skipped, as a line that does not hold
39
+ }
40
+ }
41
+ return { ids, failed: false };
42
+ }
43
+ async add(id, at) {
44
+ if (!SESSION_ID.test(id))
45
+ return false;
46
+ try {
47
+ await mkdir(dirname(this.#file), { recursive: true, mode: 0o700 });
48
+ await appendFile(this.#file, JSON.stringify({ kind: 'checked', id, at }) + '\n', { encoding: 'utf8', mode: 0o600 });
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,67 @@
1
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ import { isMarkResult } from '../ports/mark-store.js';
4
+ /**
5
+ * A person's marks for one project, as JSON lines in their home directory (`worth-running-every-day` R33): outside every
6
+ * repository, readable by that person only, appended to and never rewritten, so what was undone stays visible (R34).
7
+ */
8
+ export class FileMarkStore {
9
+ #file;
10
+ constructor(file) {
11
+ this.#file = file;
12
+ }
13
+ async read() {
14
+ let text;
15
+ try {
16
+ text = await readFile(this.#file, 'utf8');
17
+ }
18
+ catch (error) {
19
+ // Never marked is the ordinary answer; anything else is a record that exists and was not read.
20
+ const missing = error instanceof Error && 'code' in error && error.code === 'ENOENT';
21
+ return { records: [], skipped: 0, failed: !missing };
22
+ }
23
+ const records = [];
24
+ let skipped = 0;
25
+ for (const line of text.split('\n')) {
26
+ if (line.trim() === '')
27
+ continue;
28
+ try {
29
+ const parsed = JSON.parse(line);
30
+ if (isRecord(parsed))
31
+ records.push(parsed);
32
+ else
33
+ skipped += 1;
34
+ }
35
+ catch {
36
+ skipped += 1;
37
+ }
38
+ }
39
+ return { records, skipped, failed: false };
40
+ }
41
+ async append(record) {
42
+ try {
43
+ await mkdir(dirname(this.#file), { recursive: true, mode: 0o700 });
44
+ await appendFile(this.#file, JSON.stringify(record) + '\n', { encoding: 'utf8', mode: 0o600 });
45
+ return true;
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ }
51
+ }
52
+ /** Read from a file a person could have edited, so every field is checked: a line that does not hold is skipped. */
53
+ function isRecord(value) {
54
+ if (value === null || typeof value !== 'object')
55
+ return false;
56
+ const record = value;
57
+ if (typeof record.path !== 'string' || record.path === '' || typeof record.at !== 'number' || !Number.isFinite(record.at))
58
+ return false;
59
+ if (record.kind === 'unmark')
60
+ return true;
61
+ return (record.kind === 'mark' &&
62
+ typeof record.label === 'string' &&
63
+ isMarkResult(record.result) &&
64
+ (record.note === undefined || typeof record.note === 'string') &&
65
+ Array.isArray(record.sessions) &&
66
+ record.sessions.every((session) => typeof session === 'string'));
67
+ }
@@ -0,0 +1,93 @@
1
+ import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
+ import { dirname } from 'node:path';
3
+ /**
4
+ * A project as the record names it: the name its sessions' directory has, which holds letters, digits and hyphens
5
+ * only. Nothing that could be a path, and nothing else a hand-edited line could slip in.
6
+ */
7
+ const PROJECT = /^[A-Za-z0-9-]{1,1024}$/;
8
+ /**
9
+ * Which projects' onboarding a person finished, as JSON lines in their home directory beside their notice preferences
10
+ * (`.ai/specs/2026-09-24-onboarding.md` W21): outside every repository, readable by that person only, appended to.
11
+ * One file for every project, so "has this person seen the intro anywhere?" is one read (N1, N2).
12
+ */
13
+ export class FileOnboardingStore {
14
+ #file;
15
+ #project;
16
+ constructor(file, project) {
17
+ this.#file = file;
18
+ this.#project = project;
19
+ }
20
+ async read() {
21
+ const record = await this.#record();
22
+ if (record === 'failed')
23
+ return { here: false, anywhere: false, failed: true };
24
+ return { here: record.finished.has(this.#project), anywhere: record.anywhere, failed: false };
25
+ }
26
+ async doneFor(projects) {
27
+ const record = await this.#record();
28
+ if (record === 'failed')
29
+ return undefined;
30
+ return new Set(projects.filter((project) => record.finished.has(project)));
31
+ }
32
+ /**
33
+ * The record as it stands: every project whose last word is `done`, and whether any project ever finished. Absent is
34
+ * "never"; any other error is `failed`.
35
+ */
36
+ async #record() {
37
+ let text;
38
+ try {
39
+ text = await readFile(this.#file, 'utf8');
40
+ }
41
+ catch (error) {
42
+ // Never finished is the ordinary answer; anything else is a record that exists and was not read.
43
+ const missing = error instanceof Error && 'code' in error && error.code === 'ENOENT';
44
+ return missing ? { finished: new Set(), anywhere: false } : 'failed';
45
+ }
46
+ const finished = new Set();
47
+ let anywhere = false;
48
+ // In the order written: a reset for a project cancels the `done` before it (W25a), and a later `done` counts again.
49
+ for (const line of text.split('\n')) {
50
+ if (line.trim() === '')
51
+ continue;
52
+ try {
53
+ const parsed = JSON.parse(line);
54
+ // Read from a file a person could have edited: a line that does not hold is skipped.
55
+ if (parsed !== null && typeof parsed === 'object') {
56
+ const record = parsed;
57
+ if (typeof record.project !== 'string' || !PROJECT.test(record.project))
58
+ continue;
59
+ if (record.kind === 'done') {
60
+ // The intro is once per person (N2): a finish anywhere counts, whatever was reset after it.
61
+ anywhere = true;
62
+ finished.add(record.project);
63
+ }
64
+ else if (record.kind === 'reset') {
65
+ finished.delete(record.project);
66
+ }
67
+ }
68
+ }
69
+ catch {
70
+ // skipped, as a line that does not hold
71
+ }
72
+ }
73
+ return { finished, anywhere };
74
+ }
75
+ async add(at) {
76
+ return this.#append('done', at);
77
+ }
78
+ async reset(at) {
79
+ return this.#append('reset', at);
80
+ }
81
+ async #append(kind, at) {
82
+ if (!PROJECT.test(this.#project))
83
+ return false;
84
+ try {
85
+ await mkdir(dirname(this.#file), { recursive: true, mode: 0o700 });
86
+ await appendFile(this.#file, JSON.stringify({ kind, project: this.#project, at }) + '\n', { encoding: 'utf8', mode: 0o600 });
87
+ return true;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,26 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { realpath } from 'node:fs/promises';
3
+ /**
4
+ * Runs `program` with `args` and no shell, its own console window hidden where the system would draw one (Windows); a
5
+ * window the program opens is still shown. A program that exits with a code answers it; one that could not start rejects.
6
+ */
7
+ export function runProgram(program, args) {
8
+ return new Promise((resolve, reject) => {
9
+ execFile(program, [...args], { windowsHide: true }, (error, stdout, stderr) => {
10
+ if (error !== null && typeof error.code !== 'number') {
11
+ reject(error);
12
+ return;
13
+ }
14
+ resolve({ code: error === null ? 0 : Number(error.code), stdout: String(stdout), stderr: String(stderr) });
15
+ });
16
+ });
17
+ }
18
+ /** The folder a window answered, with every link followed; one that is not there is said so. */
19
+ export async function chosenFolder(path) {
20
+ try {
21
+ return { chosen: await realpath(path) };
22
+ }
23
+ catch {
24
+ return { failed: 'The folder chosen could not be found.' };
25
+ }
26
+ }