elanous 0.2.18 → 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/chunks/{1307-30dff22c9e1f3073.js → 1307-77fad80cf54477c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2116-6429c188eb846ab8.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/2549-89c518a4c8ed4d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-fad095b48a1862ee.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/7748-6e51bfe927019f5c.js +25 -0
- package/apps/pwa/out/_next/static/chunks/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-d92f7ee3994e84c7.js → page-c855c24de7db74fe.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-4733c967b56a4277.js → page-85b316bb4235980b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d22fd849cc22f104.js → page-9b2b26a33cb2cd42.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-bb742f995c6769e7.js → page-c9955fde67b1bcb5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-02b073a2ac793b83.js → page-7a852956c74015a3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-8a9b3594bc23289b.js → page-4ba4abe096b6c2a8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-575d4a8b4f7becb2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-308111abadf35670.js → page-f5d56ff5f45ad164.js} +1 -1
- 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-5d97b2901e0d1ea6.js → page-7c058069c5e7fa63.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-8c3d8e63ae2e1c98.js → page-466bf0ee16ad1432.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-9b72a07d424701c6.js → page-a23bc71713bcc08f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-a572f58cd3ca5864.js → page-9cd565005b93aca6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-be5ea965a07fbc41.js → page-474cb5888ea0e8f3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-fa244bd146a00ae8.js → page-9ca21f9ad3208a61.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-44d56d6391965a6a.js → page-f9ccb1b1adbea14d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-83a700d4c754d802.js → page-ec102c9b8b691e37.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-7b3170f374f77b9d.js → not-found-a9c2cfa8dda922a5.js} +1 -1
- 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-1467047104141297.js → page-db03ae3589a81c9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4a6463b97f0ad94f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-102310930791197a.js → page-75bffecc9aa1004e.js} +1 -1
- 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-6ed0b73bae8351d8.js → page-31d5b875760bb62d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-c26fa10aac6c21c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-43336055451f6e37.js → page-68793c6e10331d66.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-3cc47dca0922ab9a.js → page-46d5201d2207a511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-a77ee355a2304ff0.js → page-d81c07a7b4f618ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-b17cd46cf0e9b8cf.js → page-be1c4e8697f22360.js} +1 -1
- 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-7bf54c9bb9274af8.js → page-0fd895fab9b62c1b.js} +1 -1
- 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-134da468538baf48.js → page-34882c84f84021c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-c2782b330447ffb6.js → webpack-c260571513bf43c2.js} +1 -1
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +1 -0
- package/apps/pwa/out/_next/static/css/d3427996da11e480.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +16 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +16 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +24 -23
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/demo/editor-recipes.yaml +22 -0
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -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/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +3 -1
- package/scripts/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +24 -0
- package/scripts/morning-report.ts +18 -4
- package/scripts/pch-regress.ts +88 -0
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +628 -65
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +229 -0
- 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 +67 -5
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +12 -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/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- 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/presence.ts +40 -0
- package/src/away/release-watch.ts +272 -0
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +13 -7
- package/src/cli/log-event-names.ts +5 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/pr-cli.ts +20 -0
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +52 -12
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +193 -14
- package/src/context-bus/context-now-public.ts +1 -1
- package/src/context-bus/context-now-surfaces.ts +16 -6
- package/src/context-bus/context-now.ts +97 -3
- package/src/core-turn/short-question.ts +100 -0
- package/src/dashboard/dashboard-status-lines.ts +24 -4
- package/src/dashboard/index.ts +50 -5
- package/src/dashboard/input/slash-executor.ts +6 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +68 -40
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +62 -23
- package/src/dashboard/slash-runtime/help-groups.ts +44 -0
- package/src/dashboard/slash-runtime/model-choices.ts +125 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +44 -13
- package/src/domains/outbound-log-probe.ts +6 -1
- package/src/domains/schedule-registry.ts +23 -0
- package/src/domains/telegram-kind-route.ts +39 -0
- package/src/domains/watch-default-scheduler.ts +95 -0
- package/src/esc-abort-gate.ts +44 -0
- package/src/graph-runner/graph-run-api.ts +205 -0
- package/src/graph-runner/runner.ts +36 -6
- package/src/harness/harness-cli-command.ts +420 -51
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +140 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +17 -2
- package/src/index.ts +118 -6
- package/src/instance/test-write-guard-spy-first.fixture.ts +13 -0
- package/src/instance/test-write-guard.ts +200 -0
- package/src/intake-plane/digest.ts +39 -15
- package/src/intake-plane/harness-target.ts +4 -3
- package/src/intake-plane/intake-sources.ts +1 -0
- package/src/intake-plane/items.ts +1 -0
- package/src/loops/orchestrator/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +33 -3
- package/src/loops/orchestrator/tick.ts +166 -45
- package/src/maturity/feature-maturity.ts +6 -4
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- 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 +54 -8
- package/src/nexus/api/loop-edges.ts +292 -17
- package/src/nexus/api/ops-api.ts +112 -29
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/nexus/index.ts +7 -0
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding.ts +24 -6
- package/src/release-loop/checklist.ts +2 -0
- package/src/release-loop/feature-store.ts +68 -18
- package/src/release-loop/gate-shards.ts +158 -0
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +51 -17
- package/src/release-loop/release-schedule.ts +6 -2
- package/src/release-loop/rubric.ts +57 -1
- 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/dev-cli.ts +36 -0
- package/src/self-dev/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +247 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-env-retry.ts +83 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/orchestrator.ts +220 -6
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -0
- package/src/self-implement/salvage-branches.ts +254 -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/actions.ts +63 -2
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +292 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +25 -0
- package/src/task-agent/live-moves.ts +276 -0
- package/src/task-agent/mission.ts +33 -7
- package/src/task-agent/shadow.ts +192 -15
- package/src/task-agent/task-hand.ts +124 -17
- package/src/task-orchestrator/surfaces/pod-command-job.ts +82 -8
- 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 +233 -21
- package/src/task-orchestrator/surfaces/pod-pool.ts +155 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +21 -5
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +52 -0
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +190 -5
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-a0eb02638b1e6f31.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c39554c321576982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-c130b63862437814.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a3c58f5c72a5f3f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e8e1ecaea7cd71d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-44d5f767491692bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → Bam1YziK2iH9bN4K0utGk}/_ssgManifest.js +0 -0
|
@@ -2,6 +2,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { CliUserError } from '../cli/cli-user-error.js';
|
|
5
|
+
import { resolveRepositoryName } from '../harness/repository-name.js';
|
|
5
6
|
import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
6
7
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
7
8
|
import { userConfigPath } from '../user-config.js';
|
|
@@ -40,10 +41,41 @@ function versionOrder(a: string, b: string): number {
|
|
|
40
41
|
return 0;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/** Where each cell id lives. An id may repeat when one copy is in an already released version — history kept two
|
|
45
|
+
* different cells under one id (IA1 · EV10a … in 0.2.6~0.2.15) and later cells reused old ids (INSIDE1 0.2.11 ⊕
|
|
46
|
+
* 0.2.19). A released copy is history: an open copy wins over it, and between released copies the latest wins. Only
|
|
47
|
+
* two open copies are a real conflict (ORCH-LIVE-1008 rehearsal 1: every placement failed on «여러 판의 같은 칸»). */
|
|
48
|
+
function cellLocations(snapshots: Iterable<[string, readonly { id: string }[]]>, released: string | null | undefined, skip: readonly string[] = []): { locations: Map<string, string>; conflict: string | null } {
|
|
49
|
+
const locations = new Map<string, string>();
|
|
50
|
+
const shipped = (version: string) => !!released && versionOrder(version, released) <= 0;
|
|
51
|
+
for (const [version, items] of snapshots) {
|
|
52
|
+
if (skip.includes(version)) continue;
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
const previous = locations.get(item.id);
|
|
55
|
+
if (previous !== undefined) {
|
|
56
|
+
if (!shipped(previous) && !shipped(version)) return { locations, conflict: item.id };
|
|
57
|
+
// Keep the open copy; between two released copies keep the later one.
|
|
58
|
+
const keepPrevious = shipped(version) && (!shipped(previous) || versionOrder(version, previous) < 0);
|
|
59
|
+
if (keepPrevious) continue;
|
|
60
|
+
}
|
|
61
|
+
locations.set(item.id, version);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { locations, conflict: null };
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
/** The merged PR list is a first-class GitHub query, not a count inferred from checklist status. */
|
|
68
|
+
// ORCH-LIVE-1008: the orchestrator tick runs from the installed package (not a git checkout), where a bare `gh pr list`
|
|
69
|
+
// fails «not a git repository» and placement silently left every cell unplaced — so the repository is named explicitly
|
|
70
|
+
// (--repo > harness.repo > GH_REPO > the checkout's remote · `resolveRepositoryName`).
|
|
44
71
|
export function mergedPrsLast24h(now = new Date()): number {
|
|
45
72
|
const since = new Date(now.getTime() - 86_400_000).toISOString();
|
|
46
|
-
|
|
73
|
+
let repo: string;
|
|
74
|
+
try { repo = resolveRepositoryName(); }
|
|
75
|
+
catch (error) {
|
|
76
|
+
throw new CliUserError(`병합 PR 속도 조회 실패: 저장소를 못 정했다 — ${(error instanceof Error ? error.message : String(error)).split('\n')[0]}`, 'harness.repo 설정(owner/name) 또는 GH_REPO');
|
|
77
|
+
}
|
|
78
|
+
const r = spawnSync('gh', ['pr', 'list', '--repo', repo, '--state', 'merged', '--search', `merged:>=${since.slice(0, 10)}`, '--limit', '1000', '--json', 'number,mergedAt'], { encoding: 'utf8' });
|
|
47
79
|
if (r.status !== 0) throw new CliUserError(`병합 PR 속도 조회 실패: ${(r.stderr || r.error || '').toString().trim()}`);
|
|
48
80
|
let rows: Array<{ number: number; mergedAt: string }>;
|
|
49
81
|
try { rows = JSON.parse(r.stdout) as typeof rows; }
|
|
@@ -92,10 +124,16 @@ function dependencyViolation(
|
|
|
92
124
|
locations: Map<string, string>,
|
|
93
125
|
overrides: Map<string, string>,
|
|
94
126
|
input: PlacementCell,
|
|
127
|
+
released?: string | null,
|
|
95
128
|
): string | null {
|
|
96
129
|
for (const [version, snapshot] of snapshots) for (const item of snapshot.items) {
|
|
97
|
-
|
|
130
|
+
// A released version is history — its cells (and what they once depended on) never constrain a new placement.
|
|
131
|
+
if (released && versionOrder(version, released) <= 0) continue;
|
|
98
132
|
const predecessors = item.id === input.id ? input.predecessors : item.predecessors ?? [];
|
|
133
|
+
// Only the cells this placement moves, and cells that depend on them, can be made wrong by it — a pre-existing
|
|
134
|
+
// violation elsewhere on the board must not make every placement infeasible (ORCH-LIVE-1008 rehearsal 1).
|
|
135
|
+
if (!overrides.has(item.id) && !predecessors.some((predecessor) => overrides.has(predecessor))) continue;
|
|
136
|
+
const dependentVersion = overrides.get(item.id) ?? version;
|
|
99
137
|
for (const predecessor of predecessors) {
|
|
100
138
|
const predecessorVersion = overrides.get(predecessor) ?? locations.get(predecessor);
|
|
101
139
|
if (!predecessorVersion || versionOrder(dependentVersion, predecessorVersion) <= 0)
|
|
@@ -131,12 +169,8 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
131
169
|
const open = schedules.filter((row) => (!released || versionOrder(row.version, released) > 0) && row.landBy && available(row, now, backlog));
|
|
132
170
|
const sources = [...new Set([...schedules.map((row) => row.version), ...backlog])];
|
|
133
171
|
const snapshots = new Map(sources.map((version) => [version, (deps.checklist ?? listChecklist)(version)]));
|
|
134
|
-
const locations =
|
|
135
|
-
|
|
136
|
-
if (backlog.includes(version)) continue;
|
|
137
|
-
if (locations.has(item.id)) throw new CliUserError(`여러 판의 같은 칸: ${item.id}`);
|
|
138
|
-
locations.set(item.id, version);
|
|
139
|
-
}
|
|
172
|
+
const { locations, conflict } = cellLocations([...snapshots].map(([version, snapshot]) => [version, snapshot.items] as [string, readonly { id: string }[]]), released, backlog);
|
|
173
|
+
if (conflict) throw new CliUserError(`여러 판의 같은 칸: ${conflict}`);
|
|
140
174
|
// A backlog copy never outranks a scheduled cell with the same id.
|
|
141
175
|
for (const version of backlog) for (const item of snapshots.get(version)?.items ?? []) if (!locations.has(item.id)) locations.set(item.id, version);
|
|
142
176
|
const from = locations.get(input.id) ?? null;
|
|
@@ -172,7 +206,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
172
206
|
const displaced: PlacementDecision['displaced'] = [];
|
|
173
207
|
let chosen: ReleaseSchedule | undefined;
|
|
174
208
|
let loadIssue: string | null = null;
|
|
175
|
-
const feasible = candidates.filter((row) => !dependencyViolation(snapshots, locations, new Map([[input.id, row.version]]), input));
|
|
209
|
+
const feasible = candidates.filter((row) => !dependencyViolation(snapshots, locations, new Map([[input.id, row.version]]), input, released));
|
|
176
210
|
// 판은 «마감 안 가장 이른 판»부터 본다(기존 불변식). 가속 등급은 그 판이 찼을 때 같은 우선순위 비가속 칸을 밀 수 있게만 한다.
|
|
177
211
|
for (const row of feasible) {
|
|
178
212
|
const placement = new Map([[input.id, row.version]]);
|
|
@@ -192,7 +226,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
192
226
|
|| (accelerator && priority === input.priority);
|
|
193
227
|
const p2 = [...items].reverse().find((item) => {
|
|
194
228
|
if (item.accelerator === true) return false;
|
|
195
|
-
if (!victimOk(item.priority) || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input)) return false;
|
|
229
|
+
if (!victimOk(item.priority) || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input, released)) return false;
|
|
196
230
|
if (item.deadlineVersion && versionOrder(next.version, item.deadlineVersion) > 0) return false;
|
|
197
231
|
if (items.length < capacity(row) && !(seatItems.length >= seatLimit && ownerMatches(item.owner, seat))) return false;
|
|
198
232
|
if (!item.owner) return false;
|
|
@@ -235,6 +269,7 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
235
269
|
function moveConstraint(
|
|
236
270
|
item: ChecklistItem, to: ReleaseSchedule, backlog: readonly string[],
|
|
237
271
|
snapshots: Map<string, ChecklistItem[]>, now: number, rate: number, caps: Record<string, number>, rows: ReleaseSchedule[], ceoCap: number,
|
|
272
|
+
released: string | null | undefined,
|
|
238
273
|
): string | null {
|
|
239
274
|
if (!to.landBy || !available(to, now, backlog)) return '착지 마감이 지났거나 백로그 판이라 이동 불가';
|
|
240
275
|
if (item.deadlineVersion && versionOrder(to.version, item.deadlineVersion) > 0) return '마감 판 뒤로 이월 불가';
|
|
@@ -246,16 +281,15 @@ function moveConstraint(
|
|
|
246
281
|
const loadIssue = ceoOverload(item, to, new Map([...snapshots].map(([version, items]) => [version, { items }])),
|
|
247
282
|
rows, ceoCap);
|
|
248
283
|
if (loadIssue) return loadIssue;
|
|
249
|
-
const locations =
|
|
250
|
-
|
|
251
|
-
if (locations.has(cell.id)) return `여러 판의 같은 칸: ${cell.id}`;
|
|
252
|
-
locations.set(cell.id, version);
|
|
253
|
-
}
|
|
284
|
+
const { locations, conflict } = cellLocations(snapshots, released);
|
|
285
|
+
if (conflict) return `여러 판의 같은 칸: ${conflict}`;
|
|
254
286
|
for (const predecessor of item.predecessors ?? []) {
|
|
255
287
|
const location = locations.get(predecessor);
|
|
256
288
|
if (!location || versionOrder(to.version, location) <= 0) return `선행 칸 ${predecessor} 이후 판으로만 이동 가능`;
|
|
257
289
|
}
|
|
258
290
|
for (const [version, items] of snapshots) for (const dependent of items) {
|
|
291
|
+
// A released version is history — its cells never pin a move (same rule as placement).
|
|
292
|
+
if (released && versionOrder(version, released) <= 0) continue;
|
|
259
293
|
if (dependent.id !== item.id && dependent.predecessors?.includes(item.id) && versionOrder(version, to.version) <= 0)
|
|
260
294
|
return `의존 칸 ${dependent.id} 이전 판으로만 이동 가능`;
|
|
261
295
|
}
|
|
@@ -289,7 +323,7 @@ export function rebalance(version: string, deps: PlacementDeps = {}): RebalanceR
|
|
|
289
323
|
const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
|
|
290
324
|
const result: RebalanceResult = { decisions: [], blocked: [] };
|
|
291
325
|
for (const { item, score } of unstarted) {
|
|
292
|
-
const violation = moveConstraint(item, next!, backlog, snapshots, now, rate, caps, rows, ceoCap);
|
|
326
|
+
const violation = moveConstraint(item, next!, backlog, snapshots, now, rate, caps, rows, ceoCap, deps.released ?? releasedVersion());
|
|
293
327
|
if (violation) {
|
|
294
328
|
result.blocked.push({ id: item.id, from: version, to: next!.version, reason: violation });
|
|
295
329
|
continue;
|
|
@@ -315,7 +349,7 @@ export function seatMove(id: string, from: string, to: string, by: string, reaso
|
|
|
315
349
|
if (!ownerMatches(item.owner, parseOwner(by).seat) || next?.version !== to) throw new CliUserError('남의 칸 당기기 거부 — COO 에 요청');
|
|
316
350
|
const now = (deps.now ?? new Date()).getTime();
|
|
317
351
|
const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
|
|
318
|
-
const violation = moveConstraint(item, next, backlog, snapshots, now, rate, deps.seatCap ?? placementSeatCap(), rows, validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap()));
|
|
352
|
+
const violation = moveConstraint(item, next, backlog, snapshots, now, rate, deps.seatCap ?? placementSeatCap(), rows, validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap()), deps.released ?? releasedVersion());
|
|
319
353
|
if (violation) throw new CliUserError(`${id}: ${violation}`);
|
|
320
354
|
if (!deps.dryRun) move(id, from, to, by, undefined, undefined, reason);
|
|
321
355
|
}
|
|
@@ -35,12 +35,13 @@ export function listSchedules(root?: string): ReleaseSchedule[] {
|
|
|
35
35
|
return schedules;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function setSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string }, by: string, root?: string): ReleaseSchedule {
|
|
38
|
+
export function setSchedule(version: string, patch: { cutAt?: string; landBy?: string; freezeFrom?: string; freezeUntil?: string; publishAt?: string }, by: string, root?: string): ReleaseSchedule {
|
|
39
39
|
const schedule = writeSchedule(version, {
|
|
40
40
|
...(patch.cutAt !== undefined ? { cutAt: utcIso(patch.cutAt) } : {}),
|
|
41
41
|
...(patch.landBy !== undefined ? { landBy: utcIso(patch.landBy) } : {}),
|
|
42
42
|
...(patch.freezeFrom !== undefined ? { freezeFrom: utcIso(patch.freezeFrom) } : {}),
|
|
43
43
|
...(patch.freezeUntil !== undefined ? { freezeUntil: utcIso(patch.freezeUntil) } : {}),
|
|
44
|
+
...(patch.publishAt !== undefined ? { publishAt: utcIso(patch.publishAt) } : {}),
|
|
44
45
|
}, by, root);
|
|
45
46
|
debug.log('release.schedule', 'set', { version, cutAt: schedule.cutAt, landBy: schedule.landBy });
|
|
46
47
|
return schedule;
|
|
@@ -51,5 +52,8 @@ export function formatSchedule(schedule: ReleaseSchedule): string {
|
|
|
51
52
|
const deadline = schedule.landBy ? formatKst(schedule.landBy) : null;
|
|
52
53
|
const sameDay = schedule.landBy && new Date(Date.parse(schedule.landBy) + 9 * 60 * 60_000).toISOString().slice(0, 10)
|
|
53
54
|
=== new Date(Date.parse(schedule.cutAt) + 9 * 60 * 60_000).toISOString().slice(0, 10);
|
|
54
|
-
|
|
55
|
+
const publish = schedule.publishAt ? formatKst(schedule.publishAt) : null;
|
|
56
|
+
const publishSameDay = schedule.publishAt && new Date(Date.parse(schedule.publishAt) + 9 * 60 * 60_000).toISOString().slice(0, 10)
|
|
57
|
+
=== new Date(Date.parse(schedule.cutAt) + 9 * 60 * 60_000).toISOString().slice(0, 10);
|
|
58
|
+
return `${schedule.version} 컷 ${cut}${deadline ? ` · 착지 마감 ${sameDay ? deadline.split(' ')[1] : deadline}` : ''}${publish ? ` · 발행 ${publishSameDay ? publish.split(' ')[1] : publish}` : ''}`;
|
|
55
59
|
}
|
|
@@ -2,6 +2,56 @@ import { Database } from 'bun:sqlite';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import type { ChecklistItem } from './checklist.js';
|
|
5
|
+
import { debug } from '../debug/log.js';
|
|
6
|
+
import { getUserConfig } from '../user-config.js';
|
|
7
|
+
|
|
8
|
+
export type RubricAxis = { key: string; weight: number };
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_PROJECT_AXES: readonly RubricAxis[] = [
|
|
11
|
+
{ key: 'V', weight: 2 }, { key: 'U', weight: 2 },
|
|
12
|
+
{ key: 'R', weight: 2 }, { key: 'S', weight: -0.5 },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const INTERNAL_AXES: readonly RubricAxis[] = [
|
|
16
|
+
{ key: 'A', weight: 2 }, { key: 'E', weight: 2 },
|
|
17
|
+
{ key: 'R', weight: 2 }, { key: 'D', weight: 1.5 },
|
|
18
|
+
{ key: 'M', weight: 1 }, { key: 'B', weight: 1 },
|
|
19
|
+
{ key: 'S', weight: -0.5 }, { key: 'X', weight: -0.5 },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export function rubricScoreWith(axes: readonly RubricAxis[], values: Readonly<{ [key: string]: number } | Rubric>): number {
|
|
23
|
+
const scores: Readonly<Record<string, number>> = values as Readonly<Record<string, number>>;
|
|
24
|
+
return axes.reduce((score, { key, weight }) => {
|
|
25
|
+
const value = Object.hasOwn(scores, key) ? scores[key] : undefined;
|
|
26
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(`루브릭 축 ${key} 값이 없다`);
|
|
27
|
+
if (typeof weight !== 'number' || !Number.isFinite(weight)) throw new Error(`루브릭 축 ${key} 가중이 유한수가 아니다`);
|
|
28
|
+
return score + weight * value;
|
|
29
|
+
}, 0);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* RFC §A4b⑤ — unconfigured projects (and the internal pack `elanous`) keep the internal eight axes;
|
|
34
|
+
* a project with `projects.<id>.rubric.axes` starts from V/U/R/S and replaces matching keys or appends new ones.
|
|
35
|
+
* An invalid axes entry raises its config error text here (loading the config itself stays non-fatal).
|
|
36
|
+
*/
|
|
37
|
+
export function resolveRubricAxes(projectId?: string): RubricAxis[] {
|
|
38
|
+
const projects = projectId && projectId !== 'elanous' ? getUserConfig().projects : undefined;
|
|
39
|
+
const rubric = projects && Object.hasOwn(projects, projectId!) ? projects[projectId!]?.rubric : undefined;
|
|
40
|
+
if (rubric?.error) {
|
|
41
|
+
debug.log('release.rubric', 'axes-invalid', { project: projectId, error: rubric.error });
|
|
42
|
+
throw new Error(rubric.error);
|
|
43
|
+
}
|
|
44
|
+
const configured = rubric?.axes;
|
|
45
|
+
const axes = (configured ? DEFAULT_PROJECT_AXES : INTERNAL_AXES).map((axis) => ({ ...axis }));
|
|
46
|
+
for (const axis of configured ?? []) {
|
|
47
|
+
const index = axes.findIndex((current) => current.key === axis.key);
|
|
48
|
+
if (index < 0) axes.push({ ...axis });
|
|
49
|
+
else axes[index] = { ...axis };
|
|
50
|
+
}
|
|
51
|
+
// An omitted project id is logged as null (unspecified) — not as `elanous` — even though it resolves to the internal axes.
|
|
52
|
+
debug.log('release.rubric', 'axes', { project: projectId ?? null, keys: axes.map(({ key }) => key) });
|
|
53
|
+
return axes;
|
|
54
|
+
}
|
|
5
55
|
|
|
6
56
|
export interface Rubric {
|
|
7
57
|
A: number;
|
|
@@ -33,7 +83,7 @@ export function parseRubric(text: string): Rubric | null {
|
|
|
33
83
|
}
|
|
34
84
|
|
|
35
85
|
export function rubricScore(r: Rubric): number {
|
|
36
|
-
return
|
|
86
|
+
return rubricScoreWith(INTERNAL_AXES, r);
|
|
37
87
|
}
|
|
38
88
|
|
|
39
89
|
export function rubricGrade(score: number): 'P1' | 'P2' | 'P3' | 'P4' {
|
|
@@ -43,6 +93,12 @@ export function rubricGrade(score: number): 'P1' | 'P2' | 'P3' | 'P4' {
|
|
|
43
93
|
return 'P4';
|
|
44
94
|
}
|
|
45
95
|
|
|
96
|
+
/** The rule-only split uses the same numeric rubric as the release checklist. */
|
|
97
|
+
export function rubricPriority(text: string): ReturnType<typeof rubricGrade> {
|
|
98
|
+
const rubric = parseRubric(text);
|
|
99
|
+
return rubric ? rubricGrade(rubricScore(rubric)) : 'P2';
|
|
100
|
+
}
|
|
101
|
+
|
|
46
102
|
/** Inspect an existing checklist without initializing, importing, or migrating its ledger. */
|
|
47
103
|
export function readRubricItems(version: string, ledgerRoot: string): Pick<ChecklistItem, 'id' | 'title' | 'evidence' | 'priority'>[] {
|
|
48
104
|
const dbPath = join(ledgerRoot, 'release', 'features.sqlite');
|
|
@@ -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
|
+
}
|