@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.
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +181 -0
- package/THIRD_PARTY_NOTICES.md +31 -0
- package/dist/adapter/claude-code/contract/assumptions.js +208 -0
- package/dist/adapter/claude-code/contract/denials.js +15 -0
- package/dist/adapter/claude-code/contract/entry-points.js +15 -0
- package/dist/adapter/claude-code/contract/fields.js +73 -0
- package/dist/adapter/claude-code/contract/hooks.js +96 -0
- package/dist/adapter/claude-code/contract/identifiers.js +15 -0
- package/dist/adapter/claude-code/contract/layout.js +29 -0
- package/dist/adapter/claude-code/contract/line-types.js +26 -0
- package/dist/adapter/claude-code/contract/projects.js +23 -0
- package/dist/adapter/claude-code/contract/recognition.js +14 -0
- package/dist/adapter/claude-code/contract/session-title.js +20 -0
- package/dist/adapter/claude-code/contract/settings.js +27 -0
- package/dist/adapter/claude-code/contract/task-notifications.js +33 -0
- package/dist/adapter/claude-code/contract/tools.js +37 -0
- package/dist/adapter/claude-code/contract/version.js +51 -0
- package/dist/adapter/claude-code/discovery/claude-code-project-catalogue.js +129 -0
- package/dist/adapter/claude-code/discovery/claude-code-recognition.js +46 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-catalogue.js +100 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-discovery.js +61 -0
- package/dist/adapter/claude-code/discovery/claude-code-session-titles.js +28 -0
- package/dist/adapter/claude-code/discovery/discovered-session.js +1 -0
- package/dist/adapter/claude-code/discovery/session-entries.js +41 -0
- package/dist/adapter/claude-code/discovery/transcript-tail.js +81 -0
- package/dist/adapter/claude-code/events/claude-code-session-source.js +197 -0
- package/dist/adapter/claude-code/events/delegation-index.js +15 -0
- package/dist/adapter/claude-code/events/transcript-scan.js +179 -0
- package/dist/adapter/claude-code/hooks/hook-output.js +42 -0
- package/dist/adapter/claude-code/hooks/pre-tool-use-input.js +23 -0
- package/dist/adapter/claude-code/hooks/stop-input.js +25 -0
- package/dist/adapter/claude-code/hooks/subagent-stop-input.js +26 -0
- package/dist/adapter/claude-code/policy/deny-rules.js +46 -0
- package/dist/adapter/claude-code/probe/claude-code-probe.js +86 -0
- package/dist/adapter/claude-code/probe/collectors/agent-tool-input-key-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/denial-kind-collector.js +20 -0
- package/dist/adapter/claude-code/probe/collectors/denied-call-collector.js +11 -0
- package/dist/adapter/claude-code/probe/collectors/identifier-collector.js +19 -0
- package/dist/adapter/claude-code/probe/collectors/line-collector.js +1 -0
- package/dist/adapter/claude-code/probe/collectors/line-type-collector.js +29 -0
- package/dist/adapter/claude-code/probe/collectors/sidechain-collector.js +20 -0
- package/dist/adapter/claude-code/probe/collectors/task-notification-collector.js +51 -0
- package/dist/adapter/claude-code/probe/collectors/tool-result-reference-collector.js +31 -0
- package/dist/adapter/claude-code/probe/collectors/tool-use-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/tool-version-collector.js +14 -0
- package/dist/adapter/claude-code/probe/collectors/top-level-key-collector.js +14 -0
- package/dist/adapter/claude-code/probe/collectors/unknown-line-type-key-collector.js +24 -0
- package/dist/adapter/claude-code/probe/collectors/working-directory-collector.js +28 -0
- package/dist/adapter/claude-code/probe/doctor-report.js +1 -0
- package/dist/adapter/claude-code/probe/meta-tally.js +42 -0
- package/dist/adapter/claude-code/probe/session-probe.js +1 -0
- package/dist/adapter/claude-code/probe/session-tally.js +41 -0
- package/dist/adapter/claude-code/probe/transcript-line.js +30 -0
- package/dist/adapter/claude-code/probe/transcript-tally.js +74 -0
- package/dist/adapter/claude-code/settings/deny-entries.js +65 -0
- package/dist/adapter/claude-code/settings/hook-entries.js +303 -0
- package/dist/adapter/codex/contract/actions.js +69 -0
- package/dist/adapter/codex/contract/assumptions.js +55 -0
- package/dist/adapter/codex/contract/capabilities.js +24 -0
- package/dist/adapter/codex/contract/delegations.js +30 -0
- package/dist/adapter/codex/contract/deliveries.js +25 -0
- package/dist/adapter/codex/contract/envelope.js +33 -0
- package/dist/adapter/codex/contract/hook-refusals.js +14 -0
- package/dist/adapter/codex/contract/hooks.js +46 -0
- package/dist/adapter/codex/contract/messages.js +47 -0
- package/dist/adapter/codex/contract/permissions.js +27 -0
- package/dist/adapter/codex/contract/reviews.js +13 -0
- package/dist/adapter/codex/contract/session.js +31 -0
- package/dist/adapter/codex/contract/turns.js +23 -0
- package/dist/adapter/codex/contract/version.js +15 -0
- package/dist/adapter/codex/discovery/codex-conversations.js +44 -0
- package/dist/adapter/codex/discovery/codex-project-catalogue.js +49 -0
- package/dist/adapter/codex/discovery/codex-recognition.js +20 -0
- package/dist/adapter/codex/discovery/codex-session-catalogue.js +28 -0
- package/dist/adapter/codex/discovery/codex-session-discovery.js +115 -0
- package/dist/adapter/codex/discovery/codex-session-index.js +21 -0
- package/dist/adapter/codex/discovery/codex-session-titles.js +135 -0
- package/dist/adapter/codex/discovery/session-header.js +86 -0
- package/dist/adapter/codex/discovery/session-owners.js +11 -0
- package/dist/adapter/codex/discovery/session-roots.js +49 -0
- package/dist/adapter/codex/events/action-items.js +138 -0
- package/dist/adapter/codex/events/capability-records.js +26 -0
- package/dist/adapter/codex/events/codex-session-source.js +121 -0
- package/dist/adapter/codex/events/hook-refusals.js +27 -0
- package/dist/adapter/codex/events/rollout-scan.js +614 -0
- package/dist/adapter/codex/events/turn-permissions.js +60 -0
- package/dist/adapter/codex/hooks/pre-tool-use-input.js +24 -0
- package/dist/adapter/codex/hooks/stop-refusals.js +57 -0
- package/dist/adapter/codex/probe/codex-doctor-report.js +1 -0
- package/dist/adapter/codex/probe/codex-probe.js +268 -0
- package/dist/adapter/codex/settings/codex-hooks.js +83 -0
- package/dist/cli/cli-command.js +1 -0
- package/dist/cli/command-router.js +42 -0
- package/dist/cli/commands/check-cli-command.js +128 -0
- package/dist/cli/commands/codex-stop-cli-command.js +27 -0
- package/dist/cli/commands/doctor-arguments.js +32 -0
- package/dist/cli/commands/doctor-cli-command.js +31 -0
- package/dist/cli/commands/doctor-usage.js +14 -0
- package/dist/cli/commands/init-cli-command.js +123 -0
- package/dist/cli/commands/menu-cli-command.js +58 -0
- package/dist/cli/commands/notify-cli-command.js +158 -0
- package/dist/cli/commands/refuse-cli-command.js +79 -0
- package/dist/cli/commands/report-arguments.js +62 -0
- package/dist/cli/commands/report-cli-command.js +36 -0
- package/dist/cli/commands/report-usage.js +41 -0
- package/dist/cli/commands/sessions-cli-command.js +184 -0
- package/dist/cli/commands/start-cli-command.js +83 -0
- package/dist/cli/commands/watch-cli-command.js +142 -0
- package/dist/cli/exit-codes.js +13 -0
- package/dist/cli/project-switches.js +46 -0
- package/dist/cli.js +80 -0
- package/dist/composition-root.js +411 -0
- package/dist/core/access/command-line.js +454 -0
- package/dist/core/access/listing.js +58 -0
- package/dist/core/access/path-shape.js +59 -0
- package/dist/core/access/path-tokens.js +37 -0
- package/dist/core/access/protected-access.js +310 -0
- package/dist/core/access/protected-values.js +102 -0
- package/dist/core/access/recorded-effect.js +67 -0
- package/dist/core/access/returns.js +173 -0
- package/dist/core/access/search-output.js +34 -0
- package/dist/core/access/search-reach.js +268 -0
- package/dist/core/access/uses.js +143 -0
- package/dist/core/access/words-before.js +52 -0
- package/dist/core/agent.js +1 -0
- package/dist/core/capability.js +11 -0
- package/dist/core/combined-project-catalogue.js +36 -0
- package/dist/core/combined-session-catalogue.js +25 -0
- package/dist/core/completeness.js +10 -0
- package/dist/core/context.js +1 -0
- package/dist/core/correlation/correlate.js +329 -0
- package/dist/core/delegation.js +1 -0
- package/dist/core/detector/prompt-signals.js +78 -0
- package/dist/core/entry-point.js +1 -0
- package/dist/core/event.js +1 -0
- package/dist/core/evidence.js +9 -0
- package/dist/core/late-message.js +32 -0
- package/dist/core/message.js +1 -0
- package/dist/core/nearest-project.js +28 -0
- package/dist/core/policy/default-policy.js +32 -0
- package/dist/core/policy/glob.js +56 -0
- package/dist/core/policy/parse-policy.js +70 -0
- package/dist/core/policy/policy.js +20 -0
- package/dist/core/policy/resolve-policy.js +16 -0
- package/dist/core/project-catalogue.js +1 -0
- package/dist/core/project-root.js +133 -0
- package/dist/core/provider-titles.js +10 -0
- package/dist/core/redaction/entropy.js +46 -0
- package/dist/core/redaction/redacted.js +4 -0
- package/dist/core/redaction/redactor.js +339 -0
- package/dist/core/redaction/secret-patterns.js +22 -0
- package/dist/core/redaction/value-shapes.js +81 -0
- package/dist/core/redaction/value-trace.js +110 -0
- package/dist/core/review.js +1 -0
- package/dist/core/session-catalogue.js +19 -0
- package/dist/core/session-filter.js +42 -0
- package/dist/core/session-format.js +27 -0
- package/dist/core/session-model.js +1 -0
- package/dist/core/session-reader.js +22 -0
- package/dist/core/session-records.js +1 -0
- package/dist/core/session-source.js +1 -0
- package/dist/core/session-titles.js +21 -0
- package/dist/core/turn.js +1 -0
- package/dist/doctor/codex-doctor.js +14 -0
- package/dist/doctor/doctor-use-case.js +1 -0
- package/dist/doctor/format-selecting-doctor.js +28 -0
- package/dist/doctor/render/attention/attention-rule.js +1 -0
- package/dist/doctor/render/attention/doctor-attention-rules.js +28 -0
- package/dist/doctor/render/attention/rules/damaged-meta-files-rule.js +8 -0
- package/dist/doctor/render/attention/rules/damaged-transcripts-rule.js +9 -0
- package/dist/doctor/render/attention/rules/incomplete-subagent-pairs-rule.js +6 -0
- package/dist/doctor/render/attention/rules/missing-main-transcript-rule.js +5 -0
- package/dist/doctor/render/attention/rules/missing-spilled-results-rule.js +6 -0
- package/dist/doctor/render/attention/rules/multiple-working-directories-rule.js +15 -0
- package/dist/doctor/render/attention/rules/unknown-denial-kinds-rule.js +5 -0
- package/dist/doctor/render/attention/rules/unknown-line-types-rule.js +6 -0
- package/dist/doctor/render/attention/rules/unrecognised-entries-rule.js +6 -0
- package/dist/doctor/render/attention/rules/unusable-directories-rule.js +12 -0
- package/dist/doctor/render/attention/transcript-scopes.js +6 -0
- package/dist/doctor/render/codex-text-doctor-renderer.js +97 -0
- package/dist/doctor/render/doctor-renderers.js +1 -0
- package/dist/doctor/render/json-doctor-renderer.js +7 -0
- package/dist/doctor/render/output-format.js +1 -0
- package/dist/doctor/render/text-doctor-renderer.js +124 -0
- package/dist/doctor/session-doctor.js +16 -0
- package/dist/infrastructure/clack-asker.js +24 -0
- package/dist/infrastructure/clack-chooser.js +40 -0
- package/dist/infrastructure/clack-multi-chooser.js +31 -0
- package/dist/infrastructure/file-alert-store.js +120 -0
- package/dist/infrastructure/file-checked-store.js +55 -0
- package/dist/infrastructure/file-mark-store.js +67 -0
- package/dist/infrastructure/file-onboarding-store.js +93 -0
- package/dist/infrastructure/folder-window.js +26 -0
- package/dist/infrastructure/node-agentwhy-invocation.js +99 -0
- package/dist/infrastructure/node-browser.js +40 -0
- package/dist/infrastructure/node-file-system.js +111 -0
- package/dist/infrastructure/node-local-server.js +115 -0
- package/dist/infrastructure/node-text-input.js +26 -0
- package/dist/infrastructure/os-notifier.js +21 -0
- package/dist/infrastructure/osascript-folder-chooser.js +53 -0
- package/dist/infrastructure/powershell-folder-chooser.js +78 -0
- package/dist/infrastructure/real-directory.js +18 -0
- package/dist/infrastructure/stream-printer.js +11 -0
- package/dist/infrastructure/terminal-banner.js +19 -0
- package/dist/ports/agentwhy-invocation.js +1 -0
- package/dist/ports/alert-store.js +1 -0
- package/dist/ports/asker.js +1 -0
- package/dist/ports/browser.js +1 -0
- package/dist/ports/checked-store.js +1 -0
- package/dist/ports/chooser.js +1 -0
- package/dist/ports/directory-reader.js +1 -0
- package/dist/ports/file-access-error.js +11 -0
- package/dist/ports/file-reader.js +1 -0
- package/dist/ports/file-tail-reader.js +1 -0
- package/dist/ports/file-writer.js +1 -0
- package/dist/ports/folder-chooser.js +1 -0
- package/dist/ports/local-server.js +1 -0
- package/dist/ports/mark-store.js +10 -0
- package/dist/ports/multi-chooser.js +1 -0
- package/dist/ports/notifier.js +1 -0
- package/dist/ports/onboarding-store.js +1 -0
- package/dist/ports/printer.js +1 -0
- package/dist/ports/text-input.js +1 -0
- package/dist/refuse/command-reach.js +135 -0
- package/dist/refuse/command-refusal.js +88 -0
- package/dist/refuse/render/codex-stop-words.js +21 -0
- package/dist/refuse/render/refusal-words.js +34 -0
- package/dist/report/build-report.js +1037 -0
- package/dist/report/check/actions-digest.js +73 -0
- package/dist/report/check/check-lines.js +83 -0
- package/dist/report/check/mark-request.js +90 -0
- package/dist/report/check/marks.js +84 -0
- package/dist/report/check/render/text-digest-renderer.js +332 -0
- package/dist/report/check/session-actions.js +90 -0
- package/dist/report/check/session-check.js +91 -0
- package/dist/report/choose-policy.js +61 -0
- package/dist/report/flow-reads.js +18 -0
- package/dist/report/private-files/tell-lists.js +84 -0
- package/dist/report/project-rules.js +33 -0
- package/dist/report/render/home-relative.js +18 -0
- package/dist/report/render/html-head.js +26 -0
- package/dist/report/render/html-report-components.js +48 -0
- package/dist/report/render/html-report-styles.js +500 -0
- package/dist/report/render/html-sidebar.js +42 -0
- package/dist/report/render/path-tail.js +14 -0
- package/dist/report/render/report-copy.js +1177 -0
- package/dist/report/render/report-page/advanced-view.js +167 -0
- package/dist/report/render/report-page/file-story.js +114 -0
- package/dist/report/render/report-page/files-view.js +381 -0
- package/dist/report/render/report-page/files.js +77 -0
- package/dist/report/render/report-page/fix-wizard-script.js +269 -0
- package/dist/report/render/report-page/fix-wizard.js +310 -0
- package/dist/report/render/report-page/helpers-view.js +383 -0
- package/dist/report/render/report-page/helpers.js +51 -0
- package/dist/report/render/report-page/item-names.js +63 -0
- package/dist/report/render/report-page/protect-patterns.js +13 -0
- package/dist/report/render/report-page/providers.js +208 -0
- package/dist/report/render/report-page/record-view.js +367 -0
- package/dist/report/render/report-page/report-page-renderer.js +129 -0
- package/dist/report/render/report-page/report-views.js +31 -0
- package/dist/report/render/report-page/story-window.js +356 -0
- package/dist/report/render/report-page/times.js +25 -0
- package/dist/report/render/report-page/to-do-view.js +299 -0
- package/dist/report/render/report-page/to-do.js +152 -0
- package/dist/report/render/report-page.js +1 -0
- package/dist/report/render/session-view.js +250 -0
- package/dist/report/render/text-report-renderer.js +981 -0
- package/dist/report/render/ui/add-file-popup.js +122 -0
- package/dist/report/render/ui/app-sidebar.js +78 -0
- package/dist/report/render/ui/ask-panel.js +80 -0
- package/dist/report/render/ui/avatar.js +19 -0
- package/dist/report/render/ui/backdrop.js +29 -0
- package/dist/report/render/ui/brand-mark.js +4 -0
- package/dist/report/render/ui/button.js +67 -0
- package/dist/report/render/ui/callout.js +20 -0
- package/dist/report/render/ui/chat-example.js +17 -0
- package/dist/report/render/ui/checklist.js +50 -0
- package/dist/report/render/ui/confirm-dialog.js +34 -0
- package/dist/report/render/ui/data-table.js +49 -0
- package/dist/report/render/ui/drawer.js +25 -0
- package/dist/report/render/ui/file-chip.js +37 -0
- package/dist/report/render/ui/fold-line.js +22 -0
- package/dist/report/render/ui/guide-card.js +25 -0
- package/dist/report/render/ui/hero.js +37 -0
- package/dist/report/render/ui/labelled-select.js +10 -0
- package/dist/report/render/ui/live-script.js +222 -0
- package/dist/report/render/ui/local-date.js +58 -0
- package/dist/report/render/ui/mode-icon.js +6 -0
- package/dist/report/render/ui/page-shell.js +101 -0
- package/dist/report/render/ui/pill-tabs.js +47 -0
- package/dist/report/render/ui/popup.js +77 -0
- package/dist/report/render/ui/progress.js +35 -0
- package/dist/report/render/ui/project-list.js +284 -0
- package/dist/report/render/ui/scope-choice.js +19 -0
- package/dist/report/render/ui/stats.js +21 -0
- package/dist/report/render/ui/status-icon.js +30 -0
- package/dist/report/render/ui/status-look.js +21 -0
- package/dist/report/render/ui/switch.js +11 -0
- package/dist/report/render/ui/tag.js +19 -0
- package/dist/report/render/ui/task-list.js +56 -0
- package/dist/report/render/ui/tokens.js +46 -0
- package/dist/report/render/ui/update-notice.js +146 -0
- package/dist/report/render/ui/words/app-words.js +163 -0
- package/dist/report/render/ui/words/conversations-words.js +457 -0
- package/dist/report/render/ui/words/month-words.js +59 -0
- package/dist/report/render/ui/words/onboarding-words.js +446 -0
- package/dist/report/render/ui/words/projects-words.js +217 -0
- package/dist/report/render/ui/words/report-words.js +2099 -0
- package/dist/report/render/ui/words/settings-words.js +611 -0
- package/dist/report/render/ui/words/to-fix-words.js +287 -0
- package/dist/report/report-model.js +2 -0
- package/dist/report/report-use-case.js +1 -0
- package/dist/report/rule-names.js +20 -0
- package/dist/report/session-report.js +163 -0
- package/dist/report/start/app-nav.js +37 -0
- package/dist/report/start/conversations/calendar-view.js +104 -0
- package/dist/report/start/conversations/conversation-columns.js +187 -0
- package/dist/report/start/conversations/conversations-renderer.js +54 -0
- package/dist/report/start/conversations/empty-week.js +120 -0
- package/dist/report/start/conversations/files-window.js +166 -0
- package/dist/report/start/conversations/period-section.js +280 -0
- package/dist/report/start/conversations/periods-script.js +440 -0
- package/dist/report/start/conversations/periods.js +110 -0
- package/dist/report/start/conversations/week-view.js +30 -0
- package/dist/report/start/conversations/weeks.js +17 -0
- package/dist/report/start/month/month-renderer.js +50 -0
- package/dist/report/start/month/month-view.js +135 -0
- package/dist/report/start/month/months.js +17 -0
- package/dist/report/start/onboarding/done.js +171 -0
- package/dist/report/start/onboarding/finish-onboarding.js +93 -0
- package/dist/report/start/onboarding/intro.js +100 -0
- package/dist/report/start/onboarding/onboarding-changes.js +40 -0
- package/dist/report/start/onboarding/onboarding-renderer.js +195 -0
- package/dist/report/start/onboarding/onboarding-script.js +400 -0
- package/dist/report/start/onboarding/onboarding-view.js +98 -0
- package/dist/report/start/onboarding/project-step.js +210 -0
- package/dist/report/start/onboarding/steps.js +239 -0
- package/dist/report/start/onboarding/welcome.js +66 -0
- package/dist/report/start/page-notice.js +10 -0
- package/dist/report/start/projects/index-projects.js +43 -0
- package/dist/report/start/projects/moved-page.js +82 -0
- package/dist/report/start/projects/project-switch.js +138 -0
- package/dist/report/start/projects/projects-window.js +67 -0
- package/dist/report/start/projects/temporary-space.js +18 -0
- package/dist/report/start/render/air-datepicker-vendor.js +27 -0
- package/dist/report/start/render/session-status.js +41 -0
- package/dist/report/start/render/start-words.js +84 -0
- package/dist/report/start/repository.js +28 -0
- package/dist/report/start/serve/index-handler.js +372 -0
- package/dist/report/start/serve/onboarding-request.js +50 -0
- package/dist/report/start/serve/settings-request.js +1 -0
- package/dist/report/start/serve/settings-setup.js +193 -0
- package/dist/report/start/session-index.js +1 -0
- package/dist/report/start/session-start.js +974 -0
- package/dist/report/start/settings/alerts-tab.js +104 -0
- package/dist/report/start/settings/files-tab.js +169 -0
- package/dist/report/start/settings/general-tab.js +71 -0
- package/dist/report/start/settings/settings-renderer.js +254 -0
- package/dist/report/start/settings/settings-script.js +161 -0
- package/dist/report/start/settings/settings-view.js +240 -0
- package/dist/report/start/settings/settings-windows.js +334 -0
- package/dist/report/start/settings-files.js +43 -0
- package/dist/report/start/to-fix/done-list.js +45 -0
- package/dist/report/start/to-fix/file-window.js +110 -0
- package/dist/report/start/to-fix/to-fix-list.js +70 -0
- package/dist/report/start/to-fix/to-fix-renderer.js +237 -0
- package/dist/report/start/to-fix/to-fix-script.js +94 -0
- package/dist/report/start/to-fix/to-fix-view.js +124 -0
- package/dist/report/watch/agent-alert.js +72 -0
- package/dist/report/watch/notice-choices.js +35 -0
- package/dist/report/watch/notice-settings.js +65 -0
- package/dist/report/watch/preferences.js +100 -0
- package/dist/report/watch/render/instruction-words.js +54 -0
- package/dist/report/watch/render/notice-words.js +182 -0
- package/dist/report/watch/subagent-watch.js +345 -0
- package/dist/report/watch/watch-notice.js +1 -0
- package/dist/setup/behind.js +18 -0
- package/dist/setup/codex-mirror.js +171 -0
- package/dist/setup/not-a-project.js +19 -0
- package/dist/setup/project-setup.js +644 -0
- package/dist/setup/protected-patterns.js +30 -0
- package/dist/shared/colour.js +23 -0
- package/dist/shared/compare.js +4 -0
- package/dist/shared/content-version.js +13 -0
- package/dist/shared/counter.js +11 -0
- package/dist/shared/file-stamp.js +4 -0
- package/dist/shared/json.js +13 -0
- package/dist/shared/label.js +13 -0
- package/dist/shared/package-name.js +6 -0
- package/dist/shared/plain-invocation.js +44 -0
- package/dist/shared/plain-version.js +21 -0
- package/dist/shared/printable.js +25 -0
- package/dist/shared/renderer.js +1 -0
- package/dist/shared/sentence.js +20 -0
- package/dist/shared/terminal-logo.js +41 -0
- package/dist/shared/text-table.js +15 -0
- package/dist/shared/wrap.js +29 -0
- package/package.json +70 -0
|
@@ -0,0 +1,981 @@
|
|
|
1
|
+
import { PROVIDER_NAMES } from '../../core/session-format.js';
|
|
2
|
+
import { paint } from '../../shared/colour.js';
|
|
3
|
+
import { logoWidth, TAGLINE, terminalLogo } from '../../shared/terminal-logo.js';
|
|
4
|
+
import { wrap } from '../../shared/wrap.js';
|
|
5
|
+
const INDENT = ' ';
|
|
6
|
+
const DEEP = ' ';
|
|
7
|
+
/**
|
|
8
|
+
* The lines the tree is drawn with. `--ascii` exists for a terminal whose `LANG` lacks UTF-8 (§7.5 constraint
|
|
9
|
+
* 2); the shape has to survive the substitution, which is why the branch marks are the same width in both.
|
|
10
|
+
*/
|
|
11
|
+
const MARKS = {
|
|
12
|
+
unicode: { branch: '├─ ', last: '└─ ', trunk: '│ ', clear: ' ', rule: '━' },
|
|
13
|
+
ascii: { branch: '+- ', last: '`- ', trunk: '| ', clear: ' ', rule: '-' },
|
|
14
|
+
};
|
|
15
|
+
/** How many files a group of the summary shows before it says how many more there are (R7). */
|
|
16
|
+
const GROUP_LIMIT = 8;
|
|
17
|
+
/** Between a path and what is said about it. */
|
|
18
|
+
const GAP = ' ';
|
|
19
|
+
/** Narrower than this, a path column cuts too much to read, and every path takes a line of its own instead. */
|
|
20
|
+
const MIN_COLUMN = 12;
|
|
21
|
+
/** The outcome as a word, so that colour, when it comes, only ever repeats what is already written (R14). */
|
|
22
|
+
const OUTCOME_WORD = {
|
|
23
|
+
succeeded: 'reached',
|
|
24
|
+
blocked: 'refused',
|
|
25
|
+
unknown: 'outcome unknown',
|
|
26
|
+
};
|
|
27
|
+
/** The same outcome as a mark, so a column of rows can be read before its words are - never without them (R14). */
|
|
28
|
+
const OUTCOME_MARK = {
|
|
29
|
+
succeeded: '●',
|
|
30
|
+
blocked: '○',
|
|
31
|
+
unknown: '?',
|
|
32
|
+
};
|
|
33
|
+
/** A summary row's outcome mark and the space after it. */
|
|
34
|
+
const MARK_WIDTH = 2;
|
|
35
|
+
/** Under the first line of the headline, so the sentence reads as one block beside its mark. */
|
|
36
|
+
const HANG = ' ';
|
|
37
|
+
/**
|
|
38
|
+
* The §7.2 sections, in reading order: what was examined, what it adds up to, what happened per delegation,
|
|
39
|
+
* what was found, and what could not be established.
|
|
40
|
+
*
|
|
41
|
+
* There is no verdict here and no hypothesis: judging a delegation is the detector's work, and the detector
|
|
42
|
+
* does not exist until its measurement says it should (spec §6.5). The report states facts and where each one
|
|
43
|
+
* was read from.
|
|
44
|
+
*
|
|
45
|
+
* **Two views of one model** (`specs/2026-09-15-findings-worth-reading.md` R5, R13). The summary is the default: on a real
|
|
46
|
+
* session the detailed sections ran to 700 lines, and a gap printed below them was a gap nobody read. `--full`
|
|
47
|
+
* prints those sections exactly as they were, and the summary's last line says so.
|
|
48
|
+
*/
|
|
49
|
+
export class TextReportRenderer {
|
|
50
|
+
#width;
|
|
51
|
+
#marks;
|
|
52
|
+
#ascii;
|
|
53
|
+
#full;
|
|
54
|
+
#colour;
|
|
55
|
+
constructor(width, view = {}) {
|
|
56
|
+
this.#width = width;
|
|
57
|
+
this.#ascii = view.ascii === true;
|
|
58
|
+
this.#full = view.full === true;
|
|
59
|
+
this.#colour = view.colour === true && !this.#ascii;
|
|
60
|
+
this.#marks = this.#ascii ? MARKS.ascii : MARKS.unicode;
|
|
61
|
+
}
|
|
62
|
+
render(report) {
|
|
63
|
+
const lines = this.#full ? this.#detailed(report) : this.#summarised(report);
|
|
64
|
+
const body = this.#colour ? colourise(lines) : lines;
|
|
65
|
+
const text = [...this.#masthead(report), '', ...body].join('\n');
|
|
66
|
+
return this.#ascii ? toAscii(text) : text;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The wordmark, and beside it what this report is about - the tool, the session and the policy - before a single
|
|
70
|
+
* number (§7.5, "a header visible without scrolling"). A width too narrow for the words keeps the mark alone.
|
|
71
|
+
*/
|
|
72
|
+
#masthead(report) {
|
|
73
|
+
const room = this.#width - logoWidth(this.#ascii);
|
|
74
|
+
const about = [TAGLINE, `session ${report.scope.sessionId} · policy ${report.scope.policy.level}`];
|
|
75
|
+
const beside = room < 20 ? [] : about.map((words) => fitWords(words, room, this.#ascii));
|
|
76
|
+
return terminalLogo(beside, { colour: this.#colour, ascii: this.#ascii });
|
|
77
|
+
}
|
|
78
|
+
/** Every section, as the report printed them before it had a summary (R13). */
|
|
79
|
+
#detailed(report) {
|
|
80
|
+
return [
|
|
81
|
+
...this.#headline(report),
|
|
82
|
+
'',
|
|
83
|
+
...this.#graph(report, false),
|
|
84
|
+
...this.#stories(report),
|
|
85
|
+
...this.#returns(report),
|
|
86
|
+
...this.#uses(report),
|
|
87
|
+
...this.#summary(report),
|
|
88
|
+
'',
|
|
89
|
+
...this.#scope(report),
|
|
90
|
+
'',
|
|
91
|
+
...this.#secretShapes(report),
|
|
92
|
+
...this.#unattributed(report),
|
|
93
|
+
...this.#missing(report),
|
|
94
|
+
...this.#detail(report),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* What a reader takes in without scrolling far (R5): the answer, the shape, what could not be established, what
|
|
99
|
+
* was reached, and the scope. The last line says where the rest is (R10), as flags to add rather than a command
|
|
100
|
+
* to type: the command would read the newest session, not necessarily this one.
|
|
101
|
+
*/
|
|
102
|
+
#summarised(report) {
|
|
103
|
+
return [
|
|
104
|
+
...this.#headline(report),
|
|
105
|
+
'',
|
|
106
|
+
...this.#graph(report, true),
|
|
107
|
+
...this.#gaps(report),
|
|
108
|
+
...this.#reached(report),
|
|
109
|
+
...this.#scope(report),
|
|
110
|
+
'',
|
|
111
|
+
...wrap('Every detail: add --full · the page: add --open', this.#width),
|
|
112
|
+
'',
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
/** The line above and below the answer. It is drawn with the same characters the tree is (§7.5 constraint 2). */
|
|
116
|
+
#rule() {
|
|
117
|
+
return this.#marks.rule.repeat(Math.max(this.#width, 8));
|
|
118
|
+
}
|
|
119
|
+
/** The answer, before anything else. Someone who reads four lines has still been told what happened. */
|
|
120
|
+
#headline(report) {
|
|
121
|
+
const { headline } = report;
|
|
122
|
+
const mark = headline.severity === 'clear' ? '✓' : '!';
|
|
123
|
+
return [
|
|
124
|
+
this.#rule(),
|
|
125
|
+
...this.#hanging(` ${mark} `, `${headline.sentence}`),
|
|
126
|
+
...(headline.caveat === undefined ? [] : this.#hanging(HANG, `${headline.caveat}`)),
|
|
127
|
+
this.#rule(),
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The chain, per file: who reached it, what they had been asked to do, and how. The instruction comes first
|
|
132
|
+
* because it is the cause - the whole argument of this tool is that the cause is in the delegation, not in
|
|
133
|
+
* the call that ends up in an audit log.
|
|
134
|
+
*/
|
|
135
|
+
#stories(report) {
|
|
136
|
+
if (report.stories.length === 0)
|
|
137
|
+
return [];
|
|
138
|
+
return [
|
|
139
|
+
heading('What was reached'),
|
|
140
|
+
'',
|
|
141
|
+
...[...byCall(report.stories).values()].flatMap((group) => this.#call(group)),
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* One call, and the files it reached (the maintainer's choice of 2026-09-15, on a real session). A search that
|
|
146
|
+
* returns four protected paths is one thing that happened: printing who, what they were asked, what they ran and
|
|
147
|
+
* what they wrote four times over made the page read as four incidents.
|
|
148
|
+
*/
|
|
149
|
+
#call(group) {
|
|
150
|
+
const lead = group[0];
|
|
151
|
+
const files = group.map((story) => `${story.path}${story.occurrences > 1 ? ` (${story.occurrences}×)` : ''}`);
|
|
152
|
+
return [
|
|
153
|
+
`${INDENT}${group.length === 1 ? files[0] : `${group.length} files, in one call`}`,
|
|
154
|
+
`${DEEP}${lead.source === 'result' ? 'appeared in the result' : 'named by the call'} · ${lead.outcome}${printed(group.reduce((sum, story) => sum + (story.lines ?? 0), 0))}`,
|
|
155
|
+
...(group.length === 1
|
|
156
|
+
? []
|
|
157
|
+
: files.map((file, index) => `${DEEP}${index === 0 ? 'files ' : ' '}${file}`)),
|
|
158
|
+
`${DEEP}who ${lead.who}`,
|
|
159
|
+
...(lead.askedTo === undefined
|
|
160
|
+
? []
|
|
161
|
+
// The label is put on the first line below; wrapping it in as well printed it twice - `asked to asked to`.
|
|
162
|
+
: wrap(`${lead.askedTo}`, this.#width - DEEP.length - 9).map((line, index) => index === 0 ? `${DEEP}asked to ${line}` : `${DEEP} ${line}`)),
|
|
163
|
+
`${DEEP}did ${lead.did} [${lead.evidence}]`,
|
|
164
|
+
...this.#wrote(lead.wrote),
|
|
165
|
+
'',
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* What the agent wrote immediately before the call (`specs/2026-09-15-why-this-call.md` R6-R8), at the level §5.1 settles:
|
|
170
|
+
* one sentence, or the fact alone. It is introduced as what was written, never as a reason - naming it one
|
|
171
|
+
* would be the verdict this tool does not make.
|
|
172
|
+
*/
|
|
173
|
+
#wrote(wrote) {
|
|
174
|
+
// Nothing written is nothing to count and nothing to point at: a record reference here would point at a silence.
|
|
175
|
+
// A block the record kept empty is said as that, because it is not the same fact.
|
|
176
|
+
if (wrote.size === 0) {
|
|
177
|
+
return [
|
|
178
|
+
`${DEEP}wrote ${wrote.blank === 0 ? 'nothing before this call' : `no words before this call - the record kept ${wrote.blank} of them empty`}`,
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
const covers = wrote.covers > 1 ? ` · stands before ${wrote.covers} calls` : '';
|
|
182
|
+
const words = wrote.sentence !== undefined
|
|
183
|
+
? `"${wrote.sentence}"${wrote.cut === true ? ' (cut)' : ''} · ${wrote.kind === 'reasoning' ? 'its own working-out' : 'a message to you'}`
|
|
184
|
+
: wrote.carried.length > 0
|
|
185
|
+
? `not shown - these words carried a value from ${wrote.carried.join(', ')}`
|
|
186
|
+
: `${wrote.size} characters, not shown in this view`;
|
|
187
|
+
// The label is put on after wrapping: `wrap` collapses runs of spaces, and the column it keeps is the point.
|
|
188
|
+
return wrap(`${words}${covers} [${wrote.evidence}]`, this.#width - DEEP.length - 9).map((line, index) => index === 0 ? `${DEEP}wrote ${line}` : `${DEEP} ${line}`);
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* The session as a shape: who was asked for what, and which of them reached something. The graph orients and
|
|
192
|
+
* the list below it expands (§7.5) - which is why it comes first and says as little per agent as it can.
|
|
193
|
+
*
|
|
194
|
+
* An indented tree, not a fan-out: one real session holds fifteen agents, and at that count a fan-out is
|
|
195
|
+
* unreadable at any width (lesson L006). Nothing here is carried by colour: the text is the whole signal, so
|
|
196
|
+
* a redirected report says the same as one on a terminal.
|
|
197
|
+
*
|
|
198
|
+
* In the summary, more than one agent with no recorded delegation is said once, under the tree, by number
|
|
199
|
+
* (R12): the same sentence under every node is a column of repetition a reader learns to skip.
|
|
200
|
+
*/
|
|
201
|
+
#graph(report, summarised) {
|
|
202
|
+
const { graph } = report;
|
|
203
|
+
if (graph.agents.length === 0)
|
|
204
|
+
return [];
|
|
205
|
+
const known = new Set([graph.main.index, ...graph.agents.map((agent) => agent.index)]);
|
|
206
|
+
const children = new Map();
|
|
207
|
+
for (const agent of graph.agents) {
|
|
208
|
+
// A parent that cannot be established is not invented: the agent hangs from the session and says so.
|
|
209
|
+
const parent = agent.parentIndex !== undefined && known.has(agent.parentIndex) ? agent.parentIndex : undefined;
|
|
210
|
+
const key = parent ?? graph.main.index;
|
|
211
|
+
children.set(key, [...(children.get(key) ?? []), agent]);
|
|
212
|
+
}
|
|
213
|
+
const missing = graph.totalAgents - graph.agents.length - 1;
|
|
214
|
+
const unresolved = graph.agents.filter((agent) => agent.parentIndex === undefined);
|
|
215
|
+
const once = summarised && unresolved.length > 1;
|
|
216
|
+
const tree = { children, drawn: new Set(), graph, markUnresolved: !once, fold: summarised };
|
|
217
|
+
return [
|
|
218
|
+
heading('Who ran, and what they reached'),
|
|
219
|
+
'',
|
|
220
|
+
...this.#tree([{ prefix: INDENT, agent: graph.main }, ...this.#branches(graph.main.index, INDENT, tree)]),
|
|
221
|
+
...(missing > 0 ? [`${INDENT}${this.#marks.clear}… and ${missing} more not drawn`] : []),
|
|
222
|
+
...(once ? this.#item(`nothing recorded what asked for ${agentsNamed(unresolved)}`) : []),
|
|
223
|
+
'',
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The agents below `parent`, in the order they ran. In the summary, two or more that reached, refused, returned
|
|
228
|
+
* and wrote nothing, and have no agent below them, are said in one line: R12's reasoning one step further. On a
|
|
229
|
+
* real session nine rows of fifteen said only how many actions an agent took, and buried the six that mattered.
|
|
230
|
+
*/
|
|
231
|
+
#branches(parent, prefix, tree) {
|
|
232
|
+
const { children, drawn, graph, markUnresolved, fold } = tree;
|
|
233
|
+
const here = (children.get(parent) ?? []).filter((agent) => !drawn.has(agent.index));
|
|
234
|
+
const quiet = fold ? here.filter((agent) => isQuiet(agent, parent, tree)) : [];
|
|
235
|
+
const folded = new Set(quiet.length > 1 ? quiet.map((agent) => agent.index) : []);
|
|
236
|
+
const drawnHere = here.filter((agent) => !folded.has(agent.index));
|
|
237
|
+
for (const index of folded)
|
|
238
|
+
drawn.add(index);
|
|
239
|
+
const rows = drawnHere.flatMap((agent, position) => {
|
|
240
|
+
if (drawn.has(agent.index))
|
|
241
|
+
return [];
|
|
242
|
+
drawn.add(agent.index);
|
|
243
|
+
const last = position === drawnHere.length - 1 && folded.size === 0;
|
|
244
|
+
const mark = last ? this.#marks.last : this.#marks.branch;
|
|
245
|
+
const below = prefix + (last ? this.#marks.clear : this.#marks.trunk);
|
|
246
|
+
const unresolved = agent.parentIndex === undefined && parent === graph.main.index;
|
|
247
|
+
return [
|
|
248
|
+
{ prefix: `${prefix}${mark}`, agent },
|
|
249
|
+
...(unresolved && markUnresolved
|
|
250
|
+
? [{ prefix: `${below}${INDENT}`, text: 'nothing recorded what asked for this agent' }]
|
|
251
|
+
: []),
|
|
252
|
+
...this.#branches(agent.index, below, tree),
|
|
253
|
+
];
|
|
254
|
+
});
|
|
255
|
+
if (folded.size === 0)
|
|
256
|
+
return rows;
|
|
257
|
+
return [
|
|
258
|
+
...rows,
|
|
259
|
+
{
|
|
260
|
+
prefix: `${prefix}${this.#marks.last}`,
|
|
261
|
+
text: `${quiet.length} ${plural(quiet.length, 'agent')} reached nothing: ${agentsNamed(quiet)}`,
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* The tree in columns, so an eye can run down "reached" without reading every line. When the columns do not fit
|
|
267
|
+
* the width, every agent is one line of its own words instead (§7.5 constraint 1): the same content, never a
|
|
268
|
+
* squeezed table.
|
|
269
|
+
*/
|
|
270
|
+
#tree(rows) {
|
|
271
|
+
const agents = rows.flatMap((row) => ('agent' in row ? [row] : []));
|
|
272
|
+
const digits = Math.max(...agents.map(({ agent }) => String(agent.actions).length));
|
|
273
|
+
const cells = new Map(agents.map((row) => [row, nodeCells(row.agent, digits)]));
|
|
274
|
+
const title = Math.max(...agents.map((row) => `${row.prefix}${agentTitle(row.agent)}`.length));
|
|
275
|
+
const widths = [0, 1, 2, 3].map((column) => Math.max(0, ...agents.map((row) => cells.get(row)?.[column]?.length ?? 0)));
|
|
276
|
+
const columned = (row) => [
|
|
277
|
+
`${row.prefix}${agentTitle(row.agent)}`.padEnd(title),
|
|
278
|
+
...(cells.get(row) ?? []).flatMap((cell, column) => (widths[column] ?? 0) === 0 ? [] : [cell.padEnd(widths[column] ?? 0)]),
|
|
279
|
+
]
|
|
280
|
+
.join(GAP)
|
|
281
|
+
.trimEnd();
|
|
282
|
+
const fits = agents.every((row) => columned(row).length <= this.#width);
|
|
283
|
+
return rows.flatMap((row) => {
|
|
284
|
+
if (!('agent' in row))
|
|
285
|
+
return this.#hanging(row.prefix, row.text);
|
|
286
|
+
return fits ? [columned(row)] : this.#hanging(row.prefix, nodeLine(row.agent));
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/** Text after a prefix, wrapped under itself rather than back at the margin. */
|
|
290
|
+
#hanging(prefix, text) {
|
|
291
|
+
return wrap(text, Math.max(this.#width - prefix.length, 20)).map((line, index) => `${index === 0 ? prefix : ' '.repeat(prefix.length)}${line}`);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* What could not be established, before what was reached (R8). A gap printed below the findings is read after
|
|
295
|
+
* the reader has decided the report is complete, which §5.3 of the main specification says it must not read as.
|
|
296
|
+
*/
|
|
297
|
+
#gaps(report) {
|
|
298
|
+
const { summary, scope } = report;
|
|
299
|
+
const unknown = summary.byOutcome.unknown;
|
|
300
|
+
const unattributed = summary.unattributedAgents;
|
|
301
|
+
const lines = [
|
|
302
|
+
...(scope.completeness === 'complete' ? [] : [`evidence: ${scope.completeness}`]),
|
|
303
|
+
...(unknown === 0
|
|
304
|
+
? []
|
|
305
|
+
: [`no outcome was established for ${unknown} of ${summary.actions} ${plural(summary.actions, 'action')}`]),
|
|
306
|
+
...report.missing.map((item) => `${item}`),
|
|
307
|
+
// R9: a count, and where to see who they were.
|
|
308
|
+
...(unattributed === 0
|
|
309
|
+
? []
|
|
310
|
+
: [
|
|
311
|
+
`${unattributed} ${plural(unattributed, 'agent')} ran that nothing in the transcript asked for — ` +
|
|
312
|
+
'add --full to see what they did',
|
|
313
|
+
]),
|
|
314
|
+
];
|
|
315
|
+
return [
|
|
316
|
+
heading('What could not be established'),
|
|
317
|
+
'',
|
|
318
|
+
...(lines.length === 0 ? [`${INDENT}nothing: every source was read`] : lines.flatMap((line) => this.#item(line))),
|
|
319
|
+
'',
|
|
320
|
+
];
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* What was reached, as a list to run an eye down (R6, R7): one line per file, grouped under the agent that
|
|
324
|
+
* reached it, at most eight to a group. A line starts with a mark for its outcome - `●` reached, `○` refused, `?`
|
|
325
|
+
* not known - beside the word that says the same (R14). The chain behind each line - what the agent ran, and
|
|
326
|
+
* which record says so - is what `--full` is for.
|
|
327
|
+
*/
|
|
328
|
+
#reached(report) {
|
|
329
|
+
const { stories, graph, summary } = report;
|
|
330
|
+
// R9: which formats came back is detail; that something did is not.
|
|
331
|
+
const keyFormats = summary.secretShapes === 0
|
|
332
|
+
? []
|
|
333
|
+
: this.#item(`${summary.secretShapes} ${plural(summary.secretShapes, 'result')} carried something shaped like a key — ` +
|
|
334
|
+
'replaced, never shown; add --full for the formats');
|
|
335
|
+
if (stories.length === 0) {
|
|
336
|
+
return [heading('What was reached'), '', `${INDENT}nothing this policy protects`, ...keyFormats, ''];
|
|
337
|
+
}
|
|
338
|
+
const groups = new Map();
|
|
339
|
+
for (const story of stories) {
|
|
340
|
+
const key = story.agentIndex === undefined ? `who ${story.who}` : `agent ${story.agentIndex}`;
|
|
341
|
+
groups.set(key, [...(groups.get(key) ?? []), story]);
|
|
342
|
+
}
|
|
343
|
+
const listed = [...groups.values()].map((group) => ({ first: group[0], rows: reachedRows(group) }));
|
|
344
|
+
const shown = listed.flatMap(({ rows }) => rows.slice(0, GROUP_LIMIT));
|
|
345
|
+
const columns = {
|
|
346
|
+
how: Math.max(...shown.map((row) => row.how.length)),
|
|
347
|
+
outcome: Math.max(...shown.map((row) => OUTCOME_WORD[row.outcome].length)),
|
|
348
|
+
};
|
|
349
|
+
const column = Math.min(Math.max(...shown.map((row) => row.path.length)), this.#width - DEEP.length - MARK_WIDTH - GAP.length - Math.max(...shown.map((row) => said(row, columns).length)));
|
|
350
|
+
const fitted = column < MIN_COLUMN
|
|
351
|
+
? new Map()
|
|
352
|
+
: fitPaths(shown.map((row) => row.path), column, this.#ascii);
|
|
353
|
+
const told = new Set();
|
|
354
|
+
return [
|
|
355
|
+
heading('What was reached'),
|
|
356
|
+
'',
|
|
357
|
+
...listed.flatMap(({ first, rows }) => [
|
|
358
|
+
...this.#whoReached(first, graph),
|
|
359
|
+
...rows.slice(0, GROUP_LIMIT).flatMap((row) => this.#row(row, fitted.get(row.path), column, columns)),
|
|
360
|
+
...(rows.length > GROUP_LIMIT ? [`${DEEP}… and ${rows.length - GROUP_LIMIT} more — add --full to see them`] : []),
|
|
361
|
+
...this.#cameBackFrom(report, first.agentIndex),
|
|
362
|
+
...this.#appearedAfter(report, rows, told),
|
|
363
|
+
'',
|
|
364
|
+
]),
|
|
365
|
+
// An agent that reached nothing itself can still hand back what an agent below it reached (R8).
|
|
366
|
+
...[...new Set(report.returns.map((entry) => entry.agentIndex))]
|
|
367
|
+
.filter((index) => index !== undefined && !stories.some((story) => story.agentIndex === index))
|
|
368
|
+
.flatMap((index) => [`${INDENT}${titleOf(graph, index)}`, ...this.#cameBackFrom(report, index), '']),
|
|
369
|
+
...keyFormats,
|
|
370
|
+
...(keyFormats.length === 0 ? [] : ['']),
|
|
371
|
+
];
|
|
372
|
+
}
|
|
373
|
+
/** R12: what came back from this agent, in words that need no colour to be read. */
|
|
374
|
+
#cameBackFrom(report, agentIndex) {
|
|
375
|
+
return report.returns
|
|
376
|
+
.filter((entry) => agentIndex !== undefined && entry.agentIndex === agentIndex)
|
|
377
|
+
.flatMap((entry) => wrap(`${cameBack(entry, titleOf(report.graph, entry.parentAgentIndex))}${written(entry)}`, this.#width - DEEP.length - MARK_WIDTH).map((line, index) => `${DEEP}${index === 0 ? '↩ ' : ' '}${line}`));
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* where-the-value-went R12: under the files a group reached, where a value from them appeared after - the files it
|
|
381
|
+
* was written into, the commands that carried it, the agents whose words did. Once for each file, in words alone.
|
|
382
|
+
*/
|
|
383
|
+
#appearedAfter(report, rows, told) {
|
|
384
|
+
const paths = new Set(rows.map((row) => row.path).filter((path) => !told.has(path)));
|
|
385
|
+
for (const path of paths)
|
|
386
|
+
told.add(path);
|
|
387
|
+
const words = appearedAfter(report.uses.filter((use) => use.files.some((file) => paths.has(`${file}`))), report.graph);
|
|
388
|
+
return words === undefined
|
|
389
|
+
? []
|
|
390
|
+
: wrap(`then the same value was in: ${words}`, this.#width - DEEP.length - MARK_WIDTH).map((line, index) => `${DEEP}${index === 0 ? '→ ' : ' '}${line}`);
|
|
391
|
+
}
|
|
392
|
+
/** R13: every return, where it came back to, and the record that says so. */
|
|
393
|
+
#returns(report) {
|
|
394
|
+
if (report.returns.length === 0)
|
|
395
|
+
return [];
|
|
396
|
+
const { graph } = report;
|
|
397
|
+
return [
|
|
398
|
+
heading('What the agents answered'),
|
|
399
|
+
'',
|
|
400
|
+
...report.returns.flatMap((entry) => [
|
|
401
|
+
`${INDENT}${titleOf(graph, entry.agentIndex)}, answering ${titleOf(graph, entry.parentAgentIndex)}`,
|
|
402
|
+
...wrap(`${returnInFull(entry)}${written(entry)}`, this.#width - DEEP.length).map((line) => `${DEEP}${line}`),
|
|
403
|
+
`${DEEP}[${entry.evidence}]`,
|
|
404
|
+
'',
|
|
405
|
+
]),
|
|
406
|
+
];
|
|
407
|
+
}
|
|
408
|
+
/** where-the-value-went R12: every use, grouped by where it landed, with its agent, what it came after and its record. */
|
|
409
|
+
#uses(report) {
|
|
410
|
+
if (report.uses.length === 0)
|
|
411
|
+
return [];
|
|
412
|
+
const { graph } = report;
|
|
413
|
+
return [
|
|
414
|
+
heading('Where the same value was afterwards'),
|
|
415
|
+
'',
|
|
416
|
+
...LANDINGS.flatMap((landed) => {
|
|
417
|
+
const uses = report.uses.filter((use) => use.landed === landed);
|
|
418
|
+
return uses.length === 0
|
|
419
|
+
? []
|
|
420
|
+
: [
|
|
421
|
+
`${INDENT}${LANDING_WORDS[landed]}`,
|
|
422
|
+
...uses.flatMap((use) => [
|
|
423
|
+
...wrap(useInFull(use, graph), this.#width - DEEP.length).map((line) => `${DEEP}${line}`),
|
|
424
|
+
`${DEEP}[${use.evidence}]`,
|
|
425
|
+
]),
|
|
426
|
+
'',
|
|
427
|
+
];
|
|
428
|
+
}),
|
|
429
|
+
];
|
|
430
|
+
}
|
|
431
|
+
/** Who reached the files below, named as the graph names them, and what they had been asked to do. */
|
|
432
|
+
#whoReached(story, graph) {
|
|
433
|
+
const node = [graph.main, ...graph.agents].find((agent) => agent.index === story.agentIndex);
|
|
434
|
+
const name = node === undefined ? `${story.who}` : agentTitle(node);
|
|
435
|
+
const heading = story.askedTo === undefined ? name : `${name} — asked to ${story.askedTo}`;
|
|
436
|
+
return wrap(heading, this.#width - DEEP.length).map((line, index) => `${index === 0 ? INDENT : DEEP}${line}`);
|
|
437
|
+
}
|
|
438
|
+
/** One file, one line. A path no cut can keep apart from the others is given a line of its own instead (R11). */
|
|
439
|
+
#row(row, fitted, column, columns) {
|
|
440
|
+
const mark = OUTCOME_MARK[row.outcome];
|
|
441
|
+
if (fitted === undefined) {
|
|
442
|
+
return [
|
|
443
|
+
...wrap(row.path, this.#width - DEEP.length - MARK_WIDTH).map((line, index) => `${DEEP}${index === 0 ? `${mark} ` : ' '}${line}`),
|
|
444
|
+
`${DEEP} ${GAP}${said(row, columns)}`,
|
|
445
|
+
];
|
|
446
|
+
}
|
|
447
|
+
return [`${DEEP}${mark} ${fitted.padEnd(column)}${GAP}${said(row, columns)}`];
|
|
448
|
+
}
|
|
449
|
+
/** A line of a list, wrapped under itself rather than back at the margin. */
|
|
450
|
+
#item(text) {
|
|
451
|
+
return wrap(text, this.#width - DEEP.length).map((line, index) => `${index === 0 ? INDENT : DEEP}${line}`);
|
|
452
|
+
}
|
|
453
|
+
/** Everything that is not a finding, as counts. An inventory of 740 actions is not a report. */
|
|
454
|
+
#detail(report) {
|
|
455
|
+
const tools = Object.entries(report.summary.tools).sort(([, a], [, b]) => b - a);
|
|
456
|
+
if (tools.length === 0)
|
|
457
|
+
return [];
|
|
458
|
+
return [
|
|
459
|
+
heading('Everything the session did'),
|
|
460
|
+
`${INDENT}${tools.map(([tool, count]) => `${tool} ${count}`).join(' · ')}`,
|
|
461
|
+
`${INDENT}${report.delegations.length} delegations, listed above only where they reached something.`,
|
|
462
|
+
'',
|
|
463
|
+
];
|
|
464
|
+
}
|
|
465
|
+
#scope(report) {
|
|
466
|
+
const { scope } = report;
|
|
467
|
+
const { redaction } = scope;
|
|
468
|
+
// Every field is wrapped: a policy file can sit at a long path, and a header that runs off the page is the
|
|
469
|
+
// first thing a reader stops trusting.
|
|
470
|
+
return [
|
|
471
|
+
// X28: which AI the conversation was with, as the page and the list say it.
|
|
472
|
+
...this.#field('session', `${scope.sessionId} · ${PROVIDER_NAMES[scope.provider]}`),
|
|
473
|
+
...this.#field('policy', `${scope.policy.level} · ${scope.policy.patterns} patterns · ${scope.policy.exceptions} exceptions`),
|
|
474
|
+
...this.#field('source', scope.policy.origin),
|
|
475
|
+
...this.#field('paths', describeView(scope.paths.shared)),
|
|
476
|
+
...this.#field('project root', describeRoot(scope.paths)),
|
|
477
|
+
...this.#field('redaction', `ruleset v${redaction.rulesetVersion} · ${redaction.redactions} replacements of ` +
|
|
478
|
+
`${redaction.distinctValues} distinct values · ${redaction.protectedContents} protected contents`),
|
|
479
|
+
...(redaction.missingClasses.length === 0
|
|
480
|
+
? []
|
|
481
|
+
: this.#field('not implemented', redaction.missingClasses.join(', '))),
|
|
482
|
+
];
|
|
483
|
+
}
|
|
484
|
+
/** `label: value`, with continuation lines under the value rather than under the label. */
|
|
485
|
+
#field(label, value) {
|
|
486
|
+
const prefix = `${label}: `;
|
|
487
|
+
const lines = wrap(value, Math.max(this.#width - prefix.length, 20));
|
|
488
|
+
return lines.map((line, index) => (index === 0 ? `${prefix}${line}` : `${' '.repeat(prefix.length)}${line}`));
|
|
489
|
+
}
|
|
490
|
+
#summary(report) {
|
|
491
|
+
const { summary, scope } = report;
|
|
492
|
+
const { byOutcome } = summary;
|
|
493
|
+
return [
|
|
494
|
+
heading('The session in numbers'),
|
|
495
|
+
`${INDENT}${summary.agents} agents ran, ${summary.delegations} of them asked for by a recorded delegation`,
|
|
496
|
+
`${INDENT}${summary.actions} actions: ${byOutcome.succeeded} worked, ${byOutcome.blocked} were refused, ` +
|
|
497
|
+
`${byOutcome.unknown} could not be established`,
|
|
498
|
+
`${INDENT}evidence: ${scope.completeness}`,
|
|
499
|
+
...(summary.unattributedAgents === 0
|
|
500
|
+
? []
|
|
501
|
+
: [`${INDENT}${summary.unattributedAgents} agents ran that nothing in the transcript asked for — see below`]),
|
|
502
|
+
...(summary.secretShapes === 0
|
|
503
|
+
? []
|
|
504
|
+
: [`${INDENT}${summary.secretShapes} results carried something shaped like a key — replaced, never shown`]),
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
#mainAgent(report) {
|
|
508
|
+
if (report.mainAgent.length === 0)
|
|
509
|
+
return [];
|
|
510
|
+
return ['The session itself', '', ...report.mainAgent.map((action) => actionLine(action)), ''];
|
|
511
|
+
}
|
|
512
|
+
#sequence(report) {
|
|
513
|
+
if (report.delegations.length === 0)
|
|
514
|
+
return ['Delegations', `${INDENT}none`, ''];
|
|
515
|
+
return ['Delegations', '', ...report.delegations.flatMap((delegation, index) => this.#delegation(delegation, index))];
|
|
516
|
+
}
|
|
517
|
+
#delegation(delegation, index) {
|
|
518
|
+
const heading = `${String(index + 1).padStart(2, '0')} · ${delegation.id}`;
|
|
519
|
+
const type = delegation.agentType === undefined ? 'agent type not recorded' : `${delegation.agentType}`;
|
|
520
|
+
const depth = delegation.depth === undefined ? 'depth not recorded' : `depth ${delegation.depth}`;
|
|
521
|
+
return [
|
|
522
|
+
`${INDENT}${heading}`,
|
|
523
|
+
`${INDENT}${type} · ${depth} · evidence ${delegation.completeness}`,
|
|
524
|
+
'',
|
|
525
|
+
...(delegation.description === undefined
|
|
526
|
+
? [`${DEEP}(no description was recorded for this delegation)`]
|
|
527
|
+
: wrap(delegation.description, this.#width - DEEP.length).map((line) => `${DEEP}${line}`)),
|
|
528
|
+
`${DEEP}instruction: ${describeInstruction(delegation.instructionSize)}`,
|
|
529
|
+
'',
|
|
530
|
+
...(delegation.actions.length === 0
|
|
531
|
+
? [`${DEEP}no actions recorded for this agent`]
|
|
532
|
+
: delegation.actions.map((action) => actionLine(action))),
|
|
533
|
+
'',
|
|
534
|
+
];
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* An agent ran and nothing says who asked for it. Leaving it out would make a session that delegated three
|
|
538
|
+
* times read as one that delegated none - the failure this tool exists to prevent, in its own output.
|
|
539
|
+
*/
|
|
540
|
+
#unattributed(report) {
|
|
541
|
+
if (report.unattributedAgents.length === 0)
|
|
542
|
+
return [];
|
|
543
|
+
return [
|
|
544
|
+
heading('Agents with no recorded delegation'),
|
|
545
|
+
// Corrected 2026-09-14: written as one line, it ran to 106 characters at 100 columns on a real session.
|
|
546
|
+
...wrap('Started by a route that records no call - a skill, for instance - so what asked for them cannot be read.', this.#width - INDENT.length).map((line) => `${INDENT}${line}`),
|
|
547
|
+
'',
|
|
548
|
+
...report.unattributedAgents.flatMap((agent) => [
|
|
549
|
+
`${INDENT}${agent.id} · ${agent.type ?? 'type not recorded'} · ` +
|
|
550
|
+
`${agent.depth === undefined ? 'depth not recorded' : `depth ${agent.depth}`}`,
|
|
551
|
+
...(agent.actions.length === 0
|
|
552
|
+
? [`${DEEP}no actions recorded for this agent`]
|
|
553
|
+
: agent.actions.map((action) => actionLine(action))),
|
|
554
|
+
'',
|
|
555
|
+
]),
|
|
556
|
+
];
|
|
557
|
+
}
|
|
558
|
+
#findings(report) {
|
|
559
|
+
if (report.findings.length === 0) {
|
|
560
|
+
return ['Protected paths', `${INDENT}none reached under this policy`, ''];
|
|
561
|
+
}
|
|
562
|
+
return ['Protected paths', '', ...report.findings.flatMap((finding) => this.#finding(finding)), ''];
|
|
563
|
+
}
|
|
564
|
+
#finding(finding) {
|
|
565
|
+
// Where it was named is the whole point: `result` means no call parameter mentioned it.
|
|
566
|
+
const named = finding.source === 'input' ? 'named by the call' : 'reached through the result';
|
|
567
|
+
return [
|
|
568
|
+
`${INDENT}${finding.path}`,
|
|
569
|
+
`${DEEP}${finding.tool} — ${finding.outcome} · ${named}`,
|
|
570
|
+
`${DEEP}matched ${finding.pattern} · [${finding.evidence}]`,
|
|
571
|
+
];
|
|
572
|
+
}
|
|
573
|
+
/** What kind of key came back, never which one. A path was not needed for this to be worth knowing. */
|
|
574
|
+
#secretShapes(report) {
|
|
575
|
+
if (report.secretShapes.length === 0)
|
|
576
|
+
return [];
|
|
577
|
+
return [
|
|
578
|
+
heading('Recognised key formats in results'),
|
|
579
|
+
'',
|
|
580
|
+
...report.secretShapes.flatMap((shape) => [
|
|
581
|
+
`${INDENT}${shape.classes.join(', ')}`,
|
|
582
|
+
`${DEEP}${shape.tool} — ${shape.outcome} · [${shape.evidence}]`,
|
|
583
|
+
]),
|
|
584
|
+
'',
|
|
585
|
+
];
|
|
586
|
+
}
|
|
587
|
+
#missing(report) {
|
|
588
|
+
if (report.missing.length === 0)
|
|
589
|
+
return [heading('Missing data'), `${INDENT}nothing: every source was read`, ''];
|
|
590
|
+
return [heading('Missing data'), ...report.missing.map((item) => `${INDENT}${item}`), ''];
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
function actionLine(action) {
|
|
594
|
+
return (`${DEEP}${String(action.sequence).padStart(3)}. ${action.tool} — ${action.outcome}` +
|
|
595
|
+
`${action.target === undefined ? '' : ` → ${action.target}`} [${action.evidence}]`);
|
|
596
|
+
}
|
|
597
|
+
/** Which view a reader is holding. A short path means one thing under `--share` and another without it. */
|
|
598
|
+
function describeView(shared) {
|
|
599
|
+
return shared
|
|
600
|
+
? 'SHARED — relative to the project root, nothing above it appears. This reduces exposure; it does not anonymise'
|
|
601
|
+
: 'full — paths as written';
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Why a path looks the way it does. With no root the two views do opposite things - one shows every path as
|
|
605
|
+
* written, the other can place none of them inside a project and shows every absolute one as a category - so
|
|
606
|
+
* the sentence has to know which view it is in. Saying "shown as written" above a body of categories was the
|
|
607
|
+
* header contradicting the report under it.
|
|
608
|
+
*/
|
|
609
|
+
function describeRoot(paths) {
|
|
610
|
+
if (paths.root === 'known')
|
|
611
|
+
return 'established from the session; paths under it are shown relative to it';
|
|
612
|
+
const consequence = paths.shared
|
|
613
|
+
? 'so nothing can be placed inside the project: every path that says where it starts is shown as a category'
|
|
614
|
+
: 'so paths are shown as written';
|
|
615
|
+
const cause = paths.root === 'ambiguous'
|
|
616
|
+
? `the session recorded ${paths.workingDirectories ?? 0} working directories, so no single root describes it`
|
|
617
|
+
: 'no record carried one';
|
|
618
|
+
return `NOT established — ${cause}, ${consequence}`;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* An agent in one line. What it reached is said only when it reached something: with fifteen agents, repeating
|
|
622
|
+
* "nothing reached" on fourteen of them buries the one row a reader is looking for.
|
|
623
|
+
*/
|
|
624
|
+
function nodeLine(agent) {
|
|
625
|
+
return [
|
|
626
|
+
agentTitle(agent),
|
|
627
|
+
`${agent.actions} ${plural(agent.actions, 'action')}`,
|
|
628
|
+
...(agent.filesReached === 0 ? [] : [`${agent.filesReached} ${plural(agent.filesReached, 'file')} reached`]),
|
|
629
|
+
...(agent.refusedAttempts === 0 ? [] : [`${agent.refusedAttempts} refused`]),
|
|
630
|
+
...(agent.returned === undefined ? [] : [`gave the ${agent.returned} back`]),
|
|
631
|
+
...(agent.wroteValue === true ? ['wrote the value'] : []),
|
|
632
|
+
...(agent.wroteOnward === true ? ['wrote the value into files'] : []),
|
|
633
|
+
].join(' · ');
|
|
634
|
+
}
|
|
635
|
+
/** What the graph and the summary both call an agent, so that a reader can find one in the other. */
|
|
636
|
+
function agentTitle(agent) {
|
|
637
|
+
// The session's own agent has a name rather than a number, and naming its kind as well says it twice.
|
|
638
|
+
const named = agent.ordinal === undefined;
|
|
639
|
+
const name = named ? 'the session itself' : `Agent ${agent.ordinal}`;
|
|
640
|
+
return named || agent.type === undefined ? name : `${name} · ${agent.type}`;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Findings that came out of one call, kept together and in the order they were ranked. A call is named by the
|
|
644
|
+
* record its evidence points at, so two calls that reached the same file stay apart, and one call that reached
|
|
645
|
+
* four files is one block. The source and the outcome are part of the key: a call that named one file and found
|
|
646
|
+
* another in what came back states two different things.
|
|
647
|
+
*/
|
|
648
|
+
function byCall(stories) {
|
|
649
|
+
const groups = new Map();
|
|
650
|
+
for (const story of stories) {
|
|
651
|
+
const key = JSON.stringify([story.agentIndex, `${story.evidence}`, story.source, story.outcome]);
|
|
652
|
+
groups.set(key, [...(groups.get(key) ?? []), story]);
|
|
653
|
+
}
|
|
654
|
+
return groups;
|
|
655
|
+
}
|
|
656
|
+
/** An agent by index, named as the graph names it. */
|
|
657
|
+
function titleOf(graph, index) {
|
|
658
|
+
const node = [graph.main, ...graph.agents].find((agent) => agent.index === index);
|
|
659
|
+
return node === undefined ? 'an agent this report does not hold' : agentTitle(node);
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* The files a value is said to have come from. `or` where a call printed several protected files at once and the
|
|
663
|
+
* transcript does not say which of them the line came from - naming them all would name a file it never held.
|
|
664
|
+
*/
|
|
665
|
+
function filesOf(paths, entry) {
|
|
666
|
+
return entry.fileUncertain === true && paths.length > 1 ? paths.join(' or ') : paths.join(', ');
|
|
667
|
+
}
|
|
668
|
+
/** What an agent's answer carried, in plain words (R12): to whom when the summary says it, the strength first. */
|
|
669
|
+
function cameBack(entry, parent) {
|
|
670
|
+
const answer = `its answer${parent === undefined ? '' : ` to ${parent}`}`;
|
|
671
|
+
switch (entry.strength) {
|
|
672
|
+
case 'value':
|
|
673
|
+
return `${answer} carried a value from ${filesOf(entry.paths, entry)}`;
|
|
674
|
+
case 'path':
|
|
675
|
+
// A path in an answer was read out of the answer itself, so it names its file exactly.
|
|
676
|
+
return `${answer} named ${entry.paths.join(', ')}`;
|
|
677
|
+
case 'report':
|
|
678
|
+
return (`${answer} carried no traced value (${entry.reportLength === undefined ? 'length unknown' : `${entry.reportLength} characters`}; ` +
|
|
679
|
+
`it reached ${entry.filesReached} protected ${plural(entry.filesReached, 'file')})`);
|
|
680
|
+
case 'unknown':
|
|
681
|
+
return entry.awaited === true ? `it ran in the background, and ${answer} never arrived` : `${answer} could not be read`;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/** R4b: a value the agent wrote in its own messages, said beside its answer unless the answer carried one anyway. */
|
|
685
|
+
function written(entry) {
|
|
686
|
+
return entry.strength === 'value' || entry.writtenFrom.length === 0
|
|
687
|
+
? ''
|
|
688
|
+
: `; it also wrote a value from ${filesOf(entry.writtenFrom, entry)} in its own messages`;
|
|
689
|
+
}
|
|
690
|
+
/** What an answer carried, as `--full` says it: what the statement rests on, in plain words. */
|
|
691
|
+
function returnInFull(entry) {
|
|
692
|
+
switch (entry.strength) {
|
|
693
|
+
case 'value':
|
|
694
|
+
return `its answer carried a value from a protected file · ${filesOf(entry.paths, entry)}`;
|
|
695
|
+
case 'path':
|
|
696
|
+
return `its answer named a protected file it had reached · ${entry.paths.join(', ')}`;
|
|
697
|
+
case 'report':
|
|
698
|
+
case 'unknown':
|
|
699
|
+
return cameBack(entry);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
/** The order `--full` lists uses in: what outlives the session first, what stayed in messages last. */
|
|
703
|
+
const LANDINGS = ['file', 'command', 'delegation', 'said', 'reasoning', 'call'];
|
|
704
|
+
const LANDING_WORDS = {
|
|
705
|
+
file: 'In a file it wrote',
|
|
706
|
+
command: 'In a command',
|
|
707
|
+
delegation: 'In instructions to another agent',
|
|
708
|
+
said: 'In its own messages',
|
|
709
|
+
reasoning: 'In its reasoning',
|
|
710
|
+
call: 'In another call',
|
|
711
|
+
};
|
|
712
|
+
/** What came before a use, in the agent's own record (R8), in plain words. Nothing earlier is never said to be the user. */
|
|
713
|
+
const SOURCE_WORDS = {
|
|
714
|
+
read: 'after it read that file itself',
|
|
715
|
+
returned: "after another agent's answer carried it",
|
|
716
|
+
unprotected: 'after a file that is not protected showed it',
|
|
717
|
+
delivered: 'after output it was given showed it, from no call the record names',
|
|
718
|
+
none: 'nothing earlier in its record had it',
|
|
719
|
+
};
|
|
720
|
+
/** Where the same value was afterwards, as the summary says it (R12): files, commands, instructions, messages. */
|
|
721
|
+
function appearedAfter(uses, graph) {
|
|
722
|
+
if (uses.length === 0)
|
|
723
|
+
return undefined;
|
|
724
|
+
const onward = uses.filter((use) => use.landed === 'file' && use.intoProtected !== true);
|
|
725
|
+
const files = [...new Set(onward.flatMap((use) => (use.targets ?? []).map(String)))];
|
|
726
|
+
const commands = uses.filter((use) => use.landed === 'command');
|
|
727
|
+
const programs = [...new Set(commands.flatMap((use) => (use.programs ?? []).map(String)))];
|
|
728
|
+
const handed = uses.filter((use) => use.landed === 'delegation').length;
|
|
729
|
+
const spoken = uses.filter((use) => use.landed === 'said' || use.landed === 'reasoning');
|
|
730
|
+
const speakers = [...new Set(spoken.map((use) => use.agentIndex))].map((index) => titleOf(graph, index));
|
|
731
|
+
const rest = uses.length - onward.length - commands.length - handed - spoken.length;
|
|
732
|
+
const parts = [
|
|
733
|
+
...(files.length === 0
|
|
734
|
+
? []
|
|
735
|
+
: [`${files.length} ${plural(files.length, 'file')} that ${files.length === 1 ? 'is' : 'are'} not protected (${files.join(', ')})`]),
|
|
736
|
+
...(commands.length === 0
|
|
737
|
+
? []
|
|
738
|
+
: [`${commands.length} ${plural(commands.length, 'command')}${programs.length === 0 ? '' : ` (${programs.join(', ')})`}`]),
|
|
739
|
+
...(handed === 0 ? [] : [`instructions to ${handed} ${plural(handed, 'agent')}`]),
|
|
740
|
+
...(speakers.length === 0
|
|
741
|
+
? []
|
|
742
|
+
: [`messages of ${speakers.length === 1 ? speakers[0] : `${speakers.slice(0, -1).join(', ')} and ${speakers[speakers.length - 1]}`}`]),
|
|
743
|
+
...(rest === 0 ? [] : [`${rest} more (add --full)`]),
|
|
744
|
+
];
|
|
745
|
+
return parts.join(' · ');
|
|
746
|
+
}
|
|
747
|
+
/** One use, as `--full` says it: whose, from which file, where, and what came before it. */
|
|
748
|
+
function useInFull(use, graph) {
|
|
749
|
+
const from = use.fileUncertain === true && use.files.length > 1 ? use.files.join(' or ') : use.files.join(', ');
|
|
750
|
+
const programs = (use.programs ?? []).length === 0 ? '' : ` (${(use.programs ?? []).join(', ')})`;
|
|
751
|
+
const where = use.landed === 'file'
|
|
752
|
+
? ` · into ${(use.targets ?? []).join(', ')}${use.intoProtected === true ? ' (a protected file)' : ''}`
|
|
753
|
+
: use.landed === 'command'
|
|
754
|
+
? ` · in ${use.commits === true ? 'a command that commits' : 'a command'}${programs}`
|
|
755
|
+
: '';
|
|
756
|
+
const named = use.sourceTargets === undefined ? '' : ` (${use.sourceTargets.join(', ')})`;
|
|
757
|
+
return `${titleOf(graph, use.agentIndex)} · value from ${from}${where} · ${SOURCE_WORDS[use.source]}${named}`;
|
|
758
|
+
}
|
|
759
|
+
/** `Agents 1, 3 and 4`, by the numbers the graph draws them with. */
|
|
760
|
+
function agentsNamed(agents) {
|
|
761
|
+
const ordinals = agents.map((agent) => agent.ordinal ?? 0).sort((first, second) => first - second);
|
|
762
|
+
const last = ordinals.pop();
|
|
763
|
+
return ordinals.length === 0 ? `Agent ${last}` : `Agents ${ordinals.join(', ')} and ${last}`;
|
|
764
|
+
}
|
|
765
|
+
/** A section's heading, marked so it stands apart from the lines under it. `--ascii` drops the mark. */
|
|
766
|
+
function heading(text) {
|
|
767
|
+
return `▍${text}`;
|
|
768
|
+
}
|
|
769
|
+
/** Words cut to a width, marked where they were cut. */
|
|
770
|
+
function fitWords(words, width, ascii) {
|
|
771
|
+
const mark = ascii ? '...' : '…';
|
|
772
|
+
return words.length <= width ? words : `${words.slice(0, Math.max(width - mark.length, 0))}${mark}`;
|
|
773
|
+
}
|
|
774
|
+
/** An agent's row of the tree, column by column: actions, files reached, refused attempts, what came back. */
|
|
775
|
+
function nodeCells(agent, digits) {
|
|
776
|
+
return [
|
|
777
|
+
`${String(agent.actions).padStart(digits)} ${plural(agent.actions, 'action')}`,
|
|
778
|
+
agent.filesReached === 0 ? '' : `${agent.filesReached} ${plural(agent.filesReached, 'file')} reached`,
|
|
779
|
+
agent.refusedAttempts === 0 ? '' : `${agent.refusedAttempts} refused`,
|
|
780
|
+
[
|
|
781
|
+
...(agent.returned === undefined ? [] : [`gave the ${agent.returned} back`]),
|
|
782
|
+
...(agent.wroteValue === true ? ['wrote the value'] : []),
|
|
783
|
+
...(agent.wroteOnward === true ? ['wrote the value into files'] : []),
|
|
784
|
+
].join(' · '),
|
|
785
|
+
];
|
|
786
|
+
}
|
|
787
|
+
/** An agent with nothing to say for itself and no agent below it, which does not need a line of its own. */
|
|
788
|
+
function isQuiet(agent, parent, tree) {
|
|
789
|
+
const unresolved = agent.parentIndex === undefined && parent === tree.graph.main.index;
|
|
790
|
+
return (agent.filesReached === 0 &&
|
|
791
|
+
agent.refusedAttempts === 0 &&
|
|
792
|
+
agent.returned === undefined &&
|
|
793
|
+
agent.wroteValue !== true &&
|
|
794
|
+
(tree.children.get(agent.index) ?? []).length === 0 &&
|
|
795
|
+
!(unresolved && tree.markUnresolved));
|
|
796
|
+
}
|
|
797
|
+
/** Both routes to one file, said in one line rather than in two that read as a duplicate. */
|
|
798
|
+
const BOTH_WAYS = 'named and in a result';
|
|
799
|
+
function howOf(story) {
|
|
800
|
+
return story.source === 'result' ? 'in a result' : 'named';
|
|
801
|
+
}
|
|
802
|
+
/** H10 of `search-hits-are-reads`: a search's hit lines are the file's text, so the count is said beside how. */
|
|
803
|
+
function printed(lines) {
|
|
804
|
+
return lines === 0 ? '' : ` · ${lines} ${plural(lines, 'line')} printed`;
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* R6: one line per file. The model keeps a story per route - a file named by a call, and the same file seen again in
|
|
808
|
+
* a result, each with its own evidence - which is what `--full` and the page show. In a list to run an eye down the
|
|
809
|
+
* two read as a duplicate, so here they are one line that says both routes. **Outcomes are never merged:** reached
|
|
810
|
+
* and refused for the same file are the two facts a reader came for.
|
|
811
|
+
*/
|
|
812
|
+
function reachedRows(stories) {
|
|
813
|
+
const rows = new Map();
|
|
814
|
+
const lines = new Map();
|
|
815
|
+
for (const story of stories) {
|
|
816
|
+
const key = `${story.outcome} ${story.path}`;
|
|
817
|
+
const seen = rows.get(key);
|
|
818
|
+
const how = howOf(story);
|
|
819
|
+
rows.set(key, seen === undefined
|
|
820
|
+
? { path: `${story.path}`, how, outcome: story.outcome, occurrences: story.occurrences }
|
|
821
|
+
: { ...seen, how: seen.how === how ? seen.how : BOTH_WAYS, occurrences: seen.occurrences + story.occurrences });
|
|
822
|
+
lines.set(key, (lines.get(key) ?? 0) + (story.lines ?? 0));
|
|
823
|
+
}
|
|
824
|
+
return [...rows].map(([key, row]) => ({ ...row, how: row.how + printed(lines.get(key) ?? 0) }));
|
|
825
|
+
}
|
|
826
|
+
/** What R6 says of a file after its path, in columns: how it was reached, what came of it, and how often. */
|
|
827
|
+
function said(row, columns) {
|
|
828
|
+
const outcome = OUTCOME_WORD[row.outcome];
|
|
829
|
+
if (row.occurrences <= 1)
|
|
830
|
+
return [row.how.padEnd(columns.how), outcome].join(GAP);
|
|
831
|
+
return [row.how.padEnd(columns.how), outcome.padEnd(columns.outcome), `${row.occurrences}×`].join(GAP);
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Every path on the page fitted to a column (R11), or `undefined` for one that has to take a line of its own. A
|
|
835
|
+
* path keeps its end and loses leading directories at a boundary. It is not cut when what would be left is a name
|
|
836
|
+
* another file on the page shares, when two paths would be cut to the same text, or when not even its name fits:
|
|
837
|
+
* two files that read alike are what a list of files must never show (spec §11).
|
|
838
|
+
*/
|
|
839
|
+
function fitPaths(paths, width, ascii) {
|
|
840
|
+
const distinct = [...new Set(paths)];
|
|
841
|
+
const names = counted(distinct.map(nameOf));
|
|
842
|
+
const cuts = new Map(distinct.map((path) => [path, shorten(path, width, ascii)]));
|
|
843
|
+
const readings = counted([...cuts.values()].flatMap((cut) => (cut === undefined ? [] : [cut.text])));
|
|
844
|
+
return new Map(distinct.map((path) => {
|
|
845
|
+
const cut = cuts.get(path);
|
|
846
|
+
const alike = cut === undefined || (readings.get(cut.text) ?? 0) > 1 || (cut.nameOnly && (names.get(nameOf(path)) ?? 0) > 1);
|
|
847
|
+
return [path, alike ? undefined : cut.text];
|
|
848
|
+
}));
|
|
849
|
+
}
|
|
850
|
+
/** A path cut from the front at a directory boundary, marked `…`. `undefined` when not even its name fits. */
|
|
851
|
+
function shorten(path, width, ascii) {
|
|
852
|
+
if (path.length <= width)
|
|
853
|
+
return { text: path, nameOnly: false };
|
|
854
|
+
const mark = ascii ? '...' : '…';
|
|
855
|
+
const segments = path.split('/');
|
|
856
|
+
const name = segments.pop() ?? path;
|
|
857
|
+
let tail = name;
|
|
858
|
+
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
859
|
+
const longer = `${segments[index]}/${tail}`;
|
|
860
|
+
if (`${mark}/${longer}`.length > width)
|
|
861
|
+
break;
|
|
862
|
+
tail = longer;
|
|
863
|
+
}
|
|
864
|
+
const text = `${mark}/${tail}`;
|
|
865
|
+
return text.length > width ? undefined : { text, nameOnly: tail === name };
|
|
866
|
+
}
|
|
867
|
+
function nameOf(path) {
|
|
868
|
+
return path.split('/').pop() ?? path;
|
|
869
|
+
}
|
|
870
|
+
function counted(values) {
|
|
871
|
+
const counts = new Map();
|
|
872
|
+
for (const value of values)
|
|
873
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
874
|
+
return counts;
|
|
875
|
+
}
|
|
876
|
+
function plural(count, word) {
|
|
877
|
+
return count === 1 ? word : `${word}s`;
|
|
878
|
+
}
|
|
879
|
+
/** Its size, never its text (§7.3). Absence is stated, so it cannot be read as an empty instruction. */
|
|
880
|
+
function describeInstruction(size) {
|
|
881
|
+
return size === undefined ? 'the record carrying it could not be read' : `${size} characters, not quoted here`;
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* The tool's own characters, in the plainest form a terminal without UTF-8 can show (§7.5 constraint 2). Only
|
|
885
|
+
* these: what a transcript said is quoted as it was, because transliterating a task description changes it.
|
|
886
|
+
*/
|
|
887
|
+
const ASCII = {
|
|
888
|
+
'✓': '*',
|
|
889
|
+
'·': '-',
|
|
890
|
+
'—': '-',
|
|
891
|
+
'×': 'x',
|
|
892
|
+
'…': '...',
|
|
893
|
+
'→': '>',
|
|
894
|
+
'─': '-',
|
|
895
|
+
'━': '-',
|
|
896
|
+
'│': '|',
|
|
897
|
+
'├': '+',
|
|
898
|
+
'└': '`',
|
|
899
|
+
'●': '*',
|
|
900
|
+
'○': 'o',
|
|
901
|
+
// One column, like the mark it stands for: two would push a row past the width every other line was measured to.
|
|
902
|
+
'↩': '<',
|
|
903
|
+
// A heading's mark is decoration: without it the heading still stands on a line of its own.
|
|
904
|
+
'▍': '',
|
|
905
|
+
};
|
|
906
|
+
/**
|
|
907
|
+
* **Corrected 2026-09-14 after a review.** `--ascii` degraded the tree and left the check mark, the middle dot, the
|
|
908
|
+
* dash and the multiplication sign in the output - and its test passed, because it looked for box-drawing
|
|
909
|
+
* characters only. That was the wrong edge of the requirement: a terminal without UTF-8 garbles every one of them.
|
|
910
|
+
*/
|
|
911
|
+
function toAscii(text) {
|
|
912
|
+
return text.replace(/[✓·—×…→─━│├└●○↩▍]/g, (character) => ASCII[character] ?? character);
|
|
913
|
+
}
|
|
914
|
+
/** The headings both views print, without their mark. Colour picks them out wherever they stand. */
|
|
915
|
+
const HEADINGS = new Set([
|
|
916
|
+
'Who ran, and what they reached',
|
|
917
|
+
'What could not be established',
|
|
918
|
+
'What was reached',
|
|
919
|
+
'The session in numbers',
|
|
920
|
+
'Recognised key formats in results',
|
|
921
|
+
'Agents with no recorded delegation',
|
|
922
|
+
'Missing data',
|
|
923
|
+
'Everything the session did',
|
|
924
|
+
'What the agents answered',
|
|
925
|
+
'Where the same value was afterwards',
|
|
926
|
+
]);
|
|
927
|
+
/** Reaching a protected file is what a reader came to find; a refusal is the policy holding. */
|
|
928
|
+
const OUTCOME_HUE = { reached: 'red', refused: 'green', 'outcome unknown': 'yellow' };
|
|
929
|
+
/** The same, for the mark a row starts with. */
|
|
930
|
+
const MARK_HUE = { '●': 'red', '○': 'green', '?': 'yellow' };
|
|
931
|
+
/** The end of a summary row, exactly as `said` writes it. */
|
|
932
|
+
const ROW_END = /((?:named and in a result|named|in a result)(?: · \d+ lines? printed)?)( +)(reached|refused|outcome unknown)((?: +\d+×)?)$/;
|
|
933
|
+
/**
|
|
934
|
+
* Colour laid over lines that are already laid out (R14), so it cannot move anything: every width was measured on
|
|
935
|
+
* the plain text, and removing the escapes gives that text back byte for byte. Only the tool's own words are
|
|
936
|
+
* coloured, in the positions it writes them - a task description that happens to say "refused" stays plain - and
|
|
937
|
+
* each coloured word or mark stands beside a word first, so a report read without colour says the same.
|
|
938
|
+
*/
|
|
939
|
+
function colourise(lines) {
|
|
940
|
+
let section = '';
|
|
941
|
+
return lines.map((line, index) => {
|
|
942
|
+
const title = line.startsWith('▍') ? line.slice(1) : undefined;
|
|
943
|
+
if (title !== undefined && HEADINGS.has(title)) {
|
|
944
|
+
section = title;
|
|
945
|
+
return `${paint('▍', 'indigo')}${paint(title, 'bold')}`;
|
|
946
|
+
}
|
|
947
|
+
if (index === 1 && /^ [!✓] {2}/.test(line)) {
|
|
948
|
+
return `${line.charAt(0)}${paint(line.charAt(1), line.charAt(1) === '!' ? 'red' : 'green')}${line.slice(2)}`;
|
|
949
|
+
}
|
|
950
|
+
if (line.startsWith('Every detail: '))
|
|
951
|
+
return paint(line, 'dim');
|
|
952
|
+
switch (section) {
|
|
953
|
+
case 'Who ran, and what they reached':
|
|
954
|
+
return line
|
|
955
|
+
.replace(/\d+ files? reached/, (reached) => paint(reached, 'red'))
|
|
956
|
+
.replace(/\d+ refused(?= |$)/, (refused) => paint(refused, 'green'))
|
|
957
|
+
.replace(/gave the value back/, (returned) => paint(returned, 'red'))
|
|
958
|
+
.replace(/gave the path back/, (returned) => paint(returned, 'yellow'))
|
|
959
|
+
.replace(/wrote the value into files/, (written) => paint(written, 'red'))
|
|
960
|
+
.replace(/\d+ agents? reached nothing: .+$/, (quiet) => paint(quiet, 'dim'))
|
|
961
|
+
.replace(/nothing recorded what asked for .+$/, (unrecorded) => paint(unrecorded, 'yellow'));
|
|
962
|
+
case 'What could not be established':
|
|
963
|
+
case 'Missing data':
|
|
964
|
+
return line.replace(/^( +)(.+)$/, (_, margin, text) => `${margin}${paint(text, text.startsWith('nothing: ') ? 'green' : 'yellow')}`);
|
|
965
|
+
case 'What was reached':
|
|
966
|
+
if (/^ {4}↩ /.test(line)) {
|
|
967
|
+
return line.replace(/carried a value|named|could not be read|never arrived/, (what) => paint(what, what === 'carried a value' ? 'red' : 'yellow'));
|
|
968
|
+
}
|
|
969
|
+
if (/^ {4}→ then the same value was in: /.test(line)) {
|
|
970
|
+
return line.replace(/\d+ files? that (?:is|are) not protected/, (what) => paint(what, 'red'));
|
|
971
|
+
}
|
|
972
|
+
if (!/^ {4}/.test(line))
|
|
973
|
+
return line;
|
|
974
|
+
return line
|
|
975
|
+
.replace(ROW_END, (_, how, space, outcome, times) => `${how.includes('in a result') ? paint(how, 'bold') : how}${space}${paint(outcome, OUTCOME_HUE[outcome] ?? 'bold')}${times}`)
|
|
976
|
+
.replace(/^( {4})([●○?]) /, (_, margin, mark) => `${margin}${paint(mark, MARK_HUE[mark] ?? 'bold')} `);
|
|
977
|
+
default:
|
|
978
|
+
return line;
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
}
|