create-egregore 0.21.6 → 0.22.0
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/assets/egregore-launcher.js +58 -6
- package/assets/egregore-launcher.sh +15 -2
- package/lib/claude-runtime.js +2 -0
- package/lib/codex-runtime.js +2 -0
- package/lib/setup.js +20 -3
- package/package.json +2 -2
- package/runtime/claude/.claude/context/commit-format.md +7 -4
- package/runtime/claude/.claude/context/handoff-claiming.md +32 -0
- package/runtime/claude/.claude/context/notification-consent.md +12 -9
- package/runtime/claude/.claude/hooks/boundary-check.sh +14 -331
- package/runtime/claude/.claude/hooks/memory-read-guard.sh +9 -1
- package/runtime/claude/.claude/rules/voice-bedrock.md +6 -2
- package/runtime/claude/.claude/settings.json +1 -1
- package/runtime/claude/.claude/skills/commit/SKILL.md +5 -3
- package/runtime/claude/.claude/skills/dark-mode/SKILL.md +48 -369
- package/runtime/claude/.claude/skills/records/SKILL.md +158 -0
- package/runtime/claude/.claude/skills/reflect/SKILL.md +50 -21
- package/runtime/claude/.claude/skills/review-pr/SKILL.md +4 -2
- package/runtime/claude/.claude/skills/save/SKILL.md +9 -3
- package/runtime/claude/.claude/skills/summon/SKILL.md +138 -207
- package/runtime/claude/.claude/skills/tui-design/SKILL.md +1 -1
- package/runtime/claude/.claude/skills/update/SKILL.md +1 -1
- package/runtime/claude/.claude/skills/view/SKILL.md +52 -16
- package/runtime/claude/.claude/skills/view/compose-scaffold.html +19 -13
- package/runtime/claude/CLAUDE.md +59 -95
- package/runtime/claude/bin/agent.sh +231 -7
- package/runtime/claude/bin/api-call.sh +44 -23
- package/runtime/claude/bin/artifact-register.sh +7 -1
- package/runtime/claude/bin/artifact-writeback.sh +1 -1
- package/runtime/claude/bin/artifacts.sh +66 -265
- package/runtime/claude/bin/attendant.sh +4 -1
- package/runtime/claude/bin/boundary-guard.cjs +1848 -0
- package/runtime/claude/bin/boundary-hook.sh +41 -0
- package/runtime/claude/bin/boundary.sh +15 -155
- package/runtime/claude/bin/capture-run.sh +32 -2
- package/runtime/claude/bin/codex-render-spec.mjs +24 -46
- package/runtime/claude/bin/codex-session-start.sh +19 -0
- package/runtime/claude/bin/codex-skill-render.mjs +18 -0
- package/runtime/claude/bin/config-get.sh +10 -1
- package/runtime/claude/bin/connect-refresh.sh +16 -0
- package/runtime/claude/bin/corpus_map.py +5 -0
- package/runtime/claude/bin/data/capabilities/claude.json +1480 -0
- package/runtime/claude/bin/genui.sh +19 -0
- package/runtime/claude/bin/graph-batch.sh +1 -1
- package/runtime/claude/bin/graph-op.sh +14 -0
- package/runtime/claude/bin/graph.sh +1 -1
- package/runtime/claude/bin/handoff-pr-backfill.sh +1 -1
- package/runtime/claude/bin/handoff-run.sh +11 -6
- package/runtime/claude/bin/ingest-item.sh +1 -1
- package/runtime/claude/bin/ingest-surface.html +22 -4
- package/runtime/claude/bin/issue.sh +1 -1
- package/runtime/claude/bin/knowledge.sh +1 -1
- package/runtime/claude/bin/lib/card-delivery.sh +329 -0
- package/runtime/claude/bin/lib/config.sh +148 -3
- package/runtime/claude/bin/lib/context.sh +38 -67
- package/runtime/claude/bin/lib/genui-entry.py +36 -0
- package/runtime/claude/bin/lib/git-message.sh +25 -6
- package/runtime/claude/bin/lib/git-sync.sh +12 -4
- package/runtime/claude/bin/lib/greeting.sh +255 -28
- package/runtime/claude/bin/lib/handoff_meta.py +294 -0
- package/runtime/claude/bin/lib/health-footer.sh +83 -2
- package/runtime/claude/bin/lib/identity.sh +31 -10
- package/runtime/claude/bin/lib/instance-registry.sh +71 -0
- package/runtime/claude/bin/lib/local_ci_receipt.py +249 -0
- package/runtime/claude/bin/lib/metrics.sh +121 -8
- package/runtime/claude/bin/lib/notices.sh +36 -0
- package/runtime/claude/bin/lib/record_outcomes.py +729 -0
- package/runtime/claude/bin/lib/reflexes.py +267 -0
- package/runtime/claude/bin/lib/retrieval_scorecard.py +624 -0
- package/runtime/claude/bin/lib/session-record-prompt.txt +18 -0
- package/runtime/claude/bin/lib/session_record.py +3747 -0
- package/runtime/claude/bin/lib/session_trail.py +1983 -0
- package/runtime/claude/bin/lib/settings-drift.sh +8 -5
- package/runtime/claude/bin/lib/startup-restart.sh +41 -0
- package/runtime/claude/bin/lib/worktree-links.sh +92 -0
- package/runtime/claude/bin/memory-push.sh +6 -0
- package/runtime/claude/bin/merge-gate.sh +498 -0
- package/runtime/claude/bin/notify.sh +5 -2
- package/runtime/claude/bin/page-access.sh +103 -0
- package/runtime/claude/bin/person.sh +13 -2
- package/runtime/claude/bin/pi-render-spec.mjs +0 -1
- package/runtime/claude/bin/pi-spirit-discovery.mjs +34 -0
- package/runtime/claude/bin/post-update.sh +181 -0
- package/runtime/claude/bin/prime-render-spec.mjs +0 -1
- package/runtime/claude/bin/publish-artifact.sh +89 -42
- package/runtime/claude/bin/qa-inventory.sh +2 -2
- package/runtime/claude/bin/quest.sh +1 -1
- package/runtime/claude/bin/records.sh +11 -0
- package/runtime/claude/bin/reflexes/05-vitals.sh +9 -0
- package/runtime/claude/bin/reflexes/10-open-items.sh +12 -0
- package/runtime/claude/bin/reflexes/15-retrieval.sh +8 -0
- package/runtime/claude/bin/reflexes/20-nightly.sh +100 -0
- package/runtime/claude/bin/reflexes/20-sandbox.sh +85 -0
- package/runtime/claude/bin/reflexes/30-cost.py +116 -0
- package/runtime/claude/bin/reflexes/40-shells.py +51 -0
- package/runtime/claude/bin/reflexes/README.md +61 -0
- package/runtime/claude/bin/reflexes.sh +7 -0
- package/runtime/claude/bin/runtime-identity.sh +140 -42
- package/runtime/claude/bin/screenshot.sh +77 -0
- package/runtime/claude/bin/scroll.sh +1 -1
- package/runtime/claude/bin/search.sh +31 -5
- package/runtime/claude/bin/session-autosave.sh +6 -3
- package/runtime/claude/bin/session-id.sh +147 -0
- package/runtime/claude/bin/session-log.sh +19 -0
- package/runtime/claude/bin/session-record.sh +26 -0
- package/runtime/claude/bin/session-start.sh +121 -151
- package/runtime/claude/bin/settings.sh +13 -8
- package/runtime/claude/bin/spirit-discovery-claude.sh +4 -0
- package/runtime/claude/bin/spirit-discovery-codex.sh +4 -0
- package/runtime/claude/bin/spirit-discovery-pi.sh +4 -0
- package/runtime/claude/bin/spirit-discovery-prime.sh +4 -0
- package/runtime/claude/bin/spirit-discovery.sh +4 -0
- package/runtime/claude/bin/startup-check.sh +1 -1
- package/runtime/claude/bin/telemetry.sh +11 -4
- package/runtime/claude/bin/test-baseline.sh +33 -5
- package/runtime/claude/bin/todo.sh +1 -1
- package/runtime/claude/bin/worktree-create.sh +15 -45
- package/runtime/claude/bin/worktree.sh +6 -0
- package/runtime/claude/docs/spirit-discovery.md +96 -0
- package/runtime/claude/egregore_runtime/__init__.py +18 -12
- package/runtime/claude/egregore_runtime/access.py +608 -0
- package/runtime/claude/egregore_runtime/access_cli.py +190 -0
- package/runtime/claude/egregore_runtime/adapters/invitation.py +5 -3
- package/runtime/claude/egregore_runtime/adapters/qmd.py +521 -47
- package/runtime/claude/egregore_runtime/admin_entry.py +56 -0
- package/runtime/claude/egregore_runtime/artifacts.py +222 -16
- package/runtime/claude/egregore_runtime/contracts.py +27 -0
- package/runtime/claude/egregore_runtime/eligibility.py +5 -12
- package/runtime/claude/egregore_runtime/episode_binding.py +16 -5
- package/runtime/claude/egregore_runtime/github_issues.py +2 -2
- package/runtime/claude/egregore_runtime/handoff_lookup.py +9 -20
- package/runtime/claude/egregore_runtime/harness_cli.py +674 -149
- package/runtime/claude/egregore_runtime/held.py +2436 -0
- package/runtime/claude/egregore_runtime/hosted_api.py +137 -0
- package/runtime/claude/egregore_runtime/identity.py +196 -65
- package/runtime/claude/egregore_runtime/identity_transfer.py +6 -2
- package/runtime/claude/egregore_runtime/ingest_cli.py +2 -2
- package/runtime/claude/egregore_runtime/ingest_index_cli.py +4 -16
- package/runtime/claude/egregore_runtime/installation.py +119 -0
- package/runtime/claude/egregore_runtime/investigation.py +64 -41
- package/runtime/claude/egregore_runtime/investigation_guidance.py +16 -11
- package/runtime/claude/egregore_runtime/issue_cli.py +51 -32
- package/runtime/claude/egregore_runtime/issue_reporting.py +4 -3
- package/runtime/claude/egregore_runtime/issue_triage_service.py +2 -2
- package/runtime/claude/egregore_runtime/issues.py +12 -6
- package/runtime/claude/egregore_runtime/knowledge.py +331 -5
- package/runtime/claude/egregore_runtime/knowledge_cli.py +57 -17
- package/runtime/claude/egregore_runtime/lifecycle.py +88 -22
- package/runtime/claude/egregore_runtime/lifecycle_cli.py +21 -13
- package/runtime/claude/egregore_runtime/line_ids.py +10 -0
- package/runtime/claude/egregore_runtime/memory_push.py +349 -0
- package/runtime/claude/egregore_runtime/observe.py +64 -25
- package/runtime/claude/egregore_runtime/observers.py +19 -0
- package/runtime/claude/egregore_runtime/optional_modules.py +13 -0
- package/runtime/claude/egregore_runtime/outputs_cli.py +321 -0
- package/runtime/claude/egregore_runtime/policy.py +651 -24
- package/runtime/claude/egregore_runtime/private_result.py +2 -1
- package/runtime/claude/egregore_runtime/projection.py +946 -0
- package/runtime/claude/egregore_runtime/quest_cli.py +20 -23
- package/runtime/claude/egregore_runtime/question_cli.py +15 -11
- package/runtime/claude/egregore_runtime/questions.py +10 -10
- package/runtime/claude/egregore_runtime/quests.py +10 -10
- package/runtime/claude/egregore_runtime/record_publish.py +548 -0
- package/runtime/claude/egregore_runtime/record_subject.py +564 -0
- package/runtime/claude/egregore_runtime/research_ingest.py +2 -4
- package/runtime/claude/egregore_runtime/runtime.py +169 -60
- package/runtime/claude/egregore_runtime/sandbox.py +194 -0
- package/runtime/claude/egregore_runtime/scroll_cli.py +22 -32
- package/runtime/claude/egregore_runtime/scrolls.py +23 -15
- package/runtime/claude/egregore_runtime/shadow.py +262 -0
- package/runtime/claude/egregore_runtime/spirit_discovery.py +487 -0
- package/runtime/claude/egregore_runtime/spirit_discovery_cli.py +25 -0
- package/runtime/claude/egregore_runtime/spirit_discovery_transport.py +18 -0
- package/runtime/claude/egregore_runtime/status.py +101 -44
- package/runtime/claude/egregore_runtime/status_cli.py +13 -5
- package/runtime/claude/egregore_runtime/sync.py +39 -21
- package/runtime/claude/egregore_runtime/taste.py +934 -0
- package/runtime/claude/egregore_runtime/taste_cli.py +242 -0
- package/runtime/claude/egregore_runtime/taste_rules.py +91 -0
- package/runtime/claude/egregore_runtime/telemetry.py +26 -3
- package/runtime/claude/egregore_runtime/todo_cli.py +15 -12
- package/runtime/claude/egregore_runtime/todos.py +3 -2
- package/runtime/claude/egregore_runtime/upgrade.py +85 -58
- package/runtime/claude/egregore_runtime/writeback.py +330 -24
- package/runtime/claude/manifest.json +65 -5
- package/runtime/codex/.claude/context/handoff-claiming.md +32 -0
- package/runtime/codex/.claude/context/notification-consent.md +12 -9
- package/runtime/codex/.claude/skills/view/SKILL.md +438 -0
- package/runtime/codex/.codex/hooks.json +11 -0
- package/runtime/codex/.codex/skills/dark-mode/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/records/SKILL.md +29 -0
- package/runtime/codex/.codex/skills/summon/SKILL.md +1 -1
- package/runtime/codex/.codex/skills/view/assets/compose-scaffold.html +19 -13
- package/runtime/codex/.codex/spec-manifest.json +13 -19
- package/runtime/codex/AGENTS.md +54 -105
- package/runtime/codex/bin/agent.sh +231 -7
- package/runtime/codex/bin/api-call.sh +44 -23
- package/runtime/codex/bin/artifact-register.sh +7 -1
- package/runtime/codex/bin/artifact-writeback.sh +1 -1
- package/runtime/codex/bin/artifacts.sh +66 -265
- package/runtime/codex/bin/attendant.sh +4 -1
- package/runtime/codex/bin/boundary-guard.cjs +1848 -0
- package/runtime/codex/bin/boundary-hook.sh +41 -0
- package/runtime/codex/bin/capture-run.sh +32 -2
- package/runtime/codex/bin/codex-render-spec.mjs +24 -46
- package/runtime/codex/bin/codex-session-start.sh +19 -0
- package/runtime/codex/bin/codex-skill-render.mjs +18 -0
- package/runtime/codex/bin/config-get.sh +10 -1
- package/runtime/codex/bin/connect-refresh.sh +16 -0
- package/runtime/codex/bin/corpus_map.py +5 -0
- package/runtime/codex/bin/data/capabilities/codex.json +1458 -0
- package/runtime/codex/bin/genui.sh +19 -0
- package/runtime/codex/bin/graph-batch.sh +1 -1
- package/runtime/codex/bin/graph-op.sh +14 -0
- package/runtime/codex/bin/graph.sh +1 -1
- package/runtime/codex/bin/handoff-pr-backfill.sh +1 -1
- package/runtime/codex/bin/handoff-run.sh +11 -6
- package/runtime/codex/bin/ingest-item.sh +1 -1
- package/runtime/codex/bin/ingest-surface.html +22 -4
- package/runtime/codex/bin/issue.sh +1 -1
- package/runtime/codex/bin/knowledge.sh +1 -1
- package/runtime/codex/bin/lib/card-delivery.sh +329 -0
- package/runtime/codex/bin/lib/config.sh +148 -3
- package/runtime/codex/bin/lib/context.sh +38 -67
- package/runtime/codex/bin/lib/genui-entry.py +36 -0
- package/runtime/codex/bin/lib/git-message.sh +25 -6
- package/runtime/codex/bin/lib/git-sync.sh +12 -4
- package/runtime/codex/bin/lib/greeting.sh +255 -28
- package/runtime/codex/bin/lib/handoff_meta.py +294 -0
- package/runtime/codex/bin/lib/health-footer.sh +83 -2
- package/runtime/codex/bin/lib/identity.sh +31 -10
- package/runtime/codex/bin/lib/instance-registry.sh +71 -0
- package/runtime/codex/bin/lib/local_ci_receipt.py +249 -0
- package/runtime/codex/bin/lib/metrics.sh +121 -8
- package/runtime/codex/bin/lib/notices.sh +36 -0
- package/runtime/codex/bin/lib/record_outcomes.py +729 -0
- package/runtime/codex/bin/lib/reflexes.py +267 -0
- package/runtime/codex/bin/lib/retrieval_scorecard.py +624 -0
- package/runtime/codex/bin/lib/session-record-prompt.txt +18 -0
- package/runtime/codex/bin/lib/session_record.py +3747 -0
- package/runtime/codex/bin/lib/session_trail.py +1983 -0
- package/runtime/codex/bin/lib/settings-drift.sh +8 -5
- package/runtime/codex/bin/lib/startup-restart.sh +41 -0
- package/runtime/codex/bin/lib/worktree-links.sh +92 -0
- package/runtime/codex/bin/memory-push.sh +6 -0
- package/runtime/codex/bin/merge-gate.sh +498 -0
- package/runtime/codex/bin/notify.sh +5 -2
- package/runtime/codex/bin/page-access.sh +103 -0
- package/runtime/codex/bin/person.sh +13 -2
- package/runtime/codex/bin/pi-render-spec.mjs +0 -1
- package/runtime/codex/bin/pi-spirit-discovery.mjs +34 -0
- package/runtime/codex/bin/post-update.sh +181 -0
- package/runtime/codex/bin/prime-render-spec.mjs +0 -1
- package/runtime/codex/bin/publish-artifact.sh +89 -42
- package/runtime/codex/bin/qa-inventory.sh +2 -2
- package/runtime/codex/bin/quest.sh +1 -1
- package/runtime/codex/bin/records.sh +11 -0
- package/runtime/codex/bin/reflexes/05-vitals.sh +9 -0
- package/runtime/codex/bin/reflexes/10-open-items.sh +12 -0
- package/runtime/codex/bin/reflexes/15-retrieval.sh +8 -0
- package/runtime/codex/bin/reflexes/20-nightly.sh +100 -0
- package/runtime/codex/bin/reflexes/20-sandbox.sh +85 -0
- package/runtime/codex/bin/reflexes/30-cost.py +116 -0
- package/runtime/codex/bin/reflexes/40-shells.py +51 -0
- package/runtime/codex/bin/reflexes/README.md +61 -0
- package/runtime/codex/bin/reflexes.sh +7 -0
- package/runtime/codex/bin/runtime-identity.sh +140 -42
- package/runtime/codex/bin/screenshot.sh +77 -0
- package/runtime/codex/bin/scroll.sh +1 -1
- package/runtime/codex/bin/search.sh +31 -5
- package/runtime/codex/bin/session-autosave.sh +6 -3
- package/runtime/codex/bin/session-id.sh +147 -0
- package/runtime/codex/bin/session-log.sh +19 -0
- package/runtime/codex/bin/session-record.sh +26 -0
- package/runtime/codex/bin/settings.sh +13 -8
- package/runtime/codex/bin/spirit-discovery-claude.sh +4 -0
- package/runtime/codex/bin/spirit-discovery-codex.sh +4 -0
- package/runtime/codex/bin/spirit-discovery-pi.sh +4 -0
- package/runtime/codex/bin/spirit-discovery-prime.sh +4 -0
- package/runtime/codex/bin/spirit-discovery.sh +4 -0
- package/runtime/codex/bin/startup-check.sh +1 -1
- package/runtime/codex/bin/telemetry.sh +11 -4
- package/runtime/codex/bin/test-baseline.sh +33 -5
- package/runtime/codex/bin/todo.sh +1 -1
- package/runtime/codex/bin/worktree-create.sh +15 -45
- package/runtime/codex/bin/worktree.sh +6 -0
- package/runtime/codex/docs/spirit-discovery.md +96 -0
- package/runtime/codex/egregore_runtime/__init__.py +18 -12
- package/runtime/codex/egregore_runtime/access.py +608 -0
- package/runtime/codex/egregore_runtime/access_cli.py +190 -0
- package/runtime/codex/egregore_runtime/adapters/invitation.py +5 -3
- package/runtime/codex/egregore_runtime/adapters/qmd.py +521 -47
- package/runtime/codex/egregore_runtime/admin_entry.py +56 -0
- package/runtime/codex/egregore_runtime/artifacts.py +222 -16
- package/runtime/codex/egregore_runtime/contracts.py +27 -0
- package/runtime/codex/egregore_runtime/eligibility.py +5 -12
- package/runtime/codex/egregore_runtime/episode_binding.py +16 -5
- package/runtime/codex/egregore_runtime/github_issues.py +2 -2
- package/runtime/codex/egregore_runtime/handoff_lookup.py +9 -20
- package/runtime/codex/egregore_runtime/harness_cli.py +674 -149
- package/runtime/codex/egregore_runtime/held.py +2436 -0
- package/runtime/codex/egregore_runtime/hosted_api.py +137 -0
- package/runtime/codex/egregore_runtime/identity.py +196 -65
- package/runtime/codex/egregore_runtime/identity_transfer.py +6 -2
- package/runtime/codex/egregore_runtime/ingest_cli.py +2 -2
- package/runtime/codex/egregore_runtime/ingest_index_cli.py +4 -16
- package/runtime/codex/egregore_runtime/installation.py +119 -0
- package/runtime/codex/egregore_runtime/investigation.py +64 -41
- package/runtime/codex/egregore_runtime/investigation_guidance.py +16 -11
- package/runtime/codex/egregore_runtime/issue_cli.py +51 -32
- package/runtime/codex/egregore_runtime/issue_reporting.py +4 -3
- package/runtime/codex/egregore_runtime/issue_triage_service.py +2 -2
- package/runtime/codex/egregore_runtime/issues.py +12 -6
- package/runtime/codex/egregore_runtime/knowledge.py +331 -5
- package/runtime/codex/egregore_runtime/knowledge_cli.py +57 -17
- package/runtime/codex/egregore_runtime/lifecycle.py +88 -22
- package/runtime/codex/egregore_runtime/lifecycle_cli.py +21 -13
- package/runtime/codex/egregore_runtime/line_ids.py +10 -0
- package/runtime/codex/egregore_runtime/memory_push.py +349 -0
- package/runtime/codex/egregore_runtime/observe.py +64 -25
- package/runtime/codex/egregore_runtime/observers.py +19 -0
- package/runtime/codex/egregore_runtime/optional_modules.py +13 -0
- package/runtime/codex/egregore_runtime/outputs_cli.py +321 -0
- package/runtime/codex/egregore_runtime/policy.py +651 -24
- package/runtime/codex/egregore_runtime/private_result.py +2 -1
- package/runtime/codex/egregore_runtime/projection.py +946 -0
- package/runtime/codex/egregore_runtime/quest_cli.py +20 -23
- package/runtime/codex/egregore_runtime/question_cli.py +15 -11
- package/runtime/codex/egregore_runtime/questions.py +10 -10
- package/runtime/codex/egregore_runtime/quests.py +10 -10
- package/runtime/codex/egregore_runtime/record_publish.py +548 -0
- package/runtime/codex/egregore_runtime/record_subject.py +564 -0
- package/runtime/codex/egregore_runtime/research_ingest.py +2 -4
- package/runtime/codex/egregore_runtime/runtime.py +169 -60
- package/runtime/codex/egregore_runtime/sandbox.py +194 -0
- package/runtime/codex/egregore_runtime/scroll_cli.py +22 -32
- package/runtime/codex/egregore_runtime/scrolls.py +23 -15
- package/runtime/codex/egregore_runtime/shadow.py +262 -0
- package/runtime/codex/egregore_runtime/spirit_discovery.py +487 -0
- package/runtime/codex/egregore_runtime/spirit_discovery_cli.py +25 -0
- package/runtime/codex/egregore_runtime/spirit_discovery_transport.py +18 -0
- package/runtime/codex/egregore_runtime/status.py +101 -44
- package/runtime/codex/egregore_runtime/status_cli.py +13 -5
- package/runtime/codex/egregore_runtime/sync.py +39 -21
- package/runtime/codex/egregore_runtime/taste.py +934 -0
- package/runtime/codex/egregore_runtime/taste_cli.py +242 -0
- package/runtime/codex/egregore_runtime/taste_rules.py +91 -0
- package/runtime/codex/egregore_runtime/telemetry.py +26 -3
- package/runtime/codex/egregore_runtime/todo_cli.py +15 -12
- package/runtime/codex/egregore_runtime/todos.py +3 -2
- package/runtime/codex/egregore_runtime/upgrade.py +85 -58
- package/runtime/codex/egregore_runtime/writeback.py +330 -24
- package/runtime/codex/manifest.json +66 -5
- package/runtime/pi/.claude/context/handoff-claiming.md +32 -0
- package/runtime/pi/.claude/context/notification-consent.md +12 -9
- package/runtime/pi/.claude/skills/view/SKILL.md +438 -0
- package/runtime/pi/.codex/hooks.json +11 -0
- package/runtime/pi/.codex/skills/dark-mode/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/records/SKILL.md +29 -0
- package/runtime/pi/.codex/skills/summon/SKILL.md +1 -1
- package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +19 -13
- package/runtime/pi/.codex/spec-manifest.json +13 -19
- package/runtime/pi/.pi/APPEND_SYSTEM.md +53 -99
- package/runtime/pi/.pi/extensions/egregore.ts +13 -1
- package/runtime/pi/.pi/spec-manifest.json +2 -2
- package/runtime/pi/AGENTS.md +54 -105
- package/runtime/pi/bin/agent.sh +231 -7
- package/runtime/pi/bin/api-call.sh +44 -23
- package/runtime/pi/bin/artifact-register.sh +7 -1
- package/runtime/pi/bin/artifact-writeback.sh +1 -1
- package/runtime/pi/bin/artifacts.sh +66 -265
- package/runtime/pi/bin/attendant.sh +4 -1
- package/runtime/pi/bin/boundary-guard.cjs +1848 -0
- package/runtime/pi/bin/boundary-hook.sh +41 -0
- package/runtime/pi/bin/capture-run.sh +32 -2
- package/runtime/pi/bin/codex-render-spec.mjs +24 -46
- package/runtime/pi/bin/codex-session-start.sh +19 -0
- package/runtime/pi/bin/codex-skill-render.mjs +18 -0
- package/runtime/pi/bin/config-get.sh +10 -1
- package/runtime/pi/bin/connect-refresh.sh +16 -0
- package/runtime/pi/bin/corpus_map.py +5 -0
- package/runtime/pi/bin/data/capabilities/pi.json +1458 -0
- package/runtime/pi/bin/genui.sh +19 -0
- package/runtime/pi/bin/graph-batch.sh +1 -1
- package/runtime/pi/bin/graph-op.sh +14 -0
- package/runtime/pi/bin/graph.sh +1 -1
- package/runtime/pi/bin/handoff-pr-backfill.sh +1 -1
- package/runtime/pi/bin/handoff-run.sh +11 -6
- package/runtime/pi/bin/ingest-item.sh +1 -1
- package/runtime/pi/bin/ingest-surface.html +22 -4
- package/runtime/pi/bin/issue.sh +1 -1
- package/runtime/pi/bin/knowledge.sh +1 -1
- package/runtime/pi/bin/lib/card-delivery.sh +329 -0
- package/runtime/pi/bin/lib/config.sh +148 -3
- package/runtime/pi/bin/lib/context.sh +38 -67
- package/runtime/pi/bin/lib/genui-entry.py +36 -0
- package/runtime/pi/bin/lib/git-message.sh +25 -6
- package/runtime/pi/bin/lib/git-sync.sh +12 -4
- package/runtime/pi/bin/lib/greeting.sh +255 -28
- package/runtime/pi/bin/lib/handoff_meta.py +294 -0
- package/runtime/pi/bin/lib/health-footer.sh +83 -2
- package/runtime/pi/bin/lib/identity.sh +31 -10
- package/runtime/pi/bin/lib/instance-registry.sh +71 -0
- package/runtime/pi/bin/lib/local_ci_receipt.py +249 -0
- package/runtime/pi/bin/lib/metrics.sh +121 -8
- package/runtime/pi/bin/lib/notices.sh +36 -0
- package/runtime/pi/bin/lib/record_outcomes.py +729 -0
- package/runtime/pi/bin/lib/reflexes.py +267 -0
- package/runtime/pi/bin/lib/retrieval_scorecard.py +624 -0
- package/runtime/pi/bin/lib/session-record-prompt.txt +18 -0
- package/runtime/pi/bin/lib/session_record.py +3747 -0
- package/runtime/pi/bin/lib/session_trail.py +1983 -0
- package/runtime/pi/bin/lib/settings-drift.sh +8 -5
- package/runtime/pi/bin/lib/startup-restart.sh +41 -0
- package/runtime/pi/bin/lib/worktree-links.sh +92 -0
- package/runtime/pi/bin/memory-push.sh +6 -0
- package/runtime/pi/bin/merge-gate.sh +498 -0
- package/runtime/pi/bin/notify.sh +5 -2
- package/runtime/pi/bin/page-access.sh +103 -0
- package/runtime/pi/bin/person.sh +13 -2
- package/runtime/pi/bin/pi-branch-policy.mjs +37 -0
- package/runtime/pi/bin/pi-observe-context.mjs +97 -4
- package/runtime/pi/bin/pi-render-spec.mjs +0 -1
- package/runtime/pi/bin/pi-session-start.sh +4 -1
- package/runtime/pi/bin/pi-spirit-discovery.mjs +34 -0
- package/runtime/pi/bin/post-update.sh +181 -0
- package/runtime/pi/bin/prime-render-spec.mjs +0 -1
- package/runtime/pi/bin/publish-artifact.sh +89 -42
- package/runtime/pi/bin/qa-inventory.sh +2 -2
- package/runtime/pi/bin/quest.sh +1 -1
- package/runtime/pi/bin/records.sh +11 -0
- package/runtime/pi/bin/reflexes/05-vitals.sh +9 -0
- package/runtime/pi/bin/reflexes/10-open-items.sh +12 -0
- package/runtime/pi/bin/reflexes/15-retrieval.sh +8 -0
- package/runtime/pi/bin/reflexes/20-nightly.sh +100 -0
- package/runtime/pi/bin/reflexes/20-sandbox.sh +85 -0
- package/runtime/pi/bin/reflexes/30-cost.py +116 -0
- package/runtime/pi/bin/reflexes/40-shells.py +51 -0
- package/runtime/pi/bin/reflexes/README.md +61 -0
- package/runtime/pi/bin/reflexes.sh +7 -0
- package/runtime/pi/bin/runtime-identity.sh +140 -42
- package/runtime/pi/bin/screenshot.sh +77 -0
- package/runtime/pi/bin/scroll.sh +1 -1
- package/runtime/pi/bin/search.sh +31 -5
- package/runtime/pi/bin/session-autosave.sh +6 -3
- package/runtime/pi/bin/session-id.sh +147 -0
- package/runtime/pi/bin/session-log.sh +19 -0
- package/runtime/pi/bin/session-record.sh +26 -0
- package/runtime/pi/bin/settings.sh +13 -8
- package/runtime/pi/bin/spirit-discovery-claude.sh +4 -0
- package/runtime/pi/bin/spirit-discovery-codex.sh +4 -0
- package/runtime/pi/bin/spirit-discovery-pi.sh +4 -0
- package/runtime/pi/bin/spirit-discovery-prime.sh +4 -0
- package/runtime/pi/bin/spirit-discovery.sh +4 -0
- package/runtime/pi/bin/startup-check.sh +1 -1
- package/runtime/pi/bin/telemetry.sh +11 -4
- package/runtime/pi/bin/test-baseline.sh +33 -5
- package/runtime/pi/bin/todo.sh +1 -1
- package/runtime/pi/bin/worktree-create.sh +15 -45
- package/runtime/pi/bin/worktree.sh +6 -0
- package/runtime/pi/docs/spirit-discovery.md +96 -0
- package/runtime/pi/egregore_runtime/__init__.py +18 -12
- package/runtime/pi/egregore_runtime/access.py +608 -0
- package/runtime/pi/egregore_runtime/access_cli.py +190 -0
- package/runtime/pi/egregore_runtime/adapters/invitation.py +5 -3
- package/runtime/pi/egregore_runtime/adapters/qmd.py +521 -47
- package/runtime/pi/egregore_runtime/admin_entry.py +56 -0
- package/runtime/pi/egregore_runtime/artifacts.py +222 -16
- package/runtime/pi/egregore_runtime/contracts.py +27 -0
- package/runtime/pi/egregore_runtime/eligibility.py +5 -12
- package/runtime/pi/egregore_runtime/episode_binding.py +16 -5
- package/runtime/pi/egregore_runtime/github_issues.py +2 -2
- package/runtime/pi/egregore_runtime/handoff_lookup.py +9 -20
- package/runtime/pi/egregore_runtime/harness_cli.py +674 -149
- package/runtime/pi/egregore_runtime/held.py +2436 -0
- package/runtime/pi/egregore_runtime/hosted_api.py +137 -0
- package/runtime/pi/egregore_runtime/identity.py +196 -65
- package/runtime/pi/egregore_runtime/identity_transfer.py +6 -2
- package/runtime/pi/egregore_runtime/ingest_cli.py +2 -2
- package/runtime/pi/egregore_runtime/ingest_index_cli.py +4 -16
- package/runtime/pi/egregore_runtime/installation.py +119 -0
- package/runtime/pi/egregore_runtime/investigation.py +64 -41
- package/runtime/pi/egregore_runtime/investigation_guidance.py +16 -11
- package/runtime/pi/egregore_runtime/issue_cli.py +51 -32
- package/runtime/pi/egregore_runtime/issue_reporting.py +4 -3
- package/runtime/pi/egregore_runtime/issue_triage_service.py +2 -2
- package/runtime/pi/egregore_runtime/issues.py +12 -6
- package/runtime/pi/egregore_runtime/knowledge.py +331 -5
- package/runtime/pi/egregore_runtime/knowledge_cli.py +57 -17
- package/runtime/pi/egregore_runtime/lifecycle.py +88 -22
- package/runtime/pi/egregore_runtime/lifecycle_cli.py +21 -13
- package/runtime/pi/egregore_runtime/line_ids.py +10 -0
- package/runtime/pi/egregore_runtime/memory_push.py +349 -0
- package/runtime/pi/egregore_runtime/observe.py +64 -25
- package/runtime/pi/egregore_runtime/observers.py +19 -0
- package/runtime/pi/egregore_runtime/optional_modules.py +13 -0
- package/runtime/pi/egregore_runtime/outputs_cli.py +321 -0
- package/runtime/pi/egregore_runtime/policy.py +651 -24
- package/runtime/pi/egregore_runtime/private_result.py +2 -1
- package/runtime/pi/egregore_runtime/projection.py +946 -0
- package/runtime/pi/egregore_runtime/quest_cli.py +20 -23
- package/runtime/pi/egregore_runtime/question_cli.py +15 -11
- package/runtime/pi/egregore_runtime/questions.py +10 -10
- package/runtime/pi/egregore_runtime/quests.py +10 -10
- package/runtime/pi/egregore_runtime/record_publish.py +548 -0
- package/runtime/pi/egregore_runtime/record_subject.py +564 -0
- package/runtime/pi/egregore_runtime/research_ingest.py +2 -4
- package/runtime/pi/egregore_runtime/runtime.py +169 -60
- package/runtime/pi/egregore_runtime/sandbox.py +194 -0
- package/runtime/pi/egregore_runtime/scroll_cli.py +22 -32
- package/runtime/pi/egregore_runtime/scrolls.py +23 -15
- package/runtime/pi/egregore_runtime/shadow.py +262 -0
- package/runtime/pi/egregore_runtime/spirit_discovery.py +487 -0
- package/runtime/pi/egregore_runtime/spirit_discovery_cli.py +25 -0
- package/runtime/pi/egregore_runtime/spirit_discovery_transport.py +18 -0
- package/runtime/pi/egregore_runtime/status.py +101 -44
- package/runtime/pi/egregore_runtime/status_cli.py +13 -5
- package/runtime/pi/egregore_runtime/sync.py +39 -21
- package/runtime/pi/egregore_runtime/taste.py +934 -0
- package/runtime/pi/egregore_runtime/taste_cli.py +242 -0
- package/runtime/pi/egregore_runtime/taste_rules.py +91 -0
- package/runtime/pi/egregore_runtime/telemetry.py +26 -3
- package/runtime/pi/egregore_runtime/todo_cli.py +15 -12
- package/runtime/pi/egregore_runtime/todos.py +3 -2
- package/runtime/pi/egregore_runtime/upgrade.py +85 -58
- package/runtime/pi/egregore_runtime/writeback.py +330 -24
- package/runtime/pi/manifest.json +66 -5
- package/runtime/prime/.claude/context/handoff-claiming.md +32 -0
- package/runtime/prime/.claude/context/notification-consent.md +12 -9
- package/runtime/prime/.claude/skills/view/SKILL.md +438 -0
- package/runtime/prime/.codex/hooks.json +11 -0
- package/runtime/prime/.codex/skills/dark-mode/SKILL.md +1 -1
- package/runtime/prime/.codex/skills/records/SKILL.md +29 -0
- package/runtime/prime/.codex/skills/summon/SKILL.md +1 -1
- package/runtime/prime/.codex/skills/view/assets/compose-scaffold.html +19 -13
- package/runtime/prime/.codex/spec-manifest.json +13 -19
- package/runtime/prime/.prime/agent/APPEND_SYSTEM.md +53 -99
- package/runtime/prime/.prime/agent/extensions/egregore.ts +14 -1
- package/runtime/prime/.prime/agent/skills/egregore/SKILL.md +14 -6
- package/runtime/prime/.prime/agent/skills/egregore/src/egregore/__init__.py +66 -1
- package/runtime/prime/.prime/agent/spec-manifest.json +2 -2
- package/runtime/prime/AGENTS.md +54 -105
- package/runtime/prime/bin/agent.sh +231 -7
- package/runtime/prime/bin/api-call.sh +44 -23
- package/runtime/prime/bin/artifact-register.sh +7 -1
- package/runtime/prime/bin/artifact-writeback.sh +1 -1
- package/runtime/prime/bin/artifacts.sh +66 -265
- package/runtime/prime/bin/attendant.sh +4 -1
- package/runtime/prime/bin/boundary-guard.cjs +1848 -0
- package/runtime/prime/bin/boundary-hook.sh +41 -0
- package/runtime/prime/bin/capture-run.sh +32 -2
- package/runtime/prime/bin/codex-render-spec.mjs +24 -46
- package/runtime/prime/bin/codex-session-start.sh +19 -0
- package/runtime/prime/bin/codex-skill-render.mjs +18 -0
- package/runtime/prime/bin/config-get.sh +10 -1
- package/runtime/prime/bin/connect-refresh.sh +16 -0
- package/runtime/prime/bin/corpus_map.py +5 -0
- package/runtime/prime/bin/data/capabilities/prime.json +1458 -0
- package/runtime/prime/bin/genui.sh +19 -0
- package/runtime/prime/bin/graph-batch.sh +1 -1
- package/runtime/prime/bin/graph-op.sh +14 -0
- package/runtime/prime/bin/graph.sh +1 -1
- package/runtime/prime/bin/handoff-pr-backfill.sh +1 -1
- package/runtime/prime/bin/handoff-run.sh +11 -6
- package/runtime/prime/bin/ingest-item.sh +1 -1
- package/runtime/prime/bin/ingest-surface.html +22 -4
- package/runtime/prime/bin/issue.sh +1 -1
- package/runtime/prime/bin/knowledge.sh +1 -1
- package/runtime/prime/bin/lib/card-delivery.sh +329 -0
- package/runtime/prime/bin/lib/config.sh +148 -3
- package/runtime/prime/bin/lib/context.sh +38 -67
- package/runtime/prime/bin/lib/genui-entry.py +36 -0
- package/runtime/prime/bin/lib/git-message.sh +25 -6
- package/runtime/prime/bin/lib/git-sync.sh +12 -4
- package/runtime/prime/bin/lib/greeting.sh +255 -28
- package/runtime/prime/bin/lib/handoff_meta.py +294 -0
- package/runtime/prime/bin/lib/health-footer.sh +83 -2
- package/runtime/prime/bin/lib/identity.sh +31 -10
- package/runtime/prime/bin/lib/instance-registry.sh +71 -0
- package/runtime/prime/bin/lib/local_ci_receipt.py +249 -0
- package/runtime/prime/bin/lib/metrics.sh +121 -8
- package/runtime/prime/bin/lib/notices.sh +36 -0
- package/runtime/prime/bin/lib/record_outcomes.py +729 -0
- package/runtime/prime/bin/lib/reflexes.py +267 -0
- package/runtime/prime/bin/lib/retrieval_scorecard.py +624 -0
- package/runtime/prime/bin/lib/session-record-prompt.txt +18 -0
- package/runtime/prime/bin/lib/session_record.py +3747 -0
- package/runtime/prime/bin/lib/session_trail.py +1983 -0
- package/runtime/prime/bin/lib/settings-drift.sh +8 -5
- package/runtime/prime/bin/lib/startup-restart.sh +41 -0
- package/runtime/prime/bin/lib/worktree-links.sh +92 -0
- package/runtime/prime/bin/memory-push.sh +6 -0
- package/runtime/prime/bin/merge-gate.sh +498 -0
- package/runtime/prime/bin/notify.sh +5 -2
- package/runtime/prime/bin/page-access.sh +103 -0
- package/runtime/prime/bin/person.sh +13 -2
- package/runtime/prime/bin/pi-branch-policy.mjs +37 -0
- package/runtime/prime/bin/pi-observe-context.mjs +97 -4
- package/runtime/prime/bin/pi-render-spec.mjs +0 -1
- package/runtime/prime/bin/pi-spirit-discovery.mjs +34 -0
- package/runtime/prime/bin/post-update.sh +181 -0
- package/runtime/prime/bin/prime-render-spec.mjs +0 -1
- package/runtime/prime/bin/prime-session-start.sh +3 -1
- package/runtime/prime/bin/publish-artifact.sh +89 -42
- package/runtime/prime/bin/qa-inventory.sh +2 -2
- package/runtime/prime/bin/quest.sh +1 -1
- package/runtime/prime/bin/records.sh +11 -0
- package/runtime/prime/bin/reflexes/05-vitals.sh +9 -0
- package/runtime/prime/bin/reflexes/10-open-items.sh +12 -0
- package/runtime/prime/bin/reflexes/15-retrieval.sh +8 -0
- package/runtime/prime/bin/reflexes/20-nightly.sh +100 -0
- package/runtime/prime/bin/reflexes/20-sandbox.sh +85 -0
- package/runtime/prime/bin/reflexes/30-cost.py +116 -0
- package/runtime/prime/bin/reflexes/40-shells.py +51 -0
- package/runtime/prime/bin/reflexes/README.md +61 -0
- package/runtime/prime/bin/reflexes.sh +7 -0
- package/runtime/prime/bin/runtime-identity.sh +140 -42
- package/runtime/prime/bin/screenshot.sh +77 -0
- package/runtime/prime/bin/scroll.sh +1 -1
- package/runtime/prime/bin/search.sh +31 -5
- package/runtime/prime/bin/session-autosave.sh +6 -3
- package/runtime/prime/bin/session-id.sh +147 -0
- package/runtime/prime/bin/session-log.sh +19 -0
- package/runtime/prime/bin/session-record.sh +26 -0
- package/runtime/prime/bin/settings.sh +13 -8
- package/runtime/prime/bin/spirit-discovery-claude.sh +4 -0
- package/runtime/prime/bin/spirit-discovery-codex.sh +4 -0
- package/runtime/prime/bin/spirit-discovery-pi.sh +4 -0
- package/runtime/prime/bin/spirit-discovery-prime.sh +4 -0
- package/runtime/prime/bin/spirit-discovery.sh +4 -0
- package/runtime/prime/bin/startup-check.sh +1 -1
- package/runtime/prime/bin/telemetry.sh +11 -4
- package/runtime/prime/bin/test-baseline.sh +33 -5
- package/runtime/prime/bin/todo.sh +1 -1
- package/runtime/prime/bin/worktree-create.sh +15 -45
- package/runtime/prime/bin/worktree.sh +6 -0
- package/runtime/prime/docs/spirit-discovery.md +96 -0
- package/runtime/prime/egregore_runtime/__init__.py +18 -12
- package/runtime/prime/egregore_runtime/access.py +608 -0
- package/runtime/prime/egregore_runtime/access_cli.py +190 -0
- package/runtime/prime/egregore_runtime/adapters/invitation.py +5 -3
- package/runtime/prime/egregore_runtime/adapters/qmd.py +521 -47
- package/runtime/prime/egregore_runtime/admin_entry.py +56 -0
- package/runtime/prime/egregore_runtime/artifacts.py +222 -16
- package/runtime/prime/egregore_runtime/contracts.py +27 -0
- package/runtime/prime/egregore_runtime/eligibility.py +5 -12
- package/runtime/prime/egregore_runtime/episode_binding.py +16 -5
- package/runtime/prime/egregore_runtime/github_issues.py +2 -2
- package/runtime/prime/egregore_runtime/handoff_lookup.py +9 -20
- package/runtime/prime/egregore_runtime/harness_cli.py +674 -149
- package/runtime/prime/egregore_runtime/held.py +2436 -0
- package/runtime/prime/egregore_runtime/hosted_api.py +137 -0
- package/runtime/prime/egregore_runtime/identity.py +196 -65
- package/runtime/prime/egregore_runtime/identity_transfer.py +6 -2
- package/runtime/prime/egregore_runtime/ingest_cli.py +2 -2
- package/runtime/prime/egregore_runtime/ingest_index_cli.py +4 -16
- package/runtime/prime/egregore_runtime/installation.py +119 -0
- package/runtime/prime/egregore_runtime/investigation.py +64 -41
- package/runtime/prime/egregore_runtime/investigation_guidance.py +16 -11
- package/runtime/prime/egregore_runtime/issue_cli.py +51 -32
- package/runtime/prime/egregore_runtime/issue_reporting.py +4 -3
- package/runtime/prime/egregore_runtime/issue_triage_service.py +2 -2
- package/runtime/prime/egregore_runtime/issues.py +12 -6
- package/runtime/prime/egregore_runtime/knowledge.py +331 -5
- package/runtime/prime/egregore_runtime/knowledge_cli.py +57 -17
- package/runtime/prime/egregore_runtime/lifecycle.py +88 -22
- package/runtime/prime/egregore_runtime/lifecycle_cli.py +21 -13
- package/runtime/prime/egregore_runtime/line_ids.py +10 -0
- package/runtime/prime/egregore_runtime/memory_push.py +349 -0
- package/runtime/prime/egregore_runtime/observe.py +64 -25
- package/runtime/prime/egregore_runtime/observers.py +19 -0
- package/runtime/prime/egregore_runtime/optional_modules.py +13 -0
- package/runtime/prime/egregore_runtime/outputs_cli.py +321 -0
- package/runtime/prime/egregore_runtime/policy.py +651 -24
- package/runtime/prime/egregore_runtime/private_result.py +2 -1
- package/runtime/prime/egregore_runtime/projection.py +946 -0
- package/runtime/prime/egregore_runtime/quest_cli.py +20 -23
- package/runtime/prime/egregore_runtime/question_cli.py +15 -11
- package/runtime/prime/egregore_runtime/questions.py +10 -10
- package/runtime/prime/egregore_runtime/quests.py +10 -10
- package/runtime/prime/egregore_runtime/record_publish.py +548 -0
- package/runtime/prime/egregore_runtime/record_subject.py +564 -0
- package/runtime/prime/egregore_runtime/research_ingest.py +2 -4
- package/runtime/prime/egregore_runtime/runtime.py +169 -60
- package/runtime/prime/egregore_runtime/sandbox.py +194 -0
- package/runtime/prime/egregore_runtime/scroll_cli.py +22 -32
- package/runtime/prime/egregore_runtime/scrolls.py +23 -15
- package/runtime/prime/egregore_runtime/shadow.py +262 -0
- package/runtime/prime/egregore_runtime/spirit_discovery.py +487 -0
- package/runtime/prime/egregore_runtime/spirit_discovery_cli.py +25 -0
- package/runtime/prime/egregore_runtime/spirit_discovery_transport.py +18 -0
- package/runtime/prime/egregore_runtime/status.py +101 -44
- package/runtime/prime/egregore_runtime/status_cli.py +13 -5
- package/runtime/prime/egregore_runtime/sync.py +39 -21
- package/runtime/prime/egregore_runtime/taste.py +934 -0
- package/runtime/prime/egregore_runtime/taste_cli.py +242 -0
- package/runtime/prime/egregore_runtime/taste_rules.py +91 -0
- package/runtime/prime/egregore_runtime/telemetry.py +26 -3
- package/runtime/prime/egregore_runtime/todo_cli.py +15 -12
- package/runtime/prime/egregore_runtime/todos.py +3 -2
- package/runtime/prime/egregore_runtime/upgrade.py +85 -58
- package/runtime/prime/egregore_runtime/writeback.py +330 -24
- package/runtime/prime/manifest.json +66 -5
- package/runtime/claude/bin/thread.sh +0 -9
- package/runtime/claude/egregore_runtime/admin_gate.py +0 -102
- package/runtime/claude/egregore_runtime/thread_cli.py +0 -173
- package/runtime/claude/egregore_runtime/threads.py +0 -521
- package/runtime/codex/bin/thread.sh +0 -9
- package/runtime/codex/egregore_runtime/admin_gate.py +0 -102
- package/runtime/codex/egregore_runtime/thread_cli.py +0 -173
- package/runtime/codex/egregore_runtime/threads.py +0 -521
- package/runtime/pi/bin/thread.sh +0 -9
- package/runtime/pi/egregore_runtime/admin_gate.py +0 -102
- package/runtime/pi/egregore_runtime/thread_cli.py +0 -173
- package/runtime/pi/egregore_runtime/threads.py +0 -521
- package/runtime/prime/bin/thread.sh +0 -9
- package/runtime/prime/egregore_runtime/admin_gate.py +0 -102
- package/runtime/prime/egregore_runtime/thread_cli.py +0 -173
- package/runtime/prime/egregore_runtime/threads.py +0 -521
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
"""Record what people like in generated interfaces and read it back as words.
|
|
2
|
+
|
|
3
|
+
Only deliberate human acts are recorded: liked, disliked, chose (one interface
|
|
4
|
+
over another), adopted (make this my default) and retracted. Nothing is
|
|
5
|
+
inferred from what a page happened to use, and nothing is scored: the summary
|
|
6
|
+
is plain counts plus the person's own words, dated.
|
|
7
|
+
|
|
8
|
+
Events carry words only. A target is never opened, so no page source or client
|
|
9
|
+
text enters memory. A registry id, hosted URL or claude.ai artifact link is
|
|
10
|
+
stored as given; a local file is stored relative to the repository when it is
|
|
11
|
+
inside it and by its file name otherwise, so shared memory never holds a home
|
|
12
|
+
directory or the folders around it. Each event is one immutable JSON file,
|
|
13
|
+
``memory/taste/<month>/<actor folder>/<time>-<kind>-<event id>.json``, created
|
|
14
|
+
once through the Runtime writeback: a new file cannot be replaced by a rebase,
|
|
15
|
+
and two machines never write the same path. The name carries the kind and id,
|
|
16
|
+
so finding an event, retracting it and resolving defaults read every file by
|
|
17
|
+
name; only the reactions ``taste`` shows are bounded. Search indexes Markdown
|
|
18
|
+
only, so taste adds no search hits.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from datetime import UTC, datetime
|
|
25
|
+
import hashlib
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
from pathlib import Path, PureWindowsPath
|
|
29
|
+
import re
|
|
30
|
+
from typing import Any, Callable, Iterable, Mapping
|
|
31
|
+
from urllib.parse import unquote, urlsplit
|
|
32
|
+
import uuid
|
|
33
|
+
|
|
34
|
+
from .taste_rules import EGREGORE_RULES
|
|
35
|
+
from .artifacts import ArtifactConflictError
|
|
36
|
+
from .contracts import ActorContext, CanonicalFilesWritebackEvent, Permission, PolicyDecision, WritebackStatus
|
|
37
|
+
from .errors import AuthorizationDenied
|
|
38
|
+
from .policy import allows, action_for
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
TASTE_SCHEMA = "egregore.taste/v1"
|
|
42
|
+
SUMMARY_SCHEMA = "egregore.taste-summary/v1"
|
|
43
|
+
EVENT_KINDS = frozenset({"liked", "disliked", "chose", "adopted", "retracted"})
|
|
44
|
+
ADOPT_KINDS = ("palette", "pattern", "principle")
|
|
45
|
+
ADOPT_SCOPES = ("person", "org")
|
|
46
|
+
WHOLE_THING = "the whole thing"
|
|
47
|
+
READING_NOTE = "Words to consider, never a template to copy."
|
|
48
|
+
|
|
49
|
+
MAX_WORDS = 1000
|
|
50
|
+
MAX_TARGET = 2048
|
|
51
|
+
MAX_FIELD = 200
|
|
52
|
+
MAX_ABOUT = 8
|
|
53
|
+
MAX_ABOUT_CHARS = 60
|
|
54
|
+
TOP_PATTERNS = 12
|
|
55
|
+
# Palettes and patterns are defaults (the latest wins); principles are rules
|
|
56
|
+
# that accumulate until someone retracts one.
|
|
57
|
+
DEFAULT_KINDS = ("palette", "pattern")
|
|
58
|
+
HOUSE_RULES = 24
|
|
59
|
+
# A pattern the organization named on this many different pages is part of
|
|
60
|
+
# its emergent taste.
|
|
61
|
+
EMERGENT_PAGES = 2
|
|
62
|
+
LAYER_NOTE = (
|
|
63
|
+
"Read in this order; a later layer wins where they conflict, and the must-pass "
|
|
64
|
+
"checks (contrast, accessibility, colours from tokens) always hold."
|
|
65
|
+
)
|
|
66
|
+
LAST_QUOTES = 8
|
|
67
|
+
LAST_CHOICES = 8
|
|
68
|
+
QUOTE_CHARS = 240
|
|
69
|
+
MAX_EVENT_FILES = 10000 # reactions ``taste`` shows; lifecycle and shared ids read every file
|
|
70
|
+
MAX_EVENT_BYTES = 64 * 1024
|
|
71
|
+
LIFECYCLE_KINDS = frozenset({"adopted", "retracted"})
|
|
72
|
+
TEXT_FIELDS = ("words", "region", "task", "audience", "form", "scope", "runtime")
|
|
73
|
+
_EVENT_NAME = re.compile(
|
|
74
|
+
r"(?P<time>\d{8}T\d{6}Z)-(?P<kind>liked|disliked|chose|adopted|retracted)-"
|
|
75
|
+
r"(?P<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.json"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
_RUNTIMES = {"claude": "claude", "claude-code": "claude", "codex": "codex", "pi": "pi"}
|
|
79
|
+
_COMMIT_MESSAGES = {
|
|
80
|
+
"liked": "docs(taste): record a liked interface",
|
|
81
|
+
"disliked": "docs(taste): record a disliked interface",
|
|
82
|
+
"chose": "docs(taste): record an interface choice",
|
|
83
|
+
"adopted": "docs(taste): record an adopted default",
|
|
84
|
+
"retracted": "docs(taste): retract a taste event",
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TasteError(ValueError):
|
|
89
|
+
"""A taste event or query is invalid."""
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _slug(value: str) -> str:
|
|
93
|
+
return re.sub(r"[^a-z0-9]+", "-", str(value).casefold()).strip("-")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _iso(moment: datetime) -> str:
|
|
97
|
+
return moment.astimezone(UTC).isoformat(timespec="seconds").replace("+00:00", "Z")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _parse_ts(value: str) -> datetime:
|
|
101
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(UTC)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def actor_folder(actor_id: str) -> str:
|
|
105
|
+
"""The folder holding one actor's events, named from the Runtime actor id.
|
|
106
|
+
|
|
107
|
+
An id the Runtime issues (lowercase letters, digits and '_') is used as it
|
|
108
|
+
is. Any other id becomes a slug plus a digest of the exact id, so ids that
|
|
109
|
+
slug alike ("Renç", "Ren") never share a folder, even on a disk that
|
|
110
|
+
ignores case; the '-' keeps the two forms apart.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
if re.fullmatch(r"[a-z0-9_]{1,64}", actor_id):
|
|
114
|
+
return actor_id
|
|
115
|
+
digest = hashlib.sha256(actor_id.encode("utf-8")).hexdigest()[:16]
|
|
116
|
+
return f"{_slug(actor_id)[:40] or 'actor'}-{digest}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _serialize(event: Mapping[str, Any]) -> str:
|
|
120
|
+
return json.dumps(event, ensure_ascii=False, sort_keys=True, indent=2) + "\n"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _reaction(event: Mapping[str, Any]) -> dict[str, Any]:
|
|
124
|
+
"""What a retry must repeat: the event without when, where and by whom."""
|
|
125
|
+
|
|
126
|
+
return {key: value for key, value in event.items() if key not in {"ts", "actor", "runtime"}}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def runtime_name(harness: str, environ: Mapping[str, str] | None = None) -> str:
|
|
130
|
+
"""Name the runtime that recorded an event: claude, codex, pi or unknown."""
|
|
131
|
+
|
|
132
|
+
known = _RUNTIMES.get(str(harness or "").strip().casefold())
|
|
133
|
+
if known:
|
|
134
|
+
return known
|
|
135
|
+
environment = os.environ if environ is None else environ
|
|
136
|
+
if environment.get("CLAUDECODE") == "1":
|
|
137
|
+
return "claude"
|
|
138
|
+
if environment.get("CODEX_THREAD_ID"):
|
|
139
|
+
return "codex"
|
|
140
|
+
return "unknown"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def target_kind(ref: str) -> str:
|
|
144
|
+
"""Classify a target as given, without opening it."""
|
|
145
|
+
|
|
146
|
+
if ref.casefold().startswith(("http://", "https://")):
|
|
147
|
+
parts = urlsplit(ref)
|
|
148
|
+
host = (parts.hostname or "").casefold()
|
|
149
|
+
if (host == "claude.ai" or host.endswith(".claude.ai")) and "/artifact" in parts.path:
|
|
150
|
+
return "claude-artifact"
|
|
151
|
+
return "url"
|
|
152
|
+
if "/" in ref or "\\" in ref or ref.startswith((".", "~")) or re.search(r"\.[A-Za-z0-9]{1,5}$", ref):
|
|
153
|
+
return "path"
|
|
154
|
+
return "registry"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def clean_target(ref: str | None, *, label: str = "target") -> str:
|
|
158
|
+
value = (ref or "").strip()
|
|
159
|
+
if not value:
|
|
160
|
+
raise TasteError(
|
|
161
|
+
f"{label} is required: a registry id, hosted URL, local path or claude.ai artifact link"
|
|
162
|
+
)
|
|
163
|
+
# A target names the interface. Markup, line breaks or a very long value
|
|
164
|
+
# mean page contents were passed instead, and those never enter memory.
|
|
165
|
+
if len(value) > MAX_TARGET or any(mark in value for mark in ("\n", "\r", "<", ">")):
|
|
166
|
+
raise TasteError(f"{label} must name the interface (id, URL or path), not carry its contents")
|
|
167
|
+
return value
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def shareable_path(ref: str, *, repository_root: Path, cwd: Path) -> str:
|
|
171
|
+
"""A local file as shared memory may hold it: relative to the repository
|
|
172
|
+
when inside it, otherwise its file name alone. A full path would carry an
|
|
173
|
+
OS user name and the client folders around the file."""
|
|
174
|
+
|
|
175
|
+
value = ref
|
|
176
|
+
if value.casefold().startswith("file:"):
|
|
177
|
+
value = unquote(urlsplit(value).path) or value
|
|
178
|
+
windows = PureWindowsPath(value)
|
|
179
|
+
if os.sep == "/" and ("\\" in value or windows.drive):
|
|
180
|
+
if windows.drive or windows.root:
|
|
181
|
+
# C:\x, C:x, \x and \\host\share\x never name a file in this checkout.
|
|
182
|
+
return windows.name or "file"
|
|
183
|
+
value = windows.as_posix() # ..\x is ../x, contained like any relative path
|
|
184
|
+
path = Path(os.path.expanduser(value))
|
|
185
|
+
if not path.is_absolute():
|
|
186
|
+
path = cwd / path
|
|
187
|
+
resolved = path.resolve()
|
|
188
|
+
if resolved.is_relative_to(repository_root):
|
|
189
|
+
return resolved.relative_to(repository_root).as_posix() or "."
|
|
190
|
+
return Path(os.path.normpath(path)).name or "file"
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def clean_words(words: str | None, *, required: bool) -> str | None:
|
|
194
|
+
if words is None or not words.strip():
|
|
195
|
+
if required:
|
|
196
|
+
raise TasteError("--words is required: what the person said, verbatim")
|
|
197
|
+
return None
|
|
198
|
+
if len(words) > MAX_WORDS:
|
|
199
|
+
raise TasteError(f"--words holds what the person said, up to {MAX_WORDS} characters")
|
|
200
|
+
return words.strip()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def clean_about(values: Iterable[str] | None) -> list[str]:
|
|
204
|
+
phrases: list[str] = []
|
|
205
|
+
for raw in values or ():
|
|
206
|
+
phrase = " ".join(str(raw).split()).casefold()
|
|
207
|
+
if not phrase:
|
|
208
|
+
continue
|
|
209
|
+
if len(phrase) > MAX_ABOUT_CHARS:
|
|
210
|
+
raise TasteError(f"--about takes short phrases of {MAX_ABOUT_CHARS} characters or fewer")
|
|
211
|
+
if phrase not in phrases:
|
|
212
|
+
phrases.append(phrase)
|
|
213
|
+
if len(phrases) > MAX_ABOUT:
|
|
214
|
+
raise TasteError(f"name at most {MAX_ABOUT} patterns with --about")
|
|
215
|
+
return phrases
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def clean_field(value: str | None, name: str, *, label: str | None = None) -> str | None:
|
|
219
|
+
"""A short plain description on one line. ``>`` is allowed (a CSS child
|
|
220
|
+
selector, "clarity > decoration"); ``<`` is not, since markup starts there."""
|
|
221
|
+
|
|
222
|
+
if value is None:
|
|
223
|
+
return None
|
|
224
|
+
text = " ".join(value.split())
|
|
225
|
+
if not text:
|
|
226
|
+
return None
|
|
227
|
+
field = label or f"--{name}"
|
|
228
|
+
if "<" in text:
|
|
229
|
+
raise TasteError(f"{field} cannot contain '<': describe the part in words, without markup")
|
|
230
|
+
if len(text) > MAX_FIELD:
|
|
231
|
+
raise TasteError(f"{field} is {len(text)} characters; keep it to {MAX_FIELD} or fewer")
|
|
232
|
+
return text
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _optional_text(value: Any) -> bool:
|
|
236
|
+
return value is None or isinstance(value, str)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _reference(value: Any) -> bool:
|
|
240
|
+
return isinstance(value, dict) and isinstance(value.get("ref"), str) and _optional_text(value.get("ref_kind"))
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def valid_event(value: Any) -> bool:
|
|
244
|
+
"""One taste event whose every field the reader uses has the type it
|
|
245
|
+
expects: JSON that parses but carries a list where a word belongs is
|
|
246
|
+
skipped like a torn file, never left to break a read."""
|
|
247
|
+
|
|
248
|
+
if not (isinstance(value, dict) and value.get("schema") == TASTE_SCHEMA):
|
|
249
|
+
return False
|
|
250
|
+
actor, about = value.get("actor"), value.get("about")
|
|
251
|
+
if not (
|
|
252
|
+
isinstance(value.get("id"), str)
|
|
253
|
+
and isinstance(value.get("kind"), str)
|
|
254
|
+
and value["kind"] in EVENT_KINDS
|
|
255
|
+
and isinstance(actor, dict)
|
|
256
|
+
and isinstance(actor.get("id"), str)
|
|
257
|
+
and _optional_text(actor.get("name"))
|
|
258
|
+
and _reference(value.get("target"))
|
|
259
|
+
and (value.get("over") is None or _reference(value["over"]))
|
|
260
|
+
and (about is None or (isinstance(about, list) and all(isinstance(phrase, str) for phrase in about)))
|
|
261
|
+
and all(_optional_text(value.get(field)) for field in TEXT_FIELDS)
|
|
262
|
+
and isinstance(value.get("ts"), str)
|
|
263
|
+
):
|
|
264
|
+
return False
|
|
265
|
+
try:
|
|
266
|
+
_parse_ts(value["ts"])
|
|
267
|
+
except (ValueError, OverflowError, OSError):
|
|
268
|
+
return False
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass(frozen=True, slots=True)
|
|
273
|
+
class EventFile:
|
|
274
|
+
"""An event file as its name describes it; nothing has been read yet."""
|
|
275
|
+
|
|
276
|
+
path: Path
|
|
277
|
+
relative: str
|
|
278
|
+
time: str
|
|
279
|
+
kind: str
|
|
280
|
+
id: str
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@dataclass(frozen=True, slots=True)
|
|
284
|
+
class TasteReceipt:
|
|
285
|
+
event: Mapping[str, Any]
|
|
286
|
+
duplicate: bool
|
|
287
|
+
writeback: CanonicalFilesWritebackEvent
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def recorded(self) -> bool:
|
|
291
|
+
"""The event is committed to canonical memory on this machine."""
|
|
292
|
+
return (
|
|
293
|
+
self.writeback.status is not WritebackStatus.REJECTED
|
|
294
|
+
and self.writeback.git_revision is not None
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def to_dict(self) -> dict[str, Any]:
|
|
298
|
+
return {
|
|
299
|
+
"event": dict(self.event),
|
|
300
|
+
"duplicate": self.duplicate,
|
|
301
|
+
"writeback": self.writeback.to_dict(),
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class TasteService:
|
|
306
|
+
"""Record taste events through the Runtime and read them back as words."""
|
|
307
|
+
|
|
308
|
+
def __init__(
|
|
309
|
+
self,
|
|
310
|
+
runtime,
|
|
311
|
+
*,
|
|
312
|
+
clock: Callable[[], datetime] | None = None,
|
|
313
|
+
id_factory: Callable[[], str] | None = None,
|
|
314
|
+
environ: Mapping[str, str] | None = None,
|
|
315
|
+
warn: Callable[[str], None] | None = None,
|
|
316
|
+
) -> None:
|
|
317
|
+
if runtime.canonical_root is None:
|
|
318
|
+
raise RuntimeError("taste needs canonical memory")
|
|
319
|
+
self.runtime = runtime
|
|
320
|
+
self.memory_root = Path(runtime.canonical_root).resolve()
|
|
321
|
+
# Local file targets are stored relative to this checkout.
|
|
322
|
+
instance = getattr(runtime, "instance_root", None)
|
|
323
|
+
self.repository_root = Path(instance or Path(runtime.canonical_root).parent).resolve()
|
|
324
|
+
self.clock = clock or (lambda: datetime.now(UTC))
|
|
325
|
+
self.id_factory = id_factory or (lambda: str(uuid.uuid4()))
|
|
326
|
+
self.environ = environ
|
|
327
|
+
self.warn = warn or (lambda _line: None)
|
|
328
|
+
|
|
329
|
+
# --- paths and authorization ------------------------------------------
|
|
330
|
+
|
|
331
|
+
@staticmethod
|
|
332
|
+
def event_path(actor: ActorContext, event: Mapping[str, Any]) -> str:
|
|
333
|
+
"""One file per event, e.g. ``taste/2026-09/actor_ada/20260924T100000Z-liked-<id>.json``."""
|
|
334
|
+
|
|
335
|
+
moment = _parse_ts(event["ts"])
|
|
336
|
+
return (
|
|
337
|
+
f"taste/{moment:%Y-%m}/{actor_folder(actor.actor.actor_id)}/"
|
|
338
|
+
f"{moment:%Y%m%dT%H%M%SZ}-{event['kind']}-{event['id']}.json"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def _require(self, actor: ActorContext, permission: Permission, resource: str, path: str) -> PolicyDecision:
|
|
342
|
+
decision = self.runtime.authorize(actor, permission, (resource,))
|
|
343
|
+
if not decision.allowed:
|
|
344
|
+
raise AuthorizationDenied("; ".join(decision.reasons) or f"taste {permission.value} denied")
|
|
345
|
+
if not allows(decision, path, action_for(decision.permission), root=self.memory_root):
|
|
346
|
+
raise AuthorizationDenied("taste path is outside the actor's authorized scope")
|
|
347
|
+
return decision
|
|
348
|
+
|
|
349
|
+
def files(self, actor: ActorContext, name: str = "*.json") -> list[EventFile]:
|
|
350
|
+
"""Event files whose names match the glob ``name``, oldest first,
|
|
351
|
+
described by their names alone: no file is opened."""
|
|
352
|
+
|
|
353
|
+
decision = self._require(actor, Permission.READ, "taste", "memory/taste")
|
|
354
|
+
root = self.memory_root / "taste"
|
|
355
|
+
found: list[EventFile] = []
|
|
356
|
+
for path in sorted(root.glob(f"*/*/{name}")) if root.is_dir() else ():
|
|
357
|
+
relative = path.relative_to(self.memory_root).as_posix()
|
|
358
|
+
if not allows(decision, f"memory/{relative}", "read", root=self.memory_root):
|
|
359
|
+
continue # each event file passes the predicate, not only its folder
|
|
360
|
+
match = _EVENT_NAME.fullmatch(path.name)
|
|
361
|
+
if match is None:
|
|
362
|
+
self.warn(f"skipped memory/{relative}: not one taste event")
|
|
363
|
+
continue
|
|
364
|
+
found.append(EventFile(path, relative, *match.group("time", "kind", "id")))
|
|
365
|
+
return sorted(found, key=lambda file: (file.time, file.relative))
|
|
366
|
+
|
|
367
|
+
def read(self, files: Iterable[EventFile]) -> list[tuple[str, dict[str, Any]]]:
|
|
368
|
+
"""The events in ``files``, one per id, each with the file it came from.
|
|
369
|
+
|
|
370
|
+
A file is skipped with one warning line when it does not hold one
|
|
371
|
+
valid event of the kind and id its name gives. Copies of one reaction
|
|
372
|
+
under an id (a retry, or the same event from two machines) count once
|
|
373
|
+
silently; different reactions under one id keep the earliest, with a
|
|
374
|
+
warning naming both files.
|
|
375
|
+
"""
|
|
376
|
+
|
|
377
|
+
rows: list[tuple[str, dict[str, Any]]] = []
|
|
378
|
+
for file in files:
|
|
379
|
+
# Read a file only at its own path: a symbolic link to it or to a
|
|
380
|
+
# directory above it would read files outside memory/taste.
|
|
381
|
+
if file.path.resolve() != file.path or not file.path.is_file():
|
|
382
|
+
continue
|
|
383
|
+
try:
|
|
384
|
+
if file.path.stat().st_size > MAX_EVENT_BYTES:
|
|
385
|
+
raise ValueError("too large")
|
|
386
|
+
value = json.loads(file.path.read_text(encoding="utf-8"))
|
|
387
|
+
except (OSError, ValueError, RecursionError):
|
|
388
|
+
value = None
|
|
389
|
+
if valid_event(value) and (value["kind"], value["id"]) == (file.kind, file.id):
|
|
390
|
+
rows.append((file.relative, value))
|
|
391
|
+
else:
|
|
392
|
+
self.warn(f"skipped memory/{file.relative}: not one taste event")
|
|
393
|
+
kept: dict[str, tuple[str, dict[str, Any]]] = {}
|
|
394
|
+
for source, event in sorted(rows, key=lambda row: (row[1]["ts"], row[0])):
|
|
395
|
+
first_source, first = kept.setdefault(event["id"], (source, event))
|
|
396
|
+
if first_source != source and (
|
|
397
|
+
first["actor"]["id"] != event["actor"]["id"] or _reaction(first) != _reaction(event)
|
|
398
|
+
):
|
|
399
|
+
self.warn(
|
|
400
|
+
f"memory/{first_source} and memory/{source} hold different reactions under "
|
|
401
|
+
f"event {event['id']}; counting memory/{first_source}"
|
|
402
|
+
)
|
|
403
|
+
return list(kept.values())
|
|
404
|
+
|
|
405
|
+
def events(self, actor: ActorContext) -> list[tuple[str, dict[str, Any]]]:
|
|
406
|
+
"""The events ``taste`` reads back: every adopted default and every
|
|
407
|
+
retraction, and the latest ``MAX_EVENT_FILES`` reactions. Every id
|
|
408
|
+
more than one file names is resolved first, reading each copy, so the
|
|
409
|
+
bound decides only which words are shown, never which copy counts."""
|
|
410
|
+
|
|
411
|
+
copies: dict[str, list[EventFile]] = {}
|
|
412
|
+
for file in self.files(actor):
|
|
413
|
+
copies.setdefault(file.id, []).append(file)
|
|
414
|
+
shared = [file for files in copies.values() if len(files) > 1 for file in files]
|
|
415
|
+
named = {file.relative: file for file in shared}
|
|
416
|
+
# One entry per event: the file that counts, and its event once read.
|
|
417
|
+
entries: list[tuple[EventFile, dict[str, Any] | None]] = [
|
|
418
|
+
(named[source], event) for source, event in self.read(shared)
|
|
419
|
+
] + [(files[0], None) for files in copies.values() if len(files) == 1]
|
|
420
|
+
reactions = sorted(
|
|
421
|
+
(file for file, _ in entries if file.kind not in LIFECYCLE_KINDS),
|
|
422
|
+
key=lambda file: (file.time, file.relative),
|
|
423
|
+
)
|
|
424
|
+
if len(reactions) > MAX_EVENT_FILES:
|
|
425
|
+
self.warn(f"read the latest {MAX_EVENT_FILES} of {len(reactions)} reaction files")
|
|
426
|
+
hidden = set(reactions[:-MAX_EVENT_FILES])
|
|
427
|
+
entries = [(file, event) for file, event in entries if file not in hidden]
|
|
428
|
+
rows = [(file.relative, event) for file, event in entries if event is not None]
|
|
429
|
+
rows += self.read([file for file, event in entries if event is None])
|
|
430
|
+
return sorted(rows, key=lambda row: (row[1]["ts"], row[0]))
|
|
431
|
+
|
|
432
|
+
def event(self, actor: ActorContext, event_id: str) -> tuple[str, dict[str, Any]] | None:
|
|
433
|
+
"""The event with this id, found by file name among every file."""
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
name = f"*-{uuid.UUID(event_id)}.json"
|
|
437
|
+
except ValueError:
|
|
438
|
+
return None
|
|
439
|
+
rows = self.read(self.files(actor, name))
|
|
440
|
+
return rows[0] if rows else None
|
|
441
|
+
|
|
442
|
+
# --- recording ---------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
def _event(self, actor: ActorContext, kind: str, event_id: str | None) -> dict[str, Any]:
|
|
445
|
+
identifier = (event_id or "").strip() or self.id_factory()
|
|
446
|
+
try:
|
|
447
|
+
identifier = str(uuid.UUID(identifier)) # one spelling, as file names carry it
|
|
448
|
+
except ValueError as exc:
|
|
449
|
+
raise TasteError("event id must be a UUID") from exc
|
|
450
|
+
return {
|
|
451
|
+
"schema": TASTE_SCHEMA,
|
|
452
|
+
"id": identifier,
|
|
453
|
+
"ts": _iso(self.clock()),
|
|
454
|
+
"actor": {"id": actor.actor.actor_id, "name": actor.actor.display_name},
|
|
455
|
+
"kind": kind,
|
|
456
|
+
"runtime": runtime_name(actor.harness, self.environ),
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
def _target(self, ref: str) -> dict[str, str]:
|
|
460
|
+
kind = target_kind(ref)
|
|
461
|
+
if kind == "path":
|
|
462
|
+
ref = shareable_path(ref, repository_root=self.repository_root, cwd=Path.cwd())
|
|
463
|
+
return {"ref": ref, "ref_kind": kind}
|
|
464
|
+
|
|
465
|
+
@staticmethod
|
|
466
|
+
def _context(event: dict[str, Any], **fields: str | None) -> None:
|
|
467
|
+
for name, value in fields.items():
|
|
468
|
+
text = clean_field(value, name)
|
|
469
|
+
if text is not None:
|
|
470
|
+
event[name] = text
|
|
471
|
+
|
|
472
|
+
def _record(self, actor: ActorContext, event: dict[str, Any], *, caller_id: bool) -> TasteReceipt:
|
|
473
|
+
path = self.event_path(actor, event)
|
|
474
|
+
self._require(actor, Permission.WRITE, f"taste:{actor.actor.actor_id}", f"memory/{path}")
|
|
475
|
+
if caller_id:
|
|
476
|
+
# A retry reuses the id its first attempt printed. That attempt may
|
|
477
|
+
# have written its file without committing it, possibly in an
|
|
478
|
+
# earlier month, so the retry commits the file holding the id: the
|
|
479
|
+
# event counts once. A different reaction under that id is refused.
|
|
480
|
+
found = self.event(actor, event["id"])
|
|
481
|
+
if found is not None:
|
|
482
|
+
source, stored = found
|
|
483
|
+
if stored["actor"]["id"] != actor.actor.actor_id:
|
|
484
|
+
raise TasteError("that event id belongs to someone else's event")
|
|
485
|
+
if _reaction(stored) != _reaction(event):
|
|
486
|
+
raise ArtifactConflictError(
|
|
487
|
+
f"event {event['id']} is already recorded as a different reaction "
|
|
488
|
+
f"(memory/{source}); record this one without --id"
|
|
489
|
+
)
|
|
490
|
+
self._require(actor, Permission.WRITE, f"taste:{actor.actor.actor_id}", f"memory/{source}")
|
|
491
|
+
return TasteReceipt(stored, True, self._create(actor, source, stored))
|
|
492
|
+
return TasteReceipt(event, False, self._create(actor, path, event))
|
|
493
|
+
|
|
494
|
+
def _create(self, actor: ActorContext, path: str, event: Mapping[str, Any]) -> CanonicalFilesWritebackEvent:
|
|
495
|
+
# The Runtime's own writeback, as every canonical write takes it.
|
|
496
|
+
return self.runtime._writeback(actor).create_file(
|
|
497
|
+
actor, f"memory/{path}", _serialize(event),
|
|
498
|
+
memory_root=self.memory_root, message=_COMMIT_MESSAGES[event["kind"]],
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
def react(
|
|
502
|
+
self,
|
|
503
|
+
actor: ActorContext,
|
|
504
|
+
kind: str,
|
|
505
|
+
target: str,
|
|
506
|
+
*,
|
|
507
|
+
words: str | None,
|
|
508
|
+
about: Iterable[str] = (),
|
|
509
|
+
region: str | None = None,
|
|
510
|
+
task: str | None = None,
|
|
511
|
+
audience: str | None = None,
|
|
512
|
+
form: str | None = None,
|
|
513
|
+
event_id: str | None = None,
|
|
514
|
+
) -> TasteReceipt:
|
|
515
|
+
if kind not in {"liked", "disliked"}:
|
|
516
|
+
raise TasteError("a reaction is liked or disliked")
|
|
517
|
+
ref = clean_target(target)
|
|
518
|
+
event = self._event(actor, kind, event_id)
|
|
519
|
+
event["target"] = self._target(ref)
|
|
520
|
+
event["words"] = clean_words(words, required=True)
|
|
521
|
+
event["about"] = clean_about(about)
|
|
522
|
+
self._context(event, region=region, task=task, audience=audience, form=form)
|
|
523
|
+
return self._record(actor, event, caller_id=bool(event_id))
|
|
524
|
+
|
|
525
|
+
def choose(
|
|
526
|
+
self,
|
|
527
|
+
actor: ActorContext,
|
|
528
|
+
winner: str,
|
|
529
|
+
loser: str,
|
|
530
|
+
*,
|
|
531
|
+
words: str | None = None,
|
|
532
|
+
about: Iterable[str] = (),
|
|
533
|
+
task: str | None = None,
|
|
534
|
+
audience: str | None = None,
|
|
535
|
+
form: str | None = None,
|
|
536
|
+
event_id: str | None = None,
|
|
537
|
+
) -> TasteReceipt:
|
|
538
|
+
chosen = clean_target(winner, label="chosen interface")
|
|
539
|
+
other = clean_target(loser, label="--over")
|
|
540
|
+
if chosen == other:
|
|
541
|
+
raise TasteError("a choice needs two different interfaces")
|
|
542
|
+
event = self._event(actor, "chose", event_id)
|
|
543
|
+
event["target"] = self._target(chosen)
|
|
544
|
+
event["over"] = self._target(other)
|
|
545
|
+
text = clean_words(words, required=False)
|
|
546
|
+
if text is not None:
|
|
547
|
+
event["words"] = text
|
|
548
|
+
event["about"] = clean_about(about)
|
|
549
|
+
self._context(event, task=task, audience=audience, form=form)
|
|
550
|
+
return self._record(actor, event, caller_id=bool(event_id))
|
|
551
|
+
|
|
552
|
+
def adopt(
|
|
553
|
+
self,
|
|
554
|
+
actor: ActorContext,
|
|
555
|
+
adopt_kind: str,
|
|
556
|
+
value: str,
|
|
557
|
+
*,
|
|
558
|
+
scope: str = "person",
|
|
559
|
+
words: str | None = None,
|
|
560
|
+
event_id: str | None = None,
|
|
561
|
+
) -> TasteReceipt:
|
|
562
|
+
if adopt_kind not in ADOPT_KINDS:
|
|
563
|
+
raise TasteError("adopt a palette, pattern or principle")
|
|
564
|
+
if scope not in ADOPT_SCOPES:
|
|
565
|
+
raise TasteError("--scope is person or org")
|
|
566
|
+
chosen = clean_field(value, adopt_kind, label=f"the {adopt_kind}")
|
|
567
|
+
if chosen is None:
|
|
568
|
+
raise TasteError(f"name the {adopt_kind} to adopt")
|
|
569
|
+
event = self._event(actor, "adopted", event_id)
|
|
570
|
+
event["target"] = {"ref": chosen, "ref_kind": adopt_kind}
|
|
571
|
+
event["scope"] = scope
|
|
572
|
+
text = clean_words(words, required=False)
|
|
573
|
+
if text is not None:
|
|
574
|
+
event["words"] = text
|
|
575
|
+
return self._record(actor, event, caller_id=bool(event_id))
|
|
576
|
+
|
|
577
|
+
def retract(
|
|
578
|
+
self,
|
|
579
|
+
actor: ActorContext,
|
|
580
|
+
target_id: str,
|
|
581
|
+
*,
|
|
582
|
+
words: str | None = None,
|
|
583
|
+
event_id: str | None = None,
|
|
584
|
+
) -> TasteReceipt:
|
|
585
|
+
found = self.event(actor, (target_id or "").strip())
|
|
586
|
+
if found is None:
|
|
587
|
+
raise TasteError("no taste event has that id")
|
|
588
|
+
original = found[1]
|
|
589
|
+
wanted = original["id"]
|
|
590
|
+
if original["actor"]["id"] != actor.actor.actor_id:
|
|
591
|
+
raise TasteError("only the person who recorded an event can retract it")
|
|
592
|
+
if original["kind"] == "retracted":
|
|
593
|
+
raise TasteError("a retraction cannot be retracted; record the reaction again")
|
|
594
|
+
event = self._event(actor, "retracted", event_id)
|
|
595
|
+
# A retraction counts as its id resolves across every file sharing
|
|
596
|
+
# it: a later file reusing an earlier event's id retracts nothing.
|
|
597
|
+
files = self.files(actor)
|
|
598
|
+
retractions = {file.id for file in files if file.kind == "retracted"}
|
|
599
|
+
if any(
|
|
600
|
+
row["kind"] == "retracted"
|
|
601
|
+
and row["target"].get("ref") == wanted
|
|
602
|
+
and row["actor"]["id"] == actor.actor.actor_id
|
|
603
|
+
and row["id"] != event["id"]
|
|
604
|
+
for _, row in self.read([file for file in files if file.id in retractions])
|
|
605
|
+
):
|
|
606
|
+
raise TasteError("that event is already retracted")
|
|
607
|
+
event["target"] = {"ref": wanted, "ref_kind": "event"}
|
|
608
|
+
text = clean_words(words, required=False)
|
|
609
|
+
if text is not None:
|
|
610
|
+
event["words"] = text
|
|
611
|
+
return self._record(actor, event, caller_id=bool(event_id))
|
|
612
|
+
|
|
613
|
+
# --- reading back ------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
def summary(
|
|
616
|
+
self,
|
|
617
|
+
actor: ActorContext,
|
|
618
|
+
*,
|
|
619
|
+
person: str | None = None,
|
|
620
|
+
task: str | None = None,
|
|
621
|
+
form: str | None = None,
|
|
622
|
+
) -> dict[str, Any]:
|
|
623
|
+
return summarize(
|
|
624
|
+
[row for _, row in self.events(actor)], viewer=actor, person=person, task=task, form=form,
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _date(event: Mapping[str, Any]) -> str:
|
|
629
|
+
return str(event["ts"])[:10]
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _quote(words: Any) -> str | None:
|
|
633
|
+
if not isinstance(words, str) or not words.strip():
|
|
634
|
+
return None
|
|
635
|
+
text = " ".join(words.split())
|
|
636
|
+
return text if len(text) <= QUOTE_CHARS else text[: QUOTE_CHARS - 1].rstrip() + "…"
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
def _words_of(value: Any) -> set[str]:
|
|
640
|
+
return {word for word in re.findall(r"[a-z0-9]+", str(value or "").casefold()) if len(word) >= 4}
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _near(query: str | None, value: Any) -> bool:
|
|
644
|
+
if not query or not value:
|
|
645
|
+
return False
|
|
646
|
+
left, right = query.casefold().strip(), str(value).casefold().strip()
|
|
647
|
+
return bool(left and right and (left in right or right in left or _words_of(left) & _words_of(right)))
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _resolve_person(
|
|
651
|
+
events: list[Mapping[str, Any]], viewer: ActorContext, person: str | None
|
|
652
|
+
) -> tuple[str | None, str]:
|
|
653
|
+
query = (person or "").strip()
|
|
654
|
+
if query.casefold() in {"", "me", "myself"}:
|
|
655
|
+
return viewer.actor.actor_id, viewer.actor.display_name
|
|
656
|
+
wanted = query.casefold()
|
|
657
|
+
viewer_names = {
|
|
658
|
+
viewer.actor.actor_id.casefold(),
|
|
659
|
+
viewer.actor.display_name.casefold(),
|
|
660
|
+
*(str(value).casefold() for value in viewer.actor.aliases.values()),
|
|
661
|
+
}
|
|
662
|
+
if wanted in viewer_names:
|
|
663
|
+
return viewer.actor.actor_id, viewer.actor.display_name
|
|
664
|
+
names: dict[str, str] = {}
|
|
665
|
+
for event in sorted(events, key=lambda row: row["ts"]):
|
|
666
|
+
actor_id = event["actor"]["id"]
|
|
667
|
+
name = str(event["actor"].get("name") or "")
|
|
668
|
+
if wanted in {actor_id.casefold(), name.casefold()} or _slug(query) == _slug(name):
|
|
669
|
+
names[actor_id] = name or actor_id
|
|
670
|
+
if len(names) > 1:
|
|
671
|
+
raise TasteError(f"more than one person matches {query!r}; pass their actor id")
|
|
672
|
+
if names:
|
|
673
|
+
actor_id, name = next(iter(names.items()))
|
|
674
|
+
return actor_id, name
|
|
675
|
+
return None, query
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _adopted_entry(event: Mapping[str, Any]) -> dict[str, Any]:
|
|
679
|
+
return {
|
|
680
|
+
"value": event["target"].get("ref"),
|
|
681
|
+
"date": _date(event),
|
|
682
|
+
"by": event["actor"].get("name") or event["actor"]["id"],
|
|
683
|
+
"words": _quote(event.get("words")),
|
|
684
|
+
"id": event["id"],
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def _latest_adopted(events: Iterable[Mapping[str, Any]]) -> dict[str, dict[str, Any]]:
|
|
689
|
+
"""One default per kind, the latest; principles accumulate as rules instead."""
|
|
690
|
+
|
|
691
|
+
latest: dict[str, dict[str, Any]] = {}
|
|
692
|
+
for event in sorted(events, key=lambda row: row["ts"]):
|
|
693
|
+
latest[str(event["target"].get("ref_kind"))] = _adopted_entry(event)
|
|
694
|
+
return {kind: latest[kind] for kind in DEFAULT_KINDS if kind in latest}
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _principles(events: Iterable[Mapping[str, Any]]) -> list[dict[str, Any]]:
|
|
698
|
+
"""Every adopted principle is a standing rule, newest first; the same
|
|
699
|
+
wording adopted twice appears once, as its latest adoption."""
|
|
700
|
+
|
|
701
|
+
rules: dict[str, dict[str, Any]] = {}
|
|
702
|
+
for event in sorted(events, key=lambda row: row["ts"]):
|
|
703
|
+
if event["target"].get("ref_kind") == "principle":
|
|
704
|
+
rules.pop(_phrase_key(event["target"].get("ref")), None)
|
|
705
|
+
rules[_phrase_key(event["target"].get("ref"))] = _adopted_entry(event)
|
|
706
|
+
return list(reversed(rules.values()))[:HOUSE_RULES]
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _phrase_key(value: Any) -> str:
|
|
710
|
+
return " ".join(str(value or "").casefold().split())
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
def _emergent(events: Iterable[Mapping[str, Any]], kind: str) -> list[dict[str, Any]]:
|
|
714
|
+
"""What the whole organization keeps naming when it likes or dislikes a
|
|
715
|
+
page: a pattern counts once it was named on two or more different pages.
|
|
716
|
+
Computed on every read; nothing is written or approved."""
|
|
717
|
+
|
|
718
|
+
stats: dict[str, dict[str, Any]] = {}
|
|
719
|
+
for event in sorted((row for row in events if row["kind"] == kind), key=lambda row: row["ts"]):
|
|
720
|
+
for phrase in event.get("about") or []:
|
|
721
|
+
key = _phrase_key(phrase)
|
|
722
|
+
if not key or key == WHOLE_THING:
|
|
723
|
+
continue
|
|
724
|
+
entry = stats.setdefault(key, {"pattern": phrase, "pages": set(), "people": set(), "last": "", "words": None})
|
|
725
|
+
entry["pattern"] = phrase
|
|
726
|
+
# Two sections of one page (paper#intro, paper#results) are one page.
|
|
727
|
+
entry["pages"].add(str(event["target"].get("ref")).split("#", 1)[0])
|
|
728
|
+
entry["people"].add(event["actor"]["id"])
|
|
729
|
+
entry["last"] = event["ts"]
|
|
730
|
+
entry["words"] = _quote(event.get("words")) or entry["words"]
|
|
731
|
+
held = [entry for entry in stats.values() if len(entry["pages"]) >= EMERGENT_PAGES]
|
|
732
|
+
held.sort(key=lambda entry: _phrase_key(entry["pattern"]))
|
|
733
|
+
held.sort(key=lambda entry: entry["last"], reverse=True)
|
|
734
|
+
held.sort(key=lambda entry: (len(entry["pages"]), len(entry["people"])), reverse=True)
|
|
735
|
+
return [
|
|
736
|
+
{
|
|
737
|
+
"pattern": entry["pattern"],
|
|
738
|
+
"pages": len(entry["pages"]),
|
|
739
|
+
"people": len(entry["people"]),
|
|
740
|
+
"last": entry["last"][:10],
|
|
741
|
+
"words": entry["words"],
|
|
742
|
+
}
|
|
743
|
+
for entry in held[:TOP_PATTERNS]
|
|
744
|
+
]
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def summarize(
|
|
748
|
+
events: Iterable[Mapping[str, Any]],
|
|
749
|
+
*,
|
|
750
|
+
viewer: ActorContext,
|
|
751
|
+
person: str | None = None,
|
|
752
|
+
task: str | None = None,
|
|
753
|
+
form: str | None = None,
|
|
754
|
+
) -> dict[str, Any]:
|
|
755
|
+
"""Plain counts and the person's own words, bounded, with retractions honoured."""
|
|
756
|
+
|
|
757
|
+
unique: dict[str, Mapping[str, Any]] = {}
|
|
758
|
+
for event in sorted((row for row in events if valid_event(row)), key=lambda row: row["ts"]):
|
|
759
|
+
unique.setdefault(event["id"], event)
|
|
760
|
+
rows = list(unique.values())
|
|
761
|
+
retracted: set[str] = set()
|
|
762
|
+
for event in rows:
|
|
763
|
+
if event["kind"] != "retracted":
|
|
764
|
+
continue
|
|
765
|
+
original = unique.get(str(event["target"].get("ref")))
|
|
766
|
+
if original and original["kind"] != "retracted" and original["actor"]["id"] == event["actor"]["id"]:
|
|
767
|
+
retracted.add(original["id"])
|
|
768
|
+
active = [row for row in rows if row["kind"] != "retracted" and row["id"] not in retracted]
|
|
769
|
+
|
|
770
|
+
person_id, name = _resolve_person(rows, viewer, person)
|
|
771
|
+
own = [row for row in active if person_id is not None and row["actor"]["id"] == person_id]
|
|
772
|
+
if own:
|
|
773
|
+
name = str(own[-1]["actor"].get("name") or name)
|
|
774
|
+
|
|
775
|
+
def ranked(kind: str) -> list[dict[str, Any]]:
|
|
776
|
+
stats: dict[str, dict[str, Any]] = {}
|
|
777
|
+
for event in (row for row in own if row["kind"] == kind):
|
|
778
|
+
for phrase in event.get("about") or [WHOLE_THING]:
|
|
779
|
+
entry = stats.setdefault(phrase, {"pattern": phrase, "count": 0, "last": "", "words": None})
|
|
780
|
+
entry["count"] += 1
|
|
781
|
+
entry["last"] = event["ts"]
|
|
782
|
+
entry["words"] = _quote(event.get("words")) or entry["words"]
|
|
783
|
+
ordered = sorted(stats.values(), key=lambda entry: entry["pattern"])
|
|
784
|
+
ordered.sort(key=lambda entry: entry["last"], reverse=True)
|
|
785
|
+
ordered.sort(key=lambda entry: entry["count"], reverse=True)
|
|
786
|
+
return [{**entry, "last": entry["last"][:10]} for entry in ordered[:TOP_PATTERNS]]
|
|
787
|
+
|
|
788
|
+
def relevant(event: Mapping[str, Any]) -> bool:
|
|
789
|
+
return _near(task, event.get("task")) or _near(form, event.get("form"))
|
|
790
|
+
|
|
791
|
+
choices = sorted((row for row in own if row["kind"] == "chose"), key=lambda row: row["ts"], reverse=True)
|
|
792
|
+
spoken = [row for row in own if row["kind"] in {"liked", "disliked", "chose"} and _quote(row.get("words"))]
|
|
793
|
+
spoken.sort(key=lambda row: row["ts"], reverse=True)
|
|
794
|
+
spoken.sort(key=relevant, reverse=True)
|
|
795
|
+
reactions = [row for row in own if row["kind"] in {"liked", "disliked", "chose"}]
|
|
796
|
+
dates = sorted(_date(row) for row in reactions)
|
|
797
|
+
return {
|
|
798
|
+
"schema": SUMMARY_SCHEMA,
|
|
799
|
+
"person": {"id": person_id, "name": name},
|
|
800
|
+
"counts": {kind: sum(1 for row in own if row["kind"] == kind) for kind in ("liked", "disliked", "chose")},
|
|
801
|
+
"first": dates[0] if dates else None,
|
|
802
|
+
"last": dates[-1] if dates else None,
|
|
803
|
+
"task": task,
|
|
804
|
+
"form": form,
|
|
805
|
+
"matching": sum(1 for row in reactions if relevant(row)) if (task or form) else None,
|
|
806
|
+
"liked": ranked("liked"),
|
|
807
|
+
"disliked": ranked("disliked"),
|
|
808
|
+
"chose": [
|
|
809
|
+
{
|
|
810
|
+
"date": _date(row),
|
|
811
|
+
"chose": row["target"].get("ref"),
|
|
812
|
+
"over": (row.get("over") or {}).get("ref"),
|
|
813
|
+
"words": _quote(row.get("words")),
|
|
814
|
+
"id": row["id"],
|
|
815
|
+
}
|
|
816
|
+
for row in choices[:LAST_CHOICES]
|
|
817
|
+
],
|
|
818
|
+
"rules": {
|
|
819
|
+
"egregore": [dict(rule) for rule in EGREGORE_RULES],
|
|
820
|
+
"org": _principles(row for row in active if row["kind"] == "adopted" and row.get("scope") == "org"),
|
|
821
|
+
"person": _principles(row for row in own if row["kind"] == "adopted" and row.get("scope") == "person"),
|
|
822
|
+
},
|
|
823
|
+
"emergent": {
|
|
824
|
+
"liked": _emergent(active, "liked"),
|
|
825
|
+
"disliked": _emergent(active, "disliked"),
|
|
826
|
+
},
|
|
827
|
+
"adopted": {
|
|
828
|
+
"person": _latest_adopted(
|
|
829
|
+
row for row in own if row["kind"] == "adopted" and row.get("scope") == "person"
|
|
830
|
+
),
|
|
831
|
+
"org": _latest_adopted(
|
|
832
|
+
row for row in active if row["kind"] == "adopted" and row.get("scope") == "org"
|
|
833
|
+
),
|
|
834
|
+
},
|
|
835
|
+
"quotes": [
|
|
836
|
+
{
|
|
837
|
+
"date": _date(row),
|
|
838
|
+
"kind": row["kind"],
|
|
839
|
+
"target": row["target"].get("ref"),
|
|
840
|
+
"task": row.get("task"),
|
|
841
|
+
"form": row.get("form"),
|
|
842
|
+
"words": _quote(row.get("words")),
|
|
843
|
+
"id": row["id"],
|
|
844
|
+
}
|
|
845
|
+
for row in spoken[:LAST_QUOTES]
|
|
846
|
+
],
|
|
847
|
+
"note": READING_NOTE,
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
|
|
851
|
+
def render_text(summary: Mapping[str, Any]) -> str:
|
|
852
|
+
"""Words only: what the person liked and disliked, their choices and defaults."""
|
|
853
|
+
|
|
854
|
+
name = summary["person"]["name"]
|
|
855
|
+
counts = summary["counts"]
|
|
856
|
+
total = sum(counts.values())
|
|
857
|
+
header = f"Taste for {name}"
|
|
858
|
+
if total:
|
|
859
|
+
header += (
|
|
860
|
+
f" · {counts['liked']} liked · {counts['disliked']} disliked · {counts['chose']} chosen"
|
|
861
|
+
f" · {summary['first']} to {summary['last']}"
|
|
862
|
+
)
|
|
863
|
+
lines = [header, READING_NOTE, LAYER_NOTE]
|
|
864
|
+
focus = " / ".join(value for value in (summary.get("task"), summary.get("form")) if value)
|
|
865
|
+
if focus and total:
|
|
866
|
+
lines.append(
|
|
867
|
+
f"{summary['matching']} of {total} reactions were about work like “{focus}”; "
|
|
868
|
+
"their words come first below."
|
|
869
|
+
)
|
|
870
|
+
|
|
871
|
+
def said(words: str | None) -> str:
|
|
872
|
+
return f" · “{words}”" if words else ""
|
|
873
|
+
|
|
874
|
+
rules = summary["rules"]
|
|
875
|
+
lines += ["", "Egregore rules (every organization)"]
|
|
876
|
+
lines += [
|
|
877
|
+
f"- {rule['rule']}"
|
|
878
|
+
+ ("" if rule["skip"] == "Never." else f" Skip it for {rule['skip'][:1].lower()}{rule['skip'][1:]}")
|
|
879
|
+
for rule in rules["egregore"]
|
|
880
|
+
]
|
|
881
|
+
if rules["org"]:
|
|
882
|
+
lines += ["", "House rules (this organization)"]
|
|
883
|
+
lines += [f"- {rule['value']} · {rule['date']} · by {rule['by']}{said(rule['words'])}" for rule in rules["org"]]
|
|
884
|
+
org_defaults = summary["adopted"]["org"]
|
|
885
|
+
if org_defaults:
|
|
886
|
+
lines += ["", "Organization defaults"]
|
|
887
|
+
lines += [
|
|
888
|
+
f"- {kind}: {entry['value']} · {entry['date']} · by {entry['by']}{said(entry['words'])}"
|
|
889
|
+
for kind, entry in org_defaults.items()
|
|
890
|
+
]
|
|
891
|
+
for key, title in (("liked", "Your organization tends to like"), ("disliked", "Your organization tends to avoid")):
|
|
892
|
+
if summary["emergent"][key]:
|
|
893
|
+
lines += ["", title]
|
|
894
|
+
lines += [
|
|
895
|
+
f"- {entry['pattern']} · {entry['pages']} pages · {entry['people']} "
|
|
896
|
+
+ ("person" if entry["people"] == 1 else "people")
|
|
897
|
+
+ f" · last {entry['last']}{said(entry['words'])}"
|
|
898
|
+
for entry in summary["emergent"][key]
|
|
899
|
+
]
|
|
900
|
+
if rules["person"]:
|
|
901
|
+
lines += ["", f"Rules {name} adopted"]
|
|
902
|
+
lines += [f"- {rule['value']} · {rule['date']}{said(rule['words'])}" for rule in rules["person"]]
|
|
903
|
+
if summary["adopted"]["person"]:
|
|
904
|
+
lines += ["", f"Defaults {name} adopted"]
|
|
905
|
+
lines += [
|
|
906
|
+
f"- {kind}: {entry['value']} · {entry['date']}{said(entry['words'])}"
|
|
907
|
+
for kind, entry in summary["adopted"]["person"].items()
|
|
908
|
+
]
|
|
909
|
+
|
|
910
|
+
for key, title in (("liked", f"{name} liked most"), ("disliked", f"{name} disliked most")):
|
|
911
|
+
if summary[key]:
|
|
912
|
+
lines += ["", title]
|
|
913
|
+
lines += [
|
|
914
|
+
f"- {entry['pattern']} · {entry['count']}× · last {entry['last']}{said(entry['words'])}"
|
|
915
|
+
for entry in summary[key]
|
|
916
|
+
]
|
|
917
|
+
if summary["chose"]:
|
|
918
|
+
lines += ["", "Chose"]
|
|
919
|
+
lines += [
|
|
920
|
+
f"- {entry['date']} · {entry['chose']} over {entry['over']}{said(entry['words'])}"
|
|
921
|
+
for entry in summary["chose"]
|
|
922
|
+
]
|
|
923
|
+
if summary["quotes"]:
|
|
924
|
+
lines += ["", "Their words"]
|
|
925
|
+
for entry in summary["quotes"]:
|
|
926
|
+
about = ", ".join(value for value in (entry.get("task"), entry.get("form")) if value)
|
|
927
|
+
lines.append(
|
|
928
|
+
f"- {entry['date']} · {entry['kind']} {entry['target']}"
|
|
929
|
+
+ (f" ({about})" if about else "")
|
|
930
|
+
+ said(entry["words"])
|
|
931
|
+
)
|
|
932
|
+
if not total and not summary["adopted"]["person"] and not rules["person"]:
|
|
933
|
+
lines += ["", f"No recorded taste for {name} yet."]
|
|
934
|
+
return "\n".join(lines) + "\n"
|