elanous 0.2.19 → 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/{4323.c5dfb6f503304775.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-c97eba9fb72a9a5e.js +1 -0
- 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/1826-5d279da64bbbc2de.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/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1993.3fd074a2a246234e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2977-e38442e86289f1d3.js → 211-9c9090c59e2c5907.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2140.d2daf9ee96e295d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.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/2489-ff55a35b202c0f94.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -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/2699-325cc923ba7b8631.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/311.e685738f92e6e2ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.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/3558-bbb5ab553a0471d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3721.ba5b82db0d9a5572.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-995ea9f5ef33d71e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.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/4144-fad095b48a1862ee.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/4565-3d7e3fdeb43686a8.js +1 -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/4795-f372490b178583fc.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/{4847.56f5bc454fbcc827.js → 6110.1297d4093fdfbb0d.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/6158.566c890adc06a459.js +2 -0
- 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/6877-74399f69c7da1433.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/{1470.98a6ab61ccb4131c.js → 726.83532d69452ee24b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 7327-2d69fb321ba73389.js} +1 -1
- 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/7797-df5aa0381fdc078f.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/8060-cc008cc6fedfa818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.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/8401-35e94e9091f5eb8d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -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/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-60fbef33e66f0a46.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-7581825d511a64c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7647.2fcd30d894d5ee30.js → 935.4ddaa99fc1d1126c.js} +1 -1
- 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/_not-found/page-c1a96669158471ec.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-d781be3e3a9cfd74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-b0a80997903852d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-2b1263165630d9c8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-ea998f7f5d156bea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-33c328b64ca508f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-e2d6bed48ac20120.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-111cabe4d1fd69ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-544dd4bd4847eeb7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-3ee191319bf97d87.js +1 -0
- 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-a449f1ed472b81c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-9045094d6d3b5cd0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
- 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-b70cd005d9169e25.js → page-be8b4615c919e36c.js} +1 -1
- 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-7a12f61be4a579d1.js +1 -0
- 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-8cc707bd09a497d9.js +1 -0
- 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-dc39ee7a36d26385.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-84ae759f76053672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-3db58ddb61fe2be9.js +1 -0
- 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-2fe368efb6de3b6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-8d04132dd0ac5e10.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-f23484de467c9ea3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-c69762987b751174.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5456692bda39aafb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d75f7cd65845ce98.js +1 -0
- 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-a6eb73b8a4840f1c.js +1 -0
- 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-6c0b88068c143f15.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-7f8d7fcdf058a845.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-22163556cd38bc88.js +1 -0
- 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/{8046cadf-211ca3c0e00f2b72.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{2ea96958.e8c7e112de50b067.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/f8626309.a4e5d362b1d55e59.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.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 +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +23 -23
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +24 -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 +23 -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/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 +22 -22
- 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/demo/editor-recipes.yaml +22 -0
- package/graphs/drafts/recipes.yaml +4 -4
- package/graphs/implement-loop.yaml +277 -5
- package/graphs/retro/daily.yaml +25 -0
- package/graphs/retro/recipes.yaml +24 -0
- package/package.json +1 -1
- package/scripts/ask-marker-check.ts +3 -1
- package/scripts/coord-channel-watch.sh +31 -4
- package/scripts/docs-lint.ts +4 -1
- package/scripts/land-global-checks.ts +89 -0
- package/scripts/lib/pch-regress-map.ts +2 -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 +296 -25
- package/scripts/release-loop/gate-timing.ts +116 -7
- package/scripts/release-loop/preflight.ts +6 -4
- package/scripts/release-loop/publish-window.ts +141 -0
- package/scripts/release-loop/release-docs.ts +1 -4
- package/scripts/release-loop/release-universe.ts +38 -0
- package/scripts/release-loop/resume-release.ts +4 -1
- package/scripts/release-loop/shard-plan.ts +50 -5
- package/scripts/release-loop/unattended-release.ts +10 -2
- package/scripts/release-story/draft.ts +5 -1
- package/scripts/retro/shadow-compare.ts +71 -0
- package/scripts/rhythm/daily.ts +8 -2
- package/src/agent-mission/driver.ts +34 -6
- package/src/autopilot/build/llm-conflict-merge.ts +132 -7
- package/src/autopilot/build/merge-intent.ts +135 -7
- package/src/away/release-watch.ts +20 -3
- package/src/browser-cdp/client.ts +10 -5
- package/src/cli/config-drift.ts +513 -0
- package/src/cli/log-event-names.ts +4 -0
- 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/pr-cli.ts +20 -0
- package/src/cli/release-cli.ts +20 -5
- package/src/cli/retro-cli.ts +78 -0
- package/src/cli/tasks-cli.ts +200 -8
- package/src/dashboard/index.ts +40 -4
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +5 -4
- package/src/dashboard/slash-runtime/help-from-registry.ts +57 -31
- package/src/domains/outbound-log-probe.ts +6 -1
- 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 +218 -0
- package/src/graph-runner/runner.ts +11 -1
- 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 +482 -59
- package/src/harness/harness-queue.ts +315 -25
- package/src/harness/harness-salvage-cli.ts +62 -0
- package/src/hq/fence-outcomes.ts +63 -0
- package/src/hq/fence-wrapper.ts +33 -0
- package/src/hq/hq.ts +11 -1
- package/src/index.ts +76 -10
- package/src/intake-plane/digest.ts +18 -11
- package/src/maturity/feature-maturity.ts +4 -3
- package/src/nexus/api/draft-metrics.ts +114 -0
- package/src/nexus/api/graph-kinds.ts +10 -4
- package/src/nexus/api/graph-wizard-api.ts +53 -0
- package/src/nexus/api/graphs-api.ts +220 -10
- package/src/nexus/api/harness-traversal.ts +205 -0
- package/src/nexus/api/http-server.ts +69 -3
- package/src/nexus/api/loop-edges.ts +4 -2
- package/src/nexus/api/ops-api.ts +22 -7
- package/src/nexus/api/ops-board-route.ts +48 -0
- package/src/nexus/outbound/router.ts +28 -8
- package/src/onboarding/entry-hints.ts +5 -0
- package/src/onboarding/first-run.ts +3 -0
- package/src/onboarding.ts +24 -6
- 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/feature-store.ts +14 -11
- package/src/release-loop/gate-shards.ts +163 -0
- package/src/release-loop/release-schedule.ts +6 -2
- package/src/retro/daily.ts +372 -0
- package/src/seat-loop/op-decide.ts +173 -0
- package/src/seat-loop/seat-loop.ts +60 -3
- package/src/seat-loop/seat-report.ts +58 -3
- package/src/self-dev/ask-launch-flow.ts +9 -6
- package/src/self-dev/ask-launch-io.ts +52 -2
- package/src/self-dev/ask-path-candidates.ts +40 -2
- package/src/self-dev/draft-sweep.ts +371 -16
- package/src/self-dev/draft-triage-rules.ts +19 -4
- 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/pr-preview.ts +84 -16
- package/src/self-dev/release-path-guard.ts +7 -4
- package/src/self-implement/blocked-draft-policy.ts +101 -0
- package/src/self-implement/execution-clarification.ts +11 -0
- package/src/self-implement/frozen-merges.ts +23 -0
- package/src/self-implement/gate-env-retry.ts +83 -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 +234 -6
- package/src/self-implement/run-stop.ts +123 -0
- package/src/self-implement/run-ttl.ts +258 -0
- package/src/self-implement/salvage-retention.ts +123 -0
- package/src/self-implement/seams.ts +7 -0
- package/src/self-implement/self-implement-cli.ts +2 -0
- package/src/self-implement/self-implement-runtime.ts +12 -0
- package/src/self-implement/sibling-resync.ts +223 -0
- package/src/self-implement/stop-autoheal.ts +204 -0
- package/src/session-runtime/index.ts +5 -3
- package/src/shell-env-bootstrap.ts +17 -3
- package/src/task-agent/board.ts +1 -1
- package/src/task-agent/card-evidence.ts +105 -10
- package/src/task-agent/judge.ts +7 -0
- package/src/task-agent/live-moves.ts +120 -20
- package/src/task-agent/mission.ts +1 -1
- package/src/task-agent/parent-host.ts +315 -0
- package/src/task-agent/shadow.ts +10 -1
- package/src/task-agent/task-hand.ts +16 -4
- package/src/task-orchestrator/surfaces/pod-command-job.ts +60 -11
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +4 -0
- package/src/task-orchestrator/surfaces/pod-gate-deps.ts +123 -0
- package/src/task-orchestrator/surfaces/pod-install-slots.ts +89 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +93 -10
- package/src/task-orchestrator/surfaces/pod-pool.ts +147 -34
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +58 -9
- package/src/telegram-commands.ts +41 -0
- package/src/testing/real-cli-spawn.ts +106 -0
- package/src/tool-runtime/browser-runtime.ts +87 -3
- package/src/tool-runtime/index.ts +1 -0
- package/src/user-config.ts +76 -7
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/J5H7BvqgSI8DUjtRRcA_S/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1157-cba0e8553d35a980.js +0 -1
- package/apps/pwa/out/_next/static/chunks/171-23a877ed11720de9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2336-e830c777eb6bce04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-e061bb24cd2b5cde.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3094.67339318c7f72a3a.js +0 -2
- package/apps/pwa/out/_next/static/chunks/338-ceb956612dcfaa86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3395-835e4ceaa6335ba4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3441-be82e3595aef3a20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3689-89905039bf3e7257.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3822-6f268bc26c6a84ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4457-d7df81c90f9d0268.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4551-77dadae76f6cbace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4978.6fd93b974441e87f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5012-f746f6549b869f91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
- package/apps/pwa/out/_next/static/chunks/62-5bfed1e23790b745.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-9bc8561fde416e07.js +0 -1
- package/apps/pwa/out/_next/static/chunks/649-ca96d1c5d09e02b8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6726-85a7db59b288c53f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6735-33267c4630d0d963.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7633-141682015873beb6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-143791231c99bd02.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8430-a10cfba8ef282d4e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8870-9614a213b990b10b.js +0 -25
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-12c2f30dea62291c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/976-4a9b4ebdca7c739a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9989-15cdf568bafa9fc8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/ae787e4c-e5b1ef00b2dbceca.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-deef5ee191830f14.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-b193da485f9a14b2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b6e1a79df0e214f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-e84df4d907f703c7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a5fb9a5db89ae4ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1499f481d7fc52d6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-c7cf753aeb648790.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4118469e514e10fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-e968f7630654332a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-505915052f947022.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c2f962fb4a3b06c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7b5ed0bd8a589ec4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-cf8193250dd42dd4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-cf3f5dfa35dea8e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-5b29d9b06ceadade.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-a15e7fcc283e9ca7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d2915e7b70ff3d0e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-2595f0eba29f57e1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-c776322454156e36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-77bbd17f4435aa10.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-049689e611ecc936.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-85954a4fb09c44eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e773e552e1068ee6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-5633c4bfa7684b4a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-d829b92bd01b4994.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-061c317fc771becf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a45ece8865ed4b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-0be990618c0d56bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-bfea4865dc244b73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-0d6ba8e337741484.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-76a88b55d816482e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-bba1542df59f61ce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4185c95689bb372e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b699e1a8d5d34b8b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-c225e338c31bf274.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-449353f70507ca1f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-90b889714102476d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-f1ee7fbeb7f96ddc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-d59a95e1434dfaa0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-71491a23414d1a47.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-de7112bdb65fa4fe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-feb4d0d88079660d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f17d887b40dd6b1d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-8da77be2192bcd5d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-72c3b9a5e9582b29.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3afe4c8d0c48c39.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-b9308f2d8a931531.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-6f35f9d892fa630b.js +0 -1
- package/apps/pwa/out/_next/static/css/85f53c6f35d11382.css +0 -1
- /package/apps/pwa/out/_next/static/{J5H7BvqgSI8DUjtRRcA_S → MObU8ujkMciAKr6ljAV05}/_ssgManifest.js +0 -0
- /package/skills/project-onboarding/{skill.md → SKILL.md} +0 -0
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
// 입력 = 게이트 로그 디렉토리(`<ledger>/release/<version>/gate-logs/cut`)의 `pod-*.json`.
|
|
3
3
|
// 첫 판 샤드 = `pod-<n>.json` · 재시도 = 그 밖의 모든 `pod-*.json`(`-c<k>`·`-c<k>-file-<m>`·`-retry` …).
|
|
4
4
|
// ⛔ 측정이 없으면 `measured:false` 로 표시한다 — 0 을 «잰 값»처럼 내지 않는다.
|
|
5
|
-
|
|
5
|
+
// ⭐ 기본 = 전 우주(LOGS-DEFAULT-ALL · 0.2.20) — 게이트는 실행 트리의 시험 우주(예: ⟨test:wt-release⟩
|
|
6
|
+
// `<tree>/.elanous-test/release/<v>/gate-logs/cut`)에 쌓인다. 운영 원장만 보면 0.2.19 판처럼
|
|
7
|
+
// «측정 없음»으로 오판한다. 그래서 버전 인자는 운영 원장 ⊕ 등록된 모든 우주(시험·파생 시험 포함)를
|
|
8
|
+
// 본다 — 우주 열거는 `eln logs --all --include-test` 와 같은 레지스트리(`readLogInstanceScope`)다.
|
|
9
|
+
// 좁히기는 opt-in `--prod-only`(종전 동작: `releaseLedgerRoot()` 하나).
|
|
10
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
6
11
|
import { join } from 'node:path';
|
|
7
12
|
import { releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
13
|
+
import { readLogInstanceScope } from '../../src/mss/logging/instance-registry.js';
|
|
8
14
|
|
|
9
15
|
export interface ShardTiming { name: string; min: number; files: number; plannedMin: number | null }
|
|
10
16
|
|
|
@@ -82,12 +88,94 @@ export function gateTiming(dir: string): GateTimingReport {
|
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
/** 인자가 버전이 아니라 디렉토리인가 — 경로처럼 보이거나 실제로 있으면. */
|
|
92
|
+
export function isGateLogDirArg(arg: string): boolean {
|
|
93
|
+
return arg.includes('/') || arg.startsWith('.') || existsSync(arg);
|
|
94
|
+
}
|
|
95
|
+
|
|
85
96
|
/** `<version>` 이면 원장 루트(`releaseLedgerRoot()`)의 `release/<version>/gate-logs/cut`, 경로처럼 보이면 그대로. */
|
|
86
97
|
export function resolveGateLogDir(arg: string, ledgerRoot: string = releaseLedgerRoot()): string {
|
|
87
|
-
if (
|
|
98
|
+
if (isGateLogDirArg(arg)) return arg;
|
|
88
99
|
return join(ledgerRoot, 'release', arg, 'gate-logs', 'cut');
|
|
89
100
|
}
|
|
90
101
|
|
|
102
|
+
export interface GateLogCandidate { universe: string; dir: string }
|
|
103
|
+
|
|
104
|
+
export interface GateLogScope {
|
|
105
|
+
/** 본 후보 원장 루트 수(물리 경로 dedup 뒤). */
|
|
106
|
+
roots: number;
|
|
107
|
+
/** 우주 레지스트리 — `read` 읽음 · `unreadable` 못 읽음(«등록 우주 0»이 아니라 «못 셈» — 운영 원장만 봤다)
|
|
108
|
+
* · `skipped` `--prod-only` 라 안 읽음. */
|
|
109
|
+
registry: 'read' | 'unreadable' | 'skipped';
|
|
110
|
+
/** `--prod-only` 로 좁혔나. */
|
|
111
|
+
prodOnly: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface GateLogScopeDeps {
|
|
115
|
+
ledgerRoot?: string;
|
|
116
|
+
readInstances?: () => { instances: ReadonlyArray<{ name: string; stateDir: string }>; queryStatus: { registeredStores: boolean } };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function physical(path: string): string {
|
|
120
|
+
try { return realpathSync(path); } catch { return path; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 버전의 게이트 로그 후보 — 운영 원장 루트(`releaseLedgerRoot()`) ⊕ 등록된 모든 우주.
|
|
124
|
+
* 물리 경로로 dedup 한다(`~/.monad` → `~/.elanous` 심링크가 같은 판을 두 번 세지 않게). */
|
|
125
|
+
export function gateLogCandidates(
|
|
126
|
+
version: string,
|
|
127
|
+
opts: { prodOnly?: boolean } = {},
|
|
128
|
+
deps: GateLogScopeDeps = {},
|
|
129
|
+
): { candidates: GateLogCandidate[]; scope: GateLogScope } {
|
|
130
|
+
const primary = deps.ledgerRoot ?? releaseLedgerRoot();
|
|
131
|
+
const roots: Array<{ universe: string; root: string }> = [{ universe: 'prod', root: primary }];
|
|
132
|
+
let registry: GateLogScope['registry'] = 'skipped';
|
|
133
|
+
if (!opts.prodOnly) {
|
|
134
|
+
const scope = (deps.readInstances ?? readLogInstanceScope)();
|
|
135
|
+
registry = scope.queryStatus.registeredStores ? 'read' : 'unreadable';
|
|
136
|
+
// 못 읽은 판의 부분 목록은 쓰지 않는다 — «운영만 봤다»는 표시와 실제 본 범위를 일치시킨다.
|
|
137
|
+
if (registry === 'read') for (const instance of scope.instances) roots.push({ universe: instance.name, root: instance.stateDir });
|
|
138
|
+
}
|
|
139
|
+
const seen = new Set<string>();
|
|
140
|
+
const candidates: GateLogCandidate[] = [];
|
|
141
|
+
for (const { universe, root } of roots) {
|
|
142
|
+
const key = physical(root);
|
|
143
|
+
if (seen.has(key)) continue;
|
|
144
|
+
seen.add(key);
|
|
145
|
+
candidates.push({ universe, dir: resolveGateLogDir(version, root) });
|
|
146
|
+
}
|
|
147
|
+
return { candidates, scope: { roots: candidates.length, registry, prodOnly: opts.prodOnly === true } };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface GateTimingScan {
|
|
151
|
+
version: string;
|
|
152
|
+
scope: GateLogScope & { found: number };
|
|
153
|
+
/** pod-*.json 디렉토리가 «있는» 우주만 — 판마다 따로 낸다(서로 다른 판의 샤드를 섞지 않는다). */
|
|
154
|
+
reports: Array<GateTimingReport & { universe: string }>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** 버전 인자의 기본 동선 — 후보 우주 전부에서 게이트 로그 디렉토리가 있는 것만 보고한다. */
|
|
158
|
+
export function scanGateTiming(version: string, opts: { prodOnly?: boolean } = {}, deps: GateLogScopeDeps = {}): GateTimingScan {
|
|
159
|
+
const { candidates, scope } = gateLogCandidates(version, opts, deps);
|
|
160
|
+
const reports = candidates
|
|
161
|
+
.filter(({ dir }) => existsSync(dir) && statSync(dir).isDirectory())
|
|
162
|
+
.map(({ universe, dir }) => ({ universe, ...gateTiming(dir) }));
|
|
163
|
+
return { version, scope: { ...scope, found: reports.length }, reports };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function formatGateTimingScan(scan: GateTimingScan): string {
|
|
167
|
+
const s = scan.scope;
|
|
168
|
+
const head = `게이트 로그 ${scan.version} — ${s.prodOnly ? '운영 원장만(--prod-only)' : '전 우주'} · 본 원장 루트 ${s.roots}개 · 로그 있는 우주 ${s.found}개`
|
|
169
|
+
+ (s.registry === 'unreadable' ? ' · ⚠ 우주 레지스트리를 못 읽었다(운영만 봤다 — «없다»가 아니라 «못 셈»)' : '');
|
|
170
|
+
if (scan.reports.length === 0) {
|
|
171
|
+
const where = s.registry === 'read' ? '본 우주 어디에도' : '운영 원장에';
|
|
172
|
+
const unknown = s.registry === 'unreadable' ? ' · 시험 우주는 못 셌다(있을 수 있다)' : '';
|
|
173
|
+
return [head, ` 게이트 시간 — 측정 없음 · ${where} release/${scan.version}/gate-logs/cut 가 없다${unknown}`,
|
|
174
|
+
...(s.prodOnly ? [' (--prod-only 를 빼면 시험 우주까지 본다)'] : [])].join('\n');
|
|
175
|
+
}
|
|
176
|
+
return [head, ...scan.reports.map((r) => `⟨${r.universe}⟩ ${formatGateTiming(r)}`)].join('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
91
179
|
export function formatGateTiming(r: GateTimingReport): string {
|
|
92
180
|
if (!r.measured) {
|
|
93
181
|
return [`게이트 시간 — 측정 없음 (${r.dir})`, ' pod-*.json 에 durationMs 가 하나도 없다 · 0 은 «잰 값»이 아니다',
|
|
@@ -106,15 +194,36 @@ export function formatGateTiming(r: GateTimingReport): string {
|
|
|
106
194
|
return lines.join('\n');
|
|
107
195
|
}
|
|
108
196
|
|
|
197
|
+
export const GATE_TIMING_USAGE = [
|
|
198
|
+
'기본 = 전 우주 — 운영 원장 ⊕ 시험 우주(⟨test:wt-release⟩ 등)의 게이트 로그를 옵션 없이 함께 읽는다',
|
|
199
|
+
'사용법: bun scripts/release-loop/gate-timing.ts <version|dir> [--json] [--prod-only]',
|
|
200
|
+
' <version> 등록된 모든 우주의 release/<version>/gate-logs/cut 를 우주마다 따로 보고한다(⟨우주⟩ 태그)',
|
|
201
|
+
' <dir> 그 디렉토리 하나만 (종전과 같은 단일 보고)',
|
|
202
|
+
' --prod-only 운영 원장 루트(releaseLedgerRoot)만 본다 — 종전 기본',
|
|
203
|
+
' --json 구조화 출력 (<version> 이면 { version, scope, reports[] })',
|
|
204
|
+
].join('\n');
|
|
205
|
+
|
|
206
|
+
const KNOWN_FLAGS = new Set(['--json', '--prod-only', '--help', '-h']);
|
|
207
|
+
|
|
109
208
|
if (import.meta.main) {
|
|
110
209
|
const args = process.argv.slice(2);
|
|
210
|
+
if (args.includes('--help') || args.includes('-h')) { console.log(GATE_TIMING_USAGE); process.exit(0); }
|
|
211
|
+
const unknown = args.filter((a) => a.startsWith('-') && !KNOWN_FLAGS.has(a));
|
|
212
|
+
if (unknown.length) { console.error(`알 수 없는 옵션: ${unknown.join(' ')}\n${GATE_TIMING_USAGE}`); process.exit(2); }
|
|
111
213
|
const json = args.includes('--json');
|
|
112
|
-
const
|
|
214
|
+
const prodOnly = args.includes('--prod-only');
|
|
215
|
+
const target = args.find((a) => !a.startsWith('-'));
|
|
113
216
|
if (!target) {
|
|
114
|
-
console.error(
|
|
217
|
+
console.error(GATE_TIMING_USAGE);
|
|
115
218
|
process.exit(2);
|
|
116
219
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
220
|
+
if (isGateLogDirArg(target)) {
|
|
221
|
+
const report = gateTiming(target);
|
|
222
|
+
console.log(json ? JSON.stringify(report, null, 2) : formatGateTiming(report));
|
|
223
|
+
if (!report.measured) process.exit(1);
|
|
224
|
+
} else {
|
|
225
|
+
const scan = scanGateTiming(target, { prodOnly });
|
|
226
|
+
console.log(json ? JSON.stringify(scan, null, 2) : formatGateTimingScan(scan));
|
|
227
|
+
if (!scan.reports.some((r) => r.measured)) process.exit(1);
|
|
228
|
+
}
|
|
120
229
|
}
|
|
@@ -27,7 +27,7 @@ export interface PreflightResult {
|
|
|
27
27
|
blockers: number;
|
|
28
28
|
warnings: number;
|
|
29
29
|
findings: PreflightFinding[];
|
|
30
|
-
schedule: { cutAt: string; landBy: string | null; publishAt: string | null; publishAtSource: 'option' | 'cadence' | null } | null;
|
|
30
|
+
schedule: { cutAt: string; landBy: string | null; publishAt: string | null; publishAtSource: 'option' | 'schedule' | 'cadence' | null } | null;
|
|
31
31
|
/** The immediately previous release (by version order) whose gate is compared. */
|
|
32
32
|
previousVersion: string | null;
|
|
33
33
|
gateMeasure: GateMeasure | null;
|
|
@@ -206,15 +206,17 @@ export async function releasePreflight(version: string, deps: PreflightDeps = {}
|
|
|
206
206
|
findings.push({ level: 'warn', check: 'schedule', message: `판 일정 없음 — elanous release schedule set --version ${version} --cut-at <iso> --land-by <iso>` });
|
|
207
207
|
} else {
|
|
208
208
|
const cut = Date.parse(schedule.cutAt);
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
// --publish-at > the stored schedule publish time (`release schedule set --publish-at`) > the October cadence.
|
|
210
|
+
const publishAtSource = deps.publishAt ? 'option' : schedule.publishAt ? 'schedule' : 'cadence';
|
|
211
|
+
const publishAt = deps.publishAt ? new Date(deps.publishAt).toISOString() : schedule.publishAt ?? cadencePublishAt(schedule.cutAt);
|
|
212
|
+
scheduleOut = { cutAt: schedule.cutAt, landBy: schedule.landBy, publishAt, publishAtSource };
|
|
211
213
|
if (!schedule.landBy) findings.push({ level: 'warn', check: 'schedule', message: `착지 마감 없음 — 컷 ${formatKst(schedule.cutAt)} · 규칙은 컷 − 30분` });
|
|
212
214
|
else if (cut - Date.parse(schedule.landBy) < 30 * 60_000) findings.push({ level: 'warn', check: 'schedule', message: `착지 마감 ${formatKst(schedule.landBy)} 이 컷 − 30분보다 늦다(컷 ${formatKst(schedule.cutAt)})` });
|
|
213
215
|
else findings.push({ level: 'ok', check: 'schedule', message: `일정 — 착지 마감 ${formatKst(schedule.landBy)} · 컷 ${formatKst(schedule.cutAt)}` });
|
|
214
216
|
gateMeasure = previousVersion ? releaseGateMeasure({ version: previousVersion }, deps.graphRunsRoot) : null;
|
|
215
217
|
if (!gateMeasure) fallbackGateMeasure = releaseGateMeasure({ below: version }, deps.graphRunsRoot);
|
|
216
218
|
const windowSeconds = Math.round((Date.parse(publishAt) - cut) / 1000);
|
|
217
|
-
const publishLabel = `발행 ${formatKst(publishAt)}${
|
|
219
|
+
const publishLabel = `발행 ${formatKst(publishAt)}${publishAtSource === 'cadence' ? '(기본: 판 주기 07·18시 · --publish-at 로 바꾼다)' : publishAtSource === 'schedule' ? '(판 일정)' : ''}`;
|
|
218
220
|
if (!gateMeasure) {
|
|
219
221
|
const fallback = fallbackGateMeasure ? ` · 참고: 가장 최근 실측은 ${fallbackGateMeasure.version} 게이트 ${minutes(fallbackGateMeasure.gateSeconds)}(직전 판 아님)` : ' · 게이트 실측 없음';
|
|
220
222
|
findings.push({ level: 'warn', check: 'schedule', message: `직전 판${previousVersion ? `(${previousVersion})` : ''} 실측 없음 — 컷→${publishLabel} 창 ${minutes(windowSeconds)}${fallback}` });
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// PUBLISH-TIME-FROM-GATE (0.2.20 P1) — 발행 시각을 사람이 고르지 않고 «실측»에서 제안한다.
|
|
2
|
+
//
|
|
3
|
+
// 🩸 계기: 판 일정이 «컷 18:45 → 발행 20:00(75분)»처럼 잡혔는데 게이트 하나가 60분+ · 게이트 뒤 노드가 ~40분이다.
|
|
4
|
+
// ⭐ 자 = 최근 N(기본 3)개 완료된(`status: done` · 안정 버전) release-loop 런마다 «런 시작(≈ 컷) → 발행 노드 끝»을 재고 중앙값을 쓴다.
|
|
5
|
+
// 창은 «최근 N개 완료 런»으로 고정한다 — 못 잰 런 대신 더 옛 런을 끌어오지 않는다(옛 판이 지금 실측인 척 섞이지 않게).
|
|
6
|
+
// 런 시작 = 컷이다(예약 시작이 컷에 런을 띄운다) — preflight 의 «런 시작→발행 노드»와 같은 기점.
|
|
7
|
+
// ⛔ 못 잰 런은 0 으로 세지 않는다 — 건너뛰고 `skipped` 에 이유와 함께 남긴다. 표본이 0 이면 제안도 경고도 «없음»이다.
|
|
8
|
+
//
|
|
9
|
+
// 시각 출처(우선순위):
|
|
10
|
+
// ① `nodes[]` 의 `startedAt`·`endedAt`(0.2.19~ 원장이 노드 시각을 적는다)
|
|
11
|
+
// ② 옛 원장 — `<runId>.json.contexts/<n>.json` 의 mtime(노드 n 이 «시작할 때» 쓰인다) ⇒ 발행 노드 끝 ≈ 다음 노드 시작.
|
|
12
|
+
// (preflight `releaseGateMeasure` 와 같은 근사 · 그 파일의 nodeId 가 path 와 맞을 때만 쓴다)
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_PUBLISH_SAMPLE = 3;
|
|
18
|
+
/** 중앙값 위에 얹는 여유(분). 제안은 그 뒤 5분 단위로 올린다. */
|
|
19
|
+
export const DEFAULT_PUBLISH_MARGIN_MIN = 10;
|
|
20
|
+
|
|
21
|
+
const STABLE = /^\d+\.\d+\.\d+$/;
|
|
22
|
+
|
|
23
|
+
export interface PublishDurationSample {
|
|
24
|
+
version: string;
|
|
25
|
+
runId: string;
|
|
26
|
+
startedAt: string;
|
|
27
|
+
/** 런 시작 → 발행 노드 끝(초). */
|
|
28
|
+
seconds: number;
|
|
29
|
+
source: 'nodes' | 'contexts';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PublishWindowMeasure {
|
|
33
|
+
root: string;
|
|
34
|
+
/** 중앙값에 쓴 표본(최신 먼저 · 최근 sample 개 완료 런 중 잰 것). */
|
|
35
|
+
samples: PublishDurationSample[];
|
|
36
|
+
/** 창 안의 완료 런이었지만 못 잰 것 — «0»이 아니라 «못 셈». */
|
|
37
|
+
skipped: Array<{ runId: string; version: string; why: string }>;
|
|
38
|
+
/** 표본이 없으면 null(측정 없음). */
|
|
39
|
+
medianSeconds: number | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface RunState {
|
|
43
|
+
runId?: string;
|
|
44
|
+
status?: string;
|
|
45
|
+
startedAt?: string;
|
|
46
|
+
path?: unknown;
|
|
47
|
+
input?: { version?: string };
|
|
48
|
+
nodes?: Array<{ nodeId?: string; startedAt?: string; endedAt?: string }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function defaultReleaseGraphRunsRoot(ledgerRoot: string = releaseLedgerRoot()): string {
|
|
52
|
+
return join(ledgerRoot, 'graph-runs', 'release-loop');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function median(xs: number[]): number {
|
|
56
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
57
|
+
const m = s.length >> 1;
|
|
58
|
+
return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function publishEndMs(root: string, file: string, state: RunState, path: string[]): { ms: number; source: PublishDurationSample['source'] } | null {
|
|
62
|
+
const node = Array.isArray(state.nodes) ? state.nodes.find((n) => n?.nodeId === 'publish') : undefined;
|
|
63
|
+
const ended = node?.endedAt ? Date.parse(node.endedAt) : NaN;
|
|
64
|
+
if (Number.isFinite(ended)) return { ms: ended, source: 'nodes' };
|
|
65
|
+
const index = path.indexOf('publish');
|
|
66
|
+
const contexts = join(root, `${file}.contexts`);
|
|
67
|
+
if (index < 0 || index + 1 >= path.length || !existsSync(contexts)) return null;
|
|
68
|
+
const ctxPath = join(contexts, `${index + 2}.json`); // contexts/<n>.json 은 1부터 — 발행 다음 노드의 시작
|
|
69
|
+
try {
|
|
70
|
+
const ctx = JSON.parse(readFileSync(ctxPath, 'utf8')) as { nodeId?: string };
|
|
71
|
+
return ctx.nodeId === path[index + 1] ? { ms: statSync(ctxPath).mtimeMs, source: 'contexts' } : null;
|
|
72
|
+
} catch { return null; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 최근 `sample` 개 완료 release-loop 런의 «런 시작 → 발행 노드 끝» 중앙값. */
|
|
76
|
+
export function measurePublishWindow(opts: { root?: string; sample?: number } = {}): PublishWindowMeasure {
|
|
77
|
+
const root = opts.root ?? defaultReleaseGraphRunsRoot();
|
|
78
|
+
const sample = Math.max(1, opts.sample ?? DEFAULT_PUBLISH_SAMPLE);
|
|
79
|
+
const runs: Array<{ file: string; state: RunState; version: string; startedAt: string; startMs: number; path: string[] | null }> = [];
|
|
80
|
+
if (existsSync(root)) {
|
|
81
|
+
for (const file of readdirSync(root)) {
|
|
82
|
+
if (!file.endsWith('.json') || file.includes('.decision')) continue;
|
|
83
|
+
try {
|
|
84
|
+
const state = JSON.parse(readFileSync(join(root, file), 'utf8')) as RunState;
|
|
85
|
+
const v = state.input?.version;
|
|
86
|
+
if (state.status !== 'done' || typeof v !== 'string' || !STABLE.test(v)) continue;
|
|
87
|
+
// A completed stable run with a broken start or path is still «a run in the window» — it lands in `skipped`, not nowhere.
|
|
88
|
+
const startedAt = typeof state.startedAt === 'string' ? state.startedAt : '';
|
|
89
|
+
runs.push({ file, state, version: v, startedAt, startMs: Date.parse(startedAt), path: Array.isArray(state.path) ? state.path.filter((p): p is string => typeof p === 'string') : null });
|
|
90
|
+
} catch { /* 못 읽은 원장은 표본이 아니다 */ }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Newest first; a run without a readable start sorts by its file mtime so it still takes its place in the window.
|
|
94
|
+
const order = (run: (typeof runs)[number]) => (Number.isFinite(run.startMs) ? run.startMs : statSync(join(root, run.file)).mtimeMs);
|
|
95
|
+
runs.sort((a, b) => order(b) - order(a));
|
|
96
|
+
const samples: PublishDurationSample[] = [];
|
|
97
|
+
const skipped: PublishWindowMeasure['skipped'] = [];
|
|
98
|
+
for (const run of runs.slice(0, sample)) {
|
|
99
|
+
const runId = run.state.runId ?? run.file.slice(0, -5);
|
|
100
|
+
if (!Number.isFinite(run.startMs)) { skipped.push({ runId, version: run.version, why: 'startedAt 없음·무효' }); continue; }
|
|
101
|
+
if (!run.path) { skipped.push({ runId, version: run.version, why: 'path 가 배열이 아님' }); continue; }
|
|
102
|
+
if (!run.path.includes('publish')) { skipped.push({ runId, version: run.version, why: 'path 에 publish 노드 없음' }); continue; }
|
|
103
|
+
const end = publishEndMs(root, run.file, run.state, run.path);
|
|
104
|
+
const start = run.startMs;
|
|
105
|
+
if (!end || end.ms < start) { skipped.push({ runId, version: run.version, why: '발행 노드 끝 시각을 못 읽음' }); continue; }
|
|
106
|
+
samples.push({ version: run.version, runId, startedAt: run.startedAt, seconds: Math.round((end.ms - start) / 1000), source: end.source });
|
|
107
|
+
}
|
|
108
|
+
return { root, samples, skipped, medianSeconds: samples.length ? Math.round(median(samples.map((s) => s.seconds))) : null };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 제안 발행 시각 = 컷 + 중앙값 + 여유, 5분 단위로 올림. 측정이 없으면 null. */
|
|
112
|
+
export function proposePublishAt(cutAt: string, measure: PublishWindowMeasure, marginMin = DEFAULT_PUBLISH_MARGIN_MIN): string | null {
|
|
113
|
+
if (measure.medianSeconds === null) return null;
|
|
114
|
+
const raw = Date.parse(cutAt) + measure.medianSeconds * 1000 + marginMin * 60_000;
|
|
115
|
+
const step = 5 * 60_000;
|
|
116
|
+
return new Date(Math.ceil(raw / step) * step).toISOString();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function kstHourMinute(iso: string): string {
|
|
120
|
+
return new Intl.DateTimeFormat('en-GB', { timeZone: 'Asia/Seoul', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).format(new Date(iso));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const mins = (seconds: number) => Math.round(seconds / 60);
|
|
124
|
+
|
|
125
|
+
/** 발행 시각이 실측 중앙값보다 짧으면 한 줄 경고, 아니면 null(측정 없음도 null — 경고할 자가 없다). */
|
|
126
|
+
export function publishWindowWarning(cutAt: string, publishAt: string, measure: PublishWindowMeasure, marginMin = DEFAULT_PUBLISH_MARGIN_MIN): string | null {
|
|
127
|
+
if (measure.medianSeconds === null) return null;
|
|
128
|
+
const windowSeconds = (Date.parse(publishAt) - Date.parse(cutAt)) / 1000;
|
|
129
|
+
if (windowSeconds >= measure.medianSeconds) return null;
|
|
130
|
+
const proposed = proposePublishAt(cutAt, measure, marginMin)!;
|
|
131
|
+
return `⚠️ 발행 시각이 실측 중앙값(${mins(measure.medianSeconds)}분)보다 짧다 — 컷→발행 ${mins(windowSeconds)}분 · 제안: ${kstHourMinute(proposed)} KST (최근 ${measure.samples.length}판 ${measure.samples.map((s) => s.version).join('·')} · +여유 ${marginMin}분)`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** `release schedule show` 의 한 줄 — 제안 발행 시각 또는 «측정 없음». */
|
|
135
|
+
export function formatPublishProposal(cutAt: string, measure: PublishWindowMeasure, marginMin = DEFAULT_PUBLISH_MARGIN_MIN): string {
|
|
136
|
+
const proposed = proposePublishAt(cutAt, measure, marginMin);
|
|
137
|
+
if (!proposed || measure.medianSeconds === null) {
|
|
138
|
+
return `발행 제안 — 실측 없음(완료된 release-loop 런에서 발행 노드 시각을 못 읽었다 · 본 곳 ${measure.root}${measure.skipped.length ? ` · 못 잰 런 ${measure.skipped.length}` : ''})`;
|
|
139
|
+
}
|
|
140
|
+
return `발행 제안 ${kstHourMinute(proposed)} KST — 컷 + 실측 중앙값 ${mins(measure.medianSeconds)}분(최근 ${measure.samples.length}판 ${measure.samples.map((s) => `${s.version} ${mins(s.seconds)}분`).join(' · ')}) + 여유 ${marginMin}분`;
|
|
141
|
+
}
|
|
@@ -114,10 +114,7 @@ export function recordDocsFollow(root: string, manifest: ReleaseManifest, items:
|
|
|
114
114
|
const journal = join(root, 'seat-requests', 'requests.jsonl');
|
|
115
115
|
withSeatRequestLedgerLock(journal, () => {
|
|
116
116
|
const known = new Set(listSeatRequests(root).map((row) => row.key));
|
|
117
|
-
const missing =
|
|
118
|
-
...report.pending.map((item) => ({ id: item.id, text: `문서 반영 대기 목록 v${manifest.version} · ${item.id}: ${item.missing.join(', ')} — 착지 ${item.sha}; docs·홈 실측 뒤 닫기` })),
|
|
119
|
-
...report.unassessed.map((id) => ({ id, text: `문서 반영 대기 목록 v${manifest.version} · ${id}: 착지·green·EXPOSE-RUBRIC 근거 미판정 — 확인 전 공개 문면 금지` })),
|
|
120
|
-
];
|
|
117
|
+
const missing = report.pending.map((item) => ({ id: item.id, text: `문서 반영 대기 목록 v${manifest.version} · ${item.id}: ${item.missing.join(', ')} — 착지 ${item.sha}; docs·홈 실측 뒤 닫기` }));
|
|
121
118
|
const rows = missing.filter((item) => !known.has(`docs-follow:${manifest.version}:${item.id}`)).map((item) => ({
|
|
122
119
|
key: `docs-follow:${manifest.version}:${item.id}`, seat: 'MK', status: 'queued' as const,
|
|
123
120
|
text: item.text, queuedAt: now.toISOString(), ref: `release:${manifest.version}:docs-follow`,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// RELEASE-LEDGER-UNIVERSE (0.2.20 P0): the release run decides its universe ONCE at the entry and every node
|
|
2
|
+
// subprocess is pinned to it (graph runner `pinChildUniverse` stamps ELANOUS_STATE_DIR = this root). Without it a node
|
|
3
|
+
// launched as `bun scripts/release-loop/*-node.ts` from a non-leader tree (wt-release) re-resolved the tree-derived test
|
|
4
|
+
// universe and wrote manifest·story·gate logs to ⟨test:wt-release⟩ while the run ledger was prod (10-07).
|
|
5
|
+
import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
|
|
6
|
+
import { debug } from '../../src/debug/log.js';
|
|
7
|
+
import { effectiveInstanceRoot, normRoot, prodInstanceRoot } from '../../src/instance/resolve.js';
|
|
8
|
+
|
|
9
|
+
export interface ReleaseUniverseDeps {
|
|
10
|
+
/** An injected ledger root (tests, or a caller that already decided) is used as is. */
|
|
11
|
+
root?: string;
|
|
12
|
+
override?: () => string | undefined;
|
|
13
|
+
prodRoot?: () => string;
|
|
14
|
+
effectiveRoot?: () => string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The single universe of a release run. An explicit `--config-dir` selects its own universe; otherwise the release
|
|
19
|
+
* ledger is the machine-wide prod root, and a process that itself resolved elsewhere (a source tree without a stamp)
|
|
20
|
+
* is refused before any node starts — its own logs would land in a different universe from its nodes.
|
|
21
|
+
*/
|
|
22
|
+
export function releaseRunUniverse(deps: ReleaseUniverseDeps = {}): string {
|
|
23
|
+
if (deps.root) return deps.root;
|
|
24
|
+
const override = (deps.override ?? getElanousConfigDirOverride)();
|
|
25
|
+
if (override) {
|
|
26
|
+
const root = (deps.effectiveRoot ?? effectiveInstanceRoot)();
|
|
27
|
+
debug.log('release.run', 'universe', { root, layer: 'explicit-flag' });
|
|
28
|
+
return root;
|
|
29
|
+
}
|
|
30
|
+
const prod = normRoot((deps.prodRoot ?? prodInstanceRoot)());
|
|
31
|
+
const parent = normRoot((deps.effectiveRoot ?? effectiveInstanceRoot)());
|
|
32
|
+
if (parent !== prod) {
|
|
33
|
+
debug.log('release.run', 'universe-refused', { parent, prod });
|
|
34
|
+
throw new Error(`release run refused: this process resolves to ${parent}, not the release ledger universe ${prod} — its nodes would split ledgers and logs across universes. Run the installed \`elanous\` or pass \`--config-dir ${prod}\``);
|
|
35
|
+
}
|
|
36
|
+
debug.log('release.run', 'universe', { root: prod, layer: 'prod' });
|
|
37
|
+
return prod;
|
|
38
|
+
}
|
|
@@ -7,6 +7,7 @@ import { join } from 'node:path';
|
|
|
7
7
|
import { debug } from '../../src/debug/log.js';
|
|
8
8
|
import { effectiveInstanceRoot, releaseLedgerRoot } from '../../src/instance/resolve.js';
|
|
9
9
|
import { lastJsonObject, runGraph, type GraphRunState } from '../../src/graph-runner/runner.js';
|
|
10
|
+
import { releaseRunUniverse } from './release-universe.js';
|
|
10
11
|
import { isReleaseVersion } from './release-version.js';
|
|
11
12
|
import { changedFilesBetween, clearPartialPlan, planPartialRegate, writePartialPlan } from './gate-partial.js';
|
|
12
13
|
|
|
@@ -165,10 +166,12 @@ function savedGraphPath(statePath: string): string | undefined {
|
|
|
165
166
|
/** Refresh the branch tip, then restart the failed run at `from` with the saved graph snapshot. */
|
|
166
167
|
export async function resumeReleaseRun(opts: { runId: string; from: string; partial?: boolean }, deps: ResumeReleaseDeps = {}): Promise<{ tip: TipRefresh; partial: PartialOutcome; state: GraphRunState }> {
|
|
167
168
|
if (opts.from === 'version-release') throw new Error('--from version-release would cut again — resume at a later node');
|
|
169
|
+
// RELEASE-LEDGER-UNIVERSE: decide the universe once; the run lookup and every resumed node use it.
|
|
170
|
+
deps = { ...deps, root: releaseRunUniverse({ root: deps.root }) };
|
|
168
171
|
const tip = refreshReleaseBranchTip(opts.runId, deps, opts.from);
|
|
169
172
|
const partial = preparePartial(tip, opts, deps);
|
|
170
173
|
// Resume the graph at the path it was started from (its snapshot is used); only tests inject another path.
|
|
171
174
|
const graphPath = deps.graphPath ?? savedGraphPath(tip.statePath) ?? GRAPH;
|
|
172
|
-
const state = await (deps.graph ?? runGraph)(graphPath, { resumeRunId: opts.runId, fromNodeId: opts.from,
|
|
175
|
+
const state = await (deps.graph ?? runGraph)(graphPath, { resumeRunId: opts.runId, fromNodeId: opts.from, pinChildUniverse: true, deps: { root: deps.root, ...(deps.runBash ? { runBash: deps.runBash } : {}) } });
|
|
173
176
|
return { tip, partial, state };
|
|
174
177
|
}
|
|
@@ -1,15 +1,60 @@
|
|
|
1
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
3
3
|
import { debug } from '../../src/debug/log.js';
|
|
4
4
|
|
|
5
5
|
/** Plan weights for a Pod sweep: wall-scaled when the previous cut recorded shard wall times, junit seconds otherwise. */
|
|
6
|
-
export function planDurations(durationSource: string | undefined): { durations: Map<string, number>; wall?: { shards: number; scaledFiles: number } } {
|
|
6
|
+
export function planDurations(durationSource: string | undefined): { durations: Map<string, number>; wall?: { shards: number; scaledFiles: number }; fallback?: string } {
|
|
7
7
|
if (!durationSource) return { durations: new Map() };
|
|
8
|
-
const
|
|
9
|
-
|
|
8
|
+
const primary = planDurationsFrom(durationSource);
|
|
9
|
+
if (primary.durations.size) return primary;
|
|
10
|
+
// GATE-PLAN-TIMINGS: the baseline cut left no reports (0.2.20: baseline 0.2.19 was gated from another universe, so
|
|
11
|
+
// all 5,585 files were «unknown», planned 3.9 min, measured 13–20 min). Use the most recent sibling cut instead.
|
|
12
|
+
const fallback = latestSiblingCutDir(durationSource);
|
|
13
|
+
if (!fallback) {
|
|
14
|
+
debug.log('release-loop.gate', 'plan-duration-fallback', { source: durationSource, fallback: null });
|
|
15
|
+
return primary;
|
|
16
|
+
}
|
|
17
|
+
const result = planDurationsFrom(fallback);
|
|
18
|
+
debug.log('release-loop.gate', 'plan-duration-fallback', { source: durationSource, fallback, files: result.durations.size, wall: !!result.wall });
|
|
19
|
+
return result.durations.size ? { ...result, fallback } : primary;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function planDurationsFrom(dir: string): { durations: Map<string, number>; wall?: { shards: number; scaledFiles: number } } {
|
|
23
|
+
const junit = readFileDurations(dir);
|
|
24
|
+
const wall = readFileWallWeights(dir, junit);
|
|
10
25
|
return wall ? { durations: wall.weights, wall: { shards: wall.shards, scaledFiles: wall.scaledFiles } } : { durations: junit };
|
|
11
26
|
}
|
|
12
27
|
|
|
28
|
+
/**
|
|
29
|
+
* For a `<root>/<version>/<a>/<b>` duration source (the gate passes `<ledger>/release/<baseline>/gate-logs/cut`), the
|
|
30
|
+
* sibling `<root>/<other>/<a>/<b>` directory whose newest `*.junit.xml` is the most recent — «the latest gate». The
|
|
31
|
+
* source itself is skipped. Undefined when the path is too shallow or no sibling holds a junit report.
|
|
32
|
+
*/
|
|
33
|
+
export function latestSiblingCutDir(durationSource: string): string | undefined {
|
|
34
|
+
const source = resolve(durationSource);
|
|
35
|
+
const b = basename(source);
|
|
36
|
+
const a = basename(dirname(source));
|
|
37
|
+
const root = dirname(dirname(dirname(source)));
|
|
38
|
+
if (!a || !b || root === dirname(dirname(source))) return undefined;
|
|
39
|
+
let versions: string[];
|
|
40
|
+
try { versions = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name); }
|
|
41
|
+
catch { return undefined; }
|
|
42
|
+
let best: { dir: string; mtimeMs: number } | undefined;
|
|
43
|
+
for (const version of versions.sort()) {
|
|
44
|
+
const dir = join(root, version, a, b);
|
|
45
|
+
if (dir === source) continue;
|
|
46
|
+
let newest = -1;
|
|
47
|
+
try {
|
|
48
|
+
for (const name of readdirSync(dir)) {
|
|
49
|
+
if (!name.endsWith('.junit.xml')) continue;
|
|
50
|
+
try { newest = Math.max(newest, statSync(join(dir, name)).mtimeMs); } catch { /* vanished mid-scan */ }
|
|
51
|
+
}
|
|
52
|
+
} catch { continue; }
|
|
53
|
+
if (newest >= 0 && (!best || newest > best.mtimeMs)) best = { dir, mtimeMs: newest };
|
|
54
|
+
}
|
|
55
|
+
return best?.dir;
|
|
56
|
+
}
|
|
57
|
+
|
|
13
58
|
/** Observation for GATE-PLAN-WALLTIME, emitted once the shards are planned so `maxPlannedMin` is the heaviest shard. */
|
|
14
59
|
export function logPlanWalltime(wall: { shards: number; scaledFiles: number } | undefined, shards: ReadonlyArray<{ plannedSeconds: number }>): void {
|
|
15
60
|
if (!wall) return;
|
|
@@ -9,6 +9,7 @@ import { getSchedule } from '../../src/release-loop/release-schedule.js';
|
|
|
9
9
|
import { isLandingFreezeRefusal, readLandingFreeze, LandingFrozenError } from '../../src/release-loop/landing-freeze.js';
|
|
10
10
|
import { debug } from '../../src/debug/log.js';
|
|
11
11
|
import { runGraph, type GraphRunState } from '../../src/graph-runner/runner.js';
|
|
12
|
+
import { releaseRunUniverse } from './release-universe.js';
|
|
12
13
|
import { baseVersion, isStableVersion, nextPrereleaseVersion, type PrereleaseKind } from './release-version.js';
|
|
13
14
|
|
|
14
15
|
const VERSION = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
|
|
@@ -18,7 +19,12 @@ export interface ReleaseLoopConfig {
|
|
|
18
19
|
gatePodPool?: string;
|
|
19
20
|
gatePodBunCache?: string;
|
|
20
21
|
gatePodShards?: number;
|
|
22
|
+
/** GATE-TIMEOUT-HEAL — the per-shard deadline floor; each shard gets planned × 1.5 above it (capped). */
|
|
21
23
|
gatePodShardTimeoutSeconds?: number;
|
|
24
|
+
/** GATE-INSTALL-CACHE — concurrent `bun install`s per gate node (default 4 · 0 = off). */
|
|
25
|
+
gatePodInstallSlots?: number;
|
|
26
|
+
/** GATE-MEM-ADMIT — seconds a gate shard waits for pool admission (memory headroom) before launching anyway with `admission-timeout` (default 300 · 0 = off). */
|
|
27
|
+
gatePodAdmissionWaitSeconds?: number;
|
|
22
28
|
/** GATE-SPEED A3①: gate shard Pod CPU — `2` (request = limit) or `{ request, limit }`. Omitted = request 1 / limit 4. */
|
|
23
29
|
gatePodCpu?: number | string | { request?: number | string; limit?: number | string };
|
|
24
30
|
gateRemote?: string;
|
|
@@ -42,7 +48,7 @@ export interface UnattendedReleaseDeps {
|
|
|
42
48
|
ledgerRoot?: string;
|
|
43
49
|
config?: ReleaseLoopConfig;
|
|
44
50
|
configPath?: string;
|
|
45
|
-
graph?: (path: string, options: { input: ReleaseRunInput }) => Promise<GraphRunState>;
|
|
51
|
+
graph?: (path: string, options: { input: ReleaseRunInput; pinChildUniverse?: boolean; deps?: { root?: string } }) => Promise<GraphRunState>;
|
|
46
52
|
checklist?: (version: string) => Pick<ReturnType<typeof cutChecklistGate>, 'ok' | 'red' | 'undecided' | 'blocked'>;
|
|
47
53
|
freezeRoot?: string;
|
|
48
54
|
/** Names already taken for prerelease numbering (remote release branches and tags). */
|
|
@@ -142,7 +148,9 @@ export async function runUnattendedRelease(
|
|
|
142
148
|
}
|
|
143
149
|
const beforeGraph = readLandingFreeze(deps.freezeRoot);
|
|
144
150
|
if (beforeGraph && !opts.forceFreeze) throw new LandingFrozenError(beforeGraph);
|
|
145
|
-
|
|
151
|
+
// RELEASE-LEDGER-UNIVERSE: one universe for the run ledger, the release ledger and every node subprocess.
|
|
152
|
+
const root = releaseRunUniverse({ root: deps.ledgerRoot });
|
|
153
|
+
const state = await (deps.graph ?? runGraph)(GRAPH, { input, pinChildUniverse: true, deps: { root } });
|
|
146
154
|
// A freeze switched on while the graph ran stops gate or publish inside it. Only a failed gate/publish node whose own
|
|
147
155
|
// record is the freeze refusal is reported as the freeze; any other failure keeps its own reason.
|
|
148
156
|
if (state.status === 'failed' && !opts.forceFreeze) {
|
|
@@ -55,7 +55,7 @@ function userFacingLines(markdown: string): { lines: string[]; internalDropped:
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function firstPhrase(title: string): string {
|
|
58
|
-
let quote: '»' | '"' | '`' | undefined;
|
|
58
|
+
let quote: '»' | '"' | '`' | '”' | '’' | '」' | undefined;
|
|
59
59
|
let angleStart = -1;
|
|
60
60
|
for (let i = 0; i < title.length; i++) {
|
|
61
61
|
const char = title[i]!;
|
|
@@ -64,6 +64,10 @@ function firstPhrase(title: string): string {
|
|
|
64
64
|
continue;
|
|
65
65
|
}
|
|
66
66
|
if (char === '«') { quote = '»'; angleStart = i; continue; }
|
|
67
|
+
if (char === '“') { quote = '”'; continue; }
|
|
68
|
+
// 여는 ‘ 는 생략 부호로 안 쓰이니 짝을 기다린다 — ASCII ' 는 한국어 생략 부호라 짝으로 보지 않는다.
|
|
69
|
+
if (char === '‘') { quote = '’'; continue; }
|
|
70
|
+
if (char === '「') { quote = '」'; continue; }
|
|
67
71
|
if (char === '"' || char === '`') { quote = char; continue; }
|
|
68
72
|
if (/[::]/.test(char) || (/[—·–]/.test(char) && /\s/.test(title[i - 1] ?? '') && /\s/.test(title[i + 1] ?? ''))) {
|
|
69
73
|
return title.slice(0, i).trim();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* RETRO-DAILY 그림자 대조 — 매일 회고 그래프를 «정답지»(RFC §0 다섯 반복 문제) 위에서 돌려 맞혔는지 본다.
|
|
4
|
+
*
|
|
5
|
+
* bun scripts/retro/shadow-compare.ts [--facts <facts.json>] [--key <answer-key.json>] [--json]
|
|
6
|
+
*
|
|
7
|
+
* 기본 입력 = test/fixtures/retro/ 의 10-07 사실 ⊕ 정답지. 그래프는 임시 우주(mkdtemp)에서 shadow 로만 돈다 —
|
|
8
|
+
* 운영 회고 파일·체크리스트·메시지 어느 것도 건드리지 않는다. 놓친 주제가 있으면 exit 1.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join, resolve } from 'node:path';
|
|
13
|
+
import { runGraph } from '../../src/graph-runner/runner.js';
|
|
14
|
+
import { retroDayFile } from '../../src/retro/daily.js';
|
|
15
|
+
|
|
16
|
+
const repo = resolve(import.meta.dir, '..', '..');
|
|
17
|
+
export const RETRO_DAILY_GRAPH = join(repo, 'graphs', 'retro', 'daily.yaml');
|
|
18
|
+
export const DEFAULT_FACTS = join(repo, 'test', 'fixtures', 'retro', 'rfc0-2026-10-07-facts.json');
|
|
19
|
+
export const DEFAULT_KEY = join(repo, 'test', 'fixtures', 'retro', 'rfc0-answer-key.json');
|
|
20
|
+
|
|
21
|
+
type Theme = { theme: string; title: string; recurring: boolean; reBrokeAfterGreen: boolean; rank: number };
|
|
22
|
+
type Key = { day: string; greenCells?: unknown[]; expected: { theme: string; title: string; reBrokeAfterGreen?: boolean }[]; notRecurring?: string[] };
|
|
23
|
+
export type ShadowComparison = {
|
|
24
|
+
ok: boolean; status: string; day: string; detected: string[]; missed: string[]; falsePositives: string[];
|
|
25
|
+
reBrokeMismatch: string[]; firstRank: string | null; actions: number; summary: string[]; wroteChecklist: boolean; mode: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function compareWithKey(dayFile: { themes: Theme[]; actions: unknown[]; summary: string[]; mode: string }, key: Key, status = 'done'): Omit<ShadowComparison, 'wroteChecklist'> {
|
|
29
|
+
const recurring = dayFile.themes.filter(t => t.recurring);
|
|
30
|
+
const detected = recurring.map(t => t.theme);
|
|
31
|
+
const missed = key.expected.filter(e => !detected.includes(e.theme)).map(e => e.theme);
|
|
32
|
+
// 정답지 밖의 반복 주제는 전부 오탐이다(목록에 적은 것만 보지 않는다).
|
|
33
|
+
const expected = new Set(key.expected.map(e => e.theme));
|
|
34
|
+
const falsePositives = [...new Set([...detected.filter(t => !expected.has(t)), ...(key.notRecurring ?? []).filter(t => detected.includes(t))])];
|
|
35
|
+
const reBrokeMismatch = key.expected.filter(e => e.reBrokeAfterGreen !== undefined
|
|
36
|
+
&& recurring.find(t => t.theme === e.theme)?.reBrokeAfterGreen !== e.reBrokeAfterGreen).map(e => e.theme);
|
|
37
|
+
const firstRank = recurring.sort((a, b) => a.rank - b.rank)[0]?.theme ?? null;
|
|
38
|
+
// «초록 뒤 재발»이 정답지에 있으면 그것이 1순위여야 한다(RFC §1 recur).
|
|
39
|
+
const mustLead = key.expected.find(e => e.reBrokeAfterGreen)?.theme;
|
|
40
|
+
const ok = status === 'done' && !missed.length && !falsePositives.length && !reBrokeMismatch.length
|
|
41
|
+
&& (!mustLead || firstRank === mustLead) && dayFile.actions.length <= 3 && dayFile.summary.length === 3;
|
|
42
|
+
return { ok, status, day: key.day, detected, missed, falsePositives, reBrokeMismatch, firstRank, actions: dayFile.actions.length, summary: dayFile.summary, mode: dayFile.mode };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function runShadowComparison(factsFile = DEFAULT_FACTS, keyFile = DEFAULT_KEY): Promise<ShadowComparison> {
|
|
46
|
+
const key = JSON.parse(readFileSync(keyFile, 'utf8')) as Key;
|
|
47
|
+
const root = mkdtempSync(join(tmpdir(), 'retro-shadow-'));
|
|
48
|
+
try {
|
|
49
|
+
const run = await runGraph(RETRO_DAILY_GRAPH, { input: { factsFile: resolve(factsFile), day: key.day, greenCells: key.greenCells ?? [], mode: 'shadow' }, deps: { root } });
|
|
50
|
+
if (run.status !== 'done') return { ok: false, status: run.status, day: key.day, detected: [], missed: key.expected.map(e => e.theme), falsePositives: [], reBrokeMismatch: [], firstRank: null, actions: 0, summary: [], wroteChecklist: false, mode: 'shadow' };
|
|
51
|
+
const dayFile = JSON.parse(readFileSync(retroDayFile(root, key.day), 'utf8'));
|
|
52
|
+
const act = run.nodes.find(n => n.nodeId === 'act');
|
|
53
|
+
const wroteChecklist = typeof act?.output === 'string' && /"wroteChecklist":true/.test(act.output);
|
|
54
|
+
const result = compareWithKey(dayFile, key, run.status);
|
|
55
|
+
return { ...result, ok: result.ok && !wroteChecklist && dayFile.mode === 'shadow', wroteChecklist };
|
|
56
|
+
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (import.meta.main) {
|
|
60
|
+
const args = process.argv.slice(2);
|
|
61
|
+
const flag = (name: string) => { const i = args.indexOf(name); return i >= 0 ? args[i + 1] : undefined; };
|
|
62
|
+
const result = await runShadowComparison(flag('--facts'), flag('--key'));
|
|
63
|
+
console.log(args.includes('--json') ? JSON.stringify(result) : [
|
|
64
|
+
`retro-daily 그림자 대조 ${result.ok ? 'OK' : 'MISS'} · ${result.day} · status=${result.status} · mode=${result.mode}`,
|
|
65
|
+
`잡음: ${result.detected.join(', ') || '없음'}`,
|
|
66
|
+
`놓침: ${result.missed.join(', ') || '없음'} · 오탐: ${result.falsePositives.join(', ') || '없음'} · 초록뒤재발 불일치: ${result.reBrokeMismatch.join(', ') || '없음'}`,
|
|
67
|
+
`1순위: ${result.firstRank ?? '없음'} · 조치 초안 ${result.actions} · 체크리스트 쓰기 ${result.wroteChecklist}`,
|
|
68
|
+
...result.summary.map(line => ` ${line}`),
|
|
69
|
+
].join('\n'));
|
|
70
|
+
if (!result.ok) process.exitCode = 1;
|
|
71
|
+
}
|