@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,73 @@
1
+ import { byString } from '../../shared/compare.js';
2
+ /** Merges per-session actions by path and route. Most sessions first, then by name, so the order is stable. */
3
+ export function digestOf(actions, context) {
4
+ const rotate = new PathCounts();
5
+ const templates = new Set();
6
+ const onlyInResults = new PathCounts();
7
+ const unknown = new PathCounts();
8
+ const routes = new Map();
9
+ const shapes = new Map();
10
+ for (const session of actions) {
11
+ for (const file of session.rotate) {
12
+ rotate.add(file.path);
13
+ if (file.template)
14
+ templates.add(file.path);
15
+ }
16
+ for (const path of new Set(session.onlyInResults))
17
+ onlyInResults.add(path);
18
+ for (const path of new Set(session.unknown))
19
+ unknown.add(path);
20
+ for (const route of session.openRoutes) {
21
+ const key = `${route.path}\u0000${route.did}`;
22
+ const known = routes.get(key);
23
+ routes.set(key, {
24
+ route: { path: route.path, did: route.did, ...(route.pattern === undefined ? {} : { pattern: route.pattern }), sessions: 0, occurrences: 0 },
25
+ sessions: (known?.sessions ?? 0) + 1,
26
+ occurrences: (known?.occurrences ?? 0) + route.occurrences,
27
+ });
28
+ }
29
+ for (const name of session.secretShapes)
30
+ shapes.set(name, (shapes.get(name) ?? 0) + 1);
31
+ }
32
+ const first = actions[0];
33
+ return {
34
+ asked: context.asked,
35
+ sessionsRead: actions.length,
36
+ sessionsUnreadable: context.sessionsUnreadable,
37
+ policyKind: context.policyKind,
38
+ ...(first === undefined ? {} : { policy: first.policy }),
39
+ rotate: rotate.sorted().map((entry) => (templates.has(entry.path) ? { ...entry, template: true } : entry)),
40
+ openRoutes: [...routes.values()]
41
+ .map(({ route, sessions, occurrences }) => ({ ...route, sessions, occurrences }))
42
+ .sort((a, b) => b.sessions - a.sessions || b.occurrences - a.occurrences || byString(a.path, b.path) || byString(a.did, b.did)),
43
+ onlyInResults: onlyInResults.sorted(),
44
+ unknown: unknown.sorted(),
45
+ refusedAttempts: actions.reduce((sum, session) => sum + session.refusedAttempts, 0),
46
+ secretShapes: [...shapes].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count || byString(a.name, b.name)),
47
+ mentions: actions.reduce((sum, session) => sum + session.mentions, 0),
48
+ ...(context.marks === undefined ? {} : { marks: context.marks }),
49
+ };
50
+ }
51
+ /**
52
+ * Whether anything in the digest asks a person to act. Refusals alone do not: they are the rules holding. An unknown
53
+ * outcome does: it is a question the transcript could not answer.
54
+ */
55
+ export function needsAction(digest) {
56
+ return (digest.rotate.length > 0 ||
57
+ digest.openRoutes.length > 0 ||
58
+ digest.onlyInResults.length > 0 ||
59
+ digest.unknown.length > 0 ||
60
+ digest.secretShapes.length > 0);
61
+ }
62
+ /** How many sessions each path appeared in, most first. */
63
+ class PathCounts {
64
+ #counts = new Map();
65
+ add(key) {
66
+ this.#counts.set(key, (this.#counts.get(key) ?? 0) + 1);
67
+ }
68
+ sorted() {
69
+ return [...this.#counts]
70
+ .map(([path, sessions]) => ({ path, sessions }))
71
+ .sort((a, b) => b.sessions - a.sessions || byString(a.path, b.path));
72
+ }
73
+ }
@@ -0,0 +1,83 @@
1
+ import { NAMES_PER_FILE } from '../report-model.js';
2
+ const ORDER = ['rotate', 'template', 'unknown', 'route', 'result'];
3
+ /**
4
+ * The labels that ask a person to do something: a key to change, a template to confirm, an outcome nobody recorded.
5
+ * The other two say where a path went and ask for nothing. Every page that counts what is left to do counts these.
6
+ */
7
+ export const TO_DO_LABELS = ['rotate', 'template', 'unknown'];
8
+ /**
9
+ * `sessions` are expected with marked files already taken out (`actionsAfterMarks`), so a line here is still to do;
10
+ * `marks` then says which of them had been marked, and lists the history.
11
+ */
12
+ export function checkWithMarks(sessions, marks) {
13
+ const base = checkOf(sessions);
14
+ const rows = base.rows.map((row) => {
15
+ const mark = marks.standing.get(row.path);
16
+ return mark === undefined ? row : { ...row, reopened: { result: mark.result, at: mark.at } };
17
+ });
18
+ const open = new Set(rows.map((row) => row.path));
19
+ const history = marks.lines.map(({ mark, status, endedAt }) => ({
20
+ path: mark.path,
21
+ label: mark.label,
22
+ result: mark.result,
23
+ at: mark.at,
24
+ ...(marks.keepNotes && mark.note !== undefined && mark.note !== '' ? { note: mark.note } : {}),
25
+ sessions: mark.sessions.flatMap((id) => {
26
+ const name = marks.nameOf(id);
27
+ return name === undefined ? [] : [name];
28
+ }),
29
+ status,
30
+ ...(endedAt === undefined ? {} : { endedAt }),
31
+ reopened: status === 'standing' && open.has(mark.path),
32
+ }));
33
+ return { rows, refusedAttempts: base.refusedAttempts, history, marksUnreadable: marks.unreadable };
34
+ }
35
+ export function checkOf(sessions) {
36
+ const found = new Map();
37
+ const note = (path, label, name, keyed = false, read) => {
38
+ const rank = ORDER.indexOf(label);
39
+ const row = found.get(path);
40
+ if (row === undefined) {
41
+ found.set(path, { rank, sessions: [name], keyed, ...(read === undefined ? {} : { read }) });
42
+ return;
43
+ }
44
+ row.rank = Math.min(row.rank, rank);
45
+ // One session that read a key from it is enough: the key is what has to be changed.
46
+ row.keyed ||= keyed;
47
+ if (read !== undefined)
48
+ row.read = row.read === undefined ? read : mergeReads(row.read, read);
49
+ if (!row.sessions.includes(name))
50
+ row.sessions.push(name);
51
+ };
52
+ for (const { name, actions } of sessions) {
53
+ for (const file of actions.rotate)
54
+ note(file.path, file.template ? 'template' : 'rotate', name, file.keyed === true, file.read);
55
+ for (const path of actions.unknown)
56
+ note(path, 'unknown', name);
57
+ for (const route of actions.openRoutes)
58
+ note(route.path, 'route', name);
59
+ for (const path of actions.onlyInResults)
60
+ note(path, 'result', name);
61
+ }
62
+ const rows = [...found.entries()]
63
+ .map(([path, row]) => ({ label: ORDER[row.rank] ?? 'result', path, sessions: row.sessions, rank: row.rank, keyed: row.keyed, read: row.read }))
64
+ .sort((a, b) => a.rank - b.rank || b.sessions.length - a.sessions.length || a.path.localeCompare(b.path))
65
+ .map(({ label, path, sessions: names, keyed, read }) => ({
66
+ label,
67
+ path,
68
+ sessions: names,
69
+ ...(keyed ? { keyed: true } : {}),
70
+ ...(read === undefined ? {} : { read }),
71
+ }));
72
+ return { rows, refusedAttempts: sessions.reduce((sum, { actions }) => sum + actions.refusedAttempts, 0) };
73
+ }
74
+ /** Two sessions' reads of one file: every key format and line either saw, in the order first seen. */
75
+ function mergeReads(a, b) {
76
+ const keyed = new Map([...a.keyed, ...b.keyed].map((line) => [line.name + '\u0000' + line.key, line]));
77
+ return {
78
+ keys: [...new Set([...a.keys, ...b.keys])],
79
+ names: [...new Set([...a.names, ...b.names])].slice(0, NAMES_PER_FILE),
80
+ keyed: [...keyed.values()].slice(0, NAMES_PER_FILE),
81
+ ...(a.mixed === true || b.mixed === true ? { mixed: true } : {}),
82
+ };
83
+ }
@@ -0,0 +1,90 @@
1
+ import { NOTE_LIMIT, resultAllowed, standingMarks } from './marks.js';
2
+ /** How many lines a refused mark offers as the ones it could have meant. */
3
+ const SUGGESTIONS = 8;
4
+ const LABEL = {
5
+ rotate: 'ROTATE',
6
+ template: 'CHECK',
7
+ unknown: 'UNKNOWN',
8
+ route: 'REACHED',
9
+ result: 'IN RESULT',
10
+ };
11
+ const RESULT = {
12
+ rotated: 'rotated',
13
+ 'not-secret': 'not a real secret',
14
+ handled: 'handled',
15
+ 'not-private': 'not private',
16
+ };
17
+ /** A note is one line a person typed; `undefined` when it may be recorded. */
18
+ export function noteProblem(note) {
19
+ const trimmed = note?.trim();
20
+ return trimmed !== undefined && (trimmed.length > NOTE_LIMIT || /[\r\n]/.test(trimmed))
21
+ ? { outcome: 'mark-refused', output: `A note is one line of at most ${NOTE_LIMIT} characters. Nothing was recorded.\n` }
22
+ : undefined;
23
+ }
24
+ /**
25
+ * Records a mark against the lines of a check (`worth-running-every-day` R31, R35), for `check --mark` and for the
26
+ * served page alike (R52): the same rules, the same words. `lines` name sessions by id, and are the check before marks.
27
+ */
28
+ export async function recordMark(store, lines, request, context) {
29
+ const problem = noteProblem(request.note);
30
+ if (problem !== undefined)
31
+ return problem;
32
+ const note = request.note?.trim();
33
+ const line = lines.find((row) => row.path === request.path);
34
+ if (line === undefined) {
35
+ const names = lines.slice(0, SUGGESTIONS).map((row) => ` ${LABEL[row.label]} ${row.path}`);
36
+ return refused(`${request.path} is not a line of the check for sessions active since ${context.asked}. Nothing was recorded.\n` +
37
+ (names.length === 0 ? 'The check has no lines in this range.\n' : `Its lines are:\n${names.join('\n')}\n`) +
38
+ (lines.length > SUGGESTIONS ? ` and ${lines.length - SUGGESTIONS} more (agentwhy check --full)\n` : '') +
39
+ 'A file reached earlier than that is marked with a wider range: --since 30d\n');
40
+ }
41
+ if (!resultAllowed(line, request.result)) {
42
+ return refused(request.result === 'not-secret'
43
+ ? `${request.path} is a ${LABEL[line.label]} line: a value from it was in an agent's context, so it is marked rotated, not "not a real secret". Nothing was recorded.\n`
44
+ : `A key was read from ${request.path}, so it is marked rotated once the key is changed, not "${RESULT[request.result]}". Nothing was recorded.\n`);
45
+ }
46
+ const written = await store.append({
47
+ kind: 'mark',
48
+ path: line.path,
49
+ label: line.label,
50
+ result: request.result,
51
+ at: context.now,
52
+ ...(note === undefined || note === '' ? {} : { note }),
53
+ sessions: line.sessions,
54
+ });
55
+ if (!written)
56
+ return { outcome: 'mark-failed', output: 'The mark could not be written, so nothing was recorded.\n' };
57
+ return {
58
+ outcome: 'marked',
59
+ output: `Marked ${line.path} as ${RESULT[request.result]} on ${timeOf(context.now)}.\n` +
60
+ `It leaves To do until a session active after this reaches the file again. Nothing checks that the key itself changed.\n` +
61
+ `History: agentwhy start --since ${context.asked} Undo: agentwhy check --unmark ${quoted(line.path)}\n`,
62
+ };
63
+ }
64
+ /** Undoes a standing mark by appending, never by removing (R34). */
65
+ export async function recordUnmark(store, path, now) {
66
+ const reading = await store.read();
67
+ if (reading.failed)
68
+ return { outcome: 'mark-failed', output: 'The record of marks could not be read, so nothing was undone.\n' };
69
+ const standing = standingMarks(reading.records);
70
+ const mark = standing.get(path);
71
+ if (mark === undefined) {
72
+ const names = [...standing.keys()].slice(0, SUGGESTIONS).map((marked) => ` ${marked}`);
73
+ return refused(`${path} has no standing mark. Nothing was recorded.\n` +
74
+ (names.length === 0 ? 'Nothing is marked in this project.\n' : `Marked now:\n${names.join('\n')}\n`));
75
+ }
76
+ const written = await store.append({ kind: 'unmark', path: mark.path, at: now });
77
+ if (!written)
78
+ return { outcome: 'mark-failed', output: 'The undoing could not be written, so the mark still stands.\n' };
79
+ return { outcome: 'marked', output: `${mark.path} is back in To do. The mark of ${timeOf(mark.at)} stays in History, shown as undone.\n` };
80
+ }
81
+ function refused(output) {
82
+ return { outcome: 'mark-refused', output };
83
+ }
84
+ /** A path as a shell reads it back: in single quotes, with a quote inside closed and reopened. */
85
+ function quoted(path) {
86
+ return `'${path.replaceAll("'", "'\\''")}'`;
87
+ }
88
+ function timeOf(epoch) {
89
+ return new Date(epoch).toISOString().slice(0, 16).replace('T', ' ') + ' UTC';
90
+ }
@@ -0,0 +1,84 @@
1
+ /** The mark that stands for each path: the last mark of it, unless an unmark came after (R34). */
2
+ export function standingMarks(records) {
3
+ const standing = new Map();
4
+ for (const record of records) {
5
+ if (record.kind === 'mark')
6
+ standing.set(record.path, record);
7
+ else
8
+ standing.delete(record.path);
9
+ }
10
+ return standing;
11
+ }
12
+ /** Every mark ever made, newest first, with what became of it - the record shows what was done and undone (R34, R36). */
13
+ export function markLines(records) {
14
+ const lines = [];
15
+ const open = new Map();
16
+ for (const record of records) {
17
+ const previous = open.get(record.path);
18
+ if (previous !== undefined) {
19
+ previous.status = record.kind === 'mark' ? 'replaced' : 'undone';
20
+ previous.endedAt = record.at;
21
+ open.delete(record.path);
22
+ }
23
+ if (record.kind === 'mark') {
24
+ const line = { mark: record, status: 'standing' };
25
+ lines.push(line);
26
+ open.set(record.path, line);
27
+ }
28
+ }
29
+ return lines.sort((a, b) => b.mark.at - a.mark.at);
30
+ }
31
+ /**
32
+ * A session's actions without the files a standing mark covers - when the session was last active no later than the
33
+ * mark. A session active after it keeps the file, so a file reached again comes back, counted in the sessions since
34
+ * (R35). Refusals, key shapes and mentions name no file and are kept whole.
35
+ */
36
+ export function actionsAfterMarks(actions, lastActive, marks) {
37
+ const hidden = (path) => {
38
+ const mark = marks.get(path);
39
+ return mark !== undefined && lastActive <= mark.at;
40
+ };
41
+ return {
42
+ ...actions,
43
+ rotate: actions.rotate.filter((file) => !hidden(file.path)),
44
+ openRoutes: actions.openRoutes.filter((route) => !hidden(route.path)),
45
+ onlyInResults: actions.onlyInResults.filter((path) => !hidden(path)),
46
+ unknown: actions.unknown.filter((path) => !hidden(path)),
47
+ };
48
+ }
49
+ /** The paths a session's actions name, whatever the label. */
50
+ export function pathsOf(actions) {
51
+ return new Set([
52
+ ...actions.rotate.map((file) => file.path),
53
+ ...actions.openRoutes.map((route) => route.path),
54
+ ...actions.onlyInResults,
55
+ ...actions.unknown,
56
+ ]);
57
+ }
58
+ /**
59
+ * What the marks did to a range: how many standing marks took a file out of it entirely, and which marked files a
60
+ * later session brought back (R35). Only marks whose file this range reached at all are counted.
61
+ */
62
+ export function marksInRange(before, after, standing) {
63
+ const reached = new Set(before.flatMap((actions) => [...pathsOf(actions)]));
64
+ const still = new Set(after.flatMap((actions) => [...pathsOf(actions)]));
65
+ const inRange = [...standing.values()].filter((mark) => reached.has(mark.path));
66
+ return {
67
+ done: inRange.filter((mark) => !still.has(mark.path)).length,
68
+ reopened: inRange.filter((mark) => still.has(mark.path)),
69
+ };
70
+ }
71
+ /**
72
+ * A result means something only for some lines (R31, F25, F50): a key that was in an agent's context is not cleared by
73
+ * calling it fake, and a file a key was read from is not closed as "handled" or "not private" - the key is what has to
74
+ * change. *Rotated* fits every line; *not a real secret* a template's.
75
+ */
76
+ export function resultAllowed(row, result) {
77
+ if (result === 'rotated')
78
+ return true;
79
+ if (result === 'not-secret')
80
+ return row.label === 'template';
81
+ return row.keyed !== true;
82
+ }
83
+ /** The longest note kept, in characters. A note says what was done; it is not a place for a key. */
84
+ export const NOTE_LIMIT = 200;
@@ -0,0 +1,332 @@
1
+ import { paint } from '../../../shared/colour.js';
2
+ import { table } from '../../../shared/text-table.js';
3
+ import { needsAction } from '../actions-digest.js';
4
+ const INDENT = 2;
5
+ /** How many files the brief view names before it says how many more there are (R15a). */
6
+ const BRIEF_ROWS = 5;
7
+ /**
8
+ * How wide a path may be drawn in the brief view. One long path used to set the column for every row, so five short
9
+ * paths were pushed across the screen by the sixth. The end of a path is what tells files apart, so the front is what
10
+ * gives way; the full view and the report print it whole.
11
+ */
12
+ const PATH_WIDTH = 46;
13
+ /**
14
+ * What to do about an open route depends on where the rules came from (R13). A rule a person can add is named only
15
+ * where the person has no rules the tool read; a settings file already has them, and the route is the problem.
16
+ */
17
+ const OPEN_ROUTE_ADVICE = {
18
+ settings: () => [
19
+ 'Your deny rules cover these paths, and a route they do not name reached them: a deny rule names a tool, not a',
20
+ 'file. agentwhy init --refuse refuses a shell command that names one of these paths or searches through it. A',
21
+ 'path built at run time still passes; only a boundary below the agent - file permissions, a sandbox - closes',
22
+ 'every route.',
23
+ ],
24
+ default: (digest) => {
25
+ const patterns = [...new Set(digest.openRoutes.flatMap((route) => (route.pattern === undefined ? [] : [route.pattern])))];
26
+ return [
27
+ 'No deny rules were read, so the built-in default decided what is protected, and nothing refused these calls.',
28
+ 'To have Claude Code refuse file tools on them, add to "permissions.deny" in .claude/settings.json:',
29
+ ...patterns.map((pattern) => `${' '.repeat(INDENT)}"Read(${pattern})"`),
30
+ 'then read sessions under it: --settings .claude/settings.json. A shell command still passes a Read rule;',
31
+ 'agentwhy init --refuse covers a command line that names the path.',
32
+ ];
33
+ },
34
+ file: () => [
35
+ 'Your policy file protects these paths, and nothing refused these calls. agentwhy reads that file; Claude Code',
36
+ 'enforces only "permissions.deny" in its settings.',
37
+ ],
38
+ };
39
+ /** What the brief view says about one file, strongest first. The label leads, so a column of them reads down. */
40
+ /** The tool's own word for each kind of line, as `check` prints it; To fix's developer details read the same table. */
41
+ export const LABELS = {
42
+ rotate: 'ROTATE',
43
+ template: 'CHECK',
44
+ unknown: 'UNKNOWN',
45
+ route: 'REACHED',
46
+ result: 'IN RESULT',
47
+ };
48
+ /** Strongest first. A file is shown once, under the strongest thing known about it. */
49
+ const ORDER = ['rotate', 'template', 'unknown', 'route', 'result'];
50
+ export class TextDigestRenderer {
51
+ #view;
52
+ #colour;
53
+ /** Colour is decided in the shell and handed here, as the report's is (`findings-worth-reading` R15). */
54
+ constructor(view, options = {}) {
55
+ this.#view = view;
56
+ this.#colour = options.colour === true;
57
+ }
58
+ render(digest) {
59
+ const lines = this.#view === 'full' ? full(digest) : this.#view === 'counts' ? counts(digest) : brief(digest);
60
+ return `${(this.#colour ? colourise(lines) : lines).join('\n')}\n`;
61
+ }
62
+ }
63
+ /**
64
+ * The answer on one screen (R15a): a heading, one line per file with the strongest thing known about it, and where the
65
+ * rest is. A file reached, seen in a result and carrying a value is **one** line, not three - the sections of the full
66
+ * view describe the same handful of files, and reading them as separate findings is how a short answer became a wall.
67
+ */
68
+ function brief(digest) {
69
+ const heading = headingOf(digest);
70
+ if (heading !== undefined)
71
+ return heading;
72
+ const rows = briefRows(digest);
73
+ if (rows.length === 0) {
74
+ return [
75
+ (digest.marks?.done ?? 0) > 0
76
+ ? `Nothing left to act on: ${read(digest)}${under(digest)}.`
77
+ : `Nothing to act on: ${read(digest)}${under(digest)}, and no protected file was reached.`,
78
+ ...unreadableLines(digest),
79
+ ...(digest.refusedAttempts === 0 ? [] : [`Your rules held: ${plural(digest.refusedAttempts, 'attempt was', 'attempts were')} refused.`]),
80
+ ...markLines(digest),
81
+ ];
82
+ }
83
+ const shown = rows.slice(0, BRIEF_ROWS);
84
+ const width = Math.max(...shown.map((row) => LABELS[row.label].length));
85
+ const pathWidth = Math.max(...shown.map((row) => shorten(row.path).length));
86
+ return [
87
+ `agentwhy · ${read(digest)}${under(digest)}`,
88
+ ...unreadableLines(digest),
89
+ '',
90
+ ...table(shown.map((row) => [`${LABELS[row.label].padEnd(width)} ${shorten(row.path).padEnd(pathWidth)}`, `${row.says}, ${inSessions(row.sessions)}`]), INDENT),
91
+ ...(rows.length > shown.length ? [`${' '.repeat(INDENT)}and ${rows.length - shown.length} more`] : []),
92
+ ...(digest.refusedAttempts === 0
93
+ ? []
94
+ : [`${' '.repeat(INDENT)}your rules held: ${plural(digest.refusedAttempts, 'attempt was', 'attempts were')} refused`]),
95
+ ...(digest.mentions === 0
96
+ ? []
97
+ : [`${' '.repeat(INDENT)}${plural(digest.mentions, 'path was', 'paths were')} named in text a call carried, which opens no file; not counted`]),
98
+ ...markLines(digest, INDENT),
99
+ '',
100
+ 'What each line means, and what to do: agentwhy check --full',
101
+ 'Dealt with one: agentwhy check --mark rotated <path>',
102
+ `Session by session, in the browser: agentwhy start --since ${digest.asked}`,
103
+ ];
104
+ }
105
+ /** One row per file, strongest first: a value seen beats an unknown outcome, which beats a route, which beats a result. */
106
+ function briefRows(digest) {
107
+ const rows = new Map();
108
+ const reopened = new Map((digest.marks?.reopened ?? []).map((mark) => [mark.path, mark]));
109
+ const add = (label, path, says, sessions) => {
110
+ if (rows.has(path))
111
+ return;
112
+ const mark = reopened.get(path);
113
+ // A marked file is here only because a session after the mark reached it; the count is of those sessions (R35).
114
+ const again = mark === undefined ? '' : `; marked ${RESULT_WORDS[mark.result]} ${dayOf(mark.at)}, reached again since`;
115
+ rows.set(path, { label, path, says: says + again, sessions });
116
+ };
117
+ for (const file of digest.rotate) {
118
+ if (file.template === true)
119
+ add('template', file.path, 'a value was read from this template', file.sessions);
120
+ else
121
+ add('rotate', file.path, "a value was in an agent's context", file.sessions);
122
+ }
123
+ for (const file of digest.unknown)
124
+ add('unknown', file.path, 'a call named it, its outcome is not recorded', file.sessions);
125
+ for (const route of digest.openRoutes)
126
+ add('route', route.path, `${route.did}, nothing refused it`, route.sessions);
127
+ for (const file of digest.onlyInResults)
128
+ add('result', file.path, 'printed by a search, no call named it', file.sessions);
129
+ return [...rows.values()].sort((a, b) => ORDER.indexOf(a.label) - ORDER.indexOf(b.label) || b.sessions - a.sessions);
130
+ }
131
+ function full(digest) {
132
+ const heading = headingOf(digest);
133
+ if (heading !== undefined)
134
+ return heading;
135
+ const unreadable = unreadableLines(digest);
136
+ const refused = refusedLines(digest);
137
+ if (!needsAction(digest)) {
138
+ return [
139
+ (digest.marks?.done ?? 0) > 0
140
+ ? `Nothing left to act on: ${read(digest)}${under(digest)}.`
141
+ : `Nothing to act on: ${read(digest)}${under(digest)}, and no protected file was reached.`,
142
+ ...unreadable,
143
+ ...refused,
144
+ ...markLines(digest),
145
+ ];
146
+ }
147
+ return [
148
+ `agentwhy check - ${read(digest)}${under(digest)}.`,
149
+ ...unreadable,
150
+ ...section(digest.rotate.length > 0, 'Rotate', [
151
+ "A value from these files was in an agent's context: it was sent to the model provider, and it is kept in the",
152
+ 'transcripts on this machine. Rotating the value is the only step that undoes that. A file named as a template is',
153
+ 'marked: it is meant to be committed, so the question there is whether the value in it is real.',
154
+ ], table(digest.rotate.map((entry) => [entry.path, `${entry.template === true ? 'template · ' : ''}${inSessions(entry.sessions)}`]), INDENT)),
155
+ ...section(digest.openRoutes.length > 0, 'Open routes', ['These protected paths were reached, and nothing refused the call.'], [...routeRows(digest.openRoutes), '', ...OPEN_ROUTE_ADVICE[digest.policyKind](digest)]),
156
+ ...section(digest.onlyInResults.length > 0, 'Seen only in results', [
157
+ 'No call named these paths: they appeared in what a command printed, as a recursive search prints them.',
158
+ 'agentwhy init --refuse refuses a grep -r or rg that would read one; a boundary below the agent closes',
159
+ 'every route.',
160
+ ], pathRows(digest.onlyInResults)),
161
+ ...section(digest.unknown.length > 0, 'Outcome unknown', [
162
+ 'A call named or printed these protected paths, and its result was not recorded, so whether the file was read cannot',
163
+ 'be established. The report of the session shows which call.',
164
+ ], pathRows(digest.unknown)),
165
+ ...section(digest.secretShapes.length > 0, 'Key shapes', ['Results carried something shaped like a known key. The report of the session shows where; the value is never shown.'], table(digest.secretShapes.map((shape) => [shape.name, times(shape.count)]), INDENT)),
166
+ ...section(digest.mentions > 0, 'Not counted', [
167
+ `${plural(digest.mentions, 'protected path was', 'protected paths were')} named in text that a call carried - a message, a report, a question -`,
168
+ 'by a tool this version has no profile for. Such a call opens no file, so it is not a route. The report of the',
169
+ 'session shows them, and says the tool was not recognised.',
170
+ ], []),
171
+ ...(refused.length === 0 ? [] : ['', ...refused]),
172
+ ...(markLines(digest).length === 0 ? [] : ['', ...markLines(digest)]),
173
+ '',
174
+ `Each session in detail: agentwhy start --since ${digest.asked}`,
175
+ 'Dealt with a file: agentwhy check --mark rotated <path> (not-secret for a template)',
176
+ ];
177
+ }
178
+ /**
179
+ * The digest as one line of counts, and where the detail is (R17). "Nothing to act on" is said only of sessions that
180
+ * were read: a range whose sessions all failed is not a clean range (R15). Found by a review.
181
+ */
182
+ function counts(digest) {
183
+ const unreadable = unreadableLines(digest, 'in this range', 'here');
184
+ if (digest.sessionsRead === 0) {
185
+ return digest.sessionsUnreadable === 0 ? ['No session was active in this range.'] : unreadable;
186
+ }
187
+ // One sentence per line: the counts, what the rules held, and what is already dealt with are three answers, and
188
+ // run together they were one line long enough to wrap twice in a terminal nobody had made wide for it.
189
+ const held = digest.refusedAttempts === 0 ? [] : [`Your rules held: ${plural(digest.refusedAttempts, 'attempt was', 'attempts were')} refused.`];
190
+ const done = doneLine(digest);
191
+ if (!needsAction(digest)) {
192
+ return [`Nothing to act on in the ${plural(digest.sessionsRead, 'session', 'sessions')} read.`, ...held, ...done, ...unreadable];
193
+ }
194
+ const parts = [
195
+ ...(digest.rotate.length === 0 ? [] : [`${plural(digest.rotate.length, 'file', 'files')} to rotate`]),
196
+ ...(digest.openRoutes.length === 0 ? [] : [`${plural(digest.openRoutes.length, 'open route', 'open routes')} to a protected path`]),
197
+ ...(digest.onlyInResults.length === 0 ? [] : [`${plural(digest.onlyInResults.length, 'path', 'paths')} seen only in results`]),
198
+ ...(digest.unknown.length === 0 ? [] : [`${plural(digest.unknown.length, 'path', 'paths')} with an unknown outcome`]),
199
+ ...(digest.secretShapes.length === 0 ? [] : [`${plural(digest.secretShapes.reduce((sum, shape) => sum + shape.count, 0), 'key shape', 'key shapes')}`]),
200
+ ];
201
+ return [`To act on: ${parts.join(' · ')}`, ...held, ...done, ...unreadable, `What to do: agentwhy check --since ${digest.asked}`];
202
+ }
203
+ /** The answers that are the whole output: an empty range, and a range in which nothing could be read. */
204
+ function headingOf(digest) {
205
+ if (digest.sessionsRead === 0 && digest.sessionsUnreadable === 0) {
206
+ return [
207
+ `No session of this project was active since ${digest.asked}, so there is nothing to check.`,
208
+ 'To look further back: agentwhy check --since 30d',
209
+ ];
210
+ }
211
+ // Every session in range failed: nothing was read, so nothing can be called clear.
212
+ return digest.sessionsRead === 0 ? unreadableLines(digest) : undefined;
213
+ }
214
+ function unreadableLines(digest, where = 'inside the range', rest = 'below') {
215
+ if (digest.sessionsUnreadable === 0)
216
+ return [];
217
+ const them = digest.sessionsUnreadable === 1 ? 'it' : 'them';
218
+ return [`${plural(digest.sessionsUnreadable, 'session', 'sessions')} ${where} could not be read, and nothing ${rest} covers ${them}.`];
219
+ }
220
+ function read(digest) {
221
+ return `${plural(digest.sessionsRead, 'session', 'sessions')} active since ${digest.asked}`;
222
+ }
223
+ function under(digest) {
224
+ return digest.policy === undefined ? '' : `, read under ${digest.policy}`;
225
+ }
226
+ function refusedLines(digest) {
227
+ if (digest.refusedAttempts === 0)
228
+ return [];
229
+ return [
230
+ `Your rules held: ${plural(digest.refusedAttempts, 'attempt was', 'attempts were')} refused. A refused Read raises no hook event, so only the`,
231
+ 'transcripts show these.',
232
+ ];
233
+ }
234
+ function section(shown, heading, explanation, rows) {
235
+ return shown ? ['', heading, ...explanation, ...(rows.length === 0 ? [] : ['', ...rows])] : [];
236
+ }
237
+ function pathRows(paths) {
238
+ return table(paths.map((entry) => [entry.path, inSessions(entry.sessions)]), INDENT);
239
+ }
240
+ function routeRows(routes) {
241
+ const width = Math.max(...routes.map((route) => route.did.length));
242
+ return table(routes.map((route) => [route.path, `${route.did.padEnd(width)} ${times(route.occurrences)}, ${inSessions(route.sessions)}`]), INDENT);
243
+ }
244
+ const RESULT_WORDS = { rotated: 'rotated', 'not-secret': 'not a real secret', handled: 'handled', 'not-private': 'not private' };
245
+ /** What the person's marks did to this range, said once, and where the record is shown (R35, R36). */
246
+ function markLines(digest, indent = 0) {
247
+ const marks = digest.marks;
248
+ if (marks === undefined)
249
+ return [];
250
+ const pad = ' '.repeat(indent);
251
+ return [
252
+ ...(marks.done === 0
253
+ ? []
254
+ : [`${pad}${plural(marks.done, 'file was', 'files were')} marked done and not reached since; History: agentwhy start --since ${digest.asked}`]),
255
+ ...(marks.unreadable ? [`${pad}the record of marks could not be read, so nothing marked was left out`] : []),
256
+ ];
257
+ }
258
+ function doneLine(digest) {
259
+ const done = digest.marks?.done ?? 0;
260
+ return done === 0 ? [] : [`${plural(done, 'file', 'files')} marked done.`];
261
+ }
262
+ function dayOf(epoch) {
263
+ return new Date(epoch).toISOString().slice(0, 10);
264
+ }
265
+ /** A path cut at the front, so what is left still names the file. Never cut in the full view or in a report. */
266
+ function shorten(path) {
267
+ return path.length <= PATH_WIDTH ? path : `…${path.slice(path.length - PATH_WIDTH + 1)}`;
268
+ }
269
+ function inSessions(count) {
270
+ return `in ${plural(count, 'session', 'sessions')}`;
271
+ }
272
+ function times(count) {
273
+ return count === 1 ? 'once' : `${count} times`;
274
+ }
275
+ function plural(count, one, many) {
276
+ return `${count} ${count === 1 ? one : many}`;
277
+ }
278
+ /** The headings of the full view. Painted, never changed: the word is the heading, the weight only repeats it. */
279
+ const SECTIONS = new Set(['Rotate', 'Open routes', 'Seen only in results', 'Outcome unknown', 'Key shapes', 'Not counted']);
280
+ /** What each label of the brief view means, as colour: a value that left the machine is red, a question is yellow. */
281
+ const LABEL_HUE = {
282
+ ROTATE: 'red',
283
+ CHECK: 'yellow',
284
+ UNKNOWN: 'yellow',
285
+ REACHED: 'red',
286
+ 'IN RESULT': 'yellow',
287
+ };
288
+ /**
289
+ * Colour laid over the lines already written (R14): every coloured word is a word, and removing the escapes gives
290
+ * back exactly what was written without it. Each rule paints one part of a line and never inside another's, so no
291
+ * escape is ever nested in another.
292
+ */
293
+ function colourise(lines) {
294
+ return lines.map((line) => {
295
+ // The tool's own name, in the colour that marks the tool and never an outcome.
296
+ const named = /^agentwhy(?: check)? [·-] /.exec(line);
297
+ if (named !== null)
298
+ return `${paint('agentwhy', 'indigo')}${paint(line.slice('agentwhy'.length), 'dim')}`;
299
+ if (SECTIONS.has(line))
300
+ return paint(line, 'bold');
301
+ // A range that is clear, and rules that refused something, are the two things that went well.
302
+ if (/^Nothing (?:left )?to act on/.test(line))
303
+ return paint(line, 'green');
304
+ for (const prefix of ['Your rules held:']) {
305
+ if (line.startsWith(prefix))
306
+ return `${paint(prefix, 'green')}${line.slice(prefix.length)}`;
307
+ }
308
+ // The counts: the heading says there is something to do, the numbers are what to do it about.
309
+ if (line.startsWith('To act on:')) {
310
+ return `${paint('To act on:', 'yellow')}${count(line.slice('To act on:'.length))}`;
311
+ }
312
+ if (/could not be read/.test(line))
313
+ return paint(line, 'yellow');
314
+ // A row of the brief view: the label leads, and says the same thing the colour does.
315
+ const labelled = /^( {2})(ROTATE|CHECK|UNKNOWN|REACHED|IN RESULT)/.exec(line);
316
+ if (labelled !== null) {
317
+ const [, indent = '', label = ''] = labelled;
318
+ return `${indent}${paint(label, LABEL_HUE[label] ?? 'bold')}${line.slice(indent.length + label.length)}`;
319
+ }
320
+ // Where the rest is: the words are the question, the command is the answer to copy.
321
+ const command = /^(.*?:)( +)(agentwhy .*)$/.exec(line);
322
+ if (command !== null) {
323
+ const [, words = '', gap = '', rest = ''] = command;
324
+ return `${paint(words, 'dim')}${gap}${paint(rest, 'bold')}`;
325
+ }
326
+ return line;
327
+ });
328
+ }
329
+ /** The numbers in a line of counts, marked as the numbers they are. */
330
+ function count(text) {
331
+ return text.replace(/\b\d+\b/g, (number) => paint(number, 'bold'));
332
+ }