@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,20 @@
1
+ import { matchesGlob } from './glob.js';
2
+ /**
3
+ * The entry that protects this path, or undefined. An exception wins over every protecting pattern, so a path
4
+ * someone deliberately excluded stays excluded however many rules would otherwise cover it.
5
+ */
6
+ export function protectionOf(policy, path) {
7
+ // A directory is written both ways - `ls secrets` and `cat secrets/x` - while `**/secrets/**` only matches
8
+ // the form with the separator. Both are tried, so how the command happened to be typed does not decide.
9
+ const forms = path.endsWith('/') ? [path] : [path, `${path}/`];
10
+ if (forms.some((form) => policy.allowed.some((pattern) => matchesGlob(form, pattern))))
11
+ return undefined;
12
+ return policy.protected.find((entry) => forms.some((form) => matchesGlob(form, entry.pattern)));
13
+ }
14
+ export function protects(policy, path) {
15
+ return protectionOf(policy, path) !== undefined;
16
+ }
17
+ /** The same policy with only what is kept from the agent: what a hook that stops a command may act on (F57). */
18
+ export function blockingOnly(policy) {
19
+ return { ...policy, protected: policy.protected.filter((entry) => entry.mode !== 'tell') };
20
+ }
@@ -0,0 +1,16 @@
1
+ import { DEFAULT_POLICY } from './default-policy.js';
2
+ /**
3
+ * Chooses the policy: an explicit file, else the environment's own deny rules, else the built-in default.
4
+ *
5
+ * **A policy file that cannot be read is an error, never a fallback.** Falling back would run the whole analysis
6
+ * under rules nobody chose while the header claimed a policy was in force - the failure mode this tool exists to
7
+ * describe, in its own output. Absence is a different thing from breakage: absence falls through, breakage stops.
8
+ */
9
+ export function resolvePolicy(candidates) {
10
+ const { file, settings } = candidates;
11
+ if (file !== undefined)
12
+ return 'errors' in file ? { errors: file.errors } : { policy: file.policy };
13
+ if (settings !== undefined)
14
+ return { policy: settings };
15
+ return { policy: DEFAULT_POLICY };
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,133 @@
1
+ /**
2
+ * One recorded directory is the root. None and several are both "no root" - but they are different answers to
3
+ * the reader, so they stay different here rather than collapsing into `undefined`.
4
+ */
5
+ export function projectRootOf(directories) {
6
+ const distinct = [...new Set(directories)];
7
+ const [only] = distinct;
8
+ if (distinct.length === 1 && only !== undefined)
9
+ return { kind: 'known', path: only };
10
+ return distinct.length === 0 ? { kind: 'absent' } : { kind: 'ambiguous', count: distinct.length };
11
+ }
12
+ /** How the root itself is shown once paths are relative to it. An empty string would render as no path at all. */
13
+ export const PROJECT_ROOT_ITSELF = '.';
14
+ /** What a path outside the project becomes in a shared report: the signal kept, the location dropped (R3). */
15
+ export const OUTSIDE_THE_PROJECT = 'outside the project';
16
+ /** Either separator: a transcript recorded on Windows carries the other one, and its paths are paths too. */
17
+ const SEPARATOR = /[\\/]/;
18
+ /** The characters a path is made of. What sits next to a match decides whether the match is a path at all. */
19
+ const PATH_CHARACTER = String.raw `[\w.~@+\-\\/]`;
20
+ /**
21
+ * Absolute means "says where it starts": at the root, at a home directory, or at a drive. Anything else was
22
+ * recorded **relative to the working directory**, which is to say inside the project - that is what makes
23
+ * `apps/web/.env` from a shell command a path inside the project and not one above it.
24
+ */
25
+ function isAbsolute(value) {
26
+ return value.startsWith('/') || value.startsWith('~') || /^[A-Za-z]:[\\/]/.test(value);
27
+ }
28
+ /** The root without its trailing separator, whichever kind it was written with. */
29
+ function baseOf(root) {
30
+ return SEPARATOR.test(root.path.slice(-1)) ? root.path.slice(0, -1) : root.path;
31
+ }
32
+ /**
33
+ * Whether a path walks back up through a `..` segment. Comparing prefixes cannot see one: `<root>/../../.ssh/x`
34
+ * starts with the root and is above it, and `../../.ssh/x` is relative and is above it too. Both were shown in a
35
+ * shared report until this was written.
36
+ *
37
+ * A segment, never a substring: `..config` and `a..b` are names, not a climb.
38
+ */
39
+ function climbs(value) {
40
+ return value.split(SEPARATOR).includes('..');
41
+ }
42
+ function escapeForPattern(value) {
43
+ return value.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
44
+ }
45
+ /**
46
+ * How a path is shown: relative to the project root where it lies under one, and exactly as written where it
47
+ * does not (`specs/2026-09-14-path-display-and-share.md` R1-R3).
48
+ *
49
+ * Two things this buys beyond brevity. A reader of a monorepo sees `apps/web/.env` and `packages/widget/.env`
50
+ * as two files rather than two identical labels. And the part that names the account, the machine and the
51
+ * employer is not the finding, so it stops being repeated on every line of the report.
52
+ */
53
+ export function displayPath(value, root) {
54
+ if (root.kind !== 'known')
55
+ return value;
56
+ const base = baseOf(root);
57
+ if (value === base || (value.length === base.length + 1 && value.startsWith(base)))
58
+ return PROJECT_ROOT_ITSELF;
59
+ if (!value.startsWith(base) || !SEPARATOR.test(value.charAt(base.length)))
60
+ return value;
61
+ return value.slice(base.length + 1);
62
+ }
63
+ /**
64
+ * Whether a path can be said to lie inside the project - the question the shared view asks before it decides
65
+ * to drop a location.
66
+ *
67
+ * **Corrected 2026-09-14 after a review.** This used to be "the display path is unchanged, so it is outside",
68
+ * and that is false for every path a transcript records relative to the working directory: a `grep` line or a
69
+ * shell argument carries `apps/web/.env`, which no root prefix matches, so the shared view replaced the common
70
+ * case with a category and merged unrelated files into one node.
71
+ */
72
+ export function insideProject(value, root) {
73
+ // A path that climbs names something the root does not contain, whichever end it starts from. It is the one
74
+ // case where "recorded relative to the working directory" does not mean "inside the project".
75
+ if (climbs(value))
76
+ return false;
77
+ if (!isAbsolute(value))
78
+ return true;
79
+ if (root.kind !== 'known')
80
+ return false;
81
+ return displayPath(value, root) !== value;
82
+ }
83
+ /**
84
+ * The same transformation for a path that sits **inside** free text - a delegation description, a label. Without
85
+ * it the absolute form would simply move out of a path field and into a sentence, which is the failure L010
86
+ * names: a rule applied to one position rather than to the thing itself.
87
+ *
88
+ * The root is replaced only where it **is** the start of a path. `/a/proj` sits inside `/backup/a/proj/x` as
89
+ * text and names nothing there; replacing it produced `/backupx`, a file that exists nowhere. It is also
90
+ * replaced when it stands alone, and to the same `.` that a path field would show - one directory cannot be
91
+ * inside the project in one place and above it in another.
92
+ */
93
+ export function displayPathsIn(text, root) {
94
+ if (root.kind !== 'known')
95
+ return text;
96
+ const base = escapeForPattern(baseOf(root));
97
+ const pattern = new RegExp(`(?<!${PATH_CHARACTER})${base}(?:[\\\\/]|(?!${PATH_CHARACTER}))`, 'g');
98
+ return text.replace(pattern, (match) => (SEPARATOR.test(match.slice(-1)) ? '' : PROJECT_ROOT_ITSELF));
99
+ }
100
+ /**
101
+ * An absolute path, conservatively: something that says where it starts - the root, a home directory or a
102
+ * drive - and has at least two segments. Used **only** in the shared view, and only on free text.
103
+ *
104
+ * The leading boundary is load-bearing. Without it the rule matched inside a path that had **already** been made
105
+ * relative - `apps/web/.env` contains `/web/.env` - and a correct display path was destroyed by the step meant
106
+ * to protect it.
107
+ *
108
+ * This is pattern-matching prose, which this project distrusts on principle - so it is pointed in the safe
109
+ * direction. Over-matching costs a reader some detail in a report they chose to share; under-matching puts an
110
+ * account name on a slide. A URL is over-matched on purpose rather than carved out with a second rule.
111
+ */
112
+ const ABSOLUTE_PATH = /(?<![\w.~@+-])(?:[A-Za-z]:[\\/]|~|\/)[^\s"'`,;:()<>|{}[\]]*[\\/][^\s"'`,;:()<>|{}[\]]+/g;
113
+ /** Free text as a shared report may carry it: nothing above the project root survives, in any position. */
114
+ export function withoutAbsolutePaths(text) {
115
+ return text.replace(ABSOLUTE_PATH, OUTSIDE_THE_PROJECT);
116
+ }
117
+ /** What a machine-issued identifier becomes in a shared report. */
118
+ export const IDENTIFIER_NOT_SHOWN = 'id not shown';
119
+ /**
120
+ * A UUID: how a session, a message or a request is named on the machine that recorded it. Used only in the
121
+ * shared view, on text that is shown - the same place and the same safe direction as `ABSOLUTE_PATH`.
122
+ */
123
+ const UUID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
124
+ /**
125
+ * Text as a shared report may carry it: no absolute path and no machine-issued identifier, in any position.
126
+ *
127
+ * **Added 2026-09-14 after running `start --share` on a real project.** Identifiers were already replaced where the
128
+ * report emits them, and that was not enough: a session's own transcript can quote another session's records, and
129
+ * a quoted `uuid` travels inside whatever text carried it.
130
+ */
131
+ export function forSharing(text) {
132
+ return withoutAbsolutePaths(text).replace(UUID, IDENTIFIER_NOT_SHOWN);
133
+ }
@@ -0,0 +1,10 @@
1
+ /** Each session recognised by the titles of the AI that wrote it (`2026-09-27-what-codex-wrote.md` X28, step 5). */
2
+ export class ProviderTitles {
3
+ #titles;
4
+ constructor(titles) {
5
+ this.#titles = titles;
6
+ }
7
+ recognise(session) {
8
+ return this.#titles[session.provider].recognise(session);
9
+ }
10
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Class C of spec §5.4: entropy alone. It **redacts and never reports** — a high-entropy string is not
3
+ * evidence of anything, and lesson L001 measured what happens without that rule: in one session, 385 `toolu_`
4
+ * identifiers and 1264 UUIDs, every one of them above this threshold. An unguarded class C would have produced
5
+ * over 1600 findings, none of them real.
6
+ *
7
+ * The exclusion list is therefore not an optimisation; it is the only thing that makes the class usable.
8
+ */
9
+ const THRESHOLD_BITS = 4.0;
10
+ const MINIMUM_LENGTH = 20;
11
+ const EXCLUDED = [
12
+ // The tool's own identifiers - the single most important exclusion on the list (L001).
13
+ /^toolu_[A-Za-z0-9]{20,}$/,
14
+ /^msg_[A-Za-z0-9]{10,}$/,
15
+ /^(agent-)?a[0-9a-f]{16}$/,
16
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
17
+ /^[0-9a-f]{40}$/i,
18
+ /^[0-9a-f]{64}$/i,
19
+ /^sha(256|512)-[A-Za-z0-9+/=]+$/,
20
+ /^data:[\w/+.-]+;base64,/,
21
+ /^[\w.-]+\.(ts|js|mjs|cjs|css|map|json)$/i,
22
+ /^https?:\/\//i,
23
+ /^[~.]{0,2}\//,
24
+ // A path is not a secret by shape, and a long one is random-looking enough to cross the threshold: a
25
+ // path like `apps/web/.env.development` was once replaced by a pseudonym, which turned the finding into
26
+ // gibberish. A slash-separated run of ordinary path characters is a path. The cost is a base64 blob that
27
+ // happens to contain a slash and no padding, which class A and class B both still see.
28
+ /^[\w.@+-]+(\/[\w.@+-]+)+$/,
29
+ ];
30
+ export function shannonBits(text) {
31
+ const counts = new Map();
32
+ for (const character of text)
33
+ counts.set(character, (counts.get(character) ?? 0) + 1);
34
+ return [...counts.values()].reduce((bits, count) => {
35
+ const probability = count / text.length;
36
+ return bits - probability * Math.log2(probability);
37
+ }, 0);
38
+ }
39
+ /** High entropy, long enough, and none of the shapes this tool produces by the thousand. */
40
+ export function looksRandom(token) {
41
+ if (token.length < MINIMUM_LENGTH)
42
+ return false;
43
+ if (EXCLUDED.some((pattern) => pattern.test(token)))
44
+ return false;
45
+ return shannonBits(token) >= THRESHOLD_BITS;
46
+ }
@@ -0,0 +1,4 @@
1
+ /** The only place a `Redacted` comes into being. Not exported beyond this directory. */
2
+ export function brand(text) {
3
+ return text;
4
+ }
@@ -0,0 +1,339 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { displayPath, displayPathsIn, insideProject, forSharing, OUTSIDE_THE_PROJECT, } from '../project-root.js';
3
+ import { looksRandom } from './entropy.js';
4
+ import { brand } from './redacted.js';
5
+ import { CLASS_A, RULESET_VERSION } from './secret-patterns.js';
6
+ import { ValueTrace } from './value-trace.js';
7
+ import { isConfigName, isExcludedValue, isSensitiveName, unquote } from './value-shapes.js';
8
+ const RANK = { knownFormat: 1, urlPassword: 2, sensitiveName: 3, entropy: 4 };
9
+ /** Step 1: a known format. It **ignores every exclusion** - a real `ghp_…` in a `NEXT_PUBLIC_` name is the case
10
+ * we most want to see. */
11
+ function classAMatches(text) {
12
+ return CLASS_A.flatMap(({ name, pattern }) => [...text.matchAll(pattern)].map((match) => {
13
+ // A pattern may keep a prefix - `:_authToken=` stays, the token after it does not.
14
+ const prefix = typeof match[1] === 'string' ? match[1] : '';
15
+ return { name, rank: RANK.knownFormat, start: match.index + prefix.length, end: match.index + match[0].length };
16
+ }));
17
+ }
18
+ /**
19
+ * Step 2: only the password of a URL, so `postgres://app:[redacted]@db/main` stays readable as a finding.
20
+ *
21
+ * The scheme is bounded, and the bound is the whole point. Written as `[\w+.-]*`, the run before `://` was retried
22
+ * from every position of a line, and a transcript's lines are long - measured at about 31 KB each on real sessions.
23
+ * That made one line quadratic: a CPU profile of a 65 MB session put **3.7 of its 4.4 seconds inside this one
24
+ * pattern**, which is most of what `report`, `check`, `start` and the `Stop` hook were spending per run. Bounding the
25
+ * scheme to what a scheme can be - 32 characters, against `postgres`, `mongodb+srv`, `jdbc` - took the same text from
26
+ * 113 ms to 1 ms, with the same matches. The password itself stays unbounded: a long one is rare, it is reached only
27
+ * after a scheme and a host have already matched, and refusing to redact it would be the wrong way to save time.
28
+ *
29
+ * The lookbehind is the same argument once more. A bounded scheme is still tried from every letter of a run, which
30
+ * on 65 MB of text was another 1.3 seconds; a scheme cannot begin in the middle of a *letter* run, so those
31
+ * positions are refused in one character each. It must be letters only, not `\w` - a scheme glued to a *digit*
32
+ * (a timestamp, a line number) still begins right there, and excluding digits too silently dropped that password.
33
+ * Measured on the same line: 6 ms to none at all, and the same matches for `postgres://`, `mongodb+srv://`, a
34
+ * scheme after `=`, and one welded to a word before it.
35
+ */
36
+ const URL_PASSWORD = /(?<![A-Za-z])([a-z][\w+.-]{0,31}:\/\/[^\s:/@]+:)([^\s@]+)(?=@)/gi;
37
+ function urlPasswords(text) {
38
+ return [...text.matchAll(URL_PASSWORD)].map((match) => ({
39
+ name: 'url-password',
40
+ rank: RANK.urlPassword,
41
+ start: match.index + (match[1] ?? '').length,
42
+ end: match.index + match[0].length,
43
+ }));
44
+ }
45
+ /**
46
+ * Steps 3 and 4: a value beside a sensitive name, once the value-shape exclusions have had their say. The key
47
+ * name is never touched - it is the finding (§5.4).
48
+ */
49
+ /**
50
+ * `NAME=value`, `"name": "value"`, `name: value`.
51
+ *
52
+ * The name must start at a boundary and hold no dots, which two failures taught: without the boundary,
53
+ * `apps/web/.env.development:12:KEY=value` read `env.development` as the name and swallowed the rest of the
54
+ * line as its value, and without the optional quotes a JSON key was never seen at all.
55
+ */
56
+ const KEY_VALUE = /(^|[\s,;:{[(]["']?)([A-Za-z_][A-Za-z0-9_-]*)(["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/g;
57
+ /** What a replacement looks like. A value that is already one is not a value: re-reading it mangles the text. */
58
+ const PSEUDONYM = '[redacted:';
59
+ /**
60
+ * C0 and C1 control characters, and DEL - everything but the tab and the newline a text legitimately carries.
61
+ *
62
+ * A transcript is written by the very party the report is about, and a report is read in a terminal. Left in, an
63
+ * escape sequence of the transcript's choosing reaches that terminal and rewrites what the reader is shown: a
64
+ * `\u001b[2K\u001b[1G` in an agent's own prose overwrites the verdict line above it. `hooks/hook-output.ts` keeps
65
+ * the same guard over the one other channel that reaches a terminal, and for the same reason.
66
+ *
67
+ * A space, not nothing, so a sequence never welds the words on either side of it into one.
68
+ */
69
+ const CONTROL = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g;
70
+ function withoutControls(text) {
71
+ return text.replace(CONTROL, ' ');
72
+ }
73
+ function keyContext(text) {
74
+ return [...text.matchAll(KEY_VALUE)].flatMap((match) => {
75
+ const [, prefix = '', name = '', separator = '', value = ''] = match;
76
+ if (value.startsWith(PSEUDONYM))
77
+ return [];
78
+ if (!isSensitiveName(name) || isExcludedValue(value))
79
+ return [];
80
+ // The quotes stay in the text; only what they hold is replaced.
81
+ const bare = unquote(value);
82
+ const start = match.index + prefix.length + name.length + separator.length + (value.length === bare.length ? 0 : 1);
83
+ return [{ name: 'value beside a sensitive name', key: name, rank: RANK.sensitiveName, start, end: start + bare.length }];
84
+ });
85
+ }
86
+ /**
87
+ * Step 5: entropy. Redacts, never reports, and only after its own exclusions (lesson L001). The token may end
88
+ * in base64 padding but may not contain `=` inside it: a token that swallows an `=` swallows a whole
89
+ * `KEY=value` and reports the pair as one anonymous blob.
90
+ */
91
+ const TOKEN = /[A-Za-z0-9_+/~.-]{20,}={0,2}/g;
92
+ function randomTokens(text) {
93
+ return [...text.matchAll(TOKEN)]
94
+ .filter((match) => looksRandom(match[0]))
95
+ .map((match) => ({
96
+ name: 'high-entropy value',
97
+ rank: RANK.entropy,
98
+ start: match.index,
99
+ end: match.index + match[0].length,
100
+ }));
101
+ }
102
+ /**
103
+ * The boundary. Everything that reaches the report model passes through here, on the way **in** - never on the
104
+ * way out to a file, because the next renderer would bypass that (spec §5.4, mechanics rule 5).
105
+ *
106
+ * Four doors, each narrow and each a decision at its call site:
107
+ *
108
+ * - `scan` — free text that may hold anything.
109
+ * - `content` — text that came out of a protected resource: redacted **wholesale**, key names kept.
110
+ * - `path` — a path, shown relative to the project root where it lies under one; `--share` is next.
111
+ * - `term` — a tool name, a status, a pattern: vocabulary, not content.
112
+ *
113
+ * There is no general "trust me" door. A value never appears, a fragment of one never appears (rule 4: a prefix
114
+ * is leakage too), and the same value always gets the same pseudonym so a reader can see it is the same one
115
+ * without being told which (rule 3).
116
+ */
117
+ /**
118
+ * The two key shapes of a protected resource's lines: `KEY=value` and `"key": value`. The first group is the key with
119
+ * what joins it to the value, the second the value. `content` replaces the second and `keysIn` reads the first, from
120
+ * these same expressions. `[ \t]` rather than `\s`: `\s` crosses a newline, so a key with an empty value swallowed
121
+ * the line below it and deleted the next key's name - the one thing this promises to keep.
122
+ */
123
+ const ASSIGNED = /([A-Za-z_][A-Za-z0-9_]*[ \t]*=[ \t]*)("[^"]*"|'[^']*'|\S+)/g;
124
+ const QUOTED_KEY = /("[A-Za-z_][A-Za-z0-9_-]*"[ \t]*:[ \t]*)("[^"]*"|[^,\s]+)/g;
125
+ /**
126
+ * Of matches that overlap, the one of highest precedence - a known format over a password in a URL over a value beside a
127
+ * sensitive name over an entropy guess - and then by position; the result in the order the matches start. The one
128
+ * resolution both the redaction and a count of keys use, so the two can never name the same value differently.
129
+ */
130
+ function strongest(found) {
131
+ const chosen = [];
132
+ for (const match of [...found].sort((a, b) => a.rank - b.rank || a.start - b.start || b.end - a.end)) {
133
+ if (chosen.some((taken) => match.start < taken.end && taken.start < match.end))
134
+ continue;
135
+ chosen.push(match);
136
+ }
137
+ return chosen.sort((a, b) => a.start - b.start);
138
+ }
139
+ export class Redactor {
140
+ #salt;
141
+ #projectRoot;
142
+ /** The shared view of `specs/2026-09-14-path-display-and-share.md`: nothing above the project root leaves the tool. */
143
+ #share;
144
+ /** Hash of a value -> the pseudonym already given to it. The value itself is never kept (rule 6). */
145
+ #pseudonyms = new Map();
146
+ #counts = new Map();
147
+ /** Occurrences, not distinct values: one key seen fifty times is fifty replacements in the output. */
148
+ #replacements = 0;
149
+ #protectedContents = 0;
150
+ constructor(salt, projectRoot = { kind: 'absent' }, share = false) {
151
+ this.#salt = salt;
152
+ this.#projectRoot = projectRoot;
153
+ this.#share = share;
154
+ }
155
+ /**
156
+ * Free text: class A matches are replaced, everything else is left as written - except that a path inside it
157
+ * is shown the way a path in a path field is (R8). A description that quotes an absolute path is still a
158
+ * description carrying an absolute path.
159
+ */
160
+ scan(text) {
161
+ const shown = displayPathsIn(withoutControls(text), this.#projectRoot);
162
+ return brand(this.#scanText(this.#share ? forSharing(shown) : shown));
163
+ }
164
+ /**
165
+ * Text that came out of a protected resource. The scanner recognises the minority of secrets, so nothing here
166
+ * is trusted to be safe, and the decision is made **line by line**: deciding once for the whole text let a
167
+ * single `FOO=bar` anywhere hand the rest of the file through untouched.
168
+ *
169
+ * A line shaped `KEY=value` or `"key": "value"` keeps its key - the key name is the finding - and loses its
170
+ * value. A line of any other shape goes whole. A text where no line has a key shape is replaced by its size,
171
+ * which is all that can be said about it.
172
+ */
173
+ content(text) {
174
+ this.#protectedContents += 1;
175
+ const lines = withoutControls(text).split('\n');
176
+ const redacted = lines.map((line) => this.#contentLine(line));
177
+ if (redacted.every((line, index) => line === lines[index] || line.startsWith('[redacted: line'))) {
178
+ return brand(`[redacted: content of a protected resource — ${lines.length} lines, ${Buffer.byteLength(text)} bytes]`);
179
+ }
180
+ return brand(redacted.join('\n'));
181
+ }
182
+ /**
183
+ * A path is the finding, not the secret, and §7.3 shows it as written. It is still scanned: these come from
184
+ * splitting untrusted result text, so a value that happens to match a path pattern arrives through this door
185
+ * as readily as through any other.
186
+ */
187
+ path(value) {
188
+ const cleaned = withoutControls(value);
189
+ // The shared view drops a location only for a path that says where it starts and is not under the root.
190
+ // A path recorded relative to the working directory is already inside the project, and asking "did the
191
+ // string change" instead of "is it inside" turned every one of those into a category.
192
+ if (this.#share && !insideProject(cleaned, this.#projectRoot))
193
+ return brand(OUTSIDE_THE_PROJECT);
194
+ // What reaches this door as a path is not always one. A command, or a fragment of quoted JSON, can be taken
195
+ // for a candidate and still end in the file a policy protects; it does not start with a root, so it counts as
196
+ // inside the project - and an absolute path or an identifier further along it was shown whole. Found by
197
+ // running `start --share` on a real project. A path field is no weaker than a sentence (R8).
198
+ const shown = displayPath(cleaned, this.#projectRoot);
199
+ return brand(this.#scanText(this.#share ? forSharing(shown) : shown));
200
+ }
201
+ /**
202
+ * A trace of values read from protected resources (`specs/2026-09-15-what-came-back.md` R3), salted with this run's salt so
203
+ * that its digests mean nothing outside this one report (rule 6). The values are hashed here and not kept.
204
+ */
205
+ trace(values) {
206
+ return new ValueTrace(values, (run) => createHash('sha256').update(`${this.#salt}:trace:${run}`).digest('hex'));
207
+ }
208
+ /**
209
+ * Vocabulary: a tool name, an outcome, a policy pattern. No value passes here - but a tool name is still read
210
+ * out of the transcript, so the control characters go, as they do through every other door.
211
+ */
212
+ term(value) {
213
+ return brand(withoutControls(value));
214
+ }
215
+ /**
216
+ * Which known key formats appear in a piece of text, by class name and never by value. This is the `S-shape`
217
+ * source of §5.4: a secret can be recognised in output that touched no protected path at all, and saying
218
+ * "a key of this kind came back here" is a fact worth reporting without quoting a character of it.
219
+ */
220
+ classesIn(text) {
221
+ // Entropy never issues a finding on its own (§5.4): it redacts, and it says nothing. Only what was
222
+ // recognised - a known format, or a value beside a sensitive name - is reported.
223
+ // The report's threshold, not redaction's: a class B match is worth stating only when the key reads as one a
224
+ // configuration would carry. Measured on a real session: 139 matches, of which 129 stood beside names from the
225
+ // repository's own code - `sessionId`, `key`, `secretShapes` - and 10 beside names like `WEBHOOK_SECRET`.
226
+ return [
227
+ ...new Set([...classAMatches(text), ...urlPasswords(text), ...keyContext(text)]
228
+ .filter((match) => match.key === undefined || isConfigName(match.key))
229
+ .map((match) => match.name)),
230
+ ];
231
+ }
232
+ /**
233
+ * The names on the keyed lines of a protected resource - `STRIPE_SECRET_KEY` of `STRIPE_SECRET_KEY=sk_…` - and never
234
+ * their values. They are the same keys `content` keeps, read by the same two expressions, so a name given here is one
235
+ * the redacted text already shows. Each is scanned like any other text, in case a name is itself a key.
236
+ */
237
+ keysIn(text) {
238
+ const names = new Set();
239
+ for (const line of withoutControls(text).split('\n')) {
240
+ if (line.trim() === '' || line.trimStart().startsWith('#'))
241
+ continue;
242
+ for (const match of line.matchAll(ASSIGNED))
243
+ names.add((match[1] ?? '').replace(/[ \t]*=[ \t]*$/, ''));
244
+ for (const match of line.matchAll(QUOTED_KEY))
245
+ names.add((match[1] ?? '').replace(/^"|"[ \t]*:[ \t]*$/g, ''));
246
+ }
247
+ return [...names].filter((name) => name !== '').map((name) => brand(this.#scanText(name)));
248
+ }
249
+ /**
250
+ * The names of the lines whose value is a key - `STRIPE_SECRET_KEY` of `STRIPE_SECRET_KEY=sk_…`, not `NODE_ENV` of
251
+ * `NODE_ENV=development` - each with what its value was recognised as (`specs/2026-09-23-the-report-page.md` M2a):
252
+ * the same resolution `content` redacts by, a known format first and a random value last. A random value is counted
253
+ * here, where §5.4 lets it issue no finding, because it says only which of a protected file's lines hold a key, of a
254
+ * file already on the list. Names only, in the order first read, once each; never a value.
255
+ */
256
+ keyedIn(text) {
257
+ const found = new Map();
258
+ for (const line of withoutControls(text).split('\n')) {
259
+ if (line.trim() === '' || line.trimStart().startsWith('#'))
260
+ continue;
261
+ const secrets = strongest([...classAMatches(line), ...urlPasswords(line), ...keyContext(line), ...randomTokens(line)]
262
+ .filter((match) => match.key === undefined || isConfigName(match.key)));
263
+ if (secrets.length === 0)
264
+ continue;
265
+ for (const [expression, clean] of [[ASSIGNED, /[ \t]*=[ \t]*$/g], [QUOTED_KEY, /^"|"[ \t]*:[ \t]*$/g]]) {
266
+ for (const match of line.matchAll(expression)) {
267
+ const start = match.index + (match[1] ?? '').length;
268
+ const end = start + (match[2] ?? '').length;
269
+ const secret = secrets.find((each) => each.start < end && start < each.end);
270
+ const name = (match[1] ?? '').replace(clean, '');
271
+ if (secret !== undefined && name !== '' && !found.has(name))
272
+ found.set(name, secret.name);
273
+ }
274
+ }
275
+ }
276
+ return [...found].map(([name, key]) => ({ name: brand(this.#scanText(name)), key }));
277
+ }
278
+ /**
279
+ * The key format of each value in a text, one class per value: where two classes match the same value - a Stripe key
280
+ * standing beside `STRIPE_SECRET_KEY=` - the one the redaction itself would choose wins, the known format. What
281
+ * `classesIn` lists is every class seen; this is what each key is (`specs/2026-09-23-the-report-page.md` M1), so a
282
+ * file of Stripe keys is not also said to hold a key nobody can name. In the order the values appear, once each.
283
+ */
284
+ keyClassesIn(text) {
285
+ const found = [...classAMatches(text), ...urlPasswords(text), ...keyContext(text)]
286
+ .filter((match) => match.key === undefined || isConfigName(match.key));
287
+ return [...new Set(strongest(found).map((match) => match.name))];
288
+ }
289
+ /** What the report header has to say about redaction (§5.4, versioning). */
290
+ summary() {
291
+ return {
292
+ rulesetVersion: RULESET_VERSION,
293
+ redactions: this.#replacements,
294
+ distinctValues: this.#pseudonyms.size,
295
+ protectedContents: this.#protectedContents,
296
+ };
297
+ }
298
+ /**
299
+ * One pass over the text, not one pass per pattern. Replacing pattern by pattern lets a later pattern match
300
+ * inside an earlier replacement - `:_authToken=` swallowed the `[redacted: …]` a token pattern had just
301
+ * written, and left half of it in the output. Matches are collected first, overlaps resolved by taking the
302
+ * one that starts earliest and reaches furthest, and the text is rebuilt once.
303
+ */
304
+ /** Blank lines and comments carry nothing; `=` and `:` shapes keep their key; everything else goes whole. */
305
+ #contentLine(line) {
306
+ if (line.trim() === '' || line.trimStart().startsWith('#'))
307
+ return line;
308
+ const keyed = line
309
+ .replace(ASSIGNED, (_, key, value) => `${key}${this.#pseudonym('value from a protected resource', value)}`)
310
+ .replace(QUOTED_KEY, (_, key, value) => `${key}${this.#pseudonym('value from a protected resource', value)}`);
311
+ return keyed === line ? `[redacted: line of a protected resource — ${line.length} characters]` : this.#scanText(keyed);
312
+ }
313
+ #scanText(text) {
314
+ const chosen = strongest([...classAMatches(text), ...urlPasswords(text), ...keyContext(text), ...randomTokens(text)]);
315
+ let output = '';
316
+ let taken = 0;
317
+ for (const match of chosen) {
318
+ output += text.slice(taken, match.start) + this.#pseudonym(match.name, text.slice(match.start, match.end));
319
+ taken = match.end;
320
+ }
321
+ return output + text.slice(taken);
322
+ }
323
+ /**
324
+ * The same value twice is the same pseudonym, so "this key is also in that other file" can be read off the
325
+ * report. A per-run salt makes the hash useless outside this one report (rule 6).
326
+ */
327
+ #pseudonym(className, value) {
328
+ this.#replacements += 1;
329
+ const digest = createHash('sha256').update(`${this.#salt}:${value}`).digest('hex');
330
+ const known = this.#pseudonyms.get(digest);
331
+ if (known !== undefined)
332
+ return known;
333
+ const index = (this.#counts.get(className) ?? 0) + 1;
334
+ this.#counts.set(className, index);
335
+ const pseudonym = `[redacted: ${className} #${index}]`;
336
+ this.#pseudonyms.set(digest, pseudonym);
337
+ return pseudonym;
338
+ }
339
+ }
@@ -0,0 +1,22 @@
1
+ // v1: class A alone, with content from a protected resource redacted wholesale.
2
+ // v2 (2026-09-14): classes B and C, with the exclusion lists of §5.4 that make them usable at all.
3
+ export const RULESET_VERSION = 2;
4
+ export const CLASS_A = [
5
+ { name: 'anthropic-key', pattern: /sk-ant-[A-Za-z0-9_-]{32,}/g },
6
+ { name: 'openai-key', pattern: /sk-(?:proj-)?[A-Za-z0-9]{20,}/g },
7
+ { name: 'github-token', pattern: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}/g },
8
+ { name: 'gitlab-token', pattern: /glpat-[A-Za-z0-9_-]{10,}/g },
9
+ { name: 'slack-token', pattern: /xox[baprs]-[A-Za-z0-9-]{10,}|xapp-[A-Za-z0-9-]{10,}/g },
10
+ { name: 'aws-access-key-id', pattern: /(?:AKIA|ASIA)[A-Z0-9]{16}/g },
11
+ { name: 'google-key', pattern: /AIza[A-Za-z0-9_-]{35}|ya29\.[A-Za-z0-9_-]{10,}/g },
12
+ { name: 'stripe-key', pattern: /(?:sk|rk)_live_[A-Za-z0-9]{10,}|whsec_[A-Za-z0-9]{10,}/g },
13
+ { name: 'supabase-key', pattern: /sbp_[A-Za-z0-9]{10,}/g },
14
+ { name: 'npm-token', pattern: /npm_[A-Za-z0-9]{36}/g },
15
+ { name: 'digitalocean-token', pattern: /do[op]_v1_[a-f0-9]{30,}/g },
16
+ { name: 'sendgrid-key', pattern: /SG\.[A-Za-z0-9_-]{15,}\.[A-Za-z0-9_-]{15,}/g },
17
+ { name: 'private-key', pattern: /-----BEGIN[A-Z ]*PRIVATE KEY-----[\s\S]*?-----END[A-Z ]*PRIVATE KEY-----/g },
18
+ // Three base64url segments, the first of which starts a JSON object: `SUPABASE_SERVICE_ROLE_KEY` is one.
19
+ { name: 'jwt', pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+/g },
20
+ // The GitHub token that sat in git history since 2024 was an `.npmrc` auth token.
21
+ { name: 'npmrc-auth-token', pattern: /(:_authToken=)\S+/g },
22
+ ];