elanous 0.2.25 → 0.2.28
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/{1454-37fd16f479f5554a.js → 1454-29d1d773d5996a49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-24453f6a302816b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-6c131d30bb1cf871.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-b12149c51f7d04b1.js → page-fa46d4588e8442c2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-dc0772b11861af51.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-305cd294e754906e.js → page-256ffd365d32e100.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-dea4b795741126fd.js → page-e0de78ae7b99f01c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-45889e7070d11126.js → page-4c741eb760f2f77c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-16a2e893f83ae4d0.js → page-e9ba98f5f1440a21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-dafc901bb1b72b5f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-56888bc744e08dac.js → page-2f6f303fa114ec24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-1634081c5272006c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-952c6ceec6d9de38.js → page-ced0bcfc6542b3fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-6d0202af568f4c63.js → page-bb1f05cd51c38944.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-646b50f63b6fe732.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-4fb6fc34e20c07b7.js → page-f736324afa2117c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-e9502921f4f01b1f.js → page-6f131b51193a36f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-b6169ea2f09cf287.js → not-found-435f5e2faf02bc01.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-6a14333d1b1b3967.js → page-497e0eedf20e7890.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-8c91a9a66c9ec90a.js → page-a8d28fdf995e16ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8966452228753c5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-3d3fc73dd187ed52.js → page-66a67eb19ebd976b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-9c04dac0831c2bbb.js → page-c71267e7681da982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a21d1e1797172692.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-04de9ca6d52a3893.js → page-cb249e81ed339318.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-62527677bbc2fdc4.js → page-781cfc5114e097da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-9a2e09c4a311764c.js → page-478157214db6a023.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-bfeb35d69e8b24bb.js → page-86141f12cf090b7f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-89cb33bced824e4a.js → page-7256c73bb1c36751.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-6445ac4cbb8dd869.js → page-979d318a534e92da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-21050928a697ed7a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-20162174b77f96cf.js → page-601fb77340e63c9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-05a4b2c9b91034aa.js → page-11bf9e5725a72c22.js} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +13 -13
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/implement-loop.yaml +38 -1
- package/graphs/steward/recipes.yaml +6 -0
- package/graphs/steward/release-day.yaml +23 -0
- package/graphs/steward/steward.yaml +9 -1
- package/graphs/work/code-pr.yaml +23 -0
- package/package.json +1 -1
- package/scripts/browser-observe-cycle.ts +76 -2
- package/scripts/ci-typecheck-changed.ts +119 -7
- package/scripts/coord-msg-bridge.ts +209 -0
- package/scripts/coord-post.sh +40 -7
- package/scripts/coord-requests-bridge.ts +10 -5
- package/scripts/detect-test-interference.ts +31 -1
- package/scripts/expose-rubric-suggest.ts +1 -1
- package/scripts/measure-tsc-affected-roots.ts +10 -1
- package/scripts/release-loop/docs-node.ts +51 -22
- package/scripts/release-loop/export-check-node.ts +8 -5
- package/scripts/release-loop/gate-node.ts +37 -4
- package/scripts/release-loop/node-verdict.ts +10 -3
- package/scripts/release-loop/publish-node.ts +3 -0
- package/scripts/release-loop/tui-node-checks.ts +5 -2
- package/scripts/skills/public-check.ts +5 -2
- package/scripts/test-deterministic.ts +520 -37
- package/scripts/ux-sim.ts +7 -1
- package/scripts/verify-kpack6-live.ts +230 -0
- package/src/acp/codex-plugins.ts +17 -0
- package/src/agent/task-notification.ts +32 -2
- package/src/agent-mission/in-agent-plugin.ts +8 -3
- package/src/agent-substrate/review-observation.ts +9 -2
- package/src/agent-substrate/self-review-cli.ts +62 -16
- package/src/boot/daemon-tools/self-implement.ts +72 -4
- package/src/briefing/brief-items.ts +151 -35
- package/src/budget/llm-cost.ts +30 -3
- package/src/budget/usage-outcomes.ts +32 -0
- package/src/claims/claims-ledger.ts +23 -0
- package/src/claims/claims-render.ts +22 -2
- package/src/cli/brief-cli.ts +131 -10
- package/src/cli/claims-cli.ts +24 -4
- package/src/cli/control-cli.ts +29 -0
- package/src/cli/doctor-cli.ts +90 -8
- package/src/cli/msg-cli.ts +177 -11
- package/src/cli/plugin-cli.ts +11 -3
- package/src/cli/pr-cli.ts +133 -18
- package/src/cli/pr-land-overlap-evidence.ts +18 -10
- package/src/cli/pr-land-refusal.ts +20 -0
- package/src/cli/release-cli.ts +34 -5
- package/src/cli/rfc-cli.ts +24 -0
- package/src/cli/seat-cli.ts +121 -0
- package/src/cli/self-utterances-cli.ts +140 -0
- package/src/cli/tailscale-serve.ts +5 -8
- package/src/cli/tasks-cli.ts +528 -31
- package/src/cli/usage-cli.ts +9 -4
- package/src/context-card/cell.ts +40 -0
- package/src/dashboard/boot-timing.ts +35 -0
- package/src/dashboard/first-screen-band.ts +63 -13
- package/src/dashboard/index.ts +94 -27
- package/src/dashboard/input/chat-main-plain-turn-runtime.ts +26 -0
- package/src/dashboard/skill-detection-runtime.ts +24 -4
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +6 -0
- package/src/debug/log.ts +1 -1
- package/src/domains/morning-report.ts +3 -1
- package/src/domains/release-tool.ts +29 -12
- package/src/domains/surface-events.ts +16 -0
- package/src/elanous-config-dir.ts +2 -2
- package/src/fleet/host-doctor.ts +73 -2
- package/src/github/pr-labels.ts +10 -4
- package/src/graph-runner/runner.ts +104 -15
- package/src/harness/harness-clean.ts +21 -3
- package/src/harness/harness-cli-command.ts +68 -10
- package/src/harness/harness-pod-dispatch.ts +11 -4
- package/src/harness/harness-stop.ts +11 -7
- package/src/hq/seats.ts +17 -3
- package/src/index.ts +69 -9
- package/src/instance/current.ts +2 -6
- package/src/instance/resolve.ts +5 -6
- package/src/intelligence-map/model-bench-scores.ts +102 -0
- package/src/llm/openai-sampling-fields.ts +89 -0
- package/src/llm/runtime-model-family.ts +61 -0
- package/src/llm/zai-coding-plan.ts +27 -1
- package/src/llm.ts +13 -89
- package/src/loops/orchestrator/inbox.ts +73 -0
- package/src/loops/orchestrator/resync-shadow.ts +100 -12
- package/src/loops/orchestrator/tick.ts +73 -2
- package/src/machines/machine-ledger.ts +31 -3
- package/src/maturity/cli-maturity.ts +13 -3
- package/src/maturity/feature-maturity.ts +1 -1
- package/src/msg/msg-sse-server.ts +141 -0
- package/src/msg/msg-store.ts +149 -17
- package/src/ops/overview-collect.ts +3 -2
- package/src/ops/overview-owner.ts +29 -0
- package/src/patents/ledger.ts +128 -0
- package/src/plugin-hooks/shell-hook.ts +17 -2
- package/src/plugin-missions/evaluator-runner.ts +18 -2
- package/src/plugins/install/market-fetch.ts +32 -1
- package/src/plugins/install/plugin-install.ts +11 -1
- package/src/pod-lease/admission.ts +116 -41
- package/src/prompt-enhance/enhance.ts +11 -0
- package/src/release-loop/checklist.ts +52 -4
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/merged-pr-checklist.ts +24 -5
- package/src/release-loop/rubric.ts +70 -0
- package/src/rfc-registry/registry.ts +88 -0
- package/src/seat-dispatch/seat-request-ledger.ts +4 -0
- package/src/seat-loop/knowhow-lint.ts +233 -0
- package/src/seat-loop/seat-loop.ts +5 -1
- package/src/seat-loop/seat-pack.ts +16 -4
- package/src/seat-loop/traffic-signal.ts +7 -0
- package/src/self-dev/ask-launch-flow.ts +48 -8
- package/src/self-dev/ask-path-candidates.ts +69 -14
- package/src/self-dev/decompose-proposal.ts +43 -1
- package/src/self-dev/draft-sweep.ts +23 -14
- package/src/self-dev/graph-journey-nodes.ts +2 -0
- package/src/self-dev/launch-preflight-text.ts +5 -1
- package/src/self-dev/launch-preflight.ts +143 -10
- package/src/self-dev/orchestrate.ts +6 -2
- package/src/self-dev/run-supervisor.ts +35 -1
- package/src/self-implement/gate-cli.ts +6 -2
- package/src/self-implement/goal-author.ts +5 -28
- package/src/self-implement/goal-command-execution.ts +26 -0
- package/src/self-implement/goal-new-paths.ts +4 -5
- package/src/self-implement/graph-authority.ts +17 -3
- package/src/self-implement/graph-yaml.ts +15 -7
- package/src/self-implement/headless-elanous-driver.ts +16 -1
- package/src/self-implement/host-regate.ts +44 -9
- package/src/self-implement/orchestrator.ts +5 -11
- package/src/self-implement/pwa-import-graph.ts +194 -7
- package/src/self-implement/rework-policy.ts +80 -2
- package/src/self-implement/self-implement-runtime.ts +126 -21
- package/src/self-implement/speed-claims.ts +145 -6
- package/src/session/index.ts +14 -4
- package/src/session-runtime/index.ts +55 -11
- package/src/skills/tools/persistent-grounding.ts +59 -4
- package/src/skills/tools/shell.ts +79 -1
- package/src/steward/rejudge-tick.ts +12 -1
- package/src/steward/release-day.ts +158 -0
- package/src/steward/resync-tick.ts +123 -0
- package/src/task-agent/actions.ts +1 -0
- package/src/task-agent/adopt-pr.ts +140 -0
- package/src/task-agent/board.ts +11 -1
- package/src/task-agent/card-evidence.ts +37 -5
- package/src/task-agent/cover.ts +105 -19
- package/src/task-agent/failure-notify.ts +61 -0
- package/src/task-agent/green-sweep.ts +170 -0
- package/src/task-agent/land-worktree.ts +52 -5
- package/src/task-agent/live-move-history.ts +1 -0
- package/src/task-agent/live-moves.ts +56 -17
- package/src/task-agent/parent-host.ts +1 -1
- package/src/task-agent/postland-card.ts +139 -0
- package/src/task-agent/rejudge-on-head.ts +321 -25
- package/src/task-agent/remote-rejudge-bind.ts +62 -0
- package/src/task-agent/repair-branch-cas.ts +182 -0
- package/src/task-agent/repair-cycle.ts +275 -0
- package/src/task-agent/repair-next-move.ts +31 -0
- package/src/task-agent/repair-review.ts +24 -0
- package/src/task-agent/repair-worker.ts +87 -0
- package/src/task-agent/review-result-capture.ts +66 -2
- package/src/task-agent/shadow.ts +150 -29
- package/src/task-agent/task-hand.ts +133 -8
- package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +71 -12
- package/src/telegram.ts +30 -1
- package/src/tool-runtime/tool-search-runtime.ts +25 -0
- package/src/tui.ts +20 -1
- package/src/user-config.ts +11 -60
- package/src/ux-sim/fault-sqlite-busy.ts +50 -0
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-9360b1285b2a346e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f08f6529d09ffae1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f000d54d258f9ef6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-22f674e7e4c2c9d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-8888733e40cea6d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9717ec6964b92276.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-44eaad486bc98a45.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-97fa6cf3d34c7eb9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-35e05d7450461a67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-b2638013c0bf7dfd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-c44f93a41167536f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1b810474507f3702.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-74366ae2693a7d95.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4e634c5b98fd4e93.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ed3070d637850548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-b9cc8a10bc6078b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-128458c8d86fbb5f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-cf0c2b0183991d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-b008aff96be56654.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-814394dcbae789b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-6a87325c87f09dd2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-852170303c6cd755.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-087ceeb975ef272b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-97c7361cc2a19319.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-8c6901833f897496.js +0 -1
- /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
|
@@ -15,21 +15,27 @@ export interface PodLeaseAdmissionOptions {
|
|
|
15
15
|
dependencyMerged?: (after: PodLeasePredecessor, signal?: AbortSignal) => PredecessorState | boolean | Promise<PredecessorState | boolean>;
|
|
16
16
|
/** Recheck even when there are no local releases (another launcher may free a slot). */
|
|
17
17
|
pollMs?: number;
|
|
18
|
+
/** Maximum time spent waiting for a free slot; defaults to 30 minutes (predecessor waits do not count). */
|
|
19
|
+
maxWaitMs?: number;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/** FIFO admission across launches sharing this instance. Unknown status never grants a slot. */
|
|
21
23
|
export class PodLeaseAdmission {
|
|
22
|
-
private readonly queue: Array<{ resolve: (release: PodLeaseRelease) => void; reject: (error: Error) => void; signal?: AbortSignal; after?: PodLeasePredecessor; onAbort?: () => void; cancelCheck?: () => void; waitReason?: string }> = [];
|
|
24
|
+
private readonly queue: Array<{ resolve: (release: PodLeaseRelease) => void; reject: (error: Error) => void; signal?: AbortSignal; after?: PodLeasePredecessor; onAbort?: () => void; cancelCheck?: () => void; checkingDependency?: boolean; predecessorMerged?: boolean; waitReason?: string; waitStartedAt?: number }> = [];
|
|
23
25
|
private active = 0;
|
|
24
26
|
private limit = 0;
|
|
25
27
|
private checking: Promise<void> | undefined;
|
|
26
28
|
private recheckRequested = false;
|
|
27
29
|
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
30
|
+
private expiryTimer: ReturnType<typeof setTimeout> | undefined;
|
|
28
31
|
private readonly pollMs: number;
|
|
32
|
+
private readonly maxWaitMs: number;
|
|
29
33
|
|
|
30
34
|
constructor(private readonly options: PodLeaseAdmissionOptions) {
|
|
31
35
|
this.pollMs = options.pollMs ?? 15_000;
|
|
36
|
+
this.maxWaitMs = options.maxWaitMs ?? 30 * 60_000;
|
|
32
37
|
if (!Number.isFinite(this.pollMs) || this.pollMs <= 0) throw new Error('pod lease pollMs must be positive');
|
|
38
|
+
if (!Number.isFinite(this.maxWaitMs) || this.maxWaitMs <= 0) throw new Error('pod lease maxWaitMs must be positive');
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
/** Queue view: each waiting item says why it waits (predecessor not merged yet, or no free slot). */
|
|
@@ -42,19 +48,18 @@ export class PodLeaseAdmission {
|
|
|
42
48
|
acquire(signal?: AbortSignal, after?: PodLeasePredecessor): Promise<PodLeaseRelease> {
|
|
43
49
|
if (signal?.aborted) return Promise.reject(new Error('pod lease admission aborted'));
|
|
44
50
|
return new Promise((resolve, reject) => {
|
|
45
|
-
const waiter: (typeof this.queue)[number] = { resolve, reject, signal, after
|
|
51
|
+
const waiter: (typeof this.queue)[number] = { resolve, reject, signal, after,
|
|
52
|
+
...(after === undefined ? { waitStartedAt: Date.now(), waitReason: 'no-free-slot' }
|
|
53
|
+
: { waitReason: `predecessor-unmerged:${String(after)}` }) };
|
|
46
54
|
if (signal) {
|
|
47
55
|
waiter.onAbort = () => {
|
|
48
|
-
|
|
49
|
-
if (index < 0) return;
|
|
50
|
-
this.queue.splice(index, 1);
|
|
51
|
-
waiter.cancelCheck?.();
|
|
56
|
+
if (!this.removeWaiter(waiter)) return;
|
|
52
57
|
reject(new Error('pod lease admission aborted'));
|
|
53
|
-
this.schedule();
|
|
54
58
|
};
|
|
55
59
|
signal.addEventListener('abort', waiter.onAbort, { once: true });
|
|
56
60
|
}
|
|
57
61
|
this.queue.push(waiter);
|
|
62
|
+
this.scheduleExpiry();
|
|
58
63
|
void this.refresh();
|
|
59
64
|
});
|
|
60
65
|
}
|
|
@@ -83,41 +88,29 @@ export class PodLeaseAdmission {
|
|
|
83
88
|
} catch {
|
|
84
89
|
this.limit = 0;
|
|
85
90
|
}
|
|
86
|
-
//
|
|
91
|
+
// Start independent predecessor checks before admitting eligible waiters in queue order.
|
|
92
|
+
// A slow predecessor cannot reserve a slot or delay a follower's admission decision.
|
|
93
|
+
this.expireWaiters();
|
|
94
|
+
const checks = this.queue.filter((waiter) => waiter.after !== undefined && !waiter.checkingDependency && !waiter.predecessorMerged)
|
|
95
|
+
.map((waiter) => this.checkPredecessor(waiter));
|
|
96
|
+
if (checks.length) {
|
|
97
|
+
// Let ready states settle, but do not hold a free slot for an unanswered dependency check.
|
|
98
|
+
await Promise.race([Promise.all(checks), new Promise<void>((resolve) => { setTimeout(resolve, 0); })]);
|
|
99
|
+
}
|
|
100
|
+
this.expireWaiters();
|
|
87
101
|
let index = 0;
|
|
88
|
-
while (
|
|
102
|
+
while (index < this.queue.length) {
|
|
89
103
|
const waiter = this.queue[index]!;
|
|
90
|
-
if (waiter.after !== undefined) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
]);
|
|
99
|
-
state = result === true ? 'merged' : result === false || result === undefined ? 'waiting' : result;
|
|
100
|
-
} catch {
|
|
101
|
-
// An unavailable dependency state cannot grant admission.
|
|
102
|
-
} finally {
|
|
103
|
-
waiter.cancelCheck = undefined;
|
|
104
|
-
}
|
|
105
|
-
if (!this.queue.includes(waiter)) continue; // aborted while checking
|
|
106
|
-
if (state === 'blocked') {
|
|
107
|
-
this.queue.splice(this.queue.indexOf(waiter), 1);
|
|
108
|
-
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener('abort', waiter.onAbort);
|
|
109
|
-
debug.log('pod-lease', 'predecessor-blocked', { after: waiter.after });
|
|
110
|
-
waiter.reject(new Error(`pod lease predecessor blocked: ${String(waiter.after)} was closed without merging`));
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (state !== 'merged') {
|
|
114
|
-
waiter.waitReason = `predecessor-unmerged:${String(waiter.after)}`;
|
|
115
|
-
index++;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
104
|
+
if (waiter.after !== undefined && !waiter.predecessorMerged) {
|
|
105
|
+
index++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (this.active >= this.limit) {
|
|
109
|
+
waiter.waitReason = 'no-free-slot';
|
|
110
|
+
index++;
|
|
111
|
+
continue;
|
|
118
112
|
}
|
|
119
|
-
this.
|
|
120
|
-
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener('abort', waiter.onAbort);
|
|
113
|
+
this.removeWaiter(waiter);
|
|
121
114
|
this.active++;
|
|
122
115
|
let released = false;
|
|
123
116
|
waiter.resolve(() => {
|
|
@@ -127,21 +120,103 @@ export class PodLeaseAdmission {
|
|
|
127
120
|
void this.refresh();
|
|
128
121
|
});
|
|
129
122
|
}
|
|
130
|
-
for (const waiter of this.queue.slice(index)) if (!waiter.waitReason?.startsWith('predecessor-')) waiter.waitReason = 'no-free-slot';
|
|
131
123
|
} while (this.recheckRequested && this.queue.length > 0);
|
|
132
124
|
} finally {
|
|
133
125
|
this.checking = undefined;
|
|
126
|
+
this.scheduleExpiry();
|
|
134
127
|
this.schedule();
|
|
135
128
|
}
|
|
136
129
|
}
|
|
137
130
|
|
|
131
|
+
private async checkPredecessor(waiter: (typeof this.queue)[number]): Promise<void> {
|
|
132
|
+
waiter.checkingDependency = true;
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
let state: PredecessorState = 'waiting';
|
|
135
|
+
try {
|
|
136
|
+
const check = this.options.dependencyMerged?.(waiter.after!, controller.signal);
|
|
137
|
+
const result = await Promise.race([
|
|
138
|
+
Promise.resolve(check),
|
|
139
|
+
new Promise<PredecessorState>((resolve) => { waiter.cancelCheck = () => { controller.abort(); resolve('waiting'); }; }),
|
|
140
|
+
]);
|
|
141
|
+
state = result === true ? 'merged' : result === false || result === undefined ? 'waiting' : result;
|
|
142
|
+
} catch {
|
|
143
|
+
// An unavailable dependency state cannot grant admission.
|
|
144
|
+
} finally {
|
|
145
|
+
waiter.cancelCheck = undefined;
|
|
146
|
+
waiter.checkingDependency = false;
|
|
147
|
+
}
|
|
148
|
+
if (!this.queue.includes(waiter)) return;
|
|
149
|
+
if (state === 'blocked') {
|
|
150
|
+
this.removeWaiter(waiter);
|
|
151
|
+
debug.log('pod-lease', 'predecessor-blocked', { after: waiter.after });
|
|
152
|
+
waiter.reject(new Error(`pod lease predecessor blocked: ${String(waiter.after)} was closed without merging`));
|
|
153
|
+
} else if (state === 'merged') {
|
|
154
|
+
waiter.predecessorMerged = true;
|
|
155
|
+
waiter.waitStartedAt = Date.now();
|
|
156
|
+
waiter.waitReason = 'no-free-slot';
|
|
157
|
+
this.scheduleExpiry();
|
|
158
|
+
void this.refresh();
|
|
159
|
+
} else {
|
|
160
|
+
waiter.waitReason = `predecessor-unmerged:${String(waiter.after)}`;
|
|
161
|
+
waiter.waitStartedAt = undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private expireWaiters(): void {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
for (let index = 0; index < this.queue.length;) {
|
|
168
|
+
const waiter = this.queue[index]!;
|
|
169
|
+
if (waiter.waitReason !== 'no-free-slot' || waiter.waitStartedAt === undefined ||
|
|
170
|
+
now - waiter.waitStartedAt <= this.maxWaitMs) {
|
|
171
|
+
index++;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
this.removeWaiter(waiter);
|
|
175
|
+
const waitedMs = now - waiter.waitStartedAt;
|
|
176
|
+
const reason = waiter.waitReason;
|
|
177
|
+
debug.log('pod-lease', 'admit-starved', { waitedMs, reason, after: waiter.after });
|
|
178
|
+
waiter.reject(new Error(`pod lease admit-starved: waited ${Math.round(waitedMs / 60_000)}m · reason ${reason}`));
|
|
179
|
+
}
|
|
180
|
+
this.scheduleExpiry();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Every queue exit also updates both timers; an empty queue must retain no timer. */
|
|
184
|
+
private removeWaiter(waiter: (typeof this.queue)[number]): boolean {
|
|
185
|
+
const index = this.queue.indexOf(waiter);
|
|
186
|
+
if (index < 0) return false;
|
|
187
|
+
this.queue.splice(index, 1);
|
|
188
|
+
if (waiter.signal && waiter.onAbort) waiter.signal.removeEventListener('abort', waiter.onAbort);
|
|
189
|
+
waiter.cancelCheck?.();
|
|
190
|
+
this.scheduleExpiry();
|
|
191
|
+
if (!this.checking || this.queue.length === 0) this.schedule();
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private scheduleExpiry(): void {
|
|
196
|
+
if (this.expiryTimer) clearTimeout(this.expiryTimer);
|
|
197
|
+
this.expiryTimer = undefined;
|
|
198
|
+
const now = Date.now();
|
|
199
|
+
let delay = Infinity;
|
|
200
|
+
for (const waiter of this.queue) {
|
|
201
|
+
if (waiter.waitReason === 'no-free-slot' && waiter.waitStartedAt !== undefined) {
|
|
202
|
+
delay = Math.min(delay, Math.max(1, waiter.waitStartedAt + this.maxWaitMs - now + 1));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (Number.isFinite(delay)) {
|
|
206
|
+
this.expiryTimer = setTimeout(() => {
|
|
207
|
+
this.expiryTimer = undefined;
|
|
208
|
+
this.expireWaiters();
|
|
209
|
+
}, delay);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
138
213
|
private schedule(): void {
|
|
139
214
|
if (!this.queue.length) {
|
|
140
215
|
if (this.timer) clearTimeout(this.timer);
|
|
141
216
|
this.timer = undefined;
|
|
142
217
|
return;
|
|
143
218
|
}
|
|
144
|
-
if (this.timer)
|
|
219
|
+
if (this.timer) clearTimeout(this.timer);
|
|
145
220
|
this.timer = setTimeout(() => {
|
|
146
221
|
this.timer = undefined;
|
|
147
222
|
void this.refresh();
|
|
@@ -393,6 +393,8 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
|
|
|
393
393
|
let complication: string | undefined;
|
|
394
394
|
let decisionSignal: EnhanceResult['decisionSignal'];
|
|
395
395
|
let enhancedBy: 'llm' | 'fallback' = 'llm';
|
|
396
|
+
let fallbackReason: 'llm-error' | 'empty-reply' | 'unparsed' | 'none' = 'none';
|
|
397
|
+
let replyChars = 0;
|
|
396
398
|
const groundedFacts = opts.groundedFacts ?? [];
|
|
397
399
|
const wantsScqa = groundedFacts.length > 0;
|
|
398
400
|
|
|
@@ -439,6 +441,10 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
|
|
|
439
441
|
try { opts.onUsage?.(usage); } catch { /* usage observation must not change enhancement */ }
|
|
440
442
|
},
|
|
441
443
|
});
|
|
444
|
+
replyChars = rawOut.length;
|
|
445
|
+
if (rawOut.trim().length === 0) {
|
|
446
|
+
fallbackReason = 'empty-reply';
|
|
447
|
+
}
|
|
442
448
|
const parsed = parseEnhanceJson(rawOut);
|
|
443
449
|
if (parsed) {
|
|
444
450
|
goal = parsed.goal;
|
|
@@ -449,9 +455,11 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
|
|
|
449
455
|
complication = parsed.complication;
|
|
450
456
|
decisionSignal = parsed.decisionSignal;
|
|
451
457
|
} else {
|
|
458
|
+
fallbackReason = fallbackReason === 'empty-reply' ? 'empty-reply' : 'unparsed';
|
|
452
459
|
enhancedBy = 'fallback';
|
|
453
460
|
}
|
|
454
461
|
} catch (e) {
|
|
462
|
+
fallbackReason = 'llm-error';
|
|
455
463
|
debug.log('prompt-enhance', 'llm-error', { error: String((e as { message?: string })?.message ?? e).slice(0, 160) }, { level: 'error' });
|
|
456
464
|
enhancedBy = 'fallback';
|
|
457
465
|
}
|
|
@@ -461,6 +469,7 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
|
|
|
461
469
|
checklist = extractChecklistFallback(original);
|
|
462
470
|
checklistProvenance = checklist.map(() => 'unknown');
|
|
463
471
|
enhancedBy = 'fallback';
|
|
472
|
+
if (fallbackReason === 'none') fallbackReason = 'unparsed';
|
|
464
473
|
}
|
|
465
474
|
if (!constraints.length) {
|
|
466
475
|
constraints = [
|
|
@@ -515,6 +524,8 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
|
|
|
515
524
|
enhancedChars: enhanced.length,
|
|
516
525
|
checklist: checklist.length,
|
|
517
526
|
enhancedBy,
|
|
527
|
+
fallbackReason,
|
|
528
|
+
replyChars,
|
|
518
529
|
model,
|
|
519
530
|
verbatimPreserved,
|
|
520
531
|
// ⭐ 관측 — 「요약을 요구했나 / 받았나 / 옮겨 적어 버렸나」를 «다른 값»으로 남긴다.
|
|
@@ -10,6 +10,8 @@ export type ChecklistDisposition = 'move' | 'known-issue' | 'block';
|
|
|
10
10
|
/** `screen` = a five-surface feature cell — its evidence carries a `짝:` line (MANUAL-five-surface-parity §A). `postpub` = measured after publication, outside the pre-publication gate. */
|
|
11
11
|
export type ChecklistKind = 'screen' | 'postpub';
|
|
12
12
|
const CHECKLIST_KINDS: readonly ChecklistKind[] = ['screen', 'postpub'];
|
|
13
|
+
export type ChecklistDispatchKind = 'code' | 'ops' | 'measure' | 'decision' | 'umbrella';
|
|
14
|
+
const CHECKLIST_DISPATCH_KINDS: readonly ChecklistDispatchKind[] = ['code', 'ops', 'measure', 'decision', 'umbrella'];
|
|
13
15
|
export interface ChecklistItem {
|
|
14
16
|
id: string;
|
|
15
17
|
title: string;
|
|
@@ -27,6 +29,8 @@ export interface ChecklistItem {
|
|
|
27
29
|
evidence?: string;
|
|
28
30
|
disposition?: ChecklistDisposition;
|
|
29
31
|
kind?: ChecklistKind;
|
|
32
|
+
/** Whether the seat dispatcher should launch a coding task. Missing in old rows means code. */
|
|
33
|
+
dispatchKind?: ChecklistDispatchKind;
|
|
30
34
|
updatedAt: string;
|
|
31
35
|
updatedBy: string;
|
|
32
36
|
}
|
|
@@ -122,6 +126,37 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
|
|
|
122
126
|
|
|
123
127
|
const REPO_ROOT = join(import.meta.dir, '..', '..');
|
|
124
128
|
|
|
129
|
+
const PATH_TOKEN = /(?:^|[^A-Za-z0-9_.\/-])((?:(?:[A-Za-z0-9_.-]+\/)+)?[A-Za-z0-9_.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|yaml|yml|sh|py|css|html|toml|sql|rs|swift))(?![A-Za-z0-9_.\/-])/g;
|
|
130
|
+
function mentionedPaths(text: string): string[] {
|
|
131
|
+
return [...text.matchAll(PATH_TOKEN)].map((match) => match[1]!).filter((path) => !path.split('/').includes('..'));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Shadow check at checklist authorship. Callers supply the origin/main file probe and merged-PR file reader. */
|
|
135
|
+
export function inspectChecklistTargetPaths(input: {
|
|
136
|
+
title: string; evidence?: string; refs?: readonly string[]; dispatchKind?: ChecklistDispatchKind;
|
|
137
|
+
}, deps: { existsAtMain: (path: string) => boolean; mergedPrFiles?: (number: number) => readonly string[] }): {
|
|
138
|
+
targets: string[]; warnings: string[]; candidates: string[];
|
|
139
|
+
} {
|
|
140
|
+
const lines = input.title.split(/\r?\n/);
|
|
141
|
+
const first = /^대상 경로:\s*(.*)$/.exec(lines[0] ?? '');
|
|
142
|
+
const targets = first ? [...new Set(mentionedPaths(first[1]!))] : [];
|
|
143
|
+
const newFileDeclared = /^신설:\s*/.test(lines[1] ?? '');
|
|
144
|
+
const warnings: string[] = [];
|
|
145
|
+
if ((input.dispatchKind ?? 'code') === 'code') {
|
|
146
|
+
if (!first || !first[1]!.trim() || targets.length === 0) warnings.push('첫 줄 대상 경로가 없다');
|
|
147
|
+
for (const path of targets) if (!deps.existsAtMain(path) && !newFileDeclared) warnings.push(`${path}: origin/main에 없는 파일 · 신설: 둘째 줄 선언 필요`);
|
|
148
|
+
}
|
|
149
|
+
if (!warnings.length) return { targets, warnings, candidates: [] };
|
|
150
|
+
const sources = [input.title, input.evidence ?? '', ...(input.refs ?? [])].join('\n');
|
|
151
|
+
const candidates = [...new Set(mentionedPaths(sources).filter(deps.existsAtMain))];
|
|
152
|
+
for (const match of sources.matchAll(/(?:^|[^\d])#(\d{2,8})\b/g)) {
|
|
153
|
+
for (const path of deps.mergedPrFiles?.(Number(match[1])) ?? []) {
|
|
154
|
+
if (deps.existsAtMain(path) && !candidates.includes(path)) candidates.push(path);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return { targets, warnings, candidates };
|
|
158
|
+
}
|
|
159
|
+
|
|
125
160
|
/** `--ref` 문서를 찾는 뿌리들 — 이 코드의 트리 ⊕ 지금 작업 디렉토리의 git 뿌리.
|
|
126
161
|
* ⭐ 설치본(`~/.local/share/elanous/current`)에는 `docs/` 가 없다 — 저장소 안에서 부른 설치본도 저장소 문서를 인용할 수 있게. */
|
|
127
162
|
export function refRoots(cwd = process.cwd()): string[] {
|
|
@@ -233,10 +268,11 @@ export function validateCeoLoad(input: { ceoMinutes?: number; ceoDate?: string }
|
|
|
233
268
|
if (input.ceoDate !== undefined && (!/^\d{4}-\d{2}-\d{2}$/.test(input.ceoDate) || !Number.isFinite(Date.parse(`${input.ceoDate}T00:00:00Z`)) || new Date(`${input.ceoDate}T00:00:00Z`).toISOString().slice(0, 10) !== input.ceoDate)) throw new CliUserError('대표 손 날짜는 YYYY-MM-DD 이어야 한다');
|
|
234
269
|
}
|
|
235
270
|
|
|
236
|
-
export function addItem(v: string, input: { id: string; title: string; refs?: string[]; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
|
|
271
|
+
export function addItem(v: string, input: { id: string; title: string; refs?: string[]; owner?: string; kind?: ChecklistKind; dispatchKind?: ChecklistDispatchKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
|
|
237
272
|
if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
|
|
238
273
|
if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
|
|
239
274
|
if (input.kind !== undefined && !CHECKLIST_KINDS.includes(input.kind)) throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen|postpub');
|
|
275
|
+
if (input.dispatchKind !== undefined && !CHECKLIST_DISPATCH_KINDS.includes(input.dispatchKind)) throw new CliUserError(`잘못된 발사 종류: ${input.dispatchKind}`, 'code|ops|measure|decision|umbrella');
|
|
240
276
|
if (input.owner !== undefined) parseOwner(input.owner);
|
|
241
277
|
if (input.priority !== undefined && !['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
|
|
242
278
|
if (input.predecessors !== undefined && (!Array.isArray(input.predecessors) || input.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === input.id))) throw new CliUserError('잘못된 선행 칸');
|
|
@@ -255,20 +291,32 @@ export function addItem(v: string, input: { id: string; title: string; refs?: st
|
|
|
255
291
|
assertPredecessorsExist(input.predecessors, data, otherItems);
|
|
256
292
|
const by = process.env.ELANOUS_TRACK || 'cli';
|
|
257
293
|
const at = new Date().toISOString();
|
|
258
|
-
const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), ...(refs ? { refs } : {}), updatedAt: at, updatedBy: by };
|
|
294
|
+
const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.dispatchKind !== undefined ? { dispatchKind: input.dispatchKind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), ...(refs ? { refs } : {}), updatedAt: at, updatedBy: by };
|
|
259
295
|
data.items.push(item);
|
|
260
296
|
change(data, item.id, 'add', null, item, by, at);
|
|
261
297
|
return true;
|
|
262
298
|
});
|
|
263
299
|
}
|
|
264
300
|
|
|
265
|
-
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; refs?: string[]; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
|
|
301
|
+
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; refs?: string[]; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; dispatchKind?: ChecklistDispatchKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
|
|
302
|
+
return setItemGuarded(v, id, patch, by);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Apply a measured verdict only while the release cell is still the snapshot that was judged. */
|
|
306
|
+
export function setItemIfSnapshot(v: string, id: string, patch: Parameters<typeof setItem>[2], by: string, expected: Pick<ChecklistItem, 'title' | 'status' | 'evidence'>): Checklist {
|
|
307
|
+
return setItemGuarded(v, id, patch, by, expected);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function setItemGuarded(v: string, id: string, patch: Parameters<typeof setItem>[2], by: string, expected?: Pick<ChecklistItem, 'title' | 'status' | 'evidence'>): Checklist {
|
|
266
311
|
return mutate(v, (data, otherItems) => {
|
|
267
312
|
const item = data.items.find((i) => i.id === id);
|
|
268
313
|
if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
|
|
314
|
+
if (expected && (item.title !== expected.title || item.status !== expected.status || item.evidence !== expected.evidence))
|
|
315
|
+
throw new CliUserError(`판정 중 칸 변경: ${id}`, '현재 칸으로 다시 측정한다');
|
|
269
316
|
if (patch.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(patch.status)) throw new CliUserError(`잘못된 상태: ${patch.status}`);
|
|
270
317
|
if (patch.disposition !== undefined && !['move', 'known-issue', 'block'].includes(patch.disposition)) throw new CliUserError(`잘못된 처분: ${patch.disposition}`);
|
|
271
318
|
if (patch.kind !== undefined && !CHECKLIST_KINDS.includes(patch.kind)) throw new CliUserError(`잘못된 종류: ${patch.kind}`, 'screen|postpub');
|
|
319
|
+
if (patch.dispatchKind !== undefined && !CHECKLIST_DISPATCH_KINDS.includes(patch.dispatchKind)) throw new CliUserError(`잘못된 발사 종류: ${patch.dispatchKind}`, 'code|ops|measure|decision|umbrella');
|
|
272
320
|
if (patch.priority !== undefined && !['P0', 'P1', 'P2'].includes(patch.priority)) throw new CliUserError(`잘못된 우선순위: ${patch.priority}`);
|
|
273
321
|
if (patch.predecessors !== undefined && (!Array.isArray(patch.predecessors) || patch.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === id))) throw new CliUserError('잘못된 선행 칸');
|
|
274
322
|
assertPredecessorsExist(patch.predecessors, data, otherItems);
|
|
@@ -283,7 +331,7 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
|
|
|
283
331
|
throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
|
|
284
332
|
}
|
|
285
333
|
}
|
|
286
|
-
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
|
|
334
|
+
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'dispatchKind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
|
|
287
335
|
if (fields.length === 0) return false;
|
|
288
336
|
const at = new Date().toISOString();
|
|
289
337
|
for (const field of fields) {
|
|
@@ -36,7 +36,7 @@ const walWait = new Int32Array(new SharedArrayBuffer(4));
|
|
|
36
36
|
const SCHEMA_COLUMNS: Record<string, readonly string[]> = {
|
|
37
37
|
imported_versions: ['json_hash', 'imported_at'],
|
|
38
38
|
events: ['reason', 'generation'],
|
|
39
|
-
assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator', 'refs'],
|
|
39
|
+
assignments: ['priority', 'predecessors', 'deadline_version', 'ceo_minutes', 'ceo_date', 'accelerator', 'refs', 'dispatch_kind'],
|
|
40
40
|
release_schedules: ['freeze_from', 'freeze_until', 'publish_at'],
|
|
41
41
|
};
|
|
42
42
|
function schemaCurrent(db: Database): boolean {
|
|
@@ -81,7 +81,7 @@ function open(root = releaseLedgerRoot()): Database {
|
|
|
81
81
|
if (!eventColumns.some((column) => column.name === 'generation')) db.exec('ALTER TABLE events ADD COLUMN generation INTEGER');
|
|
82
82
|
db.exec('CREATE TABLE IF NOT EXISTS release_schedules (version TEXT PRIMARY KEY, cut_at TEXT NOT NULL, land_by TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL)');
|
|
83
83
|
const assignmentColumns = db.query('PRAGMA table_info(assignments)').all() as Array<{ name: string }>;
|
|
84
|
-
for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER'], ['refs', 'TEXT']] as const) {
|
|
84
|
+
for (const [name, sql] of [['priority', 'TEXT'], ['predecessors', 'TEXT'], ['deadline_version', 'TEXT'], ['ceo_minutes', 'INTEGER'], ['ceo_date', 'TEXT'], ['accelerator', 'INTEGER'], ['refs', 'TEXT'], ['dispatch_kind', 'TEXT']] as const) {
|
|
85
85
|
if (!assignmentColumns.some((column) => column.name === name)) db.exec(`ALTER TABLE assignments ADD COLUMN ${name} ${sql}`);
|
|
86
86
|
}
|
|
87
87
|
const scheduleColumns = db.query('PRAGMA table_info(release_schedules)').all() as Array<{ name: string }>;
|
|
@@ -178,12 +178,13 @@ function evidenceRefs(db: Database, version: string, id: string): string[] {
|
|
|
178
178
|
return (db.query('SELECT ref FROM evidence WHERE feature_id = ? AND version = ? ORDER BY rowid').all(id, version) as Array<{ ref: string }>).map((row) => row.ref);
|
|
179
179
|
}
|
|
180
180
|
function items(db: Database, version: string): ChecklistItem[] {
|
|
181
|
-
const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.accelerator, a.refs, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
|
|
181
|
+
const rows = db.query(`SELECT f.id, COALESCE(a.title_override, f.title) AS title, a.owner, a.kind, a.dispatch_kind, a.priority, a.predecessors, a.deadline_version, a.ceo_minutes, a.ceo_date, a.accelerator, a.refs, a.status, a.disposition, a.evidence, a.updated_at, a.updated_by
|
|
182
182
|
FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.version = ? ORDER BY a.rowid`).all(version) as Array<Record<string, string | number | null>>;
|
|
183
183
|
return rows.map((r) => {
|
|
184
184
|
const evidence = [r.evidence, ...evidenceRefs(db, version, r.id as string)].filter((value) => value !== null).join('\n');
|
|
185
185
|
return { id: r.id as string, title: r.title as string, status: r.status as ChecklistItem['status'],
|
|
186
186
|
...(r.owner !== null ? { owner: r.owner as string } : {}), ...(r.kind !== null ? { kind: r.kind as ChecklistItem['kind'] } : {}),
|
|
187
|
+
...(r.dispatch_kind !== null ? { dispatchKind: r.dispatch_kind as ChecklistItem['dispatchKind'] } : {}),
|
|
187
188
|
...(r.priority !== null ? { priority: r.priority as ChecklistItem['priority'] } : {}), ...(r.predecessors !== null ? { predecessors: JSON.parse(r.predecessors as string) as string[] } : {}),
|
|
188
189
|
...(r.deadline_version !== null ? { deadlineVersion: r.deadline_version as string } : {}),
|
|
189
190
|
...(r.ceo_minutes !== null ? { ceoMinutes: r.ceo_minutes as number } : {}), ...(r.ceo_date !== null ? { ceoDate: r.ceo_date as string } : {}),
|
|
@@ -202,9 +203,9 @@ function putItem(db: Database, version: string, item: ChecklistItem): void {
|
|
|
202
203
|
if (!feature) db.query('INSERT INTO features (id, title, owner, kind, created_at) VALUES (?, ?, ?, ?, ?)').run(item.id, item.title, item.owner ?? null, item.kind ?? null, item.updatedAt);
|
|
203
204
|
// features.owner/kind follow the latest assignment so details() and the checklist never disagree after a set.
|
|
204
205
|
else if (item.owner !== undefined || item.kind !== undefined) db.query('UPDATE features SET owner = COALESCE(?, owner), kind = COALESCE(?, kind) WHERE id = ?').run(item.owner ?? null, item.kind ?? null, item.id);
|
|
205
|
-
db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
206
|
-
ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, accelerator=excluded.accelerator, refs=excluded.refs, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
207
|
-
.run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
|
|
206
|
+
db.query(`INSERT INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, dispatch_kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
207
|
+
ON CONFLICT(feature_id, version) DO UPDATE SET status=excluded.status, disposition=excluded.disposition, evidence=excluded.evidence, owner=excluded.owner, kind=excluded.kind, dispatch_kind=excluded.dispatch_kind, priority=excluded.priority, predecessors=excluded.predecessors, deadline_version=excluded.deadline_version, ceo_minutes=excluded.ceo_minutes, ceo_date=excluded.ceo_date, accelerator=excluded.accelerator, refs=excluded.refs, updated_at=excluded.updated_at, updated_by=excluded.updated_by`)
|
|
208
|
+
.run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, feature && feature.title !== item.title ? item.title : null, item.owner ?? null, item.kind ?? null, item.dispatchKind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
|
|
208
209
|
}
|
|
209
210
|
function jsonHash(contents: string | Buffer): string { return createHash('sha256').update(contents).digest('hex'); }
|
|
210
211
|
|
|
@@ -245,8 +246,8 @@ function importOnDb(db: Database, version: string): boolean {
|
|
|
245
246
|
// Keep a legacy per-version title until an explicit retitle unifies the feature identity.
|
|
246
247
|
if (!existing) putItem(db, version, item);
|
|
247
248
|
else {
|
|
248
|
-
db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
249
|
-
.run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
|
|
249
|
+
db.query('INSERT OR IGNORE INTO assignments (feature_id, version, status, disposition, evidence, title_override, owner, kind, dispatch_kind, priority, predecessors, deadline_version, ceo_minutes, ceo_date, accelerator, refs, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
250
|
+
.run(item.id, version, item.status, item.disposition ?? null, item.evidence ?? null, item.title === existing.title ? null : item.title, item.owner ?? null, item.kind ?? null, item.dispatchKind ?? null, item.priority ?? null, item.predecessors ? JSON.stringify(item.predecessors) : null, item.deadlineVersion ?? null, item.ceoMinutes ?? null, item.ceoDate ?? null, item.accelerator === true ? 1 : null, item.refs?.length ? JSON.stringify(item.refs) : null, item.updatedAt, item.updatedBy);
|
|
250
251
|
}
|
|
251
252
|
}
|
|
252
253
|
for (const entry of data.history) record(db, version, entry);
|
|
@@ -273,7 +274,7 @@ function importOnDb(db: Database, version: string): boolean {
|
|
|
273
274
|
record(db, version, { at: new Date().toISOString(), by: item.updatedBy, id: item.id, field: 'reimport', from: null, to: item, released: data.released, dev: data.dev });
|
|
274
275
|
continue;
|
|
275
276
|
}
|
|
276
|
-
const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const;
|
|
277
|
+
const fields = ['title', 'status', 'evidence', 'owner', 'disposition', 'kind', 'dispatchKind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator', 'refs'] as const;
|
|
277
278
|
const baseEvidence = (db.query('SELECT evidence FROM assignments WHERE feature_id = ? AND version = ?').get(item.id, version) as { evidence: string | null }).evidence ?? undefined;
|
|
278
279
|
const preserveRefs = item.evidence === baseEvidence || item.evidence === current.evidence;
|
|
279
280
|
const differs = fields.some((field) => field === 'evidence'
|
|
@@ -7,21 +7,25 @@ import { listChecklist, setItem } from './checklist.js';
|
|
|
7
7
|
import { judgeLandedCell } from './landed-to-green.js';
|
|
8
8
|
import { evidencePlan, landedButYellow, type MergedChecklistPr } from './landed-but-yellow.js';
|
|
9
9
|
import { assignedVersions, evidenceAdd } from './feature-store.js';
|
|
10
|
+
import { boundMergedTaskCells, candidateMergedTaskCells, closeMergedTaskCard } from '../task-agent/postland-card.js';
|
|
10
11
|
|
|
11
12
|
export interface MergedPrChecklistDeps {
|
|
12
|
-
readPr?: (number: number, cwd: string) => { state: string; baseRefName: string; title: string; body: string | null; mergedAt: string };
|
|
13
|
+
readPr?: (number: number, cwd: string) => { state: string; baseRefName: string; title: string; body: string | null; mergedAt: string; url?: string; headRefOid?: string; files?: Array<{ path: string }> };
|
|
13
14
|
versions?: () => string[];
|
|
14
15
|
checklist?: typeof listChecklist;
|
|
15
16
|
addEvidence?: typeof evidenceAdd;
|
|
16
17
|
setStatus?: typeof setItem;
|
|
17
18
|
judgeLanded?: typeof judgeLandedCell;
|
|
19
|
+
closeTaskCard?: typeof closeMergedTaskCard;
|
|
20
|
+
boundTaskCells?: typeof boundMergedTaskCells;
|
|
21
|
+
candidateTaskCells?: typeof candidateMergedTaskCells;
|
|
18
22
|
log?: typeof debug.log;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
/** A confirmed main merge is the only entry. The existing checklist matcher owns PR-to-cell decisions. */
|
|
22
26
|
export function syncMergedPrChecklist(number: number, cwd: string, goalFile?: string, deps: MergedPrChecklistDeps = {}): void {
|
|
23
27
|
const log = deps.log ?? debug.log.bind(debug);
|
|
24
|
-
const pr = (deps.readPr ?? ((n, dir) => JSON.parse(execFileSync('gh', ['pr', 'view', String(n), '--json', 'state,baseRefName,title,body,mergedAt'], { cwd: dir, encoding: 'utf8', timeout: 30_000 })) as ReturnType<NonNullable<MergedPrChecklistDeps['readPr']>>))(number, cwd);
|
|
28
|
+
const pr = (deps.readPr ?? ((n, dir) => JSON.parse(execFileSync('gh', ['pr', 'view', String(n), '--json', 'state,baseRefName,title,body,mergedAt,url,headRefOid,files'], { cwd: dir, encoding: 'utf8', timeout: 30_000 })) as ReturnType<NonNullable<MergedPrChecklistDeps['readPr']>>))(number, cwd);
|
|
25
29
|
if (pr.state !== 'MERGED' || pr.baseRefName !== 'main') return;
|
|
26
30
|
const merged: MergedChecklistPr = { number, title: pr.title, body: pr.body, mergedAt: pr.mergedAt };
|
|
27
31
|
const versions = deps.versions?.() ?? (() => {
|
|
@@ -43,6 +47,21 @@ export function syncMergedPrChecklist(number: number, cwd: string, goalFile?: st
|
|
|
43
47
|
const fromTitle = titleCells(pr.title);
|
|
44
48
|
const fromGoal = goal ? goalFileCells(goal) : new Set<string>();
|
|
45
49
|
const designated = [...new Set([...fromBody, ...fromTitle, ...fromGoal])];
|
|
50
|
+
// A TA card's bound checklist criterion owns its postland verdict. A prose completion
|
|
51
|
+
// declaration cannot turn that cell green before the measured TA judgment runs.
|
|
52
|
+
let taClosedId: string | null = null;
|
|
53
|
+
let taBoundIds = new Set<string>();
|
|
54
|
+
let taBindingUnreadable = false;
|
|
55
|
+
try {
|
|
56
|
+
taBoundIds = new Set((deps.candidateTaskCells ?? candidateMergedTaskCells)(number));
|
|
57
|
+
if (pr.url && pr.headRefOid) for (const id of (deps.boundTaskCells ?? boundMergedTaskCells)({ number, url: pr.url })) taBoundIds.add(id);
|
|
58
|
+
} catch (error) { taBindingUnreadable = true; log('release.checklist', 'postland-binding-unreadable', { pr: number, error: String(error) }); }
|
|
59
|
+
if (pr.url && pr.headRefOid && !taBindingUnreadable) {
|
|
60
|
+
try {
|
|
61
|
+
taClosedId = (deps.closeTaskCard ?? closeMergedTaskCard)({ ...pr, number, url: pr.url, headRefOid: pr.headRefOid }, cwd, { versions: [...new Set(versions)] });
|
|
62
|
+
if (taClosedId) taBoundIds.add(taClosedId);
|
|
63
|
+
} catch (error) { log('release.checklist', 'postland-card-error', { pr: number, ids: [...taBoundIds], error: String(error) }); }
|
|
64
|
+
}
|
|
46
65
|
for (const id of designated) {
|
|
47
66
|
log('release.checklist', 'merged-pr-cell-source', {
|
|
48
67
|
pr: number,
|
|
@@ -67,19 +86,19 @@ export function syncMergedPrChecklist(number: number, cwd: string, goalFile?: st
|
|
|
67
86
|
}
|
|
68
87
|
for (const { version, snapshot, row } of matches) {
|
|
69
88
|
if (counts.get(row.id) !== 1) continue;
|
|
70
|
-
for (const { id, ref } of evidencePlan([row])) {
|
|
89
|
+
for (const { id, ref } of taClosedId === row.id ? [] : evidencePlan([row])) {
|
|
71
90
|
(deps.addEvidence ?? evidenceAdd)(id, version, ref, 'harness', snapshot.released, snapshot.dev);
|
|
72
91
|
log('release.checklist', 'merged-pr-evidence-added', { version, id, pr: number, ref });
|
|
73
92
|
}
|
|
74
93
|
const cell = snapshot.items.find((item) => item.id === row.id)!;
|
|
75
|
-
if (cell.status !== 'green' && cell.status !== 'done') {
|
|
94
|
+
if (!taBindingUnreadable && !taBoundIds.has(row.id) && cell.status !== 'green' && cell.status !== 'done') {
|
|
76
95
|
try {
|
|
77
96
|
(deps.judgeLanded ?? judgeLandedCell)({ version, id: row.id, title: cell.title, pr: number }, { cwd });
|
|
78
97
|
} catch (error) {
|
|
79
98
|
try { log('release.checklist', 'landed-green-error', { version, id: row.id, pr: number, error: String(error) }); } catch { /* Judgment remains fail-soft. */ }
|
|
80
99
|
}
|
|
81
100
|
}
|
|
82
|
-
if (goal && declaresCompletion(goal)) {
|
|
101
|
+
if (!taBindingUnreadable && !taBoundIds.has(row.id) && goal && declaresCompletion(goal)) {
|
|
83
102
|
if (!goalCells(goal).has(row.id)) {
|
|
84
103
|
log('release.checklist', 'merged-pr-status-skipped', { version, id: row.id, pr: number, reason: 'goal-does-not-name-cell' });
|
|
85
104
|
continue;
|
|
@@ -99,6 +99,76 @@ export function rubricPriority(text: string): ReturnType<typeof rubricGrade> {
|
|
|
99
99
|
return rubric ? rubricGrade(rubricScore(rubric)) : 'P2';
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// Rubric v2 — first slice: the formula only (line parsing and v1/v2 output are later slots).
|
|
103
|
+
|
|
104
|
+
/** v2 value axes (L U F O R T M, each 0..3); Z divides the total, X subtracts from it. */
|
|
105
|
+
export const RUBRIC_V2_VALUE_AXES = ['L', 'U', 'F', 'O', 'R', 'T', 'M'] as const;
|
|
106
|
+
export const RUBRIC_V2_Z_CHOICES: readonly number[] = [1, 2, 4, 8];
|
|
107
|
+
const V2_AXIS_KEYS = new Set<string>([...RUBRIC_V2_VALUE_AXES, 'Z', 'X']);
|
|
108
|
+
|
|
109
|
+
/** Rubric v2 values: L U F O R T M each 0..3, Z ∈ {1,2,4,8}, X 0..3. */
|
|
110
|
+
export interface RubricV2 {
|
|
111
|
+
L: number;
|
|
112
|
+
U: number;
|
|
113
|
+
F: number;
|
|
114
|
+
O: number;
|
|
115
|
+
R: number;
|
|
116
|
+
T: number;
|
|
117
|
+
M: number;
|
|
118
|
+
Z: number;
|
|
119
|
+
X: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The F weight is 2 through 2026-10-28 and 1 from 2026-10-29 on; asOf must retain its original date text. */
|
|
123
|
+
export const RUBRIC_V2_F_CUTOVER_DAY = '2026-10-29';
|
|
124
|
+
|
|
125
|
+
export function rubricV2FWeight(asOf: string): 2 | 1 {
|
|
126
|
+
const invalid = () => new Error(`루브릭 v2 기준 날짜가 잘못되었다: ${String(asOf)}`);
|
|
127
|
+
// A Date may already have normalized an impossible calendar day before this call.
|
|
128
|
+
if (typeof asOf !== 'string') throw invalid();
|
|
129
|
+
const parts = /^(\d{4}|[1-9]\d{4,5}|[+-]\d{6})-(\d{2})-(\d{2})(?:$|T)/.exec(asOf);
|
|
130
|
+
if (!parts) throw invalid();
|
|
131
|
+
const year = Number(parts[1]);
|
|
132
|
+
const month = Number(parts[2]);
|
|
133
|
+
const day = Number(parts[3]);
|
|
134
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
135
|
+
const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1];
|
|
136
|
+
if (!daysInMonth || day < 1 || day > daysInMonth) throw invalid();
|
|
137
|
+
const date = new Date(asOf);
|
|
138
|
+
if (Number.isNaN(date.getTime())) throw invalid();
|
|
139
|
+
return date.getTime() >= Date.parse(`${RUBRIC_V2_F_CUTOVER_DAY}T00:00:00.000Z`) ? 1 : 2;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** V = 2L + 2U + wF + 1.5O + 1.5R + 1.5T + M (w = rubricV2FWeight), 점수 = V / Z − 0.5X. */
|
|
143
|
+
export function rubricScoreV2(values: RubricV2, asOf: string): number {
|
|
144
|
+
if (values === null || typeof values !== 'object' || Array.isArray(values)) throw new Error('루브릭 v2 값이 축 객체가 아니다');
|
|
145
|
+
const given = values as unknown as Record<string, unknown>;
|
|
146
|
+
for (const key of Object.keys(given)) {
|
|
147
|
+
if (!V2_AXIS_KEYS.has(key)) throw new Error(`루브릭 v2 알 수 없는 축: ${key}`);
|
|
148
|
+
}
|
|
149
|
+
const readAxis = (axis: (typeof RUBRIC_V2_VALUE_AXES)[number]): number => {
|
|
150
|
+
const value = given[axis];
|
|
151
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(`루브릭 v2 축 ${axis} 값이 없다`);
|
|
152
|
+
if (value < 0 || value > 3) throw new Error(`루브릭 v2 축 ${axis} 값이 0~3 밖이다: ${value}`);
|
|
153
|
+
return value;
|
|
154
|
+
};
|
|
155
|
+
const L = readAxis('L');
|
|
156
|
+
const U = readAxis('U');
|
|
157
|
+
const F = readAxis('F');
|
|
158
|
+
const O = readAxis('O');
|
|
159
|
+
const R = readAxis('R');
|
|
160
|
+
const T = readAxis('T');
|
|
161
|
+
const M = readAxis('M');
|
|
162
|
+
const Z = given.Z;
|
|
163
|
+
if (typeof Z !== 'number' || !Number.isFinite(Z)) throw new Error('루브릭 v2 크기 Z 값이 없다');
|
|
164
|
+
if (!RUBRIC_V2_Z_CHOICES.includes(Z)) throw new Error(`루브릭 v2 크기 Z 값이 1·2·4·8 밖이다: ${Z}`);
|
|
165
|
+
const X = given.X;
|
|
166
|
+
if (typeof X !== 'number' || !Number.isFinite(X)) throw new Error('루브릭 v2 위험 X 값이 없다');
|
|
167
|
+
if (X < 0 || X > 3) throw new Error(`루브릭 v2 위험 X 값이 0~3 밖이다: ${X}`);
|
|
168
|
+
const w = rubricV2FWeight(asOf);
|
|
169
|
+
return (2 * L + 2 * U + w * F + 1.5 * O + 1.5 * R + 1.5 * T + M) / Z - 0.5 * X;
|
|
170
|
+
}
|
|
171
|
+
|
|
102
172
|
/** Inspect an existing checklist without initializing, importing, or migrating its ledger. */
|
|
103
173
|
export function readRubricItems(version: string, ledgerRoot: string): Pick<ChecklistItem, 'id' | 'title' | 'evidence' | 'priority'>[] {
|
|
104
174
|
const dbPath = join(ledgerRoot, 'release', 'features.sqlite');
|