@agentwhy/cli 0.1.0 → 0.2.1

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 (275) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +57 -5
  3. package/dist/adapter/claude-code/contract/assumptions.js +16 -4
  4. package/dist/adapter/claude-code/contract/denials.js +16 -1
  5. package/dist/adapter/claude-code/contract/desktop-sessions.js +28 -0
  6. package/dist/adapter/claude-code/contract/entry-points.js +6 -1
  7. package/dist/adapter/claude-code/contract/fields.js +2 -0
  8. package/dist/adapter/claude-code/contract/hooks.js +8 -0
  9. package/dist/adapter/claude-code/contract/identifiers.js +2 -0
  10. package/dist/adapter/claude-code/contract/layout.js +2 -0
  11. package/dist/adapter/claude-code/contract/line-types.js +2 -0
  12. package/dist/adapter/claude-code/contract/projects.js +2 -0
  13. package/dist/adapter/claude-code/contract/recognition.js +2 -0
  14. package/dist/adapter/claude-code/contract/settings.js +2 -0
  15. package/dist/adapter/claude-code/contract/task-notifications.js +2 -0
  16. package/dist/adapter/claude-code/contract/version.js +14 -1
  17. package/dist/adapter/claude-code/discovery/claude-code-project-catalogue.js +12 -12
  18. package/dist/adapter/claude-code/discovery/claude-code-recognition.js +2 -0
  19. package/dist/adapter/claude-code/discovery/claude-code-session-catalogue.js +28 -7
  20. package/dist/adapter/claude-code/discovery/claude-code-session-discovery.js +2 -0
  21. package/dist/adapter/claude-code/discovery/claude-code-session-titles.js +30 -6
  22. package/dist/adapter/claude-code/discovery/claude-desktop-titles.js +102 -0
  23. package/dist/adapter/claude-code/discovery/session-entries.js +2 -0
  24. package/dist/adapter/claude-code/events/delegation-index.js +2 -0
  25. package/dist/adapter/claude-code/events/transcript-scan.js +6 -5
  26. package/dist/adapter/claude-code/hooks/hook-output.js +2 -0
  27. package/dist/adapter/claude-code/hooks/pre-tool-use-input.js +2 -0
  28. package/dist/adapter/claude-code/hooks/stop-input.js +2 -0
  29. package/dist/adapter/claude-code/hooks/subagent-stop-input.js +4 -0
  30. package/dist/adapter/claude-code/probe/claude-code-probe.js +2 -0
  31. package/dist/adapter/claude-code/probe/collectors/agent-tool-input-key-collector.js +2 -0
  32. package/dist/adapter/claude-code/probe/collectors/denial-kind-collector.js +2 -0
  33. package/dist/adapter/claude-code/probe/collectors/denied-call-collector.js +2 -0
  34. package/dist/adapter/claude-code/probe/collectors/identifier-collector.js +2 -0
  35. package/dist/adapter/claude-code/probe/collectors/line-type-collector.js +2 -0
  36. package/dist/adapter/claude-code/probe/collectors/sidechain-collector.js +2 -0
  37. package/dist/adapter/claude-code/probe/collectors/task-notification-collector.js +2 -0
  38. package/dist/adapter/claude-code/probe/collectors/tool-result-reference-collector.js +2 -0
  39. package/dist/adapter/claude-code/probe/collectors/tool-use-collector.js +2 -0
  40. package/dist/adapter/claude-code/probe/collectors/tool-version-collector.js +2 -0
  41. package/dist/adapter/claude-code/probe/collectors/top-level-key-collector.js +2 -0
  42. package/dist/adapter/claude-code/probe/collectors/unknown-line-type-key-collector.js +2 -0
  43. package/dist/adapter/claude-code/probe/collectors/working-directory-collector.js +2 -0
  44. package/dist/adapter/claude-code/probe/meta-tally.js +2 -0
  45. package/dist/adapter/claude-code/probe/transcript-line.js +2 -0
  46. package/dist/adapter/claude-code/probe/transcript-tally.js +2 -0
  47. package/dist/adapter/claude-code/settings/deny-entries.js +2 -0
  48. package/dist/adapter/claude-code/settings/hook-entries.js +9 -0
  49. package/dist/adapter/claude-code/settings/refuse-project.js +99 -0
  50. package/dist/adapter/codex/contract/actions.js +2 -0
  51. package/dist/adapter/codex/contract/assumptions.js +2 -0
  52. package/dist/adapter/codex/contract/chat-folders.js +19 -0
  53. package/dist/adapter/codex/contract/delegations.js +2 -0
  54. package/dist/adapter/codex/contract/deliveries.js +2 -0
  55. package/dist/adapter/codex/contract/envelope.js +2 -0
  56. package/dist/adapter/codex/contract/hook-refusals.js +2 -0
  57. package/dist/adapter/codex/contract/hooks.js +51 -1
  58. package/dist/adapter/codex/contract/messages.js +2 -0
  59. package/dist/adapter/codex/contract/permissions.js +2 -0
  60. package/dist/adapter/codex/contract/reviews.js +2 -0
  61. package/dist/adapter/codex/contract/session.js +29 -1
  62. package/dist/adapter/codex/contract/turns.js +2 -0
  63. package/dist/adapter/codex/contract/version.js +2 -0
  64. package/dist/adapter/codex/discovery/codex-on-this-computer.js +13 -0
  65. package/dist/adapter/codex/discovery/codex-project-catalogue.js +2 -12
  66. package/dist/adapter/codex/discovery/codex-session-catalogue.js +2 -1
  67. package/dist/adapter/codex/discovery/codex-session-discovery.js +2 -0
  68. package/dist/adapter/codex/discovery/session-owners.js +2 -0
  69. package/dist/adapter/codex/events/action-items.js +2 -0
  70. package/dist/adapter/codex/events/codex-session-source.js +2 -0
  71. package/dist/adapter/codex/events/hook-refusals.js +2 -0
  72. package/dist/adapter/codex/events/rollout-scan.js +1 -1
  73. package/dist/adapter/codex/hooks/codex-conversation.js +54 -0
  74. package/dist/adapter/codex/hooks/pre-tool-use-input.js +2 -0
  75. package/dist/adapter/codex/hooks/reader.js +38 -0
  76. package/dist/adapter/codex/hooks/stop-input.js +32 -0
  77. package/dist/adapter/codex/hooks/stop-refusals.js +3 -6
  78. package/dist/adapter/codex/probe/codex-probe.js +2 -0
  79. package/dist/adapter/codex/settings/codex-check.js +38 -0
  80. package/dist/adapter/codex/settings/codex-hooks.js +168 -1
  81. package/dist/adapter/codex/settings/hook-approval.js +374 -0
  82. package/dist/cli/command-router.js +8 -2
  83. package/dist/cli/commands/check-cli-command.js +2 -0
  84. package/dist/cli/commands/codex-stop-cli-command.js +15 -12
  85. package/dist/cli/commands/doctor-arguments.js +2 -0
  86. package/dist/cli/commands/doctor-usage.js +2 -0
  87. package/dist/cli/commands/init-cli-command.js +10 -5
  88. package/dist/cli/commands/notify-cli-command.js +3 -1
  89. package/dist/cli/commands/refuse-cli-command.js +9 -4
  90. package/dist/cli/commands/report-arguments.js +2 -0
  91. package/dist/cli/commands/report-usage.js +2 -0
  92. package/dist/cli/commands/sessions-cli-command.js +9 -1
  93. package/dist/cli/commands/start-cli-command.js +29 -2
  94. package/dist/cli/commands/watch-cli-command.js +4 -2
  95. package/dist/cli/exit-codes.js +2 -0
  96. package/dist/cli.js +12 -1
  97. package/dist/composition-root.js +101 -16
  98. package/dist/core/access/command-line.js +48 -2
  99. package/dist/core/access/listing.js +2 -0
  100. package/dist/core/access/path-tokens.js +2 -0
  101. package/dist/core/access/protected-access.js +36 -3
  102. package/dist/core/access/protected-values.js +17 -0
  103. package/dist/core/access/search-output.js +2 -0
  104. package/dist/core/access/search-reach.js +2 -0
  105. package/dist/core/combined-project-catalogue.js +7 -1
  106. package/dist/core/correlation/correlate.js +13 -1
  107. package/dist/core/detector/prompt-signals.js +2 -0
  108. package/dist/core/guarded-places.js +42 -0
  109. package/dist/core/nearest-project.js +5 -2
  110. package/dist/core/policy/glob.js +2 -0
  111. package/dist/core/policy/parse-policy.js +2 -0
  112. package/dist/core/policy/policy.js +14 -2
  113. package/dist/core/policy/resolve-policy.js +2 -0
  114. package/dist/core/redaction/entropy.js +2 -0
  115. package/dist/core/redaction/redactor.js +2 -0
  116. package/dist/core/redaction/value-shapes.js +2 -0
  117. package/dist/core/redaction/value-trace.js +2 -0
  118. package/dist/core/session-catalogue.js +7 -0
  119. package/dist/core/session-filter.js +2 -0
  120. package/dist/infrastructure/clack-asker.js +2 -0
  121. package/dist/infrastructure/clack-chooser.js +2 -0
  122. package/dist/infrastructure/clack-multi-chooser.js +2 -0
  123. package/dist/infrastructure/file-alert-store.js +10 -3
  124. package/dist/infrastructure/file-checked-store.js +2 -0
  125. package/dist/infrastructure/file-mark-store.js +2 -0
  126. package/dist/infrastructure/file-onboarding-store.js +2 -0
  127. package/dist/infrastructure/file-page-servers.js +51 -0
  128. package/dist/infrastructure/folder-window.js +2 -0
  129. package/dist/infrastructure/node-agentwhy-invocation.js +2 -0
  130. package/dist/infrastructure/node-background-run.js +45 -0
  131. package/dist/infrastructure/node-browser.js +2 -0
  132. package/dist/infrastructure/node-file-system.js +42 -1
  133. package/dist/infrastructure/node-local-server.js +2 -0
  134. package/dist/infrastructure/node-page-probe.js +40 -0
  135. package/dist/infrastructure/os-notifier.js +2 -0
  136. package/dist/infrastructure/real-directory.js +2 -0
  137. package/dist/infrastructure/shell-command-trial.js +60 -0
  138. package/dist/infrastructure/terminal-banner.js +2 -0
  139. package/dist/ports/command-trial.js +1 -0
  140. package/dist/ports/directory-reader.js +14 -1
  141. package/dist/ports/file-replacer.js +1 -0
  142. package/dist/ports/mark-store.js +2 -0
  143. package/dist/ports/page-server.js +1 -0
  144. package/dist/refuse/command-reach.js +2 -0
  145. package/dist/refuse/command-refusal.js +19 -30
  146. package/dist/refuse/render/refusal-words.js +3 -1
  147. package/dist/report/build-report.js +59 -11
  148. package/dist/report/check/actions-digest.js +3 -0
  149. package/dist/report/check/check-lines.js +2 -1
  150. package/dist/report/check/render/text-digest-renderer.js +31 -9
  151. package/dist/report/check/session-actions.js +4 -6
  152. package/dist/report/check/session-check.js +8 -0
  153. package/dist/report/choose-policy.js +7 -1
  154. package/dist/report/nothing-saved-here.js +16 -0
  155. package/dist/report/private-files/tell-lists.js +2 -0
  156. package/dist/report/project-rules.js +2 -0
  157. package/dist/report/refusals.js +15 -0
  158. package/dist/report/render/home-relative.js +2 -0
  159. package/dist/report/render/html-head.js +2 -0
  160. package/dist/report/render/html-report-styles.js +2 -0
  161. package/dist/report/render/html-sidebar.js +2 -0
  162. package/dist/report/render/path-tail.js +2 -0
  163. package/dist/report/render/report-copy.js +2 -0
  164. package/dist/report/render/report-page/advanced-view.js +2 -0
  165. package/dist/report/render/report-page/files-view.js +2 -0
  166. package/dist/report/render/report-page/files.js +2 -0
  167. package/dist/report/render/report-page/fix-wizard-script.js +2 -0
  168. package/dist/report/render/report-page/helpers-view.js +2 -0
  169. package/dist/report/render/report-page/item-names.js +2 -0
  170. package/dist/report/render/report-page/protect-patterns.js +2 -0
  171. package/dist/report/render/report-page/report-views.js +2 -0
  172. package/dist/report/render/report-page/to-do-view.js +3 -2
  173. package/dist/report/render/text-report-renderer.js +2 -0
  174. package/dist/report/render/ui/add-file-popup.js +2 -0
  175. package/dist/report/render/ui/app-sidebar.js +2 -0
  176. package/dist/report/render/ui/ask-panel.js +2 -0
  177. package/dist/report/render/ui/avatar.js +2 -0
  178. package/dist/report/render/ui/backdrop.js +2 -0
  179. package/dist/report/render/ui/brand-mark.js +2 -0
  180. package/dist/report/render/ui/checklist.js +2 -0
  181. package/dist/report/render/ui/confirm-dialog.js +2 -0
  182. package/dist/report/render/ui/drawer.js +2 -0
  183. package/dist/report/render/ui/file-chip.js +2 -0
  184. package/dist/report/render/ui/fold-line.js +2 -0
  185. package/dist/report/render/ui/guide-card.js +2 -0
  186. package/dist/report/render/ui/live-script.js +2 -0
  187. package/dist/report/render/ui/page-shell.js +2 -0
  188. package/dist/report/render/ui/popup.js +2 -0
  189. package/dist/report/render/ui/progress.js +2 -0
  190. package/dist/report/render/ui/project-list.js +9 -9
  191. package/dist/report/render/ui/stats.js +2 -0
  192. package/dist/report/render/ui/status-icon.js +2 -0
  193. package/dist/report/render/ui/status-look.js +2 -0
  194. package/dist/report/render/ui/task-list.js +2 -0
  195. package/dist/report/render/ui/tokens.js +2 -0
  196. package/dist/report/render/ui/update-notice.js +2 -0
  197. package/dist/report/render/ui/words/app-words.js +3 -3
  198. package/dist/report/render/ui/words/onboarding-words.js +9 -9
  199. package/dist/report/render/ui/words/projects-words.js +3 -3
  200. package/dist/report/render/ui/words/report-words.js +11 -11
  201. package/dist/report/render/ui/words/settings-words.js +30 -6
  202. package/dist/report/rule-names.js +2 -0
  203. package/dist/report/session-report.js +2 -0
  204. package/dist/report/start/conversations/calendar-view.js +2 -0
  205. package/dist/report/start/conversations/conversation-columns.js +2 -0
  206. package/dist/report/start/conversations/empty-week.js +2 -0
  207. package/dist/report/start/conversations/files-window.js +2 -0
  208. package/dist/report/start/conversations/period-section.js +2 -0
  209. package/dist/report/start/conversations/periods-script.js +2 -0
  210. package/dist/report/start/conversations/periods.js +2 -0
  211. package/dist/report/start/conversations/week-view.js +2 -0
  212. package/dist/report/start/conversations/weeks.js +2 -0
  213. package/dist/report/start/detached-start.js +94 -0
  214. package/dist/report/start/month/month-view.js +2 -0
  215. package/dist/report/start/onboarding/done.js +9 -3
  216. package/dist/report/start/onboarding/finish-onboarding.js +6 -4
  217. package/dist/report/start/onboarding/intro.js +2 -0
  218. package/dist/report/start/onboarding/onboarding-renderer.js +4 -4
  219. package/dist/report/start/onboarding/onboarding-script.js +2 -0
  220. package/dist/report/start/onboarding/onboarding-view.js +1 -1
  221. package/dist/report/start/onboarding/project-step.js +1 -1
  222. package/dist/report/start/onboarding/steps.js +2 -0
  223. package/dist/report/start/onboarding/welcome-decision.js +44 -0
  224. package/dist/report/start/onboarding/welcome.js +2 -0
  225. package/dist/report/start/page-notice.js +2 -0
  226. package/dist/report/start/projects/index-projects.js +5 -2
  227. package/dist/report/start/projects/moved-page.js +2 -0
  228. package/dist/report/start/projects/project-switch.js +2 -0
  229. package/dist/report/start/projects/projects-window.js +2 -0
  230. package/dist/report/start/projects/temporary-space.js +2 -0
  231. package/dist/report/start/render/start-words.js +21 -2
  232. package/dist/report/start/repository.js +2 -0
  233. package/dist/report/start/serve/index-handler.js +4 -1
  234. package/dist/report/start/serve/settings-setup.js +10 -0
  235. package/dist/report/start/session-start.js +80 -55
  236. package/dist/report/start/settings/alerts-tab.js +4 -1
  237. package/dist/report/start/settings/files-tab.js +18 -12
  238. package/dist/report/start/settings/general-tab.js +33 -1
  239. package/dist/report/start/settings/settings-renderer.js +2 -0
  240. package/dist/report/start/settings/settings-script.js +8 -1
  241. package/dist/report/start/settings/settings-view.js +29 -2
  242. package/dist/report/start/settings/settings-windows.js +16 -5
  243. package/dist/report/start/settings-files.js +2 -0
  244. package/dist/report/start/to-fix/done-list.js +2 -0
  245. package/dist/report/start/to-fix/file-window.js +2 -0
  246. package/dist/report/start/to-fix/to-fix-list.js +2 -0
  247. package/dist/report/start/to-fix/to-fix-renderer.js +2 -0
  248. package/dist/report/start/to-fix/to-fix-script.js +2 -0
  249. package/dist/report/watch/agent-alert.js +3 -0
  250. package/dist/report/watch/codex-turn-format.js +69 -0
  251. package/dist/report/watch/notice-choices.js +2 -0
  252. package/dist/report/watch/notice-settings.js +2 -0
  253. package/dist/report/watch/preferences.js +2 -0
  254. package/dist/report/watch/render/codex-request-words.js +106 -0
  255. package/dist/report/watch/render/instruction-words.js +114 -12
  256. package/dist/report/watch/render/notice-words.js +74 -4
  257. package/dist/report/watch/subagent-watch.js +270 -45
  258. package/dist/setup/behind.js +2 -0
  259. package/dist/setup/codex-mirror.js +311 -104
  260. package/dist/setup/not-a-project.js +2 -0
  261. package/dist/setup/project-setup.js +2 -0
  262. package/dist/shared/compare.js +2 -0
  263. package/dist/shared/content-version.js +2 -0
  264. package/dist/shared/counter.js +2 -0
  265. package/dist/shared/file-stamp.js +2 -0
  266. package/dist/shared/label.js +2 -0
  267. package/dist/shared/package-name.js +2 -0
  268. package/dist/shared/plain-invocation.js +2 -0
  269. package/dist/shared/printable.js +2 -0
  270. package/dist/shared/sentence.js +2 -0
  271. package/dist/shared/terminal-logo.js +2 -0
  272. package/dist/shared/text-table.js +2 -0
  273. package/dist/shared/wrap.js +2 -0
  274. package/package.json +2 -1
  275. package/dist/refuse/render/codex-stop-words.js +0 -21
@@ -1,20 +1,29 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { join } from 'node:path';
2
4
  import { SETTINGS_FILES } from '../adapter/claude-code/contract/settings.js';
3
- import { installedHooks, rulesReadByHooks, runningInvocation } from '../adapter/claude-code/settings/hook-entries.js';
4
- import { PROJECT_HOOKS } from '../adapter/codex/contract/hooks.js';
5
- import { codexRefuseCommand, codexRefuseIn, codexStopCommand, codexStopIn, withCodexRefuse, withoutCodexRefuse } from '../adapter/codex/settings/codex-hooks.js';
5
+ import { installedHooks } from '../adapter/claude-code/settings/hook-entries.js';
6
+ import { PROJECT_HOOKS, USER_HOOKS } from '../adapter/codex/contract/hooks.js';
7
+ import { agentwhyCodexEntries, approvedCodexEntries, codexRefuseCommand, codexRefuseIn, codexStopCommand, withOwnPair, withoutOwnEntries } from '../adapter/codex/settings/codex-hooks.js';
8
+ import { approvalKey, entryHash, tomlDefinesHooks, withOwnApprovals, withoutOwnApprovals } from '../adapter/codex/settings/hook-approval.js';
6
9
  import { FileAccessError } from '../ports/file-access-error.js';
7
10
  import { parseJsonObject } from '../shared/json.js';
8
- /** Where Codex's hook file is, as a person reads it. */
9
- const WHERE = `${PROJECT_HOOKS.directory}/${PROJECT_HOOKS.file}`;
10
- /** CK8: what holds until a person approves the hook in Codex, measured (CKB5) - said wherever the hook is written. */
11
- const UNTIL_APPROVED = 'Codex runs a new or changed hook only after you approve it in Codex: open Codex in this project and approve agentwhy\'s hook when it asks. Until then Codex skips it without a word, and does not block anything.';
11
+ import { isPlainInvocation } from '../shared/plain-invocation.js';
12
+ /** The person's two Codex files and the project's old one, as they are written about. */
13
+ const WHERE_USER = `~/${USER_HOOKS.directory}/${USER_HOOKS.file}`;
14
+ const WHERE_CONFIG = `~/${USER_HOOKS.directory}/${USER_HOOKS.config}`;
15
+ const WHERE_PROJECT = `${PROJECT_HOOKS.directory}/${PROJECT_HOOKS.file}`;
16
+ /** What holds once agentwhy has approved its own check (AO1, AO2) - said wherever it is written. */
17
+ const FROM_THE_FIRST_MESSAGE = 'Codex blocks the same files from the first message, in the terminal and in VS Code, with nothing to approve in Codex. The check acts only in projects whose rules block files.';
18
+ /** AOB9's cost, said in the plan the person consents to (AO8). */
19
+ const COST = 'It runs before every Codex shell command and at the end of every reply on this computer: about a tenth of a second where agentwhy is run directly, a quarter through npx.';
12
20
  /**
13
- * `codex-blocks-too` CK5-CK9: after the setup it wraps has written Claude Code's settings - or found nothing to change -
14
- * Codex's hook file is made to match them. agentwhy's Codex hook runs `refuse` with the invocation and the rules Claude
15
- * Code's `refuse` has, where either settings file runs `refuse`, and is not there where neither does. Only in a project
16
- * that uses Codex (CK6); with its own part of the plan and the same consent (CK7); and never approved on anyone's
17
- * behalf (CK8).
21
+ * `2026-10-02-codex-approves-its-own-hook.md` AO1-AO11, AO14-AO16: after the setup it wraps has written Claude Code's
22
+ * settings - or found nothing to change - the person's own Codex files are made to match them. Where `refuse` runs in
23
+ * this project, `~/.codex/hooks.json` holds exactly one agentwhy pair, tried first as Codex will run it, and
24
+ * `~/.codex/config.toml` approves those two entries and nothing else - never a folder. The project's old copy comes
25
+ * out. Only on a computer with `~/.codex`; with its own part of the plan and the same consent (AO8). A project's
26
+ * remove keeps the computer-wide check; `--remove --codex` takes it out (AO7).
18
27
  */
19
28
  export class CodexMirror {
20
29
  #dependencies;
@@ -22,9 +31,9 @@ export class CodexMirror {
22
31
  this.#dependencies = dependencies;
23
32
  }
24
33
  async run(options) {
25
- // `init --codex` with nothing else named: Codex's hook alone, to match what Claude Code's settings already run.
26
- if (options.codex === true && onlyCodex(options))
27
- return (await this.#mirror(options)) ?? { outcome: 'unchanged', output: nothingFor(await this.#running()) };
34
+ // `init --codex` with nothing else named: Codex's part alone. A remove keeps its usual course first (AO7's "too").
35
+ if (options.codex === true && !options.remove && nothingElse(options))
36
+ return (await this.#mirror(options)) ?? { outcome: 'unchanged', output: await this.#nothingFor() };
28
37
  const result = await this.#dependencies.setup.run(options);
29
38
  if (result.outcome !== 'written' && result.outcome !== 'unchanged')
30
39
  return result;
@@ -34,101 +43,262 @@ export class CodexMirror {
34
43
  const outcome = result.outcome === 'unchanged' && codex.outcome === 'written' ? 'written' : result.outcome;
35
44
  return { outcome, output: `${result.output}${result.output.endsWith('\n') || result.output === '' ? '' : '\n'}\n${codex.output}` };
36
45
  }
37
- /** The change to Codex's hook file, written with consent; `undefined` where there is none to make. */
46
+ /** The change to the person's Codex files and the project's old one; `undefined` where there is none to make. */
38
47
  async #mirror(options) {
39
- const running = await this.#running();
40
- const directory = join(this.#dependencies.workingDirectory, PROJECT_HOOKS.directory);
41
- const path = join(directory, PROJECT_HOOKS.file);
42
- const read = await this.#readJson(path);
43
- const current = read.kind === 'object' ? codexRefuseIn(read.file) : undefined;
44
- const currentStop = read.kind === 'object' ? codexStopIn(read.file) : undefined;
45
- if (running.kind === 'elsewhere') {
46
- return current === undefined && !(await this.#usesCodex(options, read)) ? undefined : { outcome: 'refused', output: `Codex: ${ELSEWHERE}\n` };
47
- }
48
- const invoke = running.kind === 'none' ? undefined : options.invoke ?? running.invoke ?? (await this.#dependencies.invocation.find());
49
- const wanted = invoke === undefined ? undefined : codexRefuseCommand(invoke, running.kind === 'refuse' ? running.settings : undefined);
50
- const stop = invoke === undefined ? undefined : codexStopCommand(invoke);
51
- if (wanted === current && stop === currentStop)
52
- return undefined;
53
- if (wanted !== undefined && !(await this.#usesCodex(options, read)))
54
- return undefined;
55
- if (read.kind === 'broken' || read.kind === 'unreadable') {
56
- return {
57
- outcome: 'refused',
58
- output: `Codex: ${WHERE} ${read.kind === 'broken' ? 'is not a JSON object' : 'could not be read'}, so agentwhy's hook was not ${wanted === undefined ? 'taken out of it' : 'written to it'}.\n`,
59
- };
60
- }
61
- const before = read.kind === 'object' ? read.file : {};
62
- const next = wanted === undefined || stop === undefined ? withoutCodexRefuse(before).file : withCodexRefuse(before, wanted, stop);
48
+ if (!(await this.#dependencies.codexOnThisComputer())) {
49
+ // AO8: nothing is written and the folder is never created. Said only where Codex was asked for by name.
50
+ return options.codex === true ? refused(`there is no ~/${USER_HOOKS.directory} folder, so Codex is not used on this computer and nothing was written for it.`) : undefined;
51
+ }
52
+ const read = await this.#read();
53
+ if (typeof read === 'string')
54
+ return refused(read);
55
+ const projectCleaned = read.project.kind === 'object' ? withoutOwnEntries(read.project.file) : undefined;
56
+ const migrate = projectCleaned !== undefined && projectCleaned.removed > 0 ? projectCleaned.file : undefined;
57
+ if (options.remove && options.codex === true)
58
+ return this.#removal(options, read, migrate);
59
+ if (!(await this.#refuseRunsHere())) {
60
+ // Nothing for Codex to follow here. The computer-wide check stays for the other projects (AOD4): said on a remove.
61
+ const userFile = read.user.kind === 'object' ? read.user.file : {};
62
+ const stays = options.remove && codexRefuseIn(userFile) !== undefined
63
+ ? `Codex: agentwhy's check in ${WHERE_USER} stays - other projects may block files with it, and it does nothing where none do. To take it out of Codex everywhere: agentwhy init --remove --codex`
64
+ : '';
65
+ if (migrate === undefined)
66
+ return stays === '' ? undefined : { outcome: 'unchanged', output: `${stays}\n` };
67
+ return this.#write(options, read, { project: migrate }, [`agentwhy's old entries come out of ${WHERE_PROJECT} in this project.`], [
68
+ `Codex: agentwhy's old entries are out of ${WHERE_PROJECT}.`,
69
+ ...(stays === '' ? [] : [stays]),
70
+ ]);
71
+ }
72
+ return this.#install(options, read, migrate);
73
+ }
74
+ /** AO1-AO4, AO9, AO14: one approved pair in the person's file, tried first; the project's copy out. */
75
+ async #install(options, read, migrate) {
76
+ if (read.user.kind === 'broken' || read.user.kind === 'unreadable') {
77
+ return refused(`${WHERE_USER} ${read.user.kind === 'broken' ? 'is not a JSON object' : 'could not be read'}, so agentwhy's check was not written to it.`);
78
+ }
79
+ // AO16: Codex would load hooks from both files, and warn in every session.
80
+ if (read.user.kind === 'absent' && tomlDefinesHooks(read.config)) {
81
+ return refused(`${WHERE_CONFIG} defines Codex's hooks itself, so agentwhy did not create ${WHERE_USER} beside it - Codex would load both and warn about it. Adding agentwhy's check to ${WHERE_CONFIG} is not built.`);
82
+ }
83
+ const userFile = read.user.kind === 'object' ? read.user.file : {};
84
+ const invoke = await this.#invocationFor(options, userFile);
85
+ const refuse = codexRefuseCommand(invoke, undefined);
86
+ const stop = codexStopCommand(invoke);
87
+ const paired = withOwnPair(userFile, refuse, stop);
88
+ const hooksChange = JSON.stringify(paired.file) !== JSON.stringify(userFile);
89
+ const wanted = this.#approvalsOf(paired.file, read.userPath);
90
+ if (wanted === undefined)
91
+ return refused(`agentwhy's entries in ${WHERE_USER} hold something its approval was not measured over, so nothing was approved.`);
92
+ const edit = (fresh) => withOwnApprovals(fresh, read.userPath, wanted);
93
+ const first = edit(read.config);
94
+ if (first.kind === 'unsafe')
95
+ return refused(`${WHERE_CONFIG}: ${first.reason}, so agentwhy wrote nothing there - a line added beside it could stop Codex from starting.`);
96
+ const configChange = first.text !== read.config;
97
+ if (!hooksChange && !configChange) {
98
+ if (migrate === undefined)
99
+ return undefined;
100
+ return this.#write(options, read, { project: migrate }, [
101
+ `And in Codex, agentwhy's old entries come out of ${WHERE_PROJECT} in this project: your own ${WHERE_USER} already runs the check, approved. Everything else in the file stays.`,
102
+ ], [`Codex: agentwhy's old entries are out of ${WHERE_PROJECT}.`]);
103
+ }
104
+ // AO14: a new or changed command is tried before anything is written.
105
+ if (hooksChange) {
106
+ const failed = await this.#tryBoth(refuse, stop);
107
+ if (failed !== undefined)
108
+ return refused(`${failed} so nothing was written - every Codex command on this computer would have shown an error. Fix it, or name how to run agentwhy with --command, and run this again.`);
109
+ }
63
110
  const plan = [
64
- `And in Codex, agentwhy will change ${WHERE}:`,
111
+ 'And in Codex, agentwhy will change your own Codex files, outside every project:',
112
+ '',
113
+ ` In ${WHERE_USER}`,
114
+ ` ${'before a shell command'.padEnd(24)}${refuse}`,
115
+ ` ${'when a reply ends'.padEnd(24)}${stop}`,
116
+ ` In ${WHERE_CONFIG}`,
117
+ ` approve those two entries - agentwhy's own and nothing else, and no folder is trusted`,
118
+ ...(migrate === undefined ? [] : ['', ` agentwhy's old entries come out of ${WHERE_PROJECT} in this project: the check above replaces them.`]),
119
+ ...(paired.stuck === 0 ? [] : ['', ` ${paired.stuck} more agentwhy ${paired.stuck === 1 ? 'entry shares' : 'entries share'} a group with another tool's in ${WHERE_USER} and ${paired.stuck === 1 ? 'stays' : 'stay'}: taking ${paired.stuck === 1 ? 'it' : 'them'} out would un-approve that tool. Take ${paired.stuck === 1 ? 'it' : 'them'} out by hand.`]),
65
120
  '',
66
- ...(wanted === undefined
67
- ? [' Stop running', ' agentwhy refuse and its Codex conversation message']
68
- : [' Run automatically', ` ${'before a shell command'.padEnd(24)}${wanted}`, ` ${'when a reply ends'.padEnd(24)}${stop}`]),
121
+ ` ${FROM_THE_FIRST_MESSAGE}`,
122
+ ` ${COST}`,
69
123
  '',
70
- ...(wanted === undefined ? [] : [` ${UNTIL_APPROVED}`, '']),
71
- ' Everything else in the file stays. It is rewritten as two-space JSON.',
124
+ ' Everything else in these files stays. The hooks file is rewritten as two-space JSON.',
72
125
  ];
126
+ const userLine = read.user.kind === 'absent' || codexRefuseIn(userFile) === undefined
127
+ ? `Codex: agentwhy's check is in ${WHERE_USER}, approved in ${WHERE_CONFIG} - its own entries and nothing else. ${FROM_THE_FIRST_MESSAGE}`
128
+ : hooksChange
129
+ ? `Codex: agentwhy's check in ${WHERE_USER} changed, and agentwhy approved it again (${WHERE_CONFIG}).`
130
+ : configChange
131
+ ? `Codex: agentwhy's approval in ${WHERE_CONFIG} was brought up to date with ${WHERE_USER}.`
132
+ : undefined;
133
+ return this.#write(options, read, {
134
+ ...(configChange ? { config: { before: read.config, edit } } : {}),
135
+ ...(hooksChange ? { hooks: paired.file } : {}),
136
+ ...(migrate === undefined ? {} : { project: migrate }),
137
+ }, plan, [
138
+ ...(userLine === undefined ? [] : [userLine]),
139
+ ...(migrate === undefined ? [] : [`Codex: agentwhy's old entries are out of ${WHERE_PROJECT}.`]),
140
+ ]);
141
+ }
142
+ /** AO7: `--remove --codex` - agentwhy's entries and their approvals out of the person's two files, and the project's copy. */
143
+ async #removal(options, read, migrate) {
144
+ if (read.user.kind === 'broken' || read.user.kind === 'unreadable') {
145
+ return refused(`${WHERE_USER} ${read.user.kind === 'broken' ? 'is not a JSON object' : 'could not be read'}, so agentwhy's check was not taken out of it.`);
146
+ }
147
+ const userFile = read.user.kind === 'object' ? read.user.file : {};
148
+ const placed = agentwhyCodexEntries(userFile);
149
+ const keys = new Set(placed.map((entry) => approvalKey(read.userPath, entry)));
150
+ const hashes = new Set(placed.map((entry) => entryHash(entry)).filter((hash) => hash !== undefined));
151
+ const cleaned = withoutOwnEntries(userFile);
152
+ const edit = (fresh) => withoutOwnApprovals(fresh, read.userPath, keys, hashes);
153
+ const first = edit(read.config);
154
+ if (first.kind === 'unsafe')
155
+ return refused(`${WHERE_CONFIG}: ${first.reason}, so agentwhy took nothing out - take agentwhy's approval out of it by hand.`);
156
+ const configChange = first.text !== read.config;
157
+ if (cleaned.removed === 0 && !configChange && migrate === undefined) {
158
+ return { outcome: 'unchanged', output: `Codex: agentwhy's check is not in ${WHERE_USER}, so there was nothing to take out.\n` };
159
+ }
160
+ const plan = [
161
+ 'And in Codex, agentwhy will change your own Codex files, outside every project:',
162
+ '',
163
+ ' Stop running',
164
+ ` agentwhy's check before every shell command, and its Codex reply message`,
165
+ ` Take its approvals out of ${WHERE_CONFIG}`,
166
+ ...(migrate === undefined ? [] : [` Take its old entries out of ${WHERE_PROJECT} in this project`]),
167
+ '',
168
+ ' Codex then no longer blocks anything for agentwhy, in any project on this computer.',
169
+ '',
170
+ ' Everything else in these files stays. The hooks file is rewritten as two-space JSON.',
171
+ ];
172
+ return this.#write(options, read, {
173
+ ...(configChange ? { config: { before: read.config, edit } } : {}),
174
+ ...(cleaned.removed > 0 ? { hooks: cleaned.file } : {}),
175
+ ...(migrate === undefined ? {} : { project: migrate }),
176
+ }, plan, [
177
+ `Codex: agentwhy's check and its approvals are out of ${WHERE_USER} and ${WHERE_CONFIG}. Codex no longer blocks anything for agentwhy.`,
178
+ ...(cleaned.stuck === 0 ? [] : [`Codex: ${cleaned.stuck} agentwhy ${cleaned.stuck === 1 ? 'entry shares' : 'entries share'} a group with another tool's in ${WHERE_USER} and stayed, so as not to un-approve that tool. Take ${cleaned.stuck === 1 ? 'it' : 'them'} out by hand.`]),
179
+ ], 'removal');
180
+ }
181
+ /**
182
+ * R7 and AO8, then every write in one order, so a failed one never leaves Codex worse off than it found it: on an
183
+ * install the approvals go first, so Codex never sees an entry of agentwhy's unapproved; on a removal the entries go
184
+ * first, for the same reason read backwards - approvals taken out from under entries that stay would make Codex ask
185
+ * about agentwhy's check in every session. Each half says what holds when the other one has already been written.
186
+ */
187
+ async #write(options, read, writes, plan, done, direction = 'install') {
73
188
  const consent = await this.#consent(options, plan);
74
189
  if (typeof consent !== 'string')
75
190
  return consent;
76
191
  const said = consent === 'asked' ? '' : `${plan.join('\n')}\n\n`;
77
- try {
78
- await this.#dependencies.files.ensureDirectory(directory);
79
- await this.#dependencies.files.writeText(path, `${JSON.stringify(next, null, 2)}\n`);
80
- }
81
- catch (error) {
82
- if (!(error instanceof FileAccessError))
83
- throw error;
84
- return { outcome: 'unwritable', output: `${said}Codex: ${WHERE} could not be written, so Codex does not block anything yet.\n` };
85
- }
86
- // CK8: said once. Where the plan was printed above - `--yes`, a page - it already said what Codex needs.
87
- const until = said === '' ? ` ${UNTIL_APPROVED}` : '';
88
- return {
89
- outcome: 'written',
90
- output: `${said}${wanted === undefined
91
- ? `Codex: agentwhy's hook is out of ${WHERE}.\n`
92
- : current === undefined
93
- ? `Codex: agentwhy's hook is in ${WHERE}. It blocks the same files there, once approved.${until}\n`
94
- : `Codex: agentwhy's hook in ${WHERE} changed, so Codex asks you to approve it again.${until}\n`}`,
192
+ const removing = direction === 'removal';
193
+ const config = async () => {
194
+ if (writes.config === undefined)
195
+ return undefined;
196
+ // What the other half has already done by the time this one runs, and so cannot be said to be untouched.
197
+ const andHooks = removing && writes.hooks !== undefined ? `agentwhy's check is out of ${WHERE_USER}, but ` : '';
198
+ const nothing = andHooks === '' ? 'nothing was written' : 'its approvals there could not be taken out - they name entries that are gone, so take them out by hand';
199
+ // AO11: Codex writes this file too, at the start of a conversation (AOB3). Read again; recomputed once if it moved.
200
+ const fresh = await this.#readText(read.configPath);
201
+ if (fresh === undefined)
202
+ return { outcome: 'unwritable', output: `${said}Codex: ${andHooks}${WHERE_CONFIG} could not be read again, so ${nothing}.\n` };
203
+ const edit = fresh === writes.config.before ? writes.config.edit(writes.config.before) : writes.config.edit(fresh);
204
+ if (edit.kind === 'unsafe')
205
+ return { outcome: 'refused', output: `${said}Codex: ${andHooks}${WHERE_CONFIG}: ${edit.reason}, so ${nothing}.\n` };
206
+ if (edit.text !== fresh && !(await this.#replace(read.configPath, edit.text))) {
207
+ return {
208
+ outcome: 'unwritable',
209
+ output: `${said}Codex: ${andHooks}${WHERE_CONFIG} could not be written, so ${andHooks === '' ? 'nothing was changed for Codex' : nothing}.\n`,
210
+ };
211
+ }
212
+ return undefined;
95
213
  };
214
+ const hooks = async () => {
215
+ if (writes.hooks === undefined)
216
+ return undefined;
217
+ if (await this.#replace(read.userPath, `${JSON.stringify(writes.hooks, null, 2)}\n`))
218
+ return undefined;
219
+ // On a removal this half runs first, so nothing else has been touched and the check stands as it did.
220
+ const after = removing
221
+ ? `Nothing was taken out: its approvals in ${WHERE_CONFIG} stay, so Codex still runs the approved check.`
222
+ : `Its approval may be in ${WHERE_CONFIG} already; the next run puts both right.`;
223
+ return { outcome: 'unwritable', output: `${said}Codex: ${WHERE_USER} could not be written. ${after}\n` };
224
+ };
225
+ for (const step of removing ? [hooks, config] : [config, hooks]) {
226
+ const failed = await step();
227
+ if (failed !== undefined)
228
+ return failed;
229
+ }
230
+ if (writes.project !== undefined && !(await this.#replace(read.projectPath, `${JSON.stringify(writes.project, null, 2)}\n`))) {
231
+ return { outcome: 'unwritable', output: `${said}Codex: ${WHERE_PROJECT} could not be written, so agentwhy's old entries are still in it.\n` };
232
+ }
233
+ return { outcome: 'written', output: `${said}${done.join('\n')}\n` };
96
234
  }
97
235
  /**
98
- * How Claude Code's `refuse` runs here, read from the files as they are after the setup: the local file first, as
99
- * `init` reads them. Its rules become Codex's (CK2), as a path in the project (CK3).
236
+ * AO9: a new check runs the agentwhy doing this setup - `--command` where given. One already in the person's file
237
+ * is kept as it is unless `--command` names one or the run is an update, so projects set up by different releases
238
+ * do not rewrite each other's check on every write; `init --update` anywhere re-pins it (U7). Kept only where it
239
+ * names agentwhy in plain words (J4).
100
240
  */
101
- async #running() {
241
+ async #invocationFor(options, userFile) {
242
+ if (options.invoke !== undefined)
243
+ return options.invoke;
244
+ const mine = await this.#dependencies.invocation.find();
245
+ const existing = codexRefuseIn(userFile)?.replace(/\s+refuse\s+--codex(?:\s.*)?$/, '');
246
+ // AO9, J4: `codexRefuseIn` knows agentwhy's entry by the words `refuse --codex` anywhere in its command, so
247
+ // whatever stands before them was taken as the invocation - run by #tryBoth through a login shell before AO8's
248
+ // consent, then written as agentwhy's own entry and approved, which opens Codex's own gate (CKB5) from outside.
249
+ // The same test reads Claude Code's settings (`hook-entries.ts`); `--command` above is the person's own word.
250
+ if (existing !== undefined && !isPlainInvocation(existing))
251
+ return mine;
252
+ return existing === undefined || options.update === true ? mine : existing;
253
+ }
254
+ /** The approvals of agentwhy's pair as this file holds it: the first entry of each event, as `withOwnPair` keeps it. */
255
+ #approvalsOf(file, userPath) {
256
+ const wanted = [];
257
+ for (const entry of approvedCodexEntries(file)) {
258
+ const hash = entryHash(entry);
259
+ if (hash === undefined)
260
+ return undefined;
261
+ wanted.push({ key: approvalKey(userPath, entry), hash });
262
+ }
263
+ return wanted;
264
+ }
265
+ /** AO14: both commands, run once as Codex will, from a folder no project is above. The first failure, said; else none. */
266
+ async #tryBoth(refuse, stop) {
267
+ const trial = this.#dependencies.trial;
268
+ if (trial === undefined)
269
+ return 'agentwhy could not try the check before writing it,';
270
+ const folder = this.#dependencies.trialFolder;
271
+ const timeoutMs = USER_HOOKS.timeoutSeconds * 1000;
272
+ const inputs = [
273
+ [refuse, JSON.stringify({ cwd: folder, hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'true' } })],
274
+ [stop, JSON.stringify({ cwd: folder, hook_event_name: 'Stop', stop_hook_active: false })],
275
+ ];
276
+ for (const [commandLine, input] of inputs) {
277
+ const outcome = await trial.run({ commandLine, input, folder, timeoutMs });
278
+ const failure = trialFailure(commandLine, outcome);
279
+ if (failure !== undefined)
280
+ return failure;
281
+ }
282
+ return undefined;
283
+ }
284
+ /** Whether Claude Code's `refuse` runs in this project after the setup: the local file first, as `init` reads them. */
285
+ async #refuseRunsHere() {
102
286
  const directory = join(this.#dependencies.workingDirectory, SETTINGS_FILES.directory);
103
287
  for (const file of [SETTINGS_FILES.local, SETTINGS_FILES.shared]) {
104
288
  const read = await this.#readJson(join(directory, file));
105
- if (read.kind !== 'object' || !installedHooks(read.file).has('refuse'))
106
- continue;
107
- const rules = rulesReadByHooks(read.file).get('refuse') ?? 'default';
108
- const invoke = runningInvocation(read.file);
109
- if (rules === 'policy' || rules === 'other')
110
- return { kind: 'elsewhere' };
111
- return {
112
- kind: 'refuse',
113
- ...(invoke === undefined ? {} : { invoke }),
114
- ...(rules === 'default' ? {} : { settings: `${SETTINGS_FILES.directory}/${SETTINGS_FILES[rules]}` }),
115
- };
116
- }
117
- return { kind: 'none' };
118
- }
119
- /** CK6: a Codex conversation listed for the project, a `.codex` folder in it, or `init --codex`. */
120
- async #usesCodex(options, read) {
121
- if (options.codex === true || read.kind !== 'absent')
122
- return true;
123
- try {
124
- if ((await this.#dependencies.files.kindOf(join(this.#dependencies.workingDirectory, PROJECT_HOOKS.directory))) === 'directory')
289
+ if (read.kind === 'object' && installedHooks(read.file).has('refuse'))
125
290
  return true;
126
291
  }
127
- catch (error) {
128
- if (!(error instanceof FileAccessError))
129
- throw error;
130
- }
131
- return this.#dependencies.codexConversations();
292
+ return false;
293
+ }
294
+ async #read() {
295
+ const userPath = join(this.#dependencies.home, USER_HOOKS.directory, USER_HOOKS.file);
296
+ const configPath = join(this.#dependencies.home, USER_HOOKS.directory, USER_HOOKS.config);
297
+ const projectPath = join(this.#dependencies.workingDirectory, PROJECT_HOOKS.directory, PROJECT_HOOKS.file);
298
+ const config = await this.#readText(configPath);
299
+ if (config === undefined)
300
+ return `${WHERE_CONFIG} could not be read, so agentwhy did not touch Codex's approvals.`;
301
+ return { userPath, configPath, projectPath, user: await this.#readJson(userPath), config, project: await this.#readJson(projectPath) };
132
302
  }
133
303
  /** R7, as `ProjectSetup` asks it: `--yes`, a person at a terminal, or the plan printed and nothing written. */
134
304
  async #consent(options, plan) {
@@ -138,10 +308,28 @@ export class CodexMirror {
138
308
  return { outcome: 'not-confirmed', output: `${plan.join('\n')}\n\nNothing was written for Codex. To write it: the same command with --yes\n` };
139
309
  }
140
310
  const chosen = await this.#dependencies.chooser.choose(`${plan.join('\n')}\n\nWrite it?`, [
141
- { label: 'Yes', detail: `write ${WHERE}` },
311
+ { label: 'Yes', detail: "write agentwhy's Codex files" },
142
312
  { label: 'No', detail: 'leave Codex as it is' },
143
313
  ]);
144
- return chosen === 0 ? 'asked' : { outcome: 'declined', output: `Nothing was written for Codex.\n` };
314
+ return chosen === 0 ? 'asked' : { outcome: 'declined', output: 'Nothing was written for Codex.\n' };
315
+ }
316
+ /** What `init --codex` alone says where everything already matches, or there is nothing for it to follow. */
317
+ async #nothingFor() {
318
+ if (!(await this.#refuseRunsHere())) {
319
+ return "Codex: nothing to write. agentwhy blocks Codex where refuse runs, and refuse doesn't run here yet: agentwhy init --refuse --codex\n";
320
+ }
321
+ return `Codex: ${WHERE_USER} already runs agentwhy's check, approved. ${FROM_THE_FIRST_MESSAGE}\n`;
322
+ }
323
+ async #replace(path, text) {
324
+ try {
325
+ await this.#dependencies.files.replaceText(path, text);
326
+ return true;
327
+ }
328
+ catch (error) {
329
+ if (!(error instanceof FileAccessError))
330
+ throw error;
331
+ return false;
332
+ }
145
333
  }
146
334
  async #readJson(path) {
147
335
  let text;
@@ -156,16 +344,35 @@ export class CodexMirror {
156
344
  const file = parseJsonObject(text);
157
345
  return file === undefined ? { kind: 'broken' } : { kind: 'object', file };
158
346
  }
347
+ /** A file's text: the empty string where there is none yet, `undefined` where there is one and it cannot be read. */
348
+ async #readText(path) {
349
+ try {
350
+ return await this.#dependencies.files.readText(path);
351
+ }
352
+ catch (error) {
353
+ if (!(error instanceof FileAccessError))
354
+ throw error;
355
+ return error.failure === 'not-found' ? '' : undefined;
356
+ }
357
+ }
358
+ }
359
+ /** A run that wrote nothing, and why, said as `Codex: …`. */
360
+ function refused(why) {
361
+ return { outcome: 'refused', output: `Codex: ${why}\n` };
159
362
  }
160
- /** CK2: rules Codex's hook cannot be pointed at from the project - a policy file, or a settings file of another place. */
161
- const ELSEWHERE = "agentwhy's hook was not written: refuse here reads a policy or a settings file outside the project's two, which a hook in the project cannot be pointed at. Run agentwhy init --refuse to point it at this project's rules.";
162
- /** `init --codex` and nothing else: no hook named, nothing to protect or take out, no update. */
163
- function onlyCodex(options) {
164
- return options.hooks === undefined && options.protect.length === 0 && !options.remove && (options.unprotect ?? []).length === 0 && options.update !== true;
363
+ /** AO14: what a trial's failure is said as, or `undefined` where the command ran and exited 0. */
364
+ function trialFailure(commandLine, outcome) {
365
+ if (outcome.kind === 'ran' && outcome.exitCode === 0)
366
+ return undefined;
367
+ if (outcome.kind === 'ran')
368
+ return `"${commandLine}" exited with code ${outcome.exitCode}${outcome.stderr === '' ? '' : ` (${outcome.stderr})`} when tried as Codex will run it,`;
369
+ if (outcome.kind === 'timed-out')
370
+ return `"${commandLine}" did not finish within ${USER_HOOKS.timeoutSeconds} seconds when tried as Codex will run it,`;
371
+ if (outcome.kind === 'not-started')
372
+ return `"${commandLine}" could not be started (${outcome.reason}),`;
373
+ return 'agentwhy cannot try the check on this system the way Codex runs it, which was measured on macOS and Linux only,';
165
374
  }
166
- /** What `init --codex` says where Codex's hook already matches, or there is nothing for it to follow. */
167
- function nothingFor(running) {
168
- return running.kind === 'none'
169
- ? "Codex: nothing to write. agentwhy blocks Codex with the same hook it blocks Claude Code with, and refuse doesn't run here yet: agentwhy init --refuse --codex\n"
170
- : `Codex: ${WHERE} already runs agentwhy's hook. ${UNTIL_APPROVED}\n`;
375
+ /** `--codex`, with or without `--remove`, and nothing else: no hook named, nothing to protect or take out, no update. */
376
+ function nothingElse(options) {
377
+ return options.hooks === undefined && options.protect.length === 0 && (options.unprotect ?? []).length === 0 && options.update !== true;
171
378
  }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { parse, relative, resolve } from 'node:path';
2
4
  /**
3
5
  * Why a working directory is not a project, or `undefined` where it may be one (`.ai/specs/2026-09-27-which-project.md`
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { join } from 'node:path';
2
4
  import { SETTINGS_FILES } from '../adapter/claude-code/contract/settings.js';
3
5
  import { denyEntriesFor, fileRulesIn, isDenied, withDenyEntries, withoutDenyEntries } from '../adapter/claude-code/settings/deny-entries.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /** Orders strings by UTF-16 code units — the order `Array.prototype.sort` uses without a comparator. */
2
4
  export function byString(a, b) {
3
5
  return a < b ? -1 : a > b ? 1 : 0;
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * A short name for a content, so that two contents can be told apart without keeping either (`live-pages` L1). FNV-1a,
3
5
  * 32 bits, in base 36: it guards nothing - a page only asks "is my file still what I was sent?" - and a collision costs
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { byString } from './compare.js';
2
4
  /** Counts occurrences per key and reports them sorted by key, so the same input always yields the same output. */
3
5
  export class Counter {
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /** A moment as part of a file name: it sorts by time, and holds no character a file system refuses. */
2
4
  export function fileStamp(now) {
3
5
  return new Date(now).toISOString().replace(/[:.]/g, '-');
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  // Labels come only from structural positions: keys, line types, tool names, enumerations. A label must look
2
4
  // like an identifier, and anything else is replaced, so free text cannot reach the output even from a
3
5
  // malformed transcript. Known limit: an identifier-shaped secret placed in a structural position would pass;
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * The name agentwhy is published under. npm refuses the unscoped `agentwhy` as too close to `agent-why`, so the package
3
5
  * is scoped while the command it installs stays `agentwhy`. Written into a hook by the finder and read back from one by
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { PACKAGE } from './package-name.js';
2
4
  /**
3
5
  * What separates two words: a space or a tab, the only characters a shell splits on. Found by a review: `\s` also takes
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Text from a file as a terminal may be shown it: every character a terminal obeys rather than draws, as a space. A
3
5
  * settings file, a transcript and a folder's name all arrive from outside this tool - a committed settings file and a
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * The first sentence of a piece of writing, capped (`specs/2026-09-15-why-this-call.md` §5.1, level B). Measured on one
3
5
  * session: 311 of 315 first sentences run under 200 characters, so the cap is rarely reached - and when
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { paint } from './colour.js';
2
4
  /**
3
5
  * The agentwhy wordmark for a terminal: the letters of `agentwhy-logo-transparent.svg` in two rows of block
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { byString } from './compare.js';
2
4
  const EMPTY_COUNTS = '(none)';
3
5
  /** Rows with their labels aligned into one column. */
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Breaks text into lines that fit a width, at spaces where it can and inside a long run where it must - a URL
3
5
  * or a path is never silently truncated, because a report that hides half a finding is worse than a wide line.
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@agentwhy/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Reconstructs from transcripts why an AI agent reached for protected data",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
7
+ "author": "Nessprim Karol Kozer",
7
8
  "repository": {
8
9
  "type": "git",
9
10
  "url": "git+https://github.com/karolkozer/agentwhy.git"
@@ -1,21 +0,0 @@
1
- /**
2
- * A continuation request, so the refusal is said in the conversation by Codex, in the user's language (CK13). Codex's
3
- * VS Code chat folds the hook's own words into the turn's collapsed steps, so the reply is the one place a person sees
4
- * agentwhy there: it is asked to lead with agentwhy's name. The person reads this request too - the VS Code chat shows
5
- * it as "Hook feedback" and the terminal as "Blocked by hook" (CKB14) - so it is three short sentences in the page's
6
- * words (guidelines §7): what happened, what to say, and what not to do. It gives no text to copy.
7
- */
8
- export function codexStopInstruction(refusals) {
9
- const paths = [...new Set(refusals.map((refusal) => refusal.path))];
10
- const named = paths.slice(0, 3).map((path) => JSON.stringify(path));
11
- if (paths.length > 3)
12
- named.push(`${paths.length - 3} more`);
13
- const listed = named.length === 1 ? named[0] : `${named.slice(0, -1).join(', ')} and ${named.at(-1)}`;
14
- const one = refusals.length === 1;
15
- const [files, them] = paths.length === 1 ? ['file', 'it'] : ['files', 'them'];
16
- return [
17
- `agentwhy stopped ${one ? 'a command' : 'commands'} that would have read the private ${files} ${listed}, so ${one ? 'it' : 'they'} did not run.`,
18
- `Say so to the user in one short message in their language, starting with **agentwhy**, and add that they can open the ${files} in their editor.`,
19
- `Don't read ${them} another way or ask for what's in ${them}.`,
20
- ].join(' ');
21
- }