@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,7 +1,10 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { dirname, join, relative, resolve } from 'node:path';
2
- import { sessionKey } from '../../core/session-catalogue.js';
4
+ import { noStoreAnywhere, sessionKey } from '../../core/session-catalogue.js';
3
5
  import { splitBySince } from '../../core/session-filter.js';
4
6
  import { recogniseAll } from '../../core/session-titles.js';
7
+ import { isDirectory } from '../../ports/directory-reader.js';
5
8
  import { FileAccessError } from '../../ports/file-access-error.js';
6
9
  import { contentVersion } from '../../shared/content-version.js';
7
10
  import { fileStamp } from '../../shared/file-stamp.js';
@@ -16,26 +19,30 @@ import { indexHandler } from './serve/index-handler.js';
16
19
  import { movedPage } from './projects/moved-page.js';
17
20
  import { nearestProject } from '../../core/nearest-project.js';
18
21
  import { translator } from '../render/report-copy.js';
19
- import { hookComplete, readSettingsFile, setUpBy } from './settings-files.js';
22
+ import { hookComplete, readSettingsFile } from './settings-files.js';
20
23
  import { indexProjects } from './projects/index-projects.js';
21
24
  import { NOT_A_PROJECT } from '../../setup/project-setup.js';
22
25
  import { notAProject } from '../../setup/not-a-project.js';
23
26
  import { inTemporarySpace } from './projects/temporary-space.js';
27
+ import { inChatFolder } from '../../adapter/codex/contract/chat-folders.js';
24
28
  import { behind } from '../../setup/behind.js';
25
29
  import { modeChange, onlyTakesOut, settingsChangeToSetup } from './serve/settings-setup.js';
26
30
  import { DEFAULT_THRESHOLD } from '../watch/agent-alert.js';
27
31
  import { DEFAULT_CHANNELS, DEFAULT_CLEAN, DEFAULT_SAID_AS } from '../watch/notice-choices.js';
28
32
  import { actionsAfterMarks, markLines, marksInRange, standingMarks } from '../check/marks.js';
29
33
  import { finishOnboarding } from './onboarding/finish-onboarding.js';
34
+ import { welcomeIn } from './onboarding/welcome-decision.js';
30
35
  import { WHO_STEP } from './onboarding/onboarding-script.js';
31
36
  import { repositoryAbove } from './repository.js';
32
- import { pageStopped, startSaid } from './render/start-words.js';
37
+ import { pageStopped, quietlySaid, startSaid } from './render/start-words.js';
33
38
  import { PROJECT_HOOKS } from '../../adapter/codex/contract/hooks.js';
39
+ import { codexCheckState } from '../../adapter/codex/settings/codex-check.js';
34
40
  import { codexRefuseIn } from '../../adapter/codex/settings/codex-hooks.js';
35
41
  import { SETTINGS_FILES } from '../../adapter/claude-code/contract/settings.js';
36
42
  import { rulesReadByHooks } from '../../adapter/claude-code/settings/hook-entries.js';
37
43
  import { fileRulesIn, isDenied } from '../../adapter/claude-code/settings/deny-entries.js';
38
44
  import { fileRulePathOf, toPattern } from '../../adapter/claude-code/policy/deny-rules.js';
45
+ import { nothingSavedHere } from '../nothing-saved-here.js';
39
46
  const DAY = 86_400_000;
40
47
  /**
41
48
  * The patterns the page lists: the ones the policy read, and then any rule the project's own settings files hold
@@ -91,12 +98,19 @@ export class SessionStart {
91
98
  // which-project V16: a run the onboarding's project step switched to opens that project's onboarding, whatever W23
92
99
  // says - the person is in the middle of a setup, and the step chose this project for it.
93
100
  const arrivedFrom = this.#dependencies.arrivedFrom;
94
- const opensWelcome = welcome !== undefined && (arrivedFrom === 'step' || (options.open && welcome.opens));
101
+ // AW1 (`the-address-opens-the-welcome`): an address asked for with `--serve` is opened by a person - the one an AI app
102
+ // sends - so it names the page a browser this run opened would show. Not in an empty project (AW2): every empty
103
+ // listing seen there came from a place that cannot see the person's conversations, whose address nobody can open.
104
+ const anyConversation = listing.found && listing.sessions.length > 0;
105
+ const opensWelcome = welcome !== undefined && (arrivedFrom === 'step' || ((options.open || (options.serve === true && anyConversation)) && welcome.opens));
95
106
  if ((!listing.found || listing.sessions.length === 0) && !opensWelcome) {
96
107
  return {
97
108
  outcome: 'no-sessions',
98
109
  // Every place looked in, one per AI: "looked in the wrong place" is only answerable where each is named.
99
- output: noSessions(listing.searched.map((place) => place.directory).join(' and ') || listing.directory, projectName(workingDirectory), served && options.open, welcome?.setUp === true, notHere),
110
+ // R28, amended 2026-10-01: at a terminal only - a run that would open a page keeps W1a's words, naming no command.
111
+ output: noStoreAnywhere(listing) && notHere === undefined && !(served && options.open)
112
+ ? nothingSavedHere('Claude Code or Codex', listing.searched.map((place) => place.directory).join(' and '), 'use Claude Code or Codex here, then run agentwhy start again - or run agentwhy init now, to be protected in Claude Code before there is history to check.')
113
+ : noSessions(listing.searched.map((place) => place.directory).join(' and ') || listing.directory, projectName(workingDirectory), served && options.open, welcome?.setUp === true, notHere),
100
114
  };
101
115
  }
102
116
  // Resolved against the working directory given, never against whatever the process happens to be standing in.
@@ -125,6 +139,9 @@ export class SessionStart {
125
139
  // F55: a conversation a person once asked to check is read by every run after, as if it were in range - the button is
126
140
  // pressed once, not every morning. A list that cannot be read leaves the run its range, as it always had.
127
141
  const asked = new Set(this.#dependencies.checked === undefined ? [] : (await this.#dependencies.checked.read()).ids);
142
+ // SW10: the conversation a run was asked to open is written however long ago it was active. Not recorded as checked.
143
+ if (options.session !== undefined)
144
+ asked.add(options.session);
128
145
  // M5: each report is drawn with the marks that still hold for its session. A shared page carries none (R37), and a
129
146
  // record that cannot be read leaves every file to do rather than guessing which were done (invariant 4).
130
147
  const standingAtStart = options.share ? new Map() : standingMarks((await this.#dependencies.marks.read()).records);
@@ -160,14 +177,15 @@ export class SessionStart {
160
177
  const { inRange, outOfRange: older } = splitBySince(sessions, options.since.since);
161
178
  const generatedIds = new Set([...inRange, ...older.filter((session) => asked.has(sessionKey(session)))].map((session) => sessionKey(session)));
162
179
  // A title is user content, and a shared page is one that leaves this machine: it is not even read. A session that
163
- // has grown may have a new last prompt, so its title is read again with it. A Codex conversation is named in a file
164
- // of Codex's own, seconds after it starts (`what-codex-wrote` §2.9, X28a) - found by the maintainer, a row drawn in
165
- // those seconds stayed untitled once its rollout stopped changing - so a Codex row with no title asks again; the
166
- // names are a lookup in a list read again only where it changed.
180
+ // has grown may have a new last prompt, so its title is read again with it. Both desktop apps name a conversation
181
+ // in a file of their own, seconds after it starts (`what-codex-wrote` §2.9, X28a; `claude-desktop-conversations`
182
+ // CD5) - found by the maintainer, a row drawn in those seconds stayed untitled once its transcript stopped
183
+ // changing - so a row with no title asks again. That ask is a lookup in the app's own names, read again only
184
+ // where they changed; the adapter keeps an unchanged transcript's tail, so it is not read again (CD5).
167
185
  if (!options.share) {
168
186
  const stale = sessions.filter((session) => {
169
187
  const known = titles.get(sessionKey(session));
170
- return known?.modifiedAt !== session.modifiedAt || (session.provider === 'codex' && known.title === undefined);
188
+ return known?.modifiedAt !== session.modifiedAt || known.title === undefined;
171
189
  });
172
190
  const read_ = await recogniseAll(this.#dependencies.titles, stale);
173
191
  stale.forEach((session, at) => titles.set(sessionKey(session), { modifiedAt: session.modifiedAt, ...read_[at] }));
@@ -365,7 +383,9 @@ export class SessionStart {
365
383
  /*
366
384
  * R75: the conversations and the marks as they are now, drawn into every page. A page read while a refresh runs is
367
385
  * answered by that refresh; a write is not, because the refresh it would join began before the write - it waits,
368
- * then runs again. A listing that finds nothing now (a directory moved, a disk gone) leaves the pages as they were.
386
+ * then runs again. A listing that finds nothing now (a directory moved, a disk gone) leaves the conversations as
387
+ * they were, and the pages are still drawn again: Settings shows what the files hold (R60), in a project with no
388
+ * conversations yet too (W1a).
369
389
  */
370
390
  let refreshing;
371
391
  const refresh = async (when) => {
@@ -375,16 +395,16 @@ export class SessionStart {
375
395
  await refreshing;
376
396
  const running = (async () => {
377
397
  const now_ = await catalogue.list(workingDirectory);
378
- if (!now_.found || now_.sessions.length === 0)
379
- return undefined;
380
- // A policy that cannot be read now leaves the one the pages were drawn under, rather than rules nobody chose.
381
- const again = await choosePolicy(options, policyFiles, this.#dependencies.tell).catch(() => undefined);
382
- if (again !== undefined && !('errors' in again))
383
- policyNow = again.policy;
384
- const refusal = await sync(now_.sessions, standingMarks((await this.#dependencies.marks.read()).records));
385
- if (refusal !== undefined)
386
- return refusal;
387
- reportsServed();
398
+ if (now_.found && now_.sessions.length > 0) {
399
+ // A policy that cannot be read now leaves the one the pages were drawn under, rather than rules nobody chose.
400
+ const again = await choosePolicy(options, policyFiles, this.#dependencies.tell).catch(() => undefined);
401
+ if (again !== undefined && !('errors' in again))
402
+ policyNow = again.policy;
403
+ const refusal = await sync(now_.sessions, standingMarks((await this.#dependencies.marks.read()).records));
404
+ if (refusal !== undefined)
405
+ return refusal;
406
+ reportsServed();
407
+ }
388
408
  await writePages((await build()).index);
389
409
  return undefined;
390
410
  })();
@@ -428,10 +448,14 @@ export class SessionStart {
428
448
  if (chosen !== undefined)
429
449
  return switchToFolder(chosen, from);
430
450
  const project = listed?.projects.find((one) => one.id === id);
431
- if (project === undefined || !project.exists || this.#notAProject(project.path) !== undefined)
451
+ if (project === undefined || project.folder === 'gone' || this.#notAProject(project.path) !== undefined)
432
452
  return { failed: 'That project is not one this page listed.' };
433
453
  if (relative(project.path, workingDirectory) === '')
434
454
  return { failed: 'That project is the one shown already.' };
455
+ // which-project V10b: a folder the system guards was not looked into while listing; it is now, because the person
456
+ // picked it - so the system asks them, if at all, about something they did.
457
+ if (project.folder === 'not-looked' && !(await isDirectory(this.#dependencies.directories, project.path)))
458
+ return { failed: 'This folder isn’t there anymore.' };
435
459
  return switchToFolder(project.path, from);
436
460
  };
437
461
  // V12: a folder chosen in the computer's window - shown at once where it is a project or near none, and where it lies
@@ -520,16 +544,23 @@ export class SessionStart {
520
544
  },
521
545
  })
522
546
  : undefined;
547
+ // PF2: the address and its token, where a later `--detach` looks for a server to reuse. Never a shared page's.
548
+ if (serving !== undefined && !options.share)
549
+ await this.#dependencies.pageServer?.write(serving.urlOf(''));
523
550
  // W23: the onboarding is opened in place of the index where it is to be; the index otherwise, as it always was. A run
524
551
  // a page switched to opens it at *Who*: the project was chosen on the way here (which-project V16).
525
- const openAt = serving === undefined ? indexPath
526
- : opensWelcome && onboarding !== undefined ? serving.urlOf(onboarding.file) + (arrivedFrom === undefined ? '' : `#${WHO_STEP}`)
527
- : serving.url;
552
+ // SW10: a conversation asked for by name is opened in its place - the person said yes to that report, not to a setup.
553
+ const wanted = options.session === undefined ? undefined : reports.get(options.session);
554
+ const sessionFile = wanted !== undefined && wanted.report.kind === 'generated' ? wanted.file : undefined;
555
+ const openAt = sessionFile !== undefined ? (serving === undefined ? join(out, sessionFile) : serving.urlOf(sessionFile))
556
+ : serving === undefined ? indexPath
557
+ : opensWelcome && onboarding !== undefined ? serving.urlOf(onboarding.file) + (arrivedFrom === undefined ? '' : `#${WHO_STEP}`)
558
+ : serving.url;
528
559
  // V15: a run a page switched to is shown in that page's tab, so it opens none of its own.
529
560
  const handedOver = this.#dependencies.handedOver;
530
561
  const opened = options.open && handedOver === undefined ? await this.#dependencies.browser.open(openAt) : true;
531
562
  const view = this.#dependencies.terminal ?? { colour: false, decorated: false };
532
- const said = startSaid({
563
+ const said = options.quiet === true ? quietlySaid(openAt, opened && options.open, options.session === undefined || sessionFile !== undefined) : startSaid({
533
564
  ...counted(entries, outOfRange.length, indexPath, first.index, opened),
534
565
  // which-project V3: the folder the run was for, said where the terminal says what it read.
535
566
  ...(options.share ? {} : { project: projectName(workingDirectory) }),
@@ -540,7 +571,8 @@ export class SessionStart {
540
571
  ? { servedAsFile: true }
541
572
  : {}),
542
573
  ...(serving === undefined ? {} : { url: openAt }),
543
- ...(serving !== undefined && opensWelcome ? { welcome: true } : {}),
574
+ // AW3: "Opened" only where a browser or a page's tab shows it; an address handed on is said as one.
575
+ ...(serving !== undefined && opensWelcome ? { welcome: options.open || handedOver !== undefined ? 'opened' : 'served' } : {}),
544
576
  // which-project V3: a switch says which project is shown now, and where it is.
545
577
  ...(handedOver === undefined || options.share ? {} : { nowShowing: this.#dependencies.home === undefined ? projectName(workingDirectory) : `${projectName(workingDirectory)} (${homeRelative(workingDirectory, this.#dependencies.home)})` }),
546
578
  }, view);
@@ -550,6 +582,7 @@ export class SessionStart {
550
582
  printer?.write(said);
551
583
  handedOver?.(openAt);
552
584
  await serving.closed;
585
+ await this.#dependencies.pageServer?.remove();
553
586
  // V14: a run a page moved away from ends quietly - the run it moved to has already said what it shows.
554
587
  if (switchedAway)
555
588
  return { outcome: 'written', output: '' };
@@ -652,33 +685,17 @@ export class SessionStart {
652
685
  * alerts off later never brings the onboarding back.
653
686
  */
654
687
  async #welcome(workingDirectory) {
655
- const { onboarding, setup, notices } = this.#dependencies;
688
+ const { onboarding, setup, notices, policyFiles, home, realHome } = this.#dependencies;
689
+ // The onboarding writes through both (W15), so a run without them names no page it could not finish.
656
690
  if (onboarding === undefined || setup === undefined || notices === undefined)
657
691
  return undefined;
658
- const reading = await onboarding.store.read();
659
- // W24: the intro plays once per person - where no project of theirs has finished the onboarding.
660
- const intro = !reading.anywhere && !reading.failed;
661
- // which-project V7: in the home directory no project is being shown, so the onboarding opens at its project step,
662
- // whatever the record says - after the welcome only for a person who has never finished one. Nothing here is set up
663
- // and no record is kept of it: not even the silent `done` line below.
664
- if (this.#notAProject(workingDirectory) !== undefined)
665
- return { opens: true, intro, setUp: false, ...(intro ? {} : { atProject: true }) };
666
- if (reading.here)
667
- return { opens: false, intro, setUp: true };
668
- if (reading.failed)
669
- return { opens: false, intro, setUp: false };
670
- // A local file that cannot be read leaves what runs unknown, and the onboarding closed (as `#settingsNow` does).
671
- const local = await this.#settingsFile(join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.local));
672
- if (local === 'unreadable')
673
- return { opens: false, intro, setUp: false };
674
- const shared = await this.#settingsFile(join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.shared));
675
- // N6, widened 2026-09-28 by the maintainer (which-project V10): a project where one of agentwhy's hooks runs, or whose
676
- // settings block files, was set up - before this screen existed, or by hand - and is recorded so, silently.
677
- if (setUpBy([local, shared])) {
678
- await onboarding.store.add((this.#dependencies.clock ?? (() => this.#dependencies.now))());
679
- return { opens: false, intro, setUp: true };
680
- }
681
- return { opens: true, intro, setUp: false };
692
+ return welcomeIn(workingDirectory, {
693
+ store: onboarding.store,
694
+ files: policyFiles,
695
+ ...(home === undefined ? {} : { home }),
696
+ ...(realHome === undefined ? {} : { realHome }),
697
+ now: this.#dependencies.clock ?? (() => this.#dependencies.now),
698
+ });
682
699
  }
683
700
  /**
684
701
  * The person's projects, read again with every refresh as the settings are, since a project set up in another tab is
@@ -690,7 +707,8 @@ export class SessionStart {
690
707
  return {};
691
708
  const listing = await projects.list();
692
709
  const noProject = (path) => this.#notAProject(path) !== undefined;
693
- const temporary = (path) => inTemporarySpace(path, this.#dependencies.temporaryDirectory, home);
710
+ // V10b: a folder the ChatGPT app made for a chat with no project is a quick try too, and counted with them.
711
+ const temporary = (path) => inTemporarySpace(path, this.#dependencies.temporaryDirectory, home) || inChatFolder(path, home);
694
712
  // V20: a folder with no conversations of its own, inside a project that has them - where they are.
695
713
  const near = nearestProject(workingDirectory, listing, noProject);
696
714
  const above = near.kind === 'above' ? { id: near.project.id, within: relative(near.project.path, workingDirectory) } : undefined;
@@ -788,9 +806,16 @@ export class SessionStart {
788
806
  // U2: the version is this run's own, read once; the hooks are read from the files as they are now.
789
807
  this.#serving ??= this.#dependencies.invocation?.version() ?? Promise.resolve(undefined);
790
808
  const late = behind({ ...(local === undefined ? {} : { local }), ...(readable === undefined ? {} : { shared: readable }) }, await this.#serving);
791
- // CK8: Codex's hook file, read as the settings are; its state is drawn only where the project uses Codex (CK6).
809
+ // AO3: Codex's state is read, never assumed - `on` only where agentwhy's check in the person's own Codex files is
810
+ // verified as approved; `stale` where a check is written somewhere (the person's files unverified, or the old
811
+ // project-level copy) and Codex may still ask; `off` where the project uses Codex (CK6) and nothing is written.
792
812
  const codexHooks = await this.#settingsFile(join(workingDirectory, PROJECT_HOOKS.directory, PROJECT_HOOKS.file));
793
- const codex = typeof codexHooks === 'object' && codexRefuseIn(codexHooks) !== undefined ? 'on' : codexListed || codexHooks !== undefined ? 'off' : undefined;
813
+ const check = this.#dependencies.home === undefined ? 'absent' : await codexCheckState(this.#dependencies.policyFiles, this.#dependencies.home);
814
+ const legacy = typeof codexHooks === 'object' && codexRefuseIn(codexHooks) !== undefined;
815
+ const codex = check === 'on' ? 'on'
816
+ : check === 'stale' || legacy ? 'stale'
817
+ : codexListed || codexHooks !== undefined || (await this.#dependencies.codexOnThisComputer?.()) === true ? 'off'
818
+ : undefined;
794
819
  return {
795
820
  hooks: {
796
821
  watch: installed('watch'),
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { escapeHtml as e } from '../../render/html-report-components.js';
2
4
  import { inLanguages, labelAttributes } from '../../render/report-copy.js';
3
5
  import { textButton } from '../../render/ui/button.js';
@@ -78,7 +80,8 @@ function lockedSwitch(key, turnOn) {
78
80
  (turnOn === undefined ? '' : textButton(inLanguages((t) => t('set.need.go')), turnOn, 'set-tip-go')) +
79
81
  '</div></div>';
80
82
  }
81
- function stateLine(state) {
83
+ /** A switch's state in words, under its row: shared with General's system notifications (SW13). */
84
+ export function stateLine(state) {
82
85
  return '<div class="set-state' + (state.on ? ' set-state-on' : '') + '"><span class="set-dot" aria-hidden="true"></span>' +
83
86
  inLanguages((t) => t(state.key)) + '</div>';
84
87
  }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { escapeHtml as e } from '../../render/html-report-components.js';
2
4
  import { inLanguages, labelAttributes } from '../../render/report-copy.js';
3
5
  import { pill, trashButton } from '../../render/ui/button.js';
@@ -91,13 +93,17 @@ function ruleRow(row, at, view) {
91
93
  const action = row.remove !== undefined && canWrite ? remove(removeWindowId(at))
92
94
  : row.untell !== undefined ? remove(untellWindowId(at))
93
95
  : '<span class="set-none" aria-hidden="true">—</span>';
94
- const icon = !row.watched ? '' : row.kept !== undefined ? OPEN_LOCK_SVG : MODE_SVG[row.mode];
95
- const iconClass = !row.watched ? '' : ' set-icon-' + (row.kept !== undefined ? 'gap' : row.mode);
96
+ const covered = row.covered;
97
+ const gapped = row.kept !== undefined || covered !== undefined;
98
+ const icon = !row.watched ? '' : gapped ? OPEN_LOCK_SVG : MODE_SVG[row.mode];
99
+ const iconClass = !row.watched ? '' : ' set-icon-' + (gapped ? 'gap' : row.mode);
96
100
  return '<li class="set-rule' + (row.watched ? '' : ' set-rule-off') + (watchable ? ' set-rule-watchable' : '') + '"' + (watchable ? ' data-set-row' : '') + '>' +
97
101
  '<span class="set-icon' + iconClass + '" aria-hidden="true">' + icon + '</span>' +
98
102
  '<span class="set-rule-text"><span class="set-rule-name">' + label + '</span>' +
99
103
  '<span class="chip set-rule-chip" title="' + e(pattern) + '">' + e(patternShort(pattern)) + '</span>' +
100
- (row.kept === undefined ? '' : '<span class="set-rule-gap">' + inLanguages((t) => t('set.rule.gap.' + row.kept)) + '</span>') + '</span>' +
104
+ (row.kept === undefined ? '' : '<span class="set-rule-gap">' + inLanguages((t) => t('set.rule.gap.' + row.kept)) + '</span>') +
105
+ // SW19: tracked here, and still refused by Claude Code's own rule for a wider pattern - said on the row, like a half block.
106
+ (covered === undefined ? '' : '<span class="set-rule-gap">' + inLanguages((t) => t('set.rule.covered', { pattern: e(patternShort(covered)) })) + '</span>') + '</span>' +
101
107
  '<span class="set-rule-mode">' + mode + '</span>' +
102
108
  '<span class="set-rule-src">' + tag(inLanguages((t) => t('set.src.short.' + row.source)), SOURCE_TONE[row.source]) + '</span>' +
103
109
  '<span class="set-rule-act">' + action + '</span>' +
@@ -149,21 +155,21 @@ function finishCard(view) {
149
155
  });
150
156
  }
151
157
  /**
152
- * `codex-blocks-too` CK8: in a project that uses Codex, whether the files blocked here are kept from Codex too. Where
153
- * agentwhy's hook is written, a grey line says it holds once approved in Codex, which nothing here can see (CKB9). Where
154
- * it is not and Claude Code's block runs, a coral line with the one write that adds it; where Claude Code's does not
155
- * run either, Finish blocking above writes both, and this says nothing.
158
+ * `codex-approves-its-own-hook` AO3, AO10: in a project that uses Codex, whether the files blocked here are kept from
159
+ * Codex too. Verified (`on`), a grey line states the fact: from the first message, in the terminal and in VS Code,
160
+ * nothing to confirm in Codex. Written but not verified (`stale`), a coral line says Codex may still ask, with the one
161
+ * write that makes it automatic. Not written (`off`) and Claude Code's block running, a coral line with the same
162
+ * write; where Claude Code's block does not run either, Finish blocking above writes both, and this says nothing.
156
163
  */
157
164
  function codexCard(view) {
158
- if (!view.known || view.codex === undefined)
165
+ if (!view.known || view.codex === undefined || !view.stop.on)
159
166
  return '';
160
167
  if (view.codex === 'on')
161
168
  return callout({ tone: 'grey', body: inLanguages((t) => t('set.codex.on')) });
162
- if (!view.stop.on)
163
- return '';
169
+ const stale = view.codex === 'stale';
164
170
  return callout({
165
171
  tone: 'coral',
166
- body: inLanguages((t) => t('set.codex.off')),
167
- ...(view.canWrite ? { action: pill({ label: inLanguages((t) => t('set.codex.go')), tone: 'primary', size: 'task', href: '#' + CODEX_WINDOW, attributes: opener(CODEX_WINDOW) }) } : {}),
172
+ body: inLanguages((t) => t(stale ? 'set.codex.stale' : 'set.codex.off')),
173
+ ...(view.canWrite ? { action: pill({ label: inLanguages((t) => t(stale ? 'set.codex.fix' : 'set.codex.go')), tone: 'primary', size: 'task', href: '#' + CODEX_WINDOW, attributes: opener(CODEX_WINDOW) }) } : {}),
168
174
  });
169
175
  }
@@ -1,10 +1,15 @@
1
- import { inLanguages } from '../../render/report-copy.js';
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { escapeHtml as e } from '../../render/html-report-components.js';
4
+ import { inLanguages, labelAttributes } from '../../render/report-copy.js';
2
5
  import { avatar } from '../../render/ui/avatar.js';
3
6
  import { BRAND_MARK } from '../../render/ui/brand-mark.js';
4
7
  import { pill, TRASH_SVG } from '../../render/ui/button.js';
5
8
  import { callout } from '../../render/ui/callout.js';
6
9
  import { opener } from '../../render/ui/popup.js';
10
+ import { toggleSwitch } from '../../render/ui/switch.js';
7
11
  import { tag } from '../../render/ui/tag.js';
12
+ import { stateLine } from './alerts-tab.js';
8
13
  import { scopeWindowId, UNINSTALL_WINDOW } from './settings-windows.js';
9
14
  /**
10
15
  * General (`for-people-who-build-with-ai.md` F56, asked for 2026-09-24; redrawn 2026-09-25): who what agentwhy saved in
@@ -25,9 +30,36 @@ export function generalTab(view, welcome) {
25
30
  // which-project V5: which AI agentwhy sees, in the words the project step and the list of projects say it.
26
31
  '<p class="set-quiet set-general-note"><span class="set-info" aria-hidden="true">i</span><span>' + inLanguages((t) => t('proj.which')) + '</span></p>' +
27
32
  '</section>' +
33
+ systemCard(view) +
28
34
  setupCard(welcome) +
29
35
  uninstallCard(view);
30
36
  }
37
+ /**
38
+ * Whether agentwhy also shows what it says in the corner of the screen (`2026-10-02-said-where-the-person-is.md` SW13).
39
+ * Asked for by the maintainer on 2026-10-02, who found the notifications tiresome and had only a command to turn them
40
+ * off. Written at once, as Alerts' optional rows are (R26a), keeping every other channel in force; on a page opened as a
41
+ * file, the script offers the command to copy instead.
42
+ */
43
+ function systemCard(view) {
44
+ const on = view.system.on;
45
+ const others = view.system.channels.filter((channel) => channel !== 'os');
46
+ const next = on ? (others.length === 0 ? ['chat'] : others) : [...others, 'os'];
47
+ return '<section class="set-box set-system"><div class="set-head"><h2 class="set-h2">' + inLanguages((t) => t('set.system.title')) + '</h2>' +
48
+ '<p class="set-lead">' + inLanguages((t) => t('set.system.lead')) + '</p></div>' +
49
+ // Not `set-msg-opt`: that row hangs from Alerts' first on a line, and this one hangs from nothing.
50
+ '<article class="set-msg' + (on ? ' set-msg-on' : '') + '"><div class="set-card-body">' +
51
+ '<div class="set-card-name"><h3 class="set-msg-name">' + inLanguages((t) => t('set.system.name')) + '</h3></div>' +
52
+ '<p class="set-msg-why">' + inLanguages((t) => t('set.system.when')) + '</p>' +
53
+ // As Alerts' optional rows: where the hooks are unknown, so is whether anything is said at all - the switch alone.
54
+ (view.known ? stateLine({ key: on ? 'set.state.on' : 'set.state.off', on }) : '') +
55
+ '<p class="set-say" data-set-say hidden></p></div>' +
56
+ toggleSwitch({
57
+ on,
58
+ disabled: !view.noticesWritable,
59
+ attributes: labelAttributes((t) => t('set.system.name')) + ' data-set-notice="' + e(JSON.stringify({ notify: next })) + '"',
60
+ }) +
61
+ '</article></section>';
62
+ }
31
63
  /**
32
64
  * W25a: the onboarding again, where this run serves it - drawn as Uninstall is (a mark, one sentence, the button), and
33
65
  * above it: going through the setup again comes before taking it all out. Nothing is written until its Finish (W10).
@@ -115,6 +115,8 @@ const SETTINGS_STYLE = String.raw `
115
115
  .set-cannot{margin:-8px 0 22px}
116
116
  .set-quiet{font-size:14px;line-height:1.5;color:var(--text-3)}
117
117
  .set-soft{color:var(--text-2)}
118
+ .set-tick{display:flex;gap:10px;align-items:flex-start;font-size:14px;color:var(--text-2);cursor:pointer}
119
+ .set-tick input{margin-top:3px;accent-color:var(--coral)}
118
120
  .set-head{margin-bottom:16px}
119
121
  .co+.set-head{margin-top:30px}
120
122
  .set-h2{margin:0;font-size:19px;font-weight:650;line-height:1.3}
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * What only Settings does (`.ai/plans/2026-09-23-settings-redesign.md`, step 3). The kit opens and closes the windows
3
5
  * and switches the tabs; this posts what a confirm button carries, and reads the page again once the server has
@@ -43,7 +45,7 @@ export const SETTINGS_SCRIPT = String.raw `
43
45
  if (body.change === 'protect') return 'agentwhy init --protect ' + quote(body.pattern) + ' --refuse' + shared(body.where);
44
46
  if (body.change === 'adopt') return 'agentwhy init' + body.patterns.map((pattern) => ' --protect ' + quote(pattern)).join('') + ' --refuse' + shared(body.where);
45
47
  if (body.change === 'unprotect') return 'agentwhy init --remove --unprotect ' + quote(body.pattern) + shared(body.where);
46
- if (body.scope === 'project') return 'agentwhy notify' + (body.on ? ' --on ' + body.on : '') + (body.clean ? ' --clean ' + body.clean : '');
48
+ if (body.scope === 'project') return 'agentwhy notify' + (body.on ? ' --on ' + body.on : '') + (body.clean ? ' --clean ' + body.clean : '') + (body.notify ? ' --notify ' + body.notify.join(',') : '');
47
49
  return '';
48
50
  };
49
51
 
@@ -101,6 +103,11 @@ export const SETTINGS_SCRIPT = String.raw `
101
103
  const picked = document.querySelector('input[name="' + confirm.dataset.setWho + '"]:checked');
102
104
  body.where = picked ? picked.value : 'local';
103
105
  }
106
+ // AO17: the window's unticked choice - out of the person's own Codex files too, only when ticked.
107
+ if (confirm.dataset.setTick) {
108
+ const tick = document.querySelector('input[name="' + confirm.dataset.setTick + '"]');
109
+ if (tick && tick.checked) body.codex = true;
110
+ }
104
111
  change(body, confirm, confirm.getAttribute('data-set-command-' + (body.where || 'local')));
105
112
  return;
106
113
  }
@@ -1,7 +1,10 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { DEFAULT_POLICY } from '../../../core/policy/default-policy.js';
4
+ import { matchesGlob } from '../../../core/policy/glob.js';
2
5
  import { SETTINGS_FILES } from '../../../adapter/claude-code/contract/settings.js';
3
6
  import { DEFAULT_THRESHOLD } from '../../watch/agent-alert.js';
4
- import { DEFAULT_CLEAN } from '../../watch/notice-choices.js';
7
+ import { DEFAULT_CHANNELS, DEFAULT_CLEAN } from '../../watch/notice-choices.js';
5
8
  import { RULE_NAMES } from '../../rule-names.js';
6
9
  /**
7
10
  * What the Settings page shows, decided from what the files hold (`for-people-who-build-with-ai.md` §3 C; the plan's
@@ -32,7 +35,9 @@ export function settingsView(settings) {
32
35
  const read = known ? readPatterns(settings, reads) : undefined;
33
36
  const told = toldIn(settings);
34
37
  const canTell = canWrite && settings.told !== undefined && settings.told.local !== 'unreadable' && settings.told.shared !== 'unreadable';
35
- const rows = read === undefined ? policyRows(settings) : fileRows(settings, reads, read, told, canTell).map((row) => keptOf(row, settings, hooks));
38
+ const rows = read === undefined
39
+ ? policyRows(settings)
40
+ : fileRows(settings, reads, read, told, canTell).map((row) => coveredOf(keptOf(row, settings, hooks), settings, told));
36
41
  const unread = read === undefined ? [] : unreadRules(settings, read);
37
42
  const addTo = reads === 'local' || reads === 'shared' ? reads : alerts.who ?? stop.who ?? 'local';
38
43
  // A built-in pattern on a told list is read and told, never blocked again by an add (F57).
@@ -45,6 +50,7 @@ export function settingsView(settings) {
45
50
  stopped: { on: on === 'refused', locked: known && !alerts.on },
46
51
  fine: { on: clean !== 'off', locked: known && !alerts.on },
47
52
  noticesWritable: notices !== undefined && !notices.unusable,
53
+ system: { on: (notices?.notify ?? DEFAULT_CHANNELS).includes('os'), channels: notices?.notify ?? DEFAULT_CHANNELS },
48
54
  stop,
49
55
  scope,
50
56
  moves: { local: movesInto(settings, hooks, 'local'), shared: movesInto(settings, hooks, 'shared') },
@@ -189,6 +195,27 @@ function keptOf(row, settings, hooks) {
189
195
  const whole = ruled.length === row.patterns.length && (searched === undefined || row.patterns.every((pattern) => searched.has(pattern)));
190
196
  return whole ? row : { ...row, kept: 'open' };
191
197
  }
198
+ /**
199
+ * SW19, found by the maintainer on 2026-10-02: `demo.env` was switched to Track, and Claude Code still refused every
200
+ * read - its own deny rule for every `.env` file covers that name, and deny rules know no exceptions. The row carries
201
+ * the shortest written Block rule that still matches the row's file, by the name a command or a path would use. Only
202
+ * a row tracked by a file's name is asked - a folder or a hand-written pattern names no one file to try - and only
203
+ * rules a settings file actually holds count, because only those are rules Claude Code applies itself.
204
+ */
205
+ function coveredOf(row, settings, told) {
206
+ if (row.mode !== 'tell' || !row.watched)
207
+ return row;
208
+ const names = row.patterns.flatMap((pattern) => {
209
+ const name = /^\*\*\/([^*?/]+)$/.exec(pattern)?.[1];
210
+ return name === undefined ? [] : [name];
211
+ });
212
+ if (names.length === 0)
213
+ return row;
214
+ const written = FILES.flatMap((file) => settings.held?.[file] ?? []).filter((pattern) => !told.has(pattern));
215
+ const covering = written.filter((pattern) => names.some((name) => matchesGlob(name, pattern) || matchesGlob(`a/${name}`, pattern)));
216
+ const covered = covering.sort((one, two) => one.length - two.length || (one < two ? -1 : 1))[0];
217
+ return covered === undefined ? row : { ...row, covered };
218
+ }
192
219
  function denied(settings, pattern) {
193
220
  return FILES.some((file) => settings.held?.[file]?.includes(pattern) ?? false);
194
221
  }
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { escapeHtml as e } from '../../render/html-report-components.js';
2
4
  import { inLanguages } from '../../render/report-copy.js';
3
5
  import { addFilePopup } from '../../render/ui/add-file-popup.js';
@@ -115,18 +117,25 @@ function uninstallWindow(view) {
115
117
  return '';
116
118
  const command = files.map((file) => 'agentwhy init --remove --watch --refuse' +
117
119
  (view.uninstall[file] ?? []).map((rule) => ' --unprotect ' + quoted(rule)).join('') + shared(file)).join(' && ');
120
+ // AO17: one project's uninstall leaves agentwhy's check in the person's own Codex settings for the other projects
121
+ // (AOD4) - said here, with the unticked choice that takes it out of Codex too.
122
+ const codex = view.codex === undefined ? '' :
123
+ '<label class="set-tick"><input type="checkbox" name="set-uninstall-codex">' +
124
+ '<span>' + inLanguages((t) => t('set.confirm.uninstall.codex')) + '</span></label>';
118
125
  return confirmDialog({
119
126
  id: UNINSTALL_WINDOW,
120
127
  glyph: '',
121
128
  title: inLanguages((t) => t('set.confirm.uninstall')),
122
129
  subject: '',
123
130
  sentence: inLanguages((t) => t('set.confirm.uninstall.text') + (files.includes('shared') ? ' ' + t('set.confirm.everyone') : '')),
124
- option: '',
131
+ option: codex,
125
132
  note: say(),
126
133
  cancel: inLanguages((t) => t('app.cancel')),
127
134
  confirm: inLanguages((t) => t('set.confirm.uninstall.go')),
128
135
  tone: 'primary',
129
- confirmAttributes: write({ change: 'uninstall', rules: view.uninstall }) + commandAttribute('local', command),
136
+ confirmAttributes: write({ change: 'uninstall', rules: view.uninstall }) +
137
+ (view.codex === undefined ? '' : ' data-set-tick="set-uninstall-codex"') +
138
+ commandAttribute('local', command),
130
139
  });
131
140
  }
132
141
  /** The hooks a file runs once the moved ones are in it: `init` with hook flags takes out any it is not given (R4b). */
@@ -167,11 +176,13 @@ function finishWindow(view) {
167
176
  });
168
177
  }
169
178
  /**
170
- * CK5, CK7: Codex's hook, written to match the block Claude Code runs - the page's confirmation is the consent, as for
171
- * every block - and the honesty line under it. Opened as a file, it hands over `agentwhy init --codex`.
179
+ * `codex-approves-its-own-hook` AO8, AO10: agentwhy's Codex check, written to the person's own files and approved by
180
+ * agentwhy itself - the page's confirmation is the consent, as for every block - and the honesty line under it. Opened
181
+ * from `off` and from `stale` alike: the same write installs and heals. Opened as a file, it hands over
182
+ * `agentwhy init --codex`.
172
183
  */
173
184
  function codexWindow(view) {
174
- if (view.codex !== 'off' || !view.stop.on)
185
+ if ((view.codex !== 'off' && view.codex !== 'stale') || !view.stop.on)
175
186
  return '';
176
187
  return confirmDialog({
177
188
  id: CODEX_WINDOW,
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { fileRulesIn } from '../../adapter/claude-code/settings/deny-entries.js';
2
4
  import { missingHookEntries } from '../../adapter/claude-code/settings/hook-entries.js';
3
5
  /**
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  import { escapeHtml as e } from '../../render/html-report-components.js';
2
4
  import { inLanguages } from '../../render/report-copy.js';
3
5
  import { textButton } from '../../render/ui/button.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 '../../render/html-report-components.js';
2
4
  import { inLanguages, labelAttributes } from '../../render/report-copy.js';
3
5
  import { closeButton, pill } from '../../render/ui/button.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 '../../render/html-report-components.js';
2
4
  import { inLanguages, labelAttributes } from '../../render/report-copy.js';
3
5
  import { pill } from '../../render/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 { idInKey } from '../../../core/session-catalogue.js';
3
5
  import { escapeHtml as e } from '../../render/html-report-components.js';
@@ -1,3 +1,5 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
4
  * What only To fix does (`.ai/plans/2026-09-23-to-fix-redesign.md`, step 3). The kit opens and closes the windows and
3
5
  * switches the tabs, and the Fix it wizard writes a mark; this posts an undo, and reads the page again once the server