elanous 0.2.6 → 0.2.8
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-3084c58c1725a40b.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/7032-f4ef698518fba61c.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-d976b441a104ab74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.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-8f5ff96c522b5f55.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-9aa47b0bc6d570ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-d4b6df905f185055.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a70ba27c33003dd0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-186fc6952f027acc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-18166f885d831d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-147a328753d983e2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-799395a9df7d95aa.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-6a8158b71d1fe9d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-e7ed3d010153d818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-c0c0d19d51342d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2e002ad8faa43291.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/page-b672e02e4bec8deb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-70ee65f5f7a151dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-eb52580c4b75b905.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-7464c1987dd76e6d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-e573437b0a1c728f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.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-085847b088675978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d8ab4d1489223459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-82efa177a30f3741.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-647ed2b66c1d49f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.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/5ef72a618a8994f3.css +1 -0
- package/apps/pwa/out/_next/static/lywc8m_HDzH9MKSDQhyEj/_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/graphs/test-diet/recipes.yaml +9 -0
- package/graphs/test-diet/test-diet.yaml +22 -0
- package/package.json +6 -5
- package/persona-presets/_index.yaml +10 -0
- package/persona-presets/_schema.json +146 -0
- package/persona-presets/dana.yaml +26 -0
- package/persona-presets/haru.yaml +27 -0
- package/persona-presets/kai.yaml +27 -0
- package/persona-presets/leo.yaml +27 -0
- package/persona-presets/mira.yaml +30 -0
- package/persona-presets/nari.yaml +27 -0
- package/persona-presets/sora.yaml +27 -0
- package/persona-presets/yuna.yaml +27 -0
- 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 +58 -13
- 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 +57 -0
- package/scripts/field-feed/feed-node.ts +67 -0
- package/scripts/field-feed/lib.ts +164 -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 +109 -21
- 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/persona-presets/lint.ts +79 -0
- package/scripts/public-export.ts +8 -4
- package/scripts/release-loop/baseline-from-logs.ts +92 -0
- package/scripts/release-loop/checklist-gate-node.ts +2 -2
- package/scripts/release-loop/env-known-failures.ts +31 -0
- package/scripts/release-loop/gate-node.ts +108 -22
- package/scripts/release-loop/known-issues-node.ts +159 -0
- package/scripts/release-loop/npm-publish-node.ts +124 -0
- package/scripts/release-loop/ops-upgrade-node.ts +95 -0
- package/scripts/release-loop/publish-node.ts +23 -1
- package/scripts/release-loop/release-readiness.ts +58 -0
- package/scripts/release-loop/shard-plan.ts +17 -0
- package/scripts/release-loop/unattended-release.ts +93 -0
- package/scripts/storyboard/storyboard.ts +167 -0
- package/scripts/test-diet/lib.ts +88 -0
- package/scripts/test-diet/node.ts +102 -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/auth-methods.ts +30 -0
- package/src/acp/elanous-aware-peers.ts +30 -0
- package/src/acp/server.ts +96 -20
- 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/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -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/config-test-sync.ts +4 -0
- package/src/cli/decisions-cli.ts +13 -3
- package/src/cli/dependence-gauge.ts +237 -0
- package/src/cli/doctor-bundle.ts +128 -0
- package/src/cli/doctor-cli.ts +41 -1
- package/src/cli/doctor-local-llm.ts +95 -0
- package/src/cli/doctor-modes.ts +48 -0
- package/src/cli/import-cli.ts +60 -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/mcp-login.ts +13 -4
- package/src/cli/msg-cli.ts +137 -0
- package/src/cli/persona-cli.ts +116 -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 +50 -8
- 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/skills-cli.ts +173 -0
- package/src/cli/start-cli.ts +13 -4
- package/src/cli/usage-cli.ts +11 -1
- package/src/codex/setup.ts +37 -16
- package/src/dashboard/index.ts +18 -2
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- package/src/decisions/decision-cards.ts +291 -0
- package/src/decisions/decision-ledger.ts +5 -2
- package/src/decisions/discord-decision-cards.ts +133 -0
- package/src/decisions/telegram-decision-cards.ts +100 -0
- package/src/directives/version-at.ts +77 -0
- package/src/discord.ts +73 -6
- package/src/domains/core-tools.ts +37 -14
- package/src/domains/outbound-alert.ts +94 -12
- package/src/domains/repeated-failure.ts +1 -1
- package/src/error-report/report.ts +200 -0
- package/src/exec-requests/planner.ts +146 -0
- package/src/exec-requests/runner.ts +378 -0
- package/src/exec-requests/store.ts +78 -0
- package/src/field/field-feed-auto.ts +48 -0
- package/src/field/field-media.ts +8 -3
- package/src/field/field-reel.ts +203 -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/hooks/error-report.ts +210 -0
- package/src/hooks/receiver.ts +41 -1
- package/src/import/apply.ts +128 -0
- package/src/import/plan.ts +197 -0
- package/src/index.ts +168 -21
- package/src/input-core/text-entry.ts +13 -5
- 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-answer.ts +96 -0
- package/src/intake-plane/seat-doc-route.ts +221 -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 +128 -0
- package/src/nexus/api/discord-trigger-bot.ts +3 -0
- package/src/nexus/api/error-reports.ts +108 -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 +82 -8
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -0
- package/src/nexus/api/seat-requests.ts +254 -0
- package/src/nexus/api/share-captures.ts +91 -0
- package/src/nexus/api/skill-problems.ts +56 -0
- package/src/nexus/api/terminals.ts +24 -8
- package/src/nexus/api/trace-run-scan.ts +37 -0
- package/src/nexus/api/trace.ts +19 -3
- package/src/nexus/index.ts +28 -1
- package/src/nexus/setup-status.ts +11 -7
- package/src/notify/llm-budget-alert.ts +16 -10
- package/src/oauth/browser-availability.ts +14 -0
- 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-quota-policy.ts +16 -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 +34 -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 +90 -0
- package/src/onboarding.ts +287 -26
- package/src/persona/presets.ts +105 -0
- 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 +37 -13
- package/src/self-dev/decompose.ts +1 -0
- package/src/self-dev/dev-pipeline.ts +6 -3
- package/src/self-dev/orchestrate.ts +18 -11
- 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/shell-env-bootstrap.ts +44 -7
- package/src/skills/connect-claude-plugin.ts +142 -0
- package/src/skills/connect.ts +70 -0
- package/src/skills/index.ts +29 -1
- package/src/skills/repair.ts +124 -0
- package/src/skills/runner.ts +11 -2
- package/src/skills/shared-agent-skill-roots.ts +25 -0
- package/src/skills/trigger-extract.ts +1 -1
- 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-command-job.ts +16 -8
- 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-commands.ts +13 -0
- package/src/telegram.ts +72 -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 +189 -15
- 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 → lywc8m_HDzH9MKSDQhyEj}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { debug } from '../debug/log.js';
|
|
3
|
+
import type { TriageDecision, TriageIssue } from './triage.js';
|
|
4
|
+
|
|
5
|
+
export interface HashedTriageDecision extends TriageDecision {
|
|
6
|
+
inputHash: string;
|
|
7
|
+
/** Other issues whose ref/body the judge read while producing this decision. */
|
|
8
|
+
contextInputs?: string[];
|
|
9
|
+
/** Retained from a previous run without a current judgment; never route as a fresh decision. */
|
|
10
|
+
deferred?: true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function triageInputHash(issue: TriageIssue, issues: TriageIssue[] = [], tracks?: Record<string, string>, contextInputs: string[] = []): string {
|
|
14
|
+
const byId = new Map(issues.map(item => [item.identifier, item]));
|
|
15
|
+
return createHash('sha256').update(JSON.stringify([
|
|
16
|
+
'context-inputs-v2', issue.identifier, issue.ref, issue.title, issue.body,
|
|
17
|
+
issues.map(item => [item.identifier, item.title]), tracks && Object.keys(tracks).length ? tracks : null,
|
|
18
|
+
contextInputs.map(id => [id, byId.get(id)?.ref ?? null, byId.get(id)?.body ?? null]),
|
|
19
|
+
])).digest('hex');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function triageWithinBudget({ issues, previous, judge, tracks, concurrency = 8, perIssueMs = 90_000, deadlineMs = 240_000, now = Date.now, onJudged }: {
|
|
23
|
+
issues: TriageIssue[];
|
|
24
|
+
previous: Array<TriageDecision & { inputHash?: string }>;
|
|
25
|
+
judge: (issue: TriageIssue, observedIssues?: TriageIssue[]) => Promise<TriageDecision>;
|
|
26
|
+
tracks?: Record<string, string>;
|
|
27
|
+
concurrency?: number;
|
|
28
|
+
perIssueMs?: number;
|
|
29
|
+
deadlineMs?: number;
|
|
30
|
+
now?: () => number;
|
|
31
|
+
onJudged?: (decision: HashedTriageDecision) => void;
|
|
32
|
+
}): Promise<{ decisions: HashedTriageDecision[]; stats: { reused: number; judged: number; timedOut: number; deferred: number } }> {
|
|
33
|
+
const end = now() + deadlineMs;
|
|
34
|
+
const slots = Number.isFinite(concurrency) ? Math.max(1, Math.floor(concurrency)) : 4;
|
|
35
|
+
const stats = { reused: 0, judged: 0, timedOut: 0, deferred: 0 };
|
|
36
|
+
const old = new Map(previous.map(decision => [decision.issue, decision]));
|
|
37
|
+
const isTemporary = (decision: TriageDecision): boolean =>
|
|
38
|
+
decision.rung === 'hitl' && decision.hitlReason === 'other' &&
|
|
39
|
+
['triage deferred — deadline', 'triage judgment timed out — human review',
|
|
40
|
+
'triage judgment unavailable — human review'].includes(decision.why);
|
|
41
|
+
const decisions: HashedTriageDecision[] = new Array(issues.length);
|
|
42
|
+
const pending: number[] = [];
|
|
43
|
+
for (const [index, issue] of issues.entries()) {
|
|
44
|
+
const earlier = old.get(issue.identifier) as HashedTriageDecision | undefined;
|
|
45
|
+
const hash = triageInputHash(issue, issues, tracks, earlier?.contextInputs ?? []);
|
|
46
|
+
if (earlier?.inputHash === hash && !earlier.deferred && !isTemporary(earlier)) {
|
|
47
|
+
decisions[index] = { ...earlier, inputHash: hash };
|
|
48
|
+
stats.reused++;
|
|
49
|
+
} else pending.push(index);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let cursor = 0;
|
|
53
|
+
const activeWork = new Set<Promise<TriageDecision>>();
|
|
54
|
+
const settleSignals = new Set<Promise<boolean>>();
|
|
55
|
+
const timedOut = Symbol('triage-timeout');
|
|
56
|
+
const worker = async (): Promise<void> => {
|
|
57
|
+
while (cursor < pending.length) {
|
|
58
|
+
const remaining = end - now();
|
|
59
|
+
if (remaining <= 0) return;
|
|
60
|
+
if (activeWork.size >= slots) {
|
|
61
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
62
|
+
const settled = Promise.race([...settleSignals]);
|
|
63
|
+
let deadlineElapsed: boolean;
|
|
64
|
+
try {
|
|
65
|
+
deadlineElapsed = await Promise.race([settled, new Promise<true>(resolve => { timer = setTimeout(() => resolve(true), remaining); })]);
|
|
66
|
+
} finally { if (timer) clearTimeout(timer); }
|
|
67
|
+
if (deadlineElapsed) return;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const index = pending[cursor++]!;
|
|
71
|
+
const issue = issues[index]!;
|
|
72
|
+
const observed = new Set<string>();
|
|
73
|
+
const observedIssues = issues.map(item => new Proxy(item, {
|
|
74
|
+
get(target, property, receiver) {
|
|
75
|
+
if (item.identifier !== issue.identifier && (property === 'ref' || property === 'body')) observed.add(item.identifier);
|
|
76
|
+
return Reflect.get(target, property, receiver);
|
|
77
|
+
},
|
|
78
|
+
}));
|
|
79
|
+
// Check immediately before invoking; a clock that advanced while preparing inputs must not start a new call.
|
|
80
|
+
const issueRemaining = end - now();
|
|
81
|
+
if (issueRemaining <= 0) { cursor--; return; }
|
|
82
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
83
|
+
let decision: TriageDecision;
|
|
84
|
+
try {
|
|
85
|
+
const work = Promise.resolve(judge(issue, observedIssues));
|
|
86
|
+
// Even a timed-out judge keeps its concurrency slot until the underlying call settles.
|
|
87
|
+
activeWork.add(work);
|
|
88
|
+
const settled = work.then(() => false, () => false);
|
|
89
|
+
void settled.then(() => {
|
|
90
|
+
activeWork.delete(work);
|
|
91
|
+
settleSignals.delete(settled);
|
|
92
|
+
});
|
|
93
|
+
settleSignals.add(settled);
|
|
94
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
95
|
+
timer = setTimeout(() => reject(timedOut), Math.min(perIssueMs, issueRemaining));
|
|
96
|
+
});
|
|
97
|
+
decision = await Promise.race([work, timeout]);
|
|
98
|
+
stats.judged++;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error === timedOut) {
|
|
101
|
+
stats.timedOut++;
|
|
102
|
+
decision = { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0, why: 'triage judgment timed out — human review' };
|
|
103
|
+
} else {
|
|
104
|
+
stats.judged++;
|
|
105
|
+
decision = { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0, why: 'triage judgment unavailable — human review' };
|
|
106
|
+
}
|
|
107
|
+
} finally { if (timer) clearTimeout(timer); }
|
|
108
|
+
const contextInputs = [...observed].sort();
|
|
109
|
+
decisions[index] = { ...decision, inputHash: triageInputHash(issue, issues, tracks, contextInputs),
|
|
110
|
+
...(contextInputs.length ? { contextInputs } : {}) };
|
|
111
|
+
onJudged?.(decisions[index]!);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
await Promise.all(Array.from({ length: Math.min(slots, pending.length) }, () => worker()));
|
|
115
|
+
for (const index of pending.slice(cursor)) {
|
|
116
|
+
const issue = issues[index]!;
|
|
117
|
+
const earlier = old.get(issue.identifier);
|
|
118
|
+
decisions[index] = earlier
|
|
119
|
+
? { ...earlier, inputHash: earlier.inputHash ?? '', deferred: true }
|
|
120
|
+
: { issue: issue.identifier, rung: 'hitl', hitlReason: 'other', dependsOn: [], priority: 0,
|
|
121
|
+
why: 'triage deferred — deadline', inputHash: triageInputHash(issue, issues, tracks), deferred: true };
|
|
122
|
+
if (!earlier) onJudged?.(decisions[index]!);
|
|
123
|
+
stats.deferred++;
|
|
124
|
+
}
|
|
125
|
+
debug.log('steward.triage', 'budget', stats);
|
|
126
|
+
return { decisions, stats };
|
|
127
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { resolveRoleModel } from '../user-config.js';
|
|
2
|
+
import type { HitlReason, Rung, TriageIssue } from './triage.js';
|
|
3
|
+
|
|
4
|
+
export type StewardAsk = (prompt: string, role: 'classify' | 'planning') => Promise<unknown>;
|
|
5
|
+
export interface ClassifiedIssue { issue: string; rung: Rung; why: string; hitlReason?: HitlReason; duplicateOf?: string }
|
|
6
|
+
export interface IssuePlan { issue: string; priority: number; dependsOn: string[]; owner?: string }
|
|
7
|
+
|
|
8
|
+
const unsafe: Array<[HitlReason, RegExp]> = [
|
|
9
|
+
['money', /돈|유료|결제|구매|판매|송금|환불|크레딧\s*(?:추가|구매)|paid|payment|purchase/i],
|
|
10
|
+
['public', /공개|발행|외부\s*게시|publish|public\s*release/i],
|
|
11
|
+
['security', /보안|(?:API|보안|인증|접근)\s*키|(?:키|자격|권한|secret|credential|permission)\s*(?:변경|교체|발급|삭제|공유|노출)|키체인|\b(?:api[ -]?key|secret[ -]?key|access[ -]?token)\b/i],
|
|
12
|
+
['irreversible', /비가역|강제\s*푸시|force.push|영구\s*삭제|리셋권\s*소비|drop\s*table|초기화/i],
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export function unsafeReason(issue: TriageIssue): HitlReason | undefined {
|
|
16
|
+
return unsafe.find(([, pattern]) => pattern.test(`${issue.title}\n${issue.body}`))?.[0];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parsed(raw: unknown): Record<string, unknown> {
|
|
20
|
+
const value: unknown = typeof raw === 'string'
|
|
21
|
+
? JSON.parse(raw.replace(/^```(?:json)?\s*|\s*```$/g, '').trim()) : raw;
|
|
22
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid steward response');
|
|
23
|
+
return value as Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ruleJudgment(issue: TriageIssue, issues: TriageIssue[]): ClassifiedIssue | undefined {
|
|
27
|
+
const normalize = (text: string) => text.normalize('NFKC').trim().replace(/\s+/g, ' ').toLocaleLowerCase();
|
|
28
|
+
const title = normalize(issue.title);
|
|
29
|
+
const index = issues.findIndex(item => item.identifier === issue.identifier);
|
|
30
|
+
const prior = index < 0 ? issues.filter(item => item.identifier !== issue.identifier) : issues.slice(0, index);
|
|
31
|
+
// Same title alone is not proof: two different tasks can share a title. Only same title AND same body is a
|
|
32
|
+
// rule duplicate; anything else goes to classification.
|
|
33
|
+
const body = normalize(issue.body ?? '');
|
|
34
|
+
const duplicate = title && prior.find(item => normalize(item.title) === title && normalize(item.body ?? '') === body);
|
|
35
|
+
if (duplicate) return { issue: issue.identifier, rung: 0, duplicateOf: duplicate.identifier, why: `duplicate of ${duplicate.identifier}` };
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function askSteward(prompt: string, role: 'classify' | 'planning'): Promise<unknown> {
|
|
40
|
+
const { streamLLM } = await import('../llm.js');
|
|
41
|
+
const result = await streamLLM([{ role: 'user', content: prompt }], () => {}, { model: resolveRoleModel(role).model });
|
|
42
|
+
return parsed(result);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function classifyIssue(issue: TriageIssue, ask: StewardAsk = askSteward): Promise<ClassifiedIssue> {
|
|
46
|
+
const candidate = unsafeReason(issue);
|
|
47
|
+
const response = parsed(await ask(`스튜어드 분류. JSON 객체만: {"rung":0|1|2|3|4|5|"hitl","hitlReason":null,"why":"한 문장"}. 0=이미됨, 1=셸, 2=CLI, 3=조사, 4=하니스 골, 5=외부 에이전트. 돈·공개·보안·비가역은 hitl; 그 외 사람 확인은 other. 확인되지 않은 완료를 추측하지 마라. ${candidate ? `안전 후보 ${candidate}: 이유를 확인하라.` : ''}\n이슈: ${JSON.stringify(issue)}`, 'classify'));
|
|
48
|
+
if (![0, 1, 2, 3, 4, 5, 'hitl'].includes(response.rung as Rung) || typeof response.why !== 'string' || !response.why.trim()) throw new Error(`Invalid steward classification: ${issue.identifier}`);
|
|
49
|
+
const reason = candidate ?? (unsafe.some(([name]) => name === response.hitlReason) ? response.hitlReason as HitlReason : undefined)
|
|
50
|
+
?? (response.rung === 'hitl' ? 'other' : undefined);
|
|
51
|
+
return { issue: issue.identifier, rung: reason ? 'hitl' : response.rung as Rung,
|
|
52
|
+
...(reason ? { hitlReason: reason } : {}), why: response.why.trim().replace(/\s+/g, ' ') };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One request for the current batch, never one request per issue. */
|
|
56
|
+
export async function planIssues(classified: ClassifiedIssue[], ask: StewardAsk = askSteward, tracks?: Record<string, string>, issues: TriageIssue[] = []): Promise<IssuePlan[]> {
|
|
57
|
+
if (!classified.length) return [];
|
|
58
|
+
const keys = Object.keys(tracks ?? {});
|
|
59
|
+
const byId = new Map(issues.map(issue => [issue.identifier, issue]));
|
|
60
|
+
const response = parsed(await ask(`스튜어드 계획. 새로 분류한 이슈마다 priority(숫자), dependsOn(제공된 이슈 키 배열), owner(담당 트랙 키 또는 null)를 정해 JSON {"plans":[{"issue":"키","priority":1,"dependsOn":[],"owner":null}]} 만 출력. 새로 분류한 이슈만 한 번씩 포함하라. 다른 키를 의존으로 넣지 마라. 담당 트랙: ${JSON.stringify(tracks ?? {})}\n제공된 이슈 키: ${JSON.stringify(issues.map(row => ({ key: row.identifier, title: row.title })))}\n새로 분류한 이슈: ${JSON.stringify(classified.map(row => ({ ...row, title: byId.get(row.issue)?.title ?? '', body: byId.get(row.issue)?.body ?? '' })))}`, 'planning'));
|
|
61
|
+
const plans = response.plans;
|
|
62
|
+
if (!Array.isArray(plans) || plans.length !== classified.length) throw new Error('Invalid steward plan');
|
|
63
|
+
const allowed = new Set(issues.length ? issues.map(row => row.identifier) : classified.map(row => row.issue));
|
|
64
|
+
const planned = new Set(classified.map(row => row.issue));
|
|
65
|
+
const seen = new Set<string>();
|
|
66
|
+
return plans.map((raw: unknown) => {
|
|
67
|
+
const row = parsed(raw);
|
|
68
|
+
if (typeof row.issue !== 'string' || !planned.has(row.issue) || seen.has(row.issue) ||
|
|
69
|
+
typeof row.priority !== 'number' || !Number.isFinite(row.priority) ||
|
|
70
|
+
!Array.isArray(row.dependsOn) || !row.dependsOn.every((dep: unknown) => typeof dep === 'string' && allowed.has(dep) && dep !== row.issue)) throw new Error('Invalid steward plan');
|
|
71
|
+
seen.add(row.issue);
|
|
72
|
+
return { issue: row.issue, priority: row.priority, dependsOn: row.dependsOn as string[],
|
|
73
|
+
...(typeof row.owner === 'string' && keys.includes(row.owner) ? { owner: row.owner } : {}) };
|
|
74
|
+
});
|
|
75
|
+
}
|
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',
|