@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,8 +1,10 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { parseArgs } from 'node:util';
2
4
  import { parseSince } from '../../core/session-filter.js';
3
5
  import { EXIT_CODE } from '../exit-codes.js';
4
6
  export const START_USAGE = `Usage: agentwhy start [--since <span | date>] [--out <dir>] [--policy <file> | --settings <file>]
5
- [--no-open] [--serve | --no-serve] [--share]
7
+ [--no-open] [--serve | --no-serve | --detach] [--share] [--session <id>] [--quiet]
6
8
 
7
9
  Lists every session of the current directory's project on one page, writes a report for each
8
10
  session active inside the range, and opens the page. The page it opens is served from 127.0.0.1,
@@ -18,8 +20,15 @@ after 30 minutes with no request, or with Ctrl+C. The reports are files either w
18
20
  --no-open write the files and print where, without opening anything
19
21
  --serve serve the page even with --no-open, and print its address
20
22
  --no-serve open the page as a file and return; marks on it are copied as commands
23
+ --detach serve the page from the background and return at once: the server already
24
+ running for this project if there is one, else a new one, which stops 30
25
+ minutes after its last page is closed. Where none can run, the page is a file
21
26
  --share no session ids, no project path, nothing above the project root. This
22
27
  reduces exposure; it does not anonymise
28
+ --session <id> open this session's report instead of the list, beside every other one, so
29
+ its "All conversations" leads back to them. Written whatever the range
30
+ --quiet say where the page is and nothing else - for an agent opening a report
31
+ because you said yes, as \`report --quiet\` does
23
32
  -h, --help this text
24
33
  `;
25
34
  // A Record over the outcome union: a new outcome does not compile until it has an exit code.
@@ -52,6 +61,9 @@ export class StartCliCommand {
52
61
  serve: { type: 'boolean' },
53
62
  'no-serve': { type: 'boolean', default: false },
54
63
  share: { type: 'boolean', default: false },
64
+ session: { type: 'string' },
65
+ quiet: { type: 'boolean', default: false },
66
+ detach: { type: 'boolean', default: false },
55
67
  help: { type: 'boolean', short: 'h', default: false },
56
68
  },
57
69
  strict: true,
@@ -66,10 +78,23 @@ export class StartCliCommand {
66
78
  if (values.serve === true && values['no-serve']) {
67
79
  return { kind: 'usage-error', message: '--serve and --no-serve cannot be given together', usage: this.usage };
68
80
  }
81
+ // A shared page names no session, and has none to open by its id.
82
+ if (values.share && values.session !== undefined) {
83
+ return { kind: 'usage-error', message: '--session names a session by its id, which --share leaves out', usage: this.usage };
84
+ }
85
+ if (values.session === '')
86
+ return { kind: 'usage-error', message: '--session needs a session id', usage: this.usage };
87
+ // PF3: from the background is served, and a shared page is never served (R37).
88
+ if (values.detach && (values.share || values['no-serve'] || values.serve === true)) {
89
+ return { kind: 'usage-error', message: '--detach serves the page, and cannot be given with --share, --serve or --no-serve', usage: this.usage };
90
+ }
91
+ const detached = this.#dependencies.detached;
92
+ if (values.detach && detached === undefined)
93
+ return { kind: 'usage-error', message: '--detach is not available here', usage: this.usage };
69
94
  const since = parseSince(values.since, this.#dependencies.now);
70
95
  if ('error' in since)
71
96
  return { kind: 'usage-error', message: since.error, usage: this.usage };
72
- const result = await this.#dependencies.start.run({
97
+ const result = await (values.detach ? detached : this.#dependencies.start).run({
73
98
  since,
74
99
  ...(values.out === undefined || values.out === '' ? {} : { out: values.out }),
75
100
  ...(values.policy === undefined ? {} : { policyPath: values.policy }),
@@ -77,6 +102,8 @@ export class StartCliCommand {
77
102
  open: !values['no-open'],
78
103
  share: values.share,
79
104
  ...(values['no-serve'] ? { serve: false } : values.serve === true ? { serve: true } : {}),
105
+ ...(values.session === undefined ? {} : { session: values.session }),
106
+ ...(values.quiet ? { quiet: true } : {}),
80
107
  });
81
108
  return { kind: 'completed', output: result.output, exitCode: EXIT_CODE_BY_OUTCOME[result.outcome] };
82
109
  }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { parseArgs } from 'node:util';
2
4
  import { ALERT_THRESHOLDS } from '../../report/watch/agent-alert.js';
3
5
  import { SAID_AS } from '../../report/watch/notice-choices.js';
@@ -20,8 +22,8 @@ and a Stop hook that exits 2 keeps the turn from ending.
20
22
  --settings <file> a settings file whose deny rules stand in for a policy, as for \`report\`
21
23
  --on <level> value (default): only a value written from a protected file.
22
24
  reached: also an agent that reached a protected file.
23
- refused: also an attempt a rule refused, where nothing was reached -
24
- the one notice that asks for nothing and says a rule held
25
+ refused: also an attempt that was stopped - by a rule, by auto mode or
26
+ by you - where nothing was reached: the one notice that asks for nothing
25
27
  --clean <when> when a turn that found nothing says so, in the conversation
26
28
  once (default): the first quiet turn says the session is being watched.
27
29
  off: nothing is ever said about a quiet turn, as before.
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  // Documented in each command's usage text; tests/cli/commands/doctor-usage.test.ts pins the doctor usage to it.
2
4
  export const EXIT_CODE = {
3
5
  ok: 0,
package/dist/cli.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ // Copyright 2026 Nessprim Karol Kozer
3
+ // SPDX-License-Identifier: Apache-2.0
2
4
  import { homedir, tmpdir } from 'node:os';
3
5
  import { EXIT_CODE, HOOK_BLOCK_EXIT_CODE } from './cli/exit-codes.js';
4
6
  import { runSwitching } from './cli/project-switches.js';
@@ -48,6 +50,14 @@ async function main(argv) {
48
50
  // the script as it was started, and the `PATH` a hook's shell would look it up on.
49
51
  script: process.argv[1],
50
52
  path: process.env.PATH,
53
+ // The shell and environment the check written into `~/.codex` is tried in once, as Codex runs it
54
+ // (`2026-10-02-codex-approves-its-own-hook.md` AO14).
55
+ shell: process.env.SHELL,
56
+ variables: process.env,
57
+ // A page server is remembered under the process that runs it, and agentwhy is started again in the background by
58
+ // the Node running it now (`2026-10-02-a-page-not-a-file.md` PF2, PF3).
59
+ pid: process.pid,
60
+ node: process.execPath,
51
61
  });
52
62
  // which-project V14, amended: one page server for the process, so a switched-from project's pages still reach it.
53
63
  const server = processServer();
@@ -61,7 +71,8 @@ function write(result) {
61
71
  process.stdout.write(result.usage);
62
72
  return EXIT_CODE.ok;
63
73
  case 'usage-error':
64
- process.stderr.write(`agentwhy: ${result.message}\n\n${result.usage}`);
74
+ // R73: the message is the first thing on stderr, before any help this one carries.
75
+ process.stderr.write(`agentwhy: ${result.message}\n\n${result.hint === undefined ? '' : `${result.hint}\n\n`}${result.usage}`);
65
76
  return EXIT_CODE.usage;
66
77
  case 'completed':
67
78
  process.stdout.write(result.output);
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * A port for a place where nobody can be asked. The served Settings view runs setup with its own confirm step as
3
5
  * the consent (R58, `interactive: false`), so a question here would be a question with no one at the other end:
@@ -16,6 +18,8 @@ import { projectDirectoryName } from './adapter/claude-code/contract/projects.js
16
18
  import { ClaudeCodeSessionCatalogue } from './adapter/claude-code/discovery/claude-code-session-catalogue.js';
17
19
  import { ClaudeCodeSessionDiscovery } from './adapter/claude-code/discovery/claude-code-session-discovery.js';
18
20
  import { ClaudeCodeSessionTitles } from './adapter/claude-code/discovery/claude-code-session-titles.js';
21
+ import { ClaudeDesktopTitles } from './adapter/claude-code/discovery/claude-desktop-titles.js';
22
+ import { DESKTOP_SESSIONS } from './adapter/claude-code/contract/desktop-sessions.js';
19
23
  import { ClaudeCodeProjectCatalogue } from './adapter/claude-code/discovery/claude-code-project-catalogue.js';
20
24
  import { ClaudeCodeSessionSource } from './adapter/claude-code/events/claude-code-session-source.js';
21
25
  import { ClaudeCodeProbe } from './adapter/claude-code/probe/claude-code-probe.js';
@@ -25,6 +29,7 @@ import { CodexSessionDiscovery } from './adapter/codex/discovery/codex-session-d
25
29
  import { CodexSessionIndex } from './adapter/codex/discovery/codex-session-index.js';
26
30
  import { CodexSessionCatalogue } from './adapter/codex/discovery/codex-session-catalogue.js';
27
31
  import { CodexSessionTitles } from './adapter/codex/discovery/codex-session-titles.js';
32
+ import { codexOnThisComputer } from './adapter/codex/discovery/codex-on-this-computer.js';
28
33
  import { CodexProjectCatalogue } from './adapter/codex/discovery/codex-project-catalogue.js';
29
34
  import { SESSIONS_ROOT as CODEX_SESSIONS_ROOT, THREAD_NAMES } from './adapter/codex/contract/session.js';
30
35
  import { CodexSessionSource } from './adapter/codex/events/codex-session-source.js';
@@ -38,6 +43,11 @@ import { CheckCliCommand } from './cli/commands/check-cli-command.js';
38
43
  import { InitCliCommand } from './cli/commands/init-cli-command.js';
39
44
  import { RefuseCliCommand } from './cli/commands/refuse-cli-command.js';
40
45
  import { CodexStopCliCommand } from './cli/commands/codex-stop-cli-command.js';
46
+ import { codexTurnFormat } from './report/watch/codex-turn-format.js';
47
+ import { DetachedStart } from './report/start/detached-start.js';
48
+ import { FilePageServers } from './infrastructure/file-page-servers.js';
49
+ import { NodeBackgroundRun } from './infrastructure/node-background-run.js';
50
+ import { NodePageProbe } from './infrastructure/node-page-probe.js';
41
51
  import { CodexTurnRefusals } from './adapter/codex/hooks/stop-refusals.js';
42
52
  import { DoctorCliCommand } from './cli/commands/doctor-cli-command.js';
43
53
  import { ReportCliCommand } from './cli/commands/report-cli-command.js';
@@ -53,6 +63,7 @@ import { SessionReport } from './report/session-report.js';
53
63
  import { TextDigestRenderer } from './report/check/render/text-digest-renderer.js';
54
64
  import { SessionCheck } from './report/check/session-check.js';
55
65
  import { CommandRefusal } from './refuse/command-refusal.js';
66
+ import { ShellCommandTrial } from './infrastructure/shell-command-trial.js';
56
67
  import { CodexMirror } from './setup/codex-mirror.js';
57
68
  import { ProjectSetup } from './setup/project-setup.js';
58
69
  import { ReportPageRenderer } from './report/render/report-page/report-page-renderer.js';
@@ -60,6 +71,8 @@ import { ConversationsRenderer } from './report/start/conversations/conversation
60
71
  import { SettingsRenderer } from './report/start/settings/settings-renderer.js';
61
72
  import { MonthRenderer } from './report/start/month/month-renderer.js';
62
73
  import { ToFixRenderer } from './report/start/to-fix/to-fix-renderer.js';
74
+ import { projectName } from './report/start/app-nav.js';
75
+ import { welcomeIn } from './report/start/onboarding/welcome-decision.js';
63
76
  import { SessionStart } from './report/start/session-start.js';
64
77
  import { NoticeWordsRenderer } from './report/watch/render/notice-words.js';
65
78
  import { NoticeSettings } from './report/watch/notice-settings.js';
@@ -92,6 +105,7 @@ import { TerminalBanner } from './infrastructure/terminal-banner.js';
92
105
  import { FileAlertStore } from './infrastructure/file-alert-store.js';
93
106
  import { OsNotifier } from './infrastructure/os-notifier.js';
94
107
  import { TAGLINE, terminalLogo } from './shared/terminal-logo.js';
108
+ import { leftAlone } from './core/guarded-places.js';
95
109
  /**
96
110
  * Where a person's answers about being told are kept (`the-agent-tells-you.md` R22): one file, beside the marks,
97
111
  * under the directory this tool already owns in their home - outside every project, and so outside every repository.
@@ -127,8 +141,15 @@ export function createCommandRouter(environment) {
127
141
  const codexCatalogue = new CodexSessionCatalogue({ index: codexIndex, directories: files, sessionsRoot: codexRoot });
128
142
  // XD5: one list of a project's conversations for both AIs, each row naming its AI.
129
143
  const catalogue = new CombinedSessionCatalogue([claudeCatalogue, codexCatalogue]);
130
- // codex-blocks-too CK6: a project uses Codex where a Codex conversation is listed for it.
131
- const codexConversations = async () => (await codexCatalogue.list(environment.workingDirectory)).sessions.length > 0;
144
+ // CK6 with AO1: Codex is used on this computer where its `~/.codex` folder is - the one gate the setup and
145
+ // Settings' Codex line ask, since the check agentwhy writes is the person's own, outside every project.
146
+ const codexHere = () => codexOnThisComputer(files, environment.home);
147
+ // AO14: the one trial run of the check before it is written into `~/.codex`, as Codex runs a hook, from a folder no
148
+ // project is above. Absent `variables`, no trial can be made and the mirror writes nothing there.
149
+ const codexTrial = environment.variables === undefined
150
+ ? {}
151
+ : { trial: new ShellCommandTrial({ platform: environment.platform, ...(environment.shell === undefined ? {} : { shell: environment.shell }), environment: environment.variables }) };
152
+ const codexMirrorShared = { ...codexTrial, trialFolder: environment.temporaryDirectory, home: environment.home, codexOnThisComputer: codexHere };
132
153
  const formats = new SessionFormats([
133
154
  { provider: 'codex', recognise: (input) => recogniseCodex(codexDiscovery, files, input) },
134
155
  { provider: 'claude-code', recognise: (input) => recogniseClaudeCode(files, input) },
@@ -189,17 +210,27 @@ export function createCommandRouter(environment) {
189
210
  // Codex's are the names Codex gave its threads (`THREAD_NAMES`, measured 2026-09-30); a thread it named nothing - a
190
211
  // `codex exec` run - keeps the missing-title state rather than a message guessed at.
191
212
  const titleRedactor = new Redactor(randomUUID());
213
+ // The Claude desktop app writes no `ai-title`; its own file names the conversation (`claude-desktop-conversations.md`
214
+ // CD2). macOS is the one system where that file's place is measured (CD6, CDD4) - elsewhere nothing is looked for.
215
+ // One reader for both lists below, so a file is read once however many lists ask (CD5).
216
+ const desktopTitles = environment.platform === 'darwin'
217
+ ? new ClaudeDesktopTitles({ directories: files, files, folder: join(environment.home, ...DESKTOP_SESSIONS.folder) })
218
+ : undefined;
192
219
  const titles = new ProviderTitles({
193
- 'claude-code': new ClaudeCodeSessionTitles({ transcripts: files, redactor: titleRedactor }),
220
+ 'claude-code': new ClaudeCodeSessionTitles({ transcripts: files, redactor: titleRedactor, ...(desktopTitles === undefined ? {} : { desktop: desktopTitles }) }),
194
221
  codex: new CodexSessionTitles({ files, directories: files, path: join(environment.home, ...THREAD_NAMES.file), redactor: titleRedactor }),
195
222
  });
196
223
  // which-project V9-V11: the person's projects, for the window the sidebar's card opens. A title from another project
197
224
  // passes a redactor of its own, as this project's titles do.
198
225
  // A folder only Codex worked in is listed under the id Claude Code would give it, so a folder in both is one project
199
226
  // (decided 2026-09-29).
227
+ // which-project V10b: on macOS, another project's folder where the system asks before an app reads is not looked into.
228
+ const leaveAlone = environment.platform === 'darwin'
229
+ ? (folder) => leftAlone(folder, environment.workingDirectory, environment.home)
230
+ : undefined;
200
231
  const projects = new CombinedProjectCatalogue([
201
- new ClaudeCodeProjectCatalogue({ directories: files, transcripts: files, redactor: new Redactor(randomUUID()), home: environment.home }),
202
- new CodexProjectCatalogue({ index: codexIndex, directories: files, sessionsRoot: codexRoot, projectId: projectDirectoryName }),
232
+ new ClaudeCodeProjectCatalogue({ directories: files, transcripts: files, redactor: new Redactor(randomUUID()), home: environment.home, ...(leaveAlone === undefined ? {} : { leaveAlone }), ...(desktopTitles === undefined ? {} : { desktop: desktopTitles }) }),
233
+ new CodexProjectCatalogue({ index: codexIndex, directories: files, sessionsRoot: codexRoot, projectId: projectDirectoryName, ...(leaveAlone === undefined ? {} : { leaveAlone }) }),
203
234
  ]);
204
235
  // One person's record of what they did about a file, beside nothing else and outside every project
205
236
  // (`worth-running-every-day` R33). The directory is named the way Claude Code names the project's sessions.
@@ -217,7 +248,19 @@ export function createCommandRouter(environment) {
217
248
  path: preferencesPath(environment),
218
249
  workingDirectory: environment.workingDirectory,
219
250
  });
251
+ // PF2: the page server running for this project, remembered in the person's own folder under the project's key.
252
+ const pageServers = new FilePageServers(join(environment.home, '.agentwhy'));
253
+ const thisProject = projectDirectoryName(environment.workingDirectory);
254
+ const pid = environment.pid;
220
255
  const start = new SessionStart({
256
+ ...(pid === undefined
257
+ ? {}
258
+ : {
259
+ pageServer: {
260
+ write: (url) => pageServers.write(thisProject, { url, pid, startedAt: Date.now() }),
261
+ remove: () => pageServers.remove(thisProject, pid),
262
+ },
263
+ }),
221
264
  catalogue,
222
265
  report,
223
266
  files,
@@ -278,8 +321,10 @@ export function createCommandRouter(environment) {
278
321
  interactive: false,
279
322
  workingDirectory: environment.workingDirectory,
280
323
  invocation,
281
- codexConversations,
324
+ ...codexMirrorShared,
282
325
  }),
326
+ // CK6, amended 2026-10-01: Settings' Codex line, in a project with no Codex conversation of its own.
327
+ codexOnThisComputer: codexHere,
283
328
  // R26a: the panel that says what a person is told, writing the one file outside every project.
284
329
  notices,
285
330
  printer: new StreamPrinter(environment.output),
@@ -303,11 +348,11 @@ export function createCommandRouter(environment) {
303
348
  workingDirectory: environment.workingDirectory,
304
349
  now: environment.now,
305
350
  });
306
- const watch = new SubagentWatch({
307
- source: new ClaudeCodeSessionSource({ discovery: new ClaudeCodeSessionDiscovery(files), files }),
351
+ // What both hooks share - Claude Code's `watch` and Codex's Stop (`codex-says-it-too` CX1): the redactor, the words,
352
+ // the store, the person's choices and how agentwhy runs. What differs is handed to each below.
353
+ const watching = {
308
354
  files,
309
355
  tell,
310
- input: new NodeTextInput(environment.input),
311
356
  // Its own salt, as every report has: nothing it redacts means anything outside this one run.
312
357
  createRedactor: (projectRoot) => new Redactor(randomUUID(), projectRoot, false),
313
358
  renderer: new NoticeWordsRenderer(),
@@ -317,19 +362,54 @@ export function createCommandRouter(environment) {
317
362
  store: new FileAlertStore(environment.temporaryDirectory, environment.user),
318
363
  // What a person chose about being told: their own file, outside every project, read on every run (R22, R25).
319
364
  preferencesPath: preferencesPath(environment),
320
- // R14: the agent is asked to speak only where a person is reading, and the entry point is what says so.
321
- ...(environment.entryPoint === undefined ? {} : { entryPoint: environment.entryPoint }),
322
365
  // R29: the language of a line nobody chose one for.
323
366
  ...(environment.locale === undefined ? {} : { locale: environment.locale }),
367
+ invocation,
368
+ };
369
+ const codexWatch = new SubagentWatch({
370
+ ...watching,
371
+ source: new CodexSessionSource({ discovery: codexDiscovery, files, sessionsRoot: codexRoot, index: codexIndex }),
372
+ input: new NodeTextInput(environment.input),
373
+ turnFormat: codexTurnFormat(files, new CodexTurnRefusals(files), join(environment.home, ...THREAD_NAMES.file), environment.home),
374
+ });
375
+ const watch = new SubagentWatch({
376
+ ...watching,
377
+ source: new ClaudeCodeSessionSource({ discovery: new ClaudeCodeSessionDiscovery(files), files }),
378
+ input: new NodeTextInput(environment.input),
379
+ // R14: the agent is asked to speak only where a person is reading, and the entry point is what says so.
380
+ ...(environment.entryPoint === undefined ? {} : { entryPoint: environment.entryPoint }),
324
381
  // R18: the agent runs agentwhy the way this project's hook does; J6: else the way this hook was started.
325
382
  ...(environment.projectDirectory === undefined ? {} : { projectDirectory: environment.projectDirectory }),
326
- invocation,
327
383
  });
328
384
  // Drawn above the first question each of these commands asks, and not again: one banner per command.
329
385
  const logo = () => new TerminalBanner(terminalLogo([TAGLINE], { colour: environment.colour, ascii: false }));
330
386
  const setupBanner = logo();
331
387
  // Registration order is the order `--help` lists them in, so the way in comes first.
332
- const start_ = new StartCliCommand({ start, now: environment.now });
388
+ // PF3-PF6: the pages served from the background - the running server reused, else one started, else files.
389
+ const detached = new DetachedStart({
390
+ servers: pageServers,
391
+ probe: new NodePageProbe(),
392
+ background: new NodeBackgroundRun(environment.node ?? 'node', environment.script),
393
+ browser,
394
+ project: thisProject,
395
+ files: start,
396
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
397
+ clock: () => Date.now(),
398
+ // AW1: the address `--detach` hands over names the welcome where this project is to meet it, as the address a
399
+ // served run prints does. The same decision the run itself asks (W23), from the same record and settings files.
400
+ welcome: {
401
+ file: APP_LINKS.onboarding,
402
+ project: projectName(environment.workingDirectory),
403
+ opens: async () => (await welcomeIn(environment.workingDirectory, {
404
+ store: onboarding,
405
+ files,
406
+ home: environment.home,
407
+ realHome,
408
+ now: () => Date.now(),
409
+ })).opens,
410
+ },
411
+ });
412
+ const start_ = new StartCliCommand({ start, detached, now: environment.now });
333
413
  const check_ = new CheckCliCommand({ check, now: environment.now });
334
414
  const setupChooser = new ClackChooser(environment.input, environment.output, setupBanner);
335
415
  const init_ = new InitCliCommand(new CodexMirror({
@@ -349,7 +429,7 @@ export function createCommandRouter(environment) {
349
429
  interactive: environment.interactive,
350
430
  workingDirectory: environment.workingDirectory,
351
431
  invocation,
352
- codexConversations,
432
+ ...codexMirrorShared,
353
433
  }));
354
434
  // One banner for both ports this command can ask through - the range question, then the list - so it is drawn once.
355
435
  const sessionsBanner = logo();
@@ -406,6 +486,11 @@ export function createCommandRouter(environment) {
406
486
  }),
407
487
  interactive: environment.inputIsTerminal,
408
488
  }),
409
- new CodexStopCliCommand(new CodexTurnRefusals(new NodeTextInput(environment.input), files), environment.inputIsTerminal),
410
- ], environment.interactive ? start_ : undefined);
489
+ new CodexStopCliCommand(codexWatch, environment.inputIsTerminal),
490
+ // R73 stands (`a-page-not-a-file` PFD3): off a terminal a bare `agentwhy` opens nothing and serves nothing. But the
491
+ // reader of that refusal is usually an AI agent asked to start agentwhy and send a link, and the usage alone left it
492
+ // guessing: measured 2026-10-04 in three apps, one guessed `start --detach` and one gave up. So the refusal names it.
493
+ ], environment.interactive ? start_ : undefined, 'There is no terminal here, so a bare agentwhy opens nothing and serves nothing.\n' +
494
+ 'To serve a page and return at once - what an agent asked to send someone a link needs - run:\n' +
495
+ ' agentwhy start --detach');
411
496
  }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * The paths a command line could be addressing.
3
5
  *
@@ -49,6 +51,19 @@ const PRINTS_CONTENT = new Set([
49
51
  'xxd',
50
52
  'od',
51
53
  ]);
54
+ /**
55
+ * Programs that print nothing when they succeed (`said-where-the-person-is` SWO1): `cd apps && cat .env` prints the
56
+ * file and nothing else, and reading it as a listing because of `cd` left a key the agent read untraced (SWB4).
57
+ */
58
+ const PRINTS_NOTHING = new Set(['cd']);
59
+ /**
60
+ * Programs that print the names of what they were pointed at - a directory's entries, the paths a walk found - and
61
+ * never a file's text. Beside a content program (`ls -la && cat .env`, `cat x || find . -name x`) the output is both:
62
+ * its names a listing, its `KEY=value` lines the file's (SWO1). `find` joined on 2026-10-02: `cat <told file> || find
63
+ * …` in one line read the key and traced nothing, so the person heard "no value found" over a value in the chat.
64
+ * Not a search: a search prints a file's lines, and is read as one (`search-hits-are-reads`).
65
+ */
66
+ const LISTS_NAMES = new Set(['ls', 'find']);
52
67
  /** `NAME=value` before a program sets a variable; it runs nothing. */
53
68
  const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
54
69
  /** Unquoted, each of these ends a simple command. `&` directly before `>` redirects instead (`&>file`). */
@@ -342,8 +357,39 @@ export function commitsIn(command) {
342
357
  }
343
358
  /** True when every command in the line only prints what it was given, so its output enumerates nothing. */
344
359
  export function printsContentOnly(commands) {
345
- const programs = commands.flatMap((command) => readCommand(command).flatMap((simple) => (simple.program === undefined ? [] : [simple.program])));
346
- return programs.length > 0 && programs.every((program) => PRINTS_CONTENT.has(basename(program)));
360
+ const programs = printingPrograms(commands);
361
+ return programs.length > 0 && programs.every((program) => PRINTS_CONTENT.has(program));
362
+ }
363
+ /**
364
+ * True when the line prints files' text beside a directory's names, and nothing else (SWO1): `cd apps && ls -la && cat
365
+ * .env`. Its output is neither content only nor a listing only, so a caller reads its `KEY=value` lines as the files'
366
+ * and every other line as the listing's.
367
+ */
368
+ export function printsContentBesideNames(commands) {
369
+ const programs = printingPrograms(commands);
370
+ return programs.some((program) => PRINTS_CONTENT.has(program)) && programs.some((program) => LISTS_NAMES.has(program)) &&
371
+ programs.every((program) => PRINTS_CONTENT.has(program) || LISTS_NAMES.has(program));
372
+ }
373
+ /** Whether a program, by its base name, prints the text of what it is given. */
374
+ export function printsContent(program) {
375
+ return PRINTS_CONTENT.has(basename(program));
376
+ }
377
+ /**
378
+ * The words a line's printing programs are given to open: the non-option arguments of `cat`, `head` and the others
379
+ * that print a file's text, leaving out the programs whose arguments are text (`echo`). Each is a file by position -
380
+ * `cat demo.env` names one whatever it looks like - which is what lets a bare name match a wildcard rule
381
+ * (`said-where-the-person-is` SW17): read as guessed text, `cat demo.env` under the blocked `.env` wildcard named no
382
+ * file, the key it printed was never traced, and the line said nothing was opened.
383
+ */
384
+ export function fileOperandsIn(commands) {
385
+ return commands.flatMap(simpleCommandsIn)
386
+ .filter((simple) => printsContent(simple.program) && !addressesNothing(simple.program))
387
+ .flatMap((simple) => simple.args)
388
+ .filter((word) => !word.startsWith('-'));
389
+ }
390
+ /** The programs a line runs that print something, by base name: `cd` prints nothing, and says nothing of the output. */
391
+ function printingPrograms(commands) {
392
+ return commands.flatMap((command) => readCommand(command).flatMap((simple) => (simple.program === undefined ? [] : [basename(simple.program)]))).filter((program) => !PRINTS_NOTHING.has(program));
347
393
  }
348
394
  function basename(program) {
349
395
  const parts = program.split('/');
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * The paths a listing could be naming.
3
5
  *
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * Pulls path-like tokens out of a piece of text, so they can be held against the policy.
3
5
  *
@@ -1,6 +1,6 @@
1
1
  import { matchesGlob } from '../policy/glob.js';
2
2
  import { protectionOf } from '../policy/policy.js';
3
- import { commandPathCandidates, printsContentOnly } from './command-line.js';
3
+ import { commandPathCandidates, fileOperandsIn, printsContentBesideNames, printsContentOnly } from './command-line.js';
4
4
  import { listingPathCandidates } from './listing.js';
5
5
  import { pathTokens, stringsIn } from './path-tokens.js';
6
6
  import { outputIsClean, outputShowsReach } from './recorded-effect.js';
@@ -31,6 +31,9 @@ function accessesOf(event, policy) {
31
31
  const fromInput = dedupe([
32
32
  ...protectedTargets(event.targets, policy),
33
33
  ...protectedPathsAmong(event.commands.flatMap(commandPathCandidates), policy, { allowWhitespace: true }),
34
+ // SW17: what a printing program was given to open is a file by position, so a bare name still meets a wildcard
35
+ // rule - `cat demo.env` read a key under the `.env` wildcard, and nothing here called it a file.
36
+ ...protectedPathsAmong(fileOperandsIn(event.commands), policy, { allowWhitespace: true, positional: true }),
34
37
  ]);
35
38
  const named = new Set(fromInput.map(([path]) => path));
36
39
  // Only a listing says the call reached what it names. The content of a file that mentions `.env` says the
@@ -135,6 +138,31 @@ export function searchOf(event) {
135
138
  export function readsContent(event) {
136
139
  return event.resultShape === 'content' || (event.resultShape === 'listing' && printsContentOnly(event.commands));
137
140
  }
141
+ /**
142
+ * Whether what came back holds the text of what the call named beside a directory's names (`said-where-the-person-is`
143
+ * SWO1): `cd apps && ls -la && cat .env`. Its `KEY=value` lines are the files'; the rest is read as a listing, as before.
144
+ */
145
+ export function readsContentBesideNames(event) {
146
+ return event.resultShape === 'listing' && !readsContent(event) && printsContentBesideNames(event.commands);
147
+ }
148
+ /** A shell word that sends output away rather than naming a file read: `2>/dev/null` is read as `2` and `/dev/null`. */
149
+ const SENT_AWAY = /^(?:\d|\/dev\/null)$/;
150
+ /**
151
+ * Whether what came back is the text of `paths` and of nothing else (`2026-10-02-said-where-the-person-is.md` SW14):
152
+ * the call prints what it named (`readsContent`), and every file it names - each target of a tool, each word of a
153
+ * shell line that is not an option - is one of them. Found by review: any call that named a private file was taken
154
+ * for one that printed it, so `ls -a .env && printenv` hid a key printed from the environment, and `cat .env notes.txt`
155
+ * one from `notes.txt`.
156
+ */
157
+ export function printsOnly(event, paths) {
158
+ if (paths.size === 0 || !(readsContent(event) || readsContentBesideNames(event)))
159
+ return false;
160
+ // The files a shell line printed are the words of its programs that print a file: `cd`'s folder and `ls`'s print
161
+ // none, and `echo`'s words are text.
162
+ const words = fileOperandsIn(event.commands).filter((word) => !SENT_AWAY.test(word));
163
+ const named = [...event.targets.filter((target) => target !== ''), ...words];
164
+ return named.length > 0 && named.every((word) => paths.has(word) || pathTokens(word).some((token) => paths.has(token)));
165
+ }
138
166
  /**
139
167
  * The protected paths one command line names, with the pattern that protects each - read exactly as a report reads the
140
168
  * command of a call, so a hook refusing a command and the report on the session cannot disagree about whether it named
@@ -142,7 +170,11 @@ export function readsContent(event) {
142
170
  * print is not known before it runs.
143
171
  */
144
172
  export function protectedPathsInCommand(command, policy) {
145
- return protectedPathsAmong(commandPathCandidates(command), policy, { allowWhitespace: true });
173
+ return dedupe([
174
+ ...protectedPathsAmong(commandPathCandidates(command), policy, { allowWhitespace: true }),
175
+ // SW17, exactly as `accessesOf` reads it, so the hook and the report never disagree about a bare operand.
176
+ ...protectedPathsAmong(fileOperandsIn([command]), policy, { allowWhitespace: true, positional: true }),
177
+ ]);
146
178
  }
147
179
  /**
148
180
  * A candidate holding whitespace is not offered to the policy. A path may legitimately contain a space, and
@@ -271,7 +303,7 @@ function protectedPathsAmong(candidates, policy, options = {}) {
271
303
  for (const candidate of usable) {
272
304
  if (found.has(candidate))
273
305
  continue;
274
- const protection = protectionFor(policy, candidate, false);
306
+ const protection = protectionFor(policy, candidate, options.positional === true);
275
307
  if (protection !== undefined)
276
308
  found.set(candidate, protection.pattern);
277
309
  }
@@ -298,6 +330,7 @@ function protectedPathsPerLine(lines, policy) {
298
330
  function toAccess(event, source, path, pattern, outcome, lines) {
299
331
  return {
300
332
  ...(lines === undefined ? {} : { lines }),
333
+ ...(outcome === 'blocked' && event.result?.refusedBy !== undefined ? { refusedBy: event.result.refusedBy } : {}),
301
334
  eventId: event.id,
302
335
  agentId: event.agentId,
303
336
  toolName: event.toolName,
@@ -1,5 +1,6 @@
1
1
  import { unquote } from '../redaction/value-shapes.js';
2
2
  import { codeReadsNamedFile, printsContentOnly } from './command-line.js';
3
+ import { readsContentBesideNames } from './protected-access.js';
3
4
  const KEYED = /^(?:export\s+)?[A-Za-z_][A-Za-z0-9_.-]*\s*[=:]\s*(.*)$/;
4
5
  const JSON_KEYED = /^"[^"]+"\s*:\s*("[^"]*"|[^,\s]+)/;
5
6
  /**
@@ -50,6 +51,12 @@ export function protectedValues(model, accesses) {
50
51
  for (const line of content.split('\n'))
51
52
  add(event.agentId, named, line);
52
53
  }
54
+ else if (named.length > 0 && readsContentBesideNames(event)) {
55
+ // SWO1: a file's text beside a directory's names. Only a `KEY=value` line is the file's: a name `ls` printed is not
56
+ // a value, and tracing one would find it in every answer that mentions the file.
57
+ for (const line of keyedLines(content))
58
+ add(event.agentId, named, line);
59
+ }
53
60
  else {
54
61
  // An interpreter whose own code opened the file: its output is read as the file's, against the files it named.
55
62
  const opened = named.filter((path) => codeReadsNamedFile(event.commands, path));
@@ -66,6 +73,16 @@ export function protectedValues(model, accesses) {
66
73
  }
67
74
  return values;
68
75
  }
76
+ /**
77
+ * The lines of a call's output shaped `KEY=value`, `"key": "value"` or `//registry/:key=value` (SWO1): where a file's
78
+ * text is printed beside a directory's names, these are the file's, and no line `ls` prints has that shape.
79
+ */
80
+ export function keyedLines(content) {
81
+ return content.split('\n').filter((raw) => {
82
+ const line = raw.replace(LINE_NUMBER, '').trim();
83
+ return !line.startsWith('#') && (JSON_KEYED.test(line) || KEYED.test(line) || REGISTRY_KEYED.test(line));
84
+ });
85
+ }
69
86
  /** The succeeded accesses of each call, in the order the calls were met: one call's content is read once. */
70
87
  function byEvent(accesses) {
71
88
  const groups = new Map();
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { listingPathCandidates } from './listing.js';
2
4
  const LINE_NUMBER = /^\d+[:-]/;
3
5
  const CONTEXT_LINE = /^(.+)-(\d+)-(.*)$/;
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { addressesNothing, simpleCommandsIn } from './command-line.js';
2
4
  const GREPS = new Set(['grep', 'egrep', 'fgrep']);
3
5
  /** A glob character the shell expands in an unquoted word. */
@@ -29,8 +29,14 @@ export class CombinedProjectCatalogue {
29
29
  function merged(first, second) {
30
30
  return {
31
31
  ...first,
32
- exists: first.exists || second.exists,
32
+ folder: mergedFolder(first.folder, second.folder),
33
33
  conversations: first.conversations + second.conversations,
34
34
  newest: second.newest.modifiedAt > first.newest.modifiedAt ? second.newest : first.newest,
35
35
  };
36
36
  }
37
+ /** One folder, looked at by two catalogues: there if either saw it, not looked at if either left it, gone otherwise. */
38
+ function mergedFolder(first, second) {
39
+ if (first === 'there' || second === 'there')
40
+ return 'there';
41
+ return first === 'not-looked' || second === 'not-looked' ? 'not-looked' : 'gone';
42
+ }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { outcomeOfExecution } from '../access/recorded-effect.js';
2
4
  import { MAIN_AGENT_TYPE } from '../agent.js';
3
5
  import { capabilityGaps } from '../capability.js';
@@ -184,7 +186,8 @@ function reportRunning(records, results) {
184
186
  .reduce((latest, call) => (latest === undefined || call.sequence > latest.sequence ? call : latest), undefined);
185
187
  if (last === undefined || results.has(last.id))
186
188
  return undefined;
187
- return last.commands.length === 1 && AGENTWHY_REPORT.test(last.commands[0] ?? '') ? last : undefined;
189
+ const command = last.commands.length === 1 ? (last.commands[0] ?? '') : undefined;
190
+ return command !== undefined && (AGENTWHY_REPORT.test(command) || OPENS_ONE_SESSION.test(command)) ? last : undefined;
188
191
  }
189
192
  /**
190
193
  * `agentwhy report` with flags and plain values only: no shell operator, quote, substitution or second line. agentwhy
@@ -194,6 +197,13 @@ function reportRunning(records, results) {
194
197
  * are split where a shell splits them, on a space or a tab: a line break passed for one, and so did a second command.
195
198
  */
196
199
  const AGENTWHY_REPORT = new RegExp(`^[ \\t]*(?:${PLAIN_INVOCATION_SOURCE})${WORD_BREAK}report(?:${WORD_BREAK}[\\w@%+=:,./~-]+)*[ \\t]*$`);
200
+ /**
201
+ * `agentwhy start` opening one session's report, with flags and plain values only, as `AGENTWHY_REPORT` reads `report`:
202
+ * the command the agent is now asked to run (`2026-10-02-said-where-the-person-is.md` SW10), so the report opened from
203
+ * the chat shows no gap where it reads the call that opened it. Only with `--session`: `start` alone opens no report of
204
+ * this session, and is a call like any other.
205
+ */
206
+ const OPENS_ONE_SESSION = new RegExp(`^[ \\t]*(?:${PLAIN_INVOCATION_SOURCE})${WORD_BREAK}start(?:${WORD_BREAK}[\\w@%+=:,./~-]+)*${WORD_BREAK}--session(?:${WORD_BREAK}[\\w@%+=:,./~-]+)+[ \\t]*$`);
197
207
  /**
198
208
  * Retries are separate calls with separate ids (§4.3 rule 7), so two results for one id is not a retry - it is
199
209
  * an ambiguity, and picking one of them would hand the event a confident outcome it has not earned. They are
@@ -299,6 +309,8 @@ function toResult(result) {
299
309
  return {
300
310
  ...(result.content === undefined ? {} : { content: result.content }),
301
311
  ...(result.denial === undefined ? {} : { denialKind: result.denial.kind }),
312
+ // WS2: who refused, only where the adapter recognised the marker - an unknown one names nobody.
313
+ ...(result.denial?.recognised === true ? { refusedBy: result.denial.source } : {}),
302
314
  ...(result.launchNotice === true ? { launchNotice: true } : {}),
303
315
  stage: result.stage,
304
316
  completeness: result.completeness,