@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,14 +1,13 @@
1
- import { isAbsolute, join, dirname } from 'node:path';
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { isAbsolute, join } from 'node:path';
2
4
  import { parsePreToolUseInput } from '../adapter/claude-code/hooks/pre-tool-use-input.js';
3
- import { PROJECT_HOOKS } from '../adapter/codex/contract/hooks.js';
5
+ import { refuseProjectAbove, refuseRulesOf } from '../adapter/claude-code/settings/refuse-project.js';
4
6
  import { parseCodexPreToolUseInput } from '../adapter/codex/hooks/pre-tool-use-input.js';
5
7
  import { protectedPathsInCommand } from '../core/access/protected-access.js';
6
- import { FileAccessError } from '../ports/file-access-error.js';
7
8
  import { choosePolicy } from '../report/choose-policy.js';
8
9
  import { blockingOnly } from '../core/policy/policy.js';
9
10
  import { protectedFileReached } from './command-reach.js';
10
- /** How many folders above the session's the project is looked for in: deeper than any project, short of a loop. */
11
- const MAX_PROJECT_DEPTH = 64;
12
11
  /** A command line is a few kilobytes; past this the input is not held at all, and the command is not checked. */
13
12
  const MAX_INPUT_BYTES = 1024 * 1024;
14
13
  /**
@@ -32,6 +31,10 @@ export class CommandRefusal {
32
31
  if ('anotherTool' in parsed)
33
32
  return { kind: 'allow' };
34
33
  const settled = codex ? await this.#inCodexProject(choice, parsed.cwd ?? this.#dependencies.workingDirectory) : choice;
34
+ // AO6: the computer-wide check runs in every Codex session; where no project above runs `refuse`, there are no
35
+ // rules anybody chose here, and the command runs with no message.
36
+ if (settled === 'no-project')
37
+ return { kind: 'allow' };
35
38
  if (settled === undefined)
36
39
  return { kind: 'not-checked', reason: 'project' };
37
40
  const chosen = await choosePolicy(settled, this.#dependencies.files, this.#dependencies.tell);
@@ -55,34 +58,20 @@ export class CommandRefusal {
55
58
  return { kind: 'refuse', path: reach.path, pattern: reach.pattern, others: 0, route };
56
59
  }
57
60
  /**
58
- * CK3: Codex runs its hook in the session's folder, which can lie below the project, and names the project nowhere
59
- * (CKB6). A relative settings path is read in the project: the nearest folder, at or above `folder`, that holds the
60
- * `.codex/hooks.json` this hook is written in. None found is `undefined`: the command runs, and is said to be unchecked.
61
+ * CK3, AO5, AO6: Codex runs its check in the session's folder, which can lie below the project, and names the
62
+ * project nowhere (CKB6). The project is the nearest folder at or above `folder`, short of the home directory, whose
63
+ * Claude Code settings run `refuse`. With no flag of its own, the check reads the rules that project's `refuse`
64
+ * names, and with no project found it is `'no-project'`: the command runs, silently. A relative `--settings`, as an
65
+ * older project-level entry wrote it, resolves against the same project; none found is `undefined`, and the command
66
+ * runs said to be unchecked (R20). An absolute path or a policy file is read as given.
61
67
  */
62
68
  async #inCodexProject(choice, folder) {
63
69
  const relative = choice.settingsPath;
64
- if (relative === undefined || isAbsolute(relative))
70
+ if (choice.policyPath !== undefined || (relative !== undefined && isAbsolute(relative)))
65
71
  return choice;
66
- for (let at = folder, depth = 0; depth < MAX_PROJECT_DEPTH; depth += 1) {
67
- if (await this.#holds(join(at, PROJECT_HOOKS.directory, PROJECT_HOOKS.file)))
68
- return { ...choice, settingsPath: join(at, relative) };
69
- const up = dirname(at);
70
- if (up === at)
71
- return undefined;
72
- at = up;
73
- }
74
- return undefined;
75
- }
76
- /** Whether a file is there to read; a folder or anything unreadable is not. */
77
- async #holds(path) {
78
- try {
79
- await this.#dependencies.files.readText(path);
80
- return true;
81
- }
82
- catch (error) {
83
- if (error instanceof FileAccessError)
84
- return false;
85
- throw error;
86
- }
72
+ const found = await refuseProjectAbove(this.#dependencies.files, folder, this.#dependencies.home);
73
+ if (relative !== undefined)
74
+ return found === undefined ? undefined : { ...choice, settingsPath: join(found.project, relative) };
75
+ return found === undefined ? 'no-project' : refuseRulesOf(found);
87
76
  }
88
77
  }
@@ -1,10 +1,12 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { HOOK_OUTPUT } from '../../adapter/claude-code/contract/hooks.js';
2
4
  const NOT_CHECKED = {
3
5
  arguments: 'agentwhy refuse was given arguments it does not recognise, so this command was not checked.',
4
6
  input: 'agentwhy refuse could not read the hook input, so this command was not checked.',
5
7
  policy: 'agentwhy refuse could not read the policy it was given, so this command was not checked.',
6
8
  search: 'agentwhy refuse could not look through everything this search reaches, so this command was not checked.',
7
- project: 'agentwhy refuse found no .codex/hooks.json in this folder or above it, so it could not read the project\'s rules and this command was not checked.',
9
+ project: 'agentwhy refuse found no project above this folder whose Claude Code settings run refuse, so it could not read the rules it was pointed at and this command was not checked.',
8
10
  };
9
11
  /** Said to the user, as a hook's JSON on stdout, when a command ran without being checked (R20). */
10
12
  export function notCheckedMessage(reason) {
@@ -1,10 +1,15 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { programsIn } from '../core/access/command-line.js';
2
- import { printedLines, protectedAccesses, readsContent } from '../core/access/protected-access.js';
4
+ import { printedLines, printsOnly, protectedAccesses, readsContent, readsContentBesideNames } from '../core/access/protected-access.js';
5
+ import { keyedLines } from '../core/access/protected-values.js';
3
6
  import { filesTracedIn, returnsOf, traceValues } from '../core/access/returns.js';
4
7
  import { valueUses } from '../core/access/uses.js';
5
8
  import { wordsBeforeCalls } from '../core/access/words-before.js';
9
+ import { displayPath } from '../core/project-root.js';
6
10
  import { firstSentence } from '../shared/sentence.js';
7
11
  import { NAMES_PER_FILE } from './report-model.js';
12
+ import { refusedByOthers } from './refusals.js';
8
13
  import { filesRead } from './flow-reads.js';
9
14
  /**
10
15
  * Identifiers are shortened in a report (§7.3). Both ends are kept: a `toolu_01…` prefix is shared by every
@@ -50,7 +55,7 @@ export function buildReport(model, policy, redactor, view = FULL_VIEW) {
50
55
  // F57: which patterns a person asked only to be told about, so a finding under one says the agent was let read it.
51
56
  const told = new Set(policy.protected.filter((entry) => entry.mode === 'tell').map((entry) => entry.pattern));
52
57
  const findings = accesses.map((access) => toFinding(access, model, redactor, view, told.has(access.pattern)));
53
- const secretShapes = secretShapesIn(model, redactor, view);
58
+ const secretShapes = secretShapesIn(model, accesses, redactor, view);
54
59
  const delegations = model.delegations.map((delegation) => toDelegation(delegation, model, accesses, redactor, view));
55
60
  const gaps = gapsOf(model.gaps, returns);
56
61
  const attributed = new Set(model.delegations.map((delegation) => delegation.childAgentId));
@@ -183,7 +188,11 @@ function privateFilesOf(model, accesses, redactor) {
183
188
  readInto(file, lines.join('\n'), redactor);
184
189
  }
185
190
  }
186
- if (paths.length === 0 || content === undefined || event.outcome !== 'succeeded' || !readsContent(event))
191
+ if (paths.length === 0 || content === undefined || event.outcome !== 'succeeded')
192
+ continue;
193
+ // SWO1: a file's text beside a directory's names - its `KEY=value` lines are the file's, the names are not.
194
+ const besideNames = readsContentBesideNames(event);
195
+ if (!besideNames && !readsContent(event))
187
196
  continue;
188
197
  if (paths.length > 1) {
189
198
  for (const path of paths)
@@ -192,13 +201,32 @@ function privateFilesOf(model, accesses, redactor) {
192
201
  }
193
202
  const file = found.get(paths[0]);
194
203
  if (file !== undefined)
195
- readInto(file, content, redactor);
204
+ readInto(file, besideNames ? keyedLines(content).join('\n') : content, redactor);
205
+ }
206
+ /*
207
+ * One file, however its path was written. `ls` prints it relative and the Read tool names it absolute, and both are
208
+ * shown the same: kept apart, the to-do list drew the same file twice (seen by the maintainer on 2026-10-02, a file
209
+ * listed by `ls` and then read). Told apart by where they are, never by how a view shows them: the shared view shows
210
+ * every file outside the project as the same words, and merging by those made several files one (found by review).
211
+ */
212
+ const shown = new Map();
213
+ for (const [path, file] of found) {
214
+ const where = displayPath(path, model.projectRoot);
215
+ const into = shown.get(where) ?? { path: redactor.path(path), keys: new Set(), names: new Map(), keyed: new Map(), mixed: false };
216
+ for (const key of file.keys)
217
+ into.keys.add(key);
218
+ for (const name of file.names)
219
+ into.names.set(name, name);
220
+ for (const line of file.keyed)
221
+ into.keyed.set(JSON.stringify([line.name, line.key]), line);
222
+ into.mixed ||= file.mixed;
223
+ shown.set(where, into);
196
224
  }
197
- return [...found].map(([path, file]) => ({
198
- path: redactor.path(path),
199
- keys: file.keys.map((key) => redactor.term(key)),
200
- names: file.names,
201
- keyed: file.keyed.map((line) => ({ name: line.name, key: redactor.term(line.key) })),
225
+ return [...shown.values()].map((file) => ({
226
+ path: file.path,
227
+ keys: [...file.keys].map((key) => redactor.term(key)),
228
+ names: [...file.names.values()],
229
+ keyed: [...file.keyed.values()].map((line) => ({ name: line.name, key: redactor.term(line.key) })),
202
230
  ...(file.mixed ? { mixed: true } : {}),
203
231
  }));
204
232
  }
@@ -246,7 +274,14 @@ function everydayFilesOf(model, accesses, redactor) {
246
274
  * A recognised key format in what came back - `S-shape` of §5.4. It is reported even when the call touched no
247
275
  * protected path: a secret exported in a shell profile sits in no `.env` file, so path matching is blind to it.
248
276
  */
249
- function secretShapesIn(model, redactor, view) {
277
+ function secretShapesIn(model, accesses, redactor, view) {
278
+ // The private files each call named and reached; its result is their text only where it printed them and nothing else.
279
+ const readPrivate = new Map();
280
+ for (const access of accesses) {
281
+ if (access.source !== 'input' || access.outcome !== 'succeeded')
282
+ continue;
283
+ readPrivate.set(access.eventId, (readPrivate.get(access.eventId) ?? new Set()).add(access.path));
284
+ }
250
285
  return model.events.flatMap((event) => {
251
286
  const content = event.result?.content;
252
287
  if (content === undefined)
@@ -262,6 +297,7 @@ function secretShapesIn(model, redactor, view) {
262
297
  outcome: event.outcome,
263
298
  classes: classes.map((name) => redactor.term(name)),
264
299
  evidence: redactor.term(describeEvidence(event.result?.evidence ?? event.evidence, model, view)),
300
+ ...(printsOnly(event, readPrivate.get(event.id) ?? new Set()) ? { inPrivateFile: true } : {}),
265
301
  },
266
302
  ];
267
303
  });
@@ -314,6 +350,7 @@ function tallyOf(accesses, returns, uses, contentsSeen) {
314
350
  onlyThroughResult,
315
351
  namedByCall: files.size - onlyThroughResult,
316
352
  refusedAttempts: accesses.filter((access) => access.outcome === 'blocked').length,
353
+ ...othersAmong(accesses),
317
354
  unknownAttempts: accesses.filter((access) => access.outcome === 'unknown').length,
318
355
  valuesReturned: returns.filter((entry) => entry.strength === 'value').length,
319
356
  valuesWritten: returns.filter((entry) => entry.strength !== 'value' && entry.writtenFrom.length > 0).length,
@@ -324,6 +361,11 @@ function tallyOf(accesses, returns, uses, contentsSeen) {
324
361
  valueUses: uses.length,
325
362
  };
326
363
  }
364
+ /** WS3: who refused the refused ones among these, where it was not a rule - absent where a rule refused them all. */
365
+ function othersAmong(accesses) {
366
+ const byOthers = refusedByOthers(accesses.filter((access) => access.outcome === 'blocked'));
367
+ return byOthers === undefined ? {} : { refusedByOthers: byOthers };
368
+ }
327
369
  /**
328
370
  * How many agents the contents of a protected file reached, by the same rule `session-view.ts` applies to draw
329
371
  * one: what a delegation returned, what an agent wrote of it, and every step of its flow that carried a value.
@@ -392,6 +434,7 @@ function graphOf(model, accesses, returns, uses, traced, redactor, view) {
392
434
  actions: model.events.filter((event) => event.agentId === agent.id).length,
393
435
  filesReached: files.length,
394
436
  refusedAttempts: own.filter((access) => access.outcome === 'blocked').length,
437
+ ...othersAmong(own),
395
438
  ...(worst === undefined ? {} : { topPath: redactor.path(worst) }),
396
439
  ...returnedOf(returns, agent.id),
397
440
  ...(uses.some((use) => use.agentId === agent.id && isWrittenOnward(use)) ? { wroteOnward: true } : {}),
@@ -490,17 +533,22 @@ function storiesOf(model, accesses, wordsBefore, traced, redactor, view) {
490
533
  const access = group[0];
491
534
  const agent = byAgent.get(access.agentId);
492
535
  const asked = askedOf.get(access.agentId);
536
+ const event = model.events.find((candidate) => candidate.id === access.eventId);
537
+ // S3: read is the page's word - the call printed the file's text, or a search printed its lines.
538
+ const read = access.source === 'input' && access.outcome === 'succeeded' && event !== undefined &&
539
+ (readsContent(event) || readsContentBesideNames(event) || (access.lines ?? 0) > 0);
493
540
  return {
494
541
  ...(agent === undefined ? {} : { agentIndex: model.agents.indexOf(agent) }),
495
542
  path: redactor.path(access.path),
496
543
  source: access.source,
497
544
  outcome: access.outcome,
545
+ ...(read ? { read: true } : {}),
498
546
  who: access.agentId === model.sessionId
499
547
  ? redactor.term('the session itself')
500
548
  : redactor.term(`${agentName(access.agentId, model, view)}${agent?.type === undefined ? '' : ` (${agent.type})`}`),
501
549
  ...(asked === undefined ? {} : { askedTo: redactor.scan(asked) }),
502
550
  did: redactor.term(didOf(access, model)),
503
- toolKnown: model.events.find((event) => event.id === access.eventId)?.toolKnown ?? false,
551
+ toolKnown: event?.toolKnown ?? false,
504
552
  evidence: redactor.term(describeEvidence(access.evidence, model, view)),
505
553
  occurrences: group.length,
506
554
  ...linesTotal(group),
@@ -1,4 +1,5 @@
1
1
  import { byString } from '../../shared/compare.js';
2
+ import { sumRefusedByOthers } from '../refusals.js';
2
3
  /** Merges per-session actions by path and route. Most sessions first, then by name, so the order is stable. */
3
4
  export function digestOf(actions, context) {
4
5
  const rotate = new PathCounts();
@@ -30,6 +31,7 @@ export function digestOf(actions, context) {
30
31
  shapes.set(name, (shapes.get(name) ?? 0) + 1);
31
32
  }
32
33
  const first = actions[0];
34
+ const byOthers = sumRefusedByOthers(actions.map((session) => session.refusedByOthers));
33
35
  return {
34
36
  asked: context.asked,
35
37
  sessionsRead: actions.length,
@@ -43,6 +45,7 @@ export function digestOf(actions, context) {
43
45
  onlyInResults: onlyInResults.sorted(),
44
46
  unknown: unknown.sorted(),
45
47
  refusedAttempts: actions.reduce((sum, session) => sum + session.refusedAttempts, 0),
48
+ ...(byOthers === undefined ? {} : { refusedByOthers: byOthers }),
46
49
  secretShapes: [...shapes].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count || byString(a.name, b.name)),
47
50
  mentions: actions.reduce((sum, session) => sum + session.mentions, 0),
48
51
  ...(context.marks === undefined ? {} : { marks: context.marks }),
@@ -1,4 +1,5 @@
1
1
  import { NAMES_PER_FILE } from '../report-model.js';
2
+ import { refusedByRule } from '../refusals.js';
2
3
  const ORDER = ['rotate', 'template', 'unknown', 'route', 'result'];
3
4
  /**
4
5
  * The labels that ask a person to do something: a key to change, a template to confirm, an outcome nobody recorded.
@@ -69,7 +70,7 @@ export function checkOf(sessions) {
69
70
  ...(keyed ? { keyed: true } : {}),
70
71
  ...(read === undefined ? {} : { read }),
71
72
  }));
72
- return { rows, refusedAttempts: sessions.reduce((sum, { actions }) => sum + actions.refusedAttempts, 0) };
73
+ return { rows, refusedAttempts: sessions.reduce((sum, { actions }) => sum + refusedByRule(actions.refusedAttempts, actions.refusedByOthers), 0) };
73
74
  }
74
75
  /** Two sessions' reads of one file: every key format and line either saw, in the order first seen. */
75
76
  function mergeReads(a, b) {
@@ -1,6 +1,9 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { paint } from '../../../shared/colour.js';
2
4
  import { table } from '../../../shared/text-table.js';
3
5
  import { needsAction } from '../actions-digest.js';
6
+ import { refusedByRule } from '../../refusals.js';
4
7
  const INDENT = 2;
5
8
  /** How many files the brief view names before it says how many more there are (R15a). */
6
9
  const BRIEF_ROWS = 5;
@@ -76,7 +79,7 @@ function brief(digest) {
76
79
  ? `Nothing left to act on: ${read(digest)}${under(digest)}.`
77
80
  : `Nothing to act on: ${read(digest)}${under(digest)}, and no protected file was reached.`,
78
81
  ...unreadableLines(digest),
79
- ...(digest.refusedAttempts === 0 ? [] : [`Your rules held: ${plural(digest.refusedAttempts, 'attempt was', 'attempts were')} refused.`]),
82
+ ...stoppedSentences(digest),
80
83
  ...markLines(digest),
81
84
  ];
82
85
  }
@@ -89,9 +92,8 @@ function brief(digest) {
89
92
  '',
90
93
  ...table(shown.map((row) => [`${LABELS[row.label].padEnd(width)} ${shorten(row.path).padEnd(pathWidth)}`, `${row.says}, ${inSessions(row.sessions)}`]), INDENT),
91
94
  ...(rows.length > shown.length ? [`${' '.repeat(INDENT)}and ${rows.length - shown.length} more`] : []),
92
- ...(digest.refusedAttempts === 0
93
- ? []
94
- : [`${' '.repeat(INDENT)}your rules held: ${plural(digest.refusedAttempts, 'attempt was', 'attempts were')} refused`]),
95
+ // Under the rows, as their footnote: lower case and no full stop, as the lines beside them are.
96
+ ...stoppedSentences(digest).map((sentence) => `${' '.repeat(INDENT)}${sentence.charAt(0).toLowerCase()}${sentence.slice(1, -1)}`),
95
97
  ...(digest.mentions === 0
96
98
  ? []
97
99
  : [`${' '.repeat(INDENT)}${plural(digest.mentions, 'path was', 'paths were')} named in text a call carried, which opens no file; not counted`]),
@@ -186,7 +188,7 @@ function counts(digest) {
186
188
  }
187
189
  // One sentence per line: the counts, what the rules held, and what is already dealt with are three answers, and
188
190
  // run together they were one line long enough to wrap twice in a terminal nobody had made wide for it.
189
- const held = digest.refusedAttempts === 0 ? [] : [`Your rules held: ${plural(digest.refusedAttempts, 'attempt was', 'attempts were')} refused.`];
191
+ const held = stoppedSentences(digest);
190
192
  const done = doneLine(digest);
191
193
  if (!needsAction(digest)) {
192
194
  return [`Nothing to act on in the ${plural(digest.sessionsRead, 'session', 'sessions')} read.`, ...held, ...done, ...unreadable];
@@ -224,11 +226,30 @@ function under(digest) {
224
226
  return digest.policy === undefined ? '' : `, read under ${digest.policy}`;
225
227
  }
226
228
  function refusedLines(digest) {
227
- if (digest.refusedAttempts === 0)
229
+ const [rules, ...others] = stoppedSentences(digest);
230
+ if (rules === undefined)
228
231
  return [];
232
+ // What follows is about a rule's refusal alone, so it stays with the rules' line and is not said of the others (WS4).
233
+ return rules.startsWith(RULES_HELD)
234
+ ? [`${rules} A refused Read raises no hook event, so only the`, 'transcripts show these.', ...others]
235
+ : [rules, ...others];
236
+ }
237
+ const RULES_HELD = 'Your rules held:';
238
+ const AUTO_MODE_STOPPED = 'Auto mode stopped';
239
+ const YOU_TURNED_DOWN = 'You turned down';
240
+ /**
241
+ * What was stopped, and by whom (`specs/2026-10-01-who-stopped-it.md` WS4): a rule's refusals are the rules having held,
242
+ * and an attempt Claude Code's auto mode or the person stopped is said as theirs - crediting a rule with it would tell a
243
+ * person their rules cover a route they do not. One sentence each, the rules' first; a count of zero says nothing.
244
+ */
245
+ function stoppedSentences(digest) {
246
+ const byRule = refusedByRule(digest.refusedAttempts, digest.refusedByOthers);
247
+ const reviewer = digest.refusedByOthers?.reviewer ?? 0;
248
+ const person = digest.refusedByOthers?.person ?? 0;
229
249
  return [
230
- `Your rules held: ${plural(digest.refusedAttempts, 'attempt was', 'attempts were')} refused. A refused Read raises no hook event, so only the`,
231
- 'transcripts show these.',
250
+ ...(byRule === 0 ? [] : [`${RULES_HELD} ${plural(byRule, 'attempt was', 'attempts were')} refused.`]),
251
+ ...(reviewer === 0 ? [] : [`${AUTO_MODE_STOPPED} ${plural(reviewer, 'attempt', 'attempts')}.`]),
252
+ ...(person === 0 ? [] : [`${YOU_TURNED_DOWN} ${plural(person, 'attempt', 'attempts')}.`]),
232
253
  ];
233
254
  }
234
255
  function section(shown, heading, explanation, rows) {
@@ -301,7 +322,8 @@ function colourise(lines) {
301
322
  // A range that is clear, and rules that refused something, are the two things that went well.
302
323
  if (/^Nothing (?:left )?to act on/.test(line))
303
324
  return paint(line, 'green');
304
- for (const prefix of ['Your rules held:']) {
325
+ // What stopped an attempt went well, whoever stopped it.
326
+ for (const prefix of [RULES_HELD, AUTO_MODE_STOPPED, YOU_TURNED_DOWN]) {
305
327
  if (line.startsWith(prefix))
306
328
  return `${paint(prefix, 'green')}${line.slice(prefix.length)}`;
307
329
  }
@@ -1,5 +1,4 @@
1
1
  import { filesRead } from '../flow-reads.js';
2
- import { RULE_NAMES } from '../rule-names.js';
3
2
  /**
4
3
  * Whether a file read holds keys, as far as what was read says: a recognised key format, or a read shared with another
5
4
  * protected file, whose keys may be this one's (`PrivateFile.mixed`). The to-do list and the marks decide by this alone.
@@ -9,8 +8,6 @@ export function holdsKeys(file) {
9
8
  }
10
9
  /** Names a file kept as an example rather than as configuration. A convention, matched on the name and nothing else. */
11
10
  const TEMPLATE = /\.(example|sample|template|dist)$/;
12
- /** The patterns of the built-in rules, every one of them for passwords and keys (F45). */
13
- const KEY_RULES = new Set(RULE_NAMES.flatMap((group) => group.patterns));
14
11
  /** Whether a path is named as a template (R12c): the one test the check and the report page share. */
15
12
  export function isTemplate(path) {
16
13
  return TEMPLATE.test(path);
@@ -67,9 +64,9 @@ export function actionsOf(report) {
67
64
  const readOf = new Map(report.privateFiles.map((file) => [file.path, readOfFile(file)]));
68
65
  return {
69
66
  policy: report.scope.policy.origin,
70
- // F57: a told file's contents were read because the person let them be; only keys in it still need changing - a key
71
- // of a known format, or any file a built-in rule for passwords and keys covers (F45: that rule is itself the fact).
72
- rotate: [...rotate].filter((path) => !toldPaths.has(path) || keyed.has(path) || KEY_RULES.has(patternOf.get(path))).sort().map((path) => {
67
+ // F57a, amended 2026-10-02 by the maintainer: a told file's contents were read because the person let them be, keys
68
+ // included - they chose Track for it - so nothing about it is to fix. It stays on `told`, said and never asked for.
69
+ rotate: [...rotate].filter((path) => !toldPaths.has(path)).sort().map((path) => {
73
70
  const read = readOf.get(path);
74
71
  return { path, template: TEMPLATE.test(path), ...(keyed.has(path) ? { keyed: true } : {}), ...(read === undefined ? {} : { read }) };
75
72
  }),
@@ -78,6 +75,7 @@ export function actionsOf(report) {
78
75
  onlyInResults: [...onlyInResults].sort(),
79
76
  unknown: [...new Set(reaching.filter((story) => story.outcome === 'unknown').map((story) => story.path))].sort(),
80
77
  refusedAttempts: report.tally.refusedAttempts,
78
+ ...(report.tally.refusedByOthers === undefined ? {} : { refusedByOthers: report.tally.refusedByOthers }),
81
79
  secretShapes: report.secretShapes.flatMap((finding) => finding.classes),
82
80
  mentions: mentions.size,
83
81
  };
@@ -1,9 +1,13 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { noStoreAnywhere } from '../../core/session-catalogue.js';
1
4
  import { splitBySince } from '../../core/session-filter.js';
2
5
  import { choosePolicy, policyRefusal } from '../choose-policy.js';
3
6
  import { digestOf } from './actions-digest.js';
4
7
  import { checkOf } from './check-lines.js';
5
8
  import { noteProblem, recordMark, recordUnmark } from './mark-request.js';
6
9
  import { actionsAfterMarks, marksInRange, standingMarks } from './marks.js';
10
+ import { nothingSavedHere } from '../nothing-saved-here.js';
7
11
  /** Each report is run for its actions; the text it also renders is discarded, so this only has to be a valid width. */
8
12
  const DISCARDED_WIDTH = 100;
9
13
  /**
@@ -56,6 +60,10 @@ export class SessionCheck {
56
60
  if ('errors' in chosen)
57
61
  return { answer: { outcome: 'policy-refused', output: policyRefusal(chosen.errors) } };
58
62
  const listing = await catalogue.list(workingDirectory);
63
+ if (!listing.found && noStoreAnywhere(listing)) {
64
+ const ways = 'use Claude Code here, then run agentwhy check again - or run agentwhy init now, to be protected before there is history to check.';
65
+ return { answer: { outcome: 'no-sessions', output: nothingSavedHere('Claude Code', listing.directory, ways) } };
66
+ }
59
67
  if (!listing.found || listing.sessions.length === 0) {
60
68
  return {
61
69
  answer: {
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { policyFromDenyRules, readDenyRules } from '../adapter/claude-code/policy/deny-rules.js';
2
4
  import { parsePolicy } from '../core/policy/parse-policy.js';
3
5
  import { readTellLists, tellPatterns } from './private-files/tell-lists.js';
@@ -40,9 +42,13 @@ function withTold(policy, told) {
40
42
  const builtIn = policy.origin.kind === 'default';
41
43
  const kept = policy.protected.filter((entry) => !(builtIn && told.includes(entry.pattern)));
42
44
  const blocked = new Set(kept.map((entry) => entry.pattern));
45
+ // The told patterns answer first: a file tracked by its name (`**/demo.env`) under a broader blocking pattern
46
+ // (`**/*.env`) is told however its path was written. Appended last, the Read tool's absolute path was answered by
47
+ // the broad pattern and called a key to change, while the same file read by its bare name was told (found by the
48
+ // maintainer, 2026-10-02). A pattern on both lists is still blocked - it is never given a told entry at all.
43
49
  return {
44
50
  ...policy,
45
- protected: [...kept, ...told.filter((pattern) => !blocked.has(pattern)).map((pattern) => ({ pattern, mode: 'tell' }))],
51
+ protected: [...told.filter((pattern) => !blocked.has(pattern)).map((pattern) => ({ pattern, mode: 'tell' })), ...kept],
46
52
  };
47
53
  }
48
54
  /** The words a refused policy is reported in, the same for every command that reads one. */
@@ -0,0 +1,16 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * What `start`, `sessions` and `check` say where no AI's store of conversations is there at all
5
+ * (`.ai/specs/2026-09-16-worth-running-every-day.md` R28, amended 2026-10-01). Two things look the same from here: a
6
+ * computer the AI has not been used on yet, and an AI app's sandbox or virtual machine, measured twice that day, where
7
+ * "not used yet" was false. Each is said with its own way forward - R28's for the first, `init` among it, and a terminal on
8
+ * the person's own computer for the second - since nothing a run sees tells them apart.
9
+ */
10
+ export function nothingSavedHere(agents, where, notUsedYet) {
11
+ return `No ${agents} conversations are saved where agentwhy looked: ${where}\n` +
12
+ `Either ${agents} ${agents.includes(' or ') ? 'have' : 'has'} not been used on this computer yet, or agentwhy is running somewhere they are not ` +
13
+ `saved: an AI app's sandbox or a virtual machine runs commands apart from your computer.\n` +
14
+ `Not used yet: ${notUsedYet}\n` +
15
+ `Running elsewhere: run agentwhy in a terminal on your own computer, in your project's folder.\n`;
16
+ }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { dirname, join } from 'node:path';
2
4
  import { projectDirectoryName } from '../../adapter/claude-code/contract/projects.js';
3
5
  import { SETTINGS_FILES } from '../../adapter/claude-code/contract/settings.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { join } from 'node:path';
2
4
  import { SETTINGS_FILES } from '../adapter/claude-code/contract/settings.js';
3
5
  import { readDenyRules } from '../adapter/claude-code/policy/deny-rules.js';
@@ -0,0 +1,15 @@
1
+ /** Counted from what refused each attempt. An attempt with no source on record is a rule's: only a rule leaves none. */
2
+ export function refusedByOthers(refused) {
3
+ return others(refused.filter((attempt) => attempt.refusedBy === 'reviewer').length, refused.filter((attempt) => attempt.refusedBy === 'person').length);
4
+ }
5
+ /** The same, over several counts: attempts add up, so their sources do. */
6
+ export function sumRefusedByOthers(counts) {
7
+ return others(counts.reduce((sum, count) => sum + (count?.reviewer ?? 0), 0), counts.reduce((sum, count) => sum + (count?.person ?? 0), 0));
8
+ }
9
+ /** How many of a total a rule refused: what "Your rules held" may count (WS4). */
10
+ export function refusedByRule(total, byOthers) {
11
+ return Math.max(0, total - (byOthers?.reviewer ?? 0) - (byOthers?.person ?? 0));
12
+ }
13
+ function others(reviewer, person) {
14
+ return reviewer === 0 && person === 0 ? undefined : { reviewer, person };
15
+ }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { isAbsolute, relative, resolve, sep } from 'node:path';
2
4
  /**
3
5
  * Where a folder is, as a person reads it (`.ai/specs/2026-09-27-which-project.md` V1): `~/Projects/shop` under the home
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * The policy every page this tool writes is served under - by nothing, since a page is opened as a file. It says
3
5
  * that the page may run its own inline script and style and may reach **nowhere**: no fetch, no image but a `data:` one, no form
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * The session report, wearing the sessions observatory's clothes: a fixed sidebar, a topbar that says where you
3
5
  * are, the verdict as a heading rather than a banner, and one workbench of two panels — the two views of the
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { labelAttributes } from './report-copy.js';
2
4
  import { BRAND_MARK } from './ui/brand-mark.js';
3
5
  /**
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * How many of a path's last segments it takes to tell it from every other path named beside it
3
5
  * (`specs/2026-09-14-path-display-and-share.md` R5): two files a page names must never read alike, and a name alone reads
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { APP_WORDS } from './ui/words/app-words.js';
2
4
  import { CONVERSATIONS_WORDS } from './ui/words/conversations-words.js';
3
5
  import { MONTH_WORDS } from './ui/words/month-words.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { escapeHtml as e } from '../html-report-components.js';
2
4
  import { inLanguages } from '../report-copy.js';
3
5
  import { avatarGroup } from '../ui/avatar.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { escapeHtml as e } from '../html-report-components.js';
2
4
  import { inLanguages, labelAttributes, LANGS, translator } from '../report-copy.js';
3
5
  import { closeButton, pill, textButton } from '../ui/button.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { matchesGlob } from '../../../core/policy/glob.js';
2
4
  import { helperViews } from './helpers.js';
3
5
  import { nameOf, ruleKey } from './item-names.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * The Fix it wizard's steps (the report page spec P18-P25): one step shown at a time; on the first, "Skip for now", which
3
5
  * moves to the next file and writes nothing (F52, O13), beside the main button "I did it", which goes on once every row
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { escapeHtml as e } from '../html-report-components.js';
2
4
  import { inLanguages, labelAttributes } from '../report-copy.js';
3
5
  import { avatar } from '../ui/avatar.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { escapeHtml as e } from '../html-report-components.js';
2
4
  import { distinctDepth } from '../path-tail.js';
3
5
  const LOCALES = { en: 'en-US', pl: 'pl-PL', de: 'de-DE' };
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { insideProject } from '../../../core/project-root.js';
2
4
  import { nameOf } from './item-names.js';
3
5
  /**
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * The report page's views (the report page spec P2, P45): one section per sidebar item. With a script the one the
3
5
  * address names is shown - the first where it names none - and its sidebar item is the active one; with none, every
@@ -103,7 +103,8 @@ function gapBanner(report) {
103
103
  }
104
104
  function nothingView(report, gaps, rows, context, names) {
105
105
  const say = (key, vars = {}) => inLanguages((t) => t(key, vars));
106
- const shapes = [...new Set(report.secretShapes.flatMap((finding) => finding.classes.map(String)))];
106
+ // P59 is a key in no private file. One in a private file's text is that file's (SW7): a tracked file says so below.
107
+ const shapes = [...new Set(report.secretShapes.filter((finding) => finding.inPrivateFile !== true).flatMap((finding) => finding.classes.map(String)))];
107
108
  const back = context.back ? '<div class="rp-back">' + pill({ label: say('rp.nothing.back'), tone: 'light', size: 'lg', href: 'index.html' }) + '</div>' : '';
108
109
  // P59: a key's shape in a result is never a clean screen - but it is not a to-do item either, since no protected file
109
110
  // is known to hold it. The answer says what to do and where the record shows it, and the summary says what else the AI
@@ -162,7 +163,7 @@ function heroGo(label) {
162
163
  }
163
164
  /**
164
165
  * The private files the AI read that ask for nothing (F57a): on a screen with nothing to fix, every private file read is
165
- * one the person chose Tell me for and that held no key - the to-do list keeps every other read.
166
+ * one the person chose Tell me for - keys or not, since 2026-10-02 (SW7) - and the to-do list keeps every other read.
166
167
  */
167
168
  function allowedReads(rows) {
168
169
  return rows.filter((row) => row.private && row.access === 'read' && row.item === undefined).map((row) => row.path);