@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,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * The signals of specification §6.2, read out of the text a delegation carried.
3
5
  *
@@ -0,0 +1,42 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { isAbsolute, join, relative, sep } from 'node:path';
4
+ /**
5
+ * Where macOS asks the person before an app reads, under their home directory (`which-project.md` V10b): Desktop,
6
+ * Documents, Downloads, iCloud Drive and the folders other cloud drives keep. **Documented by Apple, measured here only
7
+ * for Documents** (2026-10-01).
8
+ */
9
+ const UNDER_HOME = [['Desktop'], ['Documents'], ['Downloads'], ['Library', 'Mobile Documents'], ['Library', 'CloudStorage']];
10
+ /** Other disks, removable or on a network: one place each, asked about as a kind rather than by name. */
11
+ const VOLUMES = '/Volumes';
12
+ /**
13
+ * The guarded place a folder lies in, as the root of that place, or `undefined` where it lies in none. Compared without
14
+ * regard to case, as the file system macOS starts with compares names. A pure answer about a path: nothing is read.
15
+ */
16
+ export function guardedPlaceOf(path, home) {
17
+ for (const parts of UNDER_HOME) {
18
+ const root = join(home, ...parts);
19
+ if (sameOrWithin(root, path))
20
+ return root;
21
+ }
22
+ if (!within(VOLUMES, path))
23
+ return undefined;
24
+ // `within` compared without case; the disk's own name is read from the path once its first part is `/Volumes`'s.
25
+ const disk = relative(VOLUMES, VOLUMES + path.slice(VOLUMES.length)).split(sep)[0];
26
+ return disk === undefined || disk === '' ? undefined : join(VOLUMES, disk);
27
+ }
28
+ /**
29
+ * Whether a run working in `workingDirectory` must leave `path` alone (V10b): it lies in a guarded place, and not in the
30
+ * one the working directory lies in - which the app agentwhy runs under can read already, or it could not work there.
31
+ */
32
+ export function leftAlone(path, workingDirectory, home) {
33
+ const place = guardedPlaceOf(path, home);
34
+ return place !== undefined && place.toLowerCase() !== guardedPlaceOf(workingDirectory, home)?.toLowerCase();
35
+ }
36
+ function sameOrWithin(folder, path) {
37
+ return relative(folder.toLowerCase(), path.toLowerCase()) === '' || within(folder, path);
38
+ }
39
+ function within(folder, path) {
40
+ const between = relative(folder.toLowerCase(), path.toLowerCase());
41
+ return between !== '' && between !== '..' && !between.startsWith(`..${sep}`) && !isAbsolute(between);
42
+ }
@@ -1,6 +1,9 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { isAbsolute, relative, sep } from 'node:path';
2
4
  /**
3
- * A folder that is gone cannot be offered, so only projects whose folder exists are considered. Where the folder lies
5
+ * A folder that is gone cannot be offered, so only projects whose folder is not known to be gone are considered - one not
6
+ * looked at (V10b) is looked at when it is picked. Where the folder lies
4
7
  * inside more than one project, the nearest - the deepest - is the one; the projects inside it keep the listing's order.
5
8
  * Paths are compared through `relative`, which compares a Windows path without regard to case, as its file system does.
6
9
  *
@@ -9,7 +12,7 @@ import { isAbsolute, relative, sep } from 'node:path';
9
12
  * offered the home directory as the project above it, which setup then refuses.
10
13
  */
11
14
  export function nearestProject(folder, listing, noProject) {
12
- const present = listing.projects.filter((project) => project.exists && !noProject(project.path));
15
+ const present = listing.projects.filter((project) => project.folder !== 'gone' && !noProject(project.path));
13
16
  const itself = present.find((project) => relative(project.path, folder) === '');
14
17
  if (itself !== undefined)
15
18
  return { kind: 'itself', project: itself };
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Path-glob matching, written out rather than taken from a library or from an experimental Node API, because
3
5
  * which paths count as protected is the security decision of this tool: it has to be small enough to read and
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { isJsonObject, parseJsonObject } from '../../shared/json.js';
2
4
  export const POLICY_VERSION = 1;
3
5
  const LEVELS = ['no-read', 'no-disclose'];
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { matchesGlob } from './glob.js';
2
4
  /**
3
5
  * The entry that protects this path, or undefined. An exception wins over every protecting pattern, so a path
@@ -14,7 +16,17 @@ export function protectionOf(policy, path) {
14
16
  export function protects(policy, path) {
15
17
  return protectionOf(policy, path) !== undefined;
16
18
  }
17
- /** The same policy with only what is kept from the agent: what a hook that stops a command may act on (F57). */
19
+ /**
20
+ * The same policy with only what is kept from the agent: what a hook that stops a command may act on (F57). A told
21
+ * pattern becomes one of the allowed, not merely absent: dropped, a broader blocking pattern answered for the file -
22
+ * `demo.env`, told by its name but inside the blocked `.env` wildcard, was still refused - and a file the person said
23
+ * their AI may read was kept from it (found by the maintainer, 2026-10-02).
24
+ */
18
25
  export function blockingOnly(policy) {
19
- return { ...policy, protected: policy.protected.filter((entry) => entry.mode !== 'tell') };
26
+ const told = policy.protected.filter((entry) => entry.mode === 'tell');
27
+ return {
28
+ ...policy,
29
+ protected: policy.protected.filter((entry) => entry.mode !== 'tell'),
30
+ allowed: [...policy.allowed, ...told.map((entry) => entry.pattern)],
31
+ };
20
32
  }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { DEFAULT_POLICY } from './default-policy.js';
2
4
  /**
3
5
  * Chooses the policy: an explicit file, else the environment's own deny rules, else the built-in default.
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Class C of spec §5.4: entropy alone. It **redacts and never reports** — a high-entropy string is not
3
5
  * evidence of anything, and lesson L001 measured what happens without that rule: in one session, 385 `toolu_`
@@ -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';