@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
  import { join } from 'node:path';
2
4
  import { HOME_PREFIX, HOOK_INPUT } from '../../adapter/claude-code/contract/hooks.js';
3
5
  import { SETTINGS_FILES } from '../../adapter/claude-code/contract/settings.js';
@@ -7,13 +9,15 @@ import { parseJsonObject } from '../../shared/json.js';
7
9
  import { parseStopInput } from '../../adapter/claude-code/hooks/stop-input.js';
8
10
  import { parseSubagentStopInput } from '../../adapter/claude-code/hooks/subagent-stop-input.js';
9
11
  import { withLateMessage } from '../../core/late-message.js';
12
+ import { FileAccessError } from '../../ports/file-access-error.js';
10
13
  import { buildReport } from '../build-report.js';
11
14
  import { choosePolicy } from '../choose-policy.js';
12
15
  import { alertOf, alerts, DEFAULT_THRESHOLD } from './agent-alert.js';
13
16
  import { DEFAULT_CHANNELS, DEFAULT_CLEAN, DEFAULT_LANG, DEFAULT_SAID_AS, langOfLocale, } from './notice-choices.js';
14
- import { instructionFor, reportCommand } from './render/instruction-words.js';
17
+ import { CLAUDE_CODE_REQUESTS, reportCommand } from './render/instruction-words.js';
15
18
  import { preferencesFor, readPreferences } from './preferences.js';
16
19
  import { NOTICE_TITLE, withTitle } from './render/notice-words.js';
20
+ import { toDoItems } from '../render/report-page/to-do.js';
17
21
  // Where they live now: both the notice and the preferences that choose it name them, and neither owns the other.
18
22
  export { CLEAN_MODES, DEFAULT_CHANNELS, DEFAULT_CLEAN, NOTICE_CHANNELS } from './notice-choices.js';
19
23
  /**
@@ -38,21 +42,45 @@ const SESSION_COUNTS = 'session:counts';
38
42
  const CLEAN_SAID = 'session:clean';
39
43
  /** That an unreadable preferences file has been said already: once a session, never after every turn (R24). */
40
44
  const PREFERENCES_SAID = 'session:preferences';
45
+ /** That an agent which kept no record has been said already: once a session (`when-an-agent-finishes.md` R4a). */
46
+ const NO_RECORD_SAID = 'session:no-record';
41
47
  /**
42
48
  * The ways into Claude Code that have a person reading, as `CLAUDE_CODE_ENTRYPOINT` names them - measured on the
43
49
  * terminal and on the editor extension (B9e2). `sdk-cli`, which is `claude -p`, is left out on purpose, and so is
44
- * any name this version has not seen.
50
+ * any name this version has not seen. `claude-desktop`, the Claude desktop app, was measured too (B9e3): the agent's
51
+ * message is shown there in the open, under folded rows for the hook's own words.
45
52
  */
46
- const ATTENDED = ['cli', 'claude-vscode'];
53
+ const ATTENDED = ['cli', 'claude-vscode', 'claude-desktop'];
54
+ /**
55
+ * Where the first quiet turn of a session is said by the agent as well as the line (R5, amended 2026-10-02 by the
56
+ * maintainer): the Claude desktop app, which folds the line away (B9e3), and the VS Code extension, a flow the
57
+ * maintainer wants the same. Not the terminal, where the line is in the open under the reply.
58
+ */
59
+ const CLEAN_SAID_BY_AGENT = ['claude-desktop', 'claude-vscode'];
47
60
  /** The records the display keeps about the session itself. None of them is an agent, and none is ever counted as one. */
48
- const KEPT_NAMES = [OWN_REACH_SAID, SESSION_COUNTS, CLEAN_SAID, PREFERENCES_SAID];
49
- /** What the session had said about it before, plus what is being said now. Levels only, never what they were about. */
61
+ const KEPT_NAMES = [OWN_REACH_SAID, SESSION_COUNTS, CLEAN_SAID, PREFERENCES_SAID, NO_RECORD_SAID];
62
+ /** WS5: remembered with the alert, so the turn's own line knows a rule did not refuse everything it counts. */
63
+ function notByRule(alert) {
64
+ return alert.level === 'refused' && alert.refusedByOthers !== undefined ? { notByRule: true } : {};
65
+ }
66
+ /**
67
+ * What the session had said about it before, plus what is being said now. Levels only, never what they were about. A
68
+ * value from a file the person lets their AI read is counted apart: a later quiet turn must not call it a key (F57a).
69
+ */
50
70
  function countedWith(before, saying) {
71
+ const told = (before?.told ?? 0) + saying.filter((alert) => alert.told === true).length;
72
+ const data = (before?.data ?? 0) + saying.filter((alert) => alert.level === 'value' && alert.data === true).length;
51
73
  return {
52
- values: (before?.values ?? 0) + saying.filter((alert) => alert.level === 'value').length,
53
- reached: (before?.reached ?? 0) + saying.filter((alert) => alert.level === 'reached').length,
74
+ values: (before?.values ?? 0) + saying.filter((alert) => alert.level === 'value' && alert.told !== true && alert.data !== true).length,
75
+ reached: (before?.reached ?? 0) + saying.filter((alert) => alert.level === 'reached' && alert.told !== true).length,
76
+ ...(told > 0 ? { told } : {}),
77
+ ...(data > 0 ? { data } : {}),
54
78
  };
55
79
  }
80
+ /** Whether a session has had anything said about it: a key, a private file opened, or one the person lets it read. */
81
+ function anythingSaid(counts) {
82
+ return counts.values > 0 || counts.reached > 0 || (counts.told ?? 0) > 0 || (counts.data ?? 0) > 0;
83
+ }
56
84
  /**
57
85
  * `agentwhy watch`: one finished agent, read the way `report` reads a session, and one notice about it
58
86
  * (`specs/2026-09-16-when-an-agent-finishes.md`). Every way it can fail to look is a notice saying so - never an
@@ -74,13 +102,59 @@ export class SubagentWatch {
74
102
  return this.announce({ kind: 'not-checked', reason: 'input' }, options.channels ?? DEFAULT_CHANNELS);
75
103
  // Where the session runs decides which project's answers apply, and it is on every input this hook is given.
76
104
  const settled = await this.#settle(options, directoryIn(text));
77
- const stop = parseStopInput(text);
105
+ const format = this.#dependencies.turnFormat;
106
+ // CX2, CK13: a command agentwhy stopped this turn is the turn's message, and nothing else is asked of the agent. Looked
107
+ // for before the turn is read: it needs the turn and its rollout alone, as the Stop hook it replaced did, so a `Stop`
108
+ // without a session id (CKB12) still says it. Its continuation finds nothing.
109
+ if (format !== undefined) {
110
+ const stopped = await format.stoppedIn(text);
111
+ if (stopped.length > 0) {
112
+ const paths = [...new Set(stopped.map((one) => one.path))];
113
+ return { notice: { kind: 'quiet' }, output: blockNotice(format.requests.stopped(paths, settled.lang), '') };
114
+ }
115
+ // CX9, after the refusals: a command agentwhy refused is said wherever the hook ran, but outside a project the
116
+ // person set up, nothing else is - a user-level hook fires in every conversation, the app's scratch folders too.
117
+ if (!(await format.watchedHere(text)))
118
+ return { notice: { kind: 'quiet' }, output: '' };
119
+ }
120
+ const stop = format === undefined ? parseStopInput(text) : format.readStop(text);
78
121
  if (!('unusable' in stop))
79
- return this.#sayWhatTheTurnFound(stop.turn, settled);
122
+ return this.#sayWhatTheTurnFound(stop.turn, await this.#rulesFor(settled, text));
123
+ // CX1, §4 of its specification: another AI's hook says what `Stop` says, and nothing for any other event.
124
+ if (format !== undefined)
125
+ return { notice: { kind: 'quiet' }, output: '' };
80
126
  const { notice, agent } = await this.#noticeFor(text, settled);
127
+ if (await this.#saidBefore(notice, agent))
128
+ return { notice: { kind: 'quiet' }, output: '' };
81
129
  await this.#remember(notice, agent, settled);
82
130
  return this.announce(notice, settled.channels);
83
131
  }
132
+ /**
133
+ * An agent that kept no record is said once a session (R4a). The Claude desktop app finishes one after many of its
134
+ * turns (B4g), and the same sentence after each is a line a person learns to skip. Said once is still said
135
+ * (invariant 4), and recorded before it is said, so a hook that fires again stays quiet. A session the input does
136
+ * not name has no "once", and hears it every time.
137
+ */
138
+ async #saidBefore(notice, agent) {
139
+ if (notice.kind !== 'no-record' || agent?.sessionId === undefined)
140
+ return false;
141
+ const { store } = this.#dependencies;
142
+ if ((await store.peek(agent.sessionId)).some((record) => record.agentId === NO_RECORD_SAID))
143
+ return true;
144
+ await store.remember(agent.sessionId, { agentId: NO_RECORD_SAID, level: 'no-record', words: '' });
145
+ return false;
146
+ }
147
+ /**
148
+ * CX5: where the hook is another AI's and no flag named the rules, the project's own, found from where it ran - the
149
+ * rules Claude Code's hooks read there, so one list is read by both (CK2).
150
+ */
151
+ async #rulesFor(settled, text) {
152
+ const format = this.#dependencies.turnFormat;
153
+ if (format === undefined || settled.policyPath !== undefined || settled.settingsPath !== undefined)
154
+ return settled;
155
+ const settingsPath = await format.settingsFor(text);
156
+ return settingsPath === undefined ? settled : { ...settled, settingsPath };
157
+ }
84
158
  /**
85
159
  * One question at a time, in the order R24 sets: the flag if it was written, then this project's answer, then this
86
160
  * person's, then what this tool does when nobody has said anything. A file that cannot be read answers nothing and
@@ -116,53 +190,87 @@ export class SubagentWatch {
116
190
  * unless it changes.
117
191
  */
118
192
  async #sayWhatTheTurnFound(turn, options) {
119
- // Taking clears the store, so a run that cannot show the words must not take them.
120
- if (!options.channels.includes('chat'))
193
+ const format = this.#dependencies.turnFormat;
194
+ // Taking clears the store, so a run that cannot show the words must not take them: they are shown as the line in the
195
+ // conversation, or as a system notification (`when-an-agent-finishes.md` R15a).
196
+ const chat = options.channels.includes('chat');
197
+ if (!chat && !options.channels.includes('os'))
121
198
  return { notice: { kind: 'quiet' }, output: '' };
199
+ // What is said once a conversation is remembered under the conversation (CX8), which for Claude Code is the session.
200
+ const conversation = format === undefined ? turn.sessionId : await format.conversationOf(turn);
122
201
  const { store } = this.#dependencies;
123
- const remembered = await store.take(turn.sessionId);
202
+ const remembered = await store.take(conversation);
124
203
  const kept = new Map(remembered.map((alert) => [alert.agentId, alert]));
125
204
  const said = kept.get(OWN_REACH_SAID);
126
205
  const delegated = remembered.filter((alert) => !KEPT_NAMES.includes(alert.agentId));
127
206
  const own = await this.#ownReach(turn, options);
128
207
  const reach = own.kind === 'alert' ? own.alert : undefined;
129
208
  if (reach !== undefined)
130
- await store.remember(turn.sessionId, { ...reach, agentId: OWN_REACH_SAID });
131
- // Written back because `take` cleared them. A turn nobody looked at changes nothing that was said: without this the
132
- // turn after it took the same finding for a new one and blocked a second time (R10). An unusable preferences file
133
- // is said once a session (R24), whatever this turn says. A clean line said once is let go on a turn with a finding,
134
- // on purpose: the next quiet turn then says what the session holds (R4, R6).
135
- const unchanged = own.kind === 'not-looked' ? [said, kept.get(CLEAN_SAID), kept.get(PREFERENCES_SAID)] : [kept.get(PREFERENCES_SAID)];
209
+ await store.remember(conversation, { ...reach, agentId: OWN_REACH_SAID });
210
+ // Written back because `take` cleared them. A turn nobody looked at, or one that opened a file nobody asked to hear
211
+ // about (R6a), changes nothing that was said: without this the turn after it took the same finding for a new one
212
+ // and blocked a second time (R10). An unusable preferences file and an agent that kept no record are each said once
213
+ // a session (R24; `when-an-agent-finishes` R4a), whatever this turn says. A clean line said once is let go on a turn
214
+ // with a finding, on purpose: the next quiet turn then says what the session holds (R4, R6).
215
+ const always = [kept.get(PREFERENCES_SAID), kept.get(NO_RECORD_SAID)];
216
+ const unchanged = own.kind === 'not-looked' || own.kind === 'unsaid' ? [said, kept.get(CLEAN_SAID), ...always] : always;
136
217
  for (const record of unchanged)
137
218
  if (record !== undefined)
138
- await store.remember(turn.sessionId, record);
219
+ await store.remember(conversation, record);
139
220
  const fresh = reach !== undefined && reach.words !== said?.words ? [reach] : [];
140
221
  const saying = [...fresh, ...delegated];
141
222
  // What this session has had said about it, this turn included. Written back because `take` cleared it.
142
223
  const counts = countedWith(kept.get(SESSION_COUNTS)?.counts, saying);
143
- if (counts.values > 0 || counts.reached > 0) {
144
- await store.remember(turn.sessionId, { agentId: SESSION_COUNTS, level: 'counts', words: '', counts });
224
+ if (anythingSaid(counts)) {
225
+ await store.remember(conversation, { agentId: SESSION_COUNTS, level: 'counts', words: '', counts });
145
226
  }
146
227
  /*
147
228
  * A turn this run could not look at is not a quiet turn, and must never be said as one: every way of failing to
148
229
  * look is silence on this event (`the-agent-nobody-watches` R9), and a clean line over a reading that did not
149
- * happen is the one sentence this tool must never write.
230
+ * happen is the one sentence this tool must never write. Nor is a turn that opened a private file the person did
231
+ * not ask to hear about (R6a): the clean line says no private file was opened.
150
232
  */
233
+ // R13: the turn a block caused is the agent saying what was found, and a quiet line after it - "nothing new now,
234
+ // earlier a key was read" - repeats it, in the chat and as a notification. Only something new is said there.
151
235
  const notice = saying.length > 0
152
236
  ? { kind: 'turn', remembered: saying }
153
- : own.kind === 'not-looked'
237
+ : own.kind === 'not-looked' || turn.active
154
238
  ? { kind: 'quiet' }
155
- : (await this.#preferencesNotice(turn, options, kept.has(PREFERENCES_SAID))) ??
156
- (await this.#cleanNotice(turn, options, counts, kept.has(CLEAN_SAID)));
239
+ : (await this.#preferencesNotice(conversation, options, kept.has(PREFERENCES_SAID))) ??
240
+ (own.kind === 'unsaid' ? { kind: 'quiet' } : await this.#cleanNotice(conversation, options, counts, kept.has(CLEAN_SAID)));
241
+ await this.#notifyTurn(notice, fresh, options.channels);
242
+ if (!chat)
243
+ return { notice, output: '' };
157
244
  // The other two channels carry a title of their own; a line in the conversation has none, so it says who speaks.
158
245
  const words = this.#dependencies.renderer.render(notice);
159
246
  const files = own.kind === 'alert' ? own.files : [];
160
- if (this.#agentSpeaks(turn, options, fresh)) {
161
- const command = reportCommand(turn.sessionId, await this.#invocation());
162
- return { notice, output: blockNotice(instructionFor(files, command), withTitle(words)) };
247
+ const reader = await this.#readerOf(turn);
248
+ const requests = format?.requests ?? CLAUDE_CODE_REQUESTS;
249
+ if (this.#agentSpeaks(turn, options, fresh, reader)) {
250
+ const command = reportCommand(format === undefined ? turn.sessionId : format.sessionKey(turn.sessionId), await this.#invocation());
251
+ const asked = requests[own.kind === 'alert' ? own.found : 'keys'];
252
+ return { notice, output: blockNotice(asked(files, command, options.lang, reader.foldsTurn === true), withTitle(words)) };
163
253
  }
254
+ if (this.#agentSaysClean(turn, options, notice, reader))
255
+ return { notice, output: blockNotice(requests.quiet(options.lang, reader.foldsTurn === true), withTitle(words)) };
164
256
  return { notice, output: chatNotice(withTitle(words)) };
165
257
  }
258
+ /**
259
+ * The end of a turn as a system notification, where one was chosen (`when-an-agent-finishes.md` R15a): the Claude
260
+ * desktop app shows no line in the conversation (B4h), and this is what reaches a person there. Only what this turn
261
+ * adds: a delegated agent's notice went out on it when that agent finished, so where the conversation is told as
262
+ * well, the new part is the turn's own reach or the line of a quiet turn. Without the conversation it is the whole turn.
263
+ */
264
+ async #notifyTurn(notice, fresh, channels) {
265
+ if (!channels.includes('os'))
266
+ return;
267
+ const shown = notice.kind !== 'turn' || !channels.includes('chat')
268
+ ? notice
269
+ : fresh.length > 0 ? { kind: 'turn', remembered: fresh } : { kind: 'quiet' };
270
+ const words = this.#dependencies.renderer.render(shown);
271
+ if (words !== '')
272
+ await this.#dependencies.notifier.notify(NOTICE_TITLE, words);
273
+ }
166
274
  /**
167
275
  * Whether this turn's finding is said by the session's own agent rather than as a line (R9-R14). Every condition
168
276
  * is a measurement or a reason written down beside it, and all of them must hold:
@@ -179,12 +287,37 @@ export class SubagentWatch {
179
287
  * - **a person is reading** - the entry point is one measured attended (B9e2, R14). One this version does not
180
288
  * know, or none at all, gets the line: a scripted run continued by a block prints an answer it never asked for.
181
289
  */
182
- #agentSpeaks(turn, options, fresh) {
290
+ #agentSpeaks(turn, options, fresh, reader) {
291
+ return fresh.some((alert) => alert.level === 'value' || alert.told === true) &&
292
+ options.say === 'agent' &&
293
+ !turn.active &&
294
+ reader.attended;
295
+ }
296
+ /**
297
+ * Who reads the session (R14, R5): for Claude Code, the entry point its hooks are handed (B9e2, B9e3); for another
298
+ * AI, what its own record says (`codex-says-it-too` CX4).
299
+ */
300
+ async #readerOf(turn) {
301
+ const format = this.#dependencies.turnFormat;
302
+ if (format !== undefined)
303
+ return format.readerOf(turn);
183
304
  const { entryPoint } = this.#dependencies;
184
- return fresh.some((alert) => alert.level === 'value') &&
305
+ return {
306
+ attended: entryPoint !== undefined && ATTENDED.includes(entryPoint),
307
+ quietSaidByAgent: entryPoint !== undefined && CLEAN_SAID_BY_AGENT.includes(entryPoint),
308
+ };
309
+ }
310
+ /**
311
+ * Whether the first quiet turn of a session is said by the agent too (R5, amended 2026-10-02): the line that says the
312
+ * watch is running and nothing has been opened - never a later quiet turn, never one over an earlier finding (R6),
313
+ * never every turn - where the person left `say` at `agent`, outside a continuation (R13), and in an interface the
314
+ * maintainer asked it for (`CLEAN_SAID_BY_AGENT`). The line was marked said before this is asked, so it blocks once.
315
+ */
316
+ #agentSaysClean(turn, options, notice, reader) {
317
+ return notice.kind === 'clean' && notice.first && !anythingSaid(notice.counts) &&
185
318
  options.say === 'agent' &&
186
319
  !turn.active &&
187
- entryPoint !== undefined && ATTENDED.includes(entryPoint);
320
+ reader.quietSaidByAgent;
188
321
  }
189
322
  /**
190
323
  * How the agent is to run agentwhy: the way the `Stop` hook running now does, read from the project's settings -
@@ -209,11 +342,11 @@ export class SubagentWatch {
209
342
  * never as a block. Without it a broken file reads exactly like choices that were honoured - the person who set
210
343
  * `off` and still hears a line, or set `every-turn` and hears nothing, would have no way to know why.
211
344
  */
212
- async #preferencesNotice(turn, options, said) {
345
+ async #preferencesNotice(conversation, options, said) {
213
346
  if (!options.preferencesUnusable)
214
347
  return undefined;
215
348
  // Written back whatever happens next, since `take` cleared it with everything else.
216
- await this.#dependencies.store.remember(turn.sessionId, { agentId: PREFERENCES_SAID, level: 'preferences', words: '' });
349
+ await this.#dependencies.store.remember(conversation, { agentId: PREFERENCES_SAID, level: 'preferences', words: '' });
217
350
  return said ? undefined : { kind: 'preferences-unusable' };
218
351
  }
219
352
  /**
@@ -221,17 +354,17 @@ export class SubagentWatch {
221
354
  * it is held to two things: it never speaks where the person asked for silence, and it never says a session is
222
355
  * clean when an earlier turn of it was not - what the turn found and what the session holds are said apart.
223
356
  */
224
- async #cleanNotice(turn, options, counts, cleanSaid) {
357
+ async #cleanNotice(conversation, options, counts, cleanSaid) {
225
358
  const mode = options.clean;
226
359
  if (mode === 'off')
227
360
  return { kind: 'quiet' };
228
361
  // Said once means once: the marker is written back, since `take` cleared it with everything else.
229
362
  if (mode === 'once' && cleanSaid) {
230
- await this.#dependencies.store.remember(turn.sessionId, { agentId: CLEAN_SAID, level: 'clean', words: '' });
363
+ await this.#dependencies.store.remember(conversation, { agentId: CLEAN_SAID, level: 'clean', words: '' });
231
364
  return { kind: 'quiet' };
232
365
  }
233
366
  if (mode === 'once')
234
- await this.#dependencies.store.remember(turn.sessionId, { agentId: CLEAN_SAID, level: 'clean', words: '' });
367
+ await this.#dependencies.store.remember(conversation, { agentId: CLEAN_SAID, level: 'clean', words: '' });
235
368
  return { kind: 'clean', first: !cleanSaid && mode === 'once', counts, lang: options.lang };
236
369
  }
237
370
  /**
@@ -253,13 +386,56 @@ export class SubagentWatch {
253
386
  const model = turn.lastMessage === undefined ? read : withLateMessage(read, read.sessionId, turn.lastMessage);
254
387
  const report = buildReport(model, policy.policy, createRedactor(model.projectRoot), { share: false, projectRoot: model.projectRoot });
255
388
  const alert = alertOf(report, report.graph.main.index);
389
+ /*
390
+ * S1 of `the-chat-says-what-the-report-says`: a read of files the person tracks is `told` at every threshold -
391
+ * choosing Tell me is asking to be told, and only switching alerts off silences it. Asked before the threshold,
392
+ * because a tracked read without a traced value is `reached`, which the default threshold never says: the person
393
+ * heard "no value found", in English, over a read they had asked to hear about - and in the ChatGPT/Codex app,
394
+ * which shows no plain line, nothing at all (seen by the maintainer, 2026-10-02).
395
+ */
396
+ if (alert !== undefined && (alert.level === 'value' || alert.level === 'reached')) {
397
+ const readByMain = this.#toldReadBy(report);
398
+ if (readByMain !== undefined) {
399
+ const words = renderer.render({ kind: 'told', lang: options.lang });
400
+ return {
401
+ kind: 'alert',
402
+ alert: { agentId: OWN_REACH_SAID, level: alert.level, words, ...notByRule(alert), told: true },
403
+ files: readByMain,
404
+ found: 'told',
405
+ };
406
+ }
407
+ }
408
+ // A refusal opened nothing, so a clean line stays true over it; a reach did, so it does not (R6a).
409
+ if (alert !== undefined && alert.level === 'reached' && !alerts(alert.level, options.on))
410
+ return { kind: 'unsaid' };
256
411
  if (alert === undefined || !alerts(alert.level, options.on))
257
412
  return { kind: 'nothing' };
258
- return {
259
- kind: 'alert',
260
- alert: { agentId: OWN_REACH_SAID, level: alert.level, words: renderer.render({ kind: 'alert', alert }) },
261
- files: readFrom(report, report.graph.main.index),
262
- };
413
+ const opened = readFrom(report, report.graph.main.index);
414
+ if (alert.level !== 'value') {
415
+ const words = renderer.render({ kind: 'alert', alert });
416
+ return { kind: 'alert', alert: { agentId: OWN_REACH_SAID, level: alert.level, words, ...notByRule(alert) }, files: opened, found: 'keys' };
417
+ }
418
+ const found = foundIn(report, opened);
419
+ const { what } = found;
420
+ const words = renderer.render(what === 'told' ? { kind: 'told', lang: options.lang } : { kind: 'read', what, lang: options.lang });
421
+ const remembered = what === 'told' ? { told: true } : what === 'data' ? { data: true } : {};
422
+ return { kind: 'alert', alert: { agentId: OWN_REACH_SAID, level: alert.level, words, ...notByRule(alert), ...remembered }, files: found.files, found: what };
423
+ }
424
+ /**
425
+ * The files the session's own agent read where every one is tracked (S1, `told`): their paths, or nothing where any
426
+ * is blocked - one blocked file among them keeps the stronger words - or where nothing was read at all. Read from
427
+ * the stories, by the page's word for read (S3): a call that printed the file's text, never a name seen in a
428
+ * listing, never a write, and never a delegated agent's read (R12b).
429
+ */
430
+ #toldReadBy(report) {
431
+ const main = report.graph.main.index;
432
+ const read = [...new Set(report.stories
433
+ .filter((story) => story.agentIndex === main && story.read === true)
434
+ .map((story) => story.path))];
435
+ if (read.length === 0)
436
+ return undefined;
437
+ const told = new Set(report.findings.filter((finding) => finding.told === true).map((finding) => finding.path));
438
+ return read.every((path) => told.has(path)) ? read : undefined;
263
439
  }
264
440
  /**
265
441
  * What `chat` does at `SubagentStop`: keep the words for the end of the turn, because this event discards a
@@ -268,7 +444,7 @@ export class SubagentWatch {
268
444
  async #remember(notice, agent, options) {
269
445
  if (!options.channels.includes('chat'))
270
446
  return;
271
- if (notice.kind !== 'alert' && notice.kind !== 'not-checked')
447
+ if (notice.kind !== 'alert' && notice.kind !== 'not-checked' && notice.kind !== 'no-record')
272
448
  return;
273
449
  // Without a session there is no conversation to speak in; the immediate channels said it if they were asked to.
274
450
  if (agent?.sessionId === undefined)
@@ -276,10 +452,11 @@ export class SubagentWatch {
276
452
  const words = this.#dependencies.renderer.render(notice);
277
453
  if (words === '')
278
454
  return;
279
- // A reason it could not look is remembered under the reason, so a hook that fires again says it once (R8).
455
+ // A reason it could not look is remembered under the reason, so a hook that fires again says it once (R8). An agent
456
+ // that kept no record is one that could not be checked, and is counted as one where a turn sums several.
280
457
  const which = notice.kind === 'alert'
281
- ? { agentId: agent.agentId, level: notice.alert.level }
282
- : { agentId: `not-checked:${notice.reason}`, level: 'not-checked' };
458
+ ? { agentId: agent.agentId, level: notice.alert.level, ...notByRule(notice.alert) }
459
+ : { agentId: `not-checked:${notice.kind === 'no-record' ? 'no-record' : notice.reason}`, level: 'not-checked' };
283
460
  await this.#dependencies.store.remember(agent.sessionId, { ...which, words });
284
461
  }
285
462
  async #noticeFor(text, options) {
@@ -298,6 +475,8 @@ export class SubagentWatch {
298
475
  const model = agent.lastMessage === undefined ? read : withLateMessage(read, agent.agentId, agent.lastMessage);
299
476
  // R3: by the id the hook names, and by nothing else.
300
477
  const agentIndex = model.agents.findIndex((candidate) => candidate.id === agent.agentId);
478
+ if (agentIndex < 0 && (await leftNoRecord(files, agent, home)))
479
+ return { notice: { kind: 'no-record', lang: options.lang }, agent };
301
480
  if (agentIndex < 0 || model.agents[agentIndex]?.id === model.sessionId) {
302
481
  return { notice: { kind: 'not-checked', reason: 'agent-not-found' }, agent };
303
482
  }
@@ -316,6 +495,30 @@ export class SubagentWatch {
316
495
  * file the session only wrote a value into. Found by the first test of this, on a session that read `.env` and
317
496
  * wrote into `.env.production`.
318
497
  */
498
+ /**
499
+ * What the files the conversation read held (`the-chat-says-what-the-report-says.md` S1): `told` where every one is a
500
+ * file the person lets their AI read (F57); else the strongest kind among the others, as the report's to-do list says it -
501
+ * keys, then a template, then private data - with the files of that kind alone, the rest being the report's. A tracked
502
+ * file beside a blocked one keeps the blocked one's words, the safer of the two. A file the list does not hold is said as
503
+ * keys, the words used for every value before S1.
504
+ */
505
+ function foundIn(report, opened) {
506
+ const told = new Set(report.findings.filter((finding) => finding.told === true).map((finding) => finding.path));
507
+ if (opened.length > 0 && opened.every((file) => told.has(file)))
508
+ return { what: 'told', files: opened };
509
+ const items = new Map(toDoItems(report).map((item) => [item.path, item]));
510
+ const kindOf = (file) => {
511
+ const item = items.get(file);
512
+ return item === undefined ? 'keys' : item.template ? 'template' : item.kind;
513
+ };
514
+ const blocked = opened.filter((file) => !told.has(file));
515
+ for (const what of ['keys', 'template', 'data']) {
516
+ const files = blocked.filter((file) => kindOf(file) === what);
517
+ if (files.length > 0)
518
+ return { what, files };
519
+ }
520
+ return { what: 'keys', files: opened };
521
+ }
319
522
  function readFrom(report, agentIndex) {
320
523
  return [...new Set([
321
524
  ...report.uses.filter((use) => use.agentIndex === agentIndex).flatMap((use) => use.files),
@@ -339,6 +542,28 @@ async function textOf(files, path) {
339
542
  return undefined;
340
543
  }
341
544
  }
545
+ /**
546
+ * Whether a finished agent kept no record at all: the input names its own file, and the file is not there (R4a, B4g).
547
+ * One line is read, enough to know the file exists. A file that cannot be read, or an input that names none, is not
548
+ * this - it stays an agent that was not found (R4).
549
+ */
550
+ async function leftNoRecord(files, agent, home) {
551
+ if (agent.agentTranscriptPath === undefined)
552
+ return false;
553
+ const lines = files.readLines(expandHome(agent.agentTranscriptPath, home))[Symbol.asyncIterator]();
554
+ try {
555
+ await lines.next();
556
+ return false;
557
+ }
558
+ catch (error) {
559
+ if (error instanceof FileAccessError)
560
+ return error.failure === 'not-found';
561
+ throw error;
562
+ }
563
+ finally {
564
+ await lines.return?.();
565
+ }
566
+ }
342
567
  /** A path the hooks reference writes with a leading `~/` in its examples: expanded, never read as relative. */
343
568
  function expandHome(path, home) {
344
569
  return path.startsWith(HOME_PREFIX) ? join(home, path.slice(HOME_PREFIX.length)) : path;
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { pinnedVersions } from '../adapter/claude-code/settings/hook-entries.js';
2
4
  import { isOlder, plainVersion } from '../shared/plain-version.js';
3
5
  /**