elanous 0.2.4 → 0.2.6
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/README.md +7 -6
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5080-1613752891fcadb4.js → 2984-977cfa339fcd65a5.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8142-011e1f474efb8b33.js → 3190-e9282753e7ebde85.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3995-bee512a7063036ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 4240-7153881b0b377306.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +25 -0
- package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3403.4bc97b4066945a94.js → 5329.8b7e4bca684b6eec.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5791-bb82743fa6299562.js → 763-7d0e1913b5074e98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7633.0654fe6ea259c1ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → 8d0d9faf-97887ff99713b84e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 91ff6dda-3b33d42d20657429.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +1 -0
- package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-e5bea36f183b25e6.js → webpack-37b8bf78a3c6eec5.js} +1 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +1 -0
- package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_buildManifest.js +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +29 -27
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +29 -27
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +29 -27
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +26 -24
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +26 -24
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +26 -24
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +29 -27
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +29 -27
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +29 -27
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/icons/elanous-16.png +0 -0
- package/apps/pwa/out/icons/elanous-180.png +0 -0
- package/apps/pwa/out/icons/elanous-192.png +0 -0
- package/apps/pwa/out/icons/elanous-32.png +0 -0
- package/apps/pwa/out/icons/elanous-512.png +0 -0
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +26 -24
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +29 -27
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +29 -27
- package/apps/pwa/out/manifest.webmanifest +5 -2
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +26 -24
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +26 -24
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +29 -27
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +29 -27
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +27 -25
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +29 -27
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +26 -24
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +29 -27
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +29 -27
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +29 -27
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +29 -27
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +29 -27
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +26 -24
- package/apps/pwa/out/sw-strategies.js +44 -0
- package/apps/pwa/out/sw.js +6 -44
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +29 -27
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +29 -27
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +29 -27
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +29 -27
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +29 -27
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +29 -27
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +29 -27
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +29 -27
- package/catalog/resources.yaml +6 -6
- package/docs/goal-context/steward-directives-2026-09-28.md +35 -37
- package/graphs/landing/landing-heal.yaml +1 -1
- package/graphs/release/recipes.yaml +11 -1
- package/graphs/release/release-loop.yaml +9 -2
- package/graphs/video/README.md +1 -0
- package/graphs/video/explainer-line.yaml +58 -0
- package/package.json +3 -1
- package/plugins/card-followup/README.md +33 -0
- package/plugins/card-followup/examples/card-sample.json +7 -0
- package/plugins/card-followup/graphs/card-followup.yaml +16 -0
- package/plugins/card-followup/graphs/recipes.yaml +12 -0
- package/plugins/card-followup/graphs/run-step.ts +172 -0
- package/plugins/card-followup/plugin.json +25 -0
- package/plugins/geo-check/README.md +13 -0
- package/plugins/geo-check/examples/brand-sample.json +8 -0
- package/plugins/geo-check/graphs/geo-check.yaml +20 -0
- package/plugins/geo-check/graphs/recipes.yaml +18 -0
- package/plugins/geo-check/graphs/run-step.ts +177 -0
- package/plugins/geo-check/plugin.json +24 -0
- package/plugins/job-coach/.codex-plugin/plugin.json +18 -1
- package/plugins/job-coach/README.md +42 -0
- package/plugins/job-coach/connectors/ncs/server.ts +4 -3
- package/plugins/job-coach/graphs/gap-judge.ts +44 -0
- package/plugins/job-coach/graphs/recipes.yaml +27 -0
- package/plugins/job-coach/graphs/report-enterprise.yaml +28 -0
- package/plugins/job-coach/graphs/run-step.ts +211 -17
- package/plugins/job-coach/plugin.json +16 -3
- package/plugins/job-coach/skills/enterprise-ai-fit/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-competency/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-needs/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-roadmap/SKILL.md +8 -0
- package/plugins/job-coach/skills/enterprise-task-analysis/SKILL.md +8 -0
- package/plugins/job-coach/skills/interview-to-profile/SKILL.md +18 -1
- package/scripts/backup/elanous-backup.sh +4 -1
- package/scripts/build-docs-site.ts +4 -3
- package/scripts/coord-channel-watch.sh +23 -4
- package/scripts/coord-post.sh +49 -12
- package/scripts/coord-tracks.json +5 -5
- package/scripts/daemon-port-baseline.txt +1 -2
- package/scripts/eval-bot-vs-grok.ts +79 -0
- package/scripts/eval-grok-live-baseline.ts +36 -0
- package/scripts/fixtures/front-route-corpus.sample.json +17 -0
- package/scripts/fleet/node-setup.sh +46 -7
- package/scripts/landing-heal/must-fix-match.ts +26 -0
- package/scripts/landing-heal/run-mission.ts +114 -20
- package/scripts/lib/android-test-device.ts +32 -0
- package/scripts/lib/front-route-measurement.ts +131 -0
- package/scripts/lib/tui-sim-keys.ts +42 -0
- package/scripts/measure-front-route-classifier.ts +56 -0
- package/scripts/media/ocr-leak-scan.sh +21 -0
- package/scripts/public-export.ts +2 -3
- package/scripts/public-import-pr.ts +80 -6
- package/scripts/publish-internal-dist.ts +2 -3
- package/scripts/red-sweep-diff.ts +1 -1
- package/scripts/release-loop/auto-approve-node.ts +72 -0
- package/scripts/release-loop/checklist-gate-node.ts +35 -0
- package/scripts/release-loop/gate-diff.ts +29 -0
- package/scripts/release-loop/gate-node.ts +366 -45
- package/scripts/release-loop/publish-node.ts +7 -3
- package/scripts/release-loop/release-docs.ts +20 -13
- package/scripts/release-loop/shard-plan.ts +47 -0
- package/scripts/release-loop/test-durations.ts +63 -0
- package/scripts/release-loop/upgrade-node.ts +3 -2
- package/scripts/test-audit-coverage-per-file.sh +16 -0
- package/scripts/test-audit-meaningless.ts +88 -0
- package/scripts/test-audit-overlap-and-weak.ts +73 -0
- package/scripts/test-deterministic.ts +27 -5
- package/scripts/tui-sim.ts +59 -14
- package/scripts/ux/bump-installed.sh +20 -0
- package/scripts/ux/lib/cdp.ts +72 -0
- package/scripts/ux/record-page.ts +53 -0
- package/skills/explainer-video/SKILL.md +117 -0
- package/skills/explainer-video/engine/brand/elanous-mark-on-dark.svg +1 -0
- package/skills/explainer-video/engine/build.mjs +431 -0
- package/skills/explainer-video/engine/reel.mjs +128 -0
- package/skills/explainer-video/engine/reel.sh +8 -0
- package/skills/explainer-video/engine/vo.ts +79 -0
- package/skills/explainer-video/examples/elanous-0.2.5/script.json +155 -0
- package/src/agent/nested-agent-env.ts +1 -0
- package/src/agent-mission/driver.ts +95 -16
- package/src/agent-mission/mission-cli.ts +6 -0
- package/src/agent-mission/resource-ladder.ts +73 -25
- package/src/auth/acp-token.ts +16 -0
- package/src/auth/github-app-token.ts +112 -0
- package/src/autopilot/gh-rest-fallback.ts +177 -0
- package/src/autopilot/mission-origin.ts +20 -4
- package/src/autopilot/mission-phase-track.ts +11 -1
- package/src/autopilot/pr-manager.ts +51 -4
- package/src/autopilot/track-agent.ts +3 -3
- package/src/autopilot/track-courier.ts +2 -2
- package/src/boot/acp-server.ts +2 -16
- package/src/boot/daemon-tools/web-search.ts +2 -1
- package/src/boot/ws-bridge.ts +11 -0
- package/src/chat/index.ts +37 -8
- package/src/chat/log-scroll-key.ts +12 -0
- package/src/chat/submit-trace.ts +30 -0
- package/src/cli/agent-cli.ts +7 -2
- package/src/cli/decisions-cli.ts +94 -0
- package/src/cli/doctor-cli.ts +40 -54
- package/src/cli/doctor-readiness.ts +16 -24
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/mcp-call.ts +9 -0
- package/src/cli/mcp-gateway-cli.ts +7 -3
- package/src/cli/nexus-ios-bind.ts +25 -8
- package/src/cli/phone-link.ts +117 -0
- package/src/cli/plugin-cli.ts +136 -11
- package/src/cli/pr-lineage.ts +2 -1
- package/src/cli/private-ledger-commands.ts +31 -0
- package/src/cli/pwa-start.ts +3 -0
- package/src/cli/release-cli.ts +97 -7
- package/src/cli/self-update.ts +7 -4
- package/src/cli/start-cli.ts +33 -14
- package/src/dashboard/acp-boot.ts +3 -0
- package/src/dashboard/index.ts +62 -13
- package/src/dashboard/input/no-provider-reply.ts +13 -0
- package/src/dashboard/slash-runtime/unknown-slash-reply.ts +78 -0
- package/src/dashboard/stale-install-check.ts +77 -0
- package/src/decisions/decision-ledger.ts +404 -0
- package/src/domains/outbound-alert.ts +13 -7
- package/src/domains/telegram-kind-route.ts +21 -0
- package/src/execution-loop/dispatch-task.ts +139 -0
- package/src/execution-loop/gate-decision-summary.ts +29 -0
- package/src/execution-loop/gates.ts +197 -0
- package/src/field/field-media.ts +245 -0
- package/src/graph-kinds/installed-plugin-nodes.ts +95 -0
- package/src/graph-kinds/plugin-nodes.ts +10 -2
- package/src/graph-runner/runner.ts +35 -5
- package/src/grounding/token.ts +12 -6
- package/src/harness/harness-cli-command.ts +68 -9
- package/src/harness/harness-pod-dispatch.ts +8 -2
- package/src/harness/harness-stop.ts +147 -18
- package/src/harness/self-send-decide.ts +26 -17
- package/src/harness/self-send-target.ts +22 -1
- package/src/harness/worktree-branch-prefix.ts +4 -3
- package/src/index.ts +50 -13
- package/src/instance/resolve.ts +6 -0
- package/src/intake-plane/collect-telegram-saved.ts +5 -0
- package/src/intake-plane/digest.ts +11 -2
- package/src/live/detail-switch.ts +8 -3
- package/src/llm/usable-llm.ts +32 -0
- package/src/loops/registry.ts +27 -5
- package/src/market/publish.ts +59 -3
- package/src/native-tool-catalog.ts +2 -2
- package/src/nexus/api/field-uploads.ts +143 -0
- package/src/nexus/api/graph-approvals.ts +174 -0
- package/src/nexus/api/harness-api.ts +219 -5
- package/src/nexus/api/http-server.ts +69 -6
- package/src/nexus/api/log-fabric.ts +10 -1
- package/src/nexus/api/mcp-oauth-hint.ts +10 -9
- package/src/nexus/api/merge-approvals.ts +2 -1
- package/src/nexus/api/meta-api.ts +14 -0
- package/src/nexus/api/plugins-credentials-api.ts +47 -0
- package/src/nexus/api/plugins-market.ts +87 -3
- package/src/nexus/api/pod-credential-api.ts +68 -14
- package/src/nexus/api/public-routes.ts +7 -0
- package/src/nexus/api/trace.ts +57 -6
- package/src/nexus/boot/register-mcp-clients.ts +99 -14
- package/src/nexus/index.ts +33 -32
- package/src/nexus/outbound/router.ts +1 -1
- package/src/notify/llm-budget-alert.ts +119 -0
- package/src/onboarding.ts +25 -2
- package/src/plugins/core/host.ts +14 -0
- package/src/plugins/install/market-fetch.ts +4 -3
- package/src/plugins/install/plugin-credentials.ts +106 -0
- package/src/plugins/install/plugin-install.ts +163 -32
- package/src/plugins/maker/plugin-maker.ts +208 -0
- package/src/prompt-library/universal-preamble.ts +8 -2
- package/src/relay/channel.ts +184 -0
- package/src/relay/mux.ts +182 -0
- package/src/relay/test-vectors.json +29 -0
- package/src/release-loop/checklist.ts +217 -0
- package/src/release-loop/release-note.ts +6 -3
- package/src/seat-address/seat-address.ts +34 -0
- package/src/self-dev/dev-cli.ts +40 -20
- package/src/self-dev/dev-pipeline.ts +77 -1
- package/src/self-implement/gate-baseline.ts +3 -1
- package/src/self-implement/goal-author.ts +18 -1
- package/src/self-implement/host-regate.ts +6 -2
- package/src/self-implement/orchestrator.ts +7 -2
- package/src/self-implement/run-ledger-compact.ts +1 -1
- package/src/self-implement/seams.ts +4 -2
- package/src/session/index.ts +23 -10
- package/src/skills/tools/dev-harness.ts +42 -0
- package/src/steward/steward-cards.ts +45 -0
- package/src/steward/triage.ts +50 -14
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +61 -3
- package/src/task-orchestrator/surfaces/pod-bun-cache.ts +18 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +36 -6
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +39 -9
- package/src/task-orchestrator/surfaces/pod-source-bundle.ts +16 -1
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +51 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +172 -28
- package/src/telegram-report.ts +12 -6
- package/src/telegram.ts +108 -1
- package/src/tui-client/acp-transport-ws-client.ts +72 -5
- package/src/tui.ts +3 -2
- package/src/user-config.ts +32 -2
- package/src/util/spawn-sync-output.ts +15 -0
- package/src/version/code-revision.ts +1 -1
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/explainer.ts +213 -0
- package/src/video-pipeline/walk-line.ts +3 -2
- package/src/web-search/free.ts +77 -0
- package/src/web-search/index.ts +2 -0
- package/src/workflow-runtime/deps-bridge.ts +4 -3
- package/src/workflow-runtime/plugin-kind-node.ts +32 -4
- package/src/workflow-runtime/types.ts +2 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1191-7b13a24117a689fa.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.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/2375-3fcae5186c64fe6b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
- package/apps/pwa/out/_next/static/chunks/319-d37df1b6e0cbeff4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4836-3c93795ab834381e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6090-1bd6077591603947.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7457-f5cb1cc8db9f7453.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.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-f74bf14d67ad98f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-0059d82bdb4812bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-52467f244755917f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.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-21c3261af6ab377c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-1991445b8ea86274.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-4ebfaee2369fe248.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-9fa879ebf5959096.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-ca7c3cc305c3285a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9951930add3d2051.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-4472ddf1e2458e69.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4de08d4b766f134b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7a975e2b9db1dfb7.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/intake/page-49270714ef35bb96.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-33fbdf353e2a5982.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-259f122e2dd64317.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-65b82726753658e1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-4b8df3748dc06285.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-112f7e477bda77fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-542a5c0006da637c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-63ffb347831262ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-98e4e359d97263b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-97e24ac675ce576e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-ac6391f26391d89d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9f45c9d45ffc43bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-36995b9cb5d98218.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0c795b577988d63f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-b4311e2f71c6735a.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-eb2381074df20dba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-b01336e64eba01ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7af7f21c1f2858ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-a98ecd364586c89b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-57f0502dfbfa089b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-3467d2e1abd8a14a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-7842ca23d53ba682.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-ae9ac882912b0198.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-3a0d72e3f783bcef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-f3b39fe8358c2a97.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-5a22af3181ac2afb.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-1457627bb302a22c.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/css/e66b7f5efc8df3f3.css +0 -1
- /package/apps/pwa/out/_next/static/{zoQX5iTvXOsIvCjxCe1Ji → qMI0jnlZaBjjm4aIDYNGM}/_ssgManifest.js +0 -0
|
@@ -1,26 +1,27 @@
|
|
|
1
|
+
import { debug } from '../../src/debug/log.js';
|
|
1
2
|
import type { ReleaseManifest } from '../../src/release-loop/manifest.js';
|
|
2
3
|
|
|
3
|
-
/** Render
|
|
4
|
+
/** Render public IN lines only; keep private titles and PR numbers out of release notes. */
|
|
4
5
|
export function renderReleaseNotes(manifest: ReleaseManifest, version: string): string {
|
|
5
6
|
const sections = [
|
|
6
7
|
{ heading: 'Behavior changes', kinds: ['feat', 'fix'] },
|
|
7
8
|
{ heading: 'Security', kinds: ['security'] },
|
|
8
|
-
{ heading: 'Internal', kinds: ['internal'] },
|
|
9
|
-
{ heading: 'Other changes', kinds: ['unknown'] },
|
|
10
9
|
] as const;
|
|
11
10
|
const lines = [`# ${version}`, ''];
|
|
11
|
+
let shown = 0;
|
|
12
12
|
for (const section of sections) {
|
|
13
|
-
const entries = manifest.in.filter((entry) => section.kinds.some((kind) => kind ===
|
|
13
|
+
const entries = manifest.in.filter((entry) => section.kinds.some((kind) => kind === entry.kind) && entry.line);
|
|
14
14
|
if (!entries.length) continue;
|
|
15
15
|
lines.push(`## ${section.heading}`, '');
|
|
16
16
|
for (const entry of entries) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
? `- ${label}`
|
|
20
|
-
: `- ${label} ([#${entry.prNumber}](https://github.com/ElanvitalAI/elanous/pull/${entry.prNumber}))`);
|
|
17
|
+
lines.push(`- ${entry.line!.replace(/\r?\n/g, ' ').trim()}`);
|
|
18
|
+
shown++;
|
|
21
19
|
}
|
|
22
20
|
lines.push('');
|
|
23
21
|
}
|
|
22
|
+
const omitted = manifest.in.length - shown;
|
|
23
|
+
if (omitted) lines.push(`Plus ${omitted} internal ${omitted === 1 ? 'change' : 'changes'}.`);
|
|
24
|
+
debug.log('release-loop.docs', 'notes-rendered', { version, shown, omitted });
|
|
24
25
|
return `${lines.join('\n').trimEnd()}\n`;
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -33,8 +34,8 @@ export function flipNextReleaseMarkers(text: string, version: string): string {
|
|
|
33
34
|
* Fold notes that landed before the release loop (a PR appending its own line, a headline paragraph)
|
|
34
35
|
* into the rendered notes instead of refusing or overwriting them. Paragraphs before any section stay
|
|
35
36
|
* under the title (the headline slot); list lines the cutoff did not render keep their own `##` section
|
|
36
|
-
* (lines outside any section go to «Also in this release»). A
|
|
37
|
-
*
|
|
37
|
+
* (lines outside any section go to «Also in this release»). A hand-written link cannot be identified
|
|
38
|
+
* as generator output from its shape alone. Folding its own output again gives the same bytes.
|
|
38
39
|
*/
|
|
39
40
|
export function foldPreLandedNotes(existing: string, rendered: string): { text: string; folded: number } {
|
|
40
41
|
const key = (line: string) => line.trim().replace(/\s*\(\[#\d+\]\([^)]*\)\)\s*$/, '').trim();
|
|
@@ -46,12 +47,14 @@ export function foldPreLandedNotes(existing: string, rendered: string): { text:
|
|
|
46
47
|
const alreadyRendered = (line: string, section: string | null) => section === null ? renderedAll.has(key(line)) : (renderedBySection.get(section)?.has(key(line)) ?? false);
|
|
47
48
|
const paragraphs: string[] = [];
|
|
48
49
|
const sections = new Map<string, string[]>();
|
|
50
|
+
const [title, ...rest] = rendered.trimEnd().split('\n');
|
|
51
|
+
const footer = rest.find((line) => /^Plus \d+ internal changes?\.$/.test(line));
|
|
49
52
|
const DEFAULT = '## Also in this release';
|
|
50
53
|
let heading: string | null = null;
|
|
51
54
|
for (const raw of existing.split(/\r?\n/)) {
|
|
52
55
|
const line = raw.trimEnd();
|
|
53
56
|
const trimmed = line.trim();
|
|
54
|
-
if (/^#\s/.test(trimmed)) continue;
|
|
57
|
+
if (/^#\s/.test(trimmed) || /^Plus \d+ internal changes?\.$/.test(trimmed)) continue;
|
|
55
58
|
if (!trimmed) {
|
|
56
59
|
// Keep paragraph breaks (collapse runs of blank lines to one) in the headline block and in sections.
|
|
57
60
|
if (heading === null) { if (paragraphs.length && paragraphs.at(-1) !== '') paragraphs.push(''); }
|
|
@@ -75,9 +78,9 @@ export function foldPreLandedNotes(existing: string, rendered: string): { text:
|
|
|
75
78
|
}
|
|
76
79
|
}
|
|
77
80
|
// Rendered notes = title ⊕ ordered `##` sections. A pre-landed section with the same heading merges into it.
|
|
78
|
-
const [title, ...rest] = rendered.trimEnd().split('\n');
|
|
79
81
|
const renderedSections: Array<{ heading: string; lines: string[] }> = [];
|
|
80
82
|
for (const line of rest) {
|
|
83
|
+
if (line === footer) continue;
|
|
81
84
|
if (/^##\s/.test(line.trim())) renderedSections.push({ heading: line.trim(), lines: [] });
|
|
82
85
|
else if (renderedSections.length && line.trim()) renderedSections.at(-1)!.lines.push(line);
|
|
83
86
|
}
|
|
@@ -97,6 +100,10 @@ export function foldPreLandedNotes(existing: string, rendered: string): { text:
|
|
|
97
100
|
while (paragraphs.at(-1) === '') paragraphs.pop();
|
|
98
101
|
if (paragraphs.length) parts.push(paragraphs.join('\n'), '');
|
|
99
102
|
for (const section of renderedSections) parts.push(section.heading, '', ...section.lines, '');
|
|
100
|
-
for (const [name, lines] of sections) {
|
|
103
|
+
for (const [name, lines] of sections) {
|
|
104
|
+
parts.push(name, '', ...lines, '');
|
|
105
|
+
folded += Math.max(lines.filter(Boolean).length, 1);
|
|
106
|
+
}
|
|
107
|
+
if (footer) parts.push(footer);
|
|
101
108
|
return { text: `${parts.join('\n').trimEnd()}\n`, folded };
|
|
102
109
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** Read per-file seconds from previous cut Pod reports; missing or damaged reports are not estimates. */
|
|
5
|
+
export function readFileDurations(dir: string): Map<string, number> {
|
|
6
|
+
const durations = new Map<string, number>();
|
|
7
|
+
let names: string[];
|
|
8
|
+
const entities: Record<string, string> = { '&': '&', '"': '"', ''': "'", '<': '<', '>': '>' };
|
|
9
|
+
try { names = readdirSync(dir).filter((name) => name.endsWith('.junit.xml')).sort(); }
|
|
10
|
+
catch { return durations; }
|
|
11
|
+
for (const name of names) {
|
|
12
|
+
try {
|
|
13
|
+
const xml = readFileSync(join(dir, name), 'utf8');
|
|
14
|
+
for (const match of xml.matchAll(/<testsuite\b([^>]*)>/g)) {
|
|
15
|
+
const attrs = match[1]!;
|
|
16
|
+
const file = /\bfile="([^"]+)"/.exec(attrs)?.[1];
|
|
17
|
+
const rawTime = /\btime="([^"]+)"/.exec(attrs)?.[1];
|
|
18
|
+
if (!file || rawTime === undefined) continue;
|
|
19
|
+
const seconds = Number(rawTime);
|
|
20
|
+
if (!Number.isFinite(seconds) || seconds < 0) continue;
|
|
21
|
+
const path = file.replace(/&(?:amp|quot|apos|lt|gt);/g, (entity) => entities[entity]!);
|
|
22
|
+
durations.set(path, Math.max(durations.get(path) ?? 0, seconds));
|
|
23
|
+
}
|
|
24
|
+
} catch { /* One unreadable report must not discard the others. */ }
|
|
25
|
+
}
|
|
26
|
+
return durations;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Longest processing time first; tie-breaks use lexical paths, then the least populated lightest shard. */
|
|
30
|
+
export function planShards(files: string[], durations: Map<string, number>, count: number): Array<{ files: string[]; plannedSeconds: number }> {
|
|
31
|
+
if (!Number.isSafeInteger(count) || count < 1) throw new Error('invalid shard count');
|
|
32
|
+
const known = files.map((file) => durations.get(file)).filter((value): value is number => value !== undefined && Number.isFinite(value) && value >= 0).sort((a, b) => a - b);
|
|
33
|
+
const median = known.length ? (known[Math.floor((known.length - 1) / 2)]! + known[Math.floor(known.length / 2)]!) / 2 : 1;
|
|
34
|
+
const weight = (file: string) => {
|
|
35
|
+
const value = durations.get(file);
|
|
36
|
+
return value !== undefined && Number.isFinite(value) && value >= 0 ? value : median;
|
|
37
|
+
};
|
|
38
|
+
const shards = Array.from({ length: Math.min(count, files.length) }, () => ({ files: [] as string[], plannedSeconds: 0 }));
|
|
39
|
+
for (const file of [...files].sort((a, b) => weight(b) - weight(a) || (a < b ? -1 : a > b ? 1 : 0))) {
|
|
40
|
+
let lightest = shards[0]!;
|
|
41
|
+
for (const shard of shards) if (shard.plannedSeconds < lightest.plannedSeconds
|
|
42
|
+
|| (shard.plannedSeconds === lightest.plannedSeconds && shard.files.length < lightest.files.length)) lightest = shard;
|
|
43
|
+
lightest.files.push(file);
|
|
44
|
+
lightest.plannedSeconds += weight(file);
|
|
45
|
+
}
|
|
46
|
+
return shards.filter((shard) => shard.files.length > 0);
|
|
47
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* 게이트 조각 junit(`gate-logs/<cut|baseline>/pod-*.junit.xml`)에서 파일별 소요를 모은다 — K10 D4 «느린 시험 목록»의 자.
|
|
4
|
+
* 같은 파일이 여러 번 돌았으면(반쪼개기·격리) 가장 적은 파일과 함께 돈 기록을 쓴다 — 혼자 돈 값이 그 파일의 값에 가장 가깝다.
|
|
5
|
+
*
|
|
6
|
+
* bun scripts/release-loop/test-durations.ts <gate-logs/cut 디렉터리> [--top 5] [--json]
|
|
7
|
+
*/
|
|
8
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
11
|
+
export type FileDuration = { file: string; seconds: number; tests: number; runFiles: number };
|
|
12
|
+
|
|
13
|
+
/** junit 한 벌에서 `<testsuite file=… time=… tests=…>` 를 읽는다. */
|
|
14
|
+
export function parseJunitSuites(xml: string): Array<{ file: string; seconds: number; tests: number }> {
|
|
15
|
+
const suites: Array<{ file: string; seconds: number; tests: number }> = [];
|
|
16
|
+
for (const match of xml.matchAll(/<testsuite\b([^>]*)>/g)) {
|
|
17
|
+
const attrs = match[1]!;
|
|
18
|
+
const file = /\bfile="([^"]+)"/.exec(attrs)?.[1];
|
|
19
|
+
const time = Number(/\btime="([^"]+)"/.exec(attrs)?.[1]);
|
|
20
|
+
const tests = Number(/\btests="([^"]+)"/.exec(attrs)?.[1] ?? '0');
|
|
21
|
+
if (file && Number.isFinite(time)) suites.push({ file, seconds: time, tests: Number.isFinite(tests) ? tests : 0 });
|
|
22
|
+
}
|
|
23
|
+
return suites;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** 여러 조각의 junit 을 파일별 하나로 접는다. */
|
|
27
|
+
export function collectDurations(reports: string[]): FileDuration[] {
|
|
28
|
+
const best = new Map<string, FileDuration>();
|
|
29
|
+
for (const xml of reports) {
|
|
30
|
+
const suites = parseJunitSuites(xml);
|
|
31
|
+
for (const suite of suites) {
|
|
32
|
+
const current = best.get(suite.file);
|
|
33
|
+
if (!current || suites.length < current.runFiles) best.set(suite.file, { ...suite, runFiles: suites.length });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return [...best.values()].sort((a, b) => b.seconds - a.seconds || a.file.localeCompare(b.file));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 상위 `percent`% 파일과 그 몫. */
|
|
40
|
+
export function topShare(durations: FileDuration[], percent: number): { files: FileDuration[]; seconds: number; totalSeconds: number; share: number } {
|
|
41
|
+
const count = Math.max(1, Math.ceil(durations.length * percent / 100));
|
|
42
|
+
const files = durations.slice(0, count);
|
|
43
|
+
const seconds = files.reduce((sum, item) => sum + item.seconds, 0);
|
|
44
|
+
const totalSeconds = durations.reduce((sum, item) => sum + item.seconds, 0);
|
|
45
|
+
return { files, seconds, totalSeconds, share: totalSeconds ? seconds / totalSeconds : 0 };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (import.meta.main) {
|
|
49
|
+
const args = process.argv.slice(2);
|
|
50
|
+
const dir = args.find((arg) => !arg.startsWith('--'));
|
|
51
|
+
if (!dir) { console.error('usage: test-durations.ts <gate-logs/cut> [--top 5] [--json]'); process.exit(2); }
|
|
52
|
+
const topIndex = args.indexOf('--top');
|
|
53
|
+
const percent = topIndex >= 0 ? Number(args[topIndex + 1]) : 5;
|
|
54
|
+
const names = readdirSync(dir).filter((name) => name.endsWith('.junit.xml'));
|
|
55
|
+
if (!names.length) { console.error(`no *.junit.xml under ${dir} — was this sweep run before junit reports were kept?`); process.exit(1); }
|
|
56
|
+
const durations = collectDurations(names.map((name) => readFileSync(join(dir, name), 'utf8')));
|
|
57
|
+
const top = topShare(durations, percent);
|
|
58
|
+
if (args.includes('--json')) console.log(JSON.stringify({ reports: names.length, files: durations.length, totalSeconds: top.totalSeconds, topPercent: percent, topSeconds: top.seconds, topShare: top.share, top: top.files }, null, 2));
|
|
59
|
+
else {
|
|
60
|
+
console.log(`reports ${names.length} · files ${durations.length} · total ${top.totalSeconds.toFixed(0)}s · top ${percent}% = ${top.files.length} files · ${top.seconds.toFixed(0)}s (${(top.share * 100).toFixed(1)}%)`);
|
|
61
|
+
for (const item of top.files) console.log(`${item.seconds.toFixed(1)}\t${item.tests}\t${item.file}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -11,8 +11,9 @@ export function runUpgrade(run: CommandRunner = runCommand) {
|
|
|
11
11
|
const output = run('bash', ['scripts/release-upgrade-check.sh', '--from', context.input.previousVersion, '--candidate', candidate]);
|
|
12
12
|
if (output.stderr) process.stderr.write(output.stderr);
|
|
13
13
|
const verdicts = output.stdout.split('\n').filter((line) => /\[upgrade\] verdict\b/.test(line));
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// `release-upgrade-check.sh` appends `from=<v> to=«<v> <sha>»` after the verdict (0.2.4) — the verdict word is what counts.
|
|
15
|
+
const passed = verdicts.length === 2 && verdicts.some((line) => /^ubuntu:24\.04\s+\[upgrade\] verdict ok(?:\s|$)/.test(line))
|
|
16
|
+
&& verdicts.some((line) => /^debian:12\s+\[upgrade\] verdict ok(?:\s|$)/.test(line));
|
|
16
17
|
const outcome: NodeOutcome = output.status === 0 && passed ? 'ok' : verdicts.length === 0 || output.status === null || output.status === 2 ? 'error' : 'fail';
|
|
17
18
|
return { outcome, verdict: outcome === 'ok' ? 'pass' as const : 'fail' as const,
|
|
18
19
|
summary: outcome === 'ok' ? `upgrade ${context.input.previousVersion} → ${context.input.version} · ${verdicts.length} images` : `upgrade failed (rc=${output.status}): ${output.stdout.trim().split('\n').at(-1) || output.stderr.trim() || 'no upgrade verdict'}` };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# K10 D5b ① — test/ 파일별 커버리지(node-b). 산출: ~/f-audit/cov/<n>/lcov.info ⊕ index.tsv(file n rc secs)
|
|
3
|
+
set -u
|
|
4
|
+
A=~/f-audit; rm -rf $A && mkdir -p $A/cov $A/logs && cd $A
|
|
5
|
+
git clone -q --depth 1 file://$HOME/mirror/elanous-agent.git repo || exit 2
|
|
6
|
+
cd repo && git log --oneline -1 > $A/COMMIT && ~/.local/share/elanous/bin/bun install --frozen-lockfile >/dev/null 2>&1 || echo "install rc=$?" >> $A/COMMIT
|
|
7
|
+
git ls-files 'test/*.test.ts' 'test/**/*.test.ts' | grep -v '^test/integration/' | awk '{print NR"\t"$0}' > $A/list.tsv
|
|
8
|
+
wc -l < $A/list.tsv > $A/TOTAL
|
|
9
|
+
run_one() {
|
|
10
|
+
n="$1"; f="$2"; s=$(date +%s)
|
|
11
|
+
( ulimit -v 8000000 2>/dev/null; timeout 120 ~/.local/share/elanous/bin/bun test "./$f" --coverage --coverage-reporter=lcov --coverage-dir "$HOME/f-audit/cov/$n" > "$HOME/f-audit/logs/$n.log" 2>&1 ); rc=$?
|
|
12
|
+
printf '%s\t%s\t%s\t%s\n' "$f" "$n" "$rc" "$(( $(date +%s) - s ))" >> "$HOME/f-audit/index.tsv"
|
|
13
|
+
}
|
|
14
|
+
export -f run_one
|
|
15
|
+
cd $A/repo && awk -F'\t' '{print $1" "$2}' $A/list.tsv | xargs -P 8 -n 2 bash -c 'run_one "$0" "$1"'
|
|
16
|
+
echo DONE > $A/DONE
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// K10 D5 — 무의미 시험 전수조사(읽기 전용 · 정적). 산출 = TSV(파일 · 분류 · 근거) ⊕ 요약 JSON.
|
|
2
|
+
// 분류: A 대상 없음(상대 import 가 가리키는 파일이 없다 · 또는 저장소 코드를 하나도 안 부른다)
|
|
3
|
+
// B 상수=상수(expect(<리터럴>)의 리터럴이 기대값과 같다 · expect(x).toBe(x) 같은 식)
|
|
4
|
+
// C 중복(파일 본문 해시 동일 · 또는 같은 대상 모듈 ⊕ 같은 시험 제목 집합)
|
|
5
|
+
// D 문면 고정(소스·문서를 readFileSync 로 읽어 toContain/toMatch 만 한다 — 동작을 안 부른다)
|
|
6
|
+
// E 영구 skip(skip/todo/xit · skipIf(true) · 전부 skip)
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
10
|
+
import { dirname, join, resolve } from 'node:path';
|
|
11
|
+
|
|
12
|
+
const root = process.argv[2]; const out = process.argv[3];
|
|
13
|
+
const files = execFileSync('git', ['ls-files', '*.test.*'], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).split('\n').filter(Boolean);
|
|
14
|
+
|
|
15
|
+
const EXT = ['', '.ts', '.tsx', '.js', '.mjs', '.cjs', '.mts', '/index.ts', '/index.tsx', '/index.js'];
|
|
16
|
+
function resolveRel(fromFile: string, spec: string): boolean {
|
|
17
|
+
const base = resolve(root, dirname(fromFile), spec);
|
|
18
|
+
const cands = [base, base.replace(/\.js$/, '.ts'), base.replace(/\.js$/, '.tsx'), base.replace(/\.mjs$/, '.mts')];
|
|
19
|
+
for (const c of cands) for (const e of EXT) if (existsSync(c + e)) return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
function resolveAlias(fromFile: string, spec: string): boolean {
|
|
23
|
+
// PWA `@/x` → apps/pwa/src/x
|
|
24
|
+
if (!spec.startsWith('@/')) return true;
|
|
25
|
+
const base = join(root, 'apps/pwa/src', spec.slice(2));
|
|
26
|
+
for (const e of EXT) if (existsSync(base + e)) return true;
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Row = { file: string; cls: string[]; evidence: string[]; lines: number; tests: number };
|
|
31
|
+
const rows: Row[] = [];
|
|
32
|
+
const hashes = new Map<string, string[]>();
|
|
33
|
+
const titleSig = new Map<string, string[]>();
|
|
34
|
+
|
|
35
|
+
for (const f of files) {
|
|
36
|
+
const text = readFileSync(join(root, f), 'utf8');
|
|
37
|
+
const lines = text.split('\n').length;
|
|
38
|
+
const cls: string[] = []; const ev: string[] = [];
|
|
39
|
+
const tests = (text.match(/\b(?:test|it)(?:\.(?:only|each\([^)]*\)))?\s*\(/g) ?? []).length;
|
|
40
|
+
// imports
|
|
41
|
+
// 실제 import 문만(줄 맨 앞 import/export … from · 줄 맨 앞 await/const … import('…')) — 픽스처 문자열·주석 속 모양은 뺀다.
|
|
42
|
+
const specs = [
|
|
43
|
+
...[...text.matchAll(/^\s*(?:import|export)\b[^'";`]*?\bfrom\s+['"]([^'"]+)['"]/gm)].map((m) => m[1]),
|
|
44
|
+
...[...text.matchAll(/^\s*import\s+['"]([^'"]+)['"]/gm)].map((m) => m[1]),
|
|
45
|
+
...[...text.matchAll(/^[^'"`\n/]*\bawait\s+import\(\s*['"]([^'"]+)['"]\s*\)/gm)].map((m) => m[1]),
|
|
46
|
+
...[...text.matchAll(/^[^'"`\n/]*\bimport\(\s*`(\.{1,2}\/[^`$?]+)/gm)].map((m) => m[1]),
|
|
47
|
+
];
|
|
48
|
+
const rel = specs.filter((s) => s.startsWith('.') || s.startsWith('@/'));
|
|
49
|
+
const missing = rel.filter((s) => (s.startsWith('@/') ? !resolveAlias(f, s) : !resolveRel(f, s)));
|
|
50
|
+
if (missing.length) { cls.push('A'); ev.push(`missing-import:${missing.slice(0, 3).join(',')}`); }
|
|
51
|
+
const repoCode = rel.filter((s) => !/\.(json|md|txt|css)$/.test(s));
|
|
52
|
+
// E: skip
|
|
53
|
+
const skips = (text.match(/\b(?:test|it|describe)\.(?:skip|todo)\s*\(|\bx(?:it|describe|test)\s*\(|\.skipIf\(\s*true\s*\)/g) ?? []).length;
|
|
54
|
+
if (skips > 0 && (tests === 0 || skips >= tests)) { cls.push('E'); ev.push(`all-skipped:${skips}/${tests}`); }
|
|
55
|
+
else if (skips > 0) ev.push(`some-skipped:${skips}/${tests}`);
|
|
56
|
+
// B: constant = constant
|
|
57
|
+
const constEq = [...text.matchAll(/^[^'"`\n]*?expect\(\s*(['"`][^'"`\n]{0,80}['"`]|-?\d+(?:\.\d+)?|true|false|null)\s*\)\s*\.\s*(?:toBe|toEqual|toStrictEqual)\(\s*(['"`][^'"`\n]{0,80}['"`]|-?\d+(?:\.\d+)?|true|false|null)\s*\)/gm)]
|
|
58
|
+
.filter((m) => m[1] === m[2]);
|
|
59
|
+
const selfEq = [...text.matchAll(/^[^'"`\n]*?expect\(\s*([A-Za-z_$][\w$.]*)\s*\)\s*\.\s*(?:toBe|toEqual|toStrictEqual)\(\s*([A-Za-z_$][\w$.]*)\s*\)/gm)].filter((m) => m[1] === m[2]);
|
|
60
|
+
const litExpect = (text.match(/expect\(\s*(?:['"`][^'"`\n]*['"`]|-?\d+(?:\.\d+)?|true|false|null)\s*\)/g) ?? []).length;
|
|
61
|
+
if (constEq.length + selfEq.length > 0) { cls.push('B'); ev.push(`const-eq:${constEq.length + selfEq.length}`); }
|
|
62
|
+
else if (litExpect >= 3) ev.push(`literal-subject-expects:${litExpect}`);
|
|
63
|
+
// D: text pinning — readFileSync of source/docs ⊕ toContain/toMatch only, no calls into imported code
|
|
64
|
+
const readsText = /readFileSync\(|Bun\.file\([^)]*\)\.text\(/.test(text);
|
|
65
|
+
const matchers = (text.match(/\.\s*(?:toContain|toMatch|not\.toContain|not\.toMatch)\(/g) ?? []).length;
|
|
66
|
+
const allExpects = (text.match(/\bexpect\(/g) ?? []).length;
|
|
67
|
+
if (readsText && allExpects > 0 && matchers / allExpects >= 0.8 && repoCode.length === 0) { cls.push('D'); ev.push(`text-pin:${matchers}/${allExpects}`); }
|
|
68
|
+
else if (readsText && allExpects > 0 && matchers / allExpects >= 0.8) ev.push(`text-pin-with-imports:${matchers}/${allExpects}`);
|
|
69
|
+
// A(2): no repo code at all and no file reads — tests only itself
|
|
70
|
+
if (repoCode.length === 0 && !readsText && !/spawn|execFile|Bun\.spawn|fetch\(/.test(text)) { if (!cls.includes('A')) { cls.push('A'); ev.push('no-repo-code'); } }
|
|
71
|
+
// C: duplicates
|
|
72
|
+
const h = createHash('sha256').update(text).digest('hex').slice(0, 16);
|
|
73
|
+
hashes.set(h, [...(hashes.get(h) ?? []), f]);
|
|
74
|
+
const titles = [...text.matchAll(/\b(?:test|it)\s*\(\s*['"`]([^'"`]{4,})['"`]/g)].map((m) => m[1]).sort();
|
|
75
|
+
if (titles.length >= 2) { const sig = createHash('sha256').update(repoCode.sort().join('|') + '#' + titles.join('|')).digest('hex').slice(0, 16); titleSig.set(sig, [...(titleSig.get(sig) ?? []), f]); }
|
|
76
|
+
rows.push({ file: f, cls, evidence: ev, lines, tests });
|
|
77
|
+
}
|
|
78
|
+
const byFile = new Map(rows.map((r) => [r.file, r]));
|
|
79
|
+
for (const [h, fs] of hashes) if (fs.length > 1) for (const f of fs) { const r = byFile.get(f)!; if (!r.cls.includes('C')) r.cls.push('C'); r.evidence.push(`same-bytes:${fs.filter((x) => x !== f).slice(0, 2).join(',')}`); }
|
|
80
|
+
for (const [s, fs] of titleSig) if (fs.length > 1) for (const f of fs) { const r = byFile.get(f)!; if (!r.cls.includes('C')) r.cls.push('C'); r.evidence.push(`same-target+titles:${fs.filter((x) => x !== f).slice(0, 2).join(',')}`); }
|
|
81
|
+
|
|
82
|
+
const tsv = ['file\tclasses\ttests\tlines\tevidence', ...rows.filter((r) => r.cls.length).map((r) => `${r.file}\t${r.cls.join(',')}\t${r.tests}\t${r.lines}\t${r.evidence.join(' ; ')}`)].join('\n');
|
|
83
|
+
writeFileSync(join(out, 'test-audit.tsv'), tsv + '\n');
|
|
84
|
+
const count = (c: string) => rows.filter((r) => r.cls.includes(c)).length;
|
|
85
|
+
const summary = { total: rows.length, flagged: rows.filter((r) => r.cls.length).length, A: count('A'), B: count('B'), C: count('C'), D: count('D'), E: count('E'),
|
|
86
|
+
A_missing: rows.filter((r) => r.evidence.some((e) => e.startsWith('missing-import'))).length, A_noRepo: rows.filter((r) => r.evidence.includes('no-repo-code')).length };
|
|
87
|
+
writeFileSync(join(out, 'test-audit-summary.json'), JSON.stringify(summary, null, 2));
|
|
88
|
+
console.log(JSON.stringify(summary));
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// K10 D5b — ① 커버리지 겹침(고유 기여) ② 은퇴 기능 시험 ③ 약한 단언. 읽기 전용.
|
|
2
|
+
// 사용: bun test-audit-b.ts <repo> <cov-root|-> <out-dir>
|
|
3
|
+
// cov-root = 파일별 lcov 폴더(<n>/lcov.info) ⊕ index.tsv(file n rc secs). '-' 면 ①을 건너뛴다.
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
const [repo, covRoot, out] = process.argv.slice(2);
|
|
9
|
+
const files = execFileSync('git', ['ls-files', '*.test.ts', '*.test.tsx'], { cwd: repo, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }).split('\n').filter(Boolean);
|
|
10
|
+
|
|
11
|
+
// ③ 약한 단언 — 파일마다 매처 종류를 센다(줄이 아니라 출현 수).
|
|
12
|
+
const WEAK = new Set(['toBeDefined', 'toBeTruthy', 'toBeFalsy', 'not.toThrow', 'toBeInstanceOf', 'not.toBeNull', 'not.toBeUndefined']);
|
|
13
|
+
const weakRows: string[] = ['file\texpects\tweak\tkinds'];
|
|
14
|
+
let weakOnly = 0;
|
|
15
|
+
// ② 은퇴 기능 — 제목에 은퇴·제거·옛 낱말 ⊕ 부정 단언(없음을 지킴)이 아닌 것
|
|
16
|
+
const RETIRED = /\b(retired|legacy|deprecated|obsolete)\b|은퇴|폐기|옛 (?:문|경로|명령|이름|동작)/i;
|
|
17
|
+
const retiredRows: string[] = ['file\ttitles\tnegative\tpositive\tverdict'];
|
|
18
|
+
for (const f of files) {
|
|
19
|
+
const text = readFileSync(join(repo, f), 'utf8');
|
|
20
|
+
const kinds = new Map<string, number>();
|
|
21
|
+
for (const m of text.matchAll(/\)\s*\.\s*((?:not\s*\.\s*)?[a-zA-Z]+)\s*\(/g)) {
|
|
22
|
+
const k = m[1].replace(/\s+/g, '');
|
|
23
|
+
if (!/^(not\.)?to[A-Z]/.test(k)) continue;
|
|
24
|
+
kinds.set(k, (kinds.get(k) ?? 0) + 1);
|
|
25
|
+
}
|
|
26
|
+
const total = [...kinds.values()].reduce((a, b) => a + b, 0);
|
|
27
|
+
const weak = [...kinds].filter(([k]) => WEAK.has(k)).reduce((a, [, v]) => a + v, 0);
|
|
28
|
+
if (total > 0 && weak === total) { weakOnly++; weakRows.push(`${f}\t${total}\t${weak}\t${[...kinds].map(([k, v]) => `${k}:${v}`).join(',')}`); }
|
|
29
|
+
const titles = [...text.matchAll(/\b(?:test|it|describe)\s*\(\s*['"`]([^'"`]{4,})['"`]/g)].map((m) => m[1]).filter((t) => RETIRED.test(t));
|
|
30
|
+
if (titles.length) {
|
|
31
|
+
const neg = [...kinds].filter(([k]) => k.startsWith('not.') || ['toThrow','toBeUndefined','toBeNull','toBeFalsy'].includes(k)).reduce((a, [, v]) => a + v, 0)
|
|
32
|
+
+ (text.match(/toHaveLength\(0\)|toEqual\(\[\]\)|toBe\(false\)|toBe\(0\)/g) ?? []).length;
|
|
33
|
+
const pos = total - neg;
|
|
34
|
+
const verdict = neg >= pos ? '가드(없음을 지킨다) — 유지' : '옛 동작 검사 후보 — 열어 보기';
|
|
35
|
+
retiredRows.push(`${f}\t${titles.slice(0, 3).join(' | ')}\t${neg}\t${pos}\t${verdict}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
writeFileSync(join(out, 'weak-assertions.tsv'), weakRows.join('\n') + '\n');
|
|
39
|
+
writeFileSync(join(out, 'retired-feature-tests.tsv'), retiredRows.join('\n') + '\n');
|
|
40
|
+
|
|
41
|
+
// ① 커버리지 겹침
|
|
42
|
+
let covSummary: Record<string, unknown> = { skipped: true };
|
|
43
|
+
if (covRoot !== '-') {
|
|
44
|
+
const index = readFileSync(join(covRoot, 'index.tsv'), 'utf8').split('\n').filter(Boolean).map((l) => l.split('\t'));
|
|
45
|
+
const lineOwners = new Map<string, number>(); // "src:line" → 몇 파일이 닿나
|
|
46
|
+
const perTest = new Map<string, Set<string>>();
|
|
47
|
+
for (const [file, n, rc] of index) {
|
|
48
|
+
if (rc !== '0') continue;
|
|
49
|
+
const p = join(covRoot, 'cov', n, 'lcov.info');
|
|
50
|
+
if (!existsSync(p)) continue;
|
|
51
|
+
const set = new Set<string>(); let sf = '';
|
|
52
|
+
for (const line of readFileSync(p, 'utf8').split('\n')) {
|
|
53
|
+
if (line.startsWith('SF:')) sf = line.slice(3);
|
|
54
|
+
else if (line.startsWith('DA:') && sf.startsWith('src/') && !/\.test\.tsx?$/.test(sf)) {
|
|
55
|
+
const [ln, hits] = line.slice(3).split(',');
|
|
56
|
+
if (Number(hits) > 0) set.add(`${sf}:${ln}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
perTest.set(file, set);
|
|
60
|
+
for (const k of set) lineOwners.set(k, (lineOwners.get(k) ?? 0) + 1);
|
|
61
|
+
}
|
|
62
|
+
const rows = [...perTest].map(([file, set]) => {
|
|
63
|
+
let unique = 0; for (const k of set) if (lineOwners.get(k) === 1) unique++;
|
|
64
|
+
return { file, lines: set.size, unique };
|
|
65
|
+
}).sort((a, b) => a.unique - b.unique || b.lines - a.lines);
|
|
66
|
+
writeFileSync(join(out, 'coverage-unique.tsv'), ['file\tcovered_src_lines\tunique_src_lines', ...rows.map((r) => `${r.file}\t${r.lines}\t${r.unique}`)].join('\n') + '\n');
|
|
67
|
+
const zero = rows.filter((r) => r.unique === 0 && r.lines > 0).length;
|
|
68
|
+
const noSrc = rows.filter((r) => r.lines === 0).length;
|
|
69
|
+
covSummary = { measured: rows.length, failedOrSkipped: index.filter((r) => r[2] !== '0').length, zeroUnique: zero, touchesNoSrc: noSrc, uniqueLe5: rows.filter((r) => r.lines > 0 && r.unique > 0 && r.unique <= 5).length, distinctSrcLines: lineOwners.size };
|
|
70
|
+
}
|
|
71
|
+
const summary = { files: files.length, weakOnly, retiredTitled: retiredRows.length - 1, retiredOldBehaviour: retiredRows.filter((r) => r.includes('옛 동작')).length, coverage: covSummary };
|
|
72
|
+
writeFileSync(join(out, 'test-audit-b-summary.json'), JSON.stringify(summary, null, 2));
|
|
73
|
+
console.log(JSON.stringify(summary));
|
|
@@ -51,7 +51,24 @@ const HARNESS_TEST_ENV_KEYS = [
|
|
|
51
51
|
'ELANOUS_HARNESS_BOUNDARY',
|
|
52
52
|
'ELANOUS_HARNESS_ROLE',
|
|
53
53
|
'ELANOUS_HARNESS_DETACHED',
|
|
54
|
+
] as const;
|
|
55
|
+
|
|
56
|
+
// Discovery: rg -n "process\.env\.ELANOUS_(HOST|RUN|ORIGIN|SUPERVISOR|PARENT)" src
|
|
57
|
+
// These are ambient execution identities, not inputs to a deterministic test.
|
|
58
|
+
// debug.log also attributes SUBSTRATE and ARM_ID to every plain-object record.
|
|
59
|
+
const EXECUTION_ORIGIN_ENV_KEYS = [
|
|
60
|
+
'ELANOUS_HOST_ID',
|
|
54
61
|
'ELANOUS_RUN_ID',
|
|
62
|
+
'ELANOUS_RUN_CONTEXT',
|
|
63
|
+
'ELANOUS_PARENT_RUN_ID',
|
|
64
|
+
'ELANOUS_PARENT_PTY_ID',
|
|
65
|
+
'ELANOUS_PARENT_SELF_DEV_RUNS_DIR',
|
|
66
|
+
'ELANOUS_SUBSTRATE',
|
|
67
|
+
'ELANOUS_ARM_ID',
|
|
68
|
+
// Parent account selection changes CLI rotation assertions in the child.
|
|
69
|
+
'ELANOUS_CODEX_ACCOUNT',
|
|
70
|
+
// A parent's derived-root label must not reclassify a test's explicit state dir.
|
|
71
|
+
'ELANOUS_STATE_DIR_SOURCE',
|
|
55
72
|
] as const;
|
|
56
73
|
|
|
57
74
|
export type ShutdownSignal = 'SIGINT' | 'SIGTERM';
|
|
@@ -254,10 +271,16 @@ export async function terminateDirectChild(
|
|
|
254
271
|
|
|
255
272
|
export function prepareIsolatedTestEnv(sourceEnv: NodeJS.ProcessEnv, testRoot: string): NodeJS.ProcessEnv {
|
|
256
273
|
const env = Object.fromEntries(
|
|
257
|
-
Object.entries(sourceEnv).filter(([key]) => !isCredentialKey(key)
|
|
274
|
+
Object.entries(sourceEnv).filter(([key]) => !isCredentialKey(key)
|
|
275
|
+
&& !HARNESS_TEST_ENV_KEYS.includes(key as typeof HARNESS_TEST_ENV_KEYS[number])
|
|
276
|
+
&& !EXECUTION_ORIGIN_ENV_KEYS.includes(key as typeof EXECUTION_ORIGIN_ENV_KEYS[number])),
|
|
258
277
|
);
|
|
259
278
|
env.HOME = testRoot;
|
|
260
|
-
|
|
279
|
+
// userConfigPath() warns whenever XDG_CONFIG_HOME is set, including when
|
|
280
|
+
// config.json is absent. Use its non-XDG path; migrateLegacyXdgUserConfig()
|
|
281
|
+
// resolves the old path through ELANOUS_TEST_HOME rather than the real home.
|
|
282
|
+
delete env.XDG_CONFIG_HOME;
|
|
283
|
+
env.ELANOUS_TEST_HOME = testRoot;
|
|
261
284
|
// ⚠️ Redirect elanous's OWN roots explicitly — do not merely inherit them.
|
|
262
285
|
// `ELANOUS_STATE_DIR` / `ELANOUS_CONFIG_DIR` are absolute paths that win over
|
|
263
286
|
// HOME, so a developer who exports either one keeps pointing the "isolated"
|
|
@@ -267,9 +290,8 @@ export function prepareIsolatedTestEnv(sourceEnv: NodeJS.ProcessEnv, testRoot: s
|
|
|
267
290
|
// isolation legible in the child's own environment rather than implied.
|
|
268
291
|
env.ELANOUS_STATE_DIR = join(testRoot, 'state');
|
|
269
292
|
env.ELANOUS_CONFIG_DIR = join(testRoot, 'config');
|
|
270
|
-
//
|
|
271
|
-
// logger API-shape tests
|
|
272
|
-
delete env.ELANOUS_RUN_ID;
|
|
293
|
+
// All ambient execution identities are removed by EXECUTION_ORIGIN_ENV_KEYS
|
|
294
|
+
// above, before the child is spawned; logger API-shape tests need their absence.
|
|
273
295
|
// ⚠️ No `ELANOUS_TEST_DETERMINISTIC` marker is exported. Nothing reads it yet,
|
|
274
296
|
// and an env var with no consumer is a surface that looks like a contract
|
|
275
297
|
// while guaranteeing nothing — a later test could branch on it believing it
|
package/scripts/tui-sim.ts
CHANGED
|
@@ -45,6 +45,10 @@ import {
|
|
|
45
45
|
writeFileSync, appendFileSync, readFileSync, mkdirSync, readdirSync, existsSync, unlinkSync, rmSync, renameSync,
|
|
46
46
|
} from 'node:fs';
|
|
47
47
|
import { execFileSync } from 'node:child_process';
|
|
48
|
+
import { resolve } from 'node:path';
|
|
49
|
+
import { getCapturedEnv } from '../src/shell-env-bootstrap.js';
|
|
50
|
+
import { identityEnvKeys } from '../src/agent/identity-env.js';
|
|
51
|
+
import { resolveKeyBytes, resolveKeyInput, writeKeyInput } from './lib/tui-sim-keys.js';
|
|
48
52
|
|
|
49
53
|
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
|
|
50
54
|
|
|
@@ -91,14 +95,18 @@ function writeMeta(id: string, m: Meta): void {
|
|
|
91
95
|
renameSync(tmp, metaPath(id));
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
// ──
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
// ── optional child credential isolation ──
|
|
99
|
+
const CLEAN_ENV_KEYS = new Set([
|
|
100
|
+
'PATH', 'HOME', 'USER', 'LOGNAME', 'SHELL', 'LANG', 'TERM', 'COLORTERM', 'TZ', 'TMPDIR',
|
|
101
|
+
'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES', 'LC_COLLATE', 'LC_NUMERIC', 'LC_TIME', 'LC_MONETARY',
|
|
102
|
+
'LC_PAPER', 'LC_NAME', 'LC_ADDRESS', 'LC_TELEPHONE', 'LC_MEASUREMENT', 'LC_IDENTIFICATION',
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
/** Block everything except basic execution/locale variables after PTY env synthesis. */
|
|
106
|
+
export function cleanPtyUnsetEnv(captured: Record<string, string>, parent: NodeJS.ProcessEnv): string[] {
|
|
107
|
+
return [...new Set([...Object.keys(captured), ...Object.keys(parent), ...identityEnvKeys()])]
|
|
108
|
+
.filter((key) => !CLEAN_ENV_KEYS.has(key));
|
|
109
|
+
}
|
|
102
110
|
|
|
103
111
|
// ── client: drop a command into inbox, optionally await result ──
|
|
104
112
|
function argVal(args: string[], flag: string): string | null {
|
|
@@ -136,6 +144,10 @@ async function runDaemon(args: string[]): Promise<void> {
|
|
|
136
144
|
const testMode = args.includes('--test');
|
|
137
145
|
const autoWorktree = args.includes('--worktree');
|
|
138
146
|
const explicitWorkdir = argVal(args, '--workdir');
|
|
147
|
+
const home = argVal(args, '--home');
|
|
148
|
+
const cleanEnv = args.includes('--clean-env');
|
|
149
|
+
if (args.includes('--home') && (!home || home.startsWith('--'))) { console.error('[tui-sim] --home requires a directory'); process.exit(1); }
|
|
150
|
+
const childHome = home ? resolve(home) : null;
|
|
139
151
|
const cols = Number(argVal(args, '--cols')) || 220;
|
|
140
152
|
const rows = Number(argVal(args, '--rows')) || 60;
|
|
141
153
|
const bootSec = Number(argVal(args, '--boot')) || 8;
|
|
@@ -176,9 +188,12 @@ async function runDaemon(args: string[]): Promise<void> {
|
|
|
176
188
|
+ `# follow live: tail -f ${forwardPath}\n\n`);
|
|
177
189
|
console.error(`[tui-sim] start id=${id} workdir=${workdir} test=${testMode}`);
|
|
178
190
|
console.error(`[tui-sim] 📡 live timeline → ${forwardPath} (follow: tail -f ${forwardPath})`);
|
|
191
|
+
if (childHome) mkdirSync(childHome, { recursive: true });
|
|
192
|
+
const unsetEnv = cleanEnv ? cleanPtyUnsetEnv(getCapturedEnv(), process.env) : undefined;
|
|
179
193
|
const h: PtyHandle = startPty({
|
|
180
194
|
cmd: 'bun', args: [binPath, ...isoArgs], cols, rows, workdir,
|
|
181
|
-
env: { ELANOUS_DRIVE_TUI: '1', ...(testMode ? { ELANOUS_STATE_DIR: testDir } : {}) },
|
|
195
|
+
env: { ELANOUS_DRIVE_TUI: '1', ...(testMode ? { ELANOUS_STATE_DIR: testDir } : {}), ...(childHome ? { HOME: childHome } : {}) },
|
|
196
|
+
...(unsetEnv ? { unsetEnv } : {}),
|
|
182
197
|
});
|
|
183
198
|
|
|
184
199
|
const meta: Meta = {
|
|
@@ -296,9 +311,9 @@ async function runDaemon(args: string[]): Promise<void> {
|
|
|
296
311
|
feed(text, verb === 'send');
|
|
297
312
|
res = { ok: true, sent: text.length };
|
|
298
313
|
} else if (verb === 'key') {
|
|
299
|
-
const seq =
|
|
300
|
-
if (
|
|
301
|
-
else { const n = Number(cmd.repeat) || 1; for (let i = 0; i < n; i++) h
|
|
314
|
+
const seq = resolveKeyInput(String(cmd.name));
|
|
315
|
+
if (seq === null) { res = { ok: false, error: `unknown key '${cmd.name}'` }; }
|
|
316
|
+
else { const n = Number(cmd.repeat) || 1; for (let i = 0; i < n; i++) writeKeyInput(h, seq); res = { ok: true }; }
|
|
302
317
|
} else if (verb === 'mouse') {
|
|
303
318
|
const x = Number(cmd.x), y = Number(cmd.y);
|
|
304
319
|
const btn = cmd.button === 'right' ? 2 : cmd.button === 'middle' ? 1 : 0;
|
|
@@ -369,6 +384,23 @@ async function main(): Promise<void> {
|
|
|
369
384
|
const verb = argv[0];
|
|
370
385
|
const rest = argv.slice(1);
|
|
371
386
|
|
|
387
|
+
if (verb === '--help' || verb === 'help') {
|
|
388
|
+
console.log(`usage: tui-sim <verb> [args]
|
|
389
|
+
start [--id id] [--test] [--worktree] [--workdir dir] [--cols n] [--rows n]
|
|
390
|
+
start --home <dir> Set the child TUI HOME (default: inherited).
|
|
391
|
+
start --clean-env Allow only basic execution/locale env into the child (default: off).
|
|
392
|
+
send|type <id> <text> Send text with|without Enter.
|
|
393
|
+
key <id> <name> [--repeat n] Send a named key; unknown keys exit 1.
|
|
394
|
+
key names: enter/return/tab/shift-tab/esc/escape/space/backspace/delete/up/down/right/left/pageup/pagedown/home/end.
|
|
395
|
+
key ctrl-<a..z> Send control byte (ctrl-j sends newline).
|
|
396
|
+
key ctrl-\\ | ctrl-] Send 0x1c | 0x1d.
|
|
397
|
+
key alt-<key> Send ESC followed by the named key or one character.
|
|
398
|
+
key shift-enter Send ESC [ 13 ; 2 u.
|
|
399
|
+
key hex:<bytes> Send raw hex bytes (e.g. hex:1b5b41).
|
|
400
|
+
resize <id> <cols> <rows> Resize the running PTY.
|
|
401
|
+
mouse|dump|text|transcript|status|list|stop|promote|clean`);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
372
404
|
if (verb === 'start') { await runDaemon(rest); return; }
|
|
373
405
|
|
|
374
406
|
if (verb === 'list') {
|
|
@@ -394,9 +426,22 @@ async function main(): Promise<void> {
|
|
|
394
426
|
}
|
|
395
427
|
case 'key': {
|
|
396
428
|
const name = rest[1];
|
|
429
|
+
if (!name || resolveKeyBytes(name) === null) { console.error(`알 수 없는 키 '${name}'`); process.exit(1); }
|
|
397
430
|
const repeat = argVal(rest, '--repeat');
|
|
398
431
|
const res = await sendCommand(id, 'key', { name, ...(repeat ? { repeat: Number(repeat) } : {}) }, true);
|
|
399
432
|
console.log(res?.ok ? `✓ key ${name}` : `✗ ${res?.error}`);
|
|
433
|
+
if (!res?.ok) process.exitCode = 1;
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
case 'resize': {
|
|
437
|
+
const cols = Number(rest[1]), rows = Number(rest[2]);
|
|
438
|
+
if (!Number.isSafeInteger(cols) || cols < 2 || !Number.isSafeInteger(rows) || rows < 2) {
|
|
439
|
+
console.error('usage: tui-sim resize <id> <cols> <rows> (positive integers >= 2)');
|
|
440
|
+
process.exit(1);
|
|
441
|
+
}
|
|
442
|
+
const res = await sendCommand(id, 'resize', { cols, rows }, true);
|
|
443
|
+
console.log(res?.ok ? `✓ resize ${cols}x${rows}` : `✗ ${res?.error ?? 'failed'}`);
|
|
444
|
+
if (!res?.ok) process.exitCode = 1;
|
|
400
445
|
break;
|
|
401
446
|
}
|
|
402
447
|
case 'mouse': {
|
|
@@ -508,9 +553,9 @@ async function main(): Promise<void> {
|
|
|
508
553
|
break;
|
|
509
554
|
}
|
|
510
555
|
default:
|
|
511
|
-
console.error(`unknown verb '${verb}'. verbs: start|send|type|key|mouse|dump|text|transcript|status|list|promote|clean|stop`);
|
|
556
|
+
console.error(`unknown verb '${verb}'. verbs: start|send|type|key|resize|mouse|dump|text|transcript|status|list|promote|clean|stop`);
|
|
512
557
|
process.exit(1);
|
|
513
558
|
}
|
|
514
559
|
}
|
|
515
560
|
|
|
516
|
-
main().catch((e) => { console.error(e); process.exit(1); });
|
|
561
|
+
if (import.meta.main) main().catch((e) => { console.error(e); process.exit(1); });
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env zsh
|
|
2
|
+
# 판올림(설치본 갱신) — UX 자리 · 내부 문서 `UX` §3a.
|
|
3
|
+
# origin/main 을 깨끗한 체크아웃(D)에 떼어 놓고, 설치본을 그 체크아웃으로 갱신하고 넥서스를 재시작한 뒤, 서빙 화면이 새 판인지 잰다.
|
|
4
|
+
# 사용: zsh scripts/ux/bump-installed.sh [<ref>=origin/main]
|
|
5
|
+
# ⛔ zsh 에서 set -e 는 아무것도 안 한다 — 단계는 전부 && 로 잇는다. ⛔ 채널 공지(전·후)는 이 스크립트 밖에서 사람이/자리가 한다.
|
|
6
|
+
REF=${1:-origin/main}
|
|
7
|
+
D=${UX_DEPLOY_DIR:-$TMPDIR/deploy-main}
|
|
8
|
+
[[ -d $D/.git || -f $D/.git ]] || { echo "deploy checkout missing: $D (git worktree add --detach $D origin/main 로 한 번 만든다)"; exit 64; }
|
|
9
|
+
cd "$D" && git fetch -q origin && git checkout -q --detach "$REF" && bun install --silent && (cd apps/pwa && bun install --silent) \
|
|
10
|
+
&& echo "[bump] checkout $(git rev-parse --short HEAD) — $(git log -1 --format=%s | cut -c1-80)" \
|
|
11
|
+
&& cd ~ && elanous update --from "$D" --restart \
|
|
12
|
+
|| { echo "[bump] FAILED — PWA 빌드가 깨졌으면 도구의 처방(cd $D/apps/pwa && bun install --force)을 먼저 본다"; exit 1; }
|
|
13
|
+
# 재는 칸 셋: 데몬 pid · 설치본 판 · 서빙 화면 = 설치본 out/index.html
|
|
14
|
+
sleep 15
|
|
15
|
+
launchctl list | rg com.elanous.nexus
|
|
16
|
+
elanous --version
|
|
17
|
+
CUR=~/.local/share/elanous/current
|
|
18
|
+
WANT=$(rg -o 'page-[a-f0-9]+\.js' -m1 "$CUR/node_modules/elanous/apps/pwa/out/index.html" | head -1)
|
|
19
|
+
GOT=$(curl -s --max-time 15 http://127.0.0.1:31415/app/ | rg -o 'page-[a-f0-9]+\.js' -m1 | head -1)
|
|
20
|
+
[[ -n $WANT && $WANT == $GOT ]] && echo "[bump] ok — serving $GOT" || { echo "[bump] MISMATCH installed=$WANT serving=$GOT"; exit 1; }
|