elanous 0.2.6 → 0.2.7
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/.env.example +10 -7
- package/README.md +4 -0
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3995-bee512a7063036ce.js → 2375-cb6debea56cce4fb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2415.ed6428ff334c7ce2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{8d0d9faf-97887ff99713b84e.js → 2ea96958-e8c7e112de50b067.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{5329.8b7e4bca684b6eec.js → 3403.4bc97b4066945a94.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7633.0654fe6ea259c1ee.js → 4323.c5dfb6f503304775.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2984-977cfa339fcd65a5.js → 5080-1613752891fcadb4.js} +3 -3
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{763-7d0e1913b5074e98.js → 5791-bb82743fa6299562.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{4240-7153881b0b377306.js → 6062-73e68343a9814f6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{91ff6dda-3b33d42d20657429.js → 8046cadf-211ca3c0e00f2b72.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9454-a3e0953307216c58.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-624f7a2f89b60c8f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-5a5fb385a4df0ffe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-7457f52683562161.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-b7dcf5d48a854b76.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-51480757e7d68b92.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-30533117beb53625.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-07267f1391b96aba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-55d50e9b0006a91d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a7e37af7d0399cb8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-15e5a15db84c56aa.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-a2e572e4511f2a16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-dd107bf1c7d52a90.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-56adbe1a1f1379bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5d422f38c052967e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +1 -0
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-37b8bf78a3c6eec5.js → webpack-e5bea36f183b25e6.js} +1 -1
- package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +1 -0
- package/apps/pwa/out/_next/static/keajW7DDyghEs1y8oJe6T/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +22 -22
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +22 -22
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +22 -22
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +21 -21
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +21 -21
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +21 -21
- package/apps/pwa/out/consult/index.html +1 -0
- package/apps/pwa/out/consult/index.txt +32 -0
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +22 -22
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +22 -22
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +22 -22
- package/apps/pwa/out/exec/index.html +1 -0
- package/apps/pwa/out/exec/index.txt +32 -0
- package/apps/pwa/out/field/index.html +1 -0
- package/apps/pwa/out/field/index.txt +32 -0
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +21 -21
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +22 -22
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +22 -22
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +21 -21
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +21 -21
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +22 -22
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +22 -22
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +22 -22
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +21 -21
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +22 -22
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +22 -22
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +22 -22
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +22 -22
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +22 -22
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +21 -21
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +22 -22
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +22 -22
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +22 -22
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +22 -22
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +22 -22
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +22 -22
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +22 -22
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +22 -22
- package/bin/elanous.cjs +107 -0
- package/graphs/field/field-feed.yaml +24 -0
- package/graphs/field/recipes.yaml +12 -0
- package/graphs/landing/landing-heal.yaml +3 -1
- package/graphs/landing/recipes.yaml +3 -0
- package/graphs/release/recipes.yaml +10 -0
- package/graphs/release/release-loop.yaml +10 -3
- package/package.json +5 -5
- package/plugins/card-followup/README.md +13 -4
- package/plugins/card-followup/graphs/card-followup.yaml +3 -1
- package/plugins/card-followup/graphs/recipes.yaml +5 -1
- package/plugins/card-followup/graphs/run-step.ts +122 -18
- package/plugins/card-followup/graphs/sales-strategy.ts +173 -0
- package/plugins/card-followup/plugin.json +1 -1
- package/plugins/doc-draft/README.md +13 -0
- package/plugins/doc-draft/examples/brief-sample.json +8 -0
- package/plugins/doc-draft/graphs/doc-draft.yaml +14 -0
- package/plugins/doc-draft/graphs/recipes.yaml +9 -0
- package/plugins/doc-draft/graphs/run-step.ts +113 -0
- package/plugins/doc-draft/plugin.json +24 -0
- package/plugins/geo-check/README.md +3 -1
- package/plugins/geo-check/graphs/run-step.ts +24 -3
- package/plugins/geo-check/plugin.json +1 -1
- package/plugins/job-coach/.codex-plugin/plugin.json +1 -1
- package/plugins/job-coach/README.md +1 -1
- package/plugins/job-coach/graphs/run-step.ts +2 -2
- package/plugins/job-coach/plugin.json +1 -1
- package/plugins/job-coach/plugin.ts +1 -1
- package/scripts/affinity/endcard.sh +3 -2
- package/scripts/affinity/reframe.sh +3 -1
- package/scripts/ci-public-export-leak-gate.ts +48 -0
- package/scripts/codex-quota-alert.ts +122 -258
- package/scripts/coord-tracks.json +4 -4
- package/scripts/field-feed/deliver-node.ts +52 -0
- package/scripts/field-feed/feed-node.ts +67 -0
- package/scripts/field-feed/lib.ts +162 -0
- package/scripts/field-feed/reel-node.ts +24 -0
- package/scripts/field-feed/start.ts +62 -0
- package/scripts/fixtures/vflow-sample.ndjson +6 -0
- package/scripts/install.sh +92 -13
- package/scripts/landing-heal/clean-checkout.ts +114 -0
- package/scripts/landing-heal/run-mission.ts +38 -3
- package/scripts/lib/pwa-scenarios/scenarios.ts +2 -1
- package/scripts/public-export.ts +8 -4
- package/scripts/release-loop/baseline-from-logs.ts +92 -0
- package/scripts/release-loop/env-known-failures.ts +31 -0
- package/scripts/release-loop/gate-node.ts +81 -19
- package/scripts/release-loop/known-issues-node.ts +159 -0
- package/scripts/release-loop/npm-publish-node.ts +118 -0
- package/scripts/release-loop/ops-upgrade-node.ts +87 -0
- package/scripts/release-loop/release-readiness.ts +58 -0
- package/scripts/release-loop/unattended-release.ts +93 -0
- package/scripts/ux/measure-first-text.ts +102 -0
- package/scripts/vflow-collect.ts +20 -7
- package/scripts/vflow-search.ts +73 -0
- package/scripts/video-free-line-test-walker.ts +36 -0
- package/scripts/video-pipeline.ts +2 -2
- package/skills/explainer-video/SKILL.md +12 -9
- package/skills/explainer-video/engine/build.mjs +1 -1
- package/skills/explainer-video/engine/reel.mjs +138 -31
- package/skills/explainer-video/engine/reel.sh +2 -1
- package/skills/explainer-video/examples/elanous-0.2.5/script.json +1 -1
- package/skills/grill-me/SKILL.md +1 -1
- package/skills/omni-digest/docs/diagrams/noncode-terminal-promotion-path-2026-08-13.mmd +1 -1
- package/src/a2a/server.ts +2 -0
- package/src/acp/elanous-aware-peers.ts +30 -0
- package/src/acp/server.ts +91 -15
- package/src/agent/monad-agent-turn.ts +25 -6
- package/src/agent/shared-app-tools.ts +5 -3
- package/src/agent-substrate/review-intent.ts +6 -2
- package/src/agent-substrate/shard-boundary-intent.ts +70 -0
- package/src/auth/github-app-token.ts +4 -1
- package/src/autopilot/track-agent-shadow.ts +151 -0
- package/src/autopilot/track-agent.ts +23 -3
- package/src/boot/acp-server-help.ts +16 -0
- package/src/boot/daemon-prompt-turn.ts +31 -10
- package/src/boot/daemon-tools/index.ts +7 -1
- package/src/budget/codex-credit-pace.ts +84 -0
- package/src/budget/codex-credit-plan.ts +154 -0
- package/src/budget/codex-reset-credit-state.ts +2 -0
- package/src/chat/chat-cli.ts +4 -1
- package/src/chat/short-question.ts +40 -0
- package/src/cli/agent-cli.ts +24 -15
- package/src/cli/ask-bare.ts +42 -0
- package/src/cli/cli-user-error.ts +25 -0
- package/src/cli/codex-credit-plan-view.ts +25 -0
- package/src/cli/dependence-gauge.ts +237 -0
- package/src/cli/doctor-cli.ts +5 -0
- package/src/cli/doctor-modes.ts +48 -0
- package/src/cli/intake-cli.ts +12 -2
- package/src/cli/intake-source-cli.ts +58 -0
- package/src/cli/machine-cli.ts +23 -0
- package/src/cli/msg-cli.ts +137 -0
- package/src/cli/plugin-cli.ts +25 -0
- package/src/cli/pr-cli.ts +36 -0
- package/src/cli/provider-cli.ts +5 -0
- package/src/cli/release-cli.ts +42 -1
- package/src/cli/release-run-if-ready.ts +48 -0
- package/src/cli/setup-cli.ts +27 -6
- package/src/cli/setup-recommend.ts +8 -0
- package/src/cli/usage-cli.ts +11 -1
- package/src/codex/setup.ts +27 -8
- package/src/dashboard/index.ts +1 -1
- package/src/discord.ts +56 -6
- package/src/domains/core-tools.ts +37 -14
- package/src/domains/outbound-alert.ts +50 -11
- package/src/exec-requests/planner.ts +140 -0
- package/src/exec-requests/runner.ts +338 -0
- package/src/exec-requests/store.ts +76 -0
- package/src/field/field-media.ts +8 -3
- package/src/field/field-reel.ts +196 -0
- package/src/grok/credential.ts +7 -5
- package/src/harness/aside-run-status.ts +33 -0
- package/src/harness/browser-verify.ts +48 -17
- package/src/harness/harness-cli-command.ts +25 -3
- package/src/harness/image-load-state.ts +21 -0
- package/src/harness/process-garbage.ts +45 -0
- package/src/index.ts +86 -14
- package/src/instance/leader.ts +13 -1
- package/src/intake-plane/collect-github.ts +5 -3
- package/src/intake-plane/collect-telegram-saved.ts +15 -3
- package/src/intake-plane/discord-seat-work.ts +56 -0
- package/src/intake-plane/intake-sources.ts +202 -0
- package/src/intake-plane/items.ts +28 -12
- package/src/intake-plane/seat-doc-route.ts +207 -0
- package/src/intake-plane/submit-intake-work.ts +9 -1
- package/src/intake-plane/telegram-seat-work.ts +72 -0
- package/src/llm/model-defaults.ts +24 -1
- package/src/llm.ts +5 -4
- package/src/machines/machine-ledger.ts +127 -0
- package/src/market/pack-internal-refs.ts +28 -0
- package/src/market/publish.ts +31 -4
- package/src/maturity/feature-maturity.ts +65 -0
- package/src/model-tier/turn-tier.ts +24 -0
- package/src/msg/msg-store.ts +149 -0
- package/src/native-tool-catalog.ts +2 -1
- package/src/nexus/api/chat-fast-path-config.ts +28 -0
- package/src/nexus/api/consult-requests.ts +117 -0
- package/src/nexus/api/discord-trigger-bot.ts +3 -0
- package/src/nexus/api/exec-requests.ts +51 -0
- package/src/nexus/api/field-uploads.ts +40 -3
- package/src/nexus/api/graph-approvals-feed.ts +140 -0
- package/src/nexus/api/graph-approvals.ts +71 -4
- package/src/nexus/api/http-server.ts +61 -7
- package/src/nexus/api/seat-requests.ts +254 -0
- package/src/nexus/api/trace-run-scan.ts +37 -0
- package/src/nexus/api/trace.ts +19 -3
- package/src/nexus/index.ts +3 -0
- package/src/nexus/setup-status.ts +4 -5
- package/src/notify/llm-budget-alert.ts +16 -10
- package/src/oauth/codex-account-rotation.ts +26 -4
- package/src/oauth/codex-account-store.ts +20 -2
- package/src/oauth/codex-browser-login.ts +173 -0
- package/src/oauth/codex.ts +4 -1
- package/src/oauth/fallback-chain.ts +5 -5
- package/src/oauth/store.ts +42 -5
- package/src/onboarding/continue-here.ts +29 -0
- package/src/onboarding/entry-hints.ts +8 -0
- package/src/onboarding/full-screen-io.ts +35 -3
- package/src/onboarding/help/skills.md +1 -1
- package/src/onboarding/non-interactive.ts +1 -0
- package/src/onboarding/subscription-hint.ts +27 -0
- package/src/onboarding/web-first.ts +83 -0
- package/src/onboarding.ts +230 -23
- package/src/plugins/maker/node-install.ts +92 -0
- package/src/plugins/maker/node-maker.ts +139 -0
- package/src/plugins/maker/plugin-maker.ts +19 -7
- package/src/policy/llm-policy.ts +268 -0
- package/src/prompt-library/session-guidance.ts +7 -1
- package/src/prompt-library/universal-preamble.ts +2 -2
- package/src/relay/relay-cli.ts +20 -0
- package/src/relay/relay-server.ts +220 -0
- package/src/release-loop/checklist.ts +9 -8
- package/src/self-dev/decompose.ts +1 -0
- package/src/self-dev/dev-pipeline.ts +6 -3
- package/src/self-dev/run-record-mirror.ts +44 -0
- package/src/self-dev/run-store.ts +12 -3
- package/src/self-implement/harness-policy-gates.ts +4 -1
- package/src/self-implement/report-origin.ts +12 -1
- package/src/session/chat.ts +17 -5
- package/src/skills/shared-agent-skill-roots.ts +25 -0
- package/src/steward/failure-streak.ts +78 -0
- package/src/steward/steward-cards.ts +24 -0
- package/src/steward/triage-budget.ts +127 -0
- package/src/steward/triage-plan.ts +75 -0
- package/src/steward/triage.ts +244 -42
- package/src/task-cards/card-store.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +11 -5
- package/src/task-orchestrator/surfaces/pod-ledger-events.ts +68 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +78 -4
- package/src/telegram.ts +38 -2
- package/src/tool-runtime/elanous-skills-list-runtime.ts +3 -3
- package/src/tool-runtime/self-cognition-runtimes.ts +160 -2
- package/src/user-config.ts +80 -4
- package/src/version/packed-revision.json +1 -1
- package/src/video-pipeline/recipes/character.ts +42 -5
- package/src/video-pipeline/recipes/ffmpeg.ts +1 -1
- package/src/video-pipeline/recipes/free-line.ts +16 -3
- package/src/video-pipeline/recipes/remote.ts +7 -1
- package/src/video-pipeline/vflow-reference.ts +109 -0
- package/apps/pwa/out/_next/static/chunks/1169-696263ac7af8c4d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1883.c91a2648af4842fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1947-9d64872eeb9cfdec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2808-dc06482994b40f70.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3165-95515ca5f299a120.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3190-e9282753e7ebde85.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4259-99614149577e77cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4283-329eabeaf233e6cd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4328-cb5058b1acc2de5e.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4801-ecee6ef62b349b18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5110-8058791dd25a2e14.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5330-c41e9670f594c38f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5423-e95fd125d1794349.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5478-b5eae07dc79ea619.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5787-96ee6642b9a661e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6448-79d72368589f98bc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6522-9866c002b5531836.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6729-b4a558b7aa891e0c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6969-cb03f9410b7d2e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7007-3b9861091bb8dd21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7930-8dc7e20fc6fc19ca.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8071-187edbc22a86741f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8198-d4101ad70cb9a579.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8250.4a2c5553a07cdf47.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8293-123b91e7b0330dc0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8296-d6d5d87dfa911d41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8305-603963c33ee8eec3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8612-4a0066f20951b8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9181-cf4e1b61c4783f92.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9262-26c2ecbd5a67e4a2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9328-45abdebce4fbecb7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9355-dbc67205adf8745f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9763.255b90dd69875689.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9933-66d31ec25c0231af.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-4e586142da455a25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-889ad5cddcf45b0d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-47ba401d56572c27.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-0b54f9c83458e038.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-4fcc296ef6c8ed90.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-fe9fda494a87f4e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f9642e25ce129590.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e1063e8ba5f3e2e7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-9d0df30027d8aa9d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d97adf2c9455b8d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-236b80d202110362.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-21a30f35f1a276ad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-b1a5be357c4fa25d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-1705a60303f7f573.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-76b337c11ad0e237.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-7c0f85fbb56f64c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-65ca16b20617b44d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-e7f9b8fc29411c1d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-92ea6213d1aeec04.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a913a005aacb5476.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-2d78804a40dfdef6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-08a99148e780ac00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-deb674e7ed36f815.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-49f8de0534d192d7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-bd06fb55f4da0381.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-ba162a28d2176e18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-1ff1a37b550ab41a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-ccaa698f56c0fbdb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-53b944082de1d7db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-9b70adf1552789bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-30d482aa63241fe8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-aecf786f54651d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-6ab9b62c7b6e08f2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-08949735455e0c9a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-a1479005234a46c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-c3890e90bd232732.js +0 -1
- package/apps/pwa/out/_next/static/chunks/beb5cf10-9adcc10b83930090.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-d68d41c88e05c801.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-11c63971b7a9593b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-d09452353bd0d9cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-184d727cebeca767.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-21bbbbac99b720b1.js +0 -1
- package/apps/pwa/out/_next/static/css/eaa74e8bb2f62269.css +0 -1
- package/apps/pwa/out/_next/static/qMI0jnlZaBjjm4aIDYNGM/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{qMI0jnlZaBjjm4aIDYNGM → keajW7DDyghEs1y8oJe6T}/_ssgManifest.js +0 -0
package/src/steward/triage.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { fetchLinearIssues } from '../connectors/linear.js';
|
|
4
4
|
import { getSecretAsync } from '../nexus/config/secrets/index.js';
|
|
@@ -9,6 +9,9 @@ import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
|
9
9
|
import { redactSecrets } from '../task-cards/card-store.js';
|
|
10
10
|
import { directiveHash } from './directive.js';
|
|
11
11
|
import { recordTriageOnCards, type StewardCardDeps } from './steward-cards.js';
|
|
12
|
+
import { triageWithinBudget } from './triage-budget.js';
|
|
13
|
+
import { askSteward, classifyIssue, planIssues, ruleJudgment, unsafeReason, type ClassifiedIssue, type StewardAsk } from './triage-plan.js';
|
|
14
|
+
import { failureAlertText, markStageStart, markStageEnd, readFailureStreak, shouldAlert, writeFailureStreak, type FailureStreak, type StewardStage } from './failure-streak.js';
|
|
12
15
|
|
|
13
16
|
export type Rung = 0 | 1 | 2 | 3 | 4 | 5 | 'hitl';
|
|
14
17
|
/** `other` = the judge chose human review for a reason outside the four named ones (kept as HITL, never downgraded). */
|
|
@@ -19,6 +22,8 @@ export interface TriageDecision {
|
|
|
19
22
|
hitlReason?: HitlReason; why: string; role?: string; cost?: number;
|
|
20
23
|
/** Owner track from `loops.steward.tracks` (absent when no table is configured or the judge picked none of its keys). */
|
|
21
24
|
owner?: string;
|
|
25
|
+
/** Carried from a previous triage without a current judgment; scheduling must not route it. */
|
|
26
|
+
deferred?: true;
|
|
22
27
|
}
|
|
23
28
|
export interface ScheduledDecision extends TriageDecision { disposition: 'now' | 'wait' | 'hitl' }
|
|
24
29
|
export interface StewardSettings {
|
|
@@ -26,6 +31,7 @@ export interface StewardSettings {
|
|
|
26
31
|
roles?: Record<string, { maxConcurrent?: number }>;
|
|
27
32
|
budget?: number;
|
|
28
33
|
tracks?: Record<string, string>;
|
|
34
|
+
alertAfterFailures?: number;
|
|
29
35
|
}
|
|
30
36
|
export interface StewardDeps {
|
|
31
37
|
fetch?: typeof fetch;
|
|
@@ -37,18 +43,15 @@ export interface StewardDeps {
|
|
|
37
43
|
root?: string;
|
|
38
44
|
cardStore?: StewardCardDeps['store'];
|
|
39
45
|
warn?: (message: string) => void;
|
|
46
|
+
ask?: StewardAsk;
|
|
47
|
+
/** Internal command boundary: the current sync has already claimed this pid. */
|
|
48
|
+
stageOwnedByCommand?: boolean;
|
|
40
49
|
}
|
|
41
50
|
|
|
42
51
|
export function stewardSettings(): StewardSettings {
|
|
43
52
|
return getUserConfig().loops?.steward ?? {};
|
|
44
53
|
}
|
|
45
54
|
|
|
46
|
-
const unsafe: Array<[HitlReason, RegExp]> = [
|
|
47
|
-
['money', /돈|유료|결제|구매|판매|송금|환불|크레딧\s*(?:추가|구매)|paid|payment|purchase/i],
|
|
48
|
-
['public', /공개|발행|외부\s*게시|publish|public\s*release/i],
|
|
49
|
-
['security', /보안|(?:키|자격|권한|secret|credential|permission)\s*(?:변경|교체|발급|삭제|공유|노출)|키체인/i],
|
|
50
|
-
['irreversible', /비가역|강제\s*푸시|force.push|영구\s*삭제|리셋권\s*소비|drop\s*table|초기화/i],
|
|
51
|
-
];
|
|
52
55
|
|
|
53
56
|
function parseJudgment(issue: TriageIssue, raw: unknown, tracks?: Record<string, string>): TriageDecision {
|
|
54
57
|
const value = typeof raw === 'string' ? JSON.parse(raw) as unknown : raw;
|
|
@@ -59,8 +62,8 @@ function parseJudgment(issue: TriageIssue, raw: unknown, tracks?: Record<string,
|
|
|
59
62
|
typeof d.priority !== 'number' || !Number.isFinite(d.priority) || typeof d.why !== 'string' || !d.why.trim()) {
|
|
60
63
|
throw new Error(`Invalid triage judgment: ${issue.identifier}`);
|
|
61
64
|
}
|
|
62
|
-
const forced =
|
|
63
|
-
const named = forced ?? (
|
|
65
|
+
const forced = unsafeReason(issue);
|
|
66
|
+
const named = forced ?? (['money', 'public', 'security', 'irreversible'].includes(d.hitlReason as string) ? d.hitlReason as HitlReason : undefined);
|
|
64
67
|
// A HITL judgment without one of the four named reasons stays HITL (`other`) — one issue must not stop the stage.
|
|
65
68
|
const hitlReason = named ?? (d.rung === 'hitl' ? 'other' as const : undefined);
|
|
66
69
|
return {
|
|
@@ -81,30 +84,68 @@ export function trackTablePrompt(tracks?: Record<string, string>): string {
|
|
|
81
84
|
return `\n담당 트랙(owner) — 아래 중 하나(${keys.map(k => JSON.stringify(k)).join('|')}) · 이슈가 어느 트랙 일인지:\n${keys.map(k => `- ${k}: ${tracks[k]}`).join('\n')}`;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
async function
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const decisions: TriageDecision[] = [];
|
|
95
|
-
for (const issue of issues) {
|
|
96
|
-
let result: TriageDecision;
|
|
87
|
+
export async function triageIssues(issues: TriageIssue[], judge?: NonNullable<StewardDeps['judge']>, decide: typeof emitDecision = emitDecision, tracks?: Record<string, string>, previous: Array<TriageDecision & { inputHash?: string }> = [], ask: StewardAsk = askSteward): Promise<TriageDecision[]> {
|
|
88
|
+
const started = judge ? 0 : Date.now();
|
|
89
|
+
const classified: ClassifiedIssue[] = [];
|
|
90
|
+
const classifiedIds = new Set<string>();
|
|
91
|
+
const counts = { rule: 0, classify: 0, planning: 0 };
|
|
92
|
+
const signal = (result: TriageDecision): void => {
|
|
93
|
+
decide({ kind: result.rung === 'hitl' ? 'ESCALATE' : 'ROUTE', what: result.issue, reason: result.why,
|
|
94
|
+
purpose: 'steward triage', target: result.owner ? `${result.rung} · ${result.owner}` : String(result.rung), refs: { issue: result.issue } });
|
|
95
|
+
};
|
|
96
|
+
const { decisions } = await triageWithinBudget({ issues, previous, tracks, judge: async (issue, observedIssues) => {
|
|
97
97
|
try {
|
|
98
|
-
|
|
98
|
+
if (judge) return parseJudgment(issue, await judge(issue, observedIssues ?? issues, tracks), tracks);
|
|
99
|
+
const byRule = ruleJudgment(issue, observedIssues ?? issues);
|
|
100
|
+
if (byRule) { counts.rule++; return { ...byRule, dependsOn: [], priority: 0 }; }
|
|
101
|
+
counts.classify++;
|
|
102
|
+
const row = await classifyIssue(issue, ask);
|
|
103
|
+
return { ...row, dependsOn: [], priority: 0 };
|
|
99
104
|
} catch (error) {
|
|
100
|
-
// A malformed or failed judgment for one issue goes to a human; the other issues are still triaged.
|
|
101
105
|
try { debug.log('steward.stage', 'judgment-invalid', { issue: issue.identifier, reason: redactSecrets(String(error)) }); } catch { /* fail-soft */ }
|
|
102
|
-
|
|
106
|
+
return { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0, why: 'triage judgment unavailable — human review' };
|
|
107
|
+
}
|
|
108
|
+
}, onJudged: result => {
|
|
109
|
+
if (!judge && !result.deferred && !result.duplicateOf &&
|
|
110
|
+
result.why !== 'triage judgment unavailable — human review' &&
|
|
111
|
+
result.why !== 'triage judgment timed out — human review' &&
|
|
112
|
+
result.why !== 'triage deferred — deadline') {
|
|
113
|
+
classified.push({ issue: result.issue, rung: result.rung, why: result.why,
|
|
114
|
+
...(result.hitlReason ? { hitlReason: result.hitlReason } : {}) });
|
|
115
|
+
classifiedIds.add(result.issue);
|
|
116
|
+
} else signal(result);
|
|
117
|
+
} });
|
|
118
|
+
const planningBatch = classified.slice();
|
|
119
|
+
if (!judge && planningBatch.length) {
|
|
120
|
+
const remaining = 240_000 - (Date.now() - started);
|
|
121
|
+
try {
|
|
122
|
+
if (remaining <= 0) throw new Error('planning deadline');
|
|
123
|
+
counts.planning++;
|
|
124
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
125
|
+
let plans;
|
|
126
|
+
try {
|
|
127
|
+
plans = await Promise.race([planIssues(planningBatch, ask, tracks, issues), new Promise<never>((_, reject) => {
|
|
128
|
+
timer = setTimeout(() => reject(new Error('planning deadline')), remaining);
|
|
129
|
+
})]);
|
|
130
|
+
} finally { if (timer) clearTimeout(timer); }
|
|
131
|
+
const byIssue = new Map(plans.map(plan => [plan.issue, plan]));
|
|
132
|
+
for (const row of decisions) {
|
|
133
|
+
const plan = byIssue.get(row.issue);
|
|
134
|
+
if (plan && !row.deferred) {
|
|
135
|
+
row.priority = plan.priority;
|
|
136
|
+
row.dependsOn = plan.dependsOn;
|
|
137
|
+
if (plan.owner) row.owner = plan.owner;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
debug.log('steward.stage', 'planning-invalid', { reason: redactSecrets(String(error)) });
|
|
142
|
+
for (const row of decisions) if (classifiedIds.has(row.issue) && !row.deferred && !row.duplicateOf) {
|
|
143
|
+
row.rung = 'hitl'; row.hitlReason = 'other'; row.why = 'triage judgment unavailable — human review';
|
|
144
|
+
}
|
|
103
145
|
}
|
|
104
|
-
decisions.
|
|
105
|
-
decide({ kind: result.rung === 'hitl' ? 'ESCALATE' : 'ROUTE', what: issue.identifier, reason: result.why,
|
|
106
|
-
purpose: 'steward triage', target: result.owner ? `${result.rung} · ${result.owner}` : String(result.rung), refs: { issue: issue.identifier } });
|
|
146
|
+
for (const row of decisions) if (classifiedIds.has(row.issue) && !row.deferred) signal(row);
|
|
107
147
|
}
|
|
148
|
+
debug.log('steward.triage', 'llm-calls', counts);
|
|
108
149
|
return decisions;
|
|
109
150
|
}
|
|
110
151
|
|
|
@@ -128,7 +169,7 @@ export function scheduleTriage(decisions: TriageDecision[], settings: StewardSet
|
|
|
128
169
|
const role = item.owner ?? item.role ?? String(item.rung);
|
|
129
170
|
const max = settings.roles?.[role]?.maxConcurrent ?? Infinity;
|
|
130
171
|
const blocked = item.dependsOn.some(dep => !completed.has(dep));
|
|
131
|
-
const status: ScheduledDecision['disposition'] = item.rung === 'hitl' || item.hitlReason ? 'hitl' :
|
|
172
|
+
const status: ScheduledDecision['disposition'] = item.deferred || item.rung === 'hitl' || item.hitlReason ? 'hitl' :
|
|
132
173
|
item.rung === 0 || item.duplicateOf || blocked || (slots[role] ?? 0) >= max || (item.cost ?? 0) > remaining ? 'wait' : 'now';
|
|
133
174
|
if (status === 'now') { slots[role] = (slots[role] ?? 0) + 1; remaining -= item.cost ?? 0; }
|
|
134
175
|
return { ...item, disposition: status };
|
|
@@ -147,6 +188,44 @@ async function linear<T>(key: string, query: string, variables: Record<string, u
|
|
|
147
188
|
return body.data;
|
|
148
189
|
}
|
|
149
190
|
|
|
191
|
+
function graphRunId(): string | undefined {
|
|
192
|
+
const path = process.env.ELANOUS_GRAPH_CONTEXT;
|
|
193
|
+
if (!path) return undefined;
|
|
194
|
+
try {
|
|
195
|
+
const context = JSON.parse(readFileSync(path, 'utf8')) as { graphId?: string; runId?: string };
|
|
196
|
+
return context.graphId === 'steward' && /^[a-zA-Z0-9._-]+$/.test(context.runId ?? '') ? context.runId : undefined;
|
|
197
|
+
} catch { return undefined; }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isProcessAlive(pid: number): boolean {
|
|
201
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
202
|
+
try { process.kill(pid, 0); return true; }
|
|
203
|
+
catch (error) { return (error as NodeJS.ErrnoException).code === 'EPERM'; }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function skippedPath(root: string): string {
|
|
207
|
+
// A graph run has a stable id across command nodes. Without a graph context,
|
|
208
|
+
// keep the skip until the next sync: stage subprocess parents may differ.
|
|
209
|
+
const runId = graphRunId();
|
|
210
|
+
return join(root, 'steward', runId ? `skipped-${runId}.json` : 'skipped-no-context.json');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function currentOpen(root: string, stage: StewardStage): FailureStreak['open'] & { pid?: number; runId?: string } {
|
|
214
|
+
const open = readFailureStreak(root).open;
|
|
215
|
+
return open ?? { stage, startedAt: new Date().toISOString() };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function skipOverlappingRun(root: string, open: FailureStreak['open'] & { pid?: number; runId?: string }): void {
|
|
219
|
+
// The skip marker is shared by every stage of one graph run. A loser inside the SAME run must not write it,
|
|
220
|
+
// or the winner's own triage·schedule·report would skip too — it just exits.
|
|
221
|
+
if (open.runId && open.runId === graphRunId()) {
|
|
222
|
+
debug.log('steward.stage', 'skipped-overlap-same-run', { openStage: open.stage, pid: open.pid });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
debug.log('steward.stage', 'skipped-overlap', { openStage: open.stage, pid: open.pid });
|
|
226
|
+
writeFileSync(skippedPath(root), JSON.stringify({ openStage: open.stage, pid: open.pid }));
|
|
227
|
+
}
|
|
228
|
+
|
|
150
229
|
function reportPath(root: string): string { return join(root, 'steward', 'observe.json'); }
|
|
151
230
|
function readState(path: string): { issues: Record<string, string>; digestDay?: string } {
|
|
152
231
|
try { return JSON.parse(readFileSync(path, 'utf8')) as ReturnType<typeof readState>; }
|
|
@@ -157,30 +236,66 @@ function readState(path: string): { issues: Record<string, string>; digestDay?:
|
|
|
157
236
|
export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'report', deps: StewardDeps = {}): Promise<void> {
|
|
158
237
|
const settings = stewardSettings();
|
|
159
238
|
if ((settings.mode ?? 'observe') !== 'observe') throw new Error('steward act mode is out of scope');
|
|
160
|
-
const key = await (deps.getSecret ?? getSecretAsync)('connector.linear.apiKey');
|
|
161
|
-
if (!key) throw new Error('connector.linear.apiKey missing');
|
|
162
239
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
240
|
+
if (stage === 'sync' && !deps.stageOwnedByCommand) {
|
|
241
|
+
return withStageSlot(root, async () => {
|
|
242
|
+
const open = readFailureStreak(root).open as (FailureStreak['open'] & { pid?: number }) | undefined;
|
|
243
|
+
if (open?.pid && isProcessAlive(open.pid)) { skipOverlappingRun(root, open); return; }
|
|
244
|
+
rmSync(skippedPath(root), { force: true });
|
|
245
|
+
await runStewardStage(stage, { ...deps, stageOwnedByCommand: true });
|
|
246
|
+
}, () => { skipOverlappingRun(root, currentOpen(root, stage)); });
|
|
247
|
+
}
|
|
163
248
|
const dir = join(root, 'steward');
|
|
164
249
|
mkdirSync(dir, { recursive: true });
|
|
165
250
|
const snapshot = join(dir, 'issues.json');
|
|
166
251
|
const judgments = join(dir, 'triage.json');
|
|
167
252
|
const schedule = join(dir, 'schedule.json');
|
|
168
253
|
const fetchFn = deps.fetch ?? fetch;
|
|
254
|
+
const skip = skippedPath(root);
|
|
255
|
+
if (stage !== 'sync' && existsSync(skip)) return;
|
|
256
|
+
const key = await (deps.getSecret ?? getSecretAsync)('connector.linear.apiKey');
|
|
257
|
+
if (!key) throw new Error('connector.linear.apiKey missing');
|
|
169
258
|
if (stage === 'sync') {
|
|
170
259
|
const events = await fetchLinearIssues({ apiKey: key, teamKey: settings.linearTeam ?? 'ELA', fetch: fetchFn });
|
|
171
260
|
writeFileSync(snapshot, JSON.stringify(events.map(e => ({ identifier: e.identifier, ref: e.ref, title: e.title, body: e.body }))));
|
|
172
261
|
} else if (stage === 'triage') {
|
|
173
262
|
const issues = JSON.parse(readFileSync(snapshot, 'utf8')) as TriageIssue[];
|
|
174
|
-
const judge = deps.judge
|
|
175
|
-
|
|
263
|
+
const judge = deps.judge;
|
|
264
|
+
let previous: Array<TriageDecision & { inputHash?: string }> = [];
|
|
265
|
+
try {
|
|
266
|
+
const parsed: unknown = JSON.parse(readFileSync(judgments, 'utf8'));
|
|
267
|
+
if (Array.isArray(parsed) && parsed.every((item): item is TriageDecision & { inputHash?: string } =>
|
|
268
|
+
item !== null && typeof item === 'object' && typeof item.issue === 'string' &&
|
|
269
|
+
(item.inputHash === undefined || typeof item.inputHash === 'string') &&
|
|
270
|
+
(item.deferred === undefined || item.deferred === true) &&
|
|
271
|
+
(item.contextInputs === undefined || (Array.isArray(item.contextInputs) &&
|
|
272
|
+
item.contextInputs.every((id: unknown) => typeof id === 'string'))) &&
|
|
273
|
+
[0, 1, 2, 3, 4, 5, 'hitl'].includes(item.rung) &&
|
|
274
|
+
Array.isArray(item.dependsOn) && item.dependsOn.every((dep: unknown) => typeof dep === 'string') &&
|
|
275
|
+
typeof item.priority === 'number' && Number.isFinite(item.priority) && typeof item.why === 'string')) previous = parsed;
|
|
276
|
+
} catch { /* Missing or corrupt previous judgments are an empty cache. */ }
|
|
277
|
+
writeFileSync(judgments, JSON.stringify(await triageIssues(issues, judge, deps.decide, settings.tracks, previous, deps.ask)));
|
|
176
278
|
} else if (stage === 'schedule') {
|
|
177
279
|
const decisions = JSON.parse(readFileSync(judgments, 'utf8')) as TriageDecision[];
|
|
178
280
|
const pending = new Set(decisions.flatMap(decision => decision.dependsOn));
|
|
179
281
|
const completed = new Set<string>();
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
282
|
+
if (pending.size) {
|
|
283
|
+
const ids = [...pending];
|
|
284
|
+
for (let offset = 0; offset < ids.length; offset += 250) {
|
|
285
|
+
const batch = ids.slice(offset, offset + 250);
|
|
286
|
+
let cursor: string | null = null;
|
|
287
|
+
do {
|
|
288
|
+
const data: { issues: { nodes: Array<{ identifier: string; state?: { type: string } }>; pageInfo: { hasNextPage: boolean; endCursor: string | null } } } = await linear(key,
|
|
289
|
+
'query($ids:[String!]!,$after:String){issues(filter:{identifier:{in:$ids}},first:250,after:$after){nodes{identifier state{type}} pageInfo{hasNextPage endCursor}}}',
|
|
290
|
+
{ ids: batch, after: cursor }, fetchFn);
|
|
291
|
+
if (!Array.isArray(data.issues?.nodes) || !data.issues.pageInfo ||
|
|
292
|
+
(data.issues.pageInfo.hasNextPage && (!data.issues.pageInfo.endCursor || data.issues.pageInfo.endCursor === cursor))) {
|
|
293
|
+
throw new Error('Linear dependencies incomplete');
|
|
294
|
+
}
|
|
295
|
+
for (const item of data.issues.nodes) if (pending.has(item.identifier) && item.state?.type === 'completed') completed.add(item.identifier);
|
|
296
|
+
cursor = data.issues.pageInfo.hasNextPage ? data.issues.pageInfo.endCursor : null;
|
|
297
|
+
} while (cursor);
|
|
298
|
+
}
|
|
184
299
|
}
|
|
185
300
|
writeFileSync(schedule, JSON.stringify(scheduleTriage(decisions, settings, {}, completed)));
|
|
186
301
|
} else {
|
|
@@ -222,17 +337,104 @@ async function sendStewardDigest(text: string): Promise<void> {
|
|
|
222
337
|
if (!deliver(text, 'digest')) throw new Error('steward digest delivery failed');
|
|
223
338
|
}
|
|
224
339
|
|
|
340
|
+
export interface StewardCommandDeps extends StewardDeps {
|
|
341
|
+
runStage?: typeof runStewardStage;
|
|
342
|
+
deliverAlert?: (text: string, kind: 'alert') => boolean | Promise<boolean>;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// One stage at a time per instance: an exclusive flock(2) on a stable file, taken in-process through libc
|
|
346
|
+
// (as graph-tick does). The kernel drops it when the process dies, so a killed stage never leaves a stale lock.
|
|
347
|
+
// The `flock` command-line tool is Linux-only — on macOS, where the steward cron runs, it does not exist (10-01).
|
|
348
|
+
const STAGE_LOCK_WAIT_MS = 15 * 60_000;
|
|
349
|
+
|
|
350
|
+
async function withStageSlot<T>(root: string, run: () => Promise<T>, onBusy?: () => T): Promise<T> {
|
|
351
|
+
const dir = join(root, 'steward');
|
|
352
|
+
mkdirSync(dir, { recursive: true });
|
|
353
|
+
const library = process.platform === 'darwin' ? '/usr/lib/libSystem.B.dylib' : process.platform === 'linux' ? 'libc.so.6' : null;
|
|
354
|
+
if (!library) throw new Error(`steward stage lock unavailable on ${process.platform}`);
|
|
355
|
+
const { dlopen, FFIType } = await import('bun:ffi');
|
|
356
|
+
const lib = dlopen(library, { flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 } });
|
|
357
|
+
const flock = (fd: number, operation: number): number => lib.symbols.flock(fd, operation) as number;
|
|
358
|
+
const fd = openSync(join(dir, 'stage-slot.lock'), 'a');
|
|
359
|
+
// LOCK_EX|LOCK_NB (2|4), retried on a timer: a blocking FFI call would freeze the event loop, and a holder in the
|
|
360
|
+
// same process could then never release (the lock is per open file, so a second open in one process contends too).
|
|
361
|
+
// With onBusy (sync), one try only: a busy slot means another run is in flight — skip instead of waiting.
|
|
362
|
+
if (onBusy && flock(fd, 2 | 4) !== 0) { closeSync(fd); lib.close(); return onBusy(); }
|
|
363
|
+
const deadline = Date.now() + STAGE_LOCK_WAIT_MS;
|
|
364
|
+
while (flock(fd, 2 | 4) !== 0) {
|
|
365
|
+
if (Date.now() >= deadline) { closeSync(fd); lib.close(); throw new Error('steward stage lock unavailable: another stage is still running'); }
|
|
366
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
367
|
+
}
|
|
368
|
+
try { return await run(); }
|
|
369
|
+
finally { try { flock(fd, 8); } finally { closeSync(fd); lib.close(); } }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The command-node boundary records both normal exits and an unclosed stage from a killed process. */
|
|
373
|
+
export async function runStewardStageCommand(stage: StewardStage, deps: StewardCommandDeps = {}): Promise<0 | 1> {
|
|
374
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
375
|
+
if (stage !== 'sync' && existsSync(skippedPath(root))) return 0;
|
|
376
|
+
return withStageSlot(root, async () => {
|
|
377
|
+
const now = deps.now ?? (() => new Date());
|
|
378
|
+
const threshold = stewardSettings().alertAfterFailures ?? 3;
|
|
379
|
+
const alertIfNeeded = async (state: FailureStreak): Promise<void> => {
|
|
380
|
+
if (!shouldAlert(state, threshold)) return;
|
|
381
|
+
try {
|
|
382
|
+
const send = deps.deliverAlert ?? (async (text: string, kind: 'alert') => {
|
|
383
|
+
const { deliver } = await import('../domains/outbound-alert.js');
|
|
384
|
+
return deliver(text, kind) !== false;
|
|
385
|
+
});
|
|
386
|
+
if (!await send(failureAlertText(state), 'alert')) throw new Error('steward alert delivery failed');
|
|
387
|
+
state.alertedAt = now().toISOString();
|
|
388
|
+
writeFailureStreak(root, state);
|
|
389
|
+
debug.log('steward.streak', 'alerted', { consecutive: state.consecutive, stage: state.lastStage });
|
|
390
|
+
} catch (error) {
|
|
391
|
+
try { debug.log('steward.streak', 'alert-failed', { consecutive: state.consecutive, stage: state.lastStage,
|
|
392
|
+
reason: redactSecretText(error instanceof Error ? error.message : String(error)) }); } catch { /* alert observability must not change the stage result */ }
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
const open = readFailureStreak(root).open as (FailureStreak['open'] & { pid?: number; runId?: string }) | undefined;
|
|
396
|
+
if (stage === 'sync') {
|
|
397
|
+
if (open?.pid && isProcessAlive(open.pid)) {
|
|
398
|
+
skipOverlappingRun(root, open);
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
rmSync(skippedPath(root), { force: true });
|
|
402
|
+
} else if (existsSync(skippedPath(root))) return 0;
|
|
403
|
+
const hadOpen = open !== undefined;
|
|
404
|
+
const started = markStageStart(root, stage, now());
|
|
405
|
+
if (started.open) {
|
|
406
|
+
Object.assign(started.open, { pid: process.pid, ...(graphRunId() ? { runId: graphRunId() } : {}) });
|
|
407
|
+
writeFailureStreak(root, started);
|
|
408
|
+
}
|
|
409
|
+
if (hadOpen) {
|
|
410
|
+
debug.log('steward.streak', 'recorded', { consecutive: started.consecutive, stage: started.lastStage });
|
|
411
|
+
await alertIfNeeded(started);
|
|
412
|
+
}
|
|
413
|
+
let error: unknown;
|
|
414
|
+
let ok = false;
|
|
415
|
+
try { await (deps.runStage ?? runStewardStage)(stage, { ...deps, stageOwnedByCommand: true }); ok = true; }
|
|
416
|
+
catch (caught) { error = caught; }
|
|
417
|
+
const reason = ok ? '' : redactSecretText(error instanceof Error ? error.message : String(error)).slice(0, 300);
|
|
418
|
+
const state = markStageEnd(root, stage, ok, reason, now());
|
|
419
|
+
debug.log('steward.streak', 'recorded', { consecutive: state.consecutive, stage });
|
|
420
|
+
await alertIfNeeded(state);
|
|
421
|
+
if (!ok) {
|
|
422
|
+
console.error(`steward ${stage}: stage failed — ${reason}`);
|
|
423
|
+
debug.log('steward.stage', 'failed', { stage, reason });
|
|
424
|
+
return 1;
|
|
425
|
+
}
|
|
426
|
+
return 0;
|
|
427
|
+
}, stage === 'sync' ? () => { skipOverlappingRun(root, currentOpen(root, stage)); return 0 as const; } : undefined);
|
|
428
|
+
}
|
|
429
|
+
|
|
225
430
|
if (import.meta.main) {
|
|
226
431
|
const stage = process.argv[2];
|
|
227
432
|
if (!['sync', 'triage', 'schedule', 'report'].includes(stage ?? '')) {
|
|
228
433
|
console.error('steward: expected sync|triage|schedule|report');
|
|
229
434
|
process.exitCode = 2;
|
|
230
435
|
} else {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const reason = redactSecretText(error instanceof Error ? error.message : String(error)).slice(0, 300);
|
|
234
|
-
console.error(`steward ${stage}: stage failed — ${reason}`);
|
|
235
|
-
debug.log('steward.stage', 'failed', { stage, reason });
|
|
436
|
+
runStewardStageCommand(stage as StewardStage).then(code => { process.exitCode = code; }).catch((error: unknown) => {
|
|
437
|
+
console.error(`steward ${stage}: ${redactSecretText(error instanceof Error ? error.message : String(error))}`);
|
|
236
438
|
process.exitCode = 1;
|
|
237
439
|
});
|
|
238
440
|
}
|
|
@@ -38,7 +38,7 @@ type CardEvent =
|
|
|
38
38
|
* A key is `<section>` or `<section>:<idempotency key>`: the same section can be appended many times over a
|
|
39
39
|
* task's life (e.g. workspace diff updates), and the same full key is written once. `incidents` is open to any loop. */
|
|
40
40
|
export const SECTION_OWNERS: Readonly<Record<string, string | null>> = {
|
|
41
|
-
intake: 'steward', triage: 'steward',
|
|
41
|
+
intake: 'steward', triage: 'steward', launch: 'steward', outcome: 'steward',
|
|
42
42
|
gates: 'execution-loop', relations: 'execution-loop', memory: 'execution-loop',
|
|
43
43
|
workspace: 'executor', run: 'executor',
|
|
44
44
|
landing: 'landing-loop', release: 'release-loop',
|
|
@@ -2,6 +2,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } fr
|
|
|
2
2
|
import { gunzipSync } from 'node:zlib';
|
|
3
3
|
import { debug } from '../../debug/log.js';
|
|
4
4
|
import { runLedgerDir, runLedgerPath } from '../../self-implement/run-ledger.js';
|
|
5
|
+
import { ledgerLineToLogEvent } from './pod-ledger-events.js';
|
|
5
6
|
|
|
6
7
|
type Ledger = { runId: string; jsonl: string };
|
|
7
8
|
type Incomplete = { runId: string; reason: string };
|
|
@@ -124,7 +125,7 @@ export function createPodLedgerFollower(opts: {
|
|
|
124
125
|
let owned = false;
|
|
125
126
|
let disabled = false;
|
|
126
127
|
let lastProgressAt = now();
|
|
127
|
-
let
|
|
128
|
+
let visibleProgressEvent: string | null = null;
|
|
128
129
|
let lastActivityMeasuredAt = lastProgressAt;
|
|
129
130
|
let lastActivityLogAt = -Infinity;
|
|
130
131
|
let nextStallMinute = stallMinutes;
|
|
@@ -132,11 +133,11 @@ export function createPodLedgerFollower(opts: {
|
|
|
132
133
|
function checkStall(ledgerRead: boolean, activityMeasured: boolean) {
|
|
133
134
|
const idleMinutes = Math.max(0, (now() - lastProgressAt) / 60_000);
|
|
134
135
|
if (idleMinutes >= nextStallMinute) {
|
|
135
|
-
log('self-implement.pod', 'stalled', { runId: opts.runId, lastProgressEvent, idleMinutes });
|
|
136
|
+
log('self-implement.pod', 'stalled', { runId: opts.runId, lastProgressEvent: visibleProgressEvent, idleMinutes });
|
|
136
137
|
const observation = ledgerRead
|
|
137
138
|
? (activityMeasured ? '원장·작업 트리 모두 조용함' : '원장 조용함')
|
|
138
139
|
: '원장 못 잼';
|
|
139
|
-
onStall(`[pod] 진행 없음 ${Math.floor(idleMinutes)}분 — ${observation} · 마지막 진행 ${
|
|
140
|
+
onStall(`[pod] 진행 없음 ${Math.floor(idleMinutes)}분 — ${observation} · 마지막 진행 ${visibleProgressEvent ?? '없음'}${activityMeasured ? '' : ' (작업 트리 못 잼)'}`);
|
|
140
141
|
stalled = true;
|
|
141
142
|
nextStallMinute = (Math.floor(idleMinutes / stallMinutes) + 1) * stallMinutes;
|
|
142
143
|
}
|
|
@@ -169,7 +170,7 @@ export function createPodLedgerFollower(opts: {
|
|
|
169
170
|
if (stalled) log('self-implement.pod', 'stall-cleared', { runId: opts.runId, idleMinutes });
|
|
170
171
|
stalled = false;
|
|
171
172
|
lastProgressAt = now();
|
|
172
|
-
|
|
173
|
+
visibleProgressEvent = 'worktree-activity';
|
|
173
174
|
nextStallMinute = stallMinutes;
|
|
174
175
|
if (now() - lastActivityLogAt >= 5 * 60_000) {
|
|
175
176
|
log('self-implement.pod', 'worktree-activity', { runId: opts.runId, files });
|
|
@@ -190,6 +191,11 @@ export function createPodLedgerFollower(opts: {
|
|
|
190
191
|
log('self-implement.pod', 'ledger-live-appended', { runId: opts.runId, lines: complete.split('\n').filter(Boolean).length, bytes: Buffer.byteLength(complete), offset });
|
|
191
192
|
for (const line of complete.split('\n')) {
|
|
192
193
|
if (!line) continue;
|
|
194
|
+
let logEvent: ReturnType<typeof ledgerLineToLogEvent>;
|
|
195
|
+
try {
|
|
196
|
+
logEvent = ledgerLineToLogEvent(line, opts.runId);
|
|
197
|
+
if (logEvent) log(logEvent.category, logEvent.event, logEvent.data);
|
|
198
|
+
} catch { /* A failed host log write must not interrupt ledger following or stall accounting. */ }
|
|
193
199
|
let entry: unknown;
|
|
194
200
|
try { entry = JSON.parse(line); } catch { continue; }
|
|
195
201
|
const event = (entry && typeof entry === 'object' && 'event' in entry) ? entry.event : undefined;
|
|
@@ -198,7 +204,7 @@ export function createPodLedgerFollower(opts: {
|
|
|
198
204
|
if (stalled) log('self-implement.pod', 'stall-cleared', { runId: opts.runId, idleMinutes });
|
|
199
205
|
stalled = false;
|
|
200
206
|
lastProgressAt = now();
|
|
201
|
-
|
|
207
|
+
visibleProgressEvent = logEvent?.event ?? '[redacted]';
|
|
202
208
|
nextStallMinute = stallMinutes;
|
|
203
209
|
}
|
|
204
210
|
checkStall(true, activityMeasured);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const SECRET_FIELD = /token|secret|password|authorization|key/i;
|
|
2
|
+
const SAFE_FIELD = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
|
|
3
|
+
const SECRET_VALUE = /(?:\b(?:bearer|basic)\s+\S+|\b(?:token|secret|password|authorization|api[-_]?key|access[-_]?key|private[-_]?key)\b\s*(?:is|[=:])\s*["']?\S+|\b(?:sk-[A-Za-z0-9_-]{8,}|gh[pousr]_[A-Za-z0-9_]{8,}))\b/i;
|
|
4
|
+
const MAX_FIELDS = 32;
|
|
5
|
+
// Review R2: only lifecycle events the host needs to see, and no free text. Event names must start with a known
|
|
6
|
+
// lifecycle prefix; string fields survive only when they are short code-like values in a fixed set of keys.
|
|
7
|
+
const EVENT_ALLOWED = /^(?:gate|gated|review|reviewed|repair|repaired|rework|merge|merged|pr|land|landed|stage|round|supervisor|child|plan|planned|decompose|decomposed|shard|stall|harvest|decision|run|pipeline|progress|human|start|started|finish|finished|done|implement|implemented|commit|test|typecheck)(?:-(?:started|finished|round|opened|closed|failed|passed|blocked|done|entry|node|outcome|delivery|decision|stop|merged|ready|requested|resolved|result|fix|must-fix|verdict|skipped|origin|cleared))*$/;
|
|
8
|
+
const CODE_FIELDS = new Set(['stage', 'outcome', 'verdict', 'status', 'kind', 'phase', 'role', 'result']);
|
|
9
|
+
const CODE_VALUE = /^[a-z0-9_.:-]{1,40}$/i;
|
|
10
|
+
|
|
11
|
+
function secretValues(root: Record<string, unknown>): string[] {
|
|
12
|
+
const secrets: string[] = [];
|
|
13
|
+
const pending: unknown[] = [root];
|
|
14
|
+
while (pending.length) {
|
|
15
|
+
const current = pending.pop();
|
|
16
|
+
if (!current || typeof current !== 'object') continue;
|
|
17
|
+
if (Array.isArray(current)) {
|
|
18
|
+
pending.push(...current);
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
for (const [name, value] of Object.entries(current)) {
|
|
22
|
+
if (SECRET_FIELD.test(name) && typeof value === 'string' && value.length >= 3) secrets.push(value);
|
|
23
|
+
if (value && typeof value === 'object') pending.push(value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return secrets;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Project bounded ledger metadata, excluding named secrets and recognizable credential values. */
|
|
30
|
+
export function ledgerLineToLogEvent(line: string, runId: string): {
|
|
31
|
+
category: string;
|
|
32
|
+
event: string;
|
|
33
|
+
data: Record<string, unknown>;
|
|
34
|
+
} | undefined {
|
|
35
|
+
let entry: unknown;
|
|
36
|
+
try { entry = JSON.parse(line); } catch { return undefined; }
|
|
37
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry) || !('event' in entry) || typeof entry.event !== 'string') return undefined;
|
|
38
|
+
// The real run-ledger writer wraps observer fields in `data`; older/synthetic lines can be flat.
|
|
39
|
+
const details = 'data' in entry && entry.data && typeof entry.data === 'object' && !Array.isArray(entry.data)
|
|
40
|
+
? entry.data as Record<string, unknown> : undefined;
|
|
41
|
+
const sources = [entry, details].filter((source): source is Record<string, unknown> => source !== undefined);
|
|
42
|
+
const knownSecrets = secretValues(entry);
|
|
43
|
+
const containsCredential = (value: string) => SECRET_VALUE.test(value) || knownSecrets.some((secret) => value.includes(secret));
|
|
44
|
+
if (containsCredential(entry.event) || !EVENT_ALLOWED.test(entry.event)) return undefined;
|
|
45
|
+
|
|
46
|
+
const data: Record<string, unknown> = { runId };
|
|
47
|
+
let fields = 0;
|
|
48
|
+
for (const source of sources) {
|
|
49
|
+
for (const [name, value] of Object.entries(source)) {
|
|
50
|
+
if (name === 'event' || name === 'runId' || name === 'data' || !SAFE_FIELD.test(name) || SECRET_FIELD.test(name)) continue;
|
|
51
|
+
if (fields >= MAX_FIELDS) break;
|
|
52
|
+
const countName = `${name}Count`;
|
|
53
|
+
const outputName = Array.isArray(value) || value && typeof value === 'object' ? countName : name;
|
|
54
|
+
if (!SAFE_FIELD.test(outputName) || SECRET_FIELD.test(outputName) || Object.hasOwn(data, outputName)) continue;
|
|
55
|
+
if (typeof value === 'string') {
|
|
56
|
+
// Free text (reasons, messages, patches) never leaves the pod ledger; only short code values in known keys.
|
|
57
|
+
if (!CODE_FIELDS.has(name) || !CODE_VALUE.test(value) || containsCredential(value)) continue;
|
|
58
|
+
data[name] = value;
|
|
59
|
+
} else if (typeof value === 'number' && Number.isFinite(value) || typeof value === 'boolean' || value === null) {
|
|
60
|
+
data[name] = value;
|
|
61
|
+
} else if (Array.isArray(value) || value && typeof value === 'object') {
|
|
62
|
+
data[countName] = Array.isArray(value) ? value.length : Object.keys(value).length;
|
|
63
|
+
} else continue;
|
|
64
|
+
fields += 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { category: 'self-implement.pod.ledger', event: entry.event, data };
|
|
68
|
+
}
|