elanous 0.2.20 → 0.2.21
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/MObU8ujkMciAKr6ljAV05/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/088d8e8a.8069543d082bec12.js +160 -0
- package/apps/pwa/out/_next/static/chunks/1109.78a7a3288c0259d9.js +148 -0
- package/apps/pwa/out/_next/static/chunks/115.1c6690b8604f81c4.js +32 -0
- package/apps/pwa/out/_next/static/chunks/119fc678.6f591cdb84ca5791.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1262.76a3f08e67c5f816.js +309 -0
- package/apps/pwa/out/_next/static/chunks/{1307-77fad80cf54477c6.js → 1307-c97eba9fb72a9a5e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1358d292.5c87f12cad6eaa91.js +122 -0
- package/apps/pwa/out/_next/static/chunks/1438.27a1a7b8f2308004.js +149 -0
- package/apps/pwa/out/_next/static/chunks/1442.02d85cd88f4c1eac.js +79 -0
- package/apps/pwa/out/_next/static/chunks/1501-1aad662d82ba43f7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1584.9e2dd53da6e6d4b1.js +24 -0
- package/apps/pwa/out/_next/static/chunks/1586.b989b0614afb77e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1609.2ad355062870151f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1612-f6df0fc0eed7157b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1923.37f1d5fe7ebe3860.js +27 -0
- package/apps/pwa/out/_next/static/chunks/1948.5f35b39eef222a94.js +5 -0
- package/apps/pwa/out/_next/static/chunks/1993.3fd074a2a246234e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2140.d2daf9ee96e295d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2212.0359851457473efc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224.583e562365a52f74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2328.0f37465646bafa7d.js +36 -0
- package/apps/pwa/out/_next/static/chunks/2372.bf71372184991937.js +286 -0
- package/apps/pwa/out/_next/static/chunks/2549-a8515c86f9e8cb96.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2655.72cc6a97276c5acc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2743-ef6e8c3ced74cce5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2840-5458ad50c49cac6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2856.f75d5baef7f49b74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3344.0de4b0ec5a454b7a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/335.a13f339e97893437.js +94 -0
- package/apps/pwa/out/_next/static/chunks/3394.646348f34b2500be.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3443.6276909b53d72d53.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3495.abd4254d808bf2dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3522-1de1319d028ac89c.js +25 -0
- package/apps/pwa/out/_next/static/chunks/3721.ba5b82db0d9a5572.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3919.9ff7dc25afefee33.js +74 -0
- package/apps/pwa/out/_next/static/chunks/3998.bd9a69d9b50e9bd2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4003.ab6db1dc266576c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4216.e8ac94b9fe6b96ab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4327.c44dc2b55cb43ee5.js +34 -0
- package/apps/pwa/out/_next/static/chunks/4401.94257d497b6d26bc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4559.17ca2cdbaa8d414f.js +115 -0
- package/apps/pwa/out/_next/static/chunks/4588.ad3364cb7fb97ada.js +86 -0
- package/apps/pwa/out/_next/static/chunks/4706.97e734bd4c9816ca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4982.6e44352f751b9a1b.js +77 -0
- package/apps/pwa/out/_next/static/chunks/5342.49c5c5a530e62b50.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-08a80abdf3a4da99.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5431.66c58ab1327ee0b7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5717.bcfcb96e701b82e0.js +90 -0
- package/apps/pwa/out/_next/static/chunks/5755-30d102586789bf7f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5957.3f1a4012388dd962.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-59fedea50d13c762.js +1 -0
- package/apps/pwa/out/_next/static/chunks/602.9bf02ac8e9502598.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6020.3a1b347e2d2dfacc.js +8 -0
- package/apps/pwa/out/_next/static/chunks/{6158.af2e93b7adff709b.js → 6158.566c890adc06a459.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6375.fce1d174ba6d0324.js +64 -0
- package/apps/pwa/out/_next/static/chunks/6546.92a828aef441271f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/658.957fdb0a8c6c6c8e.js +287 -0
- package/apps/pwa/out/_next/static/chunks/6706.323590aabb8b1293.js +226 -0
- package/apps/pwa/out/_next/static/chunks/6747.b67947f293a4c02d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6767.8e6c5f6e744fcd9f.js +38 -0
- package/apps/pwa/out/_next/static/chunks/6868.000b24a3045c5574.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6946.bba74e9c96050f30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7033.a558f1b2f6912a53.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7065.46029c26c53eac50.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7087.6fae577d09249bc1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7175-350f4ce2873bc038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/722.de8d9801e0f6d414.js +41 -0
- package/apps/pwa/out/_next/static/chunks/7497.57981e9d7a75a52a.js +24 -0
- package/apps/pwa/out/_next/static/chunks/7630.2b9a68659053ff4e.js +193 -0
- package/apps/pwa/out/_next/static/chunks/7672.4af122ad892117c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/771.6ab889a233e7dae7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7899.597af4a9996c907f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/79b78388.663188133449d707.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8096.f7da0776cfde6300.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8194.484f51e273d9abd2.js +217 -0
- package/apps/pwa/out/_next/static/chunks/8769.815ca69879f608b4.js +104 -0
- package/apps/pwa/out/_next/static/chunks/8857-f1762140ef9a96b1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/88729267.9c6c000157c83f2f.js +62 -0
- package/apps/pwa/out/_next/static/chunks/8f835b4f.048eeb02299e3e12.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9068.5f2764fccf86c1b2.js +93 -0
- package/apps/pwa/out/_next/static/chunks/9247-7581825d511a64c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9451.ebfb0dcc9c02250f.js +23 -0
- package/apps/pwa/out/_next/static/chunks/9497-b0e758d1cd4ae8d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9559.e648369b1d57cf9a.js +62 -0
- package/apps/pwa/out/_next/static/chunks/9832.2ab2e818013574c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-40c16bd0efdb04b8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-937c92292a4ab4f0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-85b316bb4235980b.js → page-d781be3e3a9cfd74.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-9b2b26a33cb2cd42.js → page-b0a80997903852d8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c9955fde67b1bcb5.js → page-2b1263165630d9c8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-7a852956c74015a3.js → page-ea998f7f5d156bea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-33c328b64ca508f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-4ba4abe096b6c2a8.js → page-e2d6bed48ac20120.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-575d4a8b4f7becb2.js → page-111cabe4d1fd69ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/{page-f5d56ff5f45ad164.js → page-544dd4bd4847eeb7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-2260e9eef0357421.js → page-3ee191319bf97d87.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e4bcb2c503dab04b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-300897dcbbd5ed76.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-466bf0ee16ad1432.js → page-a449f1ed472b81c2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-a23bc71713bcc08f.js → page-9045094d6d3b5cd0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-c06673596849ef45.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-103fa3191c1b2e08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-9f81233f29dcd409.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be8b4615c919e36c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-c685b104e0dfa609.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-8d693331cfbb6add.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-cef4f30a4c22fae1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66ea1b54fc83fa15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-ec102c9b8b691e37.js → page-7a12f61be4a579d1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-c85b0589591a99c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-725a4da02dd129eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-f6055495e9ed7ea8.js → page-8cc707bd09a497d9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-90b55be1f15ce0e8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cb16606e08be65db.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-16dc64b74ed6a40a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-21bec4afeb2f0917.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-db03ae3589a81c9e.js → page-dc39ee7a36d26385.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4a6463b97f0ad94f.js → page-84ae759f76053672.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-75bffecc9aa1004e.js → page-3db58ddb61fe2be9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-c5315e30f3528dc4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-31d5b875760bb62d.js → page-2fe368efb6de3b6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-c26fa10aac6c21c4.js → page-8d04132dd0ac5e10.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-68793c6e10331d66.js → page-f23484de467c9ea3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-46d5201d2207a511.js → page-c69762987b751174.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5456692bda39aafb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-be1c4e8697f22360.js → page-d75f7cd65845ce98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-5603f5e568b25ce3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-d2cc0f4e89f42028.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-0fd895fab9b62c1b.js → page-a6eb73b8a4840f1c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c0a3f4d10edb83fa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-c6e4364d9de8f717.js → page-6c0b88068c143f15.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-0acc8e198f7b0dfb.js → page-7f8d7fcdf058a845.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-34882c84f84021c5.js → page-22163556cd38bc88.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-141436fd26dd56fc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/d70879db.c66c56a95a91efc6.js +66 -0
- package/apps/pwa/out/_next/static/chunks/f8626309.a4e5d362b1d55e59.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-f2b4016b9b981bc8.js +1 -0
- package/apps/pwa/out/_next/static/css/{d3427996da11e480.css → b9d8151e48998417.css} +1 -1
- package/apps/pwa/out/_next/static/css/e81df11490b4413b.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/live-run/index.html +1 -0
- package/apps/pwa/out/live-run/index.txt +35 -0
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/drafts/recipes.yaml +4 -4
- package/graphs/implement-loop.yaml +277 -5
- package/package.json +1 -1
- package/scripts/coord-channel-watch.sh +31 -4
- package/scripts/docs-lint.ts +4 -1
- package/scripts/marketing/generate-ax-brief.ts +18 -1
- package/scripts/measure-nl-routing-corpus.ts +11 -3
- package/scripts/release-loop/gate-node.ts +5 -5
- package/scripts/release-loop/shard-plan.ts +50 -5
- package/scripts/rhythm/daily.ts +2 -1
- package/src/browser-cdp/client.ts +10 -5
- package/src/cli/ops-cli.ts +11 -0
- package/src/cli/ops-overview-cli.ts +50 -0
- package/src/cli/pod-cli.ts +1 -0
- package/src/cli/tasks-cli.ts +106 -0
- package/src/dashboard/index.ts +40 -4
- package/src/esc-abort-gate.ts +3 -1
- package/src/graph-runner/graph-cli.ts +57 -0
- package/src/graph-runner/graph-run-api.ts +13 -0
- package/src/graph-wizard/archetypes.ts +88 -0
- package/src/graph-wizard/generate.ts +545 -0
- package/src/graph-wizard/saved-steps.ts +120 -0
- package/src/graph-wizard/steps.ts +193 -0
- package/src/harness/harness-cli-command.ts +68 -12
- package/src/index.ts +7 -4
- package/src/intake-plane/digest.ts +18 -11
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/nexus/api/draft-metrics.ts +114 -0
- package/src/nexus/api/graph-wizard-api.ts +53 -0
- package/src/nexus/api/graphs-api.ts +50 -12
- package/src/nexus/api/harness-traversal.ts +205 -0
- package/src/nexus/api/http-server.ts +31 -0
- package/src/nexus/outbound/router.ts +28 -8
- package/src/onboarding/first-run.ts +3 -0
- package/src/ops/overview-collect.ts +345 -0
- package/src/ops/overview-seat-era.ts +90 -0
- package/src/ops/overview.ts +223 -0
- package/src/release-loop/gate-shards.ts +7 -2
- package/src/self-dev/ask-launch-flow.ts +4 -2
- package/src/self-dev/ask-launch-io.ts +3 -1
- package/src/self-dev/graph-journey-nodes.ts +157 -0
- package/src/self-dev/graph-journey-route.ts +105 -0
- package/src/self-dev/launch-preflight.ts +43 -3
- package/src/self-dev/release-path-guard.ts +7 -4
- package/src/self-implement/frozen-merges.ts +23 -0
- package/src/self-implement/goal-author.ts +14 -0
- package/src/self-implement/heal-intake.ts +3 -0
- package/src/self-implement/orchestrator.ts +16 -1
- package/src/self-implement/seams.ts +3 -0
- package/src/self-implement/self-implement-runtime.ts +12 -0
- package/src/session-runtime/index.ts +5 -3
- package/src/shell-env-bootstrap.ts +17 -3
- package/src/task-agent/card-evidence.ts +6 -0
- package/src/task-agent/parent-host.ts +315 -0
- package/src/task-agent/task-hand.ts +9 -2
- package/src/task-orchestrator/surfaces/pod-command-job.ts +41 -6
- package/src/task-orchestrator/surfaces/pod-gate-deps.ts +123 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +39 -6
- package/src/tool-runtime/browser-runtime.ts +87 -3
- package/src/tool-runtime/index.ts +1 -0
- package/src/user-config.ts +20 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/Bam1YziK2iH9bN4K0utGk/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1612-f60c0c93ec773f81.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/2116-6429c188eb846ab8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/224-e7be8f762fe74986.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2549-89c518a4c8ed4d00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5755-ea7ed88eb956f47b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-94ed6f78e9814b2b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7748-6e51bfe927019f5c.js +0 -25
- package/apps/pwa/out/_next/static/chunks/9247-c1753a059e7bac08.js +0 -1
- package/apps/pwa/out/_next/static/chunks/a9f2b54d-33c0689b70ed8cf3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-c855c24de7db74fe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-c5a5c621be0e04ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-7c058069c5e7fa63.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-dc5da0b8a2edb8a4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ed4a6f799f005d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-9cd565005b93aca6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-55f0f40c6db95f15.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-474cb5888ea0e8f3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-35bfec2e631d3eae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-9ca21f9ad3208a61.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-f9ccb1b1adbea14d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-a9c2cfa8dda922a5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6da35144878feb6d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-25204694fa9984a5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a60e035f151d84d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-189c587f36874538.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-c56634bde705a65d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a8d2fe0c669e0de4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d81c07a7b4f618ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-cf0a4b4abead71ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-a4717e2c4bf7a28f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8b6c1454a8e8f306.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-0506a1e0e4bdb2f5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-c260571513bf43c2.js +0 -1
- package/apps/pwa/out/_next/static/css/c23d959d3ee15e57.css +0 -1
- /package/apps/pwa/out/_next/static/{Bam1YziK2iH9bN4K0utGk → MObU8ujkMciAKr6ljAV05}/_ssgManifest.js +0 -0
- /package/skills/project-onboarding/{skill.md → SKILL.md} +0 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HARNESS-PARENT-ON-MSB1 — `tasks hand --live` 의 하니스 «부모»를 다른 호스트(ssh)에서 띄우는 첫 조각 ⊕ 낡은 부모 보고.
|
|
3
|
+
*
|
|
4
|
+
* 왜 «첫 조각»인가 (2026-10-08 실측):
|
|
5
|
+
* - 부모가 호스트에서 기대는 것이 전부 «호스트 로컬 파일»이다 — 착지 동결(`landing-freeze.json` ⊕ `landing-merges/` 표지
|
|
6
|
+
* 무용), 풀 예약(`HostPoolLease`), 런 원장(`run-ledger/`), logs.db. 원격 부모가 `--merge-by-host` 로 병합하면 HQ 의
|
|
7
|
+
* 동결·컷 배수(drain)를 «못 본 채» 병합한다.
|
|
8
|
+
* - 그래서 원격 부모는 «PR 까지만» 간다: `--merge-by-host` → `--no-auto-merge`. 병합 권한은 HQ 에 남는다(동결 안전).
|
|
9
|
+
* - 발사 전 점검(preflight)이 하나라도 막히면 원격으로 띄우지 않고 «종전 로컬 발사»로 돌아간다(fail-closed · 관측 남김).
|
|
10
|
+
* - 관측: 카드에 `parentHost` 를 적고, `tasks show` 가 원격 런 원장을 ssh 로 HQ 의 거울 디렉터리로 당겨 읽는다.
|
|
11
|
+
*
|
|
12
|
+
* 기본 OFF — `taskAgent.parentHost` 가 없고 `ELANOUS_TA_PARENT_HOST` 도 없으면 아무것도 바뀌지 않는다.
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { join, resolve } from 'node:path';
|
|
17
|
+
import { debug } from '../debug/log.js';
|
|
18
|
+
import { shellQuote } from './task-hand.js';
|
|
19
|
+
|
|
20
|
+
/** `taskAgent.parentHost` 설정 칸. */
|
|
21
|
+
export interface ParentHostConfig {
|
|
22
|
+
/** ssh 대상(예: `node-b`). */
|
|
23
|
+
host: string;
|
|
24
|
+
/** 원격에서 부모를 띄울 git 작업 트리(절대 경로 · 원격 기준). */
|
|
25
|
+
cwd: string;
|
|
26
|
+
/** 원격 elanous 진입점 — 기본 `$HOME/.local/share/elanous/bin/elanous`(설치본). */
|
|
27
|
+
elanous?: string;
|
|
28
|
+
/** 원격 config-dir — 기본 `$HOME/.elanous`. */
|
|
29
|
+
configDir?: string;
|
|
30
|
+
/** 원격 부모에 줄 `--pod-pool`(원격 kube context 이름이 HQ 와 다를 때). */
|
|
31
|
+
podPool?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const PARENT_HOST_ENV = 'ELANOUS_TA_PARENT_HOST';
|
|
35
|
+
const DEFAULT_REMOTE_ELANOUS = '$HOME/.local/share/elanous/bin/elanous';
|
|
36
|
+
const DEFAULT_REMOTE_CONFIG_DIR = '$HOME/.elanous';
|
|
37
|
+
const HOST_RE = /^[A-Za-z0-9][A-Za-z0-9._@-]{0,127}$/;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 켜졌나 — env 가 이긴다(`off`·`0`·빈 값이면 끔). env 가 호스트를 주면 나머지 칸(cwd 등)은 설정에서 받는다.
|
|
41
|
+
* cwd 가 없으면 켜지지 않는다(어디서 띄울지 모르는 원격 발사는 하지 않는다).
|
|
42
|
+
*/
|
|
43
|
+
export function resolveParentHost(config: Partial<ParentHostConfig> | undefined, env: NodeJS.ProcessEnv = process.env): ParentHostConfig | null {
|
|
44
|
+
const fromEnv = env[PARENT_HOST_ENV];
|
|
45
|
+
if (fromEnv !== undefined && ['', '0', 'off', 'false', 'local'].includes(fromEnv.trim().toLowerCase())) return null;
|
|
46
|
+
const host = (fromEnv?.trim() || config?.host || '').trim();
|
|
47
|
+
if (!host || !HOST_RE.test(host)) return null;
|
|
48
|
+
const cwd = config?.cwd?.trim();
|
|
49
|
+
if (!cwd || !cwd.startsWith('/')) return null;
|
|
50
|
+
return { host, cwd, ...(config?.elanous ? { elanous: config.elanous } : {}), ...(config?.configDir ? { configDir: config.configDir } : {}), ...(config?.podPool ? { podPool: config.podPool } : {}) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** `$HOME/…` 은 원격 셸이 풀도록 따옴표 밖에 둔다 — 나머지는 shellQuote. */
|
|
54
|
+
function remotePath(path: string): string {
|
|
55
|
+
return path.startsWith('$HOME/') ? `"$HOME"/${shellQuote(path.slice('$HOME/'.length))}` : shellQuote(path);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 원격 부모 인자 — HQ 병합 권한을 넘기지 않는다: `--merge-by-host` → `--no-auto-merge` ⊕ (있으면) `--pod-pool`. */
|
|
59
|
+
export function remoteParentArgs(args: readonly string[], podPool?: string): string[] {
|
|
60
|
+
const out = args.map((arg) => arg === '--merge-by-host' ? '--no-auto-merge' : arg);
|
|
61
|
+
if (!out.includes('--no-auto-merge') && out[0] === 'harness') out.splice(2, 0, '--no-auto-merge');
|
|
62
|
+
if (podPool && !out.includes('--pod-pool')) out.splice(2, 0, '--pod-pool', podPool);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 원격 로그 파일(원격 기준) — 부모 stdout/stderr. */
|
|
67
|
+
export function remoteParentLogPath(target: ParentHostConfig, runId: string): string {
|
|
68
|
+
return `${target.configDir ?? DEFAULT_REMOTE_CONFIG_DIR}/remote-parents/${runId}.log`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** ssh 로 넘길 원격 셸 한 줄 — 떼어 띄우고 pid 만 찍는다. */
|
|
72
|
+
export function remoteLaunchScript(target: ParentHostConfig, args: readonly string[], env: Record<string, string>): string {
|
|
73
|
+
const elanous = target.elanous ?? DEFAULT_REMOTE_ELANOUS;
|
|
74
|
+
const configDir = target.configDir ?? DEFAULT_REMOTE_CONFIG_DIR;
|
|
75
|
+
const log = remoteParentLogPath(target, env.ELANOUS_RUN_ID ?? 'unknown');
|
|
76
|
+
const envPart = Object.entries(env).filter(([key]) => /^[A-Z_][A-Z0-9_]*$/.test(key)).map(([key, value]) => `${key}=${shellQuote(value)}`).join(' ');
|
|
77
|
+
const argv = [remotePath(elanous), '--config-dir', remotePath(configDir), ...remoteParentArgs(args, target.podPool).map(shellQuote)].join(' ');
|
|
78
|
+
// 준비가 실패하면 «확실히 안 띄웠다»(not-launched=…)를 찍고 끝낸다 — 그때만 호출부가 로컬로 돌아간다.
|
|
79
|
+
// 띄운 뒤에는 짧게 살아 있는지 보고 pid 를 찍는다(조기 종료면 early-exit — 역시 «안 돈다»가 확실).
|
|
80
|
+
return `mkdir -p ${remotePath(`${configDir}/remote-parents`)} || { echo "not-launched=mkdir"; exit 3; }; `
|
|
81
|
+
+ `cd ${shellQuote(target.cwd)} || { echo "not-launched=cwd"; exit 3; }; `
|
|
82
|
+
+ `${envPart ? `env ${envPart} ` : ''}nohup ${argv} >> ${remotePath(log)} 2>&1 < /dev/null & p=$!; sleep 2; `
|
|
83
|
+
+ `if kill -0 "$p" 2>/dev/null; then echo "pid=$p"; else echo "not-launched=early-exit"; tail -3 ${remotePath(log)}; exit 4; fi`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** ssh 실행기(시험 주입). */
|
|
87
|
+
export type SshRunner = (host: string, script: string, timeoutMs: number) => { status: number | null; stdout: string; stderr: string };
|
|
88
|
+
|
|
89
|
+
export const defaultSshRunner: SshRunner = (host, script, timeoutMs) => {
|
|
90
|
+
const result = spawnSync('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=8', '-o', 'ControlPath=none', host, script], { encoding: 'utf8', timeout: timeoutMs, maxBuffer: 32 * 1024 * 1024 });
|
|
91
|
+
return { status: result.status, stdout: result.stdout ?? '', stderr: result.stderr ?? (result.error ? String(result.error) : '') };
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** 발사 전 점검 결과 — `gaps` 가 비어야 원격으로 띄운다. */
|
|
95
|
+
export interface ParentHostPreflight { ok: boolean; gaps: string[]; facts: Record<string, string> }
|
|
96
|
+
|
|
97
|
+
/** 원격 사실 한 번에 — key=value 줄. */
|
|
98
|
+
export function preflightScript(target: ParentHostConfig): string {
|
|
99
|
+
const elanous = remotePath(target.elanous ?? DEFAULT_REMOTE_ELANOUS);
|
|
100
|
+
const configDir = remotePath(target.configDir ?? DEFAULT_REMOTE_CONFIG_DIR);
|
|
101
|
+
return [
|
|
102
|
+
`echo "version=$(${elanous} --version 2>/dev/null | tail -1 | cut -d' ' -f1)"`,
|
|
103
|
+
`test -f ${configDir}/config.json && echo config=1`,
|
|
104
|
+
`grep -Eq '"podPool"[[:space:]]*:[[:space:]]*"[^"[:space:]]+' ${configDir}/config.json 2>/dev/null && echo podPool=1`,
|
|
105
|
+
`(cd ${shellQuote(target.cwd)} 2>/dev/null && git rev-parse --is-inside-work-tree >/dev/null 2>&1) && echo repo=1`,
|
|
106
|
+
`command -v kubectl >/dev/null 2>&1 && echo kubectl=1`,
|
|
107
|
+
`true`,
|
|
108
|
+
].join('; ');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface PreflightInput {
|
|
112
|
+
target: ParentHostConfig;
|
|
113
|
+
/** HQ 의 elanous 판(첫 낱말). */
|
|
114
|
+
localVersion: string;
|
|
115
|
+
/** HQ 착지 동결이 «발사도 막는» 상태인가 — 원격 부모는 그 동결을 못 본다. */
|
|
116
|
+
launchHoldActive: boolean;
|
|
117
|
+
ssh?: SshRunner;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 순수 판정 — 원격 사실 → 막는 칸 목록. */
|
|
121
|
+
export function judgeParentHostFacts(facts: Record<string, string>, input: Omit<PreflightInput, 'ssh' | 'target'> & { podPoolOverride?: boolean }): string[] {
|
|
122
|
+
const gaps: string[] = [];
|
|
123
|
+
if (facts.unreachable) gaps.push(`unreachable: ${facts.unreachable}`);
|
|
124
|
+
else {
|
|
125
|
+
if (!facts.version) gaps.push('remote-elanous-missing');
|
|
126
|
+
else if (facts.version !== input.localVersion) gaps.push(`version-mismatch: remote ${facts.version} ≠ HQ ${input.localVersion}`);
|
|
127
|
+
if (facts.config !== '1') gaps.push('remote-config-missing');
|
|
128
|
+
if (facts.podPool !== '1' && !input.podPoolOverride) gaps.push('remote-pod-pool-unset');
|
|
129
|
+
if (facts.repo !== '1') gaps.push('remote-repo-missing');
|
|
130
|
+
if (facts.kubectl !== '1') gaps.push('remote-kubectl-missing');
|
|
131
|
+
}
|
|
132
|
+
if (input.launchHoldActive) gaps.push('hq-launch-freeze-active');
|
|
133
|
+
return gaps;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function preflightParentHost(input: PreflightInput): ParentHostPreflight {
|
|
137
|
+
const ssh = input.ssh ?? defaultSshRunner;
|
|
138
|
+
const facts: Record<string, string> = {};
|
|
139
|
+
const result = ssh(input.target.host, preflightScript(input.target), 20_000);
|
|
140
|
+
if (result.status !== 0) facts.unreachable = (result.stderr.trim().split('\n').pop() || `rc=${result.status}`).slice(0, 200);
|
|
141
|
+
else for (const line of result.stdout.split('\n')) {
|
|
142
|
+
const at = line.indexOf('=');
|
|
143
|
+
if (at > 0) facts[line.slice(0, at).trim()] = line.slice(at + 1).trim();
|
|
144
|
+
}
|
|
145
|
+
const gaps = judgeParentHostFacts(facts, { localVersion: input.localVersion, launchHoldActive: input.launchHoldActive, podPoolOverride: Boolean(input.target.podPool) });
|
|
146
|
+
return { ok: gaps.length === 0, gaps, facts };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 원격이 «확실히 안 띄웠다»고 답했다 — 호출부가 로컬로 돌아가도 중복 발사가 아니다. */
|
|
150
|
+
export class RemoteParentNotLaunchedError extends Error {
|
|
151
|
+
constructor(host: string, reason: string) { super(`원격 부모 안 뜸(${host}): ${reason}`); this.name = 'RemoteParentNotLaunchedError'; }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 원격 발사 — pid 를 받으면 성공. 원격이 `not-launched=…` 로 답하면 `RemoteParentNotLaunchedError`(로컬로 가도 된다).
|
|
156
|
+
* 그 밖(ssh 끊김·시간 초과·알 수 없는 답)은 «떴는지 모른다» — 일반 Error 로 던져 카드가 launch-failed 로 남게 한다
|
|
157
|
+
* (⛔ 로컬로 또 띄우면 원격에 이미 뜬 부모와 중복이 될 수 있다).
|
|
158
|
+
*/
|
|
159
|
+
export function launchRemoteParent(target: ParentHostConfig, args: readonly string[], env: Record<string, string>, ssh: SshRunner = defaultSshRunner): { pid: number; log: string } {
|
|
160
|
+
const result = ssh(target.host, remoteLaunchScript(target, args, env), 30_000);
|
|
161
|
+
const log = remoteParentLogPath(target, env.ELANOUS_RUN_ID ?? 'unknown');
|
|
162
|
+
const pid = Number(/^pid=(\d+)$/m.exec(result.stdout)?.[1]);
|
|
163
|
+
if (result.status === 0 && Number.isSafeInteger(pid) && pid > 0) return { pid, log };
|
|
164
|
+
const refused = /^not-launched=(\S+)/m.exec(result.stdout)?.[1];
|
|
165
|
+
if (refused && (result.status === 3 || result.status === 4)) {
|
|
166
|
+
const tail = result.stdout.split('\n').filter((line) => line.trim() && !line.startsWith('not-launched=')).slice(-1)[0] ?? '';
|
|
167
|
+
throw new RemoteParentNotLaunchedError(target.host, `${refused}${tail ? `: ${tail.slice(0, 160)}` : ''}`);
|
|
168
|
+
}
|
|
169
|
+
throw new Error(`원격 부모 발사 결과 불확실(${target.host} · rc=${result.status}) — 중복을 피해 로컬로 다시 띄우지 않는다 · 원격에서 ELANOUS_RUN_ID=${env.ELANOUS_RUN_ID ?? '?'} 를 확인: ${(result.stderr.trim().split('\n').pop() || '').slice(0, 160)}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── 관측: 원격 런 원장 거울 ─────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
const RUN_ID_RE = /^run-[A-Za-z0-9_-]{4,64}$/;
|
|
175
|
+
|
|
176
|
+
/** HQ 쪽 거울 디렉터리 — `<prodRoot>/remote-parents/<host>/run-ledger`. */
|
|
177
|
+
export function remoteLedgerMirrorDir(prodRoot: string, host: string): string {
|
|
178
|
+
return join(prodRoot, 'remote-parents', host.replace(/[^A-Za-z0-9._-]/g, '_'), 'run-ledger');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 거울 디렉터리 전부(카드 원장 읽기가 연합 목록 뒤에 붙인다). */
|
|
182
|
+
export function remoteLedgerMirrorDirs(prodRoot: string): string[] {
|
|
183
|
+
const base = join(prodRoot, 'remote-parents');
|
|
184
|
+
if (!existsSync(base)) return [];
|
|
185
|
+
try {
|
|
186
|
+
return readdirSync(base).map((host) => join(base, host, 'run-ledger')).filter((dir) => existsSync(dir));
|
|
187
|
+
} catch { return []; }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 원격 런 원장(부모 ⊕ 그 원장이 가리키는 Pod 자식)을 HQ 거울로 당긴다 — 읽기 전용(원격에 쓰지 않는다).
|
|
192
|
+
* 돌려주는 값 = 당긴 런 id. 원격을 못 읽으면 던진다(호출부가 «없음»이 아니라 실패로 적는다).
|
|
193
|
+
*/
|
|
194
|
+
export function mirrorRemoteRunLedgers(target: Pick<ParentHostConfig, 'host' | 'configDir'>, runId: string, prodRoot: string, ssh: SshRunner = defaultSshRunner): string[] {
|
|
195
|
+
if (!RUN_ID_RE.test(runId)) throw new Error(`invalid runId: ${runId}`);
|
|
196
|
+
const dir = remoteLedgerMirrorDir(prodRoot, target.host);
|
|
197
|
+
mkdirSync(dir, { recursive: true });
|
|
198
|
+
const remoteDir = `${target.configDir ?? DEFAULT_REMOTE_CONFIG_DIR}/run-ledger`;
|
|
199
|
+
const pulled: string[] = [];
|
|
200
|
+
const pull = (id: string): string | null => {
|
|
201
|
+
const file = remotePath(`${remoteDir}/${id}.jsonl`);
|
|
202
|
+
// 부재만 ABSENT — 권한·읽기 오류는 cat 의 비0 종료로 올라와 던진다(«없음»으로 바꾸지 않는다).
|
|
203
|
+
const result = ssh(target.host, `if [ -e ${file} ]; then cat ${file}; else echo __ELANOUS_ABSENT__; fi`, 20_000);
|
|
204
|
+
if (result.status !== 0) throw new Error(`원격 원장 읽기 실패(${target.host} ${id}): ${(result.stderr.trim().split('\n').pop() || `rc=${result.status}`).slice(0, 200)}`);
|
|
205
|
+
const path = join(dir, `${id}.jsonl`);
|
|
206
|
+
if (result.stdout.trim() === '__ELANOUS_ABSENT__') {
|
|
207
|
+
// 원격에서 사라졌으면 낡은 거울도 걷는다 — 지난 사진을 지금 근거로 읽지 않게.
|
|
208
|
+
rmSync(path, { force: true });
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
212
|
+
writeFileSync(tmp, result.stdout);
|
|
213
|
+
renameSync(tmp, path);
|
|
214
|
+
pulled.push(id);
|
|
215
|
+
return result.stdout;
|
|
216
|
+
};
|
|
217
|
+
const host = pull(runId);
|
|
218
|
+
if (host) {
|
|
219
|
+
const children = new Set<string>();
|
|
220
|
+
for (const line of host.split('\n')) {
|
|
221
|
+
try {
|
|
222
|
+
const entry = JSON.parse(line) as { event?: string; data?: { childRunId?: unknown } };
|
|
223
|
+
if (entry.event === 'pod-child-run' && typeof entry.data?.childRunId === 'string' && RUN_ID_RE.test(entry.data.childRunId) && entry.data.childRunId !== runId) children.add(entry.data.childRunId);
|
|
224
|
+
} catch { /* 깨진 줄은 원장 읽기가 다룬다 */ }
|
|
225
|
+
}
|
|
226
|
+
for (const child of children) pull(child);
|
|
227
|
+
}
|
|
228
|
+
try { debug.log('task-agent', 'remote-ledger-mirrored', { host: target.host, runId, pulled }); } catch { /* fail-soft */ }
|
|
229
|
+
return pulled;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── 낡은 부모 보고(읽기 전용 · 죽이지 않는다) ─────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
export interface HarnessParentProcess { pid: number; ageHours: number; runId: string | null; version: string | null; seat: string | null; kind: 'say' | 'ask' | 'queue-child' }
|
|
235
|
+
|
|
236
|
+
/** macOS/Linux `etime` — `[[dd-]hh:]mm:ss`. */
|
|
237
|
+
export function parseEtime(value: string): number | null {
|
|
238
|
+
const match = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(value.trim());
|
|
239
|
+
if (!match) return null;
|
|
240
|
+
const [, d, h, m, s] = match;
|
|
241
|
+
return Number(d ?? 0) * 86400 + Number(h ?? 0) * 3600 + Number(m) * 60 + Number(s);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** `ps -axwwE -o pid=,etime=,command=` 출력 → 하니스 부모들. 런 id 는 env 꼬리의 «마지막» `ELANOUS_RUN_ID=`. */
|
|
245
|
+
export function parseHarnessParents(psOutput: string): HarnessParentProcess[] {
|
|
246
|
+
const rows: HarnessParentProcess[] = [];
|
|
247
|
+
for (const line of psOutput.split('\n')) {
|
|
248
|
+
const match = /^\s*(\d+)\s+(\S+)\s+(.*)$/.exec(line);
|
|
249
|
+
if (!match) continue;
|
|
250
|
+
const command = match[3]!;
|
|
251
|
+
const head = command.slice(0, 600);
|
|
252
|
+
const kind = /harness-queue-child\.ts/.test(head) ? 'queue-child' : /elanous\.mjs\b.*\bharness (say|ask)\b/.exec(head)?.[1] as 'say' | 'ask' | undefined;
|
|
253
|
+
if (!kind) continue;
|
|
254
|
+
const seconds = parseEtime(match[2]!);
|
|
255
|
+
if (seconds === null) continue;
|
|
256
|
+
const runIds = [...command.matchAll(/(?:^|\s)ELANOUS_RUN_ID=(run-[A-Za-z0-9_-]{4,64})(?=\s|$)/g)].map((m) => m[1]!);
|
|
257
|
+
rows.push({
|
|
258
|
+
pid: Number(match[1]),
|
|
259
|
+
ageHours: Math.round((seconds / 3600) * 10) / 10,
|
|
260
|
+
runId: runIds.at(-1) ?? null,
|
|
261
|
+
version: /\/versions\/([^/\s]+)\//.exec(head)?.[1] ?? null,
|
|
262
|
+
seat: /--seat (\S+)/.exec(head)?.[1] ?? null,
|
|
263
|
+
kind,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return rows;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface StaleParentRow extends HarnessParentProcess { reasons: string[]; runStatus: string | null }
|
|
270
|
+
|
|
271
|
+
export interface StaleParentDeps {
|
|
272
|
+
ps?: () => string;
|
|
273
|
+
/** 런 원장(없으면 null · 못 읽으면 던진다). */
|
|
274
|
+
loadLedger?: (runId: string) => Array<{ event: string; data?: Record<string, unknown> }> | null;
|
|
275
|
+
/** 살아 있는 Pod Job 의 `elanous.run`/`elanous.child-run` 라벨 — null 이면 «못 쟀다»(Pod 근거 없음). */
|
|
276
|
+
podRuns?: () => ReadonlySet<string> | null;
|
|
277
|
+
labelOf?: (runId: string) => string;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 나이 ≥ olderThanHours 인 부모 중 «원장이 끝났다»(run-status 줄) 또는 «Pod 이 없다»(Pod 목록을 쟀고 부모·자식 라벨이 없다)인 것.
|
|
282
|
+
* 읽기만 한다 — 죽이지 않는다. 런 id 를 모르는 부모는 `run-id-unknown` 으로 따로 적는다(낡음 판정이 아니다).
|
|
283
|
+
*/
|
|
284
|
+
export function staleHarnessParents(olderThanHours: number, deps: StaleParentDeps = {}): { rows: StaleParentRow[]; scanned: number; podRunsMeasured: boolean } {
|
|
285
|
+
const ps = deps.ps ?? (() => spawnSync('ps', ['-axwwE', '-o', 'pid=,etime=,command='], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).stdout ?? '');
|
|
286
|
+
const parents = parseHarnessParents(ps());
|
|
287
|
+
let podRuns: ReadonlySet<string> | null = null;
|
|
288
|
+
try { podRuns = deps.podRuns ? deps.podRuns() : null; } catch { podRuns = null; }
|
|
289
|
+
const label = deps.labelOf ?? ((id: string) => id);
|
|
290
|
+
const rows: StaleParentRow[] = [];
|
|
291
|
+
for (const parent of parents) {
|
|
292
|
+
if (parent.ageHours < olderThanHours) continue;
|
|
293
|
+
const reasons: string[] = [];
|
|
294
|
+
let runStatus: string | null = null;
|
|
295
|
+
if (!parent.runId) reasons.push('run-id-unknown');
|
|
296
|
+
else {
|
|
297
|
+
let ledger: ReturnType<NonNullable<StaleParentDeps['loadLedger']>> = null;
|
|
298
|
+
try { ledger = deps.loadLedger ? deps.loadLedger(parent.runId) : null; } catch (error) { reasons.push(`ledger-unreadable: ${(error instanceof Error ? error.message : String(error)).slice(0, 80)}`); }
|
|
299
|
+
const status = [...(ledger ?? [])].reverse().find((entry) => entry.event === 'run-status' && typeof entry.data?.runStatus === 'string');
|
|
300
|
+
if (status) { runStatus = String(status.data!.runStatus); reasons.push(`ledger-terminal(${runStatus})`); }
|
|
301
|
+
if (podRuns) {
|
|
302
|
+
const ids = [parent.runId, ...(ledger ?? []).filter((entry) => entry.event === 'pod-child-run' && typeof entry.data?.childRunId === 'string').map((entry) => entry.data!.childRunId as string)];
|
|
303
|
+
if (!ids.some((id) => podRuns!.has(label(id)))) reasons.push('pod-gone');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (reasons.length) rows.push({ ...parent, reasons, runStatus });
|
|
307
|
+
}
|
|
308
|
+
rows.sort((a, b) => b.ageHours - a.ageHours);
|
|
309
|
+
return { rows, scanned: parents.length, podRunsMeasured: podRuns !== null };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** HQ 의 elanous 판 — 이 패키지의 package.json(첫 낱말 = `--version` 첫 낱말과 같은 모양). */
|
|
313
|
+
export function localElanousVersion(): string {
|
|
314
|
+
try { return String((JSON.parse(readFileSync(resolve(import.meta.dir, '../../package.json'), 'utf8')) as { version?: string }).version ?? ''); } catch { return ''; }
|
|
315
|
+
}
|
|
@@ -77,6 +77,8 @@ export interface TaskCard {
|
|
|
77
77
|
launchId?: string;
|
|
78
78
|
/** Pod 런이면 PR 을 연 자식 런 id(런 묶기가 PR 과 함께 적는다). */
|
|
79
79
|
runChildId?: string;
|
|
80
|
+
/** HARNESS-PARENT-ON-MSB1 — 부모가 도는 원격 호스트(없으면 HQ 로컬). 원격 부모는 PR 까지만 간다(병합은 HQ). */
|
|
81
|
+
parentHost?: { host: string; configDir?: string; pid?: number; log?: string };
|
|
80
82
|
/** 미션 카드만 — 모든 조각 착지 → 칸 green «제안»(체크리스트는 손대지 않는다 · OP/주인이 뒤집는다). */
|
|
81
83
|
greenProposal?: { at: string; checklistId: string | null; evidence: Record<string, string> };
|
|
82
84
|
/** TA-JUDGE-LIVE-SAFE — live `review` 수가 리뷰를 요청한 PR 머리들(머리마다 한 번 · 띄우기 실패면 error · 이력은 지우지 않는다). */
|
|
@@ -193,7 +195,11 @@ export function shellQuote(arg: string): string {
|
|
|
193
195
|
/** 발사기에 넘기는 연결 — env(`ELANOUS_RUN_ID`)를 자식 환경에 얹으면 하니스 런이 `runId` 로 돈다(`resolveRunIdentity` 상속). launchId 는 카드에만 남는 발사 토큰이다. */
|
|
194
196
|
export interface TaskLaunchContext { runId: string; launchId: string; env: Record<string, string> }
|
|
195
197
|
/** 발사기가 «이 런 id 로 띄웠다»고 돌려주는 영수증 — 없으면 카드에 런 id 를 적지 않는다(추측하지 않는다). */
|
|
196
|
-
export interface TaskLaunchReceipt {
|
|
198
|
+
export interface TaskLaunchReceipt {
|
|
199
|
+
runId?: string;
|
|
200
|
+
/** HARNESS-PARENT-ON-MSB1 — 부모를 원격 호스트에서 띄웠다(ssh). 카드에 적어 `tasks show` 가 그 호스트의 원장을 당긴다. */
|
|
201
|
+
parentHost?: { host: string; configDir?: string; pid?: number; log?: string };
|
|
202
|
+
}
|
|
197
203
|
export type TaskLauncher = (args: string[], cwd?: string, context?: TaskLaunchContext) => void | TaskLaunchReceipt | Promise<void | TaskLaunchReceipt>;
|
|
198
204
|
|
|
199
205
|
export interface HandTaskOptions {
|
|
@@ -328,8 +334,9 @@ export async function handTask(opts: HandTaskOptions): Promise<HandTaskResult> {
|
|
|
328
334
|
const launched: TaskCard = {
|
|
329
335
|
...card, status: 'launched',
|
|
330
336
|
...(boundRunId ? { runId: boundRunId, launchId } : {}),
|
|
337
|
+
...(receipt && receipt.parentHost && typeof receipt.parentHost.host === 'string' ? { parentHost: receipt.parentHost } : {}),
|
|
331
338
|
history: [
|
|
332
|
-
{ at: launchedAt, event: 'launch', detail: move.command!.join(' ') },
|
|
339
|
+
{ at: launchedAt, event: 'launch', detail: `${receipt && receipt.parentHost ? `[parent@${receipt.parentHost.host} · PR 까지만] ` : ''}${move.command!.join(' ')}` },
|
|
333
340
|
...(boundRunId ? [{ at: launchedAt, event: 'run-bound', detail: boundRunId, runId: boundRunId }] : []),
|
|
334
341
|
],
|
|
335
342
|
};
|
|
@@ -13,6 +13,7 @@ import { getUserConfig } from '../../user-config.js';
|
|
|
13
13
|
import { debug } from '../../debug/log.js';
|
|
14
14
|
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
15
15
|
import { collectPodArtifacts } from './pod-artifact-return.js';
|
|
16
|
+
import { getDefaultLogStore, type LogStore } from '../../mss/logging/log-store.js';
|
|
16
17
|
import { podBunCacheVolume } from './pod-bun-cache.js';
|
|
17
18
|
import { podHostDirsInit, podInstallSlotsVolume } from './pod-install-slots.js';
|
|
18
19
|
import { type PodPoolMember, PodPoolScheduler, resolvePodPoolSpec, parsePodPool, checkPodPool, syncPoolImages, type PoolKubectl } from './pod-pool.js';
|
|
@@ -136,6 +137,19 @@ for i in $(seq 1 60); do
|
|
|
136
137
|
done
|
|
137
138
|
echo "[gate] ISOLATION NOT ENFORCED within 30s"; exit 1`;
|
|
138
139
|
|
|
140
|
+
/** Export the command's log store into the existing outbox return channel without changing the command exit status. */
|
|
141
|
+
const COMMAND_LOG_EXPORT = `if mkdir -p "$HOME/outbox/pod-logs" && elanous --test="$HOME/.elanous-test" logs --instance prod --since 12h --limit 20000 --json --json-data > "$HOME/outbox/pod-logs/logs.jsonl"; then
|
|
142
|
+
logs_size=$(( $(wc -c < "$HOME/outbox/pod-logs/logs.jsonl") ))
|
|
143
|
+
if [ "$logs_size" -gt 5242880 ]; then
|
|
144
|
+
tail -c 5242880 "$HOME/outbox/pod-logs/logs.jsonl" | tail -n +2 > "$HOME/outbox/pod-logs/logs.jsonl.tail" && mv -f "$HOME/outbox/pod-logs/logs.jsonl.tail" "$HOME/outbox/pod-logs/logs.jsonl"
|
|
145
|
+
printf 'ELANOUS_POD_LOGS_TRUNCATED %s %s\\n' "$logs_size" "$(( $(wc -c < "$HOME/outbox/pod-logs/logs.jsonl") ))"
|
|
146
|
+
fi
|
|
147
|
+
else
|
|
148
|
+
logs_rc=$?
|
|
149
|
+
rm -f "$HOME/outbox/pod-logs/logs.jsonl"
|
|
150
|
+
printf 'ELANOUS_POD_LOGS_UNAVAILABLE export-exit-%s\\n' "$logs_rc"
|
|
151
|
+
fi`;
|
|
152
|
+
|
|
139
153
|
/** 기존 Job 과 같은 `~/outbox` 조각 형식(`ELANOUS_POD_ARTIFACT`). 원장 회수는 하지 않는다. */
|
|
140
154
|
const ARTIFACT_EMIT = `set -o pipefail
|
|
141
155
|
artifact_bytes=0
|
|
@@ -172,6 +186,8 @@ export interface PodCommandJobInput {
|
|
|
172
186
|
repoUrl: string;
|
|
173
187
|
command: readonly string[];
|
|
174
188
|
skills: readonly string[];
|
|
189
|
+
/** pod run opt-in: return this command's local log store with its outbox artifacts. */
|
|
190
|
+
returnLogs?: boolean;
|
|
175
191
|
llm?: 'grok';
|
|
176
192
|
/** 저장소를 clone 한다 — 비공개 저장소라 GitHub 토큰이 Secret 으로 간다(명시 opt-in). 기본은 clone 없이 `~/work` 에서 이미지의 `elanous` 로 돈다. */
|
|
177
193
|
clone?: boolean;
|
|
@@ -222,6 +238,8 @@ export function podCommandJobManifest(o: PodCommandJobInput): Record<string, unk
|
|
|
222
238
|
`set -- ${quotedArgs}`,
|
|
223
239
|
'"$@"',
|
|
224
240
|
'rc=$?',
|
|
241
|
+
// A command may not end in LF; keep artifact markers on their own line for the existing parser.
|
|
242
|
+
...(o.returnLogs ? ["printf '\\n'", COMMAND_LOG_EXPORT] : []),
|
|
225
243
|
ARTIFACT_EMIT,
|
|
226
244
|
'exit $rc',
|
|
227
245
|
].join('\n');
|
|
@@ -325,6 +343,8 @@ export interface PodCommandResult {
|
|
|
325
343
|
|
|
326
344
|
export interface RunPodCommandOptions {
|
|
327
345
|
command: readonly string[];
|
|
346
|
+
/** pod run opt-in; other runPodCommand callers keep their existing Job script. */
|
|
347
|
+
returnLogs?: boolean;
|
|
328
348
|
pool?: string;
|
|
329
349
|
skills?: readonly string[];
|
|
330
350
|
llm?: 'grok';
|
|
@@ -357,6 +377,8 @@ export interface RunPodCommandOptions {
|
|
|
357
377
|
readSkillEnv?: (skills: readonly string[]) => Record<string, string>;
|
|
358
378
|
grokCredentials?: () => { grokAuth?: string; grokApiKey?: string; ghToken: string };
|
|
359
379
|
artifactsRoot?: string;
|
|
380
|
+
/** Test seam for the launching instance's log store. */
|
|
381
|
+
logStore?: LogStore | null;
|
|
360
382
|
sleep?: (ms: number) => Promise<void>;
|
|
361
383
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
362
384
|
env?: NodeJS.ProcessEnv;
|
|
@@ -454,7 +476,8 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
454
476
|
const context = member?.context ?? (currentContext?.status === 0 ? currentContext.stdout.trim() : '');
|
|
455
477
|
const kubectl: Kubectl = (args, stdin) => baseKubectl(context ? ['--context', context, ...args] : [...args], stdin);
|
|
456
478
|
const cleanupSecret = () => { kubectl(['-n', namespace, 'delete', 'secret', '-l', own, '--ignore-not-found']); };
|
|
457
|
-
const
|
|
479
|
+
const artifactRoot = options.artifactsRoot ?? `${effectiveInstanceRoot()}/pod-artifacts`;
|
|
480
|
+
const artifactsDir = `${artifactRoot}/${name}`;
|
|
458
481
|
try {
|
|
459
482
|
if (!context && !options.kubectl) throw new Error('pod command: kubectl context 를 확인할 수 없다');
|
|
460
483
|
let grokAuth: string | undefined;
|
|
@@ -499,7 +522,7 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
499
522
|
const manifest = podCommandJobManifest({
|
|
500
523
|
name, namespace, launch, image: imageRef ?? image,
|
|
501
524
|
...(imageRef ? { imagePullPolicy: 'IfNotPresent' as const } : {}),
|
|
502
|
-
repoUrl, command, skills, ...(llm ? { llm } : {}), ...(options.clone ? { clone: true } : {}),
|
|
525
|
+
repoUrl, command, skills, ...(options.returnLogs ? { returnLogs: true } : {}), ...(llm ? { llm } : {}), ...(options.clone ? { clone: true } : {}),
|
|
503
526
|
...(options.source ? { source: options.source } : {}), ...(hostMirror ? { hostMirror } : {}), ...(bunCache ? { bunCache } : {}), ...(installSlots ? { installSlots } : {}), ...(memoryLimit ? { memoryLimit } : {}), ...(options.cpu ? { cpu: options.cpu } : {}), deadlineSeconds,
|
|
504
527
|
...(options.runId ? { runId: options.runId } : {}),
|
|
505
528
|
});
|
|
@@ -519,19 +542,31 @@ export async function runPodCommand(options: RunPodCommandOptions): Promise<PodC
|
|
|
519
542
|
const logsResult = kubectl(['-n', namespace, 'logs', `job/${name}`, '-c', 'child']);
|
|
520
543
|
const logsFailed = logsResult.status !== 0;
|
|
521
544
|
const logs = logsFailed ? '' : logsResult.stdout;
|
|
522
|
-
const artifactDir =
|
|
545
|
+
const artifactDir = artifactRoot;
|
|
523
546
|
let artifacts: PodArtifactCollection;
|
|
524
547
|
if (logsFailed) {
|
|
525
548
|
const reason = logsResult.stderr.trim() || `kubectl logs exit ${logsResult.status}`;
|
|
526
549
|
artifacts = { files: null, names: [], error: reason };
|
|
527
550
|
log('pod.command', 'artifact-collect-failed', { job: name, reason });
|
|
528
551
|
} else {
|
|
552
|
+
const store = options.returnLogs ? (options.logStore === undefined ? getDefaultLogStore() : options.logStore) : null;
|
|
529
553
|
collectPodArtifacts(logs, {
|
|
530
|
-
dir: artifactDir,
|
|
531
|
-
|
|
532
|
-
log: (c, e, d) => log(c, e, d),
|
|
554
|
+
dir: artifactDir, job: name, log: (c, e, d) => log(c, e, d),
|
|
555
|
+
...(options.returnLogs ? { logStore: store } : {}),
|
|
533
556
|
});
|
|
534
557
|
const names = artifactNames(logs);
|
|
558
|
+
// A plain command need not write JSONL. Make its child output queryable without changing the store schema.
|
|
559
|
+
if (options.returnLogs && store) {
|
|
560
|
+
try {
|
|
561
|
+
const output = logs.split(/\r?\n/).filter((line) => !line.startsWith('ELANOUS_POD_ARTIFACT ')).join('\n').trim();
|
|
562
|
+
if (output) store.insertPodBatch(name, [{ line: -1, surface: 'pod', rec: {
|
|
563
|
+
ts: new Date().toISOString(), category: 'pod.command', event: 'child-output', level: 'info',
|
|
564
|
+
data: { job: name, output: tailBytes(output, options.childLogMaxBytes ?? podChildLogMaxBytes()), origin: 'pod', podJob: name },
|
|
565
|
+
} }]);
|
|
566
|
+
} catch (error) {
|
|
567
|
+
log('pod.command', 'child-output-import-failed', { job: name, reason: error instanceof Error ? error.message : String(error) });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
535
570
|
artifacts = { files: names.length, names, error: null };
|
|
536
571
|
}
|
|
537
572
|
// 표지가 없어도 알리는 경로는 실제로 존재해야 한다. 자식 로그 전문은 child.log (상한 넘으면 끝부분).
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// GATE-IMAGE-DEPS (0.2.21 · GATE-SPEED) — the release-gate shards' dependencies come baked in the harness image.
|
|
2
|
+
//
|
|
3
|
+
// Measured on the 0.2.20 cut gate (~/.elanous/release/0.2.20/gate-logs/cut · 24 root shards · pod-<n>.log ⊕ .junit.xml):
|
|
4
|
+
// install-slot wait median 156 s (max 509 s) ⊕ `bun install` root+pwa median 104 s (max 160 s) = median ≈ 260 s of a
|
|
5
|
+
// ≈ 1200 s shard spent before the first test — and the shards that waited longest were the ones the 1200 s deadline cut
|
|
6
|
+
// (pod-1 · 3 · 11 · 21 · 22 waited 327–509 s and ran only 215–436 s of tests). Alone the install takes seconds; 24 at
|
|
7
|
+
// once thrash the node (GATE-INSTALL-CACHE). Not installing at all removes both the wait and the install.
|
|
8
|
+
//
|
|
9
|
+
// How: `docker/harness/build.sh` stages each install tree's lockfile ⊕ the install-relevant projection of its package.json
|
|
10
|
+
// ({@link GATE_DEPS_PROJECTION_JS}) into `gate-deps/`; the Dockerfile installs them once into {@link GATE_DEPS_DIR}
|
|
11
|
+
// (`--frozen-lockfile`). The layer's inputs change only when dependencies change, so the per-commit rebuild reuses it from
|
|
12
|
+
// the build cache. A shard links `node_modules` to the baked tree only when its own lockfile and projection are
|
|
13
|
+
// byte-identical to the baked ones; anything else — no baked tree (old image), a different lockfile, a different
|
|
14
|
+
// projection, an existing node_modules, a failed link — falls back to the old path (install slot ⊕ `bun install`).
|
|
15
|
+
// The link is a symlink: copying (or hard-linking across overlay layers) is the very I/O the install slots were throttling.
|
|
16
|
+
// Module resolution follows the real path, and the baked layout mirrors the repo (`<dir>/node_modules`,
|
|
17
|
+
// `<dir>/apps/pwa/node_modules`), so the parent lookup from a package inside the pwa tree is the same as before.
|
|
18
|
+
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
|
|
23
|
+
/** Baked dependency root inside the full harness image (mirrors the repo layout). */
|
|
24
|
+
export const GATE_DEPS_DIR = '/opt/elanous-gate-deps';
|
|
25
|
+
/** The install trees, relative to the repo root, in install order (same as the old `bun install && (cd apps/pwa && bun install)`). */
|
|
26
|
+
export const GATE_DEPS_TREES = ['.', 'apps/pwa'] as const;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The part of a package.json that decides what `bun install` produces: dependency maps, overrides, trusted/patched
|
|
30
|
+
* dependencies and the install lifecycle scripts. `version`, `description` and every other script are dropped, so a
|
|
31
|
+
* release bump or a new script does not invalidate the baked layer. Run as `bun -e <this> <package.json>` (stdout).
|
|
32
|
+
* ⛔ build.sh (bake) and the shard (check) run this same text — changing it simply makes the next shards fall back once.
|
|
33
|
+
*/
|
|
34
|
+
export const GATE_DEPS_PROJECTION_JS = [
|
|
35
|
+
"const p=JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'));const o={};",
|
|
36
|
+
"for(const k of ['name','dependencies','devDependencies','optionalDependencies','peerDependencies','trustedDependencies','overrides','resolutions','patchedDependencies','workspaces'])if(p[k]!==undefined)o[k]=p[k];",
|
|
37
|
+
"const s={};for(const k of ['preinstall','install','postinstall','prepare'])if(p.scripts&&p.scripts[k]!==undefined)s[k]=p.scripts[k];",
|
|
38
|
+
"if(Object.keys(s).length)o.scripts=s;process.stdout.write(JSON.stringify(o,null,2)+'\\n');",
|
|
39
|
+
].join('');
|
|
40
|
+
|
|
41
|
+
/** Same projection in-process (tests · build staging). */
|
|
42
|
+
export function projectDepsManifest(manifest: Record<string, unknown>): Record<string, unknown> {
|
|
43
|
+
const out: Record<string, unknown> = {};
|
|
44
|
+
for (const key of ['name', 'dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies', 'trustedDependencies', 'overrides', 'resolutions', 'patchedDependencies', 'workspaces']) {
|
|
45
|
+
if (manifest[key] !== undefined) out[key] = manifest[key];
|
|
46
|
+
}
|
|
47
|
+
const scripts = manifest.scripts as Record<string, unknown> | undefined;
|
|
48
|
+
const lifecycle: Record<string, unknown> = {};
|
|
49
|
+
for (const key of ['preinstall', 'install', 'postinstall', 'prepare']) if (scripts && scripts[key] !== undefined) lifecycle[key] = scripts[key];
|
|
50
|
+
if (Object.keys(lifecycle).length) out.scripts = lifecycle;
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* build.sh staging: `<dest>/<tree>/bun.lock` (verbatim) ⊕ `<dest>/<tree>/package.json` (the projection) for every install
|
|
56
|
+
* tree. The digest (sha256 of the staged bytes, 12 hex) becomes the image label `elanous.gate-deps`.
|
|
57
|
+
*/
|
|
58
|
+
export function stageGateDeps(root: string, dest: string): { digest: string; trees: string[] } {
|
|
59
|
+
const hash = createHash('sha256');
|
|
60
|
+
const trees: string[] = [];
|
|
61
|
+
mkdirSync(dest, { recursive: true });
|
|
62
|
+
for (const tree of GATE_DEPS_TREES) {
|
|
63
|
+
const lock = join(root, tree, 'bun.lock');
|
|
64
|
+
const manifest = join(root, tree, 'package.json');
|
|
65
|
+
if (!existsSync(lock) || !existsSync(manifest)) continue;
|
|
66
|
+
const out = join(dest, tree);
|
|
67
|
+
mkdirSync(out, { recursive: true });
|
|
68
|
+
const lockBytes = readFileSync(lock);
|
|
69
|
+
const projected = `${JSON.stringify(projectDepsManifest(JSON.parse(readFileSync(manifest, 'utf8')) as Record<string, unknown>), null, 2)}\n`;
|
|
70
|
+
writeFileSync(join(out, 'bun.lock'), lockBytes, { mode: 0o644 });
|
|
71
|
+
writeFileSync(join(out, 'package.json'), projected, { mode: 0o644 });
|
|
72
|
+
hash.update(`${tree}\0`).update(lockBytes).update('\0').update(projected).update('\0');
|
|
73
|
+
trees.push(tree);
|
|
74
|
+
}
|
|
75
|
+
return { digest: hash.digest('hex').slice(0, 12), trees };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const sq = (value: string) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Bash that installs the shard's dependencies (cwd = the repo clone) and sets `irc` (0 = ready). Each tree is linked to
|
|
82
|
+
* the baked tree when it matches, else installed — only the installs take an install slot (`install_slot_acquire` /
|
|
83
|
+
* `install_slot_release` from `installSlotScript` must already be defined when `installSlots > 0`). Every decision is a
|
|
84
|
+
* `[gate] deps <tree> baked|miss (<why>)` line in `log` (the shard's install.log).
|
|
85
|
+
*/
|
|
86
|
+
export function gateDepsInstallScript(o: { log: string; installSlots: number; depsDir?: string }): string {
|
|
87
|
+
const deps = sq(o.depsDir ?? GATE_DEPS_DIR);
|
|
88
|
+
const log = o.log;
|
|
89
|
+
return [
|
|
90
|
+
'gate_deps_link() {',
|
|
91
|
+
` local t="$1" d=${deps} want`,
|
|
92
|
+
' [ "$t" = . ] || d="$d/$t"',
|
|
93
|
+
' if [ ! -d "$d/node_modules" ]; then echo "[gate] deps $t miss (no baked deps)"; return 1; fi',
|
|
94
|
+
' if [ -e "$t/node_modules" ] || [ -L "$t/node_modules" ]; then echo "[gate] deps $t miss (node_modules exists)"; return 1; fi',
|
|
95
|
+
' if ! cmp -s "$t/bun.lock" "$d/bun.lock"; then echo "[gate] deps $t miss (bun.lock differs)"; return 1; fi',
|
|
96
|
+
` want=$(bun -e ${sq(GATE_DEPS_PROJECTION_JS)} "$t/package.json" 2>/dev/null) || { echo "[gate] deps $t miss (package.json unreadable)"; return 1; }`,
|
|
97
|
+
' if [ "$want" != "$(cat "$d/package.json" 2>/dev/null)" ]; then echo "[gate] deps $t miss (package.json differs)"; return 1; fi',
|
|
98
|
+
' if ! ln -s "$d/node_modules" "$t/node_modules"; then echo "[gate] deps $t miss (link failed)"; return 1; fi',
|
|
99
|
+
' echo "[gate] deps $t baked"',
|
|
100
|
+
'}',
|
|
101
|
+
'irc=0; gate_deps_need=',
|
|
102
|
+
`for t in ${GATE_DEPS_TREES.join(' ')}; do gate_deps_link "$t" >> ${log} 2>&1 || gate_deps_need="$gate_deps_need $t"; done`,
|
|
103
|
+
'if [ -n "$gate_deps_need" ]; then',
|
|
104
|
+
...(o.installSlots > 0 ? [` install_slot_acquire >> ${log} 2>&1`] : []),
|
|
105
|
+
` for t in $gate_deps_need; do (cd "$t" && bun install) >> ${log} 2>&1; irc=$?; [ "$irc" -eq 0 ] || break; done`,
|
|
106
|
+
...(o.installSlots > 0 ? [' install_slot_release'] : []),
|
|
107
|
+
'fi',
|
|
108
|
+
].join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Per-tree decisions from a shard's install log (`baked` · `miss:<why>`), for the gate's `pod-shard` observation. */
|
|
112
|
+
export function parseGateDeps(log: string): Record<string, string> {
|
|
113
|
+
const out: Record<string, string> = {};
|
|
114
|
+
for (const match of log.matchAll(/^\[gate\] deps (\S+) (baked|miss \(([^)]*)\))$/gm)) out[match[1]!] = match[2] === 'baked' ? 'baked' : `miss:${match[3]}`;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (import.meta.main) {
|
|
119
|
+
const [cmd, dest] = process.argv.slice(2);
|
|
120
|
+
if (cmd !== 'stage' || !dest) { console.error('usage: pod-gate-deps.ts stage <dir>'); process.exit(2); }
|
|
121
|
+
const staged = stageGateDeps(process.cwd(), dest);
|
|
122
|
+
console.log(`digest=${staged.digest} trees=${staged.trees.join(',') || '-'}`);
|
|
123
|
+
}
|