@agentwhy/cli 0.0.0-dev → 0.2.0

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 (273) 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/commands/check-cli-command.js +2 -0
  83. package/dist/cli/commands/codex-stop-cli-command.js +15 -12
  84. package/dist/cli/commands/doctor-arguments.js +2 -0
  85. package/dist/cli/commands/doctor-usage.js +2 -0
  86. package/dist/cli/commands/init-cli-command.js +10 -5
  87. package/dist/cli/commands/notify-cli-command.js +3 -1
  88. package/dist/cli/commands/refuse-cli-command.js +9 -4
  89. package/dist/cli/commands/report-arguments.js +2 -0
  90. package/dist/cli/commands/report-usage.js +2 -0
  91. package/dist/cli/commands/sessions-cli-command.js +9 -1
  92. package/dist/cli/commands/start-cli-command.js +29 -2
  93. package/dist/cli/commands/watch-cli-command.js +4 -2
  94. package/dist/cli/exit-codes.js +2 -0
  95. package/dist/cli.js +10 -0
  96. package/dist/composition-root.js +80 -15
  97. package/dist/core/access/command-line.js +48 -2
  98. package/dist/core/access/listing.js +2 -0
  99. package/dist/core/access/path-tokens.js +2 -0
  100. package/dist/core/access/protected-access.js +36 -3
  101. package/dist/core/access/protected-values.js +17 -0
  102. package/dist/core/access/search-output.js +2 -0
  103. package/dist/core/access/search-reach.js +2 -0
  104. package/dist/core/combined-project-catalogue.js +7 -1
  105. package/dist/core/correlation/correlate.js +13 -1
  106. package/dist/core/detector/prompt-signals.js +2 -0
  107. package/dist/core/guarded-places.js +42 -0
  108. package/dist/core/nearest-project.js +5 -2
  109. package/dist/core/policy/glob.js +2 -0
  110. package/dist/core/policy/parse-policy.js +2 -0
  111. package/dist/core/policy/policy.js +14 -2
  112. package/dist/core/policy/resolve-policy.js +2 -0
  113. package/dist/core/redaction/entropy.js +2 -0
  114. package/dist/core/redaction/redactor.js +2 -0
  115. package/dist/core/redaction/value-shapes.js +2 -0
  116. package/dist/core/redaction/value-trace.js +2 -0
  117. package/dist/core/session-catalogue.js +7 -0
  118. package/dist/core/session-filter.js +2 -0
  119. package/dist/infrastructure/clack-asker.js +2 -0
  120. package/dist/infrastructure/clack-chooser.js +2 -0
  121. package/dist/infrastructure/clack-multi-chooser.js +2 -0
  122. package/dist/infrastructure/file-alert-store.js +10 -3
  123. package/dist/infrastructure/file-checked-store.js +2 -0
  124. package/dist/infrastructure/file-mark-store.js +2 -0
  125. package/dist/infrastructure/file-onboarding-store.js +2 -0
  126. package/dist/infrastructure/file-page-servers.js +51 -0
  127. package/dist/infrastructure/folder-window.js +2 -0
  128. package/dist/infrastructure/node-agentwhy-invocation.js +2 -0
  129. package/dist/infrastructure/node-background-run.js +45 -0
  130. package/dist/infrastructure/node-browser.js +2 -0
  131. package/dist/infrastructure/node-file-system.js +42 -1
  132. package/dist/infrastructure/node-local-server.js +2 -0
  133. package/dist/infrastructure/node-page-probe.js +40 -0
  134. package/dist/infrastructure/os-notifier.js +2 -0
  135. package/dist/infrastructure/real-directory.js +2 -0
  136. package/dist/infrastructure/shell-command-trial.js +60 -0
  137. package/dist/infrastructure/terminal-banner.js +2 -0
  138. package/dist/ports/command-trial.js +1 -0
  139. package/dist/ports/directory-reader.js +14 -1
  140. package/dist/ports/file-replacer.js +1 -0
  141. package/dist/ports/mark-store.js +2 -0
  142. package/dist/ports/page-server.js +1 -0
  143. package/dist/refuse/command-reach.js +2 -0
  144. package/dist/refuse/command-refusal.js +19 -30
  145. package/dist/refuse/render/refusal-words.js +3 -1
  146. package/dist/report/build-report.js +59 -11
  147. package/dist/report/check/actions-digest.js +3 -0
  148. package/dist/report/check/check-lines.js +2 -1
  149. package/dist/report/check/render/text-digest-renderer.js +31 -9
  150. package/dist/report/check/session-actions.js +4 -6
  151. package/dist/report/check/session-check.js +8 -0
  152. package/dist/report/choose-policy.js +7 -1
  153. package/dist/report/nothing-saved-here.js +16 -0
  154. package/dist/report/private-files/tell-lists.js +2 -0
  155. package/dist/report/project-rules.js +2 -0
  156. package/dist/report/refusals.js +15 -0
  157. package/dist/report/render/home-relative.js +2 -0
  158. package/dist/report/render/html-head.js +2 -0
  159. package/dist/report/render/html-report-styles.js +2 -0
  160. package/dist/report/render/html-sidebar.js +2 -0
  161. package/dist/report/render/path-tail.js +2 -0
  162. package/dist/report/render/report-copy.js +2 -0
  163. package/dist/report/render/report-page/advanced-view.js +2 -0
  164. package/dist/report/render/report-page/files-view.js +2 -0
  165. package/dist/report/render/report-page/files.js +2 -0
  166. package/dist/report/render/report-page/fix-wizard-script.js +2 -0
  167. package/dist/report/render/report-page/helpers-view.js +2 -0
  168. package/dist/report/render/report-page/item-names.js +2 -0
  169. package/dist/report/render/report-page/protect-patterns.js +2 -0
  170. package/dist/report/render/report-page/report-views.js +2 -0
  171. package/dist/report/render/report-page/to-do-view.js +3 -2
  172. package/dist/report/render/text-report-renderer.js +2 -0
  173. package/dist/report/render/ui/add-file-popup.js +2 -0
  174. package/dist/report/render/ui/app-sidebar.js +2 -0
  175. package/dist/report/render/ui/ask-panel.js +2 -0
  176. package/dist/report/render/ui/avatar.js +2 -0
  177. package/dist/report/render/ui/backdrop.js +2 -0
  178. package/dist/report/render/ui/brand-mark.js +2 -0
  179. package/dist/report/render/ui/checklist.js +2 -0
  180. package/dist/report/render/ui/confirm-dialog.js +2 -0
  181. package/dist/report/render/ui/drawer.js +2 -0
  182. package/dist/report/render/ui/file-chip.js +2 -0
  183. package/dist/report/render/ui/fold-line.js +2 -0
  184. package/dist/report/render/ui/guide-card.js +2 -0
  185. package/dist/report/render/ui/live-script.js +2 -0
  186. package/dist/report/render/ui/page-shell.js +2 -0
  187. package/dist/report/render/ui/popup.js +2 -0
  188. package/dist/report/render/ui/progress.js +2 -0
  189. package/dist/report/render/ui/project-list.js +9 -9
  190. package/dist/report/render/ui/stats.js +2 -0
  191. package/dist/report/render/ui/status-icon.js +2 -0
  192. package/dist/report/render/ui/status-look.js +2 -0
  193. package/dist/report/render/ui/task-list.js +2 -0
  194. package/dist/report/render/ui/tokens.js +2 -0
  195. package/dist/report/render/ui/update-notice.js +2 -0
  196. package/dist/report/render/ui/words/app-words.js +3 -3
  197. package/dist/report/render/ui/words/onboarding-words.js +9 -9
  198. package/dist/report/render/ui/words/projects-words.js +3 -3
  199. package/dist/report/render/ui/words/report-words.js +11 -11
  200. package/dist/report/render/ui/words/settings-words.js +30 -6
  201. package/dist/report/rule-names.js +2 -0
  202. package/dist/report/session-report.js +2 -0
  203. package/dist/report/start/conversations/calendar-view.js +2 -0
  204. package/dist/report/start/conversations/conversation-columns.js +2 -0
  205. package/dist/report/start/conversations/empty-week.js +2 -0
  206. package/dist/report/start/conversations/files-window.js +2 -0
  207. package/dist/report/start/conversations/period-section.js +2 -0
  208. package/dist/report/start/conversations/periods-script.js +2 -0
  209. package/dist/report/start/conversations/periods.js +2 -0
  210. package/dist/report/start/conversations/week-view.js +2 -0
  211. package/dist/report/start/conversations/weeks.js +2 -0
  212. package/dist/report/start/detached-start.js +75 -0
  213. package/dist/report/start/month/month-view.js +2 -0
  214. package/dist/report/start/onboarding/done.js +9 -3
  215. package/dist/report/start/onboarding/finish-onboarding.js +6 -4
  216. package/dist/report/start/onboarding/intro.js +2 -0
  217. package/dist/report/start/onboarding/onboarding-renderer.js +4 -4
  218. package/dist/report/start/onboarding/onboarding-script.js +2 -0
  219. package/dist/report/start/onboarding/onboarding-view.js +1 -1
  220. package/dist/report/start/onboarding/project-step.js +1 -1
  221. package/dist/report/start/onboarding/steps.js +2 -0
  222. package/dist/report/start/onboarding/welcome.js +2 -0
  223. package/dist/report/start/page-notice.js +2 -0
  224. package/dist/report/start/projects/index-projects.js +5 -2
  225. package/dist/report/start/projects/moved-page.js +2 -0
  226. package/dist/report/start/projects/project-switch.js +2 -0
  227. package/dist/report/start/projects/projects-window.js +2 -0
  228. package/dist/report/start/projects/temporary-space.js +2 -0
  229. package/dist/report/start/render/start-words.js +15 -1
  230. package/dist/report/start/repository.js +2 -0
  231. package/dist/report/start/serve/index-handler.js +4 -1
  232. package/dist/report/start/serve/settings-setup.js +10 -0
  233. package/dist/report/start/session-start.js +69 -29
  234. package/dist/report/start/settings/alerts-tab.js +4 -1
  235. package/dist/report/start/settings/files-tab.js +18 -12
  236. package/dist/report/start/settings/general-tab.js +33 -1
  237. package/dist/report/start/settings/settings-renderer.js +2 -0
  238. package/dist/report/start/settings/settings-script.js +8 -1
  239. package/dist/report/start/settings/settings-view.js +29 -2
  240. package/dist/report/start/settings/settings-windows.js +16 -5
  241. package/dist/report/start/settings-files.js +2 -0
  242. package/dist/report/start/to-fix/done-list.js +2 -0
  243. package/dist/report/start/to-fix/file-window.js +2 -0
  244. package/dist/report/start/to-fix/to-fix-list.js +2 -0
  245. package/dist/report/start/to-fix/to-fix-renderer.js +2 -0
  246. package/dist/report/start/to-fix/to-fix-script.js +2 -0
  247. package/dist/report/watch/agent-alert.js +3 -0
  248. package/dist/report/watch/codex-turn-format.js +69 -0
  249. package/dist/report/watch/notice-choices.js +2 -0
  250. package/dist/report/watch/notice-settings.js +2 -0
  251. package/dist/report/watch/preferences.js +2 -0
  252. package/dist/report/watch/render/codex-request-words.js +106 -0
  253. package/dist/report/watch/render/instruction-words.js +114 -12
  254. package/dist/report/watch/render/notice-words.js +74 -4
  255. package/dist/report/watch/subagent-watch.js +270 -45
  256. package/dist/setup/behind.js +2 -0
  257. package/dist/setup/codex-mirror.js +311 -104
  258. package/dist/setup/not-a-project.js +2 -0
  259. package/dist/setup/project-setup.js +2 -0
  260. package/dist/shared/compare.js +2 -0
  261. package/dist/shared/content-version.js +2 -0
  262. package/dist/shared/counter.js +2 -0
  263. package/dist/shared/file-stamp.js +2 -0
  264. package/dist/shared/label.js +2 -0
  265. package/dist/shared/package-name.js +2 -0
  266. package/dist/shared/plain-invocation.js +2 -0
  267. package/dist/shared/printable.js +2 -0
  268. package/dist/shared/sentence.js +2 -0
  269. package/dist/shared/terminal-logo.js +2 -0
  270. package/dist/shared/text-table.js +2 -0
  271. package/dist/shared/wrap.js +2 -0
  272. package/package.json +2 -1
  273. package/dist/refuse/render/codex-stop-words.js +0 -21
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { createHash } from 'node:crypto';
2
4
  import { displayPath, displayPathsIn, insideProject, forSharing, OUTSIDE_THE_PROJECT, } from '../project-root.js';
3
5
  import { looksRandom } from './entropy.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * The exclusions of spec §5.4 — "the real work here". Without them class B is a noise generator, because a
3
5
  * sensitive name sits beside a harmless value far more often than beside a secret: `SECRET_ENABLED=true`,
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Values read from protected resources, held as salted digests of their runs and asked about other text
3
5
  * (`specs/2026-09-15-what-came-back.md` R3, R4). No value is kept and no run of one (§5.4 rule 6): what is kept is the digest
@@ -1,3 +1,10 @@
1
+ /**
2
+ * Whether no AI's store of conversations was there at all, where this listing looked: then "not used here yet" is not
3
+ * the likely reason, and running somewhere conversations are not saved is (R28, amended 2026-10-01).
4
+ */
5
+ export function noStoreAnywhere(listing) {
6
+ return listing.searched.length > 0 && listing.searched.every((place) => place.store === 'missing');
7
+ }
1
8
  /**
2
9
  * What names a session wherever two AIs' sessions meet - a row, a file, a cached report, `report --input`: its id where
3
10
  * Claude Code wrote it, as it always was, and the id after its AI's name otherwise, so equal ids never overwrite each
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  const HOUR = 3_600_000;
2
4
  const DAY = 24 * HOUR;
3
5
  const UNIT = { h: HOUR, d: DAY, w: 7 * DAY };
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { isCancel, text } from '@clack/prompts';
2
4
  /** One line of text, through `@clack/prompts`. A blank answer comes back as an empty string, never as a cancellation. */
3
5
  export class ClackAsker {
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { autocomplete, isCancel } from '@clack/prompts';
2
4
  /** Rows shown at once. The rest are reached by moving, or found by typing. */
3
5
  const VISIBLE = 10;
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { isCancel, multiselect } from '@clack/prompts';
2
4
  /**
3
5
  * A list with boxes, through `@clack/prompts` - the package the session chooser already uses, so no new dependency
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { lstat, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
2
4
  import { join } from 'node:path';
3
5
  /** One directory of its own, so pruning never looks at a file this tool did not write. */
@@ -70,6 +72,9 @@ export class FileAlertStore {
70
72
  }
71
73
  return remembered;
72
74
  }
75
+ peek(sessionId) {
76
+ return this.#read(sessionId);
77
+ }
73
78
  /** A session id is not a file name until this makes it one: no separator, no `..`, no length worth worrying about. */
74
79
  #fileFor(sessionId) {
75
80
  const safe = sessionId.replace(UNSAFE, '-').slice(0, 80);
@@ -109,12 +114,14 @@ function isRemembered(value) {
109
114
  return false;
110
115
  const record = value;
111
116
  return typeof record.agentId === 'string' && typeof record.level === 'string' && typeof record.words === 'string' &&
112
- (record.counts === undefined || areCounts(record.counts));
117
+ (record.counts === undefined || areCounts(record.counts)) && (record.notByRule === undefined || record.notByRule === true) &&
118
+ (record.told === undefined || record.told === true) && (record.data === undefined || record.data === true);
113
119
  }
114
- /** Counts that are not two whole numbers are counts this did not write: the record is read as one without them. */
120
+ /** Counts that are not whole numbers are counts this did not write: the record is read as one without them. */
115
121
  function areCounts(value) {
116
122
  if (value === null || typeof value !== 'object')
117
123
  return false;
118
124
  const counts = value;
119
- return Number.isInteger(counts.values) && Number.isInteger(counts.reached);
125
+ return Number.isInteger(counts.values) && Number.isInteger(counts.reached) &&
126
+ [counts.told, counts.data].every((count) => count === undefined || Number.isInteger(count));
120
127
  }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
4
  import { dirname } from 'node:path';
3
5
  /** A session id as Claude Code names a transcript: nothing that could be a path or carry anything else. */
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
4
  import { dirname } from 'node:path';
3
5
  import { isMarkResult } from '../ports/mark-store.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { appendFile, mkdir, readFile } from 'node:fs/promises';
2
4
  import { dirname } from 'node:path';
3
5
  /**
@@ -0,0 +1,51 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ /** Anything but these is not a name this store wrote: a project's key is turned into one before it is used. */
6
+ const UNSAFE = /[^A-Za-z0-9._-]/g;
7
+ /**
8
+ * The running page server of each project (`2026-10-02-a-page-not-a-file.md` PF2), in the person's own agentwhy
9
+ * folder: one small file a project, readable by its owner alone, since the address holds the page's token. Never
10
+ * throws: a record that cannot be read or written is no record, and a new server is started instead.
11
+ */
12
+ export class FilePageServers {
13
+ #directory;
14
+ constructor(agentwhyDirectory) {
15
+ this.#directory = join(agentwhyDirectory, 'servers');
16
+ }
17
+ async read(project) {
18
+ try {
19
+ const parsed = JSON.parse(await readFile(this.#fileFor(project), 'utf8'));
20
+ return isRecord(parsed) ? parsed : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ async write(project, record) {
27
+ try {
28
+ await mkdir(this.#directory, { recursive: true, mode: 0o700 });
29
+ await writeFile(this.#fileFor(project), JSON.stringify(record), { encoding: 'utf8', mode: 0o600 });
30
+ }
31
+ catch {
32
+ // No record is a server that is not reused: the next open starts its own.
33
+ }
34
+ }
35
+ async remove(project, pid) {
36
+ const known = await this.read(project);
37
+ if (known?.pid !== pid)
38
+ return;
39
+ await rm(this.#fileFor(project), { force: true }).catch(() => undefined);
40
+ }
41
+ #fileFor(project) {
42
+ const safe = project.replace(UNSAFE, '-').slice(0, 200);
43
+ return join(this.#directory, `${safe === '' ? 'project' : safe}.json`);
44
+ }
45
+ }
46
+ function isRecord(value) {
47
+ if (value === null || typeof value !== 'object')
48
+ return false;
49
+ const record = value;
50
+ return typeof record.url === 'string' && Number.isInteger(record.pid) && typeof record.startedAt === 'number';
51
+ }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { execFile } from 'node:child_process';
2
4
  import { realpath } from 'node:fs/promises';
3
5
  /**
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { constants } from 'node:fs';
2
4
  import { access, readFile, realpath, stat } from 'node:fs/promises';
3
5
  import { delimiter, dirname, join, sep } from 'node:path';
@@ -0,0 +1,45 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { spawn } from 'node:child_process';
4
+ /**
5
+ * agentwhy started again as a process of its own (`2026-10-02-a-page-not-a-file.md` PF3): detached, its streams closed,
6
+ * not waited for - so the command that started it returns at once, and the page server it runs outlives it. Measured on
7
+ * 2026-10-02 (PFB1): such a process outlived its command in Claude Code in VS Code and in the desktop app, and in Codex
8
+ * when the command ran outside its sandbox.
9
+ */
10
+ export class NodeBackgroundRun {
11
+ #node;
12
+ #script;
13
+ /**
14
+ * The processes this started that have ended, as Node says when it reaps one: an ended child's id may be given to
15
+ * another process, so asking the system by id would not tell them apart.
16
+ */
17
+ #ended = new Set();
18
+ constructor(node, script) {
19
+ this.#node = node;
20
+ this.#script = script;
21
+ }
22
+ start(args) {
23
+ if (this.#script === undefined)
24
+ return Promise.resolve(undefined);
25
+ return new Promise((resolve) => {
26
+ try {
27
+ const child = spawn(this.#node, [this.#script, ...args], { detached: true, stdio: 'ignore' });
28
+ child.once('error', () => resolve(undefined));
29
+ child.once('spawn', () => {
30
+ const { pid } = child;
31
+ if (pid !== undefined)
32
+ child.once('exit', () => this.#ended.add(pid));
33
+ child.unref();
34
+ resolve(pid);
35
+ });
36
+ }
37
+ catch {
38
+ resolve(undefined);
39
+ }
40
+ });
41
+ }
42
+ running(pid) {
43
+ return !this.#ended.has(pid);
44
+ }
45
+ }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { spawn } from 'node:child_process';
2
4
  /**
3
5
  * What each platform calls to open a file with whatever is registered for it. The empty argument after `start`
@@ -1,5 +1,9 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { randomBytes } from 'node:crypto';
1
4
  import { createReadStream } from 'node:fs';
2
- import { mkdir, open, readdir, readFile, stat, writeFile } from 'node:fs/promises';
5
+ import { chmod, lstat, mkdir, open, readdir, readFile, readlink, realpath, rename, stat, unlink, writeFile } from 'node:fs/promises';
6
+ import { basename, dirname, join, resolve } from 'node:path';
3
7
  import { FileAccessError } from '../ports/file-access-error.js';
4
8
  /**
5
9
  * The owner alone, for everything this writes. What it writes is a report: which protected files an agent
@@ -16,6 +20,26 @@ export class NodeFileSystem {
16
20
  async writeText(path, text) {
17
21
  await attempt(path, () => writeFile(path, text, { mode: OWNER_ONLY_FILE }));
18
22
  }
23
+ /**
24
+ * AO15: the text goes to a temporary file beside the real target - a link followed, so a dotfiles link stays a link -
25
+ * with the target's mode, and then takes its place in one rename. A failure removes the temporary file and leaves the
26
+ * target as it was.
27
+ */
28
+ async replaceText(path, text) {
29
+ const target = await attempt(path, () => realTarget(path));
30
+ const mode = await stat(target).then((found) => found.mode & 0o777, () => OWNER_ONLY_FILE);
31
+ const temporary = join(dirname(target), `.${basename(target)}.agentwhy-${randomBytes(6).toString('hex')}.tmp`);
32
+ try {
33
+ await attempt(path, () => writeFile(temporary, text, { mode, flag: 'wx' }));
34
+ // The mode a file is created with is cut by the process's umask; the target's own is set again before the rename.
35
+ await attempt(path, () => chmod(temporary, mode));
36
+ await attempt(path, () => rename(temporary, target));
37
+ }
38
+ catch (error) {
39
+ await unlink(temporary).catch(() => undefined);
40
+ throw error;
41
+ }
42
+ }
19
43
  async ensureDirectory(path) {
20
44
  await attempt(path, () => mkdir(path, { recursive: true, mode: OWNER_ONLY_DIRECTORY }));
21
45
  }
@@ -88,6 +112,23 @@ function entryKind(entry) {
88
112
  return 'directory';
89
113
  return 'other';
90
114
  }
115
+ /**
116
+ * The file a path names, every link followed; a file not there yet is named by its folder's real path. A link whose
117
+ * target is not there yet is followed to that target, so the write creates it and the link stays a link.
118
+ */
119
+ async function realTarget(path) {
120
+ try {
121
+ return await realpath(path);
122
+ }
123
+ catch (error) {
124
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
125
+ throw error;
126
+ const linked = await lstat(path).then((found) => found.isSymbolicLink(), () => false);
127
+ if (linked)
128
+ return realTarget(resolve(dirname(path), await readlink(path)));
129
+ return join(await realpath(dirname(path)), basename(path));
130
+ }
131
+ }
91
132
  async function attempt(path, operation) {
92
133
  try {
93
134
  return await operation();
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { createServer } from 'node:http';
2
4
  const LOOPBACK = '127.0.0.1';
3
5
  /**
@@ -0,0 +1,40 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { request } from 'node:http';
4
+ /** The computer's own addresses: a record that named anything else was not written by `start`, and is not asked. */
5
+ const LOOPBACK = new Set(['127.0.0.1', 'localhost', '[::1]']);
6
+ /**
7
+ * Asks a page once (`2026-10-02-a-page-not-a-file.md` PF3): an answer of 200 is a server that is running and this page
8
+ * is one it serves. Asking also brings a served page up to date (R75), so a conversation begun since the server started
9
+ * has its report by the time it is opened - and that takes as long as reading it does. Found by review: 3 s took a
10
+ * running server busy with a long conversation for a dead one, and started a second beside it.
11
+ */
12
+ export class NodePageProbe {
13
+ #timeoutMs;
14
+ constructor(timeoutMs = 10_000) {
15
+ this.#timeoutMs = timeoutMs;
16
+ }
17
+ answers(url) {
18
+ let parsed;
19
+ try {
20
+ parsed = new URL(url);
21
+ }
22
+ catch {
23
+ return Promise.resolve(false);
24
+ }
25
+ if (parsed.protocol !== 'http:' || !LOOPBACK.has(parsed.hostname))
26
+ return Promise.resolve(false);
27
+ return new Promise((resolve) => {
28
+ const asking = request(parsed, { method: 'GET', timeout: this.#timeoutMs }, (response) => {
29
+ response.resume();
30
+ resolve(response.statusCode === 200);
31
+ });
32
+ asking.on('error', () => resolve(false));
33
+ asking.on('timeout', () => {
34
+ asking.destroy();
35
+ resolve(false);
36
+ });
37
+ asking.end();
38
+ });
39
+ }
40
+ }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { execFile } from 'node:child_process';
2
4
  /**
3
5
  * A system notification (`specs/2026-09-16-when-an-agent-finishes.md` R15). On macOS through `osascript`, which showed
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { realpathSync } from 'node:fs';
2
4
  /**
3
5
  * A directory as the system names it, every link followed: what a shell's working directory is given as, where `HOME`
@@ -0,0 +1,60 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { spawn } from 'node:child_process';
4
+ /** How much of standard error is kept to say why a trial failed: its first line, cut short. */
5
+ const STDERR_KEPT = 300;
6
+ /**
7
+ * `2026-10-02-codex-approves-its-own-hook.md` AO14, AOB8: a command line run as Codex runs a hook on macOS and Linux -
8
+ * `$SHELL -lc "<command>"`, else `/bin/sh` - with its input on standard input, from the given folder. On Windows
9
+ * Codex's way is not measured, so nothing is tried. The environment is this process's, not Codex's: what passes here
10
+ * is what can be promised, and no more. A timeout ends the shell; a process it started may outlive it briefly, which
11
+ * a trial accepts - the answer is already "timed out".
12
+ */
13
+ export class ShellCommandTrial {
14
+ #options;
15
+ constructor(options) {
16
+ this.#options = options;
17
+ }
18
+ run(trial) {
19
+ if (this.#options.platform === 'win32')
20
+ return Promise.resolve({ kind: 'not-tried' });
21
+ const shell = this.#options.shell === undefined || this.#options.shell === '' ? '/bin/sh' : this.#options.shell;
22
+ return new Promise((resolve) => {
23
+ let settled = false;
24
+ let timer;
25
+ const done = (outcome) => {
26
+ if (settled)
27
+ return;
28
+ settled = true;
29
+ if (timer !== undefined)
30
+ clearTimeout(timer);
31
+ resolve(outcome);
32
+ };
33
+ const child = spawn(shell, ['-lc', trial.commandLine], {
34
+ cwd: trial.folder,
35
+ env: { ...this.#options.environment },
36
+ stdio: ['pipe', 'ignore', 'pipe'],
37
+ });
38
+ let stderr = '';
39
+ child.stderr?.on('data', (chunk) => {
40
+ if (stderr.length < STDERR_KEPT)
41
+ stderr += chunk.toString('utf8');
42
+ });
43
+ timer = setTimeout(() => {
44
+ if (settled)
45
+ return;
46
+ child.kill('SIGKILL');
47
+ done({ kind: 'timed-out' });
48
+ }, trial.timeoutMs);
49
+ child.on('error', (error) => done({ kind: 'not-started', reason: error.message }));
50
+ child.on('close', (code, signal) => {
51
+ if (code === null)
52
+ return done({ kind: 'not-started', reason: `ended by ${signal ?? 'a signal'}` });
53
+ done({ kind: 'ran', exitCode: code, stderr: (stderr.split('\n')[0] ?? '').slice(0, STDERR_KEPT) });
54
+ });
55
+ // A command that exits before reading leaves a closed pipe; what it did is read from its exit, never this write.
56
+ child.stdin?.on('error', () => { });
57
+ child.stdin?.end(trial.input);
58
+ });
59
+ }
60
+ }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * The lines drawn above the first question a command asks, and above none of the ones after it. `init` asks up to
3
5
  * three things in a row; the mark belongs at the top of that conversation, not before every prompt. One banner is
@@ -0,0 +1 @@
1
+ export {};
@@ -1 +1,14 @@
1
- export {};
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { FileAccessError } from './file-access-error.js';
4
+ /** Whether a folder is there: an error the port names is "no", and nothing else is swallowed. */
5
+ export async function isDirectory(directories, path) {
6
+ try {
7
+ return (await directories.kindOf(path)) === 'directory';
8
+ }
9
+ catch (error) {
10
+ if (!(error instanceof FileAccessError))
11
+ throw error;
12
+ return false;
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * What a person says they did about a file (R31; `for-people-who-build-with-ai.md` F25, F50): the keys it held were
3
5
  * replaced, a template's values are placeholders, what could be done about a file whose contents cannot be changed was
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { resolve } from 'node:path';
2
4
  import { hasGlob, matchesName, reachOf } from '../core/access/search-reach.js';
3
5
  import { protectionOf } from '../core/policy/policy.js';
@@ -1,14 +1,13 @@
1
- import { isAbsolute, join, dirname } from 'node:path';
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { isAbsolute, join } from 'node:path';
2
4
  import { parsePreToolUseInput } from '../adapter/claude-code/hooks/pre-tool-use-input.js';
3
- import { PROJECT_HOOKS } from '../adapter/codex/contract/hooks.js';
5
+ import { refuseProjectAbove, refuseRulesOf } from '../adapter/claude-code/settings/refuse-project.js';
4
6
  import { parseCodexPreToolUseInput } from '../adapter/codex/hooks/pre-tool-use-input.js';
5
7
  import { protectedPathsInCommand } from '../core/access/protected-access.js';
6
- import { FileAccessError } from '../ports/file-access-error.js';
7
8
  import { choosePolicy } from '../report/choose-policy.js';
8
9
  import { blockingOnly } from '../core/policy/policy.js';
9
10
  import { protectedFileReached } from './command-reach.js';
10
- /** How many folders above the session's the project is looked for in: deeper than any project, short of a loop. */
11
- const MAX_PROJECT_DEPTH = 64;
12
11
  /** A command line is a few kilobytes; past this the input is not held at all, and the command is not checked. */
13
12
  const MAX_INPUT_BYTES = 1024 * 1024;
14
13
  /**
@@ -32,6 +31,10 @@ export class CommandRefusal {
32
31
  if ('anotherTool' in parsed)
33
32
  return { kind: 'allow' };
34
33
  const settled = codex ? await this.#inCodexProject(choice, parsed.cwd ?? this.#dependencies.workingDirectory) : choice;
34
+ // AO6: the computer-wide check runs in every Codex session; where no project above runs `refuse`, there are no
35
+ // rules anybody chose here, and the command runs with no message.
36
+ if (settled === 'no-project')
37
+ return { kind: 'allow' };
35
38
  if (settled === undefined)
36
39
  return { kind: 'not-checked', reason: 'project' };
37
40
  const chosen = await choosePolicy(settled, this.#dependencies.files, this.#dependencies.tell);
@@ -55,34 +58,20 @@ export class CommandRefusal {
55
58
  return { kind: 'refuse', path: reach.path, pattern: reach.pattern, others: 0, route };
56
59
  }
57
60
  /**
58
- * CK3: Codex runs its hook in the session's folder, which can lie below the project, and names the project nowhere
59
- * (CKB6). A relative settings path is read in the project: the nearest folder, at or above `folder`, that holds the
60
- * `.codex/hooks.json` this hook is written in. None found is `undefined`: the command runs, and is said to be unchecked.
61
+ * CK3, AO5, AO6: Codex runs its check in the session's folder, which can lie below the project, and names the
62
+ * project nowhere (CKB6). The project is the nearest folder at or above `folder`, short of the home directory, whose
63
+ * Claude Code settings run `refuse`. With no flag of its own, the check reads the rules that project's `refuse`
64
+ * names, and with no project found it is `'no-project'`: the command runs, silently. A relative `--settings`, as an
65
+ * older project-level entry wrote it, resolves against the same project; none found is `undefined`, and the command
66
+ * runs said to be unchecked (R20). An absolute path or a policy file is read as given.
61
67
  */
62
68
  async #inCodexProject(choice, folder) {
63
69
  const relative = choice.settingsPath;
64
- if (relative === undefined || isAbsolute(relative))
70
+ if (choice.policyPath !== undefined || (relative !== undefined && isAbsolute(relative)))
65
71
  return choice;
66
- for (let at = folder, depth = 0; depth < MAX_PROJECT_DEPTH; depth += 1) {
67
- if (await this.#holds(join(at, PROJECT_HOOKS.directory, PROJECT_HOOKS.file)))
68
- return { ...choice, settingsPath: join(at, relative) };
69
- const up = dirname(at);
70
- if (up === at)
71
- return undefined;
72
- at = up;
73
- }
74
- return undefined;
75
- }
76
- /** Whether a file is there to read; a folder or anything unreadable is not. */
77
- async #holds(path) {
78
- try {
79
- await this.#dependencies.files.readText(path);
80
- return true;
81
- }
82
- catch (error) {
83
- if (error instanceof FileAccessError)
84
- return false;
85
- throw error;
86
- }
72
+ const found = await refuseProjectAbove(this.#dependencies.files, folder, this.#dependencies.home);
73
+ if (relative !== undefined)
74
+ return found === undefined ? undefined : { ...choice, settingsPath: join(found.project, relative) };
75
+ return found === undefined ? 'no-project' : refuseRulesOf(found);
87
76
  }
88
77
  }
@@ -1,10 +1,12 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { HOOK_OUTPUT } from '../../adapter/claude-code/contract/hooks.js';
2
4
  const NOT_CHECKED = {
3
5
  arguments: 'agentwhy refuse was given arguments it does not recognise, so this command was not checked.',
4
6
  input: 'agentwhy refuse could not read the hook input, so this command was not checked.',
5
7
  policy: 'agentwhy refuse could not read the policy it was given, so this command was not checked.',
6
8
  search: 'agentwhy refuse could not look through everything this search reaches, so this command was not checked.',
7
- project: 'agentwhy refuse found no .codex/hooks.json in this folder or above it, so it could not read the project\'s rules and this command was not checked.',
9
+ project: 'agentwhy refuse found no project above this folder whose Claude Code settings run refuse, so it could not read the rules it was pointed at and this command was not checked.',
8
10
  };
9
11
  /** Said to the user, as a hook's JSON on stdout, when a command ran without being checked (R20). */
10
12
  export function notCheckedMessage(reason) {