elanous 0.2.19 → 0.2.20
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +1 -1
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-77fad80cf54477c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +5 -0
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2977-e38442e86289f1d3.js → 211-9c9090c59e2c5907.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2489-ff55a35b202c0f94.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4847.56f5bc454fbcc827.js → 6110.1297d4093fdfbb0d.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/6158.af2e93b7adff709b.js +2 -0
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1470.98a6ab61ccb4131c.js → 726.83532d69452ee24b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 7327-2d69fb321ba73389.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8870-9614a213b990b10b.js → 7748-6e51bfe927019f5c.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7647.2fcd30d894d5ee30.js → 935.4ddaa99fc1d1126c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{ae787e4c-e5b1ef00b2dbceca.js → a9f2b54d-33c0689b70ed8cf3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-c855c24de7db74fe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-85b316bb4235980b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-9b2b26a33cb2cd42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-c9955fde67b1bcb5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-7a852956c74015a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-4ba4abe096b6c2a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f5d56ff5f45ad164.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-2260e9eef0357421.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-7c058069c5e7fa63.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-466bf0ee16ad1432.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-a23bc71713bcc08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-5b29d9b06ceadade.js → page-ed4a6f799f005d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-9cd565005b93aca6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-474cb5888ea0e8f3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-9ca21f9ad3208a61.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-f9ccb1b1adbea14d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-ec102c9b8b691e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-a9c2cfa8dda922a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-f6055495e9ed7ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-db03ae3589a81c9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-75bffecc9aa1004e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-31d5b875760bb62d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-68793c6e10331d66.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-46d5201d2207a511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d81c07a7b4f618ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-be1c4e8697f22360.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-0fd895fab9b62c1b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-c6e4364d9de8f717.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-0acc8e198f7b0dfb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-34882c84f84021c5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{2ea96958.e8c7e112de50b067.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-c260571513bf43c2.js +1 -0
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +23 -23
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +23 -23
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +22 -22
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +22 -22
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/demo/editor-recipes.yaml +22 -0
- package/graphs/retro/daily.yaml +25 -0
- package/graphs/retro/recipes.yaml +24 -0
- package/package.json +1 -1
- package/scripts/ask-marker-check.ts +3 -1
- package/scripts/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +2 -1
- package/scripts/release-loop/gate-node.ts +296 -25
- package/scripts/release-loop/gate-timing.ts +116 -7
- package/scripts/release-loop/preflight.ts +6 -4
- package/scripts/release-loop/publish-window.ts +141 -0
- package/scripts/release-loop/release-docs.ts +1 -4
- package/scripts/release-loop/release-universe.ts +38 -0
- package/scripts/release-loop/resume-release.ts +4 -1
- package/scripts/release-loop/unattended-release.ts +10 -2
- package/scripts/release-story/draft.ts +5 -1
- package/scripts/retro/shadow-compare.ts +71 -0
- package/scripts/rhythm/daily.ts +6 -1
- package/src/agent-mission/driver.ts +34 -6
- package/src/autopilot/build/llm-conflict-merge.ts +132 -7
- package/src/autopilot/build/merge-intent.ts +135 -7
- package/src/away/release-watch.ts +20 -3
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/log-event-names.ts +4 -0
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/release-cli.ts +20 -5
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/tasks-cli.ts +94 -8
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +5 -4
- package/src/dashboard/slash-runtime/help-from-registry.ts +57 -31
- package/src/domains/outbound-log-probe.ts +6 -1
- package/src/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +11 -1
- package/src/harness/harness-cli-command.ts +419 -52
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +62 -0
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +11 -1
- package/src/index.ts +69 -6
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/nexus/api/graph-kinds.ts +10 -4
- package/src/nexus/api/graphs-api.ts +178 -6
- package/src/nexus/api/http-server.ts +38 -3
- package/src/nexus/api/loop-edges.ts +4 -2
- package/src/nexus/api/ops-api.ts +22 -7
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/feature-store.ts +14 -11
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/release-schedule.ts +6 -2
- package/src/retro/daily.ts +372 -0
- package/src/seat-loop/op-decide.ts +173 -0
- package/src/seat-loop/seat-loop.ts +60 -3
- package/src/seat-loop/seat-report.ts +58 -3
- package/src/self-dev/ask-launch-flow.ts +5 -4
- package/src/self-dev/ask-launch-io.ts +49 -1
- package/src/self-dev/ask-path-candidates.ts +40 -2
- package/src/self-dev/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/pr-preview.ts +84 -16
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/orchestrator.ts +218 -5
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -0
- package/src/self-implement/salvage-retention.ts +123 -0
- package/src/self-implement/seams.ts +4 -0
- package/src/self-implement/self-implement-cli.ts +2 -0
- package/src/self-implement/sibling-resync.ts +223 -0
- package/src/self-implement/stop-autoheal.ts +204 -0
- package/src/task-agent/board.ts +1 -1
- package/src/task-agent/card-evidence.ts +99 -10
- package/src/task-agent/judge.ts +7 -0
- package/src/task-agent/live-moves.ts +120 -20
- package/src/task-agent/mission.ts +1 -1
- package/src/task-agent/shadow.ts +10 -1
- package/src/task-agent/task-hand.ts +7 -2
- package/src/task-orchestrator/surfaces/pod-command-job.ts +19 -5
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
- package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +93 -10
- package/src/task-orchestrator/surfaces/pod-pool.ts +147 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -3
- package/src/telegram-commands.ts +41 -0
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/user-config.ts +56 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +0 -1
- package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-e061bb24cd2b5cde.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +0 -2
- package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
- package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +0 -1
- package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +0 -1
- package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +0 -1
- /package/apps/pwa/out/_next/static/{J5H7BvqgSI8DUjtRRcA_S → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* RETRO-DAILY — 매일 회고 그래프(`graphs/retro/daily.yaml`)의 노드 본체.
|
|
4
|
+
*
|
|
5
|
+
* collect → cluster → recur → insight → act → report
|
|
6
|
+
*
|
|
7
|
+
* - 사실은 `elanous retro facts --since 24h --json`(RETRO-FACTS) 에서만 온다. 글은 읽지 않는다.
|
|
8
|
+
* - 반복(지난 7일 회고 파일)과 «초록으로 닫은 뒤 다시 깨진 것»을 1순위로 올린다.
|
|
9
|
+
* - 조치는 최대 셋, 칸 «초안 글»뿐이다 — 체크리스트에 쓰지 않는다(그림자).
|
|
10
|
+
* - `retro.daily.mode` = shadow(기본) | live. shadow 는 체크리스트·메시지·브리핑 어느 것도 건드리지 않는다.
|
|
11
|
+
* live 는 day 파일에 `briefing: true` 를 찍어 08:30 데일리 리뷰가 세 줄을 싣게 한다(체크리스트 쓰기는 아직 없다).
|
|
12
|
+
*
|
|
13
|
+
* RFC: 내부 문서 `RFC-retro-loop-daily-weekly-insight-2026-10-07` §1 (§0 표가 첫 판의 정답지).
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { dirname, join, resolve } from 'node:path';
|
|
19
|
+
import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
|
|
20
|
+
import { getUserConfig } from '../user-config.js';
|
|
21
|
+
import { debug } from '../debug/log.js';
|
|
22
|
+
|
|
23
|
+
export const RETRO_DAILY_GRAPH_ID = 'retro-daily';
|
|
24
|
+
export const RETRO_MAX_ACTIONS = 3;
|
|
25
|
+
export const RETRO_LOOKBACK_DAYS = 7;
|
|
26
|
+
const MAX_FACTS = 2000;
|
|
27
|
+
|
|
28
|
+
export type RetroMode = 'shadow' | 'live';
|
|
29
|
+
export type RetroStage = 'collect' | 'cluster' | 'recur' | 'insight' | 'act' | 'report';
|
|
30
|
+
export const RETRO_STAGES: readonly RetroStage[] = ['collect', 'cluster', 'recur', 'insight', 'act', 'report'];
|
|
31
|
+
|
|
32
|
+
export type RetroFactIn = { store?: string; at: string; category?: string; event?: string; kind: string; data?: unknown };
|
|
33
|
+
export type RetroFact = RetroFactIn & { id: string; signature: string; text: string };
|
|
34
|
+
/** `greenAt` = 그 칸이 마지막으로 green/done 이 된 시각(체크리스트 history). 모르면 비운다 — «모름»은 «재발 아님»으로 읽지 않는다. */
|
|
35
|
+
export type GreenCell = { id: string; title?: string; status: string; version?: string; greenAt?: string };
|
|
36
|
+
|
|
37
|
+
export type RetroTheme = {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
owner: string;
|
|
41
|
+
/** 사실의 서명·본문에 맞는 낱말. 순서대로 처음 맞는 것이 이긴다. */
|
|
42
|
+
pattern: RegExp;
|
|
43
|
+
/** RFC §0 의 «한 단 위 원인» 틀. 사실이 이 틀을 «증명»하지는 않는다 — insight 가 사실 id 로 근거를 단다. */
|
|
44
|
+
rootCause: string;
|
|
45
|
+
falsifier: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* RFC §0 다섯 반복 문제의 서명 사전. 순서가 의미를 갖는다(조용한 실패가 draft 낱말을 같이 가져도 «조용한 실패»다).
|
|
50
|
+
* 새 반복이 생기면 여기에 줄을 더한다 — 사전에 없는 서명은 `sig:<서명>` 주제로 그대로 남는다(버리지 않는다).
|
|
51
|
+
*/
|
|
52
|
+
export const RETRO_THEMES: readonly RetroTheme[] = [
|
|
53
|
+
{ id: 'silent-failure', title: '조용한 실패', owner: 'TC',
|
|
54
|
+
pattern: /silent|조용|send-disabled|deliveryState|delivery-state|truncat|절단|no[-_ ]?alert|not[-_ ]?sent|\/tmp|log[-_ ]?only|repeated_failure/i,
|
|
55
|
+
rootCause: '«돌았다»를 «일했다»로 읽는다 — 실패가 알림으로 가지 않는다',
|
|
56
|
+
falsifier: '루프 failed·deliveryState≠sent 가 알림 없이 24h 넘게 이어진 수' },
|
|
57
|
+
{ id: 'universe-confusion', title: '우주 혼동', owner: 'TC',
|
|
58
|
+
pattern: /universe|우주|test[-_ ]?instance|config-dir|empty[-_ ]?value|빈 값/i,
|
|
59
|
+
rootCause: '자리 트리 = test 우주라는 기본값이 사람·에이전트 직관과 반대다',
|
|
60
|
+
falsifier: 'test 우주를 읽어 «빈 값»으로 오판한 수/일' },
|
|
61
|
+
{ id: 'serialization', title: '직렬화 재발', owner: 'OP',
|
|
62
|
+
pattern: /serial|직렬|overlap|겹침|hotPaths|hot-path|order[-_ ]?edge/i,
|
|
63
|
+
rootCause: '«병렬 우선»(R-RUN17)이 규칙에는 있는데 새 도구를 만들 때 검사하지 않는다',
|
|
64
|
+
falsifier: '겹침을 이유로 뺀 칸·순서 간선 수/일' },
|
|
65
|
+
{ id: 'unreadable-stop', title: '멈춤을 읽을 수 없음', owner: 'TC',
|
|
66
|
+
pattern: /unreadable|unknown|읽을 수 없|non[-_ ]?convergen|unconvergeable|비수렴|no[-_ ]?reason/i,
|
|
67
|
+
rootCause: '멈춤이 사람이 읽는 글로만 남는다 — 기계가 다음 수를 고를 어휘가 없다',
|
|
68
|
+
falsifier: '멈춤 사유를 읽을 수 없는 draft 비율' },
|
|
69
|
+
{ id: 'draft-backlog', title: 'draft 적체', owner: 'OP',
|
|
70
|
+
pattern: /draft|rework|supersede|적체|DRAFT/i,
|
|
71
|
+
rootCause: '고친 것을 «초록»으로 닫고 효과를 다시 재지 않았다',
|
|
72
|
+
falsifier: '열린 draft 수(7일 이동평균)' },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// ── 작은 도구 ──────────────────────────────────────────────────────────
|
|
76
|
+
const record = (value: unknown): Record<string, unknown> => value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
77
|
+
const str = (value: unknown): string => typeof value === 'string' ? value : '';
|
|
78
|
+
export const kstDay = (date: Date): string => new Date(date.getTime() + 9 * 3_600_000).toISOString().slice(0, 10);
|
|
79
|
+
const norm = (value: string): string => value.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^\p{L}\p{N}._:/-]+/gu, '').slice(0, 80) || 'none';
|
|
80
|
+
|
|
81
|
+
export function retroDailyDir(root: string): string { return join(root, 'retro', 'daily'); }
|
|
82
|
+
export function retroDayFile(root: string, day: string): string { return join(retroDailyDir(root), `${day}.json`); }
|
|
83
|
+
|
|
84
|
+
export function factId(fact: RetroFactIn): string {
|
|
85
|
+
return `F-${createHash('sha1').update(JSON.stringify([fact.store ?? '', fact.at, fact.category ?? '', fact.event ?? '', fact.kind, fact.data ?? null])).digest('hex').slice(0, 8)}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 서명 = 묶음 열쇠. 실패 테스트 > 멈춤 사유 > 카드 종류 순으로 고른다(RFC §1 cluster). */
|
|
89
|
+
export function factSignature(fact: RetroFactIn): string {
|
|
90
|
+
const data = record(fact.data);
|
|
91
|
+
const test = str(data.failingTest) || str(data.test) || (Array.isArray(data.failingTests) ? str(data.failingTests[0]) : '');
|
|
92
|
+
if (test) return `test:${norm(test)}`;
|
|
93
|
+
if (fact.kind === 'draft-birth' || fact.kind === 'draft-death') return fact.kind;
|
|
94
|
+
if (fact.kind === 'stop-reason') return `stop:${norm(str(data.stopReason) || 'unknown')}`;
|
|
95
|
+
if (fact.kind === 'ta-outcome') {
|
|
96
|
+
const result = str(data.result);
|
|
97
|
+
// 결과를 모르면 ok 로 접지 않는다 — 판정할 낱말이 없으면 «no-result» 로 남긴다.
|
|
98
|
+
const verdict = !result.trim() ? 'no-result' : /fail|error|실패|abandon|stuck|멈춤/i.test(result) ? 'failed'
|
|
99
|
+
: /\b(ok|done|success|merged|landed|passed|updated|completed)\b|완료|성공|착지/i.test(result) ? 'ok' : 'unjudged';
|
|
100
|
+
return `card:${norm(str(data.cardKind) || str(data.card) || 'task-agent')}:${verdict}`;
|
|
101
|
+
}
|
|
102
|
+
return `${norm(fact.kind)}:${norm(str(data.signature) || str(data.cardKind) || str(data.reason) || str(data.stopReason) || '')}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function factText(fact: RetroFactIn): string {
|
|
106
|
+
const data = record(fact.data);
|
|
107
|
+
return [fact.category, fact.event, str(data.stopReason), str(data.result), str(data.reason), str(data.summary), str(data.title), str(data.cardKind)]
|
|
108
|
+
.filter(Boolean).join(' · ').slice(0, 300);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function themeOf(signature: string, texts: readonly string[]): RetroTheme | undefined {
|
|
112
|
+
if (signature === 'draft-birth' || signature === 'draft-death') return RETRO_THEMES.find(t => t.id === 'draft-backlog');
|
|
113
|
+
// 서명이 먼저 말하면 서명을 믿는다 — 본문은 서명이 침묵할 때만 본다.
|
|
114
|
+
return RETRO_THEMES.find(t => t.pattern.test(signature)) ?? RETRO_THEMES.find(t => texts.some(text => t.pattern.test(text)));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── 단계 순수 함수 ─────────────────────────────────────────────────────
|
|
118
|
+
export function normalizeFacts(raw: unknown): { facts: RetroFact[]; truncated: boolean } {
|
|
119
|
+
const list = Array.isArray(raw) ? raw : Array.isArray(record(raw).facts) ? record(raw).facts as unknown[] : [];
|
|
120
|
+
const facts = list.map(record).filter(f => typeof f.at === 'string' && typeof f.kind === 'string')
|
|
121
|
+
.map(f => {
|
|
122
|
+
const fact = f as unknown as RetroFactIn;
|
|
123
|
+
return { ...fact, id: factId(fact), signature: factSignature(fact), text: factText(fact) };
|
|
124
|
+
});
|
|
125
|
+
return { facts: facts.slice(0, MAX_FACTS), truncated: facts.length > MAX_FACTS };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type Cluster = { signature: string; theme: string; themeTitle: string; count: number; factIds: string[]; lastAt: string; samples: string[] };
|
|
129
|
+
|
|
130
|
+
export function clusterFacts(facts: readonly RetroFact[]): Cluster[] {
|
|
131
|
+
const groups = new Map<string, RetroFact[]>();
|
|
132
|
+
for (const fact of facts) groups.set(fact.signature, [...(groups.get(fact.signature) ?? []), fact]);
|
|
133
|
+
return [...groups.entries()].map(([signature, members]) => {
|
|
134
|
+
const theme = themeOf(signature, members.map(m => m.text));
|
|
135
|
+
return { signature, theme: theme?.id ?? `sig:${signature}`, themeTitle: theme?.title ?? signature, count: members.length,
|
|
136
|
+
factIds: members.map(m => m.id), lastAt: members.map(m => m.at).sort().at(-1) ?? '', samples: [...new Set(members.map(m => m.text).filter(Boolean))].slice(0, 3) };
|
|
137
|
+
}).sort((a, b) => b.count - a.count || a.signature.localeCompare(b.signature));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export type PriorDay = { day: string; themes: string[]; signatures: string[]; actedThemes: string[] };
|
|
141
|
+
export type Recurrence = {
|
|
142
|
+
theme: string; title: string; count: number; signatures: string[]; factIds: string[];
|
|
143
|
+
daysSeen: string[]; actedBefore: string[]; greenCells: string[];
|
|
144
|
+
/** 초록(green/done) 칸이 «사실보다 먼저» 있었는데 다시 나왔다. 지난 조치 «초안»은 초록이 아니다(→ recurAfterAction). */
|
|
145
|
+
reBrokeAfterGreen: boolean;
|
|
146
|
+
/** 지난 회고가 조치 초안을 냈는데 다시 나왔다. 1순위 다음. */
|
|
147
|
+
recurAfterAction: boolean; recurring: boolean; rank: number;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export function readPriorDays(root: string, day: string, lookback = RETRO_LOOKBACK_DAYS): PriorDay[] {
|
|
151
|
+
const dir = retroDailyDir(root);
|
|
152
|
+
if (!existsSync(dir)) return [];
|
|
153
|
+
return readdirSync(dir).filter(name => /^\d{4}-\d{2}-\d{2}\.json$/.test(name) && name.slice(0, 10) < day).sort().slice(-lookback)
|
|
154
|
+
.flatMap((name) => {
|
|
155
|
+
try {
|
|
156
|
+
const file = record(JSON.parse(readFileSync(join(dir, name), 'utf8')));
|
|
157
|
+
return [{ day: name.slice(0, 10),
|
|
158
|
+
themes: Array.isArray(file.themes) ? file.themes.map(t => str(record(t).theme)).filter(Boolean) : [],
|
|
159
|
+
signatures: Array.isArray(file.clusters) ? file.clusters.map(c => str(record(c).signature)).filter(Boolean) : [],
|
|
160
|
+
actedThemes: Array.isArray(file.actions) ? file.actions.map(a => str(record(a).theme)).filter(Boolean) : [] }];
|
|
161
|
+
} catch { return []; }
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** RFC §1 recur — ① 지난 회고 ② 이미 green/done 인 칸 ③ 지난 조치. «초록 뒤 재발»이 1순위다. */
|
|
166
|
+
export function findRecurrences(clusters: readonly Cluster[], prior: readonly PriorDay[], greenCells: readonly GreenCell[]): Recurrence[] {
|
|
167
|
+
const byTheme = new Map<string, Cluster[]>();
|
|
168
|
+
for (const cluster of clusters) byTheme.set(cluster.theme, [...(byTheme.get(cluster.theme) ?? []), cluster]);
|
|
169
|
+
const out = [...byTheme.entries()].map(([theme, members]) => {
|
|
170
|
+
const def = RETRO_THEMES.find(t => t.id === theme);
|
|
171
|
+
const signatures = members.map(m => m.signature);
|
|
172
|
+
const count = members.reduce((sum, m) => sum + m.count, 0);
|
|
173
|
+
const daysSeen = prior.filter(p => p.themes.includes(theme) || p.signatures.some(s => signatures.includes(s))).map(p => p.day);
|
|
174
|
+
const actedBefore = prior.filter(p => p.actedThemes.includes(theme)).map(p => p.day);
|
|
175
|
+
const lastAt = members.map(m => m.lastAt).sort().at(-1) ?? '';
|
|
176
|
+
// 초록이 된 시각이 그 주제의 마지막 사실보다 «앞»이어야 «초록 뒤 재발»이다. 시각을 모르면(greenAt 없음) 후보로 둔다.
|
|
177
|
+
const green = def ? greenCells.filter(c => (c.status === 'green' || c.status === 'done') && def.pattern.test(`${c.id} ${c.title ?? ''}`)
|
|
178
|
+
&& (!c.greenAt || !lastAt || Date.parse(c.greenAt) < Date.parse(lastAt)))
|
|
179
|
+
.map(c => `${c.id}${c.version ? `@${c.version}` : ''}${c.greenAt ? '' : '(시각 모름)'}`) : [];
|
|
180
|
+
const reBrokeAfterGreen = green.length > 0;
|
|
181
|
+
const recurAfterAction = actedBefore.length > 0;
|
|
182
|
+
const recurring = reBrokeAfterGreen || recurAfterAction || daysSeen.length > 0 || count >= 2;
|
|
183
|
+
return { theme, title: def?.title ?? members[0]!.themeTitle, count, signatures, factIds: members.flatMap(m => m.factIds),
|
|
184
|
+
daysSeen, actedBefore, greenCells: green, reBrokeAfterGreen, recurAfterAction, recurring, rank: 0 };
|
|
185
|
+
}).sort((a, b) => Number(b.reBrokeAfterGreen) - Number(a.reBrokeAfterGreen) || Number(b.recurAfterAction) - Number(a.recurAfterAction) || Number(b.recurring) - Number(a.recurring)
|
|
186
|
+
|| b.daysSeen.length - a.daysSeen.length || b.count - a.count || a.theme.localeCompare(b.theme));
|
|
187
|
+
return out.map((r, i) => ({ ...r, rank: i + 1 }));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export type Insight = { theme: string; title: string; text: string; rootCause: string; cites: string[] };
|
|
191
|
+
|
|
192
|
+
/** 결정적 insight — 사실 id 를 반드시 단다. (LLM 노드로 바꿀 때도 `cites` 검사는 그대로 남긴다.) */
|
|
193
|
+
export function buildInsights(recurrences: readonly Recurrence[]): Insight[] {
|
|
194
|
+
return recurrences.filter(r => r.recurring).map((r) => {
|
|
195
|
+
const def = RETRO_THEMES.find(t => t.id === r.theme);
|
|
196
|
+
const cites = r.factIds.slice(0, 5);
|
|
197
|
+
const why = [
|
|
198
|
+
r.greenCells.length ? `초록으로 닫은 칸(${r.greenCells.join(', ')}) 뒤 다시 나왔다` : '',
|
|
199
|
+
r.actedBefore.length ? `지난 회고 조치(${r.actedBefore.join(', ')}) 뒤에도 남았다` : '',
|
|
200
|
+
r.daysSeen.length ? `지난 ${r.daysSeen.length}일 회고에도 있었다` : '',
|
|
201
|
+
].filter(Boolean).join(' · ');
|
|
202
|
+
const rootCause = def?.rootCause ?? `서명 ${r.signatures.join(', ')} 이 반복된다 — 사전에 없는 반복이라 원인 틀이 아직 없다`;
|
|
203
|
+
return { theme: r.theme, title: r.title, rootCause, cites,
|
|
204
|
+
text: `${r.title} ${r.count}건 [${cites.join(', ')}]${why ? ` — ${why}` : ''} ⇒ ${rootCause}` };
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function assertInsightsCite(insights: readonly Insight[], facts: readonly { id: string }[]): void {
|
|
209
|
+
const known = new Set(facts.map(f => f.id));
|
|
210
|
+
for (const insight of insights) {
|
|
211
|
+
if (!insight.cites.length || insight.cites.some(id => !known.has(id))) throw new Error(`insight ${insight.theme} cites unknown or no fact ids`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type DraftCell = { id: string; theme: string; title: string; owner: string; verdictLine: string; falsifier: string; evidence: string[]; status: 'draft' };
|
|
216
|
+
|
|
217
|
+
export function draftActions(insights: readonly Insight[], recurrences: readonly Recurrence[], day: string): DraftCell[] {
|
|
218
|
+
return insights.slice(0, RETRO_MAX_ACTIONS).map((insight) => {
|
|
219
|
+
const def = RETRO_THEMES.find(t => t.id === insight.theme);
|
|
220
|
+
const rec = recurrences.find(r => r.theme === insight.theme)!;
|
|
221
|
+
const slug = insight.theme.replace(/^sig:/, '').replace(/[^a-z0-9]+/gi, '-').toUpperCase().slice(0, 32);
|
|
222
|
+
return { id: `RETRO-${slug}-${day.replace(/-/g, '')}`, theme: insight.theme, status: 'draft' as const,
|
|
223
|
+
title: `${insight.title} 한 단 위 수리 — ${insight.rootCause}`, owner: def?.owner ?? 'OP',
|
|
224
|
+
verdictLine: rec.reBrokeAfterGreen || rec.recurAfterAction ? '다음 7일 회고에서 이 주제가 다시 나오지 않는다' : '다음 회고에서 이 주제의 건수가 줄어든다',
|
|
225
|
+
falsifier: def?.falsifier ?? `서명 ${rec.signatures.join(', ')} 건수/일`, evidence: insight.cites };
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function summaryLines(recurrences: readonly Recurrence[], actions: readonly DraftCell[], factCount: number, mode: RetroMode): string[] {
|
|
230
|
+
const top = recurrences.filter(r => r.recurring);
|
|
231
|
+
const first = top[0];
|
|
232
|
+
return [
|
|
233
|
+
first ? `회고 1순위: ${first.title} ${first.count}건${first.reBrokeAfterGreen ? ' · 초록 뒤 재발' : first.recurAfterAction ? ' · 조치 뒤 재발' : first.daysSeen.length ? ` · ${first.daysSeen.length}일째` : ''}` : '회고: 반복 없음',
|
|
234
|
+
`사실 ${factCount} · 반복 주제 ${top.length}(${top.slice(0, 5).map(r => r.title).join(' · ') || '없음'})`,
|
|
235
|
+
`조치 초안 ${actions.length}: ${actions.map(a => `${a.id}(${a.owner})`).join(', ') || '없음'} · ${mode}`,
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── 모드 ───────────────────────────────────────────────────────────────
|
|
240
|
+
export function resolveRetroMode(input: Record<string, unknown>, readConfig: () => unknown = () => getUserConfig().raw): RetroMode {
|
|
241
|
+
// 입력은 «더 안전한 쪽»으로만 바꿀 수 있다(shadow 강제). live 는 config 에서만 온다.
|
|
242
|
+
if (input.mode === 'shadow') return 'shadow';
|
|
243
|
+
try { return record(record(record(readConfig()).retro).daily).mode === 'live' ? 'live' : 'shadow'; } catch { return 'shadow'; }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── 노드 실행 ─────────────────────────────────────────────────────────
|
|
247
|
+
type Ctx = { graphId?: string; nodeId?: string; input: Record<string, unknown> | null; outputs: Record<string, Record<string, unknown> | null> };
|
|
248
|
+
export type RetroDeps = { now?: Date; readFacts?: (since: string, root: string) => unknown; readGreenCells?: () => GreenCell[]; readConfig?: () => unknown; repo?: string };
|
|
249
|
+
|
|
250
|
+
const repoRoot = resolve(import.meta.dir, '..', '..');
|
|
251
|
+
|
|
252
|
+
function readFactsViaCli(since: string, root: string, repo = repoRoot): unknown {
|
|
253
|
+
const args = ['bin/elanous.mjs', ...(root === prodInstanceRoot() ? [] : [`--test=${root}`]), 'retro', 'facts', '--since', since, '--json'];
|
|
254
|
+
const result = spawnSync('bun', args, { cwd: repo, encoding: 'utf8', timeout: 570_000, maxBuffer: 64 * 1024 * 1024 });
|
|
255
|
+
if (result.error || result.status !== 0) throw new Error(`retro facts failed: ${result.error?.message ?? result.stderr?.trim() ?? `exit ${result.status}`}`);
|
|
256
|
+
return JSON.parse(result.stdout);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** 최근 판 열 개의 체크리스트에서 green/done 칸을 모은다(예: DRAFT3 0.2.12 · DRAFT-TRIAGE 0.2.18). 못 읽은 판은 건너뛴다. */
|
|
260
|
+
async function readGreenCellsFromChecklist(span = 10): Promise<GreenCell[]> {
|
|
261
|
+
const { listChecklist, checklistDevVersion } = await import('../release-loop/checklist.js');
|
|
262
|
+
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(checklistDevVersion());
|
|
263
|
+
if (!match) throw new Error(`unparseable version: ${checklistDevVersion()}`);
|
|
264
|
+
const cells: GreenCell[] = [];
|
|
265
|
+
for (let patch = Number(match[3]); patch >= Math.max(0, Number(match[3]) - span); patch--) {
|
|
266
|
+
const version = `${match[1]}.${match[2]}.${patch}`;
|
|
267
|
+
try {
|
|
268
|
+
// 체크리스트는 릴리스 원장(releaseLedgerRoot — 기본 prod)에 있다. 회고 런의 우주가 아니라 원장 우주를 읽는다(쓰기 없음).
|
|
269
|
+
const list = listChecklist(version);
|
|
270
|
+
const greenAt = (id: string) => list.history.filter(h => h.id === id && h.field === 'status' && (h.to === 'green' || h.to === 'done')).map(h => h.at).sort().at(-1);
|
|
271
|
+
cells.push(...list.items.filter(i => i.status === 'green' || i.status === 'done').map(i => ({ id: i.id, title: i.title, status: i.status, version, greenAt: greenAt(i.id) })));
|
|
272
|
+
}
|
|
273
|
+
catch { /* 그 판의 체크리스트가 없다 */ }
|
|
274
|
+
}
|
|
275
|
+
return cells;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function writeAtomic(file: string, text: string): void {
|
|
279
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
280
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
281
|
+
writeFileSync(tmp, text);
|
|
282
|
+
renameSync(tmp, file);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function runRetroStage(stage: RetroStage, ctx: Ctx, root: string, dryRun: boolean, deps: RetroDeps = {}): Promise<Record<string, unknown>> {
|
|
286
|
+
const input = ctx.input ?? {};
|
|
287
|
+
const now = deps.now ?? new Date();
|
|
288
|
+
const out = ctx.outputs;
|
|
289
|
+
const day = str(out.collect?.day) || str(input.day) || kstDay(now);
|
|
290
|
+
switch (stage) {
|
|
291
|
+
case 'collect': {
|
|
292
|
+
const since = str(input.since) || '24h';
|
|
293
|
+
const sources: Record<string, string> = {};
|
|
294
|
+
let raw: unknown;
|
|
295
|
+
if (typeof input.factsFile === 'string') { raw = JSON.parse(readFileSync(input.factsFile, 'utf8')); sources.facts = `file:${input.factsFile}`; }
|
|
296
|
+
else if (Array.isArray(input.facts)) { raw = input.facts; sources.facts = 'input'; }
|
|
297
|
+
else { raw = (deps.readFacts ?? ((s, r) => readFactsViaCli(s, r, deps.repo)))(since, root); sources.facts = `retro facts --since ${since}`; }
|
|
298
|
+
const { facts, truncated } = normalizeFacts(raw);
|
|
299
|
+
let greenCells: GreenCell[] = [];
|
|
300
|
+
if (Array.isArray(input.greenCells)) { greenCells = input.greenCells.map(record).map(c => ({ id: str(c.id), title: str(c.title), status: str(c.status), version: str(c.version) || undefined, greenAt: str(c.greenAt) || undefined })); sources.green = 'input'; }
|
|
301
|
+
else {
|
|
302
|
+
try { greenCells = (deps.readGreenCells ? deps.readGreenCells() : await readGreenCellsFromChecklist()).map(c => ({ ...c, title: (c.title ?? "").slice(0, 120) })); sources.green = 'checklist'; }
|
|
303
|
+
catch (error) { sources.green = `unreadable: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}`; }
|
|
304
|
+
}
|
|
305
|
+
const unavailable = Array.isArray(record(raw).unavailable) ? record(raw).unavailable : [];
|
|
306
|
+
return { outcome: 'ok', day, since, mode: resolveRetroMode(input, deps.readConfig), facts, factCount: facts.length, truncated, unavailable, greenCells, sources };
|
|
307
|
+
}
|
|
308
|
+
case 'cluster': {
|
|
309
|
+
const clusters = clusterFacts((out.collect?.facts ?? []) as RetroFact[]);
|
|
310
|
+
return { outcome: 'ok', clusters };
|
|
311
|
+
}
|
|
312
|
+
case 'recur': {
|
|
313
|
+
const prior = readPriorDays(root, day);
|
|
314
|
+
const recurrences = findRecurrences((out.cluster?.clusters ?? []) as Cluster[], prior, (out.collect?.greenCells ?? []) as GreenCell[]);
|
|
315
|
+
return { outcome: 'ok', priorDays: prior.map(p => p.day), recurrences };
|
|
316
|
+
}
|
|
317
|
+
case 'insight': {
|
|
318
|
+
const insights = buildInsights((out.recur?.recurrences ?? []) as Recurrence[]);
|
|
319
|
+
assertInsightsCite(insights, (out.collect?.facts ?? []) as RetroFact[]);
|
|
320
|
+
return { outcome: 'ok', insights };
|
|
321
|
+
}
|
|
322
|
+
case 'act': {
|
|
323
|
+
// 그림자든 live 든 이 노드는 «글»만 낸다 — `checklist add` 를 부르지 않는다(RETRO-DAILY 첫 조각).
|
|
324
|
+
const actions = draftActions((out.insight?.insights ?? []) as Insight[], (out.recur?.recurrences ?? []) as Recurrence[], day);
|
|
325
|
+
return { outcome: 'ok', actions, wroteChecklist: false };
|
|
326
|
+
}
|
|
327
|
+
case 'report': {
|
|
328
|
+
const mode = (str(out.collect?.mode) || 'shadow') as RetroMode;
|
|
329
|
+
const facts = (out.collect?.facts ?? []) as RetroFact[];
|
|
330
|
+
const recurrences = (out.recur?.recurrences ?? []) as Recurrence[];
|
|
331
|
+
const actions = (out.act?.actions ?? []) as DraftCell[];
|
|
332
|
+
const summary = summaryLines(recurrences, actions, facts.length, mode);
|
|
333
|
+
const themes = recurrences.map(r => ({ theme: r.theme, title: r.title, count: r.count, recurring: r.recurring, reBrokeAfterGreen: r.reBrokeAfterGreen, recurAfterAction: r.recurAfterAction, greenCells: r.greenCells, rank: r.rank }));
|
|
334
|
+
const dayFile = { day, mode, briefing: mode === 'live', generatedAt: now.toISOString(), factCount: facts.length,
|
|
335
|
+
truncated: out.collect?.truncated === true, unavailable: out.collect?.unavailable ?? [], sources: out.collect?.sources ?? {},
|
|
336
|
+
priorDays: out.recur?.priorDays ?? [], clusters: ((out.cluster?.clusters ?? []) as Cluster[]).map(({ samples: _s, ...c }) => c),
|
|
337
|
+
themes, insights: out.insight?.insights ?? [], actions, summary };
|
|
338
|
+
const file = retroDayFile(root, day);
|
|
339
|
+
const md = [`# 매일 회고 ${day} (${mode})`, '', ...summary.map(l => `- ${l}`), '', '## 반복 (순위)',
|
|
340
|
+
...recurrences.filter(r => r.recurring).map(r => `${r.rank}. ${r.title} — ${r.count}건${r.reBrokeAfterGreen ? ' · 초록 뒤 재발' : r.recurAfterAction ? ' · 조치 뒤 재발' : ''}${r.daysSeen.length ? ` · ${r.daysSeen.length}일째` : ''} · ${r.signatures.join(', ')}`),
|
|
341
|
+
'', '## 인사이트', ...((out.insight?.insights ?? []) as Insight[]).map(i => `- ${i.text}`),
|
|
342
|
+
'', '## 조치 초안 (그림자 — 체크리스트에 쓰지 않았다)', ...actions.map(a => `- ${a.id} · ${a.owner} · ${a.title} · 판정선: ${a.verdictLine} · 반증: ${a.falsifier} · 근거: ${a.evidence.join(', ')}`), ''].join('\n');
|
|
343
|
+
if (!dryRun) {
|
|
344
|
+
writeAtomic(file, JSON.stringify(dayFile, null, 2) + '\n');
|
|
345
|
+
writeAtomic(file.replace(/\.json$/, '.md'), md);
|
|
346
|
+
}
|
|
347
|
+
debug.log('retro.daily', 'report', { day, mode, dryRun, facts: facts.length, recurring: themes.filter(t => t.recurring).length, actions: actions.length });
|
|
348
|
+
return { outcome: 'ok', day, mode, dryRun, file: dryRun ? null : file, summary, actions: actions.length, sent: false, wroteChecklist: false };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** 08:30 데일리 리뷰가 싣는 세 줄 — live 로 쓴 회고 파일만 읽는다(shadow 는 브리핑에 안 나간다). */
|
|
354
|
+
export function retroBriefingLines(root: string, day: string): string[] {
|
|
355
|
+
try {
|
|
356
|
+
const file = record(JSON.parse(readFileSync(retroDayFile(root, day), 'utf8')));
|
|
357
|
+
if (file.briefing !== true || !Array.isArray(file.summary)) return [];
|
|
358
|
+
return file.summary.map(str).filter(Boolean).slice(0, 3);
|
|
359
|
+
} catch { return []; }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (import.meta.main) {
|
|
363
|
+
const stage = process.argv[2] as RetroStage;
|
|
364
|
+
const path = process.env.ELANOUS_GRAPH_CONTEXT;
|
|
365
|
+
if (!path || !RETRO_STAGES.includes(stage)) throw new Error('invalid retro-daily graph stage');
|
|
366
|
+
const ctx = JSON.parse(readFileSync(path, 'utf8')) as Ctx;
|
|
367
|
+
if (ctx.graphId !== RETRO_DAILY_GRAPH_ID || ctx.nodeId !== stage) throw new Error('retro-daily graph context mismatch');
|
|
368
|
+
// <root>/graph-runs/<graphId>/<runId>.json.contexts/<n>.json — 런 원장의 우주가 곧 회고 파일의 우주다.
|
|
369
|
+
const statePath = resolve(path, '..', '..', '..', '..');
|
|
370
|
+
const root = statePath || effectiveInstanceRoot();
|
|
371
|
+
console.log(JSON.stringify(await runRetroStage(stage, ctx, root, process.env.ELANOUS_GRAPH_DRY_RUN === '1')));
|
|
372
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// OP-LOOP-DECIDE — the OP seat loop filters open decision cards instead of a Claude OP session doing it by hand.
|
|
2
|
+
// Delegation = 대표 10-06 COO card filtering rule (feedback_coo_filters_decision_cards) ⊕ 10-07 scope-card default b.
|
|
3
|
+
// Config `loops.seat.opDecide.mode` = shadow (default · record only) | live (decide / route).
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
5
|
+
import type { DecisionEntry, DecisionLedger } from '../decisions/decision-ledger.js';
|
|
6
|
+
import type { SeatId } from '../seat-dispatch/seat-questions.js';
|
|
7
|
+
import { releasePathHold } from '../self-dev/release-path-guard.js';
|
|
8
|
+
|
|
9
|
+
export type OpDecideMode = 'shadow' | 'live';
|
|
10
|
+
export type OpDecideRule = 'repeat-stop' | 'scope-expand' | 'scope-sensitive' | 'scope-release-path' | 'scope-unknown' | 'run-blocking' | 'forbidden';
|
|
11
|
+
export type OpDecideVerdict =
|
|
12
|
+
| { verdict: 'auto'; rule: OpDecideRule; choice: string; reason: string }
|
|
13
|
+
| { verdict: 'route'; rule: OpDecideRule; to: SeatId | 'CEO'; reason: string };
|
|
14
|
+
export type OpDecideCandidate = { kind: 'decision-filter'; id: string; mode: OpDecideMode } & OpDecideVerdict;
|
|
15
|
+
|
|
16
|
+
export const OP_DECIDE_AGENT = 'seat-loop:OP';
|
|
17
|
+
export const OP_DECIDE_DELEGATION = '대표 10-06 COO 카드 거르기 규칙 · 10-07 범위 카드 기본 b(돈·보안·비밀·공개 경로만 유지)';
|
|
18
|
+
|
|
19
|
+
/** Categories that must reach the CEO whatever the recommendation says (rule 3). */
|
|
20
|
+
const FORBIDDEN_CATEGORIES: ReadonlySet<DecisionEntry['category']> = new Set(['money', 'security', 'secret', 'publish']);
|
|
21
|
+
const FORBIDDEN_WORDS = /특허|patent|외부\s*계약|계약서|contract/i;
|
|
22
|
+
|
|
23
|
+
/** Path segments that keep a scope card open — money · security · secret · public (rule 2 exception). */
|
|
24
|
+
const SENSITIVE_PATH: ReadonlyArray<{ kind: 'money' | 'security' | 'secret' | 'public'; pattern: RegExp }> = [
|
|
25
|
+
{ kind: 'secret', pattern: /(^|[/._-])(secrets?|credentials?|\.env|keychain|apns|backup-key)([/._-]|$)|\.(pem|p8|p12|key)$/i },
|
|
26
|
+
{ kind: 'security', pattern: /(^|[/._-])(auth|oauth|security|sandbox|permissions?|acl|csp|tokens?|crypto)([/._-]|$)/i },
|
|
27
|
+
{ kind: 'money', pattern: /(^|[/._-])(money|finance|billing|payments?|payouts?|refunds?|revenue|pricing|stripe|invoices?|checkout|wallet|trad(e|ing)|broker|budget|quota)([/._-]|$)/i },
|
|
28
|
+
{ kind: 'public', pattern: /(^|\/)(README|CHANGELOG|LICENSE|NOTICE)(\.[a-z]+)?$|(^|[/._-])(publish|public|homepage|site|npm)([/._-]|$)|^\.github\/workflows\/|^scripts\/release-loop\/release-publish/i },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export function sensitivePathKind(file: string): 'money' | 'security' | 'secret' | 'public' | undefined {
|
|
32
|
+
return SENSITIVE_PATH.find(({ pattern }) => pattern.test(file))?.kind;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SCOPE_QUESTION = /outside the authored target paths/i;
|
|
36
|
+
|
|
37
|
+
/** Outside-file names listed on a harness scope card; undefined when the card carries no complete list. */
|
|
38
|
+
export function scopeOutsideFiles(card: Pick<DecisionEntry, 'pendingQuestion'>): string[] | undefined {
|
|
39
|
+
const text = card.pendingQuestion ?? '';
|
|
40
|
+
const at = text.indexOf('Outside files:');
|
|
41
|
+
if (at < 0) return undefined;
|
|
42
|
+
// «… N more outside file(s) not shown» anywhere after the list header — a hidden file cannot be vetted.
|
|
43
|
+
if (/more outside file/i.test(text.slice(at))) return undefined;
|
|
44
|
+
const lines = text.slice(at + 'Outside files:'.length).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
45
|
+
const files: string[] = [];
|
|
46
|
+
for (const line of lines) {
|
|
47
|
+
if (!line.startsWith('- ')) break;
|
|
48
|
+
// «… N more outside file(s) not shown» — a hidden file cannot be vetted.
|
|
49
|
+
if (/more outside file/i.test(line)) return undefined;
|
|
50
|
+
const name = line.slice(2).replace(/\s+\(declared-path sibling test: [a-z]+\)$/, '').trim();
|
|
51
|
+
if (name) files.push(name);
|
|
52
|
+
}
|
|
53
|
+
return files.length ? files : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function optionKey(card: DecisionEntry, label: RegExp): string | undefined {
|
|
57
|
+
return card.options.find((option) => label.test(option.label))?.key;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isRepeatStopCard(card: DecisionEntry): boolean {
|
|
61
|
+
return card.raisedBy.agent === 'seat-loop' && / 반복 착지 0$/.test(card.title);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isScopeCard(card: DecisionEntry): boolean {
|
|
65
|
+
return card.raisedBy.agent === 'harness' && (card.category === 'scope' || card.category === 'irreversible')
|
|
66
|
+
&& SCOPE_QUESTION.test(`${card.title}\n${card.pendingQuestion ?? ''}`) && !!card.resume;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Judge one open card. undefined = not this filter's card (left alone). */
|
|
70
|
+
export function judgeOpDecision(card: DecisionEntry, runOwner: (runId: string) => SeatId | undefined = () => undefined): OpDecideVerdict | undefined {
|
|
71
|
+
if (card.status !== 'open') return undefined;
|
|
72
|
+
const repeat = isRepeatStopCard(card);
|
|
73
|
+
const scope = isScopeCard(card);
|
|
74
|
+
const runId = card.resume?.runId;
|
|
75
|
+
if (!repeat && !scope && !runId) return undefined;
|
|
76
|
+
if (FORBIDDEN_CATEGORIES.has(card.category) || FORBIDDEN_WORDS.test(`${card.title}\n${card.scqa.s}`)) {
|
|
77
|
+
return { verdict: 'route', rule: 'forbidden', to: 'CEO', reason: `${card.category} 범주·금지 낱말 — 자동 결정 금지, 대표 카드로 남김` };
|
|
78
|
+
}
|
|
79
|
+
if (repeat) {
|
|
80
|
+
const stop = optionKey(card, /^멈춘다$/);
|
|
81
|
+
const recommended = 'option' in card.recommendation ? card.recommendation.option : undefined;
|
|
82
|
+
if (stop && recommended === stop) {
|
|
83
|
+
return { verdict: 'auto', rule: 'repeat-stop', choice: stop, reason: `반복 착지 0 — 권고 ${stop}(멈춘다) · 그냥 둔 것과 같은 결과` };
|
|
84
|
+
}
|
|
85
|
+
return { verdict: 'route', rule: 'repeat-stop', to: 'CEO', reason: '반복 정지 카드인데 권고가 «멈춘다»가 아님 — 확신 없음' };
|
|
86
|
+
}
|
|
87
|
+
if (scope) {
|
|
88
|
+
const expand = optionKey(card, /^Expand scope$/i);
|
|
89
|
+
const files = scopeOutsideFiles(card);
|
|
90
|
+
const owner = runId ? runOwner(runId) : undefined;
|
|
91
|
+
const route = (rule: OpDecideRule, reason: string): OpDecideVerdict =>
|
|
92
|
+
({ verdict: 'route', rule, to: owner && owner !== 'OP' ? owner : 'CEO', reason });
|
|
93
|
+
if (!expand) return route('scope-unknown', '범위 카드에 «Expand scope» 선택지가 없음');
|
|
94
|
+
if (!files) return route('scope-unknown', '바깥 파일 목록이 없거나 일부만 보임 — 경로를 확인 못 함');
|
|
95
|
+
const sensitive = files.map((file) => ({ file, kind: sensitivePathKind(file) })).filter((row) => row.kind);
|
|
96
|
+
if (sensitive.length) {
|
|
97
|
+
return { verdict: 'route', rule: 'scope-sensitive', to: 'CEO',
|
|
98
|
+
reason: `바깥 파일 중 ${sensitive[0]!.kind} 경로(${sensitive[0]!.file}${sensitive.length > 1 ? ` 외 ${sensitive.length - 1}` : ''}) — 유지 여부는 대표 판단` };
|
|
99
|
+
}
|
|
100
|
+
// Release/gate paths follow the release-path SSOT (src/self-dev/release-path-guard.ts) — never auto-widen; TC owns the gate.
|
|
101
|
+
// TODO(0.2.21 RELEASE-PATH-GUARD-GATE): src/task-orchestrator/surfaces/pod-command-job.ts and src/release-loop/gate-shards.ts
|
|
102
|
+
// are gate paths not yet in RELEASE_PATH_PREFIXES; they become covered here once that list grows.
|
|
103
|
+
const release = releasePathHold(files);
|
|
104
|
+
if (release) {
|
|
105
|
+
return { verdict: 'route', rule: 'scope-release-path', to: 'TC',
|
|
106
|
+
reason: `바깥 파일 중 발행·게이트 경로(${release}) — 게이트 주인 TC 판단` };
|
|
107
|
+
}
|
|
108
|
+
return { verdict: 'auto', rule: 'scope-expand', choice: expand,
|
|
109
|
+
reason: `범위 넓힘 b — 바깥 ${files.length}개 모두 돈·보안·비밀·공개 경로 아님 · 게이트·리뷰가 가린다` };
|
|
110
|
+
}
|
|
111
|
+
const owner = runOwner(runId!);
|
|
112
|
+
return { verdict: 'route', rule: 'run-blocking', to: owner && owner !== 'OP' ? owner : 'CEO',
|
|
113
|
+
reason: owner && owner !== 'OP' ? `런 ${runId} 을 막는 카드 — 런 주인 ${owner} 자리로` : `런 ${runId} 을 막는 카드 — 주인 자리 모름, 대표 카드로 남김` };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export type OpDecideDeps = {
|
|
117
|
+
mode: OpDecideMode;
|
|
118
|
+
ledger: Pick<DecisionLedger, 'list' | 'decide'>;
|
|
119
|
+
runOwner?: (runId: string) => SeatId | undefined;
|
|
120
|
+
/** Route a run-blocking card to its owner seat (live only); returns false when delivery failed. */
|
|
121
|
+
routeToSeat?: (to: SeatId, card: DecisionEntry, reason: string) => boolean;
|
|
122
|
+
/** Candidates already recorded for this card (dedup across ticks). */
|
|
123
|
+
recorded?: (id: string) => readonly OpDecideCandidate[];
|
|
124
|
+
record: (candidate: OpDecideCandidate) => void;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** One filtering pass over open cards. Never throws for a single card; returns what it recorded. */
|
|
128
|
+
export function runOpDecide(deps: OpDecideDeps): OpDecideCandidate[] {
|
|
129
|
+
const out: OpDecideCandidate[] = [];
|
|
130
|
+
let cards: DecisionEntry[];
|
|
131
|
+
try { cards = deps.ledger.list({ status: 'open' }); }
|
|
132
|
+
catch (error) {
|
|
133
|
+
observeOpDecide({ event: 'ledger-unreadable', error: String(error).slice(0, 200) });
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
for (const card of cards) {
|
|
137
|
+
let judged: OpDecideVerdict | undefined;
|
|
138
|
+
try { judged = judgeOpDecision(card, deps.runOwner); }
|
|
139
|
+
catch (error) {
|
|
140
|
+
observeOpDecide({ event: 'judge-failed', id: card.id, error: String(error).slice(0, 200) });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!judged) continue;
|
|
144
|
+
const candidate: OpDecideCandidate = { kind: 'decision-filter', id: card.id, mode: deps.mode, ...judged };
|
|
145
|
+
const same = (row: OpDecideCandidate) => row.mode === candidate.mode && row.verdict === candidate.verdict && row.rule === candidate.rule
|
|
146
|
+
&& ('to' in row ? row.to : row.choice) === ('to' in candidate ? candidate.to : candidate.choice);
|
|
147
|
+
if ((deps.recorded?.(card.id) ?? []).some(same)) continue;
|
|
148
|
+
if (deps.mode === 'live' && candidate.verdict === 'auto') {
|
|
149
|
+
try {
|
|
150
|
+
deps.ledger.decide(card.id, candidate.choice,
|
|
151
|
+
{ kind: 'auto', agent: OP_DECIDE_AGENT, track: 'OP', delegation: OP_DECIDE_DELEGATION }, candidate.reason);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
observeOpDecide({ event: 'decide-failed', id: card.id, rule: candidate.rule, error: String(error).slice(0, 200) });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (deps.mode === 'live' && candidate.verdict === 'route' && candidate.to !== 'CEO' && deps.routeToSeat) {
|
|
158
|
+
let delivered = false;
|
|
159
|
+
try { delivered = deps.routeToSeat(candidate.to, card, candidate.reason); }
|
|
160
|
+
catch (error) { observeOpDecide({ event: 'route-failed', id: card.id, to: candidate.to, error: String(error).slice(0, 200) }); }
|
|
161
|
+
if (!delivered) continue;
|
|
162
|
+
}
|
|
163
|
+
deps.record(candidate);
|
|
164
|
+
out.push(candidate);
|
|
165
|
+
observeOpDecide({ event: 'op-decide', id: card.id, mode: candidate.mode, verdict: candidate.verdict, rule: candidate.rule,
|
|
166
|
+
...(candidate.verdict === 'auto' ? { choice: candidate.choice } : { to: candidate.to }), category: card.category, reason: candidate.reason });
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function observeOpDecide(data: Record<string, unknown>): void {
|
|
172
|
+
try { debug.log('seat.loop', 'op-decide', data); } catch { /* observation cannot change a decision */ }
|
|
173
|
+
}
|