elanous 0.2.13 → 0.2.15
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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-33ffcc49ab45123b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.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/3558-bbb5ab553a0471d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
- package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.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/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
- 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-0e2c57fe6add4b2a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e05a77df7e005b04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b8c49f6b4fa2df91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-976813c91d7394ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-60c7a8e1a4fe62ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-2a52b316cb143d9c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-c3ce483c8d5db370.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-13c7b53e0c2519a9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-96f074f6538e8a2c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-e97be5334d83a9f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-c1343f9499d86f3f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-d23b12bb92242558.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-5efbf81124c474ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-153c2b3bcc89e8de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.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-7d4182964407c09c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-0d6d23b40263a7ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-63d14a4d8fe1e1c1.js → page-f5597e894c02cd33.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9817c7f9007466cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
- 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-539cde99554be20f.js +1 -0
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.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 -0
- package/apps/pwa/out/ceo/index.txt +33 -0
- 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/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 +23 -23
- 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 +22 -22
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -0
- package/apps/pwa/out/loops/index.txt +30 -0
- 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/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/orchestrator/orchestrator.yaml +59 -0
- package/graphs/orchestrator/recipes.yaml +18 -0
- package/graphs/release/recipes.yaml +6 -0
- package/graphs/release/release-loop.yaml +6 -1
- package/graphs/rhythm/daily.yaml +38 -0
- package/graphs/rhythm/recipes.yaml +18 -0
- package/graphs/rhythm/weekly.yaml +39 -0
- package/graphs/video/recipes.yaml +3 -0
- package/graphs/video/youtube-summary.yaml +17 -0
- package/package.json +1 -1
- package/scripts/audit-test-state-writes.ts +72 -17
- package/scripts/brand/check.ts +188 -0
- package/scripts/ci-typecheck-changed.ts +50 -18
- package/scripts/coord-post.sh +11 -1
- package/scripts/docs/generate-graph-anatomy.ts +315 -0
- package/scripts/docs-cli-check.ts +23 -6
- package/scripts/expose-rubric-check.ts +118 -0
- package/scripts/expose-rubric-suggest.ts +169 -0
- package/scripts/heal/run-role.ts +14 -0
- package/scripts/hq/standby-verify.ts +11 -7
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +112 -0
- package/scripts/marketing/generate-ax-brief.ts +140 -0
- package/scripts/marketing/generate-feature-map.ts +118 -0
- package/scripts/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/gate-node.ts +89 -14
- package/scripts/release-loop/publish-node.ts +14 -1
- package/scripts/release-loop/unattended-release.ts +23 -1
- package/scripts/release-loop/vault-note-node.ts +428 -0
- package/scripts/release-story/draft.ts +220 -0
- package/scripts/rhythm/daily.ts +570 -0
- package/scripts/rhythm/weekly.ts +315 -0
- package/scripts/skills/public-check.ts +169 -0
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/scripts/test-deterministic.ts +4 -8
- package/scripts/test-diet/effectiveness.ts +195 -0
- package/scripts/test-diet/lib.ts +16 -8
- package/scripts/test-diet/node.ts +77 -9
- package/scripts/ux/demo-rehearsal.ts +2 -2
- package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
- package/scripts/video/youtube-summary.ts +133 -0
- package/skills/diagram-master/SKILL.md +42 -49
- package/skills/diagram-master/references/pyproject.toml +4 -1
- package/skills/diagram-master/references/routing-guide.md +4 -2
- package/skills/grill-me/SKILL.md +1 -0
- package/skills/youtube-master/SKILL.md +13 -13
- package/skills/youtube-master/package-lock.json +0 -545
- package/skills/youtube-master/package.json +2 -5
- package/skills/youtube-master/scripts/main.ts +2 -2
- package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
- package/skills/youtube-master/src/util.ts +30 -7
- package/src/agent-env/profile.ts +161 -0
- package/src/agent-mission/driver.ts +3 -3
- package/src/agent-substrate/pr-reviewer.ts +2 -0
- package/src/ask-user-question/pending-questions.ts +5 -0
- package/src/auth/github-app-token.ts +112 -13
- package/src/autopilot/build/llm-conflict-merge.ts +73 -10
- package/src/autopilot/build/merge-intent.ts +53 -0
- package/src/briefing/brief-items.ts +121 -0
- package/src/chat/index.ts +2 -0
- package/src/claims/claims-ledger.ts +232 -0
- package/src/claims/claims-render.ts +166 -0
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/claims-cli.ts +94 -0
- package/src/cli/decisions-cli.ts +44 -3
- package/src/cli/docs-cli.ts +2 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/flow-cli.ts +11 -0
- package/src/cli/freeze-cli.ts +45 -0
- package/src/cli/intake-cli.ts +46 -17
- package/src/cli/lesson-cli.ts +108 -0
- package/src/cli/logs-cli.ts +104 -2
- package/src/cli/logs-model-input.ts +131 -0
- package/src/cli/pod-cli.ts +2 -0
- package/src/cli/pr-cli.ts +29 -1
- package/src/cli/private-ledger-commands.ts +54 -4
- package/src/cli/pty-drive-verdict.ts +11 -2
- package/src/cli/release-cli.ts +190 -33
- package/src/cli/rfc-status.ts +91 -0
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/connectors/linear.ts +5 -3
- package/src/context-bus/condense.ts +15 -20
- package/src/context-bus/context-now-public.ts +96 -0
- package/src/context-bus/context-now-surfaces.ts +55 -2
- package/src/context-bus/context-now.ts +6 -3
- package/src/context-bus/coord-events.ts +6 -4
- package/src/context-bus/long-term-memory.ts +92 -11
- package/src/dashboard/index.ts +75 -22
- package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
- package/src/dashboard/slash-runtime/loops-table.ts +77 -0
- package/src/decisions/decision-cards.ts +33 -7
- package/src/decisions/decision-ledger.ts +40 -1
- package/src/decisions/proact-meter.ts +286 -0
- package/src/decisions/proact1-lite.ts +86 -0
- package/src/decisions/web-push-decision-cards.ts +1 -1
- package/src/discord/sprint21-runtime.ts +16 -0
- package/src/discord-self-message.ts +8 -0
- package/src/discord-slash-wire.ts +31 -2
- package/src/domains/alert-audit.ts +271 -0
- package/src/domains/core-tools.ts +4 -3
- package/src/domains/ops-facts-tool.ts +31 -1
- package/src/domains/outbound-alert.ts +1 -1
- package/src/elanous-config-dir.ts +29 -4
- package/src/execution-loop/launch-gate.ts +29 -14
- package/src/flow/cards.ts +239 -0
- package/src/graph-runner/graph-tick.ts +4 -0
- package/src/graph-runner/runner.ts +30 -5
- package/src/harness/harness-cli-command.ts +259 -38
- package/src/harness/harness-incidents.ts +45 -12
- package/src/harness/harness-pod-dispatch.ts +102 -11
- package/src/harness/harness-queue-child.ts +3 -3
- package/src/harness/harness-queue.ts +583 -65
- package/src/harness/helper-repair.ts +87 -0
- package/src/harness/helper-scan.ts +151 -0
- package/src/harness/launch-stamp.ts +236 -0
- package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
- package/src/harness/ledger-write-guard.ts +35 -0
- package/src/harness/model-input-observation.ts +37 -0
- package/src/harness/pod-memory-policy.ts +15 -0
- package/src/harness/repository-name.ts +55 -0
- package/src/hq/fence-audit.ts +90 -0
- package/src/hq/hq.ts +63 -5
- package/src/hq/lease.ts +4 -4
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +332 -0
- package/src/index.ts +159 -27
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +37 -15
- package/src/intake-plane/front-classifier.ts +6 -0
- package/src/intake-plane/items.ts +3 -2
- package/src/intake-plane/lens.ts +177 -0
- package/src/intake-plane/linear-wish.ts +39 -11
- package/src/intake-plane/route.ts +145 -1
- package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
- package/src/intake-plane/wish-card.ts +53 -2
- package/src/intake-plane/wish-folder.ts +5 -2
- package/src/intake-plane/wish-reply.ts +72 -0
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
- package/src/launch-head/seat-goal-queue.ts +198 -0
- package/src/lessons/lesson-import.ts +106 -0
- package/src/lessons/lesson-ledger.ts +206 -0
- package/src/loops/activity.ts +203 -0
- package/src/loops/budget.ts +63 -0
- package/src/loops/checker.ts +5 -3
- package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
- package/src/loops/loop-cli.ts +36 -4
- package/src/loops/neighbors.ts +40 -0
- package/src/loops/observe.ts +10 -0
- package/src/loops/orchestrator/author-depth.ts +220 -0
- package/src/loops/orchestrator/author-input-check.ts +82 -0
- package/src/loops/orchestrator/author-ledger.ts +232 -0
- package/src/loops/orchestrator/finish-rate.ts +175 -0
- package/src/loops/orchestrator/overlap.ts +51 -12
- package/src/loops/orchestrator/resync-shadow.ts +227 -0
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +309 -0
- package/src/loops/orchestrator/traffic.ts +134 -13
- package/src/loops/registry.ts +535 -9
- package/src/loops/status-rows.ts +118 -0
- package/src/loops/verdict.ts +17 -5
- package/src/maturity/bot-slash-catalog.ts +66 -0
- package/src/maturity/feature-maturity.ts +9 -5
- package/src/maturity/tui-slash-availability.ts +31 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/dashboard.ts +6 -1
- package/src/nexus/api/http-server.ts +33 -3
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/merge-approvals-gate.ts +1 -1
- package/src/nexus/api/schedules-read.ts +18 -1
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/nexus/api/task-cards-api.ts +44 -5
- package/src/nexus/api/workflows.ts +2 -1
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +51 -0
- package/src/release-loop/checklist.ts +22 -4
- package/src/release-loop/feature-store.ts +71 -32
- package/src/release-loop/landing-freeze.ts +121 -0
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +302 -0
- package/src/release-loop/release-schedule.ts +3 -1
- package/src/seat-dispatch/ceo-commands.ts +2 -2
- package/src/seat-dispatch/seat-ask.ts +16 -3
- package/src/seat-dispatch/seat-questions.ts +58 -0
- package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
- package/src/seat-dispatch/seat-request-ledger.ts +133 -0
- package/src/seat-loop/seat-loop.ts +531 -40
- package/src/self-dev/dev-pipeline.ts +14 -6
- package/src/self-dev/draft-sweep.ts +162 -6
- package/src/self-dev/draft-triage-rules.ts +25 -8
- package/src/self-dev/orchestrate.ts +15 -1
- package/src/self-dev/review-provider-fallback.ts +6 -1
- package/src/self-dev/run-store.ts +73 -1
- package/src/self-dev/run-supervisor.ts +57 -2
- package/src/self-dev/self-orchestrate-runtime.ts +5 -1
- package/src/self-implement/budget-gate.ts +29 -0
- package/src/self-implement/expose-gate.ts +47 -0
- package/src/self-implement/frozen-merges.ts +295 -0
- package/src/self-implement/gate-cli.ts +10 -4
- package/src/self-implement/gate-scope.ts +46 -9
- package/src/self-implement/goal-archive.ts +222 -0
- package/src/self-implement/goal-author.ts +43 -5
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/goal-lookup.ts +147 -0
- package/src/self-implement/heal-intake.ts +197 -0
- package/src/self-implement/host-regate.ts +32 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +299 -48
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +97 -0
- package/src/self-implement/run-ledger.ts +3 -1
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +29 -9
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/session/index.ts +2 -0
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +4 -1
- package/src/task-cards/card-cli.ts +10 -2
- package/src/task-cards/card-store.ts +10 -6
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
- package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +224 -49
- package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
- package/src/telegram-commands.ts +27 -3
- package/src/telegram-loops-command.ts +44 -0
- package/src/telegram.ts +25 -4
- package/src/user-config.ts +146 -18
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/src/workflow-runtime/storage.ts +31 -13
- package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3992-fe5c06d6b31f8b25.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8605-9da4678f8a310d18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-d92e32d51ab5157e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
- /package/apps/pwa/out/_next/static/{D51l8jUBplJrIpWvENsDK → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
|
@@ -296,15 +296,11 @@ export function prepareIsolatedTestEnv(sourceEnv: NodeJS.ProcessEnv, testRoot: s
|
|
|
296
296
|
// resolves the old path through ELANOUS_TEST_HOME rather than the real home.
|
|
297
297
|
delete env.XDG_CONFIG_HOME;
|
|
298
298
|
env.ELANOUS_TEST_HOME = testRoot;
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
// run straight back at their real state while every other signal says the run
|
|
303
|
-
// is contained. Deleting them is not enough either: absence resolves to the
|
|
304
|
-
// default under HOME, which is fine here, but pinning them makes the
|
|
305
|
-
// isolation legible in the child's own environment rather than implied.
|
|
299
|
+
// HOME and ELANOUS_STATE_DIR isolate both the state and default config roots.
|
|
300
|
+
// ELANOUS_CONFIG_DIR is not a config-dir override; discard an inherited value
|
|
301
|
+
// rather than passing an ignored variable that appears to provide isolation.
|
|
306
302
|
env.ELANOUS_STATE_DIR = join(testRoot, 'state');
|
|
307
|
-
env.ELANOUS_CONFIG_DIR
|
|
303
|
+
delete env.ELANOUS_CONFIG_DIR;
|
|
308
304
|
// All ambient execution identities are removed by EXECUTION_ORIGIN_ENV_KEYS
|
|
309
305
|
// above, before the child is spawned; logger API-shape tests need their absence.
|
|
310
306
|
// ⚠️ No `ELANOUS_TEST_DETERMINISTIC` marker is exported. Nothing reads it yet,
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { cpSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, existsSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import ts from 'typescript';
|
|
6
|
+
import { pickRange } from './lib.js';
|
|
7
|
+
|
|
8
|
+
export type Flake = 'stable' | 'flaky' | 'failing';
|
|
9
|
+
export type Mutation = 'caught' | 'survived' | 'n/a';
|
|
10
|
+
export type TestRun = { rc: number | null; pass: number | null; fail: number | null };
|
|
11
|
+
|
|
12
|
+
export function flakeVerdict(runs: Array<{ rc: number | null; pass: number | null; fail: number | null }>): Flake {
|
|
13
|
+
if (runs.length !== 3) throw new Error('flakeVerdict requires exactly 3 runs');
|
|
14
|
+
const passed = runs.map((r) => r.rc === 0 && (r.fail === null || r.fail === 0) && r.pass !== null && r.pass > 0);
|
|
15
|
+
return passed.every(Boolean) ? 'stable' : passed.some(Boolean) ? 'flaky' : 'failing';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function sweepSlice({ totalCostSecs, runsPerDay = 4, days = 7, safety = 1.2, files, costs, start = 0, remaining = files?.length, slotsLeft = runsPerDay * days, minBudgetSecs = 0 }: {
|
|
19
|
+
totalCostSecs: number; runsPerDay?: number; days?: number; safety?: number;
|
|
20
|
+
files?: readonly string[]; costs?: ReadonlyMap<string, number>; start?: number; remaining?: number; slotsLeft?: number; minBudgetSecs?: number;
|
|
21
|
+
}): { budgetSecs: number; slicesNeeded: number } {
|
|
22
|
+
if (![totalCostSecs, runsPerDay, days, safety].every((n) => Number.isFinite(n) && n > 0)) throw new Error('sweepSlice requires positive finite inputs');
|
|
23
|
+
const baseBudget = totalCostSecs / (runsPerDay * days) * safety;
|
|
24
|
+
if (!files) return { budgetSecs: baseBudget, slicesNeeded: Math.ceil(totalCostSecs / baseBudget) };
|
|
25
|
+
if (!Number.isSafeInteger(start) || start < 0 || !Number.isSafeInteger(remaining) || remaining! < 0 || remaining! > files.length
|
|
26
|
+
|| !Number.isSafeInteger(slotsLeft) || slotsLeft < 1 || !Number.isFinite(minBudgetSecs) || minBudgetSecs < 0)
|
|
27
|
+
throw new Error('sweepSlice requires a valid cursor, remaining file count and slots');
|
|
28
|
+
const sliceCount = (budget: number): number => {
|
|
29
|
+
let cursor = start;
|
|
30
|
+
let visited = 0;
|
|
31
|
+
let count = 0;
|
|
32
|
+
while (visited < remaining!) {
|
|
33
|
+
const slice = pickRange(files, cursor, costs ?? new Map(), budget);
|
|
34
|
+
visited += slice.files.length;
|
|
35
|
+
cursor = slice.next;
|
|
36
|
+
count++;
|
|
37
|
+
}
|
|
38
|
+
return count;
|
|
39
|
+
};
|
|
40
|
+
const target = slotsLeft;
|
|
41
|
+
let budgetSecs = Math.max(baseBudget, minBudgetSecs);
|
|
42
|
+
let slicesNeeded = sliceCount(budgetSecs);
|
|
43
|
+
if (slicesNeeded > target) {
|
|
44
|
+
// Whole files cannot be split across slices. Grow the budget until the actual cursor walk fits.
|
|
45
|
+
let low = budgetSecs;
|
|
46
|
+
let high = budgetSecs;
|
|
47
|
+
do {
|
|
48
|
+
high *= 2;
|
|
49
|
+
if (!Number.isFinite(high)) throw new Error('sweepSlice budget overflow');
|
|
50
|
+
} while (sliceCount(high) > target);
|
|
51
|
+
for (let i = 0; i < 48; i++) {
|
|
52
|
+
const mid = (low + high) / 2;
|
|
53
|
+
if (sliceCount(mid) > target) low = mid;
|
|
54
|
+
else high = mid;
|
|
55
|
+
}
|
|
56
|
+
budgetSecs = high;
|
|
57
|
+
slicesNeeded = sliceCount(budgetSecs);
|
|
58
|
+
}
|
|
59
|
+
return { budgetSecs, slicesNeeded };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const repositoryRead = /\b(?:readdirSync|globSync|new\s+Glob)\b|['"]ls-files['"]|\b(?:execFileSync\s*\(\s*['"]git['"]|spawnSync\s*\(\s*['"](?:git|rg)['"])/;
|
|
63
|
+
|
|
64
|
+
/** Static signal only: literal count assertions coupled to a direct repository reader. */
|
|
65
|
+
export function fixedCountAssertions(testPath: string, root: string): Array<{ line: number; count: number }> {
|
|
66
|
+
const repo = realpathSync(resolve(root));
|
|
67
|
+
const testFile = resolve(repo, testPath);
|
|
68
|
+
const inside = (path: string) => path !== repo && !relative(repo, path).startsWith(`..${sep}`)
|
|
69
|
+
&& relative(repo, path) !== '..' && !isAbsolute(relative(repo, path));
|
|
70
|
+
if (!inside(testFile) || realpathSync(testFile) !== testFile) throw new Error('test path outside repository or symlinked');
|
|
71
|
+
const text = readFileSync(testFile, 'utf8');
|
|
72
|
+
const source = ts.createSourceFile(testFile, text, ts.ScriptTarget.Latest, true);
|
|
73
|
+
let readsRepository = repositoryRead.test(text);
|
|
74
|
+
if (!readsRepository) {
|
|
75
|
+
const imports: string[] = [];
|
|
76
|
+
const collectImports = (node: ts.Node): void => {
|
|
77
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) imports.push(node.moduleSpecifier.text);
|
|
78
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
79
|
+
&& node.arguments.length === 1 && ts.isStringLiteral(node.arguments[0]!)) imports.push(node.arguments[0]!.text);
|
|
80
|
+
ts.forEachChild(node, collectImports);
|
|
81
|
+
};
|
|
82
|
+
collectImports(source);
|
|
83
|
+
for (const specifier of imports) {
|
|
84
|
+
if (!/^\.{1,2}\//.test(specifier)) continue;
|
|
85
|
+
const base = resolve(dirname(testFile), specifier);
|
|
86
|
+
const candidates = base.endsWith('.js')
|
|
87
|
+
? [base.replace(/\.js$/, '.ts'), base, `${base}.ts`, join(base, 'index.ts')]
|
|
88
|
+
: [base, `${base}.ts`, join(base, 'index.ts')];
|
|
89
|
+
for (const candidate of candidates) {
|
|
90
|
+
if (!inside(candidate) || !existsSync(candidate) || realpathSync(candidate) !== candidate || !statSync(candidate).isFile()) continue;
|
|
91
|
+
if (repositoryRead.test(readFileSync(candidate, 'utf8'))) readsRepository = true;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
if (readsRepository) break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!readsRepository) return [];
|
|
98
|
+
|
|
99
|
+
const assertions: Array<{ line: number; count: number }> = [];
|
|
100
|
+
const visit = (node: ts.Node): void => {
|
|
101
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.arguments.length === 1
|
|
102
|
+
&& ['toBe', 'toEqual', 'toHaveLength'].includes(node.expression.name.text)
|
|
103
|
+
&& ts.isNumericLiteral(node.arguments[0]!)
|
|
104
|
+
&& /^(?:\d[\d_]*|0[xX][\da-fA-F_]+|0[bB][01_]+|0[oO][0-7_]+)$/.test(node.arguments[0]!.text)) {
|
|
105
|
+
const receiver = node.expression.expression;
|
|
106
|
+
if (ts.isCallExpression(receiver) && ts.isIdentifier(receiver.expression) && receiver.expression.text === 'expect'
|
|
107
|
+
&& receiver.arguments.length === 1) {
|
|
108
|
+
const line = source.getLineAndCharacterOfPosition(receiver.getStart(source)).line + 1;
|
|
109
|
+
const matcherLine = source.getLineAndCharacterOfPosition(node.expression.name.getStart(source)).line + 1;
|
|
110
|
+
if (line === matcherLine && (node.expression.name.text === 'toHaveLength'
|
|
111
|
+
|| /\.(?:length|size)\b|\bcount\b/.test(receiver.arguments[0]!.getText(source)))) {
|
|
112
|
+
assertions.push({ line, count: Number(node.arguments[0]!.text.replaceAll('_', '')) });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
ts.forEachChild(node, visit);
|
|
117
|
+
};
|
|
118
|
+
visit(source);
|
|
119
|
+
return assertions.sort((a, b) => a.line - b.line);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function mutationProbe(file: string, deps: { root: string; run?: (root: string, file: string) => number | null; baselineStable?: boolean }): Mutation {
|
|
123
|
+
if (deps.baselineStable === false) return 'n/a';
|
|
124
|
+
const root = realpathSync(resolve(deps.root));
|
|
125
|
+
const testPath = resolve(root, file);
|
|
126
|
+
const inside = (path: string) => path !== root && !relative(root, path).startsWith(`..${sep}`) && relative(root, path) !== '..' && !isAbsolute(relative(root, path));
|
|
127
|
+
if (!inside(testPath) || realpathSync(testPath) !== testPath) throw new Error('test path outside repository or symlinked');
|
|
128
|
+
const source = readFileSync(testPath, 'utf8');
|
|
129
|
+
// Only direct relative imports into src/ qualify; never mutate the test or an external dependency.
|
|
130
|
+
const imports = source.matchAll(/(?:\bfrom\s*|\bimport\s*\(|\bimport\s*)['"](\.{1,2}\/[^'"]+)['"]/g);
|
|
131
|
+
let target: string | undefined;
|
|
132
|
+
let changed: string | undefined;
|
|
133
|
+
for (const match of imports) {
|
|
134
|
+
const base = resolve(dirname(testPath), match[1]!);
|
|
135
|
+
for (const candidate of [base, base.replace(/\.js$/, '.ts'), `${base}.ts`, join(base, 'index.ts')]) {
|
|
136
|
+
if (!inside(candidate) || !relative(root, candidate).startsWith(`src${sep}`) || !existsSync(candidate) || realpathSync(candidate) !== candidate) continue;
|
|
137
|
+
const original = readFileSync(candidate, 'utf8');
|
|
138
|
+
const mutation = original.includes('return true') ? original.replace('return true', 'return false')
|
|
139
|
+
: original.includes('===') ? original.replace('===', '!==') : undefined;
|
|
140
|
+
if (!mutation) return 'n/a';
|
|
141
|
+
target = candidate;
|
|
142
|
+
changed = mutation;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
if (target) break;
|
|
146
|
+
}
|
|
147
|
+
if (!target || changed === undefined) return 'n/a';
|
|
148
|
+
// Real path up front: on macOS tmpdir is under /var → /private/var, and the symlink guards below compare real paths.
|
|
149
|
+
const temp = realpathSync(mkdtempSync(join(tmpdir(), 'test-diet-mutation-')));
|
|
150
|
+
try {
|
|
151
|
+
const clone = join(temp, 'repo');
|
|
152
|
+
const copied = spawnSync('git', ['clone', '--quiet', '--no-hardlinks', '--', root, clone], { encoding: 'utf8', timeout: 300_000 });
|
|
153
|
+
if (copied.status !== 0) throw new Error(`mutation copy failed: ${copied.error?.message ?? copied.stderr}`);
|
|
154
|
+
// Copy the actual file bytes (including uncommitted fixture edits) into the isolated clone.
|
|
155
|
+
for (const path of [testPath, target]) {
|
|
156
|
+
const destination = join(clone, relative(root, path));
|
|
157
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
158
|
+
if (existsSync(destination) && realpathSync(destination) !== destination) throw new Error('mutation copy destination is symlinked');
|
|
159
|
+
cpSync(path, destination);
|
|
160
|
+
}
|
|
161
|
+
if (!deps.run && existsSync(join(clone, 'package.json'))) {
|
|
162
|
+
const install = spawnSync('bun', ['install', '--frozen-lockfile'], { cwd: clone, timeout: 300_000, stdio: 'ignore' });
|
|
163
|
+
if (install.status !== 0) throw new Error('mutation copy install failed');
|
|
164
|
+
if (relative(root, testPath).startsWith(`apps${sep}pwa${sep}`) && existsSync(join(clone, 'apps/pwa/package.json'))) {
|
|
165
|
+
const pwa = spawnSync('bun', ['install', '--frozen-lockfile'], { cwd: join(clone, 'apps/pwa'), timeout: 300_000, stdio: 'ignore' });
|
|
166
|
+
if (pwa.status !== 0) throw new Error('mutation copy PWA install failed');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const targetInCopy = join(clone, relative(root, target));
|
|
170
|
+
if (existsSync(targetInCopy) && realpathSync(targetInCopy) !== targetInCopy) throw new Error('mutation target in copy is symlinked');
|
|
171
|
+
const testFile = `./${relative(root, testPath)}`;
|
|
172
|
+
const command = existsSync(join(clone, 'scripts/test-deterministic.ts'))
|
|
173
|
+
? ['run', 'scripts/test-deterministic.ts', testFile] : ['test', testFile];
|
|
174
|
+
const runTest = () => deps.run ? { status: deps.run(clone, relative(root, testPath)), stdout: '', stderr: '' }
|
|
175
|
+
: spawnSync('bun', command, { cwd: clone, timeout: 300_000, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
|
176
|
+
if (deps.baselineStable !== true) {
|
|
177
|
+
for (let i = 0; i < 3; i++) {
|
|
178
|
+
const baseline = runTest();
|
|
179
|
+
if (baseline.status === null) throw new Error('baseline test did not complete');
|
|
180
|
+
if (baseline.status !== 0) return 'n/a';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
writeFileSync(targetInCopy, changed);
|
|
184
|
+
const run = runTest();
|
|
185
|
+
if (run.status === null) throw new Error('mutation test did not complete');
|
|
186
|
+
if (!deps.run && run.status !== 0 && !/^\s*[1-9][0-9]* fail\b/m.test(`${run.stdout}\n${run.stderr}`))
|
|
187
|
+
throw new Error('mutation test did not report a failing assertion');
|
|
188
|
+
return run.status === 0 ? 'survived' : 'caught';
|
|
189
|
+
} finally { rmSync(temp, { recursive: true, force: true }); }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (import.meta.main) {
|
|
193
|
+
const result = mutationProbe(process.argv[2]!, { root: process.cwd(), baselineStable: process.argv[3] === 'stable' });
|
|
194
|
+
console.log(result);
|
|
195
|
+
}
|
package/scripts/test-diet/lib.ts
CHANGED
|
@@ -7,12 +7,13 @@ export const DEFAULT_COST_SECS = 10;
|
|
|
7
7
|
export const SLOW_SECS = 60;
|
|
8
8
|
export const HEAVY_MB = 2048;
|
|
9
9
|
|
|
10
|
-
export interface
|
|
10
|
+
export interface Effectiveness { flake: 'stable' | 'flaky' | 'failing'; mutation: 'caught' | 'survived' | 'n/a'; fixedCounts?: number }
|
|
11
|
+
export interface Measurement { file: string; secs: number; rssMb: number | null; rc: number | null; pass: number | null; fail: number | null; reason?: string; effectiveness?: Effectiveness }
|
|
11
12
|
export type Verdict = 'keep' | 'failing' | 'review';
|
|
12
13
|
export type Proposal = 'delete' | 'shrink' | 'rewrite-cheap' | 'move-out-of-gate' | 'investigate';
|
|
13
14
|
export interface Td1Disposition { disposition: string; alternative: string; guards: string; why_slow: string }
|
|
14
|
-
export interface Judged extends Measurement { caught90: number; flags: string[]; verdict: Verdict; proposal?: Proposal | null; basis?: string | null }
|
|
15
|
-
export interface LedgerLine { at: string; range: string; start: number; end: number; next: number; total: number; commit: string; budgetSecs: number; results: Judged[] }
|
|
15
|
+
export interface Judged extends Measurement { caught90: number; flags: string[]; verdict: Verdict; effectiveness?: Effectiveness; proposal?: Proposal | null; basis?: string | null }
|
|
16
|
+
export interface LedgerLine { at: string; range: string; start: number; end: number; next: number; total: number; commit: string; budgetSecs: number; results: Judged[]; sweepVisited?: number; sweepCostSecs?: number; sweepSlices?: number }
|
|
16
17
|
|
|
17
18
|
/** `file\tsecs\t…` (TD1 whole-gate TSV) → seconds per file. */
|
|
18
19
|
export function costTable(tsv: string): Map<string, number> {
|
|
@@ -47,8 +48,13 @@ export function propose(judged: Judged, td1: ReadonlyMap<string, Td1Disposition>
|
|
|
47
48
|
const proposal = reviewed.disposition === 'keep' ? null : reviewed.disposition as Proposal;
|
|
48
49
|
return { ...judged, proposal, basis: proposal ? [reviewed.alternative, reviewed.why_slow, reviewed.guards && `guards: ${reviewed.guards}`].filter(Boolean).join(' · ') : null };
|
|
49
50
|
}
|
|
51
|
+
if (judged.effectiveness?.flake === 'flaky') return { ...judged, proposal: 'investigate', basis: '3회 중 갈림' };
|
|
52
|
+
if (judged.effectiveness?.flake === 'stable' && judged.effectiveness.mutation === 'survived' && judged.caught90 === 0)
|
|
53
|
+
return { ...judged, proposal: 'rewrite-cheap', basis: '돌연변이를 못 잡음' };
|
|
50
54
|
if (judged.verdict === 'failing') return { ...judged, proposal: 'investigate', basis: `failing (rc=${judged.rc}) · caught90=${judged.caught90}` };
|
|
51
55
|
if (judged.verdict === 'review') return { ...judged, proposal: 'shrink', basis: `${judged.flags.join(', ')} · caught90=${judged.caught90}` };
|
|
56
|
+
if (judged.effectiveness?.fixedCounts && judged.effectiveness.fixedCounts > 0)
|
|
57
|
+
return { ...judged, proposal: 'investigate', basis: `저장소 전수 개수 고정 단언 ${judged.effectiveness.fixedCounts}줄 — 관련 시험 묶음 후보` };
|
|
52
58
|
return { ...judged, proposal: null, basis: null };
|
|
53
59
|
}
|
|
54
60
|
|
|
@@ -75,7 +81,9 @@ export function judge(m: Measurement, caught90: number): Judged {
|
|
|
75
81
|
const flags: string[] = [];
|
|
76
82
|
if (m.secs >= SLOW_SECS) flags.push('slow');
|
|
77
83
|
if (m.rssMb !== null && m.rssMb >= HEAVY_MB) flags.push('heavy');
|
|
78
|
-
const verdict: Verdict = m.
|
|
84
|
+
const verdict: Verdict = m.effectiveness?.flake === 'flaky' ? 'review'
|
|
85
|
+
: (m.effectiveness?.flake === 'failing' || (m.effectiveness === undefined && m.rc !== 0)) ? 'failing'
|
|
86
|
+
: flags.length > 0 && caught90 === 0 ? 'review' : 'keep';
|
|
79
87
|
return { ...m, caught90, flags, verdict };
|
|
80
88
|
}
|
|
81
89
|
|
|
@@ -96,19 +104,19 @@ export function appendLedger(root: string, line: LedgerLine): void {
|
|
|
96
104
|
|
|
97
105
|
/** Draft approval card — written as a file for a person to read; nothing is acted on. */
|
|
98
106
|
export function writeCardDraft(root: string, line: LedgerLine): string | null {
|
|
99
|
-
const items = line.results.filter((r) => r.verdict !== 'keep');
|
|
107
|
+
const items = line.results.filter((r) => r.verdict !== 'keep' || r.proposal != null);
|
|
100
108
|
if (items.length === 0) return null;
|
|
101
109
|
const dir = join(root, 'test-diet', 'cards');
|
|
102
110
|
mkdirSync(dir, { recursive: true });
|
|
103
111
|
const path = join(dir, `${line.at.slice(0, 10)}-${line.range.replace(/[^0-9a-z~#-]/gi, '')}.md`);
|
|
104
|
-
const rows = items.map((r) => `| \`${r.file}\` | ${r.verdict} | ${r.secs}s | ${r.rssMb ?? '?'} MB | ${r.caught90} | ${r.flags.join(', ') || '—'} | ${(r.reason ?? '').replace(/\|/g, '/')} |`);
|
|
112
|
+
const rows = items.map((r) => `| \`${r.file}\` | ${r.verdict} | ${r.secs}s | ${r.rssMb ?? '?'} MB | ${r.caught90} | ${r.flags.join(', ') || '—'} | ${(r.reason ?? '').replace(/\|/g, '/')} | ${r.proposal ?? '—'} | ${(r.basis ?? '—').replace(/\|/g, '/')} |`);
|
|
105
113
|
writeFileSync(path, [
|
|
106
114
|
`# 시험 리뷰 초안 — ${line.range} (${line.at.slice(0, 10)})`,
|
|
107
115
|
'',
|
|
108
116
|
'> 초안이다. 아무것도 지우거나 옮기지 않았다. 사람이 읽고 정한다.',
|
|
109
117
|
'',
|
|
110
|
-
'| 파일 | 판정 | 단독 시간 | 피크 메모리 | 90일 fix 커밋 | 표지 | 실패 사유(첫 줄) |',
|
|
111
|
-
'
|
|
118
|
+
'| 파일 | 판정 | 단독 시간 | 피크 메모리 | 90일 fix 커밋 | 표지 | 실패 사유(첫 줄) | 제안 | 근거 |',
|
|
119
|
+
'|---|---|---|---|---|---|---|---|---|',
|
|
112
120
|
...rows,
|
|
113
121
|
'',
|
|
114
122
|
`원장: ${line.results.length}개 중 ${items.length}개 · 측정 판 ${line.commit}`,
|
|
@@ -10,6 +10,7 @@ import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
|
|
|
10
10
|
import { CardStore } from '../../src/task-cards/card-store.js';
|
|
11
11
|
import { GATE_NIGHTLY_AUDITS } from '../release-loop/gate-node.js';
|
|
12
12
|
import { appendLedger, costTable, judge, lastLedgerLine, ledgerPath, nightlyAuditLedgerPath, pickRange, propose, td1Dispositions, writeCardDraft, type LedgerLine, type Measurement, type Td1Disposition } from './lib.js';
|
|
13
|
+
import { fixedCountAssertions, flakeVerdict, sweepSlice, type Mutation, type TestRun } from './effectiveness.js';
|
|
13
14
|
|
|
14
15
|
type Context = { graphId?: string; input: Record<string, unknown>; outputs: Record<string, Record<string, unknown> | null> };
|
|
15
16
|
function context(): Context {
|
|
@@ -24,20 +25,29 @@ function emit(result: Record<string, unknown> & { outcome: 'ok' | 'fail' | 'erro
|
|
|
24
25
|
}
|
|
25
26
|
const git = (args: string[]) => spawnSync('git', args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).stdout ?? '';
|
|
26
27
|
|
|
27
|
-
function pick(ctx: Context): number {
|
|
28
|
-
const budgetSecs = typeof ctx.input.budgetSecs === 'number' && ctx.input.budgetSecs > 0 ? ctx.input.budgetSecs : 1800;
|
|
28
|
+
function pick(ctx: Context, sweep = false): number {
|
|
29
29
|
const files = git(['ls-files', '*.test.ts']).split('\n').filter(Boolean).sort();
|
|
30
30
|
const costs = costTable(readFileSync(join(process.cwd(), 'docs/measurements/td1-whole-gate-mechanical-2026-10-01.tsv'), 'utf8'));
|
|
31
31
|
const last = lastLedgerLine(effectiveInstanceRoot());
|
|
32
32
|
for (const r of last?.results ?? []) costs.set(r.file, r.secs);
|
|
33
|
+
const totalCostSecs = sweep ? files.reduce((sum, file) => sum + (costs.get(file) ?? 10), 0) : 0;
|
|
33
34
|
// `start` re-looks a slice on purpose; otherwise continue from the ledger cursor.
|
|
34
35
|
const startAt = typeof ctx.input.start === 'number' && ctx.input.start >= 0 ? ctx.input.start : last?.next ?? 0;
|
|
36
|
+
const continuing = ctx.input.start === undefined && last?.sweepVisited && last.sweepVisited < files.length && last.next === startAt && last.total === files.length;
|
|
37
|
+
const remaining = continuing ? files.length - last.sweepVisited! : files.length;
|
|
38
|
+
const elapsed = continuing ? last.sweepSlices ?? 0 : 0;
|
|
39
|
+
const slotsLeft = Math.max(1, 28 - elapsed);
|
|
40
|
+
const sweepPlan = sweep && files.length > 0 ? sweepSlice({ totalCostSecs, files, costs, start: startAt, remaining,
|
|
41
|
+
slotsLeft, minBudgetSecs: continuing ? last.budgetSecs : 0 }) : undefined;
|
|
42
|
+
const budgetSecs = sweepPlan?.budgetSecs
|
|
43
|
+
?? (typeof ctx.input.budgetSecs === 'number' && ctx.input.budgetSecs > 0 ? ctx.input.budgetSecs : 1800);
|
|
35
44
|
const range = pickRange(files, startAt, costs, budgetSecs);
|
|
36
|
-
return emit({ outcome: 'ok', summary: `test-diet pick #${range.start}~#${range.end} of ${files.length} (≈${Math.round(range.estimatedSecs)}s)`, ...range, total: files.length, budgetSecs
|
|
45
|
+
return emit({ outcome: 'ok', summary: `test-diet pick #${range.start}~#${range.end} of ${files.length} (≈${Math.round(range.estimatedSecs)}s)`, ...range, total: files.length, budgetSecs,
|
|
46
|
+
...(sweep ? { sweep: true, totalCostSecs, slicesNeeded: sweepPlan?.slicesNeeded, sweepElapsed: elapsed } : {}) });
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
/** One remote run: shallow clone of the mirror, one install, each file alone with its peak memory. */
|
|
40
|
-
function measure(ctx: Context, audit = false): number {
|
|
50
|
+
function measure(ctx: Context, audit = false, effectiveness = false): number {
|
|
41
51
|
const picked = audit ? [...GATE_NIGHTLY_AUDITS] : ctx.outputs.pick?.files;
|
|
42
52
|
if (!Array.isArray(picked) || picked.length === 0) return emit({ outcome: 'fail', summary: 'nothing picked' });
|
|
43
53
|
const host = typeof ctx.input.remote === 'string' ? ctx.input.remote : 'node-b';
|
|
@@ -62,6 +72,25 @@ function measure(ctx: Context, audit = false): number {
|
|
|
62
72
|
// A failing file names its first error, so an environment gap (e.g. no Java on the host) is not read as a test defect.
|
|
63
73
|
' why=""; [ "$rc" -ne 0 ] && why=$(grep -v -E "^\\s*$|^bun test|^ *[0-9]+ \\||^error: *$" "$T/err" | grep -m1 -E "Unable to|Cannot find|not found|ENOENT|EACCES|rror" | tr "\\t" " " | cut -c1-160)',
|
|
64
74
|
' printf "M\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s\\n" "$f" "$e" "${rss:-}" "$rc" "${pass:-}" "${fail:-}" "$why"',
|
|
75
|
+
...(effectiveness && !audit ? [
|
|
76
|
+
' runs="$rc,${pass:-},${fail:-}"',
|
|
77
|
+
' for n in 2 3; do',
|
|
78
|
+
' if [ -n "$TO" ]; then "$TO" -k 10 300 bun run scripts/test-deterministic.ts "./$f" > "$T/out" 2> "$T/err"; else bun run scripts/test-deterministic.ts "./$f" > "$T/out" 2> "$T/err"; fi',
|
|
79
|
+
' rc2=$?',
|
|
80
|
+
' pass2=$(cat "$T/out" "$T/err" | grep -E -o "^ *[0-9]+ pass" | grep -E -o "[0-9]+" | tail -1); fail2=$(cat "$T/out" "$T/err" | grep -E -o "^ *[0-9]+ fail" | grep -E -o "[0-9]+" | tail -1)',
|
|
81
|
+
' runs="$runs;$rc2,${pass2:-},${fail2:-}"',
|
|
82
|
+
' done',
|
|
83
|
+
' stable=1; oldIFS=$IFS; IFS=";"',
|
|
84
|
+
' for entry in $runs; do',
|
|
85
|
+
' status=${entry%%,*}; counts=${entry#*,}; passing=${counts%%,*}; failing=${counts#*,}',
|
|
86
|
+
' [ "$status" = 0 ] && [ "${passing:-0}" -gt 0 ] && [ "${failing:-0}" -eq 0 ] || stable=0',
|
|
87
|
+
' done',
|
|
88
|
+
' IFS=$oldIFS',
|
|
89
|
+
' mutation="n/a"',
|
|
90
|
+
' if [ "$stable" -eq 1 ]; then mutation=$(bun scripts/test-diet/effectiveness.ts "$f" stable 2> "$T/mutation-err") || { cat "$T/mutation-err" >&2; exit 5; }; fi',
|
|
91
|
+
' e=$(( $(date +%s) - s ))',
|
|
92
|
+
' printf "E\\t%s\\t%s\\t%s\\t%s\\n" "$f" "$runs" "$mutation" "$e"',
|
|
93
|
+
] : []),
|
|
65
94
|
"done <<'TEST_DIET_FILES'", ...picked, 'TEST_DIET_FILES',
|
|
66
95
|
].join('\n');
|
|
67
96
|
const run = spawnSync('ssh', ['-o', 'BatchMode=yes', host, 'bash', '-s'], { input: `${script}\n`, encoding: 'utf8', timeout: 4 * 3600_000, maxBuffer: 16 * 1024 * 1024 });
|
|
@@ -73,9 +102,35 @@ function measure(ctx: Context, audit = false): number {
|
|
|
73
102
|
const num = (v: string | undefined) => (v === undefined || v === '' ? null : Number(v));
|
|
74
103
|
return { file: file!, secs: Number(secs), rssMb: num(rss), rc: num(rc), pass: num(pass), fail: num(fail), ...(why ? { reason: why } : {}) };
|
|
75
104
|
});
|
|
105
|
+
if (effectiveness && !audit) {
|
|
106
|
+
const effects = new Map(String(run.stdout).split('\n').filter((l) => l.startsWith('E\t')).map((l) => {
|
|
107
|
+
const [, file, raw, mutation, secs] = l.split('\t');
|
|
108
|
+
const runs: TestRun[] = (raw ?? '').split(';').map((entry) => {
|
|
109
|
+
const [rc, pass, fail] = entry.split(',');
|
|
110
|
+
const num = (value: string | undefined) => value === undefined || value === '' ? null : Number(value);
|
|
111
|
+
return { rc: num(rc), pass: num(pass), fail: num(fail) };
|
|
112
|
+
});
|
|
113
|
+
return [file!, { flake: flakeVerdict(runs), mutation: (['caught', 'survived', 'n/a'].includes(mutation ?? '') ? mutation : 'n/a') as Mutation, secs: Number(secs) }] as const;
|
|
114
|
+
}));
|
|
115
|
+
for (const measurement of measurements) {
|
|
116
|
+
const effect = effects.get(measurement.file);
|
|
117
|
+
if (!effect) continue;
|
|
118
|
+
measurement.effectiveness = { flake: effect.flake, mutation: effect.flake === 'stable' ? effect.mutation : 'n/a' };
|
|
119
|
+
try {
|
|
120
|
+
measurement.effectiveness.fixedCounts = fixedCountAssertions(measurement.file, process.cwd()).length;
|
|
121
|
+
} catch (error) {
|
|
122
|
+
debug.log('test-diet.effectiveness', 'fixed-count-unreadable', {
|
|
123
|
+
file: measurement.file, reason: error instanceof Error ? error.message : String(error),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
debug.log('test-diet.effectiveness', 'judged', { file: measurement.file, ...measurement.effectiveness, secs: effect.secs });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
76
129
|
const complete = measurements.length === picked.length && picked.every((file) => measurements.some((m) => m.file === file));
|
|
77
|
-
const summary = remoteError ??
|
|
78
|
-
|
|
130
|
+
const summary = remoteError ?? (effectiveness && measurements.some((m) => !m.effectiveness)
|
|
131
|
+
? `effectiveness missing for ${measurements.filter((m) => !m.effectiveness).length} file(s)`
|
|
132
|
+
: `measured ${measurements.length}/${picked.length} on ${host} at ${commit.slice(0, 9)}`);
|
|
133
|
+
return emit({ outcome: audit ? complete && !remoteError ? 'ok' : 'error' : effectiveness && (remoteError || measurements.some((m) => !m.effectiveness)) ? 'error' : measurements.length ? 'ok' : 'error',
|
|
79
134
|
summary, host, commit, measurements, ...(remoteError ? { remoteError } : {}) });
|
|
80
135
|
}
|
|
81
136
|
|
|
@@ -123,9 +178,15 @@ export function record(ctx: Context, deps: { createStore?: (root: string) => Pic
|
|
|
123
178
|
if (!p || measurements.length === 0) return emit({ outcome: 'fail', summary: 'nothing measured' });
|
|
124
179
|
const td1 = deps.td1 ?? td1Dispositions(readFileSync(join(process.cwd(), 'docs/measurements/td1-top200-content-review-2026-10-01.tsv'), 'utf8'));
|
|
125
180
|
const results = measurements.map((x) => propose(judge(x, git(['log', '--since=90.days', '--format=%s', '--', x.file]).split('\n').filter((s) => /^fix\b/i.test(s)).length), td1));
|
|
181
|
+
const previous = p.sweep === true ? lastLedgerLine(effectiveInstanceRoot()) : null;
|
|
182
|
+
const continuing = previous?.sweepVisited && previous.sweepVisited < Number(p.total)
|
|
183
|
+
&& previous.next === Number(p.start) && previous.total === Number(p.total);
|
|
184
|
+
const visited = p.sweep === true ? Math.min(Number(p.total), (continuing ? previous.sweepVisited! : 0) + results.length) : undefined;
|
|
185
|
+
const sweepCostSecs = p.sweep === true ? (continuing ? previous.sweepCostSecs ?? 0 : 0) + Number(p.estimatedSecs) : undefined;
|
|
186
|
+
const sweepSlices = p.sweep === true ? Number(p.sweepElapsed ?? (continuing ? previous.sweepSlices ?? 0 : 0)) + 1 : undefined;
|
|
126
187
|
const line = {
|
|
127
188
|
at: new Date().toISOString(), range: `#${p.start}~#${p.end}`, start: Number(p.start), end: Number(p.end), next: Number(p.next), total: Number(p.total),
|
|
128
|
-
commit: String(m?.commit ?? '?'), budgetSecs: Number(p.budgetSecs), results,
|
|
189
|
+
commit: String(m?.commit ?? '?'), budgetSecs: Number(p.budgetSecs), results, ...(visited === undefined ? {} : { sweepVisited: visited, sweepCostSecs, sweepSlices }),
|
|
129
190
|
};
|
|
130
191
|
const root = effectiveInstanceRoot();
|
|
131
192
|
appendLedger(root, line);
|
|
@@ -139,7 +200,7 @@ export function record(ctx: Context, deps: { createStore?: (root: string) => Pic
|
|
|
139
200
|
store = deps.createStore?.(root) ?? new CardStore(root);
|
|
140
201
|
const task = store.createCard({ goalId: `test-diet:${result.file}:${result.proposal}`, title: `시험 다이어트 제안 — ${result.file} · ${result.proposal}` });
|
|
141
202
|
const observation = { proposal: result.proposal, basis: result.basis, guards: td1.get(result.file)?.guards ?? '',
|
|
142
|
-
secs: result.secs, rssMb: result.rssMb, caught90: result.caught90, range: line.range };
|
|
203
|
+
secs: result.secs, rssMb: result.rssMb, caught90: result.caught90, range: line.range, ...(result.effectiveness ? { effectiveness: result.effectiveness } : {}) };
|
|
143
204
|
const fingerprint = createHash('sha256').update(JSON.stringify(observation)).digest('hex');
|
|
144
205
|
const key = `test-diet:${line.commit}:${fingerprint}`;
|
|
145
206
|
if (!task.sections.some((section) => section.key === key)) store.appendSection(task.id, {
|
|
@@ -155,6 +216,12 @@ export function record(ctx: Context, deps: { createStore?: (root: string) => Pic
|
|
|
155
216
|
}
|
|
156
217
|
}
|
|
157
218
|
}
|
|
219
|
+
if (p.sweep === true) {
|
|
220
|
+
const etaDays = visited === Number(p.total) ? 0 : p.slicesNeeded !== undefined
|
|
221
|
+
? Math.ceil((Number(p.slicesNeeded) - 1) / 4)
|
|
222
|
+
: Math.ceil(Math.max(0, Number(p.totalCostSecs) - (sweepCostSecs ?? 0)) / Number(p.budgetSecs) / 4);
|
|
223
|
+
debug.log('test-diet.sweep', 'slice', { start: line.start, end: line.end, total: line.total, etaDays });
|
|
224
|
+
}
|
|
158
225
|
debug.log('test-diet', 'proposed', { range: line.range, proposals });
|
|
159
226
|
const counts = { keep: results.filter((r) => r.verdict === 'keep').length, review: results.filter((r) => r.verdict === 'review').length, failing: results.filter((r) => r.verdict === 'failing').length };
|
|
160
227
|
debug.log('test-diet', 'recorded', { range: line.range, ...counts, card: card !== null });
|
|
@@ -210,7 +277,8 @@ if (import.meta.main) {
|
|
|
210
277
|
process.exitCode = status(effectiveInstanceRoot(), { json: args.includes('--json'), days });
|
|
211
278
|
} else {
|
|
212
279
|
const ctx = context();
|
|
213
|
-
|
|
280
|
+
const flags = process.argv.slice(3);
|
|
281
|
+
process.exitCode = step === 'pick' ? pick(ctx, flags.includes('--sweep')) : step === 'measure' ? measure(ctx, false, flags.includes('--effectiveness')) : step === 'audit' ? measure(ctx, true)
|
|
214
282
|
: step === 'record' ? (ctx.graphId === 'nightly-audit' || Object.hasOwn(ctx.outputs, 'audit') ? recordNightlyAudit(ctx) : record(ctx)) : emit({ outcome: 'error', summary: `unknown step: ${step}` });
|
|
215
283
|
}
|
|
216
284
|
} catch (error) {
|
|
@@ -2,10 +2,10 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import sharp from 'sharp';
|
|
4
4
|
import { Frames, launch, PUBLIC_LEAK_CHECK, sleep } from './lib/cdp.js';
|
|
5
|
-
import { failedApiResponse, judgeRun, judgeScene, worstSceneState, type SceneObservation, type SceneState } from './lib/rehearsal-verdict.js';
|
|
5
|
+
import { EMPTY_STATE_PHRASES, failedApiResponse, judgeRun, judgeScene, worstSceneState, type SceneObservation, type SceneState } from './lib/rehearsal-verdict.js';
|
|
6
|
+
export { EMPTY_STATE_PHRASES } from './lib/rehearsal-verdict.js';
|
|
6
7
|
|
|
7
8
|
const TITLES = ['문서 아키텍처', '라이브 트레이스', '루프 에이전트', '그래프 편집기', '마법사 → 마켓', 'PTY 인텔리전스'];
|
|
8
|
-
export const EMPTY_STATE_PHRASES = ['지금 도는 런이 없습니다', '마법사가 아직 돌지 않았습니다', '터미널을 기다리는 중', '판단을 기다리는 중', '못 읽음'] as const;
|
|
9
9
|
|
|
10
10
|
export function sceneSnapshotScript(): string {
|
|
11
11
|
return `(()=>{const sections=[...document.querySelectorAll('[data-inside-scene]')]; const visible=sections.filter(s=>!s.hidden && s.getClientRects().length>0); const text=visible.length===1?visible[0].innerText:''; return {sectionsInDom:sections.length,visibleScene:visible.length===1?Number(visible[0].getAttribute('data-inside-scene')):null,textLength:text.length,emptyStates:${JSON.stringify(EMPTY_STATE_PHRASES)}.filter(phrase=>text.includes(phrase))}})()`;
|
|
@@ -43,6 +43,7 @@ export type SceneResult = SceneObservation & { verdict: 'ok' | 'broken' | 'unver
|
|
|
43
43
|
/** A scene the demo hides is not a pass — it was not seen. OP 10-03: ⑤ (wizard → market) is the 10-08 highlight. */
|
|
44
44
|
export const UNVERIFIED_REASON = '시연 모드에서 숨긴 장면 — WIZ1 착지 뒤 실물 필요';
|
|
45
45
|
const CIRCLED = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨'];
|
|
46
|
+
export const EMPTY_STATE_PHRASES = ['지금 도는 런이 없습니다', '마법사가 아직 돌지 않았습니다', '터미널을 기다리는 중', '판단을 기다리는 중', '못 읽음', '지금 PTY 판단이 없습니다'] as const;
|
|
46
47
|
|
|
47
48
|
export function judgeScene(obs: SceneObservation): SceneResult {
|
|
48
49
|
if (obs.hiddenByDemo) return { ...obs, verdict: 'unverified', reasons: [UNVERIFIED_REASON] };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { downloadAudio, splitAudio } from '../../skills/youtube-master/src/audio.js';
|
|
5
|
+
import { transcribeChunks } from '../../skills/youtube-master/src/stt.js';
|
|
6
|
+
import { which } from '../../skills/youtube-master/src/util.js';
|
|
7
|
+
import { judge } from '../../src/llm/judge-layer.js';
|
|
8
|
+
import {
|
|
9
|
+
dispatchYoutubeTranscript, extractYoutubeVideoId, type YoutubeTranscriptResult,
|
|
10
|
+
} from '../../src/skills/tools/youtube-transcript.js';
|
|
11
|
+
|
|
12
|
+
type Summary = { verdict: string; keyPoints: string[]; cards: string[] };
|
|
13
|
+
type StudyNote = { concepts: string[]; terms: { term: string; definition: string }[]; steps: string[]; questions: string[] };
|
|
14
|
+
|
|
15
|
+
const oneItem = (item: unknown): item is string =>
|
|
16
|
+
typeof item === 'string' && !!item.trim() &&
|
|
17
|
+
!/[\r\n\u2028\u2029]/u.test(item) &&
|
|
18
|
+
!/^(?:#{1,6}(?:\s|$)|[-*+]\s|\d+[.)]\s|>)/u.test(item.trim());
|
|
19
|
+
const lines = (items: unknown): items is string[] =>
|
|
20
|
+
Array.isArray(items) && items.every(oneItem);
|
|
21
|
+
type Result =
|
|
22
|
+
| { outcome: 'ok'; path: string; videoId: string }
|
|
23
|
+
| { outcome: 'no-transcript'; videoId: string; reason?: 'stt-unavailable' }
|
|
24
|
+
| { outcome: 'invalid-video-id' };
|
|
25
|
+
|
|
26
|
+
function parseSummary(value: unknown): Summary | null {
|
|
27
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
28
|
+
const v = value as Record<string, unknown>;
|
|
29
|
+
if (!oneItem(v.verdict) ||
|
|
30
|
+
!lines(v.keyPoints) || v.keyPoints.length < 5 || v.keyPoints.length > 7 ||
|
|
31
|
+
!lines(v.cards) || v.cards.length === 0 ||
|
|
32
|
+
!v.cards.every(card => /^Q: \S.*\? A: \S.*$/u.test(card.trim()))) return null;
|
|
33
|
+
return { verdict: v.verdict.trim(), keyPoints: v.keyPoints.map(s => s.trim()), cards: v.cards.map(s => s.trim()) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseStudyNote(value: unknown): StudyNote | null {
|
|
37
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
38
|
+
const v = value as Record<string, unknown>;
|
|
39
|
+
if (!lines(v.concepts) || v.concepts.length === 0 ||
|
|
40
|
+
!Array.isArray(v.terms) || v.terms.length === 0 ||
|
|
41
|
+
!v.terms.every((entry: unknown) => entry && typeof entry === 'object' && !Array.isArray(entry) &&
|
|
42
|
+
oneItem((entry as Record<string, unknown>).term) && oneItem((entry as Record<string, unknown>).definition)) ||
|
|
43
|
+
!lines(v.steps) || v.steps.length === 0 ||
|
|
44
|
+
!lines(v.questions) || v.questions.length < 3 || v.questions.length > 5) return null;
|
|
45
|
+
return {
|
|
46
|
+
concepts: v.concepts.map(s => s.trim()),
|
|
47
|
+
terms: (v.terms as { term: string; definition: string }[]).map(({ term, definition }) => ({ term: term.trim(), definition: definition.trim() })),
|
|
48
|
+
steps: v.steps.map(s => s.trim()), questions: v.questions.map(s => s.trim()),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function runYoutubeSummary(
|
|
53
|
+
url: string,
|
|
54
|
+
stateRoot: string,
|
|
55
|
+
deps: {
|
|
56
|
+
transcript?: (url: string) => Promise<YoutubeTranscriptResult>;
|
|
57
|
+
binary?: (name: 'yt-dlp' | 'ffmpeg') => string;
|
|
58
|
+
download?: (url: string, dir: string) => Promise<string>;
|
|
59
|
+
split?: (audio: string, dir: string, seconds: number) => Promise<string[]>;
|
|
60
|
+
stt?: (chunks: string[]) => Promise<{ transcript: string }>;
|
|
61
|
+
call?: (args: { prompt: string; signal?: AbortSignal; provider: string; model: string }) => Promise<{ text: string }>;
|
|
62
|
+
format?: unknown;
|
|
63
|
+
} = {},
|
|
64
|
+
): Promise<Result> {
|
|
65
|
+
const format = deps.format === undefined ? 'summary' : deps.format;
|
|
66
|
+
if (format !== 'summary' && format !== 'study-note') throw new Error('input.format must be summary or study-note');
|
|
67
|
+
const videoId = extractYoutubeVideoId(url);
|
|
68
|
+
if (!videoId) return { outcome: 'invalid-video-id' };
|
|
69
|
+
const transcript = await (deps.transcript ?? (source => dispatchYoutubeTranscript({ url: source })))(url);
|
|
70
|
+
let text = transcript.text;
|
|
71
|
+
if (!text.trim() || transcript.provider === 'none') {
|
|
72
|
+
try {
|
|
73
|
+
for (const name of ['yt-dlp', 'ffmpeg'] as const) (deps.binary ?? which)(name);
|
|
74
|
+
} catch {
|
|
75
|
+
return { outcome: 'no-transcript', videoId, reason: 'stt-unavailable' };
|
|
76
|
+
}
|
|
77
|
+
const dir = mkdtempSync(join(tmpdir(), 'youtube-summary-stt-'));
|
|
78
|
+
try {
|
|
79
|
+
const audio = await (deps.download ?? downloadAudio)(url, dir);
|
|
80
|
+
const chunks = await (deps.split ?? splitAudio)(audio, dir, 25 * 60);
|
|
81
|
+
text = (await (deps.stt ?? transcribeChunks)(chunks)).transcript;
|
|
82
|
+
} finally {
|
|
83
|
+
rmSync(dir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
if (!text.trim()) return { outcome: 'no-transcript', videoId };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (format === 'study-note') {
|
|
89
|
+
const decision = await judge({
|
|
90
|
+
site: 'video.youtube-summary',
|
|
91
|
+
prompt: `Create a study note only from the supplied YouTube transcript. Do not invent facts. Return ONLY JSON with exactly these fields: {"concepts":["concise concept explanations"],"terms":[{"term":"term","definition":"definition"}],"steps":["ordered explanation of the subject"],"questions":["3 to 5 self-check questions"]}. Include at least one concept, term and step, and 3 to 5 questions. Keep every item on one line. Write in the transcript's language.\n\nTranscript for ${videoId}:\n${text}`,
|
|
92
|
+
schema: parseStudyNote,
|
|
93
|
+
...(deps.call ? { call: deps.call, resolveRoleProvider: () => ({ provider: { name: 'fake' }, model: 'fake' }) } : {}),
|
|
94
|
+
});
|
|
95
|
+
if (!decision.ok) throw new Error(`YouTube study-note judge failed: ${decision.reason}`);
|
|
96
|
+
const { concepts, terms, steps, questions } = decision.value;
|
|
97
|
+
const cell = (s: string) => s.replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
|
|
98
|
+
const markdown = `# YouTube study note — ${videoId}\n\nSource: https://www.youtube.com/watch?v=${videoId}\n\n## 개념 정리\n${concepts.map(s => `- ${s}`).join('\n')}\n\n## 용어 표\n| 용어 | 뜻 |\n| --- | --- |\n${terms.map(({ term, definition }) => `| ${cell(term)} | ${cell(definition)} |`).join('\n')}\n\n## 단계별 설명\n${steps.map((s, index) => `${index + 1}. ${s}`).join('\n')}\n\n## 스스로 확인 문제\n${questions.map((s, index) => `${index + 1}. ${s}`).join('\n')}\n`;
|
|
99
|
+
const path = join(resolve(stateRoot), 'youtube-summary', `${videoId}.md`);
|
|
100
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
101
|
+
writeFileSync(path, markdown);
|
|
102
|
+
return { outcome: 'ok', path, videoId };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const decision = await judge({
|
|
106
|
+
site: 'video.youtube-summary',
|
|
107
|
+
prompt: `Summarize only the supplied YouTube transcript. Do not invent facts. Return ONLY JSON with exactly these fields: {"verdict":"one-line conclusion","keyPoints":["5 to 7 concise key points"],"cards":["Q: A question? A: Its answer."]}. Include one or more cards, each on one line in this Q:/A: format. Write in the transcript's language.\n\nTranscript for ${videoId}:\n${text}`,
|
|
108
|
+
schema: parseSummary,
|
|
109
|
+
...(deps.call ? { call: deps.call, resolveRoleProvider: () => ({ provider: { name: 'fake' }, model: 'fake' }) } : {}),
|
|
110
|
+
});
|
|
111
|
+
if (!decision.ok) throw new Error(`YouTube summary judge failed: ${decision.reason}`);
|
|
112
|
+
const { verdict, keyPoints, cards } = decision.value;
|
|
113
|
+
const markdown = `# YouTube summary — ${videoId}\n\nSource: https://www.youtube.com/watch?v=${videoId}\n\n## One-line verdict\n${verdict}\n\n## Key points\n${keyPoints.map(point => `- ${point}`).join('\n')}\n\n## Cards\n${cards.map((card, index) => `### Card ${index + 1}\n${card}`).join('\n\n')}\n`;
|
|
114
|
+
const path = join(resolve(stateRoot), 'youtube-summary', `${videoId}.md`);
|
|
115
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
116
|
+
writeFileSync(path, markdown);
|
|
117
|
+
return { outcome: 'ok', path, videoId };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (import.meta.main) {
|
|
121
|
+
try {
|
|
122
|
+
const contextPath = process.env.ELANOUS_GRAPH_CONTEXT;
|
|
123
|
+
if (!contextPath) throw new Error('ELANOUS_GRAPH_CONTEXT required');
|
|
124
|
+
const context = JSON.parse(readFileSync(contextPath, 'utf8')) as { graphId?: string; nodeId?: string; input?: { url?: unknown; format?: unknown } };
|
|
125
|
+
if (context.graphId !== 'youtube-summary' || context.nodeId !== 'summary') throw new Error('youtube-summary graph context mismatch');
|
|
126
|
+
if (typeof context.input?.url !== 'string' || !context.input.url.trim()) throw new Error('input.url required');
|
|
127
|
+
const root = resolve(contextPath, '..', '..', '..', '..');
|
|
128
|
+
console.log(JSON.stringify(await runYoutubeSummary(context.input.url, root, { format: context.input.format })));
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.log(JSON.stringify({ outcome: 'fail', reason: error instanceof Error ? error.message : String(error) }));
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
}
|
|
133
|
+
}
|