@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,974 @@
|
|
|
1
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
2
|
+
import { sessionKey } from '../../core/session-catalogue.js';
|
|
3
|
+
import { splitBySince } from '../../core/session-filter.js';
|
|
4
|
+
import { recogniseAll } from '../../core/session-titles.js';
|
|
5
|
+
import { FileAccessError } from '../../ports/file-access-error.js';
|
|
6
|
+
import { contentVersion } from '../../shared/content-version.js';
|
|
7
|
+
import { fileStamp } from '../../shared/file-stamp.js';
|
|
8
|
+
import { printable } from '../../shared/printable.js';
|
|
9
|
+
import { digestOf } from '../check/actions-digest.js';
|
|
10
|
+
import { homeRelative } from '../render/home-relative.js';
|
|
11
|
+
import { projectName } from './app-nav.js';
|
|
12
|
+
import { choosePolicy, policyRefusal } from '../choose-policy.js';
|
|
13
|
+
import { checkOf, checkWithMarks } from '../check/check-lines.js';
|
|
14
|
+
import { recordMark, recordUnmark } from '../check/mark-request.js';
|
|
15
|
+
import { indexHandler } from './serve/index-handler.js';
|
|
16
|
+
import { movedPage } from './projects/moved-page.js';
|
|
17
|
+
import { nearestProject } from '../../core/nearest-project.js';
|
|
18
|
+
import { translator } from '../render/report-copy.js';
|
|
19
|
+
import { hookComplete, readSettingsFile, setUpBy } from './settings-files.js';
|
|
20
|
+
import { indexProjects } from './projects/index-projects.js';
|
|
21
|
+
import { NOT_A_PROJECT } from '../../setup/project-setup.js';
|
|
22
|
+
import { notAProject } from '../../setup/not-a-project.js';
|
|
23
|
+
import { inTemporarySpace } from './projects/temporary-space.js';
|
|
24
|
+
import { behind } from '../../setup/behind.js';
|
|
25
|
+
import { modeChange, onlyTakesOut, settingsChangeToSetup } from './serve/settings-setup.js';
|
|
26
|
+
import { DEFAULT_THRESHOLD } from '../watch/agent-alert.js';
|
|
27
|
+
import { DEFAULT_CHANNELS, DEFAULT_CLEAN, DEFAULT_SAID_AS } from '../watch/notice-choices.js';
|
|
28
|
+
import { actionsAfterMarks, markLines, marksInRange, standingMarks } from '../check/marks.js';
|
|
29
|
+
import { finishOnboarding } from './onboarding/finish-onboarding.js';
|
|
30
|
+
import { WHO_STEP } from './onboarding/onboarding-script.js';
|
|
31
|
+
import { repositoryAbove } from './repository.js';
|
|
32
|
+
import { pageStopped, startSaid } from './render/start-words.js';
|
|
33
|
+
import { PROJECT_HOOKS } from '../../adapter/codex/contract/hooks.js';
|
|
34
|
+
import { codexRefuseIn } from '../../adapter/codex/settings/codex-hooks.js';
|
|
35
|
+
import { SETTINGS_FILES } from '../../adapter/claude-code/contract/settings.js';
|
|
36
|
+
import { rulesReadByHooks } from '../../adapter/claude-code/settings/hook-entries.js';
|
|
37
|
+
import { fileRulesIn, isDenied } from '../../adapter/claude-code/settings/deny-entries.js';
|
|
38
|
+
import { fileRulePathOf, toPattern } from '../../adapter/claude-code/policy/deny-rules.js';
|
|
39
|
+
const DAY = 86_400_000;
|
|
40
|
+
/**
|
|
41
|
+
* The patterns the page lists: the ones the policy read, and then any rule the project's own settings files hold
|
|
42
|
+
* that the policy has no pattern for. The second set is what a change made on the page adds, and a run reading
|
|
43
|
+
* the built-in list or a policy file cannot see it: without this, protecting a file wrote the rule and the list
|
|
44
|
+
* came back looking exactly as it had, which reads as nothing having happened.
|
|
45
|
+
*/
|
|
46
|
+
function alsoProtected(patterns, mine) {
|
|
47
|
+
if (mine === undefined)
|
|
48
|
+
return patterns;
|
|
49
|
+
const held = new Set(patterns);
|
|
50
|
+
return [...patterns, ...Object.keys(mine).filter((pattern) => !held.has(pattern))];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The width of a terminal report `start` never prints. Each session's report is run for its HTML, and the text
|
|
54
|
+
* it also produces is discarded, so this only has to be a valid width.
|
|
55
|
+
*/
|
|
56
|
+
const DISCARDED_WIDTH = 100;
|
|
57
|
+
/**
|
|
58
|
+
* live-pages L4, LD2: a conversation that is still growing - the one the agent is working in - is read again at most
|
|
59
|
+
* this often, however often its page asks. A 65 MB session takes about 0.85 s to read (`the-agent-tells-you` R28).
|
|
60
|
+
*/
|
|
61
|
+
const GROWING_MS = 5_000;
|
|
62
|
+
/** R54: a page nobody has used for this long stops being served. */
|
|
63
|
+
const IDLE_MS = 30 * 60 * 1000;
|
|
64
|
+
/** R52: a mark is a path and a note; nothing a page sends needs more. */
|
|
65
|
+
const MAX_BODY = 16 * 1024;
|
|
66
|
+
/**
|
|
67
|
+
* `agentwhy start`: every session of this project on one page, and a report for each one inside the range.
|
|
68
|
+
*
|
|
69
|
+
* One run reads what the range selects - and the end of every listed transcript, for the title its row is found by -
|
|
70
|
+
* and writes a directory of self-contained files. A page it opens is served from 127.0.0.1 behind a token until it
|
|
71
|
+
* has been idle for a while, so a mark made on it is recorded at once (`worth-running-every-day` R50-R54, amending
|
|
72
|
+
* spec §2); `--no-open`, `--no-serve` and `--share` write and return, and the page is then a file.
|
|
73
|
+
*/
|
|
74
|
+
export class SessionStart {
|
|
75
|
+
#dependencies;
|
|
76
|
+
/** The version of the agentwhy running, read once a run (`nothing-updates-by-itself.md` U2). */
|
|
77
|
+
#serving;
|
|
78
|
+
constructor(dependencies) {
|
|
79
|
+
this.#dependencies = dependencies;
|
|
80
|
+
}
|
|
81
|
+
async run(options) {
|
|
82
|
+
const { catalogue, workingDirectory, temporaryDirectory, now, directories, files, policyFiles } = this.#dependencies;
|
|
83
|
+
const listing = await catalogue.list(workingDirectory);
|
|
84
|
+
const notHere = this.#notAProject(workingDirectory);
|
|
85
|
+
// R50: whether the pages will be served is known before any is written, so a report can say where it may write.
|
|
86
|
+
// A server that then fails to start leaves a page that tries, is refused by nothing, and hands over the command.
|
|
87
|
+
const served = !options.share && (options.serve ?? options.open) && this.#dependencies.server !== undefined;
|
|
88
|
+
// W23: whether this run opens the onboarding - decided before an empty listing answers, because a project with no
|
|
89
|
+
// conversations yet is where setting up helps most, and its reader cannot run `init` (W1a).
|
|
90
|
+
const welcome = served ? await this.#welcome(workingDirectory) : undefined;
|
|
91
|
+
// which-project V16: a run the onboarding's project step switched to opens that project's onboarding, whatever W23
|
|
92
|
+
// says - the person is in the middle of a setup, and the step chose this project for it.
|
|
93
|
+
const arrivedFrom = this.#dependencies.arrivedFrom;
|
|
94
|
+
const opensWelcome = welcome !== undefined && (arrivedFrom === 'step' || (options.open && welcome.opens));
|
|
95
|
+
if ((!listing.found || listing.sessions.length === 0) && !opensWelcome) {
|
|
96
|
+
return {
|
|
97
|
+
outcome: 'no-sessions',
|
|
98
|
+
// Every place looked in, one per AI: "looked in the wrong place" is only answerable where each is named.
|
|
99
|
+
output: noSessions(listing.searched.map((place) => place.directory).join(' and ') || listing.directory, projectName(workingDirectory), served && options.open, welcome?.setUp === true, notHere),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Resolved against the working directory given, never against whatever the process happens to be standing in.
|
|
103
|
+
const out = resolve(workingDirectory, options.out ?? join(temporaryDirectory, `agentwhy-start-${fileStamp(now)}`));
|
|
104
|
+
// Chosen once and before anything is written: a refused file used to be found by the first report, after the
|
|
105
|
+
// directory existed and with reports in it that no index would ever link or explain. Found by a review.
|
|
106
|
+
const chosen = await choosePolicy(options, policyFiles, this.#dependencies.tell);
|
|
107
|
+
if ('errors' in chosen)
|
|
108
|
+
return { outcome: 'refused', output: policyRefusal(chosen.errors) };
|
|
109
|
+
const repository = await repositoryAbove(out, directories);
|
|
110
|
+
if (repository !== undefined) {
|
|
111
|
+
return {
|
|
112
|
+
outcome: 'refused',
|
|
113
|
+
output: `Refusing to write inside a repository: ${out}\n` +
|
|
114
|
+
`The files describe sessions and would be one commit away from being published. Choose a directory outside ${repository}.\n`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
await files.ensureDirectory(out);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (!(error instanceof FileAccessError))
|
|
122
|
+
throw error;
|
|
123
|
+
return { outcome: 'unwritable', output: `The directory could not be created: ${out}\n` };
|
|
124
|
+
}
|
|
125
|
+
// F55: a conversation a person once asked to check is read by every run after, as if it were in range - the button is
|
|
126
|
+
// pressed once, not every morning. A list that cannot be read leaves the run its range, as it always had.
|
|
127
|
+
const asked = new Set(this.#dependencies.checked === undefined ? [] : (await this.#dependencies.checked.read()).ids);
|
|
128
|
+
// M5: each report is drawn with the marks that still hold for its session. A shared page carries none (R37), and a
|
|
129
|
+
// record that cannot be read leaves every file to do rather than guessing which were done (invariant 4).
|
|
130
|
+
const standingAtStart = options.share ? new Map() : standingMarks((await this.#dependencies.marks.read()).records);
|
|
131
|
+
/*
|
|
132
|
+
* R75: a served page is read again and again while the person keeps working - new conversations start, open ones
|
|
133
|
+
* grow, and a file is marked done on another page. What the run knows is kept here and brought up to date each time
|
|
134
|
+
* a page is read (`refresh`): a report is written again only where its session changed, or a mark on one of its own
|
|
135
|
+
* files did, so reading an unchanged page costs a listing and nothing more.
|
|
136
|
+
*/
|
|
137
|
+
let entries = [];
|
|
138
|
+
// Kept whole, marks not yet applied: the page is rendered again from these after a mark is made from it (R52).
|
|
139
|
+
let read = [];
|
|
140
|
+
let unreadable = 0;
|
|
141
|
+
let nameOfId = new Map();
|
|
142
|
+
let outOfRange = [];
|
|
143
|
+
const reports = new Map();
|
|
144
|
+
// live-pages L1: the version of every served file, by name, as last written.
|
|
145
|
+
const versions = new Map();
|
|
146
|
+
const elapsed = this.#dependencies.elapsed;
|
|
147
|
+
const titles = new Map();
|
|
148
|
+
// which-project V4: where the listed conversations were held, counted from the same read as their titles.
|
|
149
|
+
let entryPoints = {};
|
|
150
|
+
// which-project V17: the projects as this run last listed them - the only ones a page may switch to, by their ids.
|
|
151
|
+
let listed;
|
|
152
|
+
// V14: set once a page has moved to another project's run, so this one ends without a word of its own.
|
|
153
|
+
let switchedAway = false;
|
|
154
|
+
/** Every row and report from one listing, reusing what has not changed. A refusal is said as the one it is. */
|
|
155
|
+
// R75, F57: the policy the reports are read under, as it is now. A file switched to Tell me or back to Block in
|
|
156
|
+
// Settings changes what every report says about it, so a changed policy writes every report again.
|
|
157
|
+
let policyNow = chosen.policy;
|
|
158
|
+
const sync = async (sessions, standing) => {
|
|
159
|
+
const policyKey = keyOf(policyNow);
|
|
160
|
+
const { inRange, outOfRange: older } = splitBySince(sessions, options.since.since);
|
|
161
|
+
const generatedIds = new Set([...inRange, ...older.filter((session) => asked.has(sessionKey(session)))].map((session) => sessionKey(session)));
|
|
162
|
+
// A title is user content, and a shared page is one that leaves this machine: it is not even read. A session that
|
|
163
|
+
// has grown may have a new last prompt, so its title is read again with it. A Codex conversation is named in a file
|
|
164
|
+
// of Codex's own, seconds after it starts (`what-codex-wrote` §2.9, X28a) - found by the maintainer, a row drawn in
|
|
165
|
+
// those seconds stayed untitled once its rollout stopped changing - so a Codex row with no title asks again; the
|
|
166
|
+
// names are a lookup in a list read again only where it changed.
|
|
167
|
+
if (!options.share) {
|
|
168
|
+
const stale = sessions.filter((session) => {
|
|
169
|
+
const known = titles.get(sessionKey(session));
|
|
170
|
+
return known?.modifiedAt !== session.modifiedAt || (session.provider === 'codex' && known.title === undefined);
|
|
171
|
+
});
|
|
172
|
+
const read_ = await recogniseAll(this.#dependencies.titles, stale);
|
|
173
|
+
stale.forEach((session, at) => titles.set(sessionKey(session), { modifiedAt: session.modifiedAt, ...read_[at] }));
|
|
174
|
+
}
|
|
175
|
+
const nextEntries = [];
|
|
176
|
+
const nextRead = [];
|
|
177
|
+
const nextNames = new Map();
|
|
178
|
+
let nextUnreadable = 0;
|
|
179
|
+
for (const [position, session] of sessions.entries()) {
|
|
180
|
+
// A session is named by its key: its id for Claude Code, as always, and `codex-…` for Codex, so two AIs' equal ids
|
|
181
|
+
// never share a row, a file or a cached report (`.ai/plans/2026-09-29-what-codex-wrote.md` step 5).
|
|
182
|
+
const name = options.share ? `Session ${position + 1}` : sessionKey(session);
|
|
183
|
+
nextNames.set(sessionKey(session), name);
|
|
184
|
+
let report = { kind: 'outside-range' };
|
|
185
|
+
if (generatedIds.has(sessionKey(session))) {
|
|
186
|
+
const file = options.share ? `session-${position + 1}.html` : `${sessionKey(session)}.html`;
|
|
187
|
+
const marks = marksFor(session.modifiedAt, standing);
|
|
188
|
+
const kept = reports.get(sessionKey(session));
|
|
189
|
+
const same = kept !== undefined && kept.file === file && kept.policy === policyKey && kept.marks === marksOn(kept.report, marks);
|
|
190
|
+
// L4: only growth waits; a mark or a policy changed is the person's own doing, and shown at once.
|
|
191
|
+
const growingTooSoon = same && elapsed !== undefined && kept.modifiedAt !== session.modifiedAt && elapsed() - kept.writtenAt < GROWING_MS;
|
|
192
|
+
if (same && (kept.modifiedAt === session.modifiedAt || growingTooSoon)) {
|
|
193
|
+
report = kept.report;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const made = await this.#generate(session, out, file, options, policyNow, { marks, served, title: titles.get(sessionKey(session))?.title });
|
|
197
|
+
// Every report is handed the policy chosen above, so none reads a file of its own. Should one refuse all the
|
|
198
|
+
// same, it is said as the one refusal it would be, never as a page of sessions that could not be read.
|
|
199
|
+
if (made.kind === 'policy-refused')
|
|
200
|
+
return made.output;
|
|
201
|
+
const drawn = { file, modifiedAt: session.modifiedAt, policy: policyKey, marks: marksOn(made, marks) };
|
|
202
|
+
reports.set(sessionKey(session), { ...drawn, report: made, writtenAt: elapsed?.() ?? 0 });
|
|
203
|
+
// L1: a report's version is what it was drawn from - the same inputs draw the same page.
|
|
204
|
+
if (made.kind === 'generated')
|
|
205
|
+
versions.set(file, contentVersion(JSON.stringify(drawn)));
|
|
206
|
+
report = made;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// The digest counts what was analysed, whether or not its page could be written: a report whose file failed still
|
|
210
|
+
// read the session, and a value to rotate in it must not vanish from the terminal because a disk was full.
|
|
211
|
+
if (report.kind !== 'outside-range') {
|
|
212
|
+
if (report.actions === undefined)
|
|
213
|
+
nextUnreadable += 1;
|
|
214
|
+
else
|
|
215
|
+
nextRead.push({ id: sessionKey(session), name, modifiedAt: session.modifiedAt, actions: report.actions });
|
|
216
|
+
}
|
|
217
|
+
nextEntries.push(entryOf(name, titles.get(sessionKey(session))?.title, session, report));
|
|
218
|
+
}
|
|
219
|
+
entries = nextEntries;
|
|
220
|
+
read = nextRead;
|
|
221
|
+
unreadable = nextUnreadable;
|
|
222
|
+
nameOfId = nextNames;
|
|
223
|
+
entryPoints = countedBy(sessions.map((session) => titles.get(sessionKey(session))?.entryPoint));
|
|
224
|
+
outOfRange = older.filter((session) => !asked.has(sessionKey(session)));
|
|
225
|
+
return undefined;
|
|
226
|
+
};
|
|
227
|
+
const refused = await sync(listing.sessions, standingAtStart);
|
|
228
|
+
if (refused !== undefined)
|
|
229
|
+
return { outcome: 'refused', output: refused };
|
|
230
|
+
// F55: a conversation this run left out, written when the page asks for it - by the same report, under the same
|
|
231
|
+
// policy, with the marks as they stand now - and from then on counted as if the run had covered it.
|
|
232
|
+
const include = async (name) => {
|
|
233
|
+
const session = outOfRange.find((each) => sessionKey(each) === name);
|
|
234
|
+
if (session === undefined)
|
|
235
|
+
return { written: false, said: `${name} is not a conversation this run left out.` };
|
|
236
|
+
asked.add(sessionKey(session));
|
|
237
|
+
const refusal = await refresh({ afterWrite: true });
|
|
238
|
+
if (refusal !== undefined)
|
|
239
|
+
return { written: false, said: refusal.trim() };
|
|
240
|
+
const report = entries.find((entry) => entry.name === name)?.report;
|
|
241
|
+
if (report?.kind !== 'generated')
|
|
242
|
+
return { written: false, said: `${name} could not be read, so no report was written.` };
|
|
243
|
+
// Remembered, so the next run reads it too. A list that cannot be written still leaves this page's report.
|
|
244
|
+
const kept = await this.#dependencies.checked?.add(sessionKey(session), (this.#dependencies.clock ?? (() => this.#dependencies.now))());
|
|
245
|
+
return {
|
|
246
|
+
written: true,
|
|
247
|
+
said: kept === false ? `The report of ${name} was written; it could not be remembered, so the next run will ask again.` : `The report of ${name} was written.`,
|
|
248
|
+
file: report.file,
|
|
249
|
+
};
|
|
250
|
+
};
|
|
251
|
+
/** The index and the digest's marks, from the record as it is now: once for the run, and after every served write. */
|
|
252
|
+
const build = async () => {
|
|
253
|
+
// Read before any line is drawn: a file marked done leaves the lists until a session after the mark reaches it (R35).
|
|
254
|
+
const reading = await this.#dependencies.marks.read();
|
|
255
|
+
const standing = standingMarks(reading.records);
|
|
256
|
+
const after = read.map((session) => ({ ...session, actions: actionsAfterMarks(session.actions, session.modifiedAt, standing) }));
|
|
257
|
+
// R60: after a write the view is re-read from the file, so the rules are read again here rather than kept
|
|
258
|
+
// from the run. Every report was handed the policy chosen above and none of them is drawn again; this is
|
|
259
|
+
// the Settings view alone, which is a control surface and has to show what the file now holds. A policy
|
|
260
|
+
// that has since become unreadable leaves the run's own, rather than an empty list nobody wrote.
|
|
261
|
+
const now_ = options.share ? chosen : await choosePolicy(options, policyFiles, this.#dependencies.tell).catch(() => chosen);
|
|
262
|
+
const policy = ('errors' in now_ ? chosen : now_).policy;
|
|
263
|
+
const project = options.share ? {} : await this.#settingsNow(workingDirectory, listing.sessions.some((session) => session.provider === 'codex'));
|
|
264
|
+
// What this person chose to be told, for the panel that changes it. A shared page offers no choices at all.
|
|
265
|
+
const notices = options.share ? undefined : await this.#noticesNow();
|
|
266
|
+
// F57: read again with every refresh, as the settings files are, so a switch shows what the list now holds.
|
|
267
|
+
const told = options.share || this.#dependencies.tellLists === undefined ? undefined : await this.#dependencies.tellLists.read();
|
|
268
|
+
const known = await this.#projectsNow(served && !options.share, workingDirectory);
|
|
269
|
+
listed = known.listing;
|
|
270
|
+
const index = {
|
|
271
|
+
now,
|
|
272
|
+
...(this.#dependencies.timeZone === undefined ? {} : { timeZone: this.#dependencies.timeZone }),
|
|
273
|
+
since: options.since.since,
|
|
274
|
+
asked: options.since.asked,
|
|
275
|
+
...(options.share ? {} : { project: workingDirectory }),
|
|
276
|
+
...(options.share || this.#dependencies.home === undefined ? {} : { place: homeRelative(workingDirectory, this.#dependencies.home) }),
|
|
277
|
+
...(options.share ? {} : { entryPoints }),
|
|
278
|
+
...(known.projects === undefined ? {} : { projects: known.projects }),
|
|
279
|
+
shared: options.share,
|
|
280
|
+
widen: widenTo(outOfRange, now),
|
|
281
|
+
entries,
|
|
282
|
+
// The rules this run read, for the Settings view. The page says which policy it was rather than leaving
|
|
283
|
+
// the question to the reports' own feet.
|
|
284
|
+
settings: {
|
|
285
|
+
level: policy.level,
|
|
286
|
+
// A rule written into the project's own files after the run started is protected now and the hooks read
|
|
287
|
+
// it, so it is in the list even where the policy this run read cannot see it - a pattern added here that
|
|
288
|
+
// never appeared would read as an add that did nothing.
|
|
289
|
+
protected: alsoProtected(policy.protected.map((entry) => entry.pattern), project.mine),
|
|
290
|
+
allowed: policy.allowed,
|
|
291
|
+
origin: policy.origin,
|
|
292
|
+
...(options.share ? {} : { out }),
|
|
293
|
+
// R60: a switch shows what the file holds. A shared page names no machine and offers no switch, so it
|
|
294
|
+
// carries no hook state either.
|
|
295
|
+
...project,
|
|
296
|
+
...(told === undefined ? {} : { told }),
|
|
297
|
+
...(notices === undefined ? {} : { notices }),
|
|
298
|
+
},
|
|
299
|
+
// which-project V6, V7: a run in the home directory or a root is in no project, and its pages say so.
|
|
300
|
+
...(notHere === undefined ? {} : { notAProject: notHere }),
|
|
301
|
+
// W24, W25: the onboarding is served with this run - whether its intro plays, and that Settings may lead to it.
|
|
302
|
+
...(welcome === undefined ? {} : { onboarding: { intro: welcome.intro, ...(welcome.atProject ? { atProject: true } : {}) } }),
|
|
303
|
+
...(read.length === 0 && reading.records.length === 0 && !reading.failed
|
|
304
|
+
? {}
|
|
305
|
+
: {
|
|
306
|
+
check: checkWithMarks(after.map((session) => ({ name: session.name, actions: session.actions })), {
|
|
307
|
+
standing,
|
|
308
|
+
lines: markLines(reading.records),
|
|
309
|
+
unreadable: reading.failed,
|
|
310
|
+
nameOf: (id) => nameOfId.get(id),
|
|
311
|
+
keepNotes: !options.share,
|
|
312
|
+
}),
|
|
313
|
+
}),
|
|
314
|
+
};
|
|
315
|
+
const { done, reopened } = marksInRange(read.map((session) => session.actions), after.map((session) => session.actions), standing);
|
|
316
|
+
const marks = standing.size === 0 && !reading.failed
|
|
317
|
+
? undefined
|
|
318
|
+
: { done, reopened: reopened.map(({ path, result, at }) => ({ path, result, at })), unreadable: reading.failed };
|
|
319
|
+
return { index, after, marks };
|
|
320
|
+
};
|
|
321
|
+
const indexPath = join(out, 'index.html');
|
|
322
|
+
const onboarding = this.#dependencies.onboarding;
|
|
323
|
+
// W1: the onboarding is a page like the others where it is served, and only there - it writes, so never as a file.
|
|
324
|
+
const pages = [
|
|
325
|
+
...Object.entries(this.#dependencies.pages ?? {}),
|
|
326
|
+
...(welcome === undefined || onboarding === undefined ? [] : [[onboarding.file, onboarding.page]]),
|
|
327
|
+
];
|
|
328
|
+
// The page being written, so a failure names the one that failed rather than the index, which may already be there.
|
|
329
|
+
let writing = indexPath;
|
|
330
|
+
const writePages = async (index) => {
|
|
331
|
+
writing = indexPath;
|
|
332
|
+
const rendered = this.#dependencies.renderer.render(index);
|
|
333
|
+
await files.writeText(indexPath, rendered);
|
|
334
|
+
versions.set('index.html', contentVersion(rendered));
|
|
335
|
+
for (const [name, renderer] of pages) {
|
|
336
|
+
writing = join(out, name);
|
|
337
|
+
const page = renderer.render(index);
|
|
338
|
+
await files.writeText(writing, page);
|
|
339
|
+
versions.set(name, contentVersion(page));
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
const first = await build();
|
|
343
|
+
try {
|
|
344
|
+
await writePages(first.index);
|
|
345
|
+
}
|
|
346
|
+
catch (error) {
|
|
347
|
+
if (!(error instanceof FileAccessError))
|
|
348
|
+
throw error;
|
|
349
|
+
return { outcome: 'unwritable', output: `The index could not be written: ${writing}\n` };
|
|
350
|
+
}
|
|
351
|
+
const digest = digestOf(first.after.map((session) => session.actions), {
|
|
352
|
+
asked: options.since.asked,
|
|
353
|
+
sessionsUnreadable: unreadable,
|
|
354
|
+
policyKind: chosen.policy.origin.kind,
|
|
355
|
+
...(first.marks === undefined ? {} : { marks: first.marks }),
|
|
356
|
+
});
|
|
357
|
+
// R75: what a served page may be sent grows with the conversations it lists.
|
|
358
|
+
const servable = new Set(['index.html', ...pages.map(([name]) => name)]);
|
|
359
|
+
const reportsServed = () => {
|
|
360
|
+
for (const entry of entries)
|
|
361
|
+
if (entry.report.kind === 'generated')
|
|
362
|
+
servable.add(entry.report.file);
|
|
363
|
+
};
|
|
364
|
+
reportsServed();
|
|
365
|
+
/*
|
|
366
|
+
* R75: the conversations and the marks as they are now, drawn into every page. A page read while a refresh runs is
|
|
367
|
+
* answered by that refresh; a write is not, because the refresh it would join began before the write - it waits,
|
|
368
|
+
* then runs again. A listing that finds nothing now (a directory moved, a disk gone) leaves the pages as they were.
|
|
369
|
+
*/
|
|
370
|
+
let refreshing;
|
|
371
|
+
const refresh = async (when) => {
|
|
372
|
+
if (refreshing !== undefined && !when.afterWrite)
|
|
373
|
+
return refreshing;
|
|
374
|
+
while (refreshing !== undefined)
|
|
375
|
+
await refreshing;
|
|
376
|
+
const running = (async () => {
|
|
377
|
+
const now_ = await catalogue.list(workingDirectory);
|
|
378
|
+
if (!now_.found || now_.sessions.length === 0)
|
|
379
|
+
return undefined;
|
|
380
|
+
// A policy that cannot be read now leaves the one the pages were drawn under, rather than rules nobody chose.
|
|
381
|
+
const again = await choosePolicy(options, policyFiles, this.#dependencies.tell).catch(() => undefined);
|
|
382
|
+
if (again !== undefined && !('errors' in again))
|
|
383
|
+
policyNow = again.policy;
|
|
384
|
+
const refusal = await sync(now_.sessions, standingMarks((await this.#dependencies.marks.read()).records));
|
|
385
|
+
if (refusal !== undefined)
|
|
386
|
+
return refusal;
|
|
387
|
+
reportsServed();
|
|
388
|
+
await writePages((await build()).index);
|
|
389
|
+
return undefined;
|
|
390
|
+
})();
|
|
391
|
+
refreshing = running;
|
|
392
|
+
try {
|
|
393
|
+
return await running;
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
if (refreshing === running)
|
|
397
|
+
refreshing = undefined;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
// W15: Finish writes through the routes Settings writes with, reading the settings as the files hold them now. Never
|
|
401
|
+
// in the home directory (which-project V7): nothing is set up there, and no `done` line is written for it.
|
|
402
|
+
const { setup, notices } = this.#dependencies;
|
|
403
|
+
const finish = welcome === undefined || notHere !== undefined || onboarding === undefined || setup === undefined || notices === undefined
|
|
404
|
+
? undefined
|
|
405
|
+
: (choices) => finishOnboarding({
|
|
406
|
+
settingsNow: async () => (await build()).index.settings,
|
|
407
|
+
settings: (change) => settingsChangeToSetup(setup, change),
|
|
408
|
+
// W12a: a file switched Block or Tell me is written as Settings' own switch writes it, told lists included.
|
|
409
|
+
...(this.#dependencies.tellLists === undefined ? {} : { mode: (change) => modeChange(setup, this.#dependencies.tellLists, change) }),
|
|
410
|
+
notify: (change) => notices.change(change),
|
|
411
|
+
store: onboarding.store,
|
|
412
|
+
clock: this.#dependencies.clock ?? (() => this.#dependencies.now),
|
|
413
|
+
}, choices);
|
|
414
|
+
// which-project V14, V17: another project, by the id this run listed it under, shown in this tab by a run of its own.
|
|
415
|
+
const switchTo = this.#dependencies.switchTo;
|
|
416
|
+
// V12: folders chosen in the computer's window, under ids this run gave them - so the page never names a path.
|
|
417
|
+
const chosenFolders = new Map();
|
|
418
|
+
const switchToFolder = async (folder, from) => {
|
|
419
|
+
if (switchTo === undefined)
|
|
420
|
+
return { failed: 'This run cannot show another project.' };
|
|
421
|
+
const answer = await switchTo(folder, options.since.asked, from);
|
|
422
|
+
if ('url' in answer)
|
|
423
|
+
switchedAway = true;
|
|
424
|
+
return answer;
|
|
425
|
+
};
|
|
426
|
+
const switchProject = switchTo === undefined ? undefined : async (id, from) => {
|
|
427
|
+
const chosen = chosenFolders.get(id);
|
|
428
|
+
if (chosen !== undefined)
|
|
429
|
+
return switchToFolder(chosen, from);
|
|
430
|
+
const project = listed?.projects.find((one) => one.id === id);
|
|
431
|
+
if (project === undefined || !project.exists || this.#notAProject(project.path) !== undefined)
|
|
432
|
+
return { failed: 'That project is not one this page listed.' };
|
|
433
|
+
if (relative(project.path, workingDirectory) === '')
|
|
434
|
+
return { failed: 'That project is the one shown already.' };
|
|
435
|
+
return switchToFolder(project.path, from);
|
|
436
|
+
};
|
|
437
|
+
// V12: a folder chosen in the computer's window - shown at once where it is a project or near none, and where it lies
|
|
438
|
+
// inside a project or holds some, those are offered first, with the folder itself under an id of this run's.
|
|
439
|
+
const chooser = this.#dependencies.folderChooser;
|
|
440
|
+
const chooseFolder = switchTo === undefined || chooser === undefined || !chooser.available ? undefined : async (lang, from) => {
|
|
441
|
+
// Where the window starts: the folder this project lies in, where its neighbours are - or, in the home directory,
|
|
442
|
+
// there. Both are on this disk, so it opens at once, not in a folder a cloud keeps (VB1).
|
|
443
|
+
const startIn = notHere !== undefined ? this.#dependencies.home : dirname(workingDirectory);
|
|
444
|
+
const picked = await chooser.choose(translator(lang)('proj.choosePrompt'), startIn);
|
|
445
|
+
if ('cancelled' in picked)
|
|
446
|
+
return { kind: 'cancelled' };
|
|
447
|
+
if ('failed' in picked)
|
|
448
|
+
return { kind: 'failed', reason: picked.failed };
|
|
449
|
+
const not = this.#notAProject(picked.chosen);
|
|
450
|
+
if (not !== undefined)
|
|
451
|
+
return { kind: 'refused', not };
|
|
452
|
+
if (relative(picked.chosen, workingDirectory) === '')
|
|
453
|
+
return { kind: 'here' };
|
|
454
|
+
const near = nearestProject(picked.chosen, listed ?? { projects: [], unreadable: 0 }, (path) => this.#notAProject(path) !== undefined);
|
|
455
|
+
// Where each folder is, as the page says it (`~/Projects/my-app/src`): the page shows it and never sends it back.
|
|
456
|
+
const placeOf = (path) => this.#dependencies.home === undefined ? {} : { place: homeRelative(path, this.#dependencies.home) };
|
|
457
|
+
if (near.kind === 'above' || near.kind === 'inside') {
|
|
458
|
+
const id = `folder-${chosenFolders.size + 1}`;
|
|
459
|
+
chosenFolders.set(id, picked.chosen);
|
|
460
|
+
// The project shown is offered too, marked: on the onboarding's step choosing it goes on to Who in the page.
|
|
461
|
+
return {
|
|
462
|
+
kind: 'near',
|
|
463
|
+
above: near.kind === 'above',
|
|
464
|
+
chosen: { id, name: projectName(picked.chosen), ...placeOf(picked.chosen) },
|
|
465
|
+
projects: (near.kind === 'above' ? [near.project] : near.projects.slice(0, 3)).map((project) => ({
|
|
466
|
+
id: project.id,
|
|
467
|
+
name: projectName(project.path),
|
|
468
|
+
...placeOf(project.path),
|
|
469
|
+
...(relative(project.path, workingDirectory) === '' ? { here: true } : {}),
|
|
470
|
+
})),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
const shown = near.kind === 'itself' ? near.project.path : picked.chosen;
|
|
474
|
+
const answer = await switchToFolder(shown, from);
|
|
475
|
+
return 'url' in answer ? { kind: 'switched', url: answer.url, name: projectName(shown) } : { kind: 'failed', reason: answer.failed };
|
|
476
|
+
};
|
|
477
|
+
// V14, amended: the pages of a project this process showed before - reached by "Back" - are told it shows another,
|
|
478
|
+
// and may ask for it again; the project shown again since sends them to this run's own pages.
|
|
479
|
+
const pastRuns = this.#dependencies.pastRuns;
|
|
480
|
+
const pastRun = switchTo === undefined || pastRuns === undefined ? undefined : (other, urlOf) => {
|
|
481
|
+
const earlier = pastRuns().find((one) => one.token === other);
|
|
482
|
+
if (earlier === undefined)
|
|
483
|
+
return undefined;
|
|
484
|
+
const home = this.#dependencies.home;
|
|
485
|
+
const named = (folder) => ({ name: projectName(folder), ...(home === undefined ? {} : { place: homeRelative(folder, home) }) });
|
|
486
|
+
// **Go to blog** is the page this run would open now (W23): its onboarding while nobody has set it up - at Who,
|
|
487
|
+
// where a switch brought the person - and its conversations once someone has (the maintainer, 2026-09-29).
|
|
488
|
+
const nowUrl = async () => {
|
|
489
|
+
const setUpNow = onboarding === undefined || (await this.#welcome(workingDirectory))?.opens !== true;
|
|
490
|
+
return setUpNow ? urlOf('index.html') : urlOf(onboarding.file) + (arrivedFrom === undefined ? '' : `#${WHO_STEP}`);
|
|
491
|
+
};
|
|
492
|
+
return {
|
|
493
|
+
same: relative(earlier.folder, workingDirectory) === '',
|
|
494
|
+
page: async () => movedPage(named(earlier.folder), named(workingDirectory), await nowUrl()),
|
|
495
|
+
back: () => switchToFolder(earlier.folder, 'window'),
|
|
496
|
+
};
|
|
497
|
+
};
|
|
498
|
+
// R50: a page that was opened is served, so a mark made on it is recorded at once. Never a shared page (R37).
|
|
499
|
+
const serving = !options.share && (options.serve ?? options.open)
|
|
500
|
+
? await this.#serve(out, indexPath, {
|
|
501
|
+
servable,
|
|
502
|
+
read: () => read,
|
|
503
|
+
options,
|
|
504
|
+
rerender: async () => {
|
|
505
|
+
await refresh({ afterWrite: true });
|
|
506
|
+
},
|
|
507
|
+
fresh: async () => {
|
|
508
|
+
await refresh({ afterWrite: false });
|
|
509
|
+
},
|
|
510
|
+
include,
|
|
511
|
+
...(finish === undefined ? {} : { finish }),
|
|
512
|
+
...(switchProject === undefined ? {} : { switchProject }),
|
|
513
|
+
...(chooseFolder === undefined ? {} : { chooseFolder }),
|
|
514
|
+
...(pastRun === undefined ? {} : { pastRun }),
|
|
515
|
+
versionOf: (name) => {
|
|
516
|
+
const version = versions.get(name);
|
|
517
|
+
if (version === undefined)
|
|
518
|
+
return undefined;
|
|
519
|
+
return name === 'index.html' ? { version, conversations: entries.length } : { version };
|
|
520
|
+
},
|
|
521
|
+
})
|
|
522
|
+
: undefined;
|
|
523
|
+
// W23: the onboarding is opened in place of the index where it is to be; the index otherwise, as it always was. A run
|
|
524
|
+
// a page switched to opens it at *Who*: the project was chosen on the way here (which-project V16).
|
|
525
|
+
const openAt = serving === undefined ? indexPath
|
|
526
|
+
: opensWelcome && onboarding !== undefined ? serving.urlOf(onboarding.file) + (arrivedFrom === undefined ? '' : `#${WHO_STEP}`)
|
|
527
|
+
: serving.url;
|
|
528
|
+
// V15: a run a page switched to is shown in that page's tab, so it opens none of its own.
|
|
529
|
+
const handedOver = this.#dependencies.handedOver;
|
|
530
|
+
const opened = options.open && handedOver === undefined ? await this.#dependencies.browser.open(openAt) : true;
|
|
531
|
+
const view = this.#dependencies.terminal ?? { colour: false, decorated: false };
|
|
532
|
+
const said = startSaid({
|
|
533
|
+
...counted(entries, outOfRange.length, indexPath, first.index, opened),
|
|
534
|
+
// which-project V3: the folder the run was for, said where the terminal says what it read.
|
|
535
|
+
...(options.share ? {} : { project: projectName(workingDirectory) }),
|
|
536
|
+
asked: options.since.asked,
|
|
537
|
+
digest: this.#dependencies.digest.render(digest),
|
|
538
|
+
// Said where it happened: a page that was asked for and could not be served is what the next line is about.
|
|
539
|
+
...(serving === undefined && !options.share && (options.serve ?? options.open) && this.#dependencies.server !== undefined
|
|
540
|
+
? { servedAsFile: true }
|
|
541
|
+
: {}),
|
|
542
|
+
...(serving === undefined ? {} : { url: openAt }),
|
|
543
|
+
...(serving !== undefined && opensWelcome ? { welcome: true } : {}),
|
|
544
|
+
// which-project V3: a switch says which project is shown now, and where it is.
|
|
545
|
+
...(handedOver === undefined || options.share ? {} : { nowShowing: this.#dependencies.home === undefined ? projectName(workingDirectory) : `${projectName(workingDirectory)} (${homeRelative(workingDirectory, this.#dependencies.home)})` }),
|
|
546
|
+
}, view);
|
|
547
|
+
if (serving === undefined)
|
|
548
|
+
return { outcome: 'written', output: said };
|
|
549
|
+
const printer = this.#dependencies.printer;
|
|
550
|
+
printer?.write(said);
|
|
551
|
+
handedOver?.(openAt);
|
|
552
|
+
await serving.closed;
|
|
553
|
+
// V14: a run a page moved away from ends quietly - the run it moved to has already said what it shows.
|
|
554
|
+
if (switchedAway)
|
|
555
|
+
return { outcome: 'written', output: '' };
|
|
556
|
+
return { outcome: 'written', output: printer === undefined ? said : pageStopped(view) };
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Serves the directory this run wrote (R50-R54): the files it wrote by name, and the two writes a page can make.
|
|
560
|
+
* `undefined` when there is no server, or it could not start - the page is then opened as a file, as before.
|
|
561
|
+
*/
|
|
562
|
+
async #serve(out, indexPath, run) {
|
|
563
|
+
const { server, marks, policyFiles } = this.#dependencies;
|
|
564
|
+
const { servable: served, options, rerender, fresh, include, finish, versionOf } = run;
|
|
565
|
+
if (server === undefined)
|
|
566
|
+
return undefined;
|
|
567
|
+
const token = (this.#dependencies.token ?? (() => 'page'))();
|
|
568
|
+
const clock = this.#dependencies.clock ?? (() => this.#dependencies.now);
|
|
569
|
+
// The lines a mark is checked against are the check before marks, naming sessions by id (R52) - read at each mark,
|
|
570
|
+
// since a conversation checked on request (F55), or one begun since the run (R75), brings lines of its own.
|
|
571
|
+
const lines = () => checkOf(run.read().map((session) => ({ name: session.id, actions: session.actions }))).rows;
|
|
572
|
+
// R75: a page is brought up to date before it is sent. A refresh that fails leaves the page as it last was - an
|
|
573
|
+
// older page is still a page, and a request that failed over it would be none.
|
|
574
|
+
const page = (request) => request.method === 'GET' && request.path.startsWith(`/${token}/`) && (request.path.endsWith('.html') || request.path === `/${token}/`);
|
|
575
|
+
let handle;
|
|
576
|
+
let serving;
|
|
577
|
+
try {
|
|
578
|
+
serving = await server.serve(async (request) => {
|
|
579
|
+
if (handle === undefined)
|
|
580
|
+
return { status: 503, type: 'text/plain', body: 'Starting.' };
|
|
581
|
+
if (page(request))
|
|
582
|
+
await fresh().catch(() => undefined);
|
|
583
|
+
return handle(request);
|
|
584
|
+
}, { idleMs: IDLE_MS, maxBody: MAX_BODY });
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
handle = indexHandler({
|
|
590
|
+
// which-project V14, V18: the page moves to another project's run, and this server closes once it has the address.
|
|
591
|
+
...(run.switchProject === undefined ? {} : { switchProject: run.switchProject, switched: () => serving.close() }),
|
|
592
|
+
...(run.chooseFolder === undefined ? {} : { chooseFolder: run.chooseFolder }),
|
|
593
|
+
...(run.pastRun === undefined ? {} : { pastRun: (other) => run.pastRun?.(other, (name) => `${serving.origin}/${token}/${name}`) }),
|
|
594
|
+
origin: serving.origin,
|
|
595
|
+
token,
|
|
596
|
+
files: served,
|
|
597
|
+
read: (name) => policyFiles.readText(name === 'index.html' ? indexPath : join(out, name)),
|
|
598
|
+
mark: (request) => recordMark(marks, lines(), request, { asked: options.since.asked, now: clock() }),
|
|
599
|
+
unmark: (path) => recordUnmark(marks, path, clock()),
|
|
600
|
+
// R58: the change becomes the `SetupOptions` the same flags would build, and runs through the same setup.
|
|
601
|
+
...(this.#dependencies.setup === undefined || options.share
|
|
602
|
+
? {}
|
|
603
|
+
: {
|
|
604
|
+
settings: async (change) => {
|
|
605
|
+
// which-project V8: where no project is, a change that writes is refused before its first step, not at the
|
|
606
|
+
// step that writes - by then a rule or a told list may already have been taken out (`onlyTakesOut`).
|
|
607
|
+
const notOne = this.#notAProject(this.#dependencies.workingDirectory);
|
|
608
|
+
if (notOne !== undefined && !onlyTakesOut(change))
|
|
609
|
+
return { outcome: 'refused', output: NOT_A_PROJECT[notOne] };
|
|
610
|
+
const setup = this.#dependencies.setup;
|
|
611
|
+
const lists = this.#dependencies.tellLists;
|
|
612
|
+
// F57: a row's switch writes a told list as well as the settings files, so it goes where both are known.
|
|
613
|
+
const answer = await (change.change === 'mode' && lists !== undefined ? modeChange(setup, lists, change) : settingsChangeToSetup(setup, change));
|
|
614
|
+
// W25a: an uninstall that went through offers the onboarding again at the next run - the setup it made is gone.
|
|
615
|
+
if (change.change === 'uninstall' && (answer.outcome === 'written' || answer.outcome === 'unchanged')) {
|
|
616
|
+
await this.#dependencies.onboarding?.store.reset(clock());
|
|
617
|
+
}
|
|
618
|
+
return answer;
|
|
619
|
+
},
|
|
620
|
+
}),
|
|
621
|
+
// R26a: the third thing the page writes, and the only one that touches no project. It goes through the same
|
|
622
|
+
// use case `agentwhy notify` runs, so there is one set of rules for this file and two ways in.
|
|
623
|
+
...(this.#dependencies.notices === undefined || options.share
|
|
624
|
+
? {}
|
|
625
|
+
: { notify: (change) => this.#dependencies.notices.change(change) }),
|
|
626
|
+
// F55: a report written on request becomes one of the files this server may serve. A shared page asks for none.
|
|
627
|
+
...(options.share
|
|
628
|
+
? {}
|
|
629
|
+
: {
|
|
630
|
+
include: async (name) => {
|
|
631
|
+
const answer = await include(name);
|
|
632
|
+
if (answer.file !== undefined)
|
|
633
|
+
served.add(answer.file);
|
|
634
|
+
return { written: answer.written, said: answer.said };
|
|
635
|
+
},
|
|
636
|
+
}),
|
|
637
|
+
// W15: the onboarding's one request, where the onboarding is served (W1).
|
|
638
|
+
...(finish === undefined ? {} : { onboarding: finish }),
|
|
639
|
+
rerender,
|
|
640
|
+
versionOf,
|
|
641
|
+
});
|
|
642
|
+
// V14, amended: this run's pages are known by their token to every run of this process after it.
|
|
643
|
+
this.#dependencies.served?.(token);
|
|
644
|
+
const urlOf = (name) => `${serving.origin}/${token}/${name}`;
|
|
645
|
+
return { url: urlOf('index.html'), urlOf, closed: serving.closed };
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* The onboarding, on a served run (W23, W24; N6): `undefined` where this run cannot serve it - no page, or nothing to
|
|
649
|
+
* write through. It opens only for a project nobody has set up: never where the record says it was finished here or
|
|
650
|
+
* cannot be read, nor where the project's settings cannot be read, since what runs is then not known. A project whose
|
|
651
|
+
* `watch` hook already runs was set up before this page existed, and is recorded as such, silently, so that turning
|
|
652
|
+
* alerts off later never brings the onboarding back.
|
|
653
|
+
*/
|
|
654
|
+
async #welcome(workingDirectory) {
|
|
655
|
+
const { onboarding, setup, notices } = this.#dependencies;
|
|
656
|
+
if (onboarding === undefined || setup === undefined || notices === undefined)
|
|
657
|
+
return undefined;
|
|
658
|
+
const reading = await onboarding.store.read();
|
|
659
|
+
// W24: the intro plays once per person - where no project of theirs has finished the onboarding.
|
|
660
|
+
const intro = !reading.anywhere && !reading.failed;
|
|
661
|
+
// which-project V7: in the home directory no project is being shown, so the onboarding opens at its project step,
|
|
662
|
+
// whatever the record says - after the welcome only for a person who has never finished one. Nothing here is set up
|
|
663
|
+
// and no record is kept of it: not even the silent `done` line below.
|
|
664
|
+
if (this.#notAProject(workingDirectory) !== undefined)
|
|
665
|
+
return { opens: true, intro, setUp: false, ...(intro ? {} : { atProject: true }) };
|
|
666
|
+
if (reading.here)
|
|
667
|
+
return { opens: false, intro, setUp: true };
|
|
668
|
+
if (reading.failed)
|
|
669
|
+
return { opens: false, intro, setUp: false };
|
|
670
|
+
// A local file that cannot be read leaves what runs unknown, and the onboarding closed (as `#settingsNow` does).
|
|
671
|
+
const local = await this.#settingsFile(join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.local));
|
|
672
|
+
if (local === 'unreadable')
|
|
673
|
+
return { opens: false, intro, setUp: false };
|
|
674
|
+
const shared = await this.#settingsFile(join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.shared));
|
|
675
|
+
// N6, widened 2026-09-28 by the maintainer (which-project V10): a project where one of agentwhy's hooks runs, or whose
|
|
676
|
+
// settings block files, was set up - before this screen existed, or by hand - and is recorded so, silently.
|
|
677
|
+
if (setUpBy([local, shared])) {
|
|
678
|
+
await onboarding.store.add((this.#dependencies.clock ?? (() => this.#dependencies.now))());
|
|
679
|
+
return { opens: false, intro, setUp: true };
|
|
680
|
+
}
|
|
681
|
+
return { opens: true, intro, setUp: false };
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* The person's projects, read again with every refresh as the settings are, since a project set up in another tab is
|
|
685
|
+
* set up here too (V10). Only on a served page, which the window is drawn on, and never under `--share`.
|
|
686
|
+
*/
|
|
687
|
+
async #projectsNow(wanted, workingDirectory) {
|
|
688
|
+
const { projects, home, onboarding, policyFiles } = this.#dependencies;
|
|
689
|
+
if (!wanted || projects === undefined || home === undefined)
|
|
690
|
+
return {};
|
|
691
|
+
const listing = await projects.list();
|
|
692
|
+
const noProject = (path) => this.#notAProject(path) !== undefined;
|
|
693
|
+
const temporary = (path) => inTemporarySpace(path, this.#dependencies.temporaryDirectory, home);
|
|
694
|
+
// V20: a folder with no conversations of its own, inside a project that has them - where they are.
|
|
695
|
+
const near = nearestProject(workingDirectory, listing, noProject);
|
|
696
|
+
const above = near.kind === 'above' ? { id: near.project.id, within: relative(near.project.path, workingDirectory) } : undefined;
|
|
697
|
+
const known = await indexProjects(listing, {
|
|
698
|
+
files: policyFiles,
|
|
699
|
+
...(onboarding === undefined ? {} : { onboarding: onboarding.store }),
|
|
700
|
+
home,
|
|
701
|
+
workingDirectory,
|
|
702
|
+
noProject,
|
|
703
|
+
temporary,
|
|
704
|
+
switchable: this.#dependencies.switchTo !== undefined,
|
|
705
|
+
choosable: this.#dependencies.switchTo !== undefined && this.#dependencies.folderChooser?.available === true,
|
|
706
|
+
});
|
|
707
|
+
return { listing, projects: above === undefined ? known : { ...known, above } };
|
|
708
|
+
}
|
|
709
|
+
/** Why the working directory is not a project (V6), where the run was told the home directory to tell. */
|
|
710
|
+
#notAProject(workingDirectory) {
|
|
711
|
+
const { home, realHome } = this.#dependencies;
|
|
712
|
+
return home === undefined ? undefined : notAProject(workingDirectory, home, realHome);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* What the project's own settings file runs and protects, read the way `init` reads it: which hooks are
|
|
716
|
+
* installed, and which of the run's protected patterns are deny rules this file holds - the only ones Settings
|
|
717
|
+
* can take out again. A file that is absent leaves both switches off and nothing of its own protected; one that
|
|
718
|
+
* cannot be parsed leaves Settings without controls at all, rather than with controls drawn from a guess.
|
|
719
|
+
*/
|
|
720
|
+
/**
|
|
721
|
+
* What holds now about being told, and which answer decides each (`the-agent-tells-you.md` R26). The panel shows
|
|
722
|
+
* the answer in force rather than the file's raw contents: what a person wants to know is what will happen at the
|
|
723
|
+
* end of their next turn, and which of their answers is the reason.
|
|
724
|
+
*/
|
|
725
|
+
async #noticesNow() {
|
|
726
|
+
const { notices } = this.#dependencies;
|
|
727
|
+
if (notices === undefined)
|
|
728
|
+
return undefined;
|
|
729
|
+
const view = await notices.view();
|
|
730
|
+
const from = (key) => view.forProject[key] !== undefined ? 'project' : view.defaults[key] !== undefined ? 'everywhere' : 'default';
|
|
731
|
+
return {
|
|
732
|
+
on: view.effective.on ?? DEFAULT_THRESHOLD,
|
|
733
|
+
clean: view.effective.clean ?? DEFAULT_CLEAN,
|
|
734
|
+
say: view.effective.say ?? DEFAULT_SAID_AS,
|
|
735
|
+
notify: view.effective.notify ?? DEFAULT_CHANNELS,
|
|
736
|
+
from: { on: from('on'), clean: from('clean'), say: from('say'), notify: from('notify') },
|
|
737
|
+
path: view.path,
|
|
738
|
+
unusable: view.unusable,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
async #settingsNow(workingDirectory, codexListed) {
|
|
742
|
+
const path = join(SETTINGS_FILES.directory, SETTINGS_FILES.local);
|
|
743
|
+
const sharedPath = join(SETTINGS_FILES.directory, SETTINGS_FILES.shared);
|
|
744
|
+
const local = await this.#settingsFile(join(workingDirectory, path));
|
|
745
|
+
// The file this page writes by default is the one it cannot go on without: unreadable, there is no knowing
|
|
746
|
+
// what a switch would be turning, so the view keeps its two lists and offers nothing (R62).
|
|
747
|
+
if (local === 'unreadable')
|
|
748
|
+
return {};
|
|
749
|
+
// The committed file is the project's, and a project whose file cannot be parsed still has a local one that
|
|
750
|
+
// can. Its rules then carry no control - the same as any rule this page did not write - rather than none of
|
|
751
|
+
// the page's controls being drawn at all.
|
|
752
|
+
const shared = await this.#settingsFile(join(workingDirectory, sharedPath));
|
|
753
|
+
const readable = shared === 'unreadable' ? undefined : shared;
|
|
754
|
+
// A hook installed in the committed file runs for this project as surely as one installed here, so the switch
|
|
755
|
+
// is drawn from both files. Where both hold it, the local one is what a change would rewrite. A hook half
|
|
756
|
+
// installed reads as off (`hookComplete`).
|
|
757
|
+
const installed = (hook) => local !== undefined && hookComplete(local, hook) ? 'local'
|
|
758
|
+
: readable !== undefined && hookComplete(readable, hook) ? 'shared'
|
|
759
|
+
: false;
|
|
760
|
+
const mine = {};
|
|
761
|
+
const held = { local: [], shared: [] };
|
|
762
|
+
// The committed file first, so a pattern both files deny is named by the one a change to it would write.
|
|
763
|
+
for (const [file, settings] of [['shared', readable], ['local', local]]) {
|
|
764
|
+
if (settings === undefined)
|
|
765
|
+
continue;
|
|
766
|
+
// A pattern counts as a file's only where that file denies it for every tool `init` writes: half a pair is
|
|
767
|
+
// a rule somebody wrote by hand, and `--unprotect` must not be offered for it.
|
|
768
|
+
const paths = fileRulesIn(settings)
|
|
769
|
+
.map((entry) => fileRulePathOf(entry))
|
|
770
|
+
.filter((pattern) => pattern !== undefined);
|
|
771
|
+
// Keyed by what the policy lists the rule as protecting, because that is the list the rows are drawn from;
|
|
772
|
+
// the rule itself is kept beside it, because that is the string `--unprotect` has to be given. A rule that
|
|
773
|
+
// is only half a pair is kept too, and marked: it protects a file whatever wrote it, and the page said
|
|
774
|
+
// nothing at all about one - not the rule, not the file, not even that the project had rules of its own.
|
|
775
|
+
for (const rule of new Set(paths)) {
|
|
776
|
+
mine[toPattern(rule)] = { file, rule, whole: isDenied(settings, rule) };
|
|
777
|
+
held[file].push(toPattern(rule));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
// What each hook reads is in its own command; a hook not installed would be pointed where `init` points one
|
|
781
|
+
// (R6): at the local file where it holds file rules, else at the shared one where that does, else nowhere.
|
|
782
|
+
const pointed = local !== undefined && fileRulesIn(local).length > 0 ? 'local' : readable !== undefined && fileRulesIn(readable).length > 0 ? 'shared' : 'default';
|
|
783
|
+
const readsOf = (hook) => {
|
|
784
|
+
const file = installed(hook);
|
|
785
|
+
const settings = file === 'local' ? local : file === 'shared' ? readable : undefined;
|
|
786
|
+
return (settings === undefined ? undefined : rulesReadByHooks(settings).get(hook)) ?? pointed;
|
|
787
|
+
};
|
|
788
|
+
// U2: the version is this run's own, read once; the hooks are read from the files as they are now.
|
|
789
|
+
this.#serving ??= this.#dependencies.invocation?.version() ?? Promise.resolve(undefined);
|
|
790
|
+
const late = behind({ ...(local === undefined ? {} : { local }), ...(readable === undefined ? {} : { shared: readable }) }, await this.#serving);
|
|
791
|
+
// CK8: Codex's hook file, read as the settings are; its state is drawn only where the project uses Codex (CK6).
|
|
792
|
+
const codexHooks = await this.#settingsFile(join(workingDirectory, PROJECT_HOOKS.directory, PROJECT_HOOKS.file));
|
|
793
|
+
const codex = typeof codexHooks === 'object' && codexRefuseIn(codexHooks) !== undefined ? 'on' : codexListed || codexHooks !== undefined ? 'off' : undefined;
|
|
794
|
+
return {
|
|
795
|
+
hooks: {
|
|
796
|
+
watch: installed('watch'),
|
|
797
|
+
refuse: installed('refuse'),
|
|
798
|
+
reads: { watch: readsOf('watch'), refuse: readsOf('refuse') },
|
|
799
|
+
path,
|
|
800
|
+
sharedPath,
|
|
801
|
+
...(late === undefined ? {} : { behind: late }),
|
|
802
|
+
...(codex === undefined ? {} : { codex }),
|
|
803
|
+
},
|
|
804
|
+
mine,
|
|
805
|
+
held: { local: [...new Set(held.local)], shared: [...new Set(held.shared)] },
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* One settings file as JSON: `undefined` where there is no such file - which is not a problem, it is a project
|
|
810
|
+
* that has not written one - and `unreadable` where there is one and it does not parse, which is not something
|
|
811
|
+
* to guess past.
|
|
812
|
+
*/
|
|
813
|
+
async #settingsFile(path) {
|
|
814
|
+
return readSettingsFile(this.#dependencies.policyFiles, path);
|
|
815
|
+
}
|
|
816
|
+
async #generate(session, out, file, options, policy, page) {
|
|
817
|
+
const { share } = options;
|
|
818
|
+
const result = await this.#dependencies.report.run({
|
|
819
|
+
input: session.path,
|
|
820
|
+
policy,
|
|
821
|
+
htmlPath: join(out, file),
|
|
822
|
+
ascii: false,
|
|
823
|
+
full: false,
|
|
824
|
+
colour: false,
|
|
825
|
+
open: false,
|
|
826
|
+
share,
|
|
827
|
+
width: DISCARDED_WIDTH,
|
|
828
|
+
// `a-way-back` R8: this run writes the index beside the report, so this report may offer the way back to it.
|
|
829
|
+
withIndexLink: true,
|
|
830
|
+
marks: page.marks,
|
|
831
|
+
served: page.served,
|
|
832
|
+
// P4: the title its row is found by, for "You asked". Never read under --share (above).
|
|
833
|
+
...(page.title === undefined ? {} : { title: page.title }),
|
|
834
|
+
});
|
|
835
|
+
if (result.outcome === 'policy-refused')
|
|
836
|
+
return { kind: 'policy-refused', output: result.output };
|
|
837
|
+
// A report that was not built, or not written, is a session the page must not link to as if it were. Asked of
|
|
838
|
+
// the result, never of its text: the old check searched the report for "could not be written", and a task
|
|
839
|
+
// description quoted in that report can contain those words. Found by a review.
|
|
840
|
+
// Actions travel only with a session that was read: a report of an unreadable session has nothing to say about it.
|
|
841
|
+
const analysed = result.outcome === 'session-unreadable' ? {} : result.actions === undefined ? {} : { actions: result.actions };
|
|
842
|
+
return result.tally === undefined || result.outcome === 'session-unreadable' || result.htmlWritten !== true
|
|
843
|
+
? { kind: 'failed', ...analysed }
|
|
844
|
+
: {
|
|
845
|
+
kind: 'generated',
|
|
846
|
+
file,
|
|
847
|
+
tally: result.tally,
|
|
848
|
+
incomplete: result.outcome === 'incomplete',
|
|
849
|
+
...analysed,
|
|
850
|
+
// To fix opens the same "What happened" as this report, told once, here (T11a).
|
|
851
|
+
...(result.stories === undefined ? {} : { stories: result.stories }),
|
|
852
|
+
...(result.reached === undefined ? {} : { reached: result.reached }),
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* One row of the index from what became of its session's report. The page is given what it renders, and the actions
|
|
858
|
+
* stay with the digest they were collected for: the row lists the files it lists, and nothing else of what `check`
|
|
859
|
+
* was told about them.
|
|
860
|
+
*/
|
|
861
|
+
function entryOf(name, title, session, report) {
|
|
862
|
+
return {
|
|
863
|
+
name,
|
|
864
|
+
provider: session.provider,
|
|
865
|
+
...(title === undefined ? {} : { title }),
|
|
866
|
+
modifiedAt: session.modifiedAt,
|
|
867
|
+
delegations: session.delegations,
|
|
868
|
+
report: report.kind === 'generated'
|
|
869
|
+
? {
|
|
870
|
+
kind: 'generated',
|
|
871
|
+
file: report.file,
|
|
872
|
+
tally: report.tally,
|
|
873
|
+
incomplete: report.incomplete,
|
|
874
|
+
...(report.actions === undefined ? {} : { files: filesOf(report.actions) }),
|
|
875
|
+
...(report.stories === undefined ? {} : { stories: report.stories }),
|
|
876
|
+
...(report.reached === undefined ? {} : { reached: report.reached }),
|
|
877
|
+
}
|
|
878
|
+
: report.kind === 'failed'
|
|
879
|
+
? { kind: 'failed' }
|
|
880
|
+
: { kind: 'outside-range' },
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* The protected files of one session, each under the strongest thing known about it: its contents were seen, a
|
|
885
|
+
* call named it, it only appeared in a result, or its outcome is not recorded. The same four the row's badge
|
|
886
|
+
* climbs, so a row and the files under it never disagree.
|
|
887
|
+
*/
|
|
888
|
+
function filesOf(actions) {
|
|
889
|
+
const strongest = new Map();
|
|
890
|
+
const add = (path, kind) => {
|
|
891
|
+
if (!strongest.has(path))
|
|
892
|
+
strongest.set(path, { path, kind });
|
|
893
|
+
};
|
|
894
|
+
for (const file of actions.rotate)
|
|
895
|
+
add(file.path, 'seen');
|
|
896
|
+
for (const route of actions.openRoutes)
|
|
897
|
+
add(route.path, 'named');
|
|
898
|
+
for (const path of actions.onlyInResults)
|
|
899
|
+
add(path, 'result');
|
|
900
|
+
for (const path of actions.unknown)
|
|
901
|
+
add(path, 'unknown');
|
|
902
|
+
for (const path of actions.told ?? [])
|
|
903
|
+
add(path, 'told');
|
|
904
|
+
return [...strongest.values()];
|
|
905
|
+
}
|
|
906
|
+
/** A policy as one comparable value: what it protects, how, and its exceptions (F57). */
|
|
907
|
+
function keyOf(policy) {
|
|
908
|
+
return JSON.stringify([policy.level, policy.protected.map((entry) => [entry.pattern, entry.mode ?? 'block']).sort(), [...policy.allowed].sort()]);
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* The marks a report shows, as one comparable value: only those on its own files. A file marked done on another page
|
|
912
|
+
* changes the report of every session that reached it, and no other (R75).
|
|
913
|
+
*/
|
|
914
|
+
function marksOn(report, marks) {
|
|
915
|
+
const paths = report.actions === undefined ? [] : filesOf(report.actions).map((file) => file.path).sort();
|
|
916
|
+
return JSON.stringify(paths.map((path) => [path, marks.get(path) ?? null]));
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* The marks that hold for a session last active at `lastActive`: made at or after it, as `actionsAfterMarks` decides
|
|
920
|
+
* which files leave the check (R35). A mark older than the session's last activity may have been reached since.
|
|
921
|
+
*/
|
|
922
|
+
function marksFor(lastActive, standing) {
|
|
923
|
+
return new Map([...standing].filter(([, mark]) => lastActive <= mark.at).map(([path, mark]) => [path, mark.result]));
|
|
924
|
+
}
|
|
925
|
+
/** How many conversations were held each way (which-project V4). One whose transcript said nothing is not counted. */
|
|
926
|
+
function countedBy(kinds) {
|
|
927
|
+
const counts = {};
|
|
928
|
+
for (const kind of kinds)
|
|
929
|
+
if (kind !== undefined)
|
|
930
|
+
counts[kind] = (counts[kind] ?? 0) + 1;
|
|
931
|
+
return counts;
|
|
932
|
+
}
|
|
933
|
+
/** The span that would bring the oldest listed session into range, in whole days - the command, not advice. */
|
|
934
|
+
function widenTo(outOfRange, now) {
|
|
935
|
+
const oldest = Math.min(...outOfRange.map((session) => session.modifiedAt), now);
|
|
936
|
+
const days = Math.max(Math.ceil((now - oldest) / DAY), 1);
|
|
937
|
+
return `agentwhy start --since ${days}d`;
|
|
938
|
+
}
|
|
939
|
+
/** Where to go from a folder that is no project (which-project V7): the same words opened or not. */
|
|
940
|
+
const NOT_A_PROJECT_HERE = {
|
|
941
|
+
home: 'You started agentwhy in your home folder, which isn\'t a project. Open your project\'s folder in your code editor\'s terminal, and run agentwhy there.\n',
|
|
942
|
+
root: 'You started agentwhy at the root of a disk, which isn\'t a project. Open your project\'s folder in your code editor\'s terminal, and run agentwhy there.\n',
|
|
943
|
+
};
|
|
944
|
+
/**
|
|
945
|
+
* An empty project, where the onboarding does not open (R28, amended by the onboarding's W1a). A run that would have
|
|
946
|
+
* opened a page names no command: its reader cannot run one, and setting up is the onboarding's, which this project has
|
|
947
|
+
* been through or cannot be offered. R28's text, with `init`, stays for a run asked not to open a page.
|
|
948
|
+
*/
|
|
949
|
+
function noSessions(directory, project, wouldOpen, setUp, notOne) {
|
|
950
|
+
// which-project V7: "work with your AI here" is the wrong way on from a folder that is no project, and so is `init`,
|
|
951
|
+
// which refuses it (V8). Said where the onboarding's project step does not open to ask: a run that opens no page.
|
|
952
|
+
if (notOne !== undefined)
|
|
953
|
+
return NOT_A_PROJECT_HERE[notOne];
|
|
954
|
+
if (wouldOpen) {
|
|
955
|
+
// which-project V3: the folder by name, so a run started in the wrong one says which one it was.
|
|
956
|
+
return (setUp ? 'You\'re set up. ' : '') + `There are no AI chats in ${printable(project)} yet. Work with your AI here, then run agentwhy again.\n`;
|
|
957
|
+
}
|
|
958
|
+
return `No sessions are stored for this directory, so there is nothing to list.\nLooked in ${directory}\n` +
|
|
959
|
+
`agentwhy reads sessions Claude Code and Codex already keep, so either this directory has not been used with either yet, or it is not the one being worked in.\n` +
|
|
960
|
+
`Use Claude Code or Codex here, then run agentwhy start again - or run agentwhy init now, to be protected in Claude Code before there is history to check.\n`;
|
|
961
|
+
}
|
|
962
|
+
/** What the run wrote, counted from what it wrote it about. The words are the renderer's; these are the numbers. */
|
|
963
|
+
function counted(entries, older, indexPath, index, opened) {
|
|
964
|
+
return {
|
|
965
|
+
listed: entries.length,
|
|
966
|
+
generated: entries.filter((entry) => entry.report.kind === 'generated').length,
|
|
967
|
+
failed: entries.filter((entry) => entry.report.kind === 'failed').length,
|
|
968
|
+
older,
|
|
969
|
+
widen: index.widen,
|
|
970
|
+
indexPath,
|
|
971
|
+
shared: index.shared,
|
|
972
|
+
opened,
|
|
973
|
+
};
|
|
974
|
+
}
|