elanous 0.2.17 → 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/J5H7BvqgSI8DUjtRRcA_S/_buildManifest.js +1 -0
- 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 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- 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 +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/coord-channel-watch.sh +2 -2
- package/scripts/discord-seat-probe.ts +6 -3
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- 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/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +8 -1
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-diff.ts +18 -0
- package/scripts/release-loop/gate-node.ts +436 -59
- package/scripts/release-loop/gate-partial.ts +106 -0
- package/scripts/release-loop/gate-timing.ts +120 -0
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +8 -1
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +174 -0
- package/scripts/release-loop/shard-plan.ts +65 -0
- package/scripts/release-loop/unattended-release.ts +45 -7
- package/scripts/release-loop/version-node.ts +145 -40
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/scripts/video/scene-table.ts +159 -0
- package/src/acp/core-turn-bridge.ts +39 -2
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/away/presence.ts +40 -0
- package/src/away/release-watch.ts +255 -0
- package/src/briefing/brief-items.ts +86 -17
- package/src/chat/index.ts +2 -1
- package/src/chat/short-question.ts +3 -40
- package/src/cli/autopilot-cli.ts +30 -11
- package/src/cli/away-cli.ts +68 -0
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/guardian-cli.ts +49 -0
- package/src/cli/intake-cli.ts +85 -7
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/pod-cli.ts +5 -1
- package/src/cli/provider-cli.ts +12 -2
- package/src/cli/pty-drive-cli.ts +14 -3
- package/src/cli/release-cli.ts +128 -19
- package/src/cli/setup-cli.ts +31 -1
- package/src/cli/tasks-cli.ts +403 -4
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- 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/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/guardian-tick.ts +138 -0
- package/src/domains/outbound-alert.ts +244 -40
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- 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/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/graph-runner/runner.ts +25 -5
- package/src/harness/harness-cli-command.ts +12 -2
- package/src/harness/harness-queue.ts +388 -33
- package/src/harness/harness-salvage-cli.ts +78 -0
- package/src/harness/harness-worktree-add.ts +68 -0
- package/src/harness/helper-scan.ts +10 -3
- package/src/harness/ledger-write-guard.ts +6 -16
- package/src/hq/hq.ts +6 -1
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +185 -18
- 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/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/digest.ts +39 -15
- package/src/intake-plane/discord-seat-work.ts +8 -2
- 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/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/degradation.ts +57 -0
- package/src/loops/orchestrator/finish-rate.ts +106 -4
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +450 -54
- package/src/maturity/feature-maturity.ts +8 -5
- package/src/maturity/tui-slash-maturity.ts +1 -0
- package/src/mss/logging/log-store.ts +3 -0
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/http-server.ts +16 -5
- package/src/nexus/api/loop-edges.ts +352 -23
- package/src/nexus/api/ops-api.ts +96 -28
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/nexus/index.ts +7 -0
- package/src/oauth/codex.ts +38 -0
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +106 -6
- package/src/release-loop/feature-store.ts +64 -16
- package/src/release-loop/landing-freeze.ts +6 -4
- package/src/release-loop/light-rc.ts +173 -0
- package/src/release-loop/placement.ts +64 -20
- package/src/release-loop/rubric.ts +136 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +12 -6
- package/src/self-dev/ask-launch-flow.ts +24 -3
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-cli.ts +36 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/orchestrate.ts +1 -0
- package/src/self-dev/pr-preview.ts +179 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-supervisor.ts +14 -0
- package/src/self-dev/shard-run.ts +23 -7
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/gate-cli.ts +29 -8
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author-clarification.ts +88 -13
- package/src/self-implement/goal-author.ts +8 -3
- package/src/self-implement/goal-clarification-escalation.ts +2 -0
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +61 -13
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/self-implement/salvage-branches.ts +254 -0
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +101 -7
- 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 +49 -5
- package/src/task-agent/live-moves.ts +176 -0
- package/src/task-agent/mission.ts +449 -0
- package/src/task-agent/shadow.ts +258 -0
- package/src/task-agent/task-hand.ts +337 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +64 -4
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
- 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 +88 -20
- package/src/telegram-away-command.ts +38 -0
- package/src/telegram-commands.ts +11 -0
- package/src/telegram-loops-command.ts +1 -1
- package/src/telegram-report-log-probe.ts +21 -0
- package/src/telegram-report.ts +27 -3
- package/src/telegram.ts +5 -20
- package/src/tui-client/dashboard-session.ts +1 -0
- package/src/user-config.ts +196 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-aac7332576c4d6fc.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/2196-4700653d12a44b40.js +0 -1
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.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/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-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4795-f372490b178583fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +0 -2
- package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.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-4ce1c42c8d89903f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +0 -25
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
- 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-3143b07a817d1da9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-17ba086ef82d339b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-6e5c3a867557298b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4187666132fc98c4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-09328d499162bae4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-29ff3a8f84f03b6a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-80ec7401bc83f511.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-ab93b240f687d69a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-fc7d86467291ae38.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-043a9111e74f6b24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-23a4601ceb7f452e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-10d958605a77b3bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-12916918988ac97e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.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-6e878ebbe1737d00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-7f49fe645d5132f7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d4fef9ccd7d406b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.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-539cde99554be20f.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → J5H7BvqgSI8DUjtRRcA_S}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `elanous harness salvage <runId...> [--json]` — 런마다 «PR #n | 수확 가지 <branch> (+a/-b · N files) | 없음 | 모름(<이유>)».
|
|
3
|
+
* SALVAGE-VISIBLE(0.2.19) — 판정은 `src/self-implement/salvage-branches.ts`, 여기는 배선·출력·관측만.
|
|
4
|
+
*/
|
|
5
|
+
import type { Command } from 'commander';
|
|
6
|
+
import { debug } from '../debug/log.js';
|
|
7
|
+
import { normalizeRunId } from './harness-space.js';
|
|
8
|
+
import { loadFederatedRunLedger } from '../self-implement/run-ledger.js';
|
|
9
|
+
import {
|
|
10
|
+
decideRunSalvage,
|
|
11
|
+
findRunSalvageBranches,
|
|
12
|
+
gitSalvageRunner,
|
|
13
|
+
listRemoteSalvageRefs,
|
|
14
|
+
prStateFromLedger,
|
|
15
|
+
type RunPrState,
|
|
16
|
+
type RunSalvageVerdict,
|
|
17
|
+
type SalvageGitRunner,
|
|
18
|
+
} from '../self-implement/salvage-branches.js';
|
|
19
|
+
|
|
20
|
+
export interface HarnessSalvageDeps {
|
|
21
|
+
git?: SalvageGitRunner;
|
|
22
|
+
readPr?: (runId: string) => RunPrState;
|
|
23
|
+
print?: (line: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function defaultReadPr(runId: string): RunPrState {
|
|
27
|
+
try {
|
|
28
|
+
return prStateFromLedger(loadFederatedRunLedger(runId, { includeTest: true }));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return { status: 'unknown', reason: `원장 읽기 실패 · ${error instanceof Error ? error.message : String(error)}` };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 여러 런을 ls-remote «한 번»으로 판정한다. PR 이 있는 런은 git 을 더 부르지 않는다. */
|
|
35
|
+
export function runHarnessSalvage(runIds: readonly string[], deps: HarnessSalvageDeps = {}): RunSalvageVerdict[] {
|
|
36
|
+
const git = deps.git ?? gitSalvageRunner(process.cwd());
|
|
37
|
+
const readPr = deps.readPr ?? defaultReadPr;
|
|
38
|
+
let listing: ReturnType<typeof listRemoteSalvageRefs> | undefined;
|
|
39
|
+
const verdicts: RunSalvageVerdict[] = [];
|
|
40
|
+
for (const raw of runIds) {
|
|
41
|
+
const runId = normalizeRunId(raw);
|
|
42
|
+
const pr = readPr(runId);
|
|
43
|
+
let verdict: RunSalvageVerdict;
|
|
44
|
+
if (pr.status === 'pr') {
|
|
45
|
+
verdict = decideRunSalvage(runId, pr, { status: 'not-checked' });
|
|
46
|
+
} else {
|
|
47
|
+
listing ??= listRemoteSalvageRefs(git);
|
|
48
|
+
verdict = decideRunSalvage(runId, pr, findRunSalvageBranches(runId, git, { listing }));
|
|
49
|
+
}
|
|
50
|
+
const top = verdict.salvage.status === 'found' ? verdict.salvage.branches[0] : undefined;
|
|
51
|
+
debug.log('harness.salvage', 'visible', {
|
|
52
|
+
runId,
|
|
53
|
+
outcome: verdict.outcome,
|
|
54
|
+
branch: top?.branch ?? null,
|
|
55
|
+
files: top?.diffstat.files ?? null,
|
|
56
|
+
...(verdict.pr.status === 'pr' ? { prNumber: verdict.pr.number } : {}),
|
|
57
|
+
...(verdict.salvage.status === 'unknown' ? { reason: verdict.salvage.reason } : {}),
|
|
58
|
+
...(verdict.pr.status === 'unknown' ? { prReason: verdict.pr.reason } : {}),
|
|
59
|
+
});
|
|
60
|
+
verdicts.push(verdict);
|
|
61
|
+
}
|
|
62
|
+
return verdicts;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function installHarnessSalvageCommand(harnessCmd: Command, deps: HarnessSalvageDeps = {}): Command {
|
|
66
|
+
return harnessCmd
|
|
67
|
+
.command('salvage')
|
|
68
|
+
.description('런 → PR | 수확 가지(salvage/*) | 없음 | 모름 — Pod 런이 멈추며 남긴 가지를 보인다')
|
|
69
|
+
.argument('<runIds...>', '런 id (run-xxxxxxxx-…)')
|
|
70
|
+
.option('--json', '런마다 { runId, outcome, pr, salvage } 를 JSON 배열로')
|
|
71
|
+
.action((runIds: string[], opts: { json?: boolean }) => {
|
|
72
|
+
const print = deps.print ?? ((line: string) => console.log(line));
|
|
73
|
+
const verdicts = runHarnessSalvage(runIds, deps);
|
|
74
|
+
if (opts.json) print(JSON.stringify(verdicts.map(({ line: _line, ...rest }) => rest)));
|
|
75
|
+
else for (const verdict of verdicts) print(verdict.line);
|
|
76
|
+
if (verdicts.some((verdict) => verdict.outcome === 'unknown')) process.exitCode = 1;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -2,6 +2,7 @@ import { createWorktree, type CreateWorktreeResult } from '../git-fs/worktree.js
|
|
|
2
2
|
import { runGitCommand } from '../git-fs/runner.js';
|
|
3
3
|
import { configuredWorktreeRoot } from '../user-config.js';
|
|
4
4
|
import { debug } from '../debug/log.js';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
5
6
|
|
|
6
7
|
const OWNER_CONFIG_KEY = 'elanous.harness.owner';
|
|
7
8
|
const COMMAND_CONFIG_KEY = 'elanous.harness.command';
|
|
@@ -87,8 +88,75 @@ function restoreWorktreeProvenance(worktreePath: string, snapshot: WorktreeConfi
|
|
|
87
88
|
}
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/** Keys that must leave the shared config once `extensions.worktreeConfig` is on (`git help worktree` §CONFIGURATION FILE).
|
|
92
|
+
* `core.bare=false` is harmless when shared (linked trees default to non-bare), so only a true value moves. */
|
|
93
|
+
const MAIN_TREE_ONLY_CONFIG_KEYS = ['core.bare', 'core.worktree'] as const;
|
|
94
|
+
|
|
95
|
+
/** Moves `core.bare=true` / `core.worktree` from `$GIT_COMMON_DIR/config` to `$GIT_COMMON_DIR/config.worktree`.
|
|
96
|
+
* Runs every time (also when the extension is already on) and throws before the caller enables the extension.
|
|
97
|
+
* Incident 10-07 (HQ-WORKTREE-CONFIG): a shared `core.bare=true` made every linked worktree of a bare repo unusable. */
|
|
98
|
+
/** Values are moved byte-for-byte (a `core.worktree` path may carry edge whitespace), so read them NUL-terminated. */
|
|
99
|
+
function nulTerminatedValue(stdout: unknown, key: string): string {
|
|
100
|
+
const text = String(stdout ?? '');
|
|
101
|
+
if (!text.endsWith('\0')) throw new Error(`harness worktree ${key} read failed — git config emitted an unterminated value`);
|
|
102
|
+
return text.slice(0, -1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function moveMainTreeOnlyConfig(worktreePath: string): void {
|
|
106
|
+
const common = gitAt(worktreePath, ['rev-parse', '--git-common-dir']);
|
|
107
|
+
const commonOut = String(common.stdout ?? '').trim();
|
|
108
|
+
if (common.status !== 0 || !commonOut) {
|
|
109
|
+
throw new Error(`harness worktree common git dir lookup failed — ${gitConfigFailure(common)}`);
|
|
110
|
+
}
|
|
111
|
+
const commonDir = resolve(worktreePath, commonOut);
|
|
112
|
+
const shared = join(commonDir, 'config');
|
|
113
|
+
const perTree = join(commonDir, 'config.worktree');
|
|
114
|
+
// `null` = absent. Typed reads judge meaning (`yes`/`TRUE` are true); raw reads carry the bytes that move.
|
|
115
|
+
const readValue = (file: string, key: string, typed: boolean): string | null => {
|
|
116
|
+
const read = gitAt(worktreePath, ['config', '--file', file, ...(typed ? ['--type=bool'] : []), '--null', '--get', key]);
|
|
117
|
+
if (read.status === 1) return null;
|
|
118
|
+
if (read.status !== 0) throw new Error(`harness worktree ${key} read failed (${file}) — ${gitConfigFailure(read)}`);
|
|
119
|
+
return nulTerminatedValue(read.stdout, key);
|
|
120
|
+
};
|
|
121
|
+
for (const key of MAIN_TREE_ONLY_CONFIG_KEYS) {
|
|
122
|
+
const typed = key === 'core.bare';
|
|
123
|
+
const meaning = readValue(shared, key, typed);
|
|
124
|
+
if (meaning === null) continue;
|
|
125
|
+
if (typed && meaning !== 'true') continue;
|
|
126
|
+
const raw = typed ? readValue(shared, key, false) : meaning;
|
|
127
|
+
if (raw === null) throw new Error(`harness worktree shared ${key} vanished during the move`);
|
|
128
|
+
// A valueless `bare` means true; writing its empty raw form would mean false.
|
|
129
|
+
const value = typed && raw === '' ? 'true' : raw;
|
|
130
|
+
// A conflicting per-tree value would flip the main tree once the extension is on — refuse instead of guessing.
|
|
131
|
+
// Every per-tree value must agree (not just the last one), so a mixed list also fails closed.
|
|
132
|
+
const existingRead = gitAt(worktreePath, ['config', '--file', perTree, ...(typed ? ['--type=bool'] : []), '--null', '--get-all', key]);
|
|
133
|
+
if (existingRead.status !== 0 && existingRead.status !== 1) {
|
|
134
|
+
throw new Error(`harness worktree ${key} read failed (${perTree}) — ${gitConfigFailure(existingRead)}`);
|
|
135
|
+
}
|
|
136
|
+
const existingValues = existingRead.status === 0 ? nulTerminatedValue(existingRead.stdout, key).split('\0') : [];
|
|
137
|
+
if (existingValues.some((existing) => existing !== meaning)) {
|
|
138
|
+
throw new Error(`harness worktree ${key} conflict — ${shared} has ${JSON.stringify(meaning)} but ${perTree} has ${JSON.stringify(existingValues)}; resolve by hand before enabling extensions.worktreeConfig`);
|
|
139
|
+
}
|
|
140
|
+
const existingMeaning = existingValues.length > 0 ? meaning : null;
|
|
141
|
+
if (existingMeaning === null) {
|
|
142
|
+
const write = gitAt(worktreePath, ['config', '--file', perTree, key, value]);
|
|
143
|
+
if (write.status !== 0) throw new Error(`harness worktree ${key} move failed — ${gitConfigFailure(write)}`);
|
|
144
|
+
}
|
|
145
|
+
const unset = gitAt(worktreePath, ['config', '--file', shared, '--unset-all', key]);
|
|
146
|
+
if (unset.status !== 0) throw new Error(`harness worktree shared ${key} unset failed — ${gitConfigFailure(unset)}`);
|
|
147
|
+
debug.log('harness.worktree', 'core-bare-moved', {
|
|
148
|
+
key,
|
|
149
|
+
value,
|
|
150
|
+
commonDir,
|
|
151
|
+
perTreeHadValue: existingMeaning !== null,
|
|
152
|
+
worktreePath,
|
|
153
|
+
}, { level: 'warn' });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
90
157
|
/** Records the shared worktree-scoped provenance used by both manual and self-implement creation paths. */
|
|
91
158
|
export function recordHarnessWorktreeProvenance(worktreePath: string, provenance: HarnessWorktreeProvenance): void {
|
|
159
|
+
moveMainTreeOnlyConfig(worktreePath);
|
|
92
160
|
const enable = gitAt(worktreePath, ['config', 'extensions.worktreeConfig', 'true']);
|
|
93
161
|
if (enable.status !== 0) {
|
|
94
162
|
throw new Error(`harness worktree owner declaration failed — ${gitConfigFailure(enable)}`);
|
|
@@ -81,9 +81,16 @@ export function classifyStoppedPr({ body, comments }: { body: string; comments:
|
|
|
81
81
|
action = '증거 재측 — 인용된 판정 명령을 호스트에서 다시 돌려 결과를 PR 에 남김; 코드 수정 아님';
|
|
82
82
|
} else if (/review-budget/i.test(stopEvidence) || basis === 'must-fix-reported') {
|
|
83
83
|
// Real supervisor bodies (UNCONVERGEABLE · implementation-deficit) carry `classificationBasis: must-fix-reported` instead of the RFC word.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const previous = latest && reviews.find((review) => review.round === latest.round - 1);
|
|
85
|
+
if (latest && previous && latest.round >= 2 && latest.count > 0 && latest.count >= previous.count) {
|
|
86
|
+
category = 'review-oscillation';
|
|
87
|
+
signals.push(`must-fix ${previous.count}→${latest.count} (round ${previous.round}→${latest.round}) → review-oscillation`);
|
|
88
|
+
action = '헬퍼 자식 spawn — 기존 시험 설계 보존 후 한 라운드';
|
|
89
|
+
} else {
|
|
90
|
+
if (basis === 'must-fix-reported') signals.push('classificationBasis=must-fix-reported → review-budget');
|
|
91
|
+
category = 'review-budget';
|
|
92
|
+
action = latest?.count ? '헬퍼 자식 spawn — must-fix 원문과 기존 시험 설계로 한 라운드' : '하지 않음 — must-fix 0 또는 미관측; 수리 자식 대상 아님';
|
|
93
|
+
}
|
|
87
94
|
} else if (/no-progress/i.test(stopEvidence) || basis === 'no-must-fix-without-clean-worktree-or-completed-without-changes') {
|
|
88
95
|
if (basis === 'no-must-fix-without-clean-worktree-or-completed-without-changes') signals.push(`classificationBasis=${basis} → no-progress`);
|
|
89
96
|
category = 'no-progress';
|
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
1
|
+
import { isAbsolute, relative, sep } from 'node:path';
|
|
3
2
|
import { debug } from '../debug/log.js';
|
|
4
3
|
import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
|
|
5
|
-
|
|
6
|
-
function physicalPath(path: string): string {
|
|
7
|
-
const absolute = resolve(path);
|
|
8
|
-
try {
|
|
9
|
-
return realpathSync(absolute);
|
|
10
|
-
} catch (error) {
|
|
11
|
-
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
12
|
-
const parent = dirname(absolute);
|
|
13
|
-
if (parent === absolute) throw error;
|
|
14
|
-
return resolve(physicalPath(parent), relative(parent, absolute));
|
|
15
|
-
}
|
|
16
|
-
}
|
|
4
|
+
import { isInsideOpsRoot, physicalPath, testProcessSignal } from '../instance/test-write-guard.js';
|
|
17
5
|
|
|
18
6
|
/** Refuse a test's write to the production instance, including an explicitly supplied ledger directory. */
|
|
19
7
|
export function refuseProductionLedgerWriteInTest(
|
|
@@ -23,11 +11,13 @@ export function refuseProductionLedgerWriteInTest(
|
|
|
23
11
|
productionRoot = prodInstanceRoot(),
|
|
24
12
|
instanceRoot = effectiveInstanceRoot(),
|
|
25
13
|
): boolean {
|
|
26
|
-
if (env.
|
|
14
|
+
if (!testProcessSignal(env, env === process.env ? undefined : '')) return false;
|
|
27
15
|
const target = physicalPath(root);
|
|
28
16
|
const production = physicalPath(productionRoot);
|
|
29
17
|
const withinProduction = relative(production, target);
|
|
30
|
-
|
|
18
|
+
const outsideHomeRoot = withinProduction === '..' || withinProduction.startsWith(`..${sep}`) || isAbsolute(withinProduction);
|
|
19
|
+
// The account's real ~/.elanous is production too when a runner redirected HOME (TEST-PROD-LEAK).
|
|
20
|
+
if (outsideHomeRoot && !isInsideOpsRoot(root)) return false;
|
|
31
21
|
try {
|
|
32
22
|
debug.log('harness.incidents', 'write-refused', { store, root: target, effectiveInstanceRoot: instanceRoot });
|
|
33
23
|
} catch { /* Observation cannot reopen a closed write boundary. */ }
|
package/src/hq/hq.ts
CHANGED
|
@@ -108,7 +108,7 @@ function resolve(deps: HqDeps = {}): Resolved {
|
|
|
108
108
|
ssh,
|
|
109
109
|
probe: deps.probe ?? tailnetProbe(config),
|
|
110
110
|
localPath: deps.localPath ?? join(getElanousConfigDir(), 'hq', 'local.json'),
|
|
111
|
-
seenPath: deps.seenPath ??
|
|
111
|
+
seenPath: deps.seenPath ?? (deps.localPath && !config.seenGenerationFile ? `${deps.localPath}.seen-generation` : hqSeenGenerationPath(config)),
|
|
112
112
|
now: deps.now ?? (() => Math.floor(Date.now() / 1000)),
|
|
113
113
|
log,
|
|
114
114
|
};
|
|
@@ -161,6 +161,11 @@ function writeLocal(path: string, state: LocalHqState): void {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
/** Highest lease generation this host ever observed (null = never). */
|
|
164
|
+
/** The seen-generation marker this host keeps (hq.seenGenerationFile · default ~/.elanous-hq/seen-generation). */
|
|
165
|
+
export function hqSeenGenerationPath(config: Pick<HqConfig, 'seenGenerationFile'>): string {
|
|
166
|
+
return config.seenGenerationFile ?? join(homedir(), '.elanous-hq', 'seen-generation');
|
|
167
|
+
}
|
|
168
|
+
|
|
164
169
|
export function readSeenGeneration(path: string): number | null {
|
|
165
170
|
try {
|
|
166
171
|
if (!existsSync(path)) return null;
|
package/src/hq/seats.ts
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
// HQ seat folders are disposable views of one bare repository. No scheduler or human checkout is modified here.
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { runGitCommand } from '../git-fs/runner.js';
|
|
5
|
+
|
|
6
|
+
type HqSeatRole = 'OP' | 'MK' | 'TC' | 'UX';
|
|
7
|
+
const ROLES: readonly string[] = ['OP', 'MK', 'TC', 'UX'];
|
|
8
|
+
interface HouseLedger {
|
|
9
|
+
home: { kind: 'home'; path: string; source: 'cli-resolved' };
|
|
10
|
+
// `reclaiming` = the merged tip a `work done` proved before it started removing; it lets an interrupted done resume.
|
|
11
|
+
sandboxes: Record<string, { kind: 'sandbox'; path: string; owner: HqSeatRole; reclaiming?: string }>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hqRoot(home = process.env.HOME): string {
|
|
15
|
+
if (!home) throw new Error('HOME is required (or pass --root)');
|
|
16
|
+
return join(home, 'elanous-hq');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A first fetch or checkout of the real monorepo takes minutes; only fetch gets the long ceiling.
|
|
20
|
+
function git(cwd: string, ...args: string[]): string {
|
|
21
|
+
const result = runGitCommand(cwd, args, { encoding: 'utf8', timeout: args.includes('fetch') ? 1_800_000 : 600_000 });
|
|
22
|
+
if (result.status !== 0) throw new Error(`git ${args.join(' ')}: ${result.stderr.trim() || `exit ${result.status}`}`);
|
|
23
|
+
return result.stdout.trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function repository(root: string): string {
|
|
27
|
+
const repo = join(root, 'repo.git');
|
|
28
|
+
if (git(repo, 'rev-parse', '--is-bare-repository') !== 'true') throw new Error(`not a bare repository: ${repo}`);
|
|
29
|
+
return repo;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ledgerPath(root: string): string { return join(root, 'house.json'); }
|
|
33
|
+
function readLedger(root: string): HouseLedger {
|
|
34
|
+
const ledger = JSON.parse(readFileSync(ledgerPath(root), 'utf8')) as HouseLedger;
|
|
35
|
+
if (!ledger.home?.path || !ledger.sandboxes || typeof ledger.sandboxes !== 'object') throw new Error('invalid house.json; refusing to guess houses');
|
|
36
|
+
return ledger;
|
|
37
|
+
}
|
|
38
|
+
function saveLedger(root: string, ledger: HouseLedger): void {
|
|
39
|
+
const path = ledgerPath(root);
|
|
40
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
41
|
+
writeFileSync(tmp, `${JSON.stringify(ledger, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
42
|
+
try { renameSync(tmp, path); }
|
|
43
|
+
catch (error) { rmSync(tmp, { force: true }); throw error; }
|
|
44
|
+
}
|
|
45
|
+
// The lock covers the read as well as the atomic rename: a rename alone cannot prevent lost updates.
|
|
46
|
+
// It also serializes repo.git worktree/branch mutations: the git runner retries lock contention, and a
|
|
47
|
+
// retried `worktree add -b` that already created its branch fails with "branch already exists".
|
|
48
|
+
function withLedgerLock<T>(root: string, update: () => T, waitMs = 120_000): T {
|
|
49
|
+
const lock = `${ledgerPath(root)}.lock`;
|
|
50
|
+
const deadline = Date.now() + waitMs;
|
|
51
|
+
while (true) {
|
|
52
|
+
try { mkdirSync(lock); break; }
|
|
53
|
+
catch (error) {
|
|
54
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
55
|
+
if (Date.now() >= deadline) throw new Error(`house.json lock timed out: ${lock}`);
|
|
56
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
try { return update(); }
|
|
60
|
+
finally { rmdirSync(lock); }
|
|
61
|
+
}
|
|
62
|
+
/** Caller must hold the ledger lock. */
|
|
63
|
+
function updateLedgerLocked(root: string, update: (ledger: HouseLedger) => void): void {
|
|
64
|
+
const ledger = readLedger(root);
|
|
65
|
+
update(ledger);
|
|
66
|
+
saveLedger(root, ledger);
|
|
67
|
+
}
|
|
68
|
+
function roleOf(value: string): HqSeatRole {
|
|
69
|
+
if (!ROLES.includes(value)) throw new Error(`invalid seat role: ${value} (OP|MK|TC|UX)`);
|
|
70
|
+
return value as HqSeatRole;
|
|
71
|
+
}
|
|
72
|
+
function workName(name: string): string {
|
|
73
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(name)) throw new Error(`invalid work name: ${name}`);
|
|
74
|
+
return name;
|
|
75
|
+
}
|
|
76
|
+
function clean(path: string): boolean { return git(path, 'status', '--porcelain') === ''; }
|
|
77
|
+
function assertRegisteredWorktree(repo: string, path: string): void {
|
|
78
|
+
// The path itself must be the registered worktree: a symlink to another registered worktree
|
|
79
|
+
// (e.g. seats/TC -> work/TC-fix1) would otherwise pass and let a refresh detach that work.
|
|
80
|
+
if (lstatSync(path).isSymbolicLink()) throw new Error(`not a worktree of ${repo}: ${path} (symlink)`);
|
|
81
|
+
const own = join(realpathSync(dirname(path)), basename(path));
|
|
82
|
+
const entries = git(repo, 'worktree', 'list', '--porcelain').split('\n');
|
|
83
|
+
const registered = entries.some(line => line.startsWith('worktree ') && line.slice(9) === own);
|
|
84
|
+
if (!registered) throw new Error(`not a worktree of ${repo}: ${path}`);
|
|
85
|
+
if (realpathSync(git(path, 'rev-parse', '--git-common-dir')) !== realpathSync(repo)) {
|
|
86
|
+
throw new Error(`not a worktree of ${repo}: ${path}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const SANDBOX_IGNORE = '/.elanous-test/';
|
|
90
|
+
// The sandbox is filled by ordinary work, so every view of repo.git ignores it: `status` stays clean and
|
|
91
|
+
// `worktree remove` (no --force) deletes it with the worktree instead of refusing a merged work.
|
|
92
|
+
function ensureSandboxIgnored(repo: string): void {
|
|
93
|
+
const exclude = join(repo, 'info', 'exclude');
|
|
94
|
+
const current = existsSync(exclude) ? readFileSync(exclude, 'utf8') : '';
|
|
95
|
+
if (current.split('\n').includes(SANDBOX_IGNORE)) return;
|
|
96
|
+
mkdirSync(join(repo, 'info'), { recursive: true });
|
|
97
|
+
writeFileSync(exclude, `${current}${current && !current.endsWith('\n') ? '\n' : ''}${SANDBOX_IGNORE}\n`);
|
|
98
|
+
}
|
|
99
|
+
// A local relative origin (`../remote.git`) is relative to the calling checkout, not to repo.git. Git itself
|
|
100
|
+
// resolves it from the checkout's top level even when run from a subdirectory, so the same base is used here.
|
|
101
|
+
function callerOrigin(): string {
|
|
102
|
+
const origin = git(process.cwd(), 'remote', 'get-url', 'origin');
|
|
103
|
+
const url = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(origin);
|
|
104
|
+
const scpLike = /^[^/]*:/.test(origin);
|
|
105
|
+
if (url || scpLike || isAbsolute(origin)) return origin;
|
|
106
|
+
return resolve(git(process.cwd(), 'rev-parse', '--show-toplevel'), origin);
|
|
107
|
+
}
|
|
108
|
+
function mergedIntoMain(repo: string, commit: string): boolean {
|
|
109
|
+
const merged = runGitCommand(repo, ['merge-base', '--is-ancestor', commit, 'refs/remotes/origin/main'], { encoding: 'utf8' });
|
|
110
|
+
if (merged.status !== 0 && merged.status !== 1) throw new Error(`cannot verify merge: ${merged.stderr.trim()}`);
|
|
111
|
+
return merged.status === 0;
|
|
112
|
+
}
|
|
113
|
+
// Git lock files are created exclusively; ours carry our pid. A lock left by a killed `work done` is never
|
|
114
|
+
// removed automatically (between our check and a delete, a live git could have re-taken it): the error names it.
|
|
115
|
+
function takeLock(lock: string): void {
|
|
116
|
+
try { writeFileSync(lock, `hq-done ${process.pid}\n`, { flag: 'wx' }); }
|
|
117
|
+
catch (error) {
|
|
118
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
119
|
+
let content = '';
|
|
120
|
+
try { content = readFileSync(lock, 'utf8').trim(); } catch { /* gone or unreadable: report as held */ }
|
|
121
|
+
const owner = /^hq-done (\d+)$/.exec(content);
|
|
122
|
+
if (owner && !processAlive(Number(owner[1]))) throw new Error(`stale lock from a stopped hq work done (pid ${owner[1]}): remove ${lock} and rerun`);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function processAlive(pid: number): boolean {
|
|
127
|
+
try { process.kill(pid, 0); return true; }
|
|
128
|
+
catch (error) { return (error as NodeJS.ErrnoException).code === 'EPERM'; }
|
|
129
|
+
}
|
|
130
|
+
// Scoped `worktree prune`: drop only the admin entry whose checkout is `path` and is already gone.
|
|
131
|
+
function removeStaleWorktreeAdmin(repo: string, path: string): void {
|
|
132
|
+
const admins = join(repo, 'worktrees');
|
|
133
|
+
if (!existsSync(admins) || existsSync(path)) return;
|
|
134
|
+
for (const name of readdirSync(admins)) {
|
|
135
|
+
const gitdir = join(admins, name, 'gitdir');
|
|
136
|
+
if (existsSync(gitdir) && readFileSync(gitdir, 'utf8').trim() === join(path, '.git')) rmSync(join(admins, name), { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function quoteShell(value: string): string { return `'${value.replaceAll("'", "'\\''")}'`; }
|
|
140
|
+
|
|
141
|
+
/** Origin is read from the calling checkout; nothing in that checkout is moved or reconfigured. */
|
|
142
|
+
export function hqInit(options: { root?: string; home?: string } = {}): { root: string; created: boolean; cron: string } {
|
|
143
|
+
const root = resolve(options.root ?? hqRoot());
|
|
144
|
+
const repo = join(root, 'repo.git');
|
|
145
|
+
if (!options.home) throw new Error('resolved home house is required to register house.json');
|
|
146
|
+
mkdirSync(options.home, { recursive: true });
|
|
147
|
+
const home = realpathSync(options.home);
|
|
148
|
+
mkdirSync(root, { recursive: true });
|
|
149
|
+
// Concurrent inits serialize here: the repo.git setup and the first house.json registration are both
|
|
150
|
+
// decided under the ledger lock, so neither side can overwrite the other or miss a home mismatch.
|
|
151
|
+
const created = withLedgerLock(root, () => {
|
|
152
|
+
if (existsSync(ledgerPath(root))) {
|
|
153
|
+
const registered = readLedger(root).home.path;
|
|
154
|
+
if (registered !== home) throw new Error(`house.json home mismatch: registered ${registered}; requested ${home}`);
|
|
155
|
+
}
|
|
156
|
+
const fresh = !existsSync(repo);
|
|
157
|
+
if (fresh) {
|
|
158
|
+
const source = callerOrigin();
|
|
159
|
+
git(root, 'init', '--bare', repo);
|
|
160
|
+
git(repo, 'remote', 'add', 'origin', source);
|
|
161
|
+
}
|
|
162
|
+
repository(root);
|
|
163
|
+
if (!fresh) {
|
|
164
|
+
// An existing mirror must be of the repository init is run for; never re-point or reuse another one.
|
|
165
|
+
const mirrored = runGitCommand(repo, ['remote', 'get-url', 'origin'], { encoding: 'utf8' });
|
|
166
|
+
const caller = callerOrigin();
|
|
167
|
+
if (mirrored.status === 0 && mirrored.stdout.trim() !== caller) throw new Error(`repo.git mirrors ${mirrored.stdout.trim()}, not ${caller}; refusing to reuse ${root}`);
|
|
168
|
+
}
|
|
169
|
+
// An interrupted first fetch leaves a bare repository but no origin/main. Finish it on retry.
|
|
170
|
+
const remote = runGitCommand(repo, ['remote', 'get-url', 'origin'], { encoding: 'utf8' });
|
|
171
|
+
if (remote.status !== 0) {
|
|
172
|
+
// Only an empty, just-initialized repo.git (an init stopped before `remote add`) is finished here.
|
|
173
|
+
if (git(repo, 'for-each-ref') !== '' || existsSync(join(repo, 'worktrees'))) throw new Error(`repo.git has no origin but is not empty; refusing to adopt ${repo}`);
|
|
174
|
+
const source = callerOrigin();
|
|
175
|
+
git(repo, 'remote', 'add', 'origin', source);
|
|
176
|
+
}
|
|
177
|
+
const main = runGitCommand(repo, ['rev-parse', '--verify', 'refs/remotes/origin/main'], { encoding: 'utf8' });
|
|
178
|
+
if (fresh || remote.status !== 0 || main.status !== 0) {
|
|
179
|
+
git(repo, 'config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*');
|
|
180
|
+
git(repo, 'fetch', 'origin');
|
|
181
|
+
}
|
|
182
|
+
git(repo, 'rev-parse', '--verify', 'refs/remotes/origin/main');
|
|
183
|
+
if (!existsSync(ledgerPath(root))) {
|
|
184
|
+
saveLedger(root, { home: { kind: 'home', path: home, source: 'cli-resolved' }, sandboxes: {} });
|
|
185
|
+
}
|
|
186
|
+
return fresh;
|
|
187
|
+
}, 2_100_000); // longer than the 30-minute first fetch, so a concurrent init reaches the home check
|
|
188
|
+
// Print only: OP installs the line. Only existing role views can be refreshed; init never creates seats.
|
|
189
|
+
// cron runs with a minimal PATH, so the line names this runtime and CLI entry by absolute path.
|
|
190
|
+
const cli = [process.execPath, process.argv[1]].filter((part): part is string => Boolean(part)).map(part => quoteShell(resolve(part))).join(' ');
|
|
191
|
+
const script = `git --git-dir=${quoteShell(repo)} fetch origin && ${ROLES.map(role => `( [ ! -d ${quoteShell(join(root, 'seats', role))} ] || ${cli} hq seat ${role} --refresh --root ${quoteShell(root)} )`).join(' && ')}`;
|
|
192
|
+
const cron = `*/5 * * * * ${script}`;
|
|
193
|
+
return { root, created, cron };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function hqSeat(value: string, options: { root?: string; refresh?: boolean } = {}): { path: string; outcome: 'created' | 'unchanged' | 'refreshed' | 'dirty' } {
|
|
197
|
+
const role = roleOf(value);
|
|
198
|
+
const root = resolve(options.root ?? hqRoot());
|
|
199
|
+
const repo = repository(root);
|
|
200
|
+
readLedger(root);
|
|
201
|
+
const path = join(root, 'seats', role);
|
|
202
|
+
// A new seat starts from the current origin/main, not from whatever the mirror last fetched.
|
|
203
|
+
if (!existsSync(path)) git(repo, 'fetch', 'origin');
|
|
204
|
+
const created = withLedgerLock(root, () => {
|
|
205
|
+
if (existsSync(path)) return false;
|
|
206
|
+
mkdirSync(join(root, 'seats'), { recursive: true });
|
|
207
|
+
// A seat folder deleted by hand leaves its git registration behind; drop only that one so the seat is recreated.
|
|
208
|
+
removeStaleWorktreeAdmin(repo, join(realpathSync(join(root, 'seats')), role));
|
|
209
|
+
git(repo, 'worktree', 'add', '--detach', path, 'refs/remotes/origin/main');
|
|
210
|
+
return true;
|
|
211
|
+
});
|
|
212
|
+
if (created) return { path, outcome: 'created' };
|
|
213
|
+
// Refuse an unrelated checkout even when it is clean; do not touch its HEAD.
|
|
214
|
+
assertRegisteredWorktree(repo, path);
|
|
215
|
+
// Seats are always detached; a worktree on a branch here (e.g. a work moved into seats/) is someone's work.
|
|
216
|
+
const onBranch = runGitCommand(path, ['symbolic-ref', '--quiet', '--short', 'HEAD'], { encoding: 'utf8' });
|
|
217
|
+
if (onBranch.status === 0) throw new Error(`not a seat: ${path} is on branch ${onBranch.stdout.trim()}; refusing to touch it`);
|
|
218
|
+
// A dirty seat must be byte-for-byte untouched, including its detached HEAD.
|
|
219
|
+
if (options.refresh && !clean(path)) return { path, outcome: 'dirty' };
|
|
220
|
+
if (!options.refresh) return { path, outcome: 'unchanged' };
|
|
221
|
+
git(repo, 'fetch', 'origin');
|
|
222
|
+
// Concurrent refreshes of one seat (cron and a person) serialize on the house lock; each re-checks first.
|
|
223
|
+
return withLedgerLock(root, () => {
|
|
224
|
+
assertRegisteredWorktree(repo, path);
|
|
225
|
+
if (runGitCommand(path, ['symbolic-ref', '--quiet', 'HEAD'], { encoding: 'utf8' }).status === 0) throw new Error(`not a seat: ${path} is on a branch; refusing to touch it`);
|
|
226
|
+
if (!clean(path)) return { path, outcome: 'dirty' as const };
|
|
227
|
+
// Commits made on the detached seat are not on origin/main; moving HEAD would orphan them. Treat as dirty.
|
|
228
|
+
if (!mergedIntoMain(repo, git(path, 'rev-parse', 'HEAD'))) return { path, outcome: 'dirty' as const };
|
|
229
|
+
git(path, 'checkout', '--detach', 'refs/remotes/origin/main');
|
|
230
|
+
return { path, outcome: 'refreshed' as const };
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function hqWork(action: 'new' | 'done', value: string, nameValue: string, options: { root?: string } = {}): { path: string; outcome: 'created' | 'removed' } {
|
|
235
|
+
const role = roleOf(value);
|
|
236
|
+
const name = workName(nameValue);
|
|
237
|
+
const root = resolve(options.root ?? hqRoot());
|
|
238
|
+
const repo = repository(root);
|
|
239
|
+
readLedger(root);
|
|
240
|
+
const branch = `work/${role}-${name}`;
|
|
241
|
+
const path = join(root, 'work', `${role}-${name}`);
|
|
242
|
+
if (action === 'new') {
|
|
243
|
+
if (existsSync(path)) throw new Error(`work already exists: ${path}`);
|
|
244
|
+
// fetch stays outside the lock: it only moves remote-tracking refs, is idempotent and lock-retried, and the
|
|
245
|
+
// cron line runs it unlocked anyway. Branch, worktree and ledger changes below are all under the lock.
|
|
246
|
+
git(repo, 'fetch', 'origin');
|
|
247
|
+
withLedgerLock(root, () => {
|
|
248
|
+
if (existsSync(path)) throw new Error(`work already exists: ${path}`);
|
|
249
|
+
mkdirSync(join(root, 'work'), { recursive: true });
|
|
250
|
+
ensureSandboxIgnored(repo);
|
|
251
|
+
const base = git(repo, 'rev-parse', '--verify', 'refs/remotes/origin/main');
|
|
252
|
+
git(repo, 'worktree', 'add', '-b', branch, path, base);
|
|
253
|
+
try {
|
|
254
|
+
const sandbox = join(path, '.elanous-test');
|
|
255
|
+
mkdirSync(sandbox);
|
|
256
|
+
updateLedgerLocked(root, ledger => {
|
|
257
|
+
ledger.sandboxes[path] = { kind: 'sandbox', path: realpathSync(sandbox), owner: role };
|
|
258
|
+
});
|
|
259
|
+
} catch (error) {
|
|
260
|
+
// Roll back only a work that is still exactly what was just created (clean, at base, branch at base),
|
|
261
|
+
// without --force and with a compare-and-delete; anything a hook or another writer added is kept.
|
|
262
|
+
let untouched = false;
|
|
263
|
+
// `--ignored` also sees files in the (ignored) sandbox; only the empty sandbox dir we made is invisible to it.
|
|
264
|
+
try { untouched = git(path, 'rev-parse', '--verify', 'HEAD') === base && git(path, 'status', '--porcelain', '--ignored') === ''; } catch { /* unreadable: keep it */ }
|
|
265
|
+
const removed = untouched ? runGitCommand(repo, ['worktree', 'remove', path], { encoding: 'utf8' }) : undefined;
|
|
266
|
+
const unbranched = removed?.status === 0 ? runGitCommand(repo, ['update-ref', '-d', `refs/heads/${branch}`, base], { encoding: 'utf8' }) : undefined;
|
|
267
|
+
const rolledBack = !untouched ? `work kept: it changed after creation: ${path}`
|
|
268
|
+
: removed?.status !== 0 ? `work kept: ${removed?.stderr.trim()}`
|
|
269
|
+
: unbranched?.status !== 0 ? `rollback incomplete; branch kept: ${unbranched?.stderr.trim()}`
|
|
270
|
+
: 'rolled back';
|
|
271
|
+
throw new Error(`work registration failed (${rolledBack}): ${error instanceof Error ? error.message : String(error)}`);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
return { path, outcome: 'created' };
|
|
275
|
+
}
|
|
276
|
+
git(repo, 'fetch', 'origin');
|
|
277
|
+
withLedgerLock(root, () => {
|
|
278
|
+
if (!existsSync(path)) {
|
|
279
|
+
// Reconcile a done that removed the worktree but failed before the ledger or branch caught up.
|
|
280
|
+
if (!readLedger(root).sandboxes[path]) throw new Error(`work does not exist: ${path}`);
|
|
281
|
+
// A moved worktree (`git worktree move`) is still live work on this branch: never reclaim it from here.
|
|
282
|
+
// Only this work's own stale registration is cleaned below; other (maybe briefly missing) worktrees are left.
|
|
283
|
+
const own = join(existsSync(dirname(path)) ? realpathSync(dirname(path)) : dirname(path), basename(path));
|
|
284
|
+
const elsewhere = git(repo, 'worktree', 'list', '--porcelain').split('\n\n')
|
|
285
|
+
.filter(block => block.split('\n').includes(`branch refs/heads/${branch}`))
|
|
286
|
+
.map(block => block.split('\n')[0]!.slice(9))
|
|
287
|
+
.find(location => location !== own);
|
|
288
|
+
if (elsewhere) throw new Error(`work branch ${branch} is checked out at ${elsewhere}; refusing to reclaim ${path}`);
|
|
289
|
+
const ref = runGitCommand(repo, ['rev-parse', '--verify', '--quiet', `refs/heads/${branch}`], { encoding: 'utf8' });
|
|
290
|
+
if (ref.status !== 0) {
|
|
291
|
+
// Without the branch, the only merge proof is the tip an interrupted done recorded before removing.
|
|
292
|
+
const recorded = readLedger(root).sandboxes[path]?.reclaiming;
|
|
293
|
+
if (!recorded || !mergedIntoMain(repo, recorded)) throw new Error(`cannot verify merge: branch ${branch} is missing; house.json entry kept: ${path}`);
|
|
294
|
+
updateLedgerLocked(root, ledger => { delete ledger.sandboxes[path]; });
|
|
295
|
+
removeStaleWorktreeAdmin(repo, own);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
const tip = ref.stdout.trim();
|
|
299
|
+
const merged = runGitCommand(repo, ['merge-base', '--is-ancestor', tip, 'refs/remotes/origin/main'], { encoding: 'utf8' });
|
|
300
|
+
if (merged.status !== 0) throw new Error(merged.status === 1 ? `work is not merged into origin/main: ${branch}` : `cannot verify merge: ${merged.stderr}`);
|
|
301
|
+
// Same order as a normal done: the entry goes first, so a save that fails again keeps the branch (the proof).
|
|
302
|
+
const entry = readLedger(root).sandboxes[path];
|
|
303
|
+
updateLedgerLocked(root, ledger => { delete ledger.sandboxes[path]; });
|
|
304
|
+
const deleted = runGitCommand(repo, ['update-ref', '-d', `refs/heads/${branch}`, tip], { encoding: 'utf8' });
|
|
305
|
+
if (deleted.status !== 0) {
|
|
306
|
+
// The branch moved after the merge check: put the registration back so done can be retried.
|
|
307
|
+
updateLedgerLocked(root, ledger => { ledger.sandboxes[path] = entry; });
|
|
308
|
+
throw new Error(`work branch moved after the merge check; registration kept: ${branch}: ${deleted.stderr.trim()}`);
|
|
309
|
+
}
|
|
310
|
+
removeStaleWorktreeAdmin(repo, own);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
// Only work that `hq work new` registered is reclaimed; a hand-made worktree at the same path is not ours.
|
|
314
|
+
const entry = readLedger(root).sandboxes[path];
|
|
315
|
+
if (entry?.owner !== role) throw new Error(`work is not registered in house.json; refusing to remove: ${path}`);
|
|
316
|
+
assertRegisteredWorktree(repo, path);
|
|
317
|
+
if (!clean(path)) throw new Error(`work is dirty; refusing to remove: ${path}`);
|
|
318
|
+
const ref = `refs/heads/${branch}`;
|
|
319
|
+
const attached = runGitCommand(path, ['symbolic-ref', '--quiet', '--short', 'HEAD'], { encoding: 'utf8' });
|
|
320
|
+
const branchTip = runGitCommand(repo, ['rev-parse', '--verify', '--quiet', ref], { encoding: 'utf8' });
|
|
321
|
+
// Resume: an interrupted done left the work detached at its recorded merged tip, with the branch either
|
|
322
|
+
// still at that tip (stopped before the delete) or already deleted (stopped before the removal).
|
|
323
|
+
const branchPresent = branchTip.status === 0;
|
|
324
|
+
const resuming = attached.status !== 0 && entry.reclaiming !== undefined
|
|
325
|
+
&& git(path, 'rev-parse', 'HEAD') === entry.reclaiming
|
|
326
|
+
&& (!branchPresent || branchTip.stdout.trim() === entry.reclaiming);
|
|
327
|
+
if (!resuming && (attached.status !== 0 || attached.stdout.trim() !== branch)) throw new Error(`work branch mismatch: ${path}`);
|
|
328
|
+
const tip = resuming ? entry.reclaiming! : branchTip.stdout.trim();
|
|
329
|
+
// The ancestor check is against origin/main, not bare HEAD (which may still point to master).
|
|
330
|
+
if (!mergedIntoMain(repo, tip)) throw new Error(`work is not merged into origin/main: ${branch}`);
|
|
331
|
+
// Removal order keeps every interruption resumable: record the proven tip → detach → hold the work's git
|
|
332
|
+
// locks → compare-and-delete the branch → remove the worktree → drop the registration last.
|
|
333
|
+
// Holding this worktree's HEAD.lock and index.lock makes git refuse every commit and `add` in it until the
|
|
334
|
+
// removal ends (`worktree remove` itself does not need them), so no commit can land between the last
|
|
335
|
+
// check and the removal. The locks live in the worktree's admin dir, which the removal deletes.
|
|
336
|
+
updateLedgerLocked(root, ledger => { ledger.sandboxes[path] = { ...entry, reclaiming: tip }; });
|
|
337
|
+
const admin = git(path, 'rev-parse', '--absolute-git-dir');
|
|
338
|
+
const held: string[] = [];
|
|
339
|
+
const release = (): void => { for (const lock of held.splice(0)) rmSync(lock, { force: true }); };
|
|
340
|
+
const restore = (): string => {
|
|
341
|
+
release();
|
|
342
|
+
const head = runGitCommand(path, ['rev-parse', '--verify', 'HEAD'], { encoding: 'utf8' });
|
|
343
|
+
runGitCommand(repo, ['update-ref', ref, head.status === 0 ? head.stdout.trim() : tip, ''], { encoding: 'utf8' });
|
|
344
|
+
const reattached = runGitCommand(path, ['symbolic-ref', 'HEAD', ref], { encoding: 'utf8' }).status === 0;
|
|
345
|
+
// If HEAD could not be re-attached (e.g. a stale HEAD.lock), keep `reclaiming` so the next done resumes.
|
|
346
|
+
const restored = reattached ? { kind: entry.kind, path: entry.path, owner: entry.owner } : { ...entry, reclaiming: tip };
|
|
347
|
+
try { updateLedgerLocked(root, ledger => { ledger.sandboxes[path] = restored; }); return reattached ? 'registration restored' : 'left resumable'; }
|
|
348
|
+
catch (error) { return `REGISTRATION NOT RESTORED (fix ${path} in house.json by hand): ${error instanceof Error ? error.message : String(error)}`; }
|
|
349
|
+
};
|
|
350
|
+
const fail = (message: string): never => { throw new Error(`${message}; nothing removed: ${path} · ${restore()}`); };
|
|
351
|
+
if (!resuming) git(path, 'checkout', '--quiet', '--detach');
|
|
352
|
+
for (const name of ['HEAD.lock', 'index.lock']) {
|
|
353
|
+
const lock = join(admin, name);
|
|
354
|
+
try { takeLock(lock); held.push(lock); }
|
|
355
|
+
catch (error) { fail(`work is busy (another git process holds ${name}): ${error instanceof Error ? error.message : String(error)}`); }
|
|
356
|
+
}
|
|
357
|
+
if (git(path, 'rev-parse', 'HEAD') !== tip) fail('work gained a commit after the merge check');
|
|
358
|
+
if (branchPresent) {
|
|
359
|
+
const deleted = runGitCommand(repo, ['update-ref', '-d', ref, tip], { encoding: 'utf8' });
|
|
360
|
+
if (deleted.status !== 0) fail(`work branch moved after the merge check: ${deleted.stderr.trim()}`);
|
|
361
|
+
}
|
|
362
|
+
if (!clean(path)) fail('work changed during done');
|
|
363
|
+
// Without --force, git also refuses a worktree that became dirty after the check above.
|
|
364
|
+
const removed = runGitCommand(repo, ['worktree', 'remove', path], { encoding: 'utf8' });
|
|
365
|
+
if (removed.status !== 0) fail(`git worktree remove failed: ${removed.stderr.trim()}`);
|
|
366
|
+
updateLedgerLocked(root, ledger => { delete ledger.sandboxes[path]; });
|
|
367
|
+
});
|
|
368
|
+
return { path, outcome: 'removed' };
|
|
369
|
+
}
|