@agentwhy/cli 0.0.0-dev

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 (400) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +181 -0
  4. package/THIRD_PARTY_NOTICES.md +31 -0
  5. package/dist/adapter/claude-code/contract/assumptions.js +208 -0
  6. package/dist/adapter/claude-code/contract/denials.js +15 -0
  7. package/dist/adapter/claude-code/contract/entry-points.js +15 -0
  8. package/dist/adapter/claude-code/contract/fields.js +73 -0
  9. package/dist/adapter/claude-code/contract/hooks.js +96 -0
  10. package/dist/adapter/claude-code/contract/identifiers.js +15 -0
  11. package/dist/adapter/claude-code/contract/layout.js +29 -0
  12. package/dist/adapter/claude-code/contract/line-types.js +26 -0
  13. package/dist/adapter/claude-code/contract/projects.js +23 -0
  14. package/dist/adapter/claude-code/contract/recognition.js +14 -0
  15. package/dist/adapter/claude-code/contract/session-title.js +20 -0
  16. package/dist/adapter/claude-code/contract/settings.js +27 -0
  17. package/dist/adapter/claude-code/contract/task-notifications.js +33 -0
  18. package/dist/adapter/claude-code/contract/tools.js +37 -0
  19. package/dist/adapter/claude-code/contract/version.js +51 -0
  20. package/dist/adapter/claude-code/discovery/claude-code-project-catalogue.js +129 -0
  21. package/dist/adapter/claude-code/discovery/claude-code-recognition.js +46 -0
  22. package/dist/adapter/claude-code/discovery/claude-code-session-catalogue.js +100 -0
  23. package/dist/adapter/claude-code/discovery/claude-code-session-discovery.js +61 -0
  24. package/dist/adapter/claude-code/discovery/claude-code-session-titles.js +28 -0
  25. package/dist/adapter/claude-code/discovery/discovered-session.js +1 -0
  26. package/dist/adapter/claude-code/discovery/session-entries.js +41 -0
  27. package/dist/adapter/claude-code/discovery/transcript-tail.js +81 -0
  28. package/dist/adapter/claude-code/events/claude-code-session-source.js +197 -0
  29. package/dist/adapter/claude-code/events/delegation-index.js +15 -0
  30. package/dist/adapter/claude-code/events/transcript-scan.js +179 -0
  31. package/dist/adapter/claude-code/hooks/hook-output.js +42 -0
  32. package/dist/adapter/claude-code/hooks/pre-tool-use-input.js +23 -0
  33. package/dist/adapter/claude-code/hooks/stop-input.js +25 -0
  34. package/dist/adapter/claude-code/hooks/subagent-stop-input.js +26 -0
  35. package/dist/adapter/claude-code/policy/deny-rules.js +46 -0
  36. package/dist/adapter/claude-code/probe/claude-code-probe.js +86 -0
  37. package/dist/adapter/claude-code/probe/collectors/agent-tool-input-key-collector.js +24 -0
  38. package/dist/adapter/claude-code/probe/collectors/denial-kind-collector.js +20 -0
  39. package/dist/adapter/claude-code/probe/collectors/denied-call-collector.js +11 -0
  40. package/dist/adapter/claude-code/probe/collectors/identifier-collector.js +19 -0
  41. package/dist/adapter/claude-code/probe/collectors/line-collector.js +1 -0
  42. package/dist/adapter/claude-code/probe/collectors/line-type-collector.js +29 -0
  43. package/dist/adapter/claude-code/probe/collectors/sidechain-collector.js +20 -0
  44. package/dist/adapter/claude-code/probe/collectors/task-notification-collector.js +51 -0
  45. package/dist/adapter/claude-code/probe/collectors/tool-result-reference-collector.js +31 -0
  46. package/dist/adapter/claude-code/probe/collectors/tool-use-collector.js +24 -0
  47. package/dist/adapter/claude-code/probe/collectors/tool-version-collector.js +14 -0
  48. package/dist/adapter/claude-code/probe/collectors/top-level-key-collector.js +14 -0
  49. package/dist/adapter/claude-code/probe/collectors/unknown-line-type-key-collector.js +24 -0
  50. package/dist/adapter/claude-code/probe/collectors/working-directory-collector.js +28 -0
  51. package/dist/adapter/claude-code/probe/doctor-report.js +1 -0
  52. package/dist/adapter/claude-code/probe/meta-tally.js +42 -0
  53. package/dist/adapter/claude-code/probe/session-probe.js +1 -0
  54. package/dist/adapter/claude-code/probe/session-tally.js +41 -0
  55. package/dist/adapter/claude-code/probe/transcript-line.js +30 -0
  56. package/dist/adapter/claude-code/probe/transcript-tally.js +74 -0
  57. package/dist/adapter/claude-code/settings/deny-entries.js +65 -0
  58. package/dist/adapter/claude-code/settings/hook-entries.js +303 -0
  59. package/dist/adapter/codex/contract/actions.js +69 -0
  60. package/dist/adapter/codex/contract/assumptions.js +55 -0
  61. package/dist/adapter/codex/contract/capabilities.js +24 -0
  62. package/dist/adapter/codex/contract/delegations.js +30 -0
  63. package/dist/adapter/codex/contract/deliveries.js +25 -0
  64. package/dist/adapter/codex/contract/envelope.js +33 -0
  65. package/dist/adapter/codex/contract/hook-refusals.js +14 -0
  66. package/dist/adapter/codex/contract/hooks.js +46 -0
  67. package/dist/adapter/codex/contract/messages.js +47 -0
  68. package/dist/adapter/codex/contract/permissions.js +27 -0
  69. package/dist/adapter/codex/contract/reviews.js +13 -0
  70. package/dist/adapter/codex/contract/session.js +31 -0
  71. package/dist/adapter/codex/contract/turns.js +23 -0
  72. package/dist/adapter/codex/contract/version.js +15 -0
  73. package/dist/adapter/codex/discovery/codex-conversations.js +44 -0
  74. package/dist/adapter/codex/discovery/codex-project-catalogue.js +49 -0
  75. package/dist/adapter/codex/discovery/codex-recognition.js +20 -0
  76. package/dist/adapter/codex/discovery/codex-session-catalogue.js +28 -0
  77. package/dist/adapter/codex/discovery/codex-session-discovery.js +115 -0
  78. package/dist/adapter/codex/discovery/codex-session-index.js +21 -0
  79. package/dist/adapter/codex/discovery/codex-session-titles.js +135 -0
  80. package/dist/adapter/codex/discovery/session-header.js +86 -0
  81. package/dist/adapter/codex/discovery/session-owners.js +11 -0
  82. package/dist/adapter/codex/discovery/session-roots.js +49 -0
  83. package/dist/adapter/codex/events/action-items.js +138 -0
  84. package/dist/adapter/codex/events/capability-records.js +26 -0
  85. package/dist/adapter/codex/events/codex-session-source.js +121 -0
  86. package/dist/adapter/codex/events/hook-refusals.js +27 -0
  87. package/dist/adapter/codex/events/rollout-scan.js +614 -0
  88. package/dist/adapter/codex/events/turn-permissions.js +60 -0
  89. package/dist/adapter/codex/hooks/pre-tool-use-input.js +24 -0
  90. package/dist/adapter/codex/hooks/stop-refusals.js +57 -0
  91. package/dist/adapter/codex/probe/codex-doctor-report.js +1 -0
  92. package/dist/adapter/codex/probe/codex-probe.js +268 -0
  93. package/dist/adapter/codex/settings/codex-hooks.js +83 -0
  94. package/dist/cli/cli-command.js +1 -0
  95. package/dist/cli/command-router.js +42 -0
  96. package/dist/cli/commands/check-cli-command.js +128 -0
  97. package/dist/cli/commands/codex-stop-cli-command.js +27 -0
  98. package/dist/cli/commands/doctor-arguments.js +32 -0
  99. package/dist/cli/commands/doctor-cli-command.js +31 -0
  100. package/dist/cli/commands/doctor-usage.js +14 -0
  101. package/dist/cli/commands/init-cli-command.js +123 -0
  102. package/dist/cli/commands/menu-cli-command.js +58 -0
  103. package/dist/cli/commands/notify-cli-command.js +158 -0
  104. package/dist/cli/commands/refuse-cli-command.js +79 -0
  105. package/dist/cli/commands/report-arguments.js +62 -0
  106. package/dist/cli/commands/report-cli-command.js +36 -0
  107. package/dist/cli/commands/report-usage.js +41 -0
  108. package/dist/cli/commands/sessions-cli-command.js +184 -0
  109. package/dist/cli/commands/start-cli-command.js +83 -0
  110. package/dist/cli/commands/watch-cli-command.js +142 -0
  111. package/dist/cli/exit-codes.js +13 -0
  112. package/dist/cli/project-switches.js +46 -0
  113. package/dist/cli.js +80 -0
  114. package/dist/composition-root.js +411 -0
  115. package/dist/core/access/command-line.js +454 -0
  116. package/dist/core/access/listing.js +58 -0
  117. package/dist/core/access/path-shape.js +59 -0
  118. package/dist/core/access/path-tokens.js +37 -0
  119. package/dist/core/access/protected-access.js +310 -0
  120. package/dist/core/access/protected-values.js +102 -0
  121. package/dist/core/access/recorded-effect.js +67 -0
  122. package/dist/core/access/returns.js +173 -0
  123. package/dist/core/access/search-output.js +34 -0
  124. package/dist/core/access/search-reach.js +268 -0
  125. package/dist/core/access/uses.js +143 -0
  126. package/dist/core/access/words-before.js +52 -0
  127. package/dist/core/agent.js +1 -0
  128. package/dist/core/capability.js +11 -0
  129. package/dist/core/combined-project-catalogue.js +36 -0
  130. package/dist/core/combined-session-catalogue.js +25 -0
  131. package/dist/core/completeness.js +10 -0
  132. package/dist/core/context.js +1 -0
  133. package/dist/core/correlation/correlate.js +329 -0
  134. package/dist/core/delegation.js +1 -0
  135. package/dist/core/detector/prompt-signals.js +78 -0
  136. package/dist/core/entry-point.js +1 -0
  137. package/dist/core/event.js +1 -0
  138. package/dist/core/evidence.js +9 -0
  139. package/dist/core/late-message.js +32 -0
  140. package/dist/core/message.js +1 -0
  141. package/dist/core/nearest-project.js +28 -0
  142. package/dist/core/policy/default-policy.js +32 -0
  143. package/dist/core/policy/glob.js +56 -0
  144. package/dist/core/policy/parse-policy.js +70 -0
  145. package/dist/core/policy/policy.js +20 -0
  146. package/dist/core/policy/resolve-policy.js +16 -0
  147. package/dist/core/project-catalogue.js +1 -0
  148. package/dist/core/project-root.js +133 -0
  149. package/dist/core/provider-titles.js +10 -0
  150. package/dist/core/redaction/entropy.js +46 -0
  151. package/dist/core/redaction/redacted.js +4 -0
  152. package/dist/core/redaction/redactor.js +339 -0
  153. package/dist/core/redaction/secret-patterns.js +22 -0
  154. package/dist/core/redaction/value-shapes.js +81 -0
  155. package/dist/core/redaction/value-trace.js +110 -0
  156. package/dist/core/review.js +1 -0
  157. package/dist/core/session-catalogue.js +19 -0
  158. package/dist/core/session-filter.js +42 -0
  159. package/dist/core/session-format.js +27 -0
  160. package/dist/core/session-model.js +1 -0
  161. package/dist/core/session-reader.js +22 -0
  162. package/dist/core/session-records.js +1 -0
  163. package/dist/core/session-source.js +1 -0
  164. package/dist/core/session-titles.js +21 -0
  165. package/dist/core/turn.js +1 -0
  166. package/dist/doctor/codex-doctor.js +14 -0
  167. package/dist/doctor/doctor-use-case.js +1 -0
  168. package/dist/doctor/format-selecting-doctor.js +28 -0
  169. package/dist/doctor/render/attention/attention-rule.js +1 -0
  170. package/dist/doctor/render/attention/doctor-attention-rules.js +28 -0
  171. package/dist/doctor/render/attention/rules/damaged-meta-files-rule.js +8 -0
  172. package/dist/doctor/render/attention/rules/damaged-transcripts-rule.js +9 -0
  173. package/dist/doctor/render/attention/rules/incomplete-subagent-pairs-rule.js +6 -0
  174. package/dist/doctor/render/attention/rules/missing-main-transcript-rule.js +5 -0
  175. package/dist/doctor/render/attention/rules/missing-spilled-results-rule.js +6 -0
  176. package/dist/doctor/render/attention/rules/multiple-working-directories-rule.js +15 -0
  177. package/dist/doctor/render/attention/rules/unknown-denial-kinds-rule.js +5 -0
  178. package/dist/doctor/render/attention/rules/unknown-line-types-rule.js +6 -0
  179. package/dist/doctor/render/attention/rules/unrecognised-entries-rule.js +6 -0
  180. package/dist/doctor/render/attention/rules/unusable-directories-rule.js +12 -0
  181. package/dist/doctor/render/attention/transcript-scopes.js +6 -0
  182. package/dist/doctor/render/codex-text-doctor-renderer.js +97 -0
  183. package/dist/doctor/render/doctor-renderers.js +1 -0
  184. package/dist/doctor/render/json-doctor-renderer.js +7 -0
  185. package/dist/doctor/render/output-format.js +1 -0
  186. package/dist/doctor/render/text-doctor-renderer.js +124 -0
  187. package/dist/doctor/session-doctor.js +16 -0
  188. package/dist/infrastructure/clack-asker.js +24 -0
  189. package/dist/infrastructure/clack-chooser.js +40 -0
  190. package/dist/infrastructure/clack-multi-chooser.js +31 -0
  191. package/dist/infrastructure/file-alert-store.js +120 -0
  192. package/dist/infrastructure/file-checked-store.js +55 -0
  193. package/dist/infrastructure/file-mark-store.js +67 -0
  194. package/dist/infrastructure/file-onboarding-store.js +93 -0
  195. package/dist/infrastructure/folder-window.js +26 -0
  196. package/dist/infrastructure/node-agentwhy-invocation.js +99 -0
  197. package/dist/infrastructure/node-browser.js +40 -0
  198. package/dist/infrastructure/node-file-system.js +111 -0
  199. package/dist/infrastructure/node-local-server.js +115 -0
  200. package/dist/infrastructure/node-text-input.js +26 -0
  201. package/dist/infrastructure/os-notifier.js +21 -0
  202. package/dist/infrastructure/osascript-folder-chooser.js +53 -0
  203. package/dist/infrastructure/powershell-folder-chooser.js +78 -0
  204. package/dist/infrastructure/real-directory.js +18 -0
  205. package/dist/infrastructure/stream-printer.js +11 -0
  206. package/dist/infrastructure/terminal-banner.js +19 -0
  207. package/dist/ports/agentwhy-invocation.js +1 -0
  208. package/dist/ports/alert-store.js +1 -0
  209. package/dist/ports/asker.js +1 -0
  210. package/dist/ports/browser.js +1 -0
  211. package/dist/ports/checked-store.js +1 -0
  212. package/dist/ports/chooser.js +1 -0
  213. package/dist/ports/directory-reader.js +1 -0
  214. package/dist/ports/file-access-error.js +11 -0
  215. package/dist/ports/file-reader.js +1 -0
  216. package/dist/ports/file-tail-reader.js +1 -0
  217. package/dist/ports/file-writer.js +1 -0
  218. package/dist/ports/folder-chooser.js +1 -0
  219. package/dist/ports/local-server.js +1 -0
  220. package/dist/ports/mark-store.js +10 -0
  221. package/dist/ports/multi-chooser.js +1 -0
  222. package/dist/ports/notifier.js +1 -0
  223. package/dist/ports/onboarding-store.js +1 -0
  224. package/dist/ports/printer.js +1 -0
  225. package/dist/ports/text-input.js +1 -0
  226. package/dist/refuse/command-reach.js +135 -0
  227. package/dist/refuse/command-refusal.js +88 -0
  228. package/dist/refuse/render/codex-stop-words.js +21 -0
  229. package/dist/refuse/render/refusal-words.js +34 -0
  230. package/dist/report/build-report.js +1037 -0
  231. package/dist/report/check/actions-digest.js +73 -0
  232. package/dist/report/check/check-lines.js +83 -0
  233. package/dist/report/check/mark-request.js +90 -0
  234. package/dist/report/check/marks.js +84 -0
  235. package/dist/report/check/render/text-digest-renderer.js +332 -0
  236. package/dist/report/check/session-actions.js +90 -0
  237. package/dist/report/check/session-check.js +91 -0
  238. package/dist/report/choose-policy.js +61 -0
  239. package/dist/report/flow-reads.js +18 -0
  240. package/dist/report/private-files/tell-lists.js +84 -0
  241. package/dist/report/project-rules.js +33 -0
  242. package/dist/report/render/home-relative.js +18 -0
  243. package/dist/report/render/html-head.js +26 -0
  244. package/dist/report/render/html-report-components.js +48 -0
  245. package/dist/report/render/html-report-styles.js +500 -0
  246. package/dist/report/render/html-sidebar.js +42 -0
  247. package/dist/report/render/path-tail.js +14 -0
  248. package/dist/report/render/report-copy.js +1177 -0
  249. package/dist/report/render/report-page/advanced-view.js +167 -0
  250. package/dist/report/render/report-page/file-story.js +114 -0
  251. package/dist/report/render/report-page/files-view.js +381 -0
  252. package/dist/report/render/report-page/files.js +77 -0
  253. package/dist/report/render/report-page/fix-wizard-script.js +269 -0
  254. package/dist/report/render/report-page/fix-wizard.js +310 -0
  255. package/dist/report/render/report-page/helpers-view.js +383 -0
  256. package/dist/report/render/report-page/helpers.js +51 -0
  257. package/dist/report/render/report-page/item-names.js +63 -0
  258. package/dist/report/render/report-page/protect-patterns.js +13 -0
  259. package/dist/report/render/report-page/providers.js +208 -0
  260. package/dist/report/render/report-page/record-view.js +367 -0
  261. package/dist/report/render/report-page/report-page-renderer.js +129 -0
  262. package/dist/report/render/report-page/report-views.js +31 -0
  263. package/dist/report/render/report-page/story-window.js +356 -0
  264. package/dist/report/render/report-page/times.js +25 -0
  265. package/dist/report/render/report-page/to-do-view.js +299 -0
  266. package/dist/report/render/report-page/to-do.js +152 -0
  267. package/dist/report/render/report-page.js +1 -0
  268. package/dist/report/render/session-view.js +250 -0
  269. package/dist/report/render/text-report-renderer.js +981 -0
  270. package/dist/report/render/ui/add-file-popup.js +122 -0
  271. package/dist/report/render/ui/app-sidebar.js +78 -0
  272. package/dist/report/render/ui/ask-panel.js +80 -0
  273. package/dist/report/render/ui/avatar.js +19 -0
  274. package/dist/report/render/ui/backdrop.js +29 -0
  275. package/dist/report/render/ui/brand-mark.js +4 -0
  276. package/dist/report/render/ui/button.js +67 -0
  277. package/dist/report/render/ui/callout.js +20 -0
  278. package/dist/report/render/ui/chat-example.js +17 -0
  279. package/dist/report/render/ui/checklist.js +50 -0
  280. package/dist/report/render/ui/confirm-dialog.js +34 -0
  281. package/dist/report/render/ui/data-table.js +49 -0
  282. package/dist/report/render/ui/drawer.js +25 -0
  283. package/dist/report/render/ui/file-chip.js +37 -0
  284. package/dist/report/render/ui/fold-line.js +22 -0
  285. package/dist/report/render/ui/guide-card.js +25 -0
  286. package/dist/report/render/ui/hero.js +37 -0
  287. package/dist/report/render/ui/labelled-select.js +10 -0
  288. package/dist/report/render/ui/live-script.js +222 -0
  289. package/dist/report/render/ui/local-date.js +58 -0
  290. package/dist/report/render/ui/mode-icon.js +6 -0
  291. package/dist/report/render/ui/page-shell.js +101 -0
  292. package/dist/report/render/ui/pill-tabs.js +47 -0
  293. package/dist/report/render/ui/popup.js +77 -0
  294. package/dist/report/render/ui/progress.js +35 -0
  295. package/dist/report/render/ui/project-list.js +284 -0
  296. package/dist/report/render/ui/scope-choice.js +19 -0
  297. package/dist/report/render/ui/stats.js +21 -0
  298. package/dist/report/render/ui/status-icon.js +30 -0
  299. package/dist/report/render/ui/status-look.js +21 -0
  300. package/dist/report/render/ui/switch.js +11 -0
  301. package/dist/report/render/ui/tag.js +19 -0
  302. package/dist/report/render/ui/task-list.js +56 -0
  303. package/dist/report/render/ui/tokens.js +46 -0
  304. package/dist/report/render/ui/update-notice.js +146 -0
  305. package/dist/report/render/ui/words/app-words.js +163 -0
  306. package/dist/report/render/ui/words/conversations-words.js +457 -0
  307. package/dist/report/render/ui/words/month-words.js +59 -0
  308. package/dist/report/render/ui/words/onboarding-words.js +446 -0
  309. package/dist/report/render/ui/words/projects-words.js +217 -0
  310. package/dist/report/render/ui/words/report-words.js +2099 -0
  311. package/dist/report/render/ui/words/settings-words.js +611 -0
  312. package/dist/report/render/ui/words/to-fix-words.js +287 -0
  313. package/dist/report/report-model.js +2 -0
  314. package/dist/report/report-use-case.js +1 -0
  315. package/dist/report/rule-names.js +20 -0
  316. package/dist/report/session-report.js +163 -0
  317. package/dist/report/start/app-nav.js +37 -0
  318. package/dist/report/start/conversations/calendar-view.js +104 -0
  319. package/dist/report/start/conversations/conversation-columns.js +187 -0
  320. package/dist/report/start/conversations/conversations-renderer.js +54 -0
  321. package/dist/report/start/conversations/empty-week.js +120 -0
  322. package/dist/report/start/conversations/files-window.js +166 -0
  323. package/dist/report/start/conversations/period-section.js +280 -0
  324. package/dist/report/start/conversations/periods-script.js +440 -0
  325. package/dist/report/start/conversations/periods.js +110 -0
  326. package/dist/report/start/conversations/week-view.js +30 -0
  327. package/dist/report/start/conversations/weeks.js +17 -0
  328. package/dist/report/start/month/month-renderer.js +50 -0
  329. package/dist/report/start/month/month-view.js +135 -0
  330. package/dist/report/start/month/months.js +17 -0
  331. package/dist/report/start/onboarding/done.js +171 -0
  332. package/dist/report/start/onboarding/finish-onboarding.js +93 -0
  333. package/dist/report/start/onboarding/intro.js +100 -0
  334. package/dist/report/start/onboarding/onboarding-changes.js +40 -0
  335. package/dist/report/start/onboarding/onboarding-renderer.js +195 -0
  336. package/dist/report/start/onboarding/onboarding-script.js +400 -0
  337. package/dist/report/start/onboarding/onboarding-view.js +98 -0
  338. package/dist/report/start/onboarding/project-step.js +210 -0
  339. package/dist/report/start/onboarding/steps.js +239 -0
  340. package/dist/report/start/onboarding/welcome.js +66 -0
  341. package/dist/report/start/page-notice.js +10 -0
  342. package/dist/report/start/projects/index-projects.js +43 -0
  343. package/dist/report/start/projects/moved-page.js +82 -0
  344. package/dist/report/start/projects/project-switch.js +138 -0
  345. package/dist/report/start/projects/projects-window.js +67 -0
  346. package/dist/report/start/projects/temporary-space.js +18 -0
  347. package/dist/report/start/render/air-datepicker-vendor.js +27 -0
  348. package/dist/report/start/render/session-status.js +41 -0
  349. package/dist/report/start/render/start-words.js +84 -0
  350. package/dist/report/start/repository.js +28 -0
  351. package/dist/report/start/serve/index-handler.js +372 -0
  352. package/dist/report/start/serve/onboarding-request.js +50 -0
  353. package/dist/report/start/serve/settings-request.js +1 -0
  354. package/dist/report/start/serve/settings-setup.js +193 -0
  355. package/dist/report/start/session-index.js +1 -0
  356. package/dist/report/start/session-start.js +974 -0
  357. package/dist/report/start/settings/alerts-tab.js +104 -0
  358. package/dist/report/start/settings/files-tab.js +169 -0
  359. package/dist/report/start/settings/general-tab.js +71 -0
  360. package/dist/report/start/settings/settings-renderer.js +254 -0
  361. package/dist/report/start/settings/settings-script.js +161 -0
  362. package/dist/report/start/settings/settings-view.js +240 -0
  363. package/dist/report/start/settings/settings-windows.js +334 -0
  364. package/dist/report/start/settings-files.js +43 -0
  365. package/dist/report/start/to-fix/done-list.js +45 -0
  366. package/dist/report/start/to-fix/file-window.js +110 -0
  367. package/dist/report/start/to-fix/to-fix-list.js +70 -0
  368. package/dist/report/start/to-fix/to-fix-renderer.js +237 -0
  369. package/dist/report/start/to-fix/to-fix-script.js +94 -0
  370. package/dist/report/start/to-fix/to-fix-view.js +124 -0
  371. package/dist/report/watch/agent-alert.js +72 -0
  372. package/dist/report/watch/notice-choices.js +35 -0
  373. package/dist/report/watch/notice-settings.js +65 -0
  374. package/dist/report/watch/preferences.js +100 -0
  375. package/dist/report/watch/render/instruction-words.js +54 -0
  376. package/dist/report/watch/render/notice-words.js +182 -0
  377. package/dist/report/watch/subagent-watch.js +345 -0
  378. package/dist/report/watch/watch-notice.js +1 -0
  379. package/dist/setup/behind.js +18 -0
  380. package/dist/setup/codex-mirror.js +171 -0
  381. package/dist/setup/not-a-project.js +19 -0
  382. package/dist/setup/project-setup.js +644 -0
  383. package/dist/setup/protected-patterns.js +30 -0
  384. package/dist/shared/colour.js +23 -0
  385. package/dist/shared/compare.js +4 -0
  386. package/dist/shared/content-version.js +13 -0
  387. package/dist/shared/counter.js +11 -0
  388. package/dist/shared/file-stamp.js +4 -0
  389. package/dist/shared/json.js +13 -0
  390. package/dist/shared/label.js +13 -0
  391. package/dist/shared/package-name.js +6 -0
  392. package/dist/shared/plain-invocation.js +44 -0
  393. package/dist/shared/plain-version.js +21 -0
  394. package/dist/shared/printable.js +25 -0
  395. package/dist/shared/renderer.js +1 -0
  396. package/dist/shared/sentence.js +20 -0
  397. package/dist/shared/terminal-logo.js +41 -0
  398. package/dist/shared/text-table.js +15 -0
  399. package/dist/shared/wrap.js +29 -0
  400. package/package.json +70 -0
@@ -0,0 +1,1037 @@
1
+ import { programsIn } from '../core/access/command-line.js';
2
+ import { printedLines, protectedAccesses, readsContent } from '../core/access/protected-access.js';
3
+ import { filesTracedIn, returnsOf, traceValues } from '../core/access/returns.js';
4
+ import { valueUses } from '../core/access/uses.js';
5
+ import { wordsBeforeCalls } from '../core/access/words-before.js';
6
+ import { firstSentence } from '../shared/sentence.js';
7
+ import { NAMES_PER_FILE } from './report-model.js';
8
+ import { filesRead } from './flow-reads.js';
9
+ /**
10
+ * Identifiers are shortened in a report (§7.3). Both ends are kept: a `toolu_01…` prefix is shared by every
11
+ * call in a session, so a head-only abbreviation makes two different calls look like the same one.
12
+ */
13
+ const SHORTEN_ABOVE = 20;
14
+ const HEAD = 10;
15
+ const TAIL = 4;
16
+ /** Programs that carry text rather than reach a file; naming them in a finding only adds noise. */
17
+ const NAMES_NOTHING = new Set(['echo', 'printf', 'true', 'false', ':', 'sleep', 'cd', 'export']);
18
+ const GAP_WORDS = {
19
+ 'session-missing': 'the session transcript itself could not be read, so nothing here is a finding',
20
+ 'source-missing': 'a source was absent or could not be read',
21
+ 'record-damaged': 'a record could not be parsed',
22
+ 'result-missing': 'a call has no result, so its outcome is unknown',
23
+ 'spilled-result-missing': 'a result points at a spilled file that is gone',
24
+ 'outcome-unrecognised': 'an outcome marker carried a value this version does not know',
25
+ 'relation-unresolved': 'identifiers did not allow a confident join, so no outcome was assigned',
26
+ 'result-incomplete': 'a text may not hold everything, so what is not in it cannot be said',
27
+ 'capability-absent': 'the record does not show all of this',
28
+ 'capability-unmeasured': 'whether the record shows all of this has not been measured',
29
+ };
30
+ /** The order a call's reached steps take when its files had different outcomes: the established one first. */
31
+ const OUTCOME_ORDER = ['succeeded', 'blocked', 'unknown'];
32
+ /** What a capability gap cannot answer, completing its words (`2026-09-27-what-codex-wrote.md` X23). */
33
+ const QUESTION_WORDS = {
34
+ actions: 'which actions ran',
35
+ access: 'what an action reached',
36
+ 'output-delivery': 'what reached the AI',
37
+ 'own-words': 'everything the AI wrote',
38
+ reasoning: 'what the AI worked out before acting',
39
+ refusals: 'whether an action was refused',
40
+ permissions: 'which permissions were in force',
41
+ };
42
+ const FULL_VIEW = { share: false, projectRoot: { kind: 'absent' } };
43
+ export function buildReport(model, policy, redactor, view = FULL_VIEW) {
44
+ const accesses = protectedAccesses(model, policy);
45
+ // The trace is built by the redactor, which owns this run's salt: what it holds means nothing outside this report.
46
+ // One trace for the run: what came back and what an agent wrote before a call ask the same question of it.
47
+ const traced = traceValues(model, accesses, (values) => redactor.trace(values));
48
+ const returns = returnsOf(model, accesses, traced);
49
+ const uses = valueUses(model, accesses, traced);
50
+ // F57: which patterns a person asked only to be told about, so a finding under one says the agent was let read it.
51
+ const told = new Set(policy.protected.filter((entry) => entry.mode === 'tell').map((entry) => entry.pattern));
52
+ const findings = accesses.map((access) => toFinding(access, model, redactor, view, told.has(access.pattern)));
53
+ const secretShapes = secretShapesIn(model, redactor, view);
54
+ const delegations = model.delegations.map((delegation) => toDelegation(delegation, model, accesses, redactor, view));
55
+ const gaps = gapsOf(model.gaps, returns);
56
+ const attributed = new Set(model.delegations.map((delegation) => delegation.childAgentId));
57
+ const unattributedAgents = model.agents
58
+ .filter((agent) => agent.id !== model.sessionId && !attributed.has(agent.id))
59
+ .map((agent) => toAgent(agent, model, accesses, redactor, view));
60
+ const wordsBefore = wordsBeforeCalls(model);
61
+ const stories = storiesOf(model, accesses, wordsBefore, traced, redactor, view);
62
+ const statements = returns.map((entry) => toReturn(entry, model, redactor, view));
63
+ // The graph and the flows are built before the tally, because one of its counts is read from them: how many
64
+ // agents the contents of a protected file reached. That is the question the headline answers first, and every
65
+ // other view of this session - the index among them - must answer it with the same number.
66
+ const graph = graphOf(model, accesses, returns, uses, traced, redactor, view);
67
+ const flows = flowsOf(model, accesses, uses, returns, wordsBefore, traced, redactor, view);
68
+ const tally = tallyOf(accesses, returns, uses, contentsSeenIn(graph, flows));
69
+ return {
70
+ headline: headlineOf(tally, coverageOf(model), redactor),
71
+ tally,
72
+ graph,
73
+ stories,
74
+ // The scope is built last: the redactor can only report what it did once everything has passed through it.
75
+ scope: scopeOf(model, policy, redactor, view),
76
+ summary: {
77
+ agents: model.agents.length,
78
+ delegations: model.delegations.length,
79
+ actions: model.events.length,
80
+ byOutcome: countOutcomes(model.events),
81
+ findings: findings.length,
82
+ secretShapes: secretShapes.length,
83
+ unattributedAgents: unattributedAgents.length,
84
+ tools: toolCounts(model.events),
85
+ },
86
+ mainAgent: model.events
87
+ .filter((event) => event.agentId === model.sessionId)
88
+ .sort((first, second) => first.sequence - second.sequence)
89
+ .map((event) => toAction(event, model, accesses, redactor, view)),
90
+ delegations,
91
+ unattributedAgents,
92
+ findings,
93
+ secretShapes,
94
+ returns: statements,
95
+ uses: uses.map((use) => toUse(use, model, redactor, view)),
96
+ flows,
97
+ privateFiles: privateFilesOf(model, accesses, redactor),
98
+ everydayFiles: everydayFilesOf(model, accesses, redactor),
99
+ missing: gaps.map((gap) => redactor.term(missingWords(gap))),
100
+ gaps,
101
+ recorded: recordedOf(model, redactor, view),
102
+ };
103
+ }
104
+ /**
105
+ * What the runtime recorded beside the actions, across the redaction boundary (`2026-09-27-what-codex-wrote.md` X19-X23):
106
+ * settings through the term and path doors, reviews by agent index and evidence, and every free text by its size only.
107
+ */
108
+ function recordedOf(model, redactor, view) {
109
+ const indexOf = (agentId) => model.agents.findIndex((agent) => agent.id === agentId);
110
+ const grouped = new Map();
111
+ for (const turn of model.turns) {
112
+ if (turn.permissions === undefined)
113
+ continue;
114
+ const key = JSON.stringify(turn.permissions);
115
+ const group = grouped.get(key) ?? { permissions: turn.permissions, turns: 0, agents: new Set() };
116
+ group.turns += 1;
117
+ if (indexOf(turn.agentId) >= 0)
118
+ group.agents.add(indexOf(turn.agentId));
119
+ grouped.set(key, group);
120
+ }
121
+ const capabilities = new Map();
122
+ for (const record of model.capabilities) {
123
+ const key = `${record.question} ${record.state}`;
124
+ const known = capabilities.get(key);
125
+ capabilities.set(key, { question: record.question, state: record.state, sources: (known?.sources ?? 0) + 1 });
126
+ }
127
+ const contexts = { code: 0, conversation: 0, 'unjoined-instruction': 0, 'orphan-output': 0 };
128
+ for (const context of model.contexts)
129
+ contexts[context.kind] += 1;
130
+ return {
131
+ permissions: [...grouped.values()].map(({ permissions, turns, agents }) => ({
132
+ turns,
133
+ agentIndexes: [...agents].sort((first, second) => first - second),
134
+ ...(permissions.approval === undefined ? {} : { approval: redactor.term(permissions.approval) }),
135
+ approver: permissions.approver,
136
+ ...(permissions.sandbox === undefined ? {} : { sandbox: redactor.term(permissions.sandbox) }),
137
+ network: permissions.network,
138
+ readScopes: permissions.scopes.filter((scope) => scope.access === 'read').length,
139
+ writeScopes: permissions.scopes.filter((scope) => scope.access === 'write').length,
140
+ writablePaths: [...new Set(permissions.scopes.flatMap((scope) => scope.access === 'write' && scope.target.kind === 'path' ? [redactor.path(scope.target.path)] : []))],
141
+ complete: permissions.complete,
142
+ })),
143
+ reviews: model.reviews.map((review) => ({
144
+ ...(indexOf(review.reviewedAgentId) < 0 ? {} : { reviewedAgentIndex: indexOf(review.reviewedAgentId) }),
145
+ verdicts: review.verdicts.map((verdict) => ({
146
+ outcome: verdict.outcome,
147
+ turnKnown: verdict.turnId !== undefined,
148
+ ...(verdict.risk === undefined ? {} : { risk: redactor.term(verdict.risk) }),
149
+ ...(verdict.rationale === undefined ? {} : { rationaleSize: verdict.rationale.length }),
150
+ evidence: redactor.term(describeEvidence(verdict.evidence, model, view)),
151
+ })),
152
+ })),
153
+ capabilities: [...capabilities.values()],
154
+ deliveries: model.deliveries.length,
155
+ contexts,
156
+ };
157
+ }
158
+ /**
159
+ * What reading each protected file left behind (`specs/2026-09-23-the-report-page.md` M1, M2): the key formats and the
160
+ * variable names in its text, by name only. A read is counted for a file only when it named that one protected file and
161
+ * its result is that file's text; a read of two at once (`cat a.env b.env`) is given to neither, and both say so,
162
+ * because telling their lines apart would be a guess.
163
+ */
164
+ function privateFilesOf(model, accesses, redactor) {
165
+ const found = new Map();
166
+ const named = new Map();
167
+ for (const access of accesses) {
168
+ if (!found.has(access.path))
169
+ found.set(access.path, { keys: [], names: [], keyed: [], mixed: false });
170
+ if (access.source !== 'input')
171
+ continue;
172
+ named.set(access.eventId, (named.get(access.eventId) ?? new Set()).add(access.path));
173
+ }
174
+ const printed = new Set(accesses.filter((access) => access.lines !== undefined).map((access) => access.eventId));
175
+ for (const event of model.events) {
176
+ const content = event.result?.content;
177
+ const paths = [...(named.get(event.id) ?? [])];
178
+ // H2, H6: a search says whose each line is, so its lines are read for their own file only, and never `mixed`.
179
+ if (printed.has(event.id)) {
180
+ for (const [path, lines] of printedLines(event, paths)) {
181
+ const file = found.get(path);
182
+ if (file !== undefined)
183
+ readInto(file, lines.join('\n'), redactor);
184
+ }
185
+ }
186
+ if (paths.length === 0 || content === undefined || event.outcome !== 'succeeded' || !readsContent(event))
187
+ continue;
188
+ if (paths.length > 1) {
189
+ for (const path of paths)
190
+ found.get(path).mixed = true;
191
+ continue;
192
+ }
193
+ const file = found.get(paths[0]);
194
+ if (file !== undefined)
195
+ readInto(file, content, redactor);
196
+ }
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) })),
202
+ ...(file.mixed ? { mixed: true } : {}),
203
+ }));
204
+ }
205
+ /** What one file's text shows, through the redactor (M2, M2a): key classes, variable names, keyed lines - never a value. */
206
+ function readInto(file, content, redactor) {
207
+ for (const key of redactor.keyClassesIn(content))
208
+ if (!file.keys.includes(key))
209
+ file.keys.push(key);
210
+ for (const name of redactor.keysIn(content)) {
211
+ if (file.names.length < NAMES_PER_FILE && !file.names.includes(name))
212
+ file.names.push(name);
213
+ }
214
+ for (const line of redactor.keyedIn(content)) {
215
+ if (file.keyed.length < NAMES_PER_FILE && !file.keyed.some((each) => each.name === line.name))
216
+ file.keyed.push(line);
217
+ }
218
+ }
219
+ /**
220
+ * The files no protected pattern matches that a file tool worked on (M6): a known tool whose result is the named
221
+ * file's text, or which writes it. A call whose outcome was not recorded is left out - it may not have reached the
222
+ * file, and a list of what the AI opened must not grow by what it may have opened.
223
+ */
224
+ function everydayFilesOf(model, accesses, redactor) {
225
+ const RANK = { changed: 2, read: 1, stopped: 0 };
226
+ const guarded = new Set(accesses.map((access) => access.eventId + '\u0000' + access.path));
227
+ const delegating = new Set(model.delegations.map((delegation) => delegation.id));
228
+ const files = new Map();
229
+ for (const event of model.events) {
230
+ if (!event.toolKnown || delegating.has(event.id) || event.outcome === 'unknown')
231
+ continue;
232
+ const writes = event.written !== undefined;
233
+ if (!writes && event.resultShape !== 'content')
234
+ continue;
235
+ const how = event.outcome === 'blocked' ? 'stopped' : writes ? 'changed' : 'read';
236
+ for (const path of new Set(event.targets)) {
237
+ if (path === '' || guarded.has(event.id + '\u0000' + path))
238
+ continue;
239
+ const known = files.get(path);
240
+ files.set(path, { calls: (known?.calls ?? 0) + 1, how: known === undefined || RANK[how] > RANK[known.how] ? how : known.how });
241
+ }
242
+ }
243
+ return [...files].map(([path, file]) => ({ path: redactor.path(path), calls: file.calls, how: file.how }));
244
+ }
245
+ /**
246
+ * A recognised key format in what came back - `S-shape` of §5.4. It is reported even when the call touched no
247
+ * protected path: a secret exported in a shell profile sits in no `.env` file, so path matching is blind to it.
248
+ */
249
+ function secretShapesIn(model, redactor, view) {
250
+ return model.events.flatMap((event) => {
251
+ const content = event.result?.content;
252
+ if (content === undefined)
253
+ return [];
254
+ const classes = redactor.classesIn(content);
255
+ if (classes.length === 0)
256
+ return [];
257
+ const agentIndex = model.agents.findIndex((agent) => agent.id === event.agentId);
258
+ return [
259
+ {
260
+ ...(agentIndex < 0 ? {} : { agentIndex }),
261
+ tool: redactor.term(event.toolName),
262
+ outcome: event.outcome,
263
+ classes: classes.map((name) => redactor.term(name)),
264
+ evidence: redactor.term(describeEvidence(event.result?.evidence ?? event.evidence, model, view)),
265
+ },
266
+ ];
267
+ });
268
+ }
269
+ /**
270
+ * What the agent wrote before this call, across the redaction boundary (`specs/2026-09-15-why-this-call.md` R6, R7). The
271
+ * sentence is shown only when those words carried no traced value and the view is not the shared one; how much was
272
+ * written, and before how many calls, is shown either way. The words are never introduced as a reason: judging an
273
+ * instruction is the detector's work, behind the D1 gate.
274
+ */
275
+ function wroteBefore(before, model, traced, redactor, view) {
276
+ const words = before?.words ?? [];
277
+ // A block the record kept empty is not words, and is not nothing either: both are reported, each as itself.
278
+ const spoken = words.filter((message) => message.text.trim() !== '');
279
+ const texts = spoken.map((message) => message.text);
280
+ const carried = filesTracedIn(texts, traced);
281
+ const first = spoken[0];
282
+ const quotable = first !== undefined && carried.length === 0 && !view.share;
283
+ const sentence = quotable && first !== undefined ? firstSentence(first.text) : undefined;
284
+ return {
285
+ size: texts.reduce((total, text) => total + text.length, 0),
286
+ blank: words.length - spoken.length,
287
+ covers: before?.covers ?? 1,
288
+ ...(sentence === undefined ? {} : { sentence: redactor.scan(sentence.text) }),
289
+ ...(sentence === undefined || first === undefined ? {} : { kind: first.kind }),
290
+ ...(sentence?.cut === true ? { cut: true } : {}),
291
+ carried: [...new Set(carried.map((path) => redactor.path(path)))],
292
+ evidence: redactor.term(first === undefined ? 'nothing before this call' : describeEvidence(first.evidence, model, view)),
293
+ };
294
+ }
295
+ /**
296
+ * The findings as numbers, counted once (`Tally`). Two rules decide every count here:
297
+ *
298
+ * - A **refused** attempt reached nothing, so it is never counted as a file that was reached. Saying "30 files
299
+ * were reached, and 3 attempts were refused" while the 3 sit inside the 30 is a sentence that misleads by
300
+ * arithmetic alone.
301
+ * - "No call ever named it" means **no** call named it. A path that one command named and another only printed
302
+ * belongs to the named side; the result route is the one where nothing was there to inspect.
303
+ */
304
+ function tallyOf(accesses, returns, uses, contentsSeen) {
305
+ // X11: reached is what the record establishes. Counting every attempt that was not refused counted a missing file
306
+ // `cat` failed on, and a call with no result, as a file reached.
307
+ const reached = accesses.filter((access) => access.outcome === 'succeeded');
308
+ const files = new Set(reached.map((access) => access.path));
309
+ const named = new Set(reached.filter((access) => access.source === 'input').map((access) => access.path));
310
+ const onlyThroughResult = [...files].filter((path) => !named.has(path)).length;
311
+ return {
312
+ contentsSeen,
313
+ filesReached: files.size,
314
+ onlyThroughResult,
315
+ namedByCall: files.size - onlyThroughResult,
316
+ refusedAttempts: accesses.filter((access) => access.outcome === 'blocked').length,
317
+ unknownAttempts: accesses.filter((access) => access.outcome === 'unknown').length,
318
+ valuesReturned: returns.filter((entry) => entry.strength === 'value').length,
319
+ valuesWritten: returns.filter((entry) => entry.strength !== 'value' && entry.writtenFrom.length > 0).length,
320
+ // Counted from the uses, not from the returns: the main agent returns to nobody, and it is the one agent
321
+ // every session has. Counted per agent, because that is the question the agent's own panel answers.
322
+ wroteInMessages: new Set(uses.filter(isInOwnWords).map((use) => use.agentId)).size,
323
+ filesWrittenOnward: new Set(uses.filter(isWrittenOnward).flatMap((use) => use.targets ?? [])).size,
324
+ valueUses: uses.length,
325
+ };
326
+ }
327
+ /**
328
+ * How many agents the contents of a protected file reached, by the same rule `session-view.ts` applies to draw
329
+ * one: what a delegation returned, what an agent wrote of it, and every step of its flow that carried a value.
330
+ * The two must keep agreeing - the report says "3 agents saw what is inside sensitive files" from that rule, and
331
+ * a row of the index that says something else about the same session is a row that lies.
332
+ */
333
+ function contentsSeenIn(graph, flows) {
334
+ // The main agent is kept apart from the rest in the graph, and it is the one agent every session has: counting
335
+ // only `graph.agents` said "no agent saw anything" of a session whose main agent saw everything.
336
+ return [graph.main, ...graph.agents].filter((agent) => {
337
+ if (agent.returned === 'value' || agent.wroteValue === true || agent.wroteOnward === true || agent.received !== undefined)
338
+ return true;
339
+ const flow = flows.find((one) => one.agentIndex === agent.index);
340
+ return flow !== undefined && flow.steps.some((step) => (step.kind === 'reached' && step.outcome === 'succeeded' && filesRead(step).length > 0) ||
341
+ step.kind === 'received' ||
342
+ step.kind === 'used' ||
343
+ (step.kind === 'delegated' && step.strength === 'value'));
344
+ }).length;
345
+ }
346
+ /**
347
+ * The session as a shape. One node per agent, each carrying what it was asked and what it reached, ranked so
348
+ * that a drawing which has room for four lanes draws the four that are worth drawing.
349
+ *
350
+ * This is deliberately not a list of delegations: a delegation is a call, and an agent that no call asked for
351
+ * still ran and still reached things. The node is the agent, and a missing instruction is a fact about it.
352
+ */
353
+ function graphOf(model, accesses, returns, uses, traced, redactor, view) {
354
+ // X10: an agent saw a value its model was handed, whichever call - if any the record names - it came from.
355
+ const received = new Map();
356
+ for (const delivery of model.deliveries) {
357
+ if (delivery.status !== 'confirmed')
358
+ continue;
359
+ const files = filesTracedIn([delivery.text], traced);
360
+ if (files.length > 0)
361
+ received.set(delivery.recipientAgentId, new Set([...(received.get(delivery.recipientAgentId) ?? []), ...files]));
362
+ }
363
+ const askedOf = new Map(model.delegations
364
+ .filter((delegation) => delegation.childAgentId !== undefined && delegation.description !== undefined)
365
+ .map((delegation) => [delegation.childAgentId ?? '', delegation.description ?? '']));
366
+ // The delegated agents in the order their positions run, which is the order both renderers number them in.
367
+ const delegatedOrder = model.agents
368
+ .map((agent, index) => ({ agent, index }))
369
+ .filter(({ agent }) => agent.id !== model.sessionId)
370
+ .map(({ index }) => index);
371
+ const nodeOf = (agent) => {
372
+ const parents = model.delegations.filter((delegation) => delegation.childAgentId === agent.id);
373
+ const parentIndex = parents.length === 1
374
+ ? model.agents.findIndex((candidate) => candidate.id === parents[0]?.parentAgentId)
375
+ : -1;
376
+ const own = accesses.filter((access) => access.agentId === agent.id);
377
+ const reached = own.filter((access) => access.outcome === 'succeeded');
378
+ const files = [...new Set(reached.map((access) => access.path))];
379
+ // What came back is the worse of the two routes, so it is what the node names when there is a choice.
380
+ const worst = reached.find((access) => access.source === 'result')?.path ?? files[0];
381
+ const asked = askedOf.get(agent.id);
382
+ const index = model.agents.indexOf(agent);
383
+ return {
384
+ index,
385
+ ...(agent.id === model.sessionId ? {} : { ordinal: delegatedOrder.indexOf(index) + 1 }),
386
+ ...(agent.type === undefined ? {} : { type: redactor.term(agent.type) }),
387
+ ...(parentIndex < 0 ? {} : { parentIndex }),
388
+ label: redactor.term(agent.id === model.sessionId
389
+ ? 'the session itself'
390
+ : `${agentName(agent.id, model, view)}${agent.type === undefined ? '' : ` · ${agent.type}`}`),
391
+ ...(asked === undefined ? {} : { askedTo: redactor.scan(asked) }),
392
+ actions: model.events.filter((event) => event.agentId === agent.id).length,
393
+ filesReached: files.length,
394
+ refusedAttempts: own.filter((access) => access.outcome === 'blocked').length,
395
+ ...(worst === undefined ? {} : { topPath: redactor.path(worst) }),
396
+ ...returnedOf(returns, agent.id),
397
+ ...(uses.some((use) => use.agentId === agent.id && isWrittenOnward(use)) ? { wroteOnward: true } : {}),
398
+ ...(received.has(agent.id) ? { received: [...new Set([...(received.get(agent.id) ?? [])].map((path) => redactor.path(path)))] } : {}),
399
+ };
400
+ };
401
+ const main = model.agents.find((agent) => agent.id === model.sessionId);
402
+ return {
403
+ main: main === undefined
404
+ ? { index: -1, label: redactor.term('the session itself'), actions: 0, filesReached: 0, refusedAttempts: 0 }
405
+ : nodeOf(main),
406
+ agents: model.agents
407
+ .filter((agent) => agent.id !== model.sessionId)
408
+ .map(nodeOf)
409
+ .sort((first, second) => second.filesReached - first.filesReached ||
410
+ second.refusedAttempts - first.refusedAttempts ||
411
+ second.actions - first.actions),
412
+ totalAgents: model.agents.length,
413
+ };
414
+ }
415
+ /**
416
+ * X23: read from the declared capabilities, never from the absence of a record. `absent` outranks `unmeasured`: one source
417
+ * known not to show it settles the question that an unmeasured one leaves open.
418
+ */
419
+ function coverageOf(model) {
420
+ const states = new Set(model.capabilities
421
+ .filter((record) => record.question === 'actions' || record.question === 'access')
422
+ .map((record) => record.state));
423
+ return states.has('absent') ? 'absent' : states.has('unmeasured') ? 'unmeasured' : 'complete';
424
+ }
425
+ /**
426
+ * The answer first. A reader who reads one line should learn whether anything the policy protects was reached,
427
+ * and should not be left to work that out from a table of 65 rows.
428
+ */
429
+ function headlineOf(tally, coverage, redactor) {
430
+ const { filesReached, onlyThroughResult, refusedAttempts, unknownAttempts } = tally;
431
+ const refused = refusedAttempts === 0 ? '' : `, and ${refusedAttempts} ${refusedAttempts === 1 ? 'attempt was' : 'attempts were'} refused`;
432
+ // An attempt whose effect is not established is never "nothing was reached", however the rest went (X11).
433
+ if (filesReached === 0 && unknownAttempts > 0) {
434
+ return {
435
+ severity: 'attention',
436
+ sentence: redactor.term(`No file this policy protects is known to have been reached, but ${unknownAttempts} ` +
437
+ `${unknownAttempts === 1 ? 'attempt at one has' : 'attempts at them have'} no recorded outcome${refused}.`),
438
+ };
439
+ }
440
+ // X23: a record that does not show every action that ran, or what each reached, is never a clean report - absent and
441
+ // unmeasured said apart, in `GAP_WORDS`' words.
442
+ if (filesReached === 0 && coverage !== 'complete') {
443
+ return {
444
+ severity: 'attention',
445
+ sentence: redactor.term('No file this policy protects is known to have been reached, but ' +
446
+ (coverage === 'absent' ? 'the record does not show everything that ran' : 'whether the record shows everything that ran has not been measured') +
447
+ `${refused}.`),
448
+ };
449
+ }
450
+ if (filesReached === 0 && refusedAttempts === 0) {
451
+ return { severity: 'clear', sentence: redactor.term('No file this policy protects was reached in this session.') };
452
+ }
453
+ // Every attempt refused: the policy held throughout, which is not the same news as nothing having happened.
454
+ if (filesReached === 0) {
455
+ return {
456
+ severity: 'clear',
457
+ sentence: redactor.term(`No file this policy protects was reached: ${refusedAttempts} ` +
458
+ `${refusedAttempts === 1 ? 'attempt was' : 'attempts were'} refused, and nothing else came close.`),
459
+ };
460
+ }
461
+ return {
462
+ severity: 'attention',
463
+ sentence: redactor.term(`${filesReached} ${filesReached === 1 ? 'file' : 'files'} this policy protects ` +
464
+ `${filesReached === 1 ? 'was' : 'were'} reached` +
465
+ `${onlyThroughResult === 0 ? '' : `, ${onlyThroughResult} of them without any call ever naming ${onlyThroughResult === 1 ? 'it' : 'them'}`}` +
466
+ `${refusedAttempts === 0 ? '' : `, and ${refusedAttempts} further ${refusedAttempts === 1 ? 'attempt was' : 'attempts were'} refused`}` +
467
+ `${unknownAttempts === 0 ? '' : `${refusedAttempts === 0 ? ', and' : ';'} ${unknownAttempts} further ${unknownAttempts === 1 ? 'attempt has' : 'attempts have'} no recorded outcome`}.` +
468
+ returnedClause(tally) +
469
+ onwardClause(tally)),
470
+ // Said out loud, because a number that is partly noise and does not say so is worse than no number.
471
+ caveat: redactor.term('A path named inside a shell command is counted as reached; some of those are mentions rather than openings.'),
472
+ };
473
+ }
474
+ /**
475
+ * One line per path and route, carrying the chain: who reached it, what they had been asked to do, and how.
476
+ * The instruction is the cause - it is the reason this project exists - so it is what the line leads with.
477
+ */
478
+ function storiesOf(model, accesses, wordsBefore, traced, redactor, view) {
479
+ const byAgent = new Map(model.agents.map((agent) => [agent.id, agent]));
480
+ const askedOf = new Map(model.delegations
481
+ .filter((delegation) => delegation.childAgentId !== undefined && delegation.description !== undefined)
482
+ .map((delegation) => [delegation.childAgentId ?? '', delegation.description ?? '']));
483
+ const grouped = new Map();
484
+ for (const access of accesses) {
485
+ const key = `${access.path}\u0000${access.source}\u0000${access.agentId}\u0000${access.outcome}`;
486
+ grouped.set(key, [...(grouped.get(key) ?? []), access]);
487
+ }
488
+ return [...grouped.values()]
489
+ .map((group) => {
490
+ const access = group[0];
491
+ const agent = byAgent.get(access.agentId);
492
+ const asked = askedOf.get(access.agentId);
493
+ return {
494
+ ...(agent === undefined ? {} : { agentIndex: model.agents.indexOf(agent) }),
495
+ path: redactor.path(access.path),
496
+ source: access.source,
497
+ outcome: access.outcome,
498
+ who: access.agentId === model.sessionId
499
+ ? redactor.term('the session itself')
500
+ : redactor.term(`${agentName(access.agentId, model, view)}${agent?.type === undefined ? '' : ` (${agent.type})`}`),
501
+ ...(asked === undefined ? {} : { askedTo: redactor.scan(asked) }),
502
+ did: redactor.term(didOf(access, model)),
503
+ toolKnown: model.events.find((event) => event.id === access.eventId)?.toolKnown ?? false,
504
+ evidence: redactor.term(describeEvidence(access.evidence, model, view)),
505
+ occurrences: group.length,
506
+ ...linesTotal(group),
507
+ wrote: wroteBefore(wordsBefore.get(access.eventId), model, traced, redactor, view),
508
+ };
509
+ })
510
+ .sort((first, second) => rank(first) - rank(second) || second.occurrences - first.occurrences);
511
+ }
512
+ /**
513
+ * The most serious finding first, and the first one is what a page leads with. An agent that was given an
514
+ * instruction and reached a file through what came back is the shape of the incident this project was built
515
+ * from: the instruction explains it, and no call parameter would have shown it.
516
+ *
517
+ * A refusal ranks **last**. It is good news, and a page whose first line is good news answers "what happened?"
518
+ * with the one thing that did not. The refusals are still counted in the tally, badged as the policy holding,
519
+ * and listed in the table - they are placed, not hidden.
520
+ */
521
+ function rank(story) {
522
+ if (story.outcome === 'blocked')
523
+ return 6;
524
+ if (story.outcome === 'unknown')
525
+ return 5;
526
+ if (story.askedTo !== undefined)
527
+ return story.source === 'result' ? 0 : 1;
528
+ return story.source === 'result' ? 2 : 3;
529
+ }
530
+ /** The tool, and for a shell the programs it ran - which is structure. The command text itself stays out (§7.3). */
531
+ function didOf(access, model) {
532
+ const event = model.events.find((candidate) => candidate.id === access.eventId);
533
+ return event === undefined ? access.toolName : didOfEvent(event);
534
+ }
535
+ function didOfEvent(event) {
536
+ // Only programs that could have reached something: naming `echo` beside `grep` says nothing about the file.
537
+ const programs = [...new Set(event.commands.flatMap(programsIn))]
538
+ .filter((program) => !NAMES_NOTHING.has(program))
539
+ .slice(0, 2);
540
+ return programs.length === 0 ? event.toolName : `${event.toolName} (${programs.join(', ')})`;
541
+ }
542
+ function toolCounts(events) {
543
+ const counts = {};
544
+ for (const event of events)
545
+ counts[event.toolName] = (counts[event.toolName] ?? 0) + 1;
546
+ return counts;
547
+ }
548
+ function scopeOf(model, policy, redactor, view) {
549
+ return {
550
+ // A session id names a run on someone's machine and nothing a reader of a shared report can act on.
551
+ sessionId: redactor.term(view.share ? 'not shown' : shorten(model.sessionId)),
552
+ provider: model.provider,
553
+ policy: {
554
+ level: redactor.term(policy.level),
555
+ origin: redactor.term(describeOrigin(policy, redactor)),
556
+ patterns: policy.protected.length,
557
+ exceptions: policy.allowed.length,
558
+ },
559
+ // A reader weighing "nothing was found" deserves to know what was not looked for. All three match classes
560
+ // of §5.4 are implemented as of ruleset v2, so the list is empty and the line is not printed.
561
+ redaction: { ...redactor.summary(), missingClasses: [] },
562
+ completeness: model.completeness,
563
+ ...timesOf(model, view),
564
+ paths: {
565
+ shared: view.share,
566
+ root: view.projectRoot.kind,
567
+ ...(view.projectRoot.kind === 'ambiguous' ? { workingDirectories: view.projectRoot.count } : {}),
568
+ },
569
+ };
570
+ }
571
+ /**
572
+ * M4: the first and the last time any recorded call was written - a span to show, never an order: the earliest and the
573
+ * latest are read, nothing is sorted by them (architecture invariant 3). A shared report says when nothing happened.
574
+ */
575
+ function timesOf(model, view) {
576
+ if (view.share)
577
+ return {};
578
+ const times = model.events.flatMap((event) => [event.evidence.at, event.result?.evidence.at]).filter((at) => at !== undefined);
579
+ return times.length === 0 ? {} : { times: { first: Math.min(...times), last: Math.max(...times) } };
580
+ }
581
+ /** M4: the time a record was written, where it has one and the report may say it - never in a shared report. */
582
+ function atOf(evidence, view) {
583
+ return view.share || evidence.at === undefined ? {} : { at: evidence.at };
584
+ }
585
+ /**
586
+ * In words, because "default" printed as a value reads like a decision someone took.
587
+ *
588
+ * The file it names is a path on the same machine as every other path in the report, so it goes through the
589
+ * same door. It used to be concatenated straight in, which left `--share --policy /Users/someone/…` printing
590
+ * an account name in a report whose whole promise is that it does not.
591
+ */
592
+ function describeOrigin(policy, redactor) {
593
+ const { origin } = policy;
594
+ if (origin.kind === 'file')
595
+ return `policy file ${redactor.path(origin.path)}`;
596
+ if (origin.kind === 'settings') {
597
+ const path = redactor.path(origin.path);
598
+ return `deny rules of ${path} — ${origin.used} used, ${origin.ignored} ignored as command patterns`;
599
+ }
600
+ return 'BUILT-IN DEFAULT — no policy file was given';
601
+ }
602
+ function toDelegation(delegation, model, accesses, redactor, view) {
603
+ const events = model.events
604
+ .filter((event) => event.agentId === delegation.childAgentId)
605
+ .sort((first, second) => first.sequence - second.sequence);
606
+ const parentAgentIndex = model.agents.findIndex((agent) => agent.id === delegation.parentAgentId);
607
+ const childAgentIndex = model.agents.findIndex((agent) => agent.id === delegation.childAgentId);
608
+ return {
609
+ id: redactor.term(view.share ? `${model.delegations.indexOf(delegation) + 1}` : shorten(delegation.id)),
610
+ ...(parentAgentIndex < 0 ? {} : { parentAgentIndex }),
611
+ ...(childAgentIndex < 0 ? {} : { childAgentIndex }),
612
+ evidence: redactor.term(describeEvidence(delegation.evidence, model, view)),
613
+ ...(delegation.requestedType === undefined ? {} : { agentType: redactor.term(delegation.requestedType) }),
614
+ ...(delegation.depth === undefined ? {} : { depth: delegation.depth }),
615
+ // §7.3: the full text of a prompt does not go into a report. The short description does, scanned, and the
616
+ // instruction is reported by its size. Quoting the fragment a finding rests on needs a finding to rest on,
617
+ // and that is the detector's - until then, a report that quoted everything would be the leak it warns about.
618
+ ...(delegation.description === undefined ? {} : { description: redactor.scan(delegation.description) }),
619
+ ...(delegation.prompt === undefined ? {} : { instructionSize: delegation.prompt.length }),
620
+ laterInstructions: delegation.followUps.map((followUp) => ({
621
+ size: followUp.text.length,
622
+ evidence: redactor.term(describeEvidence(followUp.evidence, model, view)),
623
+ })),
624
+ actions: events.map((event) => toAction(event, model, accesses, redactor, view)),
625
+ completeness: delegation.completeness,
626
+ };
627
+ }
628
+ /** An agent nobody can say was asked for. Its actions are still evidence, so they are shown. */
629
+ function toAgent(agent, model, accesses, redactor, view) {
630
+ return {
631
+ agentIndex: model.agents.indexOf(agent),
632
+ id: redactor.term(agentName(agent.id, model, view)),
633
+ ...(agent.type === undefined ? {} : { type: redactor.term(agent.type) }),
634
+ ...(agent.depth === undefined ? {} : { depth: agent.depth }),
635
+ actions: model.events
636
+ .filter((event) => event.agentId === agent.id)
637
+ .sort((first, second) => first.sequence - second.sequence)
638
+ .map((event) => toAction(event, model, accesses, redactor, view)),
639
+ };
640
+ }
641
+ function toAction(event, model, accesses, redactor, view) {
642
+ const touched = accesses.find((access) => access.eventId === event.id);
643
+ return {
644
+ sequence: event.sequence,
645
+ tool: redactor.term(event.toolName),
646
+ outcome: event.outcome,
647
+ // What the call was pointed at: the protected path when there was one, and otherwise nothing - the full
648
+ // parameters are transcript content and do not belong in a report (§7.3).
649
+ ...(touched === undefined ? {} : { target: redactor.path(touched.path) }),
650
+ evidence: redactor.term(describeEvidence(event.evidence, model, view)),
651
+ ...atOf(event.evidence, view),
652
+ };
653
+ }
654
+ /** H10: how many lines of one file a group of calls printed, or nothing where none did. */
655
+ function linesTotal(group) {
656
+ const total = group.reduce((sum, access) => sum + (access.lines ?? 0), 0);
657
+ return total === 0 ? {} : { lines: total };
658
+ }
659
+ /** H5: the files of one call whose lines a search printed, with how many - redacted, as every path of a step is. */
660
+ function linesIn(group, redactor) {
661
+ const printed = group.filter((access) => (access.lines ?? 0) > 0).map((access) => ({ path: redactor.path(access.path), count: access.lines }));
662
+ return printed.length === 0 ? {} : { lines: printed };
663
+ }
664
+ function toFinding(access, model, redactor, view, told) {
665
+ return {
666
+ path: redactor.path(access.path),
667
+ source: access.source,
668
+ outcome: access.outcome,
669
+ tool: redactor.term(access.toolName),
670
+ pattern: redactor.term(access.pattern),
671
+ evidence: redactor.term(describeEvidence(access.evidence, model, view)),
672
+ ...(told ? { told: true } : {}),
673
+ };
674
+ }
675
+ /**
676
+ * R11: the headline carries the strongest fact. A value that came back is stronger than the reach it follows; failing
677
+ * that, a value an agent wrote into its own messages is, because the session's transcript now keeps it - which is all
678
+ * the motivating case's transcript records of its incident. A path or a report that came back is weaker than the
679
+ * reach already said, so it changes nothing here.
680
+ */
681
+ function returnedClause(tally) {
682
+ const { valuesReturned, valuesWritten } = tally;
683
+ if (valuesReturned === 1)
684
+ return " An agent's answer carried a value from one of them back to the agent that started it.";
685
+ if (valuesReturned > 1) {
686
+ return ` In ${valuesReturned} delegations, an agent's answer carried a value from one of them back to the agent that started it.`;
687
+ }
688
+ if (valuesWritten === 1) {
689
+ return " An agent wrote a value from one of them into its own messages, which the session's transcript now keeps.";
690
+ }
691
+ if (valuesWritten > 1) {
692
+ return ` Agents in ${valuesWritten} delegations wrote a value from one of them into their own messages, which the session's transcript now keeps.`;
693
+ }
694
+ return '';
695
+ }
696
+ /**
697
+ * where-the-value-went R11: a value put into a file that is not protected leads. Uses in words, commands, prompts and
698
+ * other calls do not change the headline, and neither does a write into a protected file - editing configuration.
699
+ */
700
+ function onwardClause({ filesWrittenOnward, valuesReturned }) {
701
+ if (filesWrittenOnward === 0)
702
+ return '';
703
+ // After a value came back the sentence before names it, and "the same value" is the plainer way to say it again.
704
+ const subject = valuesReturned > 0 ? 'The same value' : 'A value from one of them';
705
+ return filesWrittenOnward === 1
706
+ ? ` ${subject} was also written into a file that is not protected.`
707
+ : ` ${subject} was also written into ${filesWrittenOnward} files that are not protected.`;
708
+ }
709
+ function isWrittenOnward(use) {
710
+ return use.landed === 'file' && use.intoProtected !== true;
711
+ }
712
+ /** R7: an agent's own words, of either kind - the words the transcript keeps whether or not anything came back. */
713
+ function isInOwnWords(use) {
714
+ return use.landed === 'said' || use.landed === 'reasoning';
715
+ }
716
+ /** R9: where the value landed crosses the boundary; the value, the words and the command line never do. */
717
+ function toUse(use, model, redactor, view) {
718
+ const agentIndex = model.agents.findIndex((agent) => agent.id === use.agentId);
719
+ const paths = (list) => [...new Set(list.map((path) => redactor.path(path)))];
720
+ return {
721
+ ...(agentIndex < 0 ? {} : { agentIndex }),
722
+ files: paths(use.files),
723
+ ...(use.fileUncertain === true ? { fileUncertain: true } : {}),
724
+ landed: use.landed,
725
+ ...(use.targets === undefined ? {} : { targets: paths(use.targets), intoProtected: use.intoProtected === true }),
726
+ ...(use.programs === undefined ? {} : { programs: use.programs.map((program) => redactor.term(program)), commits: use.commits === true }),
727
+ source: use.source,
728
+ ...(use.sourceTargets === undefined ? {} : { sourceTargets: paths(use.sourceTargets) }),
729
+ evidence: redactor.term(describeEvidence(use.evidence, model, view)),
730
+ };
731
+ }
732
+ /** The strongest of what came back from this agent, and whether it wrote a value that did not, for its node (R12). */
733
+ function returnedOf(returns, agentId) {
734
+ const own = returns.filter((entry) => entry.agentId === agentId);
735
+ const returned = own.some((entry) => entry.strength === 'value')
736
+ ? 'value'
737
+ : own.some((entry) => entry.strength === 'path')
738
+ ? 'path'
739
+ : undefined;
740
+ const wrote = returned !== 'value' && own.some((entry) => entry.writtenFrom.length > 0);
741
+ return { ...(returned === undefined ? {} : { returned }), ...(wrote ? { wroteValue: true } : {}) };
742
+ }
743
+ /** A return across the redaction boundary: files through the path door, agents by index, and no text of it (R10). */
744
+ function toReturn(entry, model, redactor, view) {
745
+ const agentIndex = model.agents.findIndex((agent) => agent.id === entry.agentId);
746
+ const parentAgentIndex = model.agents.findIndex((agent) => agent.id === entry.parentAgentId);
747
+ return {
748
+ ...(agentIndex < 0 ? {} : { agentIndex }),
749
+ ...(parentAgentIndex < 0 ? {} : { parentAgentIndex }),
750
+ strength: entry.strength,
751
+ // Two files outside the project are one category in the shared view, and saying it twice says nothing more.
752
+ paths: [...new Set(entry.paths.map((path) => redactor.path(path)))],
753
+ writtenFrom: [...new Set(entry.writtenFrom.map((path) => redactor.path(path)))],
754
+ ...(entry.fileUncertain === true ? { fileUncertain: true } : {}),
755
+ filesReached: entry.filesReached,
756
+ ...(entry.reportLength === undefined ? {} : { reportLength: entry.reportLength }),
757
+ ...(entry.awaited === true ? { awaited: true } : {}),
758
+ evidence: redactor.term(describeEvidence(entry.evidence, model, view)),
759
+ };
760
+ }
761
+ /**
762
+ * What could not be established, counted by kind. R9: a return that could not be read is a gap, and is said among the
763
+ * others - apart from one that was never delivered (where-the-value-went R5), since "could not be read" would name the
764
+ * wrong cause.
765
+ */
766
+ function gapsOf(gaps, returns) {
767
+ const counted = new Map();
768
+ for (const gap of gaps) {
769
+ // A capability gap is counted with its question: "the record does not say which actions ran" is a different gap
770
+ // from "... what reached the AI", and one count for both would say neither.
771
+ const key = gap.kind + ' ' + (gap.question ?? '');
772
+ const known = counted.get(key);
773
+ counted.set(key, { kind: gap.kind, ...(gap.question === undefined ? {} : { question: gap.question }), count: (known?.count ?? 0) + 1 });
774
+ }
775
+ const unknown = returns.filter((entry) => entry.strength === 'unknown');
776
+ const awaited = unknown.filter((entry) => entry.awaited === true).length;
777
+ const unreadable = unknown.length - awaited;
778
+ return [
779
+ ...counted.values(),
780
+ ...(unreadable === 0 ? [] : [{ kind: 'answer-unread', count: unreadable }]),
781
+ ...(awaited === 0 ? [] : [{ kind: 'answer-awaited', count: awaited }]),
782
+ ];
783
+ }
784
+ function missingWords(gap) {
785
+ if (gap.kind === 'answer-unread')
786
+ return `${gap.count} × an agent's answer could not be read, so what was in it is unknown`;
787
+ if (gap.kind === 'answer-awaited')
788
+ return `${gap.count} × an agent ran in the background and its answer never arrived, so what was in it is unknown`;
789
+ const words = gap.question === undefined ? GAP_WORDS[gap.kind] : `${GAP_WORDS[gap.kind]}: ${QUESTION_WORDS[gap.question]}`;
790
+ return `${gap.count} × ${words}`;
791
+ }
792
+ function countOutcomes(events) {
793
+ return {
794
+ succeeded: events.filter((event) => event.outcome === 'succeeded').length,
795
+ blocked: events.filter((event) => event.outcome === 'blocked').length,
796
+ unknown: events.filter((event) => event.outcome === 'unknown').length,
797
+ };
798
+ }
799
+ function describeEvidence(evidence, model, view) {
800
+ const { source } = evidence;
801
+ const where = source.kind === 'main'
802
+ ? 'session'
803
+ : source.kind === 'agent'
804
+ ? `agent ${agentName(source.agentId, model, view)}`
805
+ // A reviewer is no agent of the session: it is named by its place among the reviews, never by its id (X19).
806
+ : `review ${model.reviews.findIndex((review) => review.id === source.reviewId) + 1}`;
807
+ return `${where} record ${evidence.record}`;
808
+ }
809
+ /**
810
+ * How an agent is named. Shortened in the full view; in the shared one, the position it already occupies on the
811
+ * page (R6). An identifier names a run on someone's machine, an ordinal names a row in this report - and the
812
+ * ordinal is what a reader is pointing at anyway when they say "agent 6".
813
+ */
814
+ function agentName(id, model, view) {
815
+ if (!view.share)
816
+ return shorten(id);
817
+ const index = model.agents.findIndex((agent) => agent.id === id);
818
+ // An agent the report does not hold is still an agent: saying so beats printing an id or printing nothing.
819
+ return index < 0 ? 'not in this report' : `${index}`;
820
+ }
821
+ function shorten(identifier) {
822
+ if (identifier.length <= SHORTEN_ABOVE)
823
+ return identifier;
824
+ return `${identifier.slice(0, HEAD)}…${identifier.slice(-TAIL)}`;
825
+ }
826
+ const FIRST = Number.MIN_SAFE_INTEGER;
827
+ const LAST = Number.MAX_SAFE_INTEGER;
828
+ /**
829
+ * Every agent's flow (`specs/2026-09-15-agent-flow.md` R1-R6): what it did with protected files and values, in the order
830
+ * of its own records, with consecutive steps that say the same thing collapsed (R3, §5.1). Built here, before the
831
+ * boundary, because the order lives in records a renderer never sees. A delegation is a step that names another agent;
832
+ * no step of one agent is ever placed among another's.
833
+ */
834
+ function flowsOf(model, accesses, uses, returns, wordsBefore, traced, redactor, view) {
835
+ const events = new Map(model.events.map((event) => [event.id, event]));
836
+ const indexOf = (agentId) => {
837
+ const index = model.agents.findIndex((agent) => agent.id === agentId);
838
+ return index < 0 ? undefined : index;
839
+ };
840
+ const paths = (list) => [...new Set(list.map((path) => redactor.path(path)))];
841
+ const raw = new Map();
842
+ const push = (agentId, step) => {
843
+ const list = raw.get(agentId);
844
+ if (list === undefined)
845
+ raw.set(agentId, [step]);
846
+ else
847
+ list.push(step);
848
+ };
849
+ // A call that reached, or was refused, a protected file - one step for the call and each outcome its files had. X11: a
850
+ // search whose own outcome is unknown reached what it printed, and only attempted the rest, so one outcome for the
851
+ // whole call would say the tally's "reached" of none of them. The established step comes first, so a use after the
852
+ // call's result follows it.
853
+ const byEvent = new Map();
854
+ for (const access of accesses)
855
+ byEvent.set(access.eventId, [...(byEvent.get(access.eventId) ?? []), access]);
856
+ const byOutcome = [...byEvent].flatMap(([eventId, all]) => OUTCOME_ORDER.map((outcome) => [eventId, outcome, all.filter((access) => access.outcome === outcome)])
857
+ .filter(([, , group]) => group.length > 0));
858
+ for (const [eventId, outcome, group] of byOutcome) {
859
+ const event = events.get(eventId);
860
+ const first = group[0];
861
+ if (event === undefined || first === undefined)
862
+ continue;
863
+ const files = [...new Set(group.map((access) => access.path))].sort();
864
+ const sources = [...new Set(group.map((access) => access.source))].sort();
865
+ const did = didOf(first, model);
866
+ // Repeats merge into their first call, so a search that printed lines is never merged into one that printed names.
867
+ const printed = group.filter((access) => access.lines !== undefined).map((access) => `${access.path}=${access.lines}`).sort();
868
+ push(event.agentId, {
869
+ at: event.evidence.record,
870
+ signature: ['reached', did, sources.join(','), outcome, files.join(','), printed.join(',')].join('\u0000'),
871
+ keys: event.result === undefined ? [] : [`result ${event.result.evidence.record}`],
872
+ evidence: event.evidence,
873
+ build: (common) => ({
874
+ ...common,
875
+ ...(outcome === 'unknown' ? { broken: true } : {}),
876
+ kind: 'reached',
877
+ did: redactor.term(did),
878
+ toolKnown: event.toolKnown,
879
+ files: paths(files),
880
+ sources,
881
+ outcome,
882
+ // X10: a value in what a process printed is in the record, not in the agent's context, until something shows it was
883
+ // handed to the agent's model.
884
+ carriedValue: event.result?.content !== undefined && event.result.stage === 'model' && traced.trace.foundIn(event.result.content).size > 0,
885
+ ...linesIn(group, redactor),
886
+ viaCommand: event.commands.length > 0,
887
+ ...(event.result !== undefined && event.result.stage !== 'model' ? { processOutput: true } : {}),
888
+ wrote: wroteBefore(wordsBefore.get(eventId), model, traced, redactor, view),
889
+ }),
890
+ });
891
+ }
892
+ // X10: output an agent's model was handed that no call of its returned, holding a value from a protected file - the
893
+ // agent read that file, and the step names no call it came from, since the record joins it to none.
894
+ for (const delivery of model.deliveries) {
895
+ if (delivery.status !== 'confirmed')
896
+ continue;
897
+ const files = filesTracedIn([delivery.text], traced).sort();
898
+ if (files.length === 0)
899
+ continue;
900
+ push(delivery.recipientAgentId, {
901
+ at: delivery.evidence.record,
902
+ signature: ['received', files.join(',')].join('\u0000'),
903
+ keys: [],
904
+ evidence: delivery.evidence,
905
+ build: (common) => ({ ...common, kind: 'received', files: paths(files) }),
906
+ });
907
+ }
908
+ // A result that reached nothing protected, but carried a value a use came after (where-the-value-went R8).
909
+ const carried = new Set();
910
+ for (const use of uses) {
911
+ if (use.source !== 'unprotected' || use.sourceRecord === undefined)
912
+ continue;
913
+ const record = use.sourceRecord;
914
+ if (carried.has(`${use.agentId} ${record}`))
915
+ continue;
916
+ carried.add(`${use.agentId} ${record}`);
917
+ const event = model.events.find((candidate) => candidate.agentId === use.agentId && candidate.result?.evidence.record === record);
918
+ if (event === undefined)
919
+ continue;
920
+ push(use.agentId, {
921
+ at: record,
922
+ signature: `carried ${record}`,
923
+ keys: [`result ${record}`],
924
+ evidence: event.result?.evidence ?? event.evidence,
925
+ build: (common) => ({ ...common, kind: 'carried', did: redactor.term(didOfEvent(event)), named: paths(event.targets) }),
926
+ });
927
+ }
928
+ for (const use of uses) {
929
+ const sourceKey = use.source === 'returned'
930
+ ? `delegation ${use.sourceDelegationId ?? ''}`
931
+ : use.sourceRecord === undefined ? undefined : `result ${use.sourceRecord}`;
932
+ const onto = use.targets ?? [];
933
+ push(use.agentId, {
934
+ at: use.evidence.record,
935
+ signature: ['used', use.landed, use.files.join(','), onto.join(','), String(use.intoProtected === true), use.source, sourceKey ?? ''].join('\u0000'),
936
+ keys: [],
937
+ evidence: use.evidence,
938
+ ...(use.programs === undefined ? {} : { programs: use.programs }),
939
+ ...(use.commits === undefined ? {} : { commits: use.commits }),
940
+ build: (common, merged, afterOf) => {
941
+ const after = sourceKey === undefined ? undefined : afterOf(sourceKey);
942
+ return {
943
+ ...common,
944
+ kind: 'used',
945
+ landed: use.landed,
946
+ files: paths(use.files),
947
+ ...(use.fileUncertain === true ? { fileUncertain: true } : {}),
948
+ ...(use.targets === undefined ? {} : { targets: paths(use.targets), intoProtected: use.intoProtected === true }),
949
+ ...(use.landed === 'command' ? { programs: merged.programs.map((program) => redactor.term(program)), commits: merged.commits } : {}),
950
+ source: use.source,
951
+ ...(after === undefined ? {} : { after }),
952
+ };
953
+ },
954
+ });
955
+ }
956
+ for (const entry of returns) {
957
+ const delegation = model.delegations.find((candidate) => candidate.id === entry.delegationId);
958
+ const child = indexOf(entry.agentId);
959
+ const parent = indexOf(entry.parentAgentId);
960
+ const came = {
961
+ strength: entry.strength,
962
+ ...(entry.awaited === true ? { awaited: true } : {}),
963
+ ...(entry.inferred === true ? { inferred: true } : {}),
964
+ ...(entry.inferred === true ? { inferred: true } : {}),
965
+ files: paths(entry.strength === 'value' || entry.strength === 'path' ? entry.paths : []),
966
+ ...(entry.strength === 'unknown' ? { broken: true } : {}),
967
+ };
968
+ push(entry.parentAgentId, {
969
+ at: delegation?.evidence.record ?? entry.evidence.record,
970
+ signature: `delegated ${entry.delegationId}`,
971
+ keys: [`delegation ${entry.delegationId}`],
972
+ evidence: delegation?.evidence ?? entry.evidence,
973
+ build: (common) => ({ ...common, ...came, kind: 'delegated', ...(child === undefined ? {} : { toAgentIndex: child }) }),
974
+ });
975
+ push(entry.agentId, {
976
+ at: LAST,
977
+ signature: `returned ${entry.delegationId}`,
978
+ keys: [],
979
+ evidence: entry.evidence,
980
+ build: (common) => ({ ...common, ...came, kind: 'returned', ...(parent === undefined ? {} : { toAgentIndex: parent }) }),
981
+ });
982
+ }
983
+ for (const delegation of model.delegations) {
984
+ if (delegation.childAgentId === undefined)
985
+ continue;
986
+ const parent = indexOf(delegation.parentAgentId);
987
+ push(delegation.childAgentId, {
988
+ at: FIRST,
989
+ signature: `asked ${delegation.id}`,
990
+ keys: [],
991
+ evidence: delegation.evidence,
992
+ alone: true,
993
+ build: (common) => ({
994
+ ...common,
995
+ kind: 'asked',
996
+ ...(parent === undefined ? {} : { byAgentIndex: parent }),
997
+ ...(delegation.description === undefined ? {} : { askedTo: redactor.scan(delegation.description) }),
998
+ }),
999
+ });
1000
+ }
1001
+ return model.agents.flatMap((agent, agentIndex) => {
1002
+ const steps = [...(raw.get(agent.id) ?? [])].sort((first, second) => first.at - second.at);
1003
+ if (steps.every((step) => step.alone === true))
1004
+ return [];
1005
+ const groups = [];
1006
+ for (const step of steps) {
1007
+ const previous = groups.at(-1);
1008
+ if (previous !== undefined && previous.first.signature === step.signature) {
1009
+ previous.last = step;
1010
+ previous.count += 1;
1011
+ for (const program of step.programs ?? [])
1012
+ previous.programs.add(program);
1013
+ previous.commits = previous.commits || step.commits === true;
1014
+ previous.keys.push(...step.keys);
1015
+ }
1016
+ else {
1017
+ groups.push({ first: step, last: step, count: 1, programs: new Set(step.programs ?? []), commits: step.commits === true, keys: [...step.keys] });
1018
+ }
1019
+ }
1020
+ const numbers = new Map();
1021
+ groups.forEach((group, index) => {
1022
+ for (const key of group.keys)
1023
+ if (!numbers.has(key))
1024
+ numbers.set(key, index + 1);
1025
+ });
1026
+ const recordOf = (step) => redactor.term(describeEvidence(step.evidence, model, view));
1027
+ return [{
1028
+ agentIndex,
1029
+ steps: groups.map((group, index) => group.first.build({
1030
+ number: index + 1,
1031
+ count: group.count,
1032
+ evidence: group.count > 1 ? [recordOf(group.first), recordOf(group.last)] : [recordOf(group.first)],
1033
+ ...atOf(group.first.evidence, view),
1034
+ }, { programs: [...group.programs], commits: group.commits }, (key) => numbers.get(key))),
1035
+ }];
1036
+ });
1037
+ }