elanous 0.2.18 → 0.2.19
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/{P6YTVgzuZz5oTZhY4Din_ → J5H7BvqgSI8DUjtRRcA_S}/_buildManifest.js +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{726.83532d69452ee24b.js → 1470.98a6ab61ccb4131c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9247-269e4464cc4b491c.js → 2375-e061bb24cd2b5cde.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{211-9c9090c59e2c5907.js → 2977-e38442e86289f1d3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f6b7c9ba.64a895b8b2fedbdc.js → 2ea96958.e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +2 -0
- package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +5 -0
- package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +1 -0
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1268.0840ff91e083fcbd.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6110.1297d4093fdfbb0d.js → 4847.56f5bc454fbcc827.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7327-2d69fb321ba73389.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +1 -0
- package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{935.4ddaa99fc1d1126c.js → 7647.2fcd30d894d5ee30.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{f6231284-45726550aba08fca.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +25 -0
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-b70cd005d9169e25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +1 -0
- package/apps/pwa/out/_next/static/css/{e99e3cff94ced78a.css → 599369d853c61df7.css} +1 -1
- package/apps/pwa/out/_next/static/css/85f53c6f35d11382.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 +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +23 -23
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +25 -24
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +24 -23
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +28 -27
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/ops/guardian.yaml +26 -0
- package/graphs/ops/recipes.yaml +3 -0
- package/package.json +1 -1
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +3 -1
- package/scripts/lib/pch-regress-map.ts +23 -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 +338 -46
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +120 -0
- package/scripts/release-loop/resume-release.ts +64 -5
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +2 -0
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +255 -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/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +13 -7
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +32 -7
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +104 -11
- 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 +63 -36
- package/src/dashboard/slash-runtime/decide-slash.ts +60 -0
- package/src/dashboard/slash-runtime/help-from-registry.ts +21 -8
- 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/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/runner.ts +25 -5
- package/src/harness/harness-cli-command.ts +2 -0
- package/src/harness/harness-salvage-cli.ts +78 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/hq.ts +6 -1
- package/src/index.ts +52 -3
- 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/http-server.ts +16 -5
- package/src/nexus/api/loop-edges.ts +290 -17
- package/src/nexus/api/ops-api.ts +96 -28
- package/src/nexus/index.ts +7 -0
- package/src/release-loop/checklist.ts +2 -0
- package/src/release-loop/feature-store.ts +54 -7
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +51 -17
- package/src/release-loop/rubric.ts +57 -1
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +179 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/gate-cli.ts +29 -8
- 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 +2 -1
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/task-agent/actions.ts +63 -2
- package/src/task-agent/board.ts +132 -0
- package/src/task-agent/card-evidence.ts +203 -0
- package/src/task-agent/completion-evidence.ts +126 -0
- package/src/task-agent/judge.ts +18 -0
- package/src/task-agent/live-moves.ts +176 -0
- package/src/task-agent/mission.ts +32 -6
- package/src/task-agent/shadow.ts +183 -15
- package/src/task-agent/task-hand.ts +117 -15
- package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +142 -13
- package/src/task-orchestrator/surfaces/pod-pool.ts +13 -5
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +2 -2
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +11 -0
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +134 -2
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-30dff22c9e1f3073.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1844-9ec4b1d12f6e80eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1948.15f356b15a625597.js +0 -5
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +0 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.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/4565-3d7e3fdeb43686a8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.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/6158.af2e93b7adff709b.js +0 -2
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-7865aba0f4d2ee49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-4733c967b56a4277.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-02b073a2ac793b83.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/chat/page-8a9b3594bc23289b.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/control/page-308111abadf35670.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/design-check/page-5d97b2901e0d1ea6.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/exec/page-8c3d8e63ae2e1c98.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-9b72a07d424701c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.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/intake/page-a572f58cd3ca5864.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.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/market/page-fa244bd146a00ae8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-83a700d4c754d802.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-7b3170f374f77b9d.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/reflection/page-1467047104141297.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/sessions/page-102310930791197a.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/settings/page-6ed0b73bae8351d8.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/setup/layout-c1a96669158471ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-3cc47dca0922ab9a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-b17cd46cf0e9b8cf.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/trace/page-7bf54c9bb9274af8.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/workspace/page-134da468538baf48.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-c2782b330447ffb6.js +0 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +0 -1
- /package/apps/pwa/out/_next/static/{P6YTVgzuZz5oTZhY4Din_ → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SALVAGE-VISIBLE — Pod 런이 멈추며 남긴 수확 가지(`salvage/<job>/<worktree>`)를 «런 하나 → PR | 수확 가지 | 없음 | 모름» 으로 보인다.
|
|
3
|
+
*
|
|
4
|
+
* S: Pod Job 은 자식 rc≠0 이면 워크트리를 `salvage/si-task-<job>/self-impl-…-r<run 16진 앞 6자>` 로 push 한다
|
|
5
|
+
* (`src/task-orchestrator/surfaces/self-implement-pod.ts` salvage 단계 · 이름은 `plannedSelfImplBranch` 의 `-r` 접미).
|
|
6
|
+
* C: 10-07 세 자리가 PR·`salvage-pushed` 로그만 보고 멈춘 Pod 런 여덟을 «수확 0» 으로 읽었다 — 여덟 다 수백 줄짜리 가지가 있었다.
|
|
7
|
+
* Q: 런 id 하나로 그 가지를 «한 번의 목록»에서 찾고, 크기를 재고, 못 읽었을 때 «없음» 이라 말하지 않으려면?
|
|
8
|
+
* A: `git ls-remote origin 'refs/heads/salvage/*'` 한 번 → `-r<6>` 접미로 Job 을 고르고, 같은 Job 의 형제(`/repo`·`-early`)까지
|
|
9
|
+
* numstat 로 재서 `.gitignore`·`docs/goals/**` 만 건드린 가지는 버린다. 목록 실패는 `unknown` 이다(⛔ `none` 이 아니다).
|
|
10
|
+
*
|
|
11
|
+
* 순수 모듈 — git 은 주입된 러너로만 부른다(시험은 네트워크 0).
|
|
12
|
+
*/
|
|
13
|
+
import { runGitCommand } from '../git-fs/runner.js';
|
|
14
|
+
|
|
15
|
+
export interface SalvageGitResult {
|
|
16
|
+
status: number | null;
|
|
17
|
+
stdout: string;
|
|
18
|
+
stderr: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type SalvageGitRunner = (args: string[]) => SalvageGitResult;
|
|
22
|
+
|
|
23
|
+
export interface SalvageDiffstat {
|
|
24
|
+
files: number;
|
|
25
|
+
insertions: number;
|
|
26
|
+
deletions: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SalvageBranch {
|
|
30
|
+
branch: string;
|
|
31
|
+
sha: string;
|
|
32
|
+
diffstat: SalvageDiffstat;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type SalvageLookup =
|
|
36
|
+
| { status: 'found'; branches: SalvageBranch[] }
|
|
37
|
+
| { status: 'none'; trivialSkipped: number }
|
|
38
|
+
| { status: 'unknown'; reason: string }
|
|
39
|
+
/** PR 이 이겨서 가지를 «조회하지 않았다» — ⛔ 부재(`none`)가 아니다. */
|
|
40
|
+
| { status: 'not-checked' };
|
|
41
|
+
|
|
42
|
+
export interface RemoteSalvageRef {
|
|
43
|
+
branch: string;
|
|
44
|
+
sha: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SALVAGE_REF_PREFIX = 'refs/heads/';
|
|
48
|
+
|
|
49
|
+
/** `run-bfe2b5c1-…` → `bfe2b5` — `plannedSelfImplBranch` 의 `-r` 접미와 같은 규칙. 6자가 안 되면 null(⛔ 짧은 접두는 남의 가지를 문다). */
|
|
50
|
+
export function runSuffixKey(runId: string): string | null {
|
|
51
|
+
// `run-` 뒤에 «연속된» 16진 6자가 있어야 한다 — 구분자를 지우고 이어 붙이면 `run-ab-cdef` 가 남의 `abcdef` 를 문다.
|
|
52
|
+
const m = /^(?:run-)?([0-9a-f]{6})/i.exec(runId.trim());
|
|
53
|
+
return m ? m[1]!.toLowerCase() : null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** `git ls-remote` 출력 → `{ branch, sha }`. salvage/ 가 아닌 «올바른» 줄은 버리고, 형식이 깨진 비어 있지 않은 줄은 `unparsed` 로 센다
|
|
57
|
+
* (⛔ rc=0 ⊕ 못 읽은 출력을 «없음» 으로 접지 않는다). */
|
|
58
|
+
export function parseLsRemote(stdout: string): RemoteSalvageRef[] & { unparsed?: number } {
|
|
59
|
+
const rows: RemoteSalvageRef[] & { unparsed?: number } = [];
|
|
60
|
+
let unparsed = 0;
|
|
61
|
+
const lines = stdout.split(/\r?\n/);
|
|
62
|
+
if (lines.at(-1) === '') lines.pop();
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
// 공백만 있는 줄도 «못 읽은 줄» 이다(⛔ 건너뛰면 깨진 목록이 «없음» 이 된다).
|
|
65
|
+
const m = /^([0-9a-f]{40}|[0-9a-f]{64})\t(refs\/\S+)$/i.exec(line);
|
|
66
|
+
if (!m) {
|
|
67
|
+
unparsed += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (!m[2]!.startsWith(`${SALVAGE_REF_PREFIX}salvage/`)) continue;
|
|
71
|
+
rows.push({ sha: m[1]!.toLowerCase(), branch: m[2]!.slice(SALVAGE_REF_PREFIX.length) });
|
|
72
|
+
}
|
|
73
|
+
if (unparsed > 0) rows.unparsed = unparsed;
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** 워크트리 조각(마지막 경로 성분)이 `-r<key>` 또는 `-r<key>-early` 로 끝나는가. */
|
|
78
|
+
export function branchMatchesRun(branch: string, key: string): boolean {
|
|
79
|
+
const leaf = branch.slice(branch.lastIndexOf('/') + 1).toLowerCase();
|
|
80
|
+
return leaf.endsWith(`-r${key}`) || leaf.endsWith(`-r${key}-early`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function jobDir(branch: string): string {
|
|
84
|
+
return branch.slice(0, branch.lastIndexOf('/'));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 런 하나에 속하는 원격 가지 — `-r<key>` 로 맞은 Job 디렉토리의 형제 전부(`/repo` 포함). */
|
|
88
|
+
export function selectRunSalvageRefs(refs: readonly RemoteSalvageRef[], key: string): RemoteSalvageRef[] {
|
|
89
|
+
const jobs = new Set(refs.filter((ref) => branchMatchesRun(ref.branch, key)).map((ref) => jobDir(ref.branch)));
|
|
90
|
+
return refs.filter((ref) => jobs.has(jobDir(ref.branch)));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 루트 `.gitignore`·`docs/goals/**` 만 바꾼 가지는 «수확할 일» 이 아니다(Pod 가 `/repo` 에 골 문서만 남긴다). */
|
|
94
|
+
export function isTrivialSalvagePath(path: string): boolean {
|
|
95
|
+
// ⛔ trim 하지 않는다 — `-z` 경로는 원 바이트이고 ` .gitignore` 는 다른 파일이다.
|
|
96
|
+
return path === '.gitignore' || path.startsWith('docs/goals/');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** `git diff --numstat -z --no-renames` → diffstat ⊕ 경로. 레코드는 NUL 로 끊기고 경로는 따옴표 없이 «원 바이트» 그대로다
|
|
100
|
+
* (⛔ `-z` 없이는 한글·특수문자 경로가 `"docs/goals/\355…"` 로 감싸져 제외 규칙을 못 탄다). 바이너리(`-`)는 줄 수 0. */
|
|
101
|
+
export function parseNumstat(stdout: string): { diffstat: SalvageDiffstat; paths: string[]; unparsed: number } {
|
|
102
|
+
const paths: string[] = [];
|
|
103
|
+
let insertions = 0;
|
|
104
|
+
let deletions = 0;
|
|
105
|
+
let unparsed = 0;
|
|
106
|
+
const records = stdout.split('\0');
|
|
107
|
+
// `-z` 출력은 레코드마다 NUL 로 «끝난다» — 마지막 조각이 비어 있지 않으면 잘린 출력이다(⛔ 정상 레코드로 받지 않는다).
|
|
108
|
+
const tail = records.pop() ?? '';
|
|
109
|
+
if (tail !== '') unparsed += 1;
|
|
110
|
+
for (const record of records) {
|
|
111
|
+
if (record === '') {
|
|
112
|
+
unparsed += 1;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const m = /^(\d+|-)\t(\d+|-)\t([^\0]+)$/.exec(record.replace(/^\n+/, ''));
|
|
116
|
+
if (!m) {
|
|
117
|
+
unparsed += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
insertions += m[1] === '-' ? 0 : Number(m[1]);
|
|
121
|
+
deletions += m[2] === '-' ? 0 : Number(m[2]);
|
|
122
|
+
paths.push(m[3]!);
|
|
123
|
+
}
|
|
124
|
+
return { diffstat: { files: paths.length, insertions, deletions }, paths, unparsed };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** 러너가 «던져도» 결과로 접는다 — 실행 예외는 rc=null ⊕ 메시지(호출부가 `unknown` 으로 옮긴다). */
|
|
128
|
+
function runSafely(git: SalvageGitRunner, args: string[]): SalvageGitResult {
|
|
129
|
+
try {
|
|
130
|
+
return git(args);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
return { status: null, stdout: '', stderr: `threw: ${error instanceof Error ? error.message : String(error)}` };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function failureReason(step: string, result: SalvageGitResult): string {
|
|
137
|
+
const tail = result.stderr.trim().split(/\r?\n/).at(-1) ?? '';
|
|
138
|
+
return `${step} rc=${result.status ?? 'signal'}${tail ? ` · ${tail.slice(0, 160)}` : ''}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface FindSalvageOptions {
|
|
142
|
+
/** 이미 읽은 ls-remote 목록(여러 런을 한 번의 목록으로). 없으면 러너로 한 번 읽는다. */
|
|
143
|
+
listing?: RemoteSalvageRef[] | { error: string };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 비교 기준 — fetch 가 함께 갱신하는 `origin/main` 하나로 고정한다(다른 기준은 fetch 와 어긋난다). */
|
|
147
|
+
const SALVAGE_DIFF_BASE = 'origin/main';
|
|
148
|
+
|
|
149
|
+
/** `git ls-remote origin 'refs/heads/salvage/*'` 한 번. 실패는 `{ error }` — 호출부가 `unknown` 으로 옮긴다. */
|
|
150
|
+
export function listRemoteSalvageRefs(git: SalvageGitRunner): RemoteSalvageRef[] | { error: string } {
|
|
151
|
+
const result = runSafely(git, ['ls-remote', 'origin', 'refs/heads/salvage/*']);
|
|
152
|
+
if (result.status !== 0) return { error: failureReason('ls-remote', result) };
|
|
153
|
+
const rows = parseLsRemote(result.stdout);
|
|
154
|
+
if (rows.unparsed) return { error: `ls-remote 출력 ${rows.unparsed}줄을 못 읽음` };
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 런 id → 그 런의 수확 가지. 목록·fetch·diff 중 하나라도 못 읽으면 `unknown`(⛔ `none` 으로 접지 않는다). */
|
|
159
|
+
export function findRunSalvageBranches(runId: string, git: SalvageGitRunner, options: FindSalvageOptions = {}): SalvageLookup {
|
|
160
|
+
const key = runSuffixKey(runId);
|
|
161
|
+
if (key === null) return { status: 'unknown', reason: `런 id 에서 16진 6자를 못 뽑았다 (${runId})` };
|
|
162
|
+
const listing = options.listing ?? listRemoteSalvageRefs(git);
|
|
163
|
+
if (!Array.isArray(listing)) return { status: 'unknown', reason: listing.error };
|
|
164
|
+
const refs = selectRunSalvageRefs(listing, key);
|
|
165
|
+
if (refs.length === 0) return { status: 'none', trivialSkipped: 0 };
|
|
166
|
+
const fetched = runSafely(git, ['fetch', '--no-tags', '--quiet', 'origin', 'main', ...refs.map((ref) => `${SALVAGE_REF_PREFIX}${ref.branch}`)]);
|
|
167
|
+
if (fetched.status !== 0) return { status: 'unknown', reason: failureReason('fetch', fetched) };
|
|
168
|
+
const branches: SalvageBranch[] = [];
|
|
169
|
+
let trivialSkipped = 0;
|
|
170
|
+
for (const ref of refs) {
|
|
171
|
+
const diff = runSafely(git, ['diff', '--numstat', '-z', '--no-renames', `${SALVAGE_DIFF_BASE}...${ref.sha}`]);
|
|
172
|
+
if (diff.status !== 0) return { status: 'unknown', reason: failureReason(`diff ${ref.branch}`, diff) };
|
|
173
|
+
const { diffstat, paths, unparsed } = parseNumstat(diff.stdout);
|
|
174
|
+
if (unparsed > 0) return { status: 'unknown', reason: `diff ${ref.branch} 출력 ${unparsed}줄을 못 읽음` };
|
|
175
|
+
if (paths.every(isTrivialSalvagePath)) {
|
|
176
|
+
trivialSkipped += 1;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
branches.push({ branch: ref.branch, sha: ref.sha, diffstat });
|
|
180
|
+
}
|
|
181
|
+
branches.sort((a, b) => b.diffstat.insertions + b.diffstat.deletions - (a.diffstat.insertions + a.diffstat.deletions));
|
|
182
|
+
return branches.length > 0 ? { status: 'found', branches } : { status: 'none', trivialSkipped };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** 실물 러너 — 공용 git 심(`runGitCommand` · 락 재시도) 위에서 cwd 를 «인자로» 받는다. */
|
|
186
|
+
export function gitSalvageRunner(cwd: string): SalvageGitRunner {
|
|
187
|
+
return (args) => {
|
|
188
|
+
const result = runGitCommand(cwd, args, { encoding: 'utf8', timeout: 120_000, maxBuffer: 64 * 1024 * 1024 });
|
|
189
|
+
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export type RunPrState =
|
|
194
|
+
| { status: 'pr'; number: number }
|
|
195
|
+
| { status: 'none' }
|
|
196
|
+
| { status: 'unknown'; reason: string };
|
|
197
|
+
|
|
198
|
+
export type RunSalvageOutcome = 'pr' | 'salvage' | 'none' | 'unknown';
|
|
199
|
+
|
|
200
|
+
export interface RunSalvageVerdict {
|
|
201
|
+
runId: string;
|
|
202
|
+
outcome: RunSalvageOutcome;
|
|
203
|
+
pr: RunPrState;
|
|
204
|
+
salvage: SalvageLookup;
|
|
205
|
+
line: string;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function branchLine(branch: SalvageBranch): string {
|
|
209
|
+
return `수확 가지 ${branch.branch} (+${branch.diffstat.insertions}/-${branch.diffstat.deletions} · ${branch.diffstat.files} files)`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 판정 — PR 이 있으면 PR 이 이긴다. 없으면 수확 가지 → 없음 → 모름 순. PR 을 못 읽었으면 «PR 모름» 을 같이 단다. */
|
|
213
|
+
export function decideRunSalvage(runId: string, pr: RunPrState, salvage: SalvageLookup): RunSalvageVerdict {
|
|
214
|
+
const prNote = pr.status === 'unknown' ? ` · PR 모름(${pr.reason})` : '';
|
|
215
|
+
if (pr.status === 'pr') return { runId, outcome: 'pr', pr, salvage, line: `${runId} PR #${pr.number}` };
|
|
216
|
+
if (salvage.status === 'found') {
|
|
217
|
+
const [first, ...rest] = salvage.branches;
|
|
218
|
+
const more = rest.length > 0 ? ` ⊕ 가지 ${rest.length}개 더` : '';
|
|
219
|
+
return { runId, outcome: 'salvage', pr, salvage, line: `${runId} ${branchLine(first!)}${more}${prNote}` };
|
|
220
|
+
}
|
|
221
|
+
if (salvage.status === 'not-checked') return { runId, outcome: 'unknown', pr, salvage, line: `${runId} 모름(수확 가지 미조회)${prNote}` };
|
|
222
|
+
if (salvage.status === 'unknown') return { runId, outcome: 'unknown', pr, salvage, line: `${runId} 모름(${salvage.reason})${prNote}` };
|
|
223
|
+
const skipped = salvage.trivialSkipped > 0 ? ` (골 문서·.gitignore 만 있는 가지 ${salvage.trivialSkipped}개 제외)` : '';
|
|
224
|
+
// PR 을 못 읽었으면 «없음» 이라 단정하지 않는다.
|
|
225
|
+
if (pr.status === 'unknown') return { runId, outcome: 'unknown', pr, salvage, line: `${runId} 모름(수확 가지 없음${skipped} · PR 모름 — ${pr.reason})` };
|
|
226
|
+
return { runId, outcome: 'none', pr, salvage, line: `${runId} 없음${skipped}` };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** 원장 항목 → PR 상태. 마지막 `pr-opened` 의 번호(없으면 `run-rollup.prNumber`).
|
|
230
|
+
* ⛔ 번호가 없다고 곧 «PR 없음» 이 아니다 — 원장이 «종료(`run-status`)» 를 PR 아닌 단계로 적었을 때만 `none`.
|
|
231
|
+
* 원장 파일이 없거나(호스트가 모르는 Pod 런) 종료 기록이 없거나 종료가 PR 단계인데 번호가 없으면 `unknown`. */
|
|
232
|
+
export function prStateFromLedger(entries: ReadonlyArray<{ event: string; data: Record<string, unknown> }> | null): RunPrState {
|
|
233
|
+
if (entries === null) return { status: 'unknown', reason: '호스트 원장 없음' };
|
|
234
|
+
const pick = (value: unknown): number | null => (typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? value : null);
|
|
235
|
+
const prNumberOf = (data: Record<string, unknown>): number | null => pick(data.number) ?? pick(data.prNumber)
|
|
236
|
+
?? (typeof data.prUrl === 'string' ? pick(Number(/\/pull\/(\d+)/.exec(data.prUrl)?.[1])) : null);
|
|
237
|
+
const openedEntries = entries.filter((entry) => entry.event === 'pr-opened');
|
|
238
|
+
const opened = openedEntries.at(-1)?.data;
|
|
239
|
+
// 번호 «있는» 마지막 pr-opened — 뒤에 번호 없는 기록이 와도 알려진 PR 을 버리지 않는다.
|
|
240
|
+
const number = openedEntries.map((entry) => prNumberOf(entry.data)).filter((n): n is number => n !== null).at(-1)
|
|
241
|
+
?? pick(entries.filter((entry) => entry.event === 'run-rollup').at(-1)?.data.prNumber) ?? null;
|
|
242
|
+
if (number !== null) return { status: 'pr', number };
|
|
243
|
+
if (opened !== undefined) return { status: 'unknown', reason: '원장에 pr-opened 가 있으나 번호 없음' };
|
|
244
|
+
const terminal = entries.filter((entry) => entry.event === 'run-status').at(-1)?.data.stage;
|
|
245
|
+
if (typeof terminal !== 'string') return { status: 'unknown', reason: '원장에 PR·종료 기록 없음' };
|
|
246
|
+
if (!NO_PR_TERMINAL_STAGES.has(terminal)) return { status: 'unknown', reason: `마지막 단계 ${terminal} — PR 없는 종료로 확인 안 됨` };
|
|
247
|
+
return { status: 'none' };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** PR 을 열기 «전에» 끝나는 종료 단계(`run-status-mapping.ts` `SelfImplementStage` 중). ⛔ merged·pr-opened·merge-ready·merge-conflict 는
|
|
251
|
+
* PR 이 있었을 단계라 빠진다 — 그 단계인데 번호가 없으면 `unknown`. 진행 중 단계도 여기 없으므로 `unknown`. */
|
|
252
|
+
const NO_PR_TERMINAL_STAGES: ReadonlySet<string> = new Set([
|
|
253
|
+
'worktree-completed', 'gate-failed', 'review-blocked', 'no-changes', 'aborted', 'timed-out', 'pr-declined', 'soft-stopped', 'parked',
|
|
254
|
+
]);
|
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import { copyFileSync, lstatSync, mkdirSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { debug } from '../debug/log.js';
|
|
4
|
+
import { judgeNextMove } from './judge.js';
|
|
5
|
+
import { checkCompletionEvidence } from './completion-evidence.js';
|
|
6
|
+
|
|
7
|
+
export type DeliveryKind = 'research-report' | 'artifact' | 'content';
|
|
8
|
+
export interface DeliveryEvidence { ok: boolean; files: string[]; reason: string }
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* DEFAULT-HANDS-a: 판독은 복사된 최종 산출물에서만 하고, 규칙은 `completion-evidence.ts` 의
|
|
12
|
+
* `checkCompletionEvidence` 하나를 쓴다(종류별 규칙을 여기서 다시 짓지 않는다).
|
|
13
|
+
* 후보 중 하나라도 그 종류의 증거를 다 갖추면 ok — 후보 목록엔 곁 파일(미리보기 등)도 섞인다.
|
|
14
|
+
*/
|
|
15
|
+
export function readDeliveryEvidence(kind: DeliveryKind, outputDir: string, files: readonly string[]): DeliveryEvidence {
|
|
16
|
+
if (!files.length) return { ok: false, files: [], reason: '전달 후보 없음' };
|
|
17
|
+
const missing = new Set<string>();
|
|
18
|
+
for (const file of files) {
|
|
19
|
+
const verdict = checkCompletionEvidence(kind, { dir: outputDir, ref: relative(outputDir, file) });
|
|
20
|
+
if (verdict.ok) return { ok: true, files: [...files], reason: `전달 산출물 확인: ${verdict.ref}` };
|
|
21
|
+
for (const item of verdict.missing) missing.add(item);
|
|
22
|
+
}
|
|
23
|
+
return { ok: false, files: [...files], reason: `전달 증거 부족: ${[...missing].join(', ')}` };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type NextActionKind = 'review' | 'land' | 'retry' | 'green' | 'decision' | 'deliver';
|
|
2
27
|
/**
|
|
3
28
|
* `retry` 의 갈래(RFC §A9 G1·G2). 없으면 종전 그대로 «must-fix 로 좁힌 재발사»다(must-fix 가 없으면 카드).
|
|
4
29
|
* - narrow: 범위를 좁혀 한 번 재발사(must-fix 없이도 간다 — no-progress)
|
|
@@ -22,6 +47,7 @@ export interface NextAction {
|
|
|
22
47
|
mustFix?: string[];
|
|
23
48
|
cwd?: string;
|
|
24
49
|
risk?: 'money' | 'security' | 'irreversible-publication';
|
|
50
|
+
delivery?: { kind: DeliveryKind; worktreePath: string; projectTarget: string; files: string[] };
|
|
25
51
|
}
|
|
26
52
|
export interface ActionDeps {
|
|
27
53
|
command(args: string[]): Promise<{ status: number; stdout: string; stderr?: string }>;
|
|
@@ -63,7 +89,11 @@ export async function executeNextAction(action: NextAction, deps: ActionDeps): P
|
|
|
63
89
|
deps.observe('task-agent.action', { taskId: action.taskId, kind: action.kind, result, reason });
|
|
64
90
|
return result;
|
|
65
91
|
};
|
|
66
|
-
if (deps.mode !== 'live')
|
|
92
|
+
if (deps.mode !== 'live') {
|
|
93
|
+
const destination = action.delivery && action.kind === 'deliver'
|
|
94
|
+
? ` → ${join(action.delivery.projectTarget, 'elanous-out', action.taskId)}/${action.delivery.files.join(', ')}` : '';
|
|
95
|
+
return record('shadow', `would ${action.kind}${action.variant ? `:${action.variant}` : ''}: ${action.rationale}${destination}`);
|
|
96
|
+
}
|
|
67
97
|
const card = async (reason: string) => {
|
|
68
98
|
const evidence = JSON.stringify({ runId: action.runId, pr: action.pr, mustFix: action.mustFix ?? [], history: action.history ?? [], selected: action.kind, reason });
|
|
69
99
|
const result = await deps.command(['decisions', 'raise', '--title', `${action.taskId}: ${reason}`, '--category', action.risk === 'money' ? 'money' : action.risk === 'security' ? 'security' : action.risk === 'irreversible-publication' ? 'publish' : 'other', '--s', `Task ${action.taskId} needs a decision.`, '--c', reason, '--ref', evidence, '--q', 'Which action should be taken?', '--option', 'h=Keep on hold:no automatic execution', '--option', 'p=Approve action:execute after approval', '--skip-recommend', 'requires human decision', '--no-xcheck', 'automated escalation']);
|
|
@@ -87,6 +117,37 @@ export async function executeNextAction(action: NextAction, deps: ActionDeps): P
|
|
|
87
117
|
if (deps.reserveLanding ? !deps.reserveLanding(day, limit) : (deps.landingsToday ?? 0) >= limit) return card('daily landing limit');
|
|
88
118
|
}
|
|
89
119
|
if (action.kind === 'decision') return card(action.rationale);
|
|
120
|
+
if (action.kind === 'deliver') {
|
|
121
|
+
const delivery = action.delivery;
|
|
122
|
+
if (!delivery?.files.length) return record('waiting', '전달 후보 없음');
|
|
123
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(action.taskId)) return record('failed', 'invalid delivery taskId');
|
|
124
|
+
const output = join(delivery.projectTarget, 'elanous-out', action.taskId);
|
|
125
|
+
const copied: string[] = [];
|
|
126
|
+
try {
|
|
127
|
+
mkdirSync(output, { recursive: true });
|
|
128
|
+
// 경로 비교는 «실경로끼리» 한다 — macOS 의 /var→/private/var 처럼 조상 링크가 있으면 resolve() 와 realpath 가 늘 갈린다.
|
|
129
|
+
const outputReal = realpathSync(output);
|
|
130
|
+
if (outputReal !== join(realpathSync(delivery.projectTarget), 'elanous-out', action.taskId)) throw new Error('delivery output points outside target');
|
|
131
|
+
const root = realpathSync(delivery.worktreePath) + sep;
|
|
132
|
+
for (const name of delivery.files) {
|
|
133
|
+
const source = resolve(root, name);
|
|
134
|
+
if (!source.startsWith(root) || !lstatSync(source).isFile() || !realpathSync(source).startsWith(root)) throw new Error(`invalid delivery file: ${name}`);
|
|
135
|
+
const destination = resolve(output, name);
|
|
136
|
+
if (!destination.startsWith(resolve(output) + sep)) throw new Error(`invalid destination: ${name}`);
|
|
137
|
+
mkdirSync(join(destination, '..'), { recursive: true });
|
|
138
|
+
const parent = realpathSync(join(destination, '..'));
|
|
139
|
+
if (parent !== outputReal && !parent.startsWith(outputReal + sep)) throw new Error(`invalid destination parent: ${name}`);
|
|
140
|
+
try { if (!lstatSync(destination).isFile()) throw new Error(`invalid destination: ${name}`); }
|
|
141
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
142
|
+
copyFileSync(source, destination);
|
|
143
|
+
copied.push(destination);
|
|
144
|
+
}
|
|
145
|
+
} catch (error) { return record('failed', `delivery copy failed: ${String(error)}`); }
|
|
146
|
+
const evidence = readDeliveryEvidence(delivery.kind, resolve(output), copied);
|
|
147
|
+
debug.log('task-agent', 'deliver', { taskId: action.taskId, kind: delivery.kind, files: copied, evidenceOk: evidence.ok });
|
|
148
|
+
const judgement = judgeNextMove({ deliveryEvidence: evidence });
|
|
149
|
+
return record(judgement.move === 'propose-green' ? 'done' : 'waiting', `${judgement.move}: ${judgement.reason}`);
|
|
150
|
+
}
|
|
90
151
|
if (action.kind === 'green') return card('green requires a verified landing in this execution');
|
|
91
152
|
// G1 — 기다림은 발사가 아니다: 대기 표지만 남기고 다음 틱이 다시 판단한다(카드로 올리지 않는다).
|
|
92
153
|
if (action.kind === 'retry' && action.variant === 'wait') return record('waiting', `wait marker: ${action.rationale}`);
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 보드 일반형 — 프로젝트 › 목표 › 이정표 › 과제 (RFC-loop-agent-map §A4b①).
|
|
3
|
+
*
|
|
4
|
+
* «판(0.2.x)·칸» 체크리스트는 이 보드의 한 사례(내부 팩)다. 어댑터 둘은 «읽기»만 한다:
|
|
5
|
+
* - `cardsBoard(statePath)` — task-agent-actions.json 의 과제 카드(project.id · goal · milestone)를 트리로.
|
|
6
|
+
* - `releasePackBoard(version)` — 릴리스 체크리스트를 project=elanous · milestone=판 · task=칸 으로 사상. 원장은 쓰지 않는다.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { releaseLedgerRoot } from '../instance/resolve.js';
|
|
11
|
+
import { listChecklist, type ChecklistStatus } from '../release-loop/checklist.js';
|
|
12
|
+
import { readTaskAgentState, type CompletionKind, type TaskCard } from './task-hand.js';
|
|
13
|
+
|
|
14
|
+
export type BoardLevel = 'project' | 'goal' | 'milestone' | 'task';
|
|
15
|
+
export type BoardStatus = 'open' | 'done' | 'blocked';
|
|
16
|
+
|
|
17
|
+
export interface BoardNode {
|
|
18
|
+
level: BoardLevel;
|
|
19
|
+
id: string;
|
|
20
|
+
title: string;
|
|
21
|
+
status: BoardStatus;
|
|
22
|
+
/** 과제 노드만 — 카드의 종결 종류(비면 code-pr 로 읽는다 · 칸은 싣지 않는다). */
|
|
23
|
+
completion?: CompletionKind;
|
|
24
|
+
/** 부모 노드 id(프로젝트는 없다). */
|
|
25
|
+
parent?: string;
|
|
26
|
+
/** 프로젝트 노드만 — 프로젝트 없는 카드의 자리(id 는 빈 문자열이라 실제 프로젝트 id 와 겹치지 않는다). */
|
|
27
|
+
unassigned?: true;
|
|
28
|
+
children?: BoardNode[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 프로젝트가 없는 카드가 모이는 자리의 id — 빈 문자열(실제 프로젝트 id 는 비지 않는다 · `handTask` 가 빈 id 를 거부). */
|
|
32
|
+
export const UNASSIGNED_PROJECT = '';
|
|
33
|
+
/** 내부 팩(릴리스 체크리스트)의 프로젝트 id. */
|
|
34
|
+
export const INTERNAL_PACK_PROJECT = 'elanous';
|
|
35
|
+
|
|
36
|
+
function branch(level: BoardLevel, id: string, title: string, parent?: string): BoardNode {
|
|
37
|
+
// 무소속 프로젝트의 id 는 '' — 빈 문자열도 부모 id 로 보존한다.
|
|
38
|
+
return { level, id, title, status: 'open', ...(parent !== undefined ? { parent } : {}), children: [] };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function child(parent: BoardNode, level: BoardLevel, id: string, title = id): BoardNode {
|
|
42
|
+
let node = parent.children!.find((candidate) => candidate.level === level && candidate.id === id);
|
|
43
|
+
if (!node) {
|
|
44
|
+
node = branch(level, id, title, parent.id);
|
|
45
|
+
parent.children!.push(node);
|
|
46
|
+
}
|
|
47
|
+
return node;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 자식이 있는 노드의 상태 = 막힘 하나라도 → blocked · 전부 done → done · 그 밖 open. */
|
|
51
|
+
function aggregate(node: BoardNode): void {
|
|
52
|
+
if (!node.children?.length) return;
|
|
53
|
+
for (const next of node.children) aggregate(next);
|
|
54
|
+
node.status = node.children.some((next) => next.status === 'blocked') ? 'blocked'
|
|
55
|
+
: node.children.every((next) => next.status === 'done') ? 'done' : 'open';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cardStatus(card: TaskCard): BoardStatus {
|
|
59
|
+
if (card.status === 'launch-failed') return 'blocked';
|
|
60
|
+
return card.greenProposal ? 'done' : 'open';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 과제 카드 → 프로젝트 트리. 프로젝트 없는 카드는 «무소속»(`unassigned: true` · id '') 아래 — 어떤 프로젝트 id 로도 걸러지지 않는다. */
|
|
64
|
+
export function cardsBoard(statePath: string): BoardNode[] {
|
|
65
|
+
const projects: BoardNode[] = [];
|
|
66
|
+
const cards = readTaskAgentState<{ tasks?: Record<string, TaskCard> }>(statePath).tasks ?? {};
|
|
67
|
+
for (const card of Object.values(cards)) {
|
|
68
|
+
const projectId = card.project?.id?.trim() || UNASSIGNED_PROJECT;
|
|
69
|
+
const unassigned = projectId === UNASSIGNED_PROJECT;
|
|
70
|
+
let project = projects.find((node) => node.id === projectId && (node.unassigned === true) === unassigned);
|
|
71
|
+
if (!project) {
|
|
72
|
+
project = unassigned ? { ...branch('project', UNASSIGNED_PROJECT, '무소속'), unassigned: true } : branch('project', projectId, projectId);
|
|
73
|
+
projects.push(project);
|
|
74
|
+
}
|
|
75
|
+
const goal = card.goal ? child(project, 'goal', card.goal) : project;
|
|
76
|
+
const milestone = card.milestone ? child(goal, 'milestone', card.milestone) : goal;
|
|
77
|
+
milestone.children!.push({
|
|
78
|
+
level: 'task', id: card.id, title: card.text, status: cardStatus(card),
|
|
79
|
+
...(card.completion ? { completion: card.completion } : {}),
|
|
80
|
+
parent: milestone.id,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
for (const project of projects) aggregate(project);
|
|
84
|
+
return projects;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function releaseStatus(status: ChecklistStatus): BoardStatus {
|
|
88
|
+
return status === 'green' || status === 'done' ? 'done' : status === 'red' ? 'blocked' : 'open';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 릴리스 체크리스트 → 내부 팩 보드(읽기 사상). 원장이 없으면 빈 보드 — 만들지 않는다.
|
|
93
|
+
* root 를 명시해 넘겨 옛 JSON 가져오기(쓰기)를 건너뛴다: 이 어댑터는 원장을 바꾸지 않는다.
|
|
94
|
+
*/
|
|
95
|
+
export function releasePackBoard(version: string, root = releaseLedgerRoot()): BoardNode[] {
|
|
96
|
+
if (!existsSync(join(root, 'release', 'features.sqlite'))) return [];
|
|
97
|
+
const checklist = listChecklist(version, root);
|
|
98
|
+
const project = branch('project', INTERNAL_PACK_PROJECT, 'elanous (내부 팩)');
|
|
99
|
+
const milestone = child(project, 'milestone', checklist.version, `판 ${checklist.version}`);
|
|
100
|
+
for (const item of checklist.items) {
|
|
101
|
+
milestone.children!.push({ level: 'task', id: item.id, title: item.title, status: releaseStatus(item.status), parent: milestone.id });
|
|
102
|
+
}
|
|
103
|
+
aggregate(project);
|
|
104
|
+
return [project];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function cloneNode(node: BoardNode): BoardNode {
|
|
108
|
+
return { ...node, ...(node.children ? { children: node.children.map(cloneNode) } : {}) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 같은 수준·id 의 프로젝트·목표·이정표는 한 노드로 합친다(과제는 그대로 잇는다 — 다른 원천의 과제를 숨기지 않는다). */
|
|
112
|
+
function mergeInto(target: BoardNode[], incoming: readonly BoardNode[]): void {
|
|
113
|
+
for (const node of incoming) {
|
|
114
|
+
const existing = node.level === 'task' ? undefined
|
|
115
|
+
: target.find((candidate) => candidate.level === node.level && candidate.id === node.id && candidate.unassigned === node.unassigned);
|
|
116
|
+
if (existing) mergeInto(existing.children ??= [], node.children ?? []);
|
|
117
|
+
else target.push(cloneNode(node));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 두 어댑터를 합친다 — 같은 수준·id 의 노드는 한 노드로(예: 카드의 이정표 0.2.19 ⊕ 체크리스트의 판 0.2.19) · 상태를 다시 모은다. 입력은 바꾸지 않는다. */
|
|
122
|
+
export function mergeBoards(...boards: readonly BoardNode[][]): BoardNode[] {
|
|
123
|
+
const merged: BoardNode[] = [];
|
|
124
|
+
for (const board of boards) mergeInto(merged, board);
|
|
125
|
+
for (const project of merged) aggregate(project);
|
|
126
|
+
return merged;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 트리 안의 과제 노드 수. */
|
|
130
|
+
export function countBoardTasks(nodes: readonly BoardNode[]): number {
|
|
131
|
+
return nodes.reduce((count, node) => count + (node.level === 'task' ? 1 : 0) + countBoardTasks(node.children ?? []), 0);
|
|
132
|
+
}
|