elanous 0.2.9 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/Y4zKzL62sakG5momz3SoQ/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1394-556de6d71bbdb0fb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1452.93700483b9d4fc61.js +9 -0
- package/apps/pwa/out/_next/static/chunks/{6062-73e68343a9814f6a.js → 1829-b453a1b22d3999bb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +1 -0
- package/apps/pwa/out/_next/static/chunks/390.c373e31b3b9d0c41.js +5 -0
- package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +25 -0
- package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4685-c3c1c625b71accd1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +2 -0
- package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8046cadf-211ca3c0e00f2b72.js → 5cd7e3a9-404fd6b8d0ecd44c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4323.c5dfb6f503304775.js → 7334.a6679d9168f37aa8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/79f8be9e-957d68913b590b08.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8039-df92e08fede5de8f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8421-1c4acfb1395a0edf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8605-ea5bee9dfae512c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2415.ed6428ff334c7ce2.js → 9269.d2a4795bc6812717.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9656-c1540088f5859c80.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-6a061bd97f0b9398.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-f972fc1c00a62e88.js → page-b12b6d2f8d2d1aea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-7c97dcdfe2e37a60.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-5721537ca8a3598c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-b654240a572e4c48.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-b9750abc897508c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-c8b1ccda6ba7ae98.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-d6b1d156fb95e74b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-9688f5b4f1ff950f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-adfbd2ba9598e59a.js → page-35414e0a007b4082.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-e001823c35c9dd67.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-371423d1a573d64b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4b764b125339ac17.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cdf8e4b01067bcf1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-3a24072b35470157.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d37cb0d52f9b6bd5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-686da0df9bf23e5c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4dc614f3ce043e63.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d02c782fc792e2af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-239c17b911b84e72.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-dc6a79fb003f746c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-0fffb84cafdc9da6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-1218753c82d585f6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-4b5935e67531efb5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-3285bc87caf1eaa5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-ad553b0e06505ecd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-fca096e3e860306f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-9ebfa78f0d9885c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-3dc5f21f7e39622d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{2ea96958-e8c7e112de50b067.js → f48170cc-d05d28bf87a43bf4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +1 -0
- package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +23 -23
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -0
- package/apps/pwa/out/inside/index.txt +31 -0
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -0
- package/apps/pwa/out/ops/seats/index.txt +30 -0
- package/apps/pwa/out/outputs/index.html +1 -0
- package/apps/pwa/out/outputs/index.txt +30 -0
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -24
- package/apps/pwa/out/today/index.html +1 -0
- package/apps/pwa/out/today/index.txt +33 -0
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -24
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/drafts/draft-cleanup.yaml +19 -0
- package/graphs/drafts/recipes.yaml +23 -0
- package/graphs/field/field-feed.yaml +2 -0
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/intake/intake-daily.yaml +2 -1
- package/graphs/ops/recipes.yaml +3 -0
- package/graphs/ops/secret-exposure.yaml +24 -0
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -1
- package/graphs/video/explainer-line.yaml +2 -2
- package/package.json +2 -1
- package/plugins/card-followup/graphs/run-step.ts +48 -13
- package/plugins/doc-draft/graphs/doc-draft.yaml +3 -3
- package/plugins/doc-draft/graphs/run-step.ts +13 -6
- package/plugins/elanous-hwp/README.md +25 -0
- package/plugins/elanous-hwp/cli.ts +22 -0
- package/plugins/elanous-hwp/converter.ts +237 -0
- package/plugins/elanous-hwp/fixtures/fixed-controls.hwpx +0 -0
- package/plugins/elanous-hwp/fixtures/fixed-table-controls.hwpx +0 -0
- package/plugins/elanous-hwp/fixtures/fixed.hwpx +0 -0
- package/plugins/elanous-hwp/plugin.json +16 -0
- package/plugins/elanous-hwp/plugin.ts +40 -0
- package/scripts/ci-public-export-leak-gate.ts +75 -14
- package/scripts/ci-public-leak-gate.ts +44 -9
- package/scripts/ci-pwa-build-gate.ts +160 -0
- package/scripts/context-hooks/claude-stop.sh +4 -0
- package/scripts/context-hooks/emit.ts +24 -0
- package/scripts/context-hooks/post-tool-use.sh +4 -0
- package/scripts/coord-channel-watch.sh +54 -8
- package/scripts/coord-digest.ts +199 -0
- package/scripts/coord-post.sh +60 -3
- package/scripts/coord-requests-bridge.ts +143 -0
- package/scripts/field-feed/lib.ts +4 -0
- package/scripts/fleet/node-setup.sh +20 -3
- package/scripts/homebrew-formula-bump.ts +69 -0
- package/scripts/install.ps1 +68 -9
- package/scripts/install.sh +39 -12
- package/scripts/lib/pwa-scenarios/scenarios.ts +20 -0
- package/scripts/lib/tui-regress-checks.ts +146 -0
- package/scripts/marketing/measure-site-numbers.ts +169 -0
- package/scripts/measure-gate-shards.ts +309 -0
- package/scripts/memory-lifecycle-cycle.ts +2 -2
- package/scripts/mission-request-judge.ts +31 -1
- package/scripts/morning-report.ts +2 -2
- package/scripts/opportunity-scan.ts +1 -1
- package/scripts/ops-now-probe.ts +327 -0
- package/scripts/publish-internal-dist.ts +11 -1
- package/scripts/release-loop/auto-approve-node.ts +21 -4
- package/scripts/release-loop/cut-branch.ts +84 -0
- package/scripts/release-loop/cutoff.ts +19 -8
- package/scripts/release-loop/gate-node.ts +14 -13
- package/scripts/release-loop/mac-smoke-node.ts +229 -0
- package/scripts/release-loop/node-verdict.ts +3 -3
- package/scripts/release-loop/ops-upgrade-node.ts +72 -16
- package/scripts/release-loop/release-readiness.ts +9 -0
- package/scripts/release-loop/tui-node-checks.ts +42 -0
- package/scripts/release-loop/tui-sim-node.ts +32 -8
- package/scripts/release-loop/unattended-release.ts +5 -3
- package/scripts/release-loop/version-node.ts +112 -13
- package/scripts/repo-watch-cycle.ts +1 -1
- package/scripts/retro-cycle.ts +2 -2
- package/scripts/schedule-health-report.ts +1 -1
- package/scripts/signal-digest-cycle.ts +2 -7
- package/scripts/signal-digest-photo.ts +11 -0
- package/scripts/storyboard/storyboard.ts +5 -161
- package/scripts/tech1-public-measure.ts +94 -0
- package/scripts/test-diet/lib.ts +30 -1
- package/scripts/test-diet/node.ts +83 -8
- package/scripts/test-timeout-audit.ts +114 -0
- package/scripts/tree-sync-alert.ts +1 -1
- package/scripts/tui-regress.ts +71 -0
- package/scripts/ux/demo-rehearsal.ts +243 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +73 -0
- package/scripts/waitlist-daily-digest.ts +1 -1
- package/skills/diagram-master/SKILL.md +366 -0
- package/skills/diagram-master/references/engines/excalidraw/color-palette.md +67 -0
- package/skills/diagram-master/references/engines/excalidraw/element-templates.md +182 -0
- package/skills/diagram-master/references/engines/excalidraw/json-schema.md +71 -0
- package/skills/diagram-master/references/engines/excalidraw/render_excalidraw.py +189 -0
- package/skills/diagram-master/references/engines/excalidraw/render_template.html +57 -0
- package/skills/diagram-master/references/engines/matplotlib/matplotlib-patterns.md +235 -0
- package/skills/diagram-master/references/engines/matplotlib/render_matplotlib.py +184 -0
- package/skills/diagram-master/references/engines/mermaid/domain-templates.md +435 -0
- package/skills/diagram-master/references/engines/mermaid/render_mermaid.py +116 -0
- package/skills/diagram-master/references/engines/mermaid/syntax-reference.md +2247 -0
- package/skills/diagram-master/references/engines/nanobannana/render_nanobannana2.py +168 -0
- package/skills/diagram-master/references/engines/svg/color-palette.md +188 -0
- package/skills/diagram-master/references/engines/svg/html-base-template.html +332 -0
- package/skills/diagram-master/references/engines/svg/interactivity-patterns.md +435 -0
- package/skills/diagram-master/references/engines/svg/render_interactive_svg.py +212 -0
- package/skills/diagram-master/references/engines/svg/svg-element-templates.md +349 -0
- package/skills/diagram-master/references/examples/README.md +69 -0
- package/skills/diagram-master/references/examples/ref_grid_architecture.png +0 -0
- package/skills/diagram-master/references/examples/ref_layered_flow.png +0 -0
- package/skills/diagram-master/references/pyproject.toml +16 -0
- package/skills/diagram-master/references/quality-checklist.md +33 -0
- package/skills/diagram-master/references/routing-guide.md +164 -0
- package/skills/diagram-master/references/uv.lock +1356 -0
- package/skills/explainer-video/SKILL.md +4 -0
- package/skills/explainer-video/engine/reel.mjs +18 -13
- package/skills/explainer-video/engine/reel.sh +24 -5
- package/skills/explainer-video/engine/stitch.mjs +145 -0
- package/skills/google-workspace/SKILL.md +33 -0
- package/skills/google-workspace/scripts/gws-safe.sh +177 -0
- package/skills/grill-me/SKILL.md +1 -1
- package/skills/lecture-note-digitizer/SKILL.md +926 -0
- package/skills/lecture-note-digitizer/references/convert_to_pdf.py +30 -0
- package/skills/lecture-note-digitizer/references/extract_slide_diagram.py +73 -0
- package/skills/lecture-note-digitizer/references/html-template.md +167 -0
- package/skills/lecture-note-digitizer/references/katex-style-guide.md +165 -0
- package/skills/omni-crawl/SKILL.md +21 -11
- package/skills/omni-crawl/scripts/main.ts +6 -6
- package/skills/omni-crawl/src/router.ts +3 -3
- package/skills/omni-digest/SKILL.md +17 -9
- package/skills/omni-digest/digest.md +2 -2
- package/skills/omni-digest/src/fetch.ts +30 -3
- package/skills/pitch-deck/SKILL.md +36 -0
- package/skills/pitch-deck/template.html +80 -0
- package/skills/youtube-master/SKILL.md +163 -0
- package/skills/youtube-master/package-lock.json +558 -0
- package/skills/youtube-master/package.json +13 -0
- package/skills/youtube-master/references/study-note-template.md +124 -0
- package/skills/youtube-master/scripts/main.ts +435 -0
- package/skills/youtube-master/scripts/transcribe-local.ts +24 -0
- package/skills/youtube-master/src/audio.ts +63 -0
- package/skills/youtube-master/src/domain-staging.ts +50 -0
- package/skills/youtube-master/src/env.ts +36 -0
- package/skills/youtube-master/src/obsidian.ts +155 -0
- package/skills/youtube-master/src/router.ts +110 -0
- package/skills/youtube-master/src/stt.ts +199 -0
- package/skills/youtube-master/src/study-note.ts +175 -0
- package/skills/youtube-master/src/summarize.ts +358 -0
- package/skills/youtube-master/src/transcript.ts +76 -0
- package/skills/youtube-master/src/types.ts +91 -0
- package/skills/youtube-master/src/util.ts +93 -0
- package/skills/youtube-master/src/youtube.ts +70 -0
- package/skills/youtube-master/tsconfig.json +13 -0
- package/src/acp/codex-approval-adapter.ts +9 -3
- package/src/acp/core-turn-bridge.ts +117 -18
- package/src/acp/fact-question.ts +11 -0
- package/src/acp/server.ts +53 -9
- package/src/acp/tool-approval.ts +126 -0
- package/src/acp/turn-runner.ts +36 -0
- package/src/agent/monad-agent-turn.ts +8 -0
- package/src/agent-mission/driver.ts +80 -10
- package/src/agent-mission/pty-decision.ts +71 -0
- package/src/agent-mission/review-loop.ts +30 -0
- package/src/ask-user-question/pending-questions.ts +25 -10
- package/src/ask-user-question/types.ts +9 -0
- package/src/auth/setup-link-tokens.ts +16 -5
- package/src/autopilot/build/integrity-gate.ts +12 -0
- package/src/autopilot/build/llm-conflict-merge.ts +16 -3
- package/src/autopilot/tool-registry.ts +2 -0
- package/src/boot/daemon-prompt-turn.ts +8 -0
- package/src/boot/daemon-runtime.ts +9 -3
- package/src/boot/daemon-tools/index.ts +8 -0
- package/src/boot/daemon-tools/types.ts +2 -0
- package/src/boot/daemon-tools/web-search.ts +4 -0
- package/src/card-followup/core.ts +109 -0
- package/src/chat/ctrl-c-in-input.ts +14 -0
- package/src/chat/index.ts +53 -0
- package/src/chat/pickers/state.ts +6 -2
- package/src/cli/agent-cli.ts +7 -0
- package/src/cli/decisions-cli.ts +25 -3
- package/src/cli/doctor-bundle-upload.ts +68 -0
- package/src/cli/doctor-cli.ts +38 -4
- package/src/cli/drive-input-jail.ts +101 -0
- package/src/cli/hooks-cli.ts +17 -3
- package/src/cli/make-cli.ts +43 -0
- package/src/cli/ops-cli.ts +20 -0
- package/src/cli/persona-cli.ts +90 -4
- package/src/cli/plugin-cli.ts +31 -22
- package/src/cli/pod-cli.ts +61 -2
- package/src/cli/pr-cli.ts +82 -17
- package/src/cli/pty-drive-cli.ts +41 -7
- package/src/cli/pty-drive-verdict.ts +40 -0
- package/src/cli/pwa-stop.ts +163 -13
- package/src/cli/pwa-test.ts +3 -0
- package/src/cli/release-cli.ts +80 -24
- package/src/cli/self-update.ts +14 -0
- package/src/cli/setup-cli.ts +43 -1
- package/src/cli/skills-cli.ts +17 -1
- package/src/cli/storyboard-cli.ts +37 -0
- package/src/cli/test-state-dir-flag.ts +11 -4
- package/src/connectors/linear.ts +146 -0
- package/src/context-bus/claude-hooks-install.ts +10 -0
- package/src/context-bus/context-now-surfaces.ts +48 -0
- package/src/context-bus/context-now.ts +104 -0
- package/src/context-bus/coord-events.ts +115 -0
- package/src/context-bus/external-events.ts +52 -0
- package/src/context-bus/long-term-memory.ts +108 -0
- package/src/dashboard/dashboard-boot-fixture.ts +23 -0
- package/src/dashboard/index.ts +63 -21
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +14 -1
- package/src/dashboard/slash-runtime/help-from-registry.ts +43 -28
- package/src/decisions/decision-cards.ts +58 -3
- package/src/decisions/decision-ledger.ts +85 -6
- package/src/decisions/decision-linear-projection.ts +157 -0
- package/src/decisions/web-push-decision-cards.ts +50 -0
- package/src/directive/interpretation-card.ts +36 -0
- package/src/discord/sprint21-runtime.ts +5 -1
- package/src/discord-self-message.ts +32 -0
- package/src/discord-slash-wire.ts +5 -1
- package/src/discord.ts +19 -2
- package/src/domains/coo-admin-tool.ts +62 -0
- package/src/domains/core-tools.ts +13 -4
- package/src/domains/elanous-card.ts +29 -0
- package/src/domains/ops-facts-tool.ts +76 -0
- package/src/domains/outbound-alert.ts +46 -13
- package/src/domains/release-tool.ts +168 -0
- package/src/domains/telegram-kind-route.ts +65 -9
- package/src/exec-requests/default-outputs.ts +25 -0
- package/src/exec-requests/planner.ts +106 -28
- package/src/exec-requests/runner.ts +123 -12
- package/src/exec-requests/store.ts +11 -2
- package/src/execution-loop/launch-gate.ts +100 -0
- package/src/field/field-media.ts +10 -7
- package/src/field/field-reel.ts +19 -5
- package/src/field-note/index.ts +201 -0
- package/src/field-note/transcribe.ts +248 -0
- package/src/git-fs/worktree.ts +45 -6
- package/src/graph-kinds/registry.ts +3 -3
- package/src/graph-runner/graph-approval-card.ts +113 -0
- package/src/graph-runner/graph-cli.ts +189 -4
- package/src/graph-runner/graph-grow-llm.ts +37 -0
- package/src/graph-runner/graph-grow.ts +47 -0
- package/src/graph-runner/graph-notify.ts +25 -3
- package/src/graph-runner/runner.ts +453 -35
- package/src/grok/agent-search.ts +8 -2
- package/src/guardian/secret-exposure-scan.ts +101 -0
- package/src/harness/browser-verify.ts +25 -2
- package/src/harness/harness-cli-command.ts +478 -42
- package/src/harness/harness-incidents-writer.fixture.ts +24 -0
- package/src/harness/harness-incidents.ts +129 -0
- package/src/harness/harness-pod-dispatch.ts +43 -23
- package/src/harness/harness-seams.ts +38 -4
- package/src/harness/harness-stop.ts +75 -6
- package/src/harness/launch-registry.ts +91 -0
- package/src/harness/process-garbage.ts +5 -3
- package/src/hitl/clarification-policy.ts +3 -1
- package/src/hooks/dispatch.ts +82 -0
- package/src/hooks/expose.ts +254 -0
- package/src/hooks/github-shadow.ts +108 -0
- package/src/hooks/providers.ts +71 -5
- package/src/hooks/queue.ts +1 -1
- package/src/hooks/receiver.ts +21 -20
- package/src/index.ts +206 -49
- package/src/intake-plane/author-goals.ts +37 -4
- package/src/intake-plane/categorize.ts +18 -46
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/discord-seat-work.ts +57 -2
- package/src/intake-plane/front-route-classifier.ts +33 -39
- package/src/intake-plane/persona-answer.ts +30 -0
- package/src/intake-plane/runtime-callables.ts +8 -1
- package/src/intake-plane/seat-doc-route.ts +170 -6
- package/src/intake-plane/telegram-seat-work.ts +80 -4
- package/src/intake-plane/telegram-work.ts +1 -0
- package/src/intake-plane/wish-folder.ts +67 -0
- package/src/intent-gate/gate.ts +30 -10
- package/src/live/detail-switch.ts +5 -0
- package/src/llm/difficulty.ts +57 -0
- package/src/llm/judge-layer.ts +101 -0
- package/src/llm/mission-router.ts +33 -19
- package/src/loops/contract/examples/cmo-seat.loop.yaml +51 -0
- package/src/loops/contract/examples/orchestrator.loop.yaml +57 -0
- package/src/loops/contract/examples/steward.loop.yaml +50 -0
- package/src/loops/contract/loop-agent.schema.json +138 -0
- package/src/loops/contract/validate.ts +64 -0
- package/src/loops/registry.ts +20 -6
- package/src/market/signed-index.ts +4 -0
- package/src/maturity/bot-command-maturity.ts +45 -0
- package/src/maturity/cli-maturity.ts +34 -0
- package/src/maturity/feature-maturity.ts +120 -1
- package/src/maturity/tui-slash-maturity.ts +31 -0
- package/src/memory.ts +32 -4
- package/src/model-tier/index.ts +2 -2
- package/src/model-tier/tier-resolver.ts +20 -20
- package/src/model-tier/types.ts +6 -6
- package/src/mss/logging/log-store.ts +15 -0
- package/src/nexus/api/config-model-tier.ts +5 -4
- package/src/nexus/api/exec-requests.ts +33 -3
- package/src/nexus/api/fabric-plans.ts +53 -0
- package/src/nexus/api/harness-api.ts +5 -1
- package/src/nexus/api/health.ts +2 -0
- package/src/nexus/api/http-server.ts +89 -1
- package/src/nexus/api/inside-events.ts +226 -0
- package/src/nexus/api/meta-api.ts +32 -8
- package/src/nexus/api/missions.ts +6 -1
- package/src/nexus/api/ops-api.ts +10 -1
- package/src/nexus/api/ops-seats-sources.ts +37 -7
- package/src/nexus/api/ops-seats.ts +49 -6
- package/src/nexus/api/outputs-route.ts +44 -0
- package/src/nexus/api/seat-requests.ts +132 -10
- package/src/nexus/api/sessions-store.ts +42 -0
- package/src/nexus/api/setup-codex-login.ts +160 -0
- package/src/nexus/api/sub-seat-charters.ts +34 -0
- package/src/nexus/api/tabs-mutations.ts +3 -1
- package/src/nexus/api/trace.ts +18 -6
- package/src/nexus/api/usage-api.ts +102 -0
- package/src/nexus/api/workflow-run-event-bridge.ts +1 -0
- package/src/nexus/api/workflows.ts +80 -6
- package/src/nexus/index.ts +47 -4
- package/src/nexus/kinds/webterm.ts +2 -1
- package/src/nexus/webterm/pty.ts +6 -2
- package/src/onboarding/cli-auth-status.ts +80 -0
- package/src/onboarding/first-run.ts +89 -0
- package/src/onboarding/lan-address.ts +45 -0
- package/src/onboarding/voice-ai.ts +3 -3
- package/src/onboarding/web-first.ts +119 -24
- package/src/onboarding.ts +4 -0
- package/src/ops-now/ops-now-refresh.ts +100 -0
- package/src/org/seat-budget.ts +48 -0
- package/src/org/stall-escalation.ts +63 -0
- package/src/outputs/ledger.ts +120 -0
- package/src/persona/persona-todo.ts +79 -0
- package/src/persona/presets.ts +116 -2
- package/src/persona/team-set.ts +155 -0
- package/src/plugins/install/plugin-install.ts +14 -0
- package/src/plugins/maker/market-bundle.ts +109 -0
- package/src/plugins/maker/plugin-maker.ts +254 -11
- package/src/plugins/maker/wizard-generate.ts +103 -0
- package/src/pod-lease/admission.ts +150 -0
- package/src/pod-lease/dependency-state.ts +85 -0
- package/src/pod-lease/host-lease.ts +127 -0
- package/src/project/project-store.ts +115 -0
- package/src/project/project-suggestion.ts +16 -0
- package/src/project/session-context.ts +24 -0
- package/src/release-loop/checklist.ts +79 -5
- package/src/release-loop/feature-store.ts +159 -24
- package/src/release-loop/next-md-merge.ts +58 -0
- package/src/release-loop/release-note.ts +59 -4
- package/src/release-loop/release-schedule.ts +53 -0
- package/src/repl/index.ts +7 -0
- package/src/seat-dispatch/ceo-commands.ts +41 -8
- package/src/seat-dispatch/ceo-intent.ts +38 -0
- package/src/seat-dispatch/seat-ask.ts +130 -0
- package/src/seat-dispatch/seat-questions.ts +100 -0
- package/src/seat-loop/seat-loop.ts +750 -0
- package/src/seat-loop/seat-report.ts +99 -0
- package/src/self-dev/decompose.ts +66 -11
- package/src/self-dev/dev-cli.ts +4 -1
- package/src/self-dev/dev-pipeline.ts +37 -1
- package/src/self-dev/draft-sweep.ts +38 -33
- package/src/self-dev/draft-triage-rules.ts +8 -5
- package/src/self-dev/fabric-plan-core.ts +297 -0
- package/src/self-dev/orchestrate.ts +36 -4
- package/src/self-dev/release-path-guard.ts +62 -0
- package/src/self-dev/run-store.ts +10 -1
- package/src/self-dev/shard-aggregate.ts +230 -0
- package/src/self-dev/shard-placement.ts +60 -0
- package/src/self-dev/shard-plan.ts +48 -0
- package/src/self-dev/shard-pod-run.ts +77 -0
- package/src/self-dev/shard-run.ts +105 -0
- package/src/self-implement/execution-clarification.ts +115 -0
- package/src/self-implement/gate-baseline.ts +1 -1
- package/src/self-implement/gate-cli.ts +46 -4
- package/src/self-implement/gate-scope.ts +37 -7
- package/src/self-implement/graph-variant.ts +171 -0
- package/src/self-implement/harness-policy-gates.ts +1 -1
- package/src/self-implement/headless-elanous-driver.ts +22 -3
- package/src/self-implement/host-regate.ts +22 -1
- package/src/self-implement/importer-test-index.ts +2 -1
- package/src/self-implement/l8-merge-queue.ts +130 -0
- package/src/self-implement/orchestrator.ts +279 -12
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/route-consumer-test-index.ts +122 -0
- package/src/self-implement/run-status-mapping.ts +5 -3
- package/src/self-implement/seams.ts +72 -11
- package/src/self-implement/test-seams.ts +1 -0
- package/src/self-implement/uncommitted-work.ts +59 -0
- package/src/session/chat.ts +2 -0
- package/src/session/index.ts +45 -13
- package/src/shell-env-bootstrap.ts +17 -0
- package/src/skills/gift-install.ts +215 -0
- package/src/skills/tools/web-search.ts +4 -1
- package/src/steward/directive.ts +51 -2
- package/src/steward/launch.ts +266 -0
- package/src/steward/outcome-notify.ts +115 -0
- package/src/steward/steward-cards.ts +25 -11
- package/src/steward/triage-plan.ts +32 -4
- package/src/steward/triage.ts +129 -13
- package/src/steward/working-backwards.ts +64 -0
- package/src/storyboard/storyboard.ts +141 -0
- package/src/task-cards/card-cli.ts +25 -1
- package/src/task-cards/card-store.ts +2 -2
- package/src/task-orchestrator/mission.ts +5 -0
- package/src/task-orchestrator/store.ts +12 -3
- package/src/task-orchestrator/surfaces/pod-command-job.ts +29 -7
- package/src/task-orchestrator/surfaces/pod-lease.ts +397 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +127 -26
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +214 -40
- package/src/task-orchestrator/surfaces/self-implement.ts +15 -2
- package/src/task-orchestrator/types.ts +2 -0
- package/src/telegram-card-followup.ts +153 -0
- package/src/telegram-commands.ts +10 -1
- package/src/telegram-project-command.ts +71 -0
- package/src/telegram-report.ts +29 -19
- package/src/telegram-run.ts +7 -0
- package/src/telegram.ts +58 -11
- package/src/tool-runtime/self-cognition-runtimes.ts +13 -3
- package/src/tool-runtime/types.ts +4 -0
- package/src/tui-client/dashboard-session.ts +2 -0
- package/src/user-config.ts +216 -27
- package/src/version/packed-revision.json +1 -1
- package/src/web-push/notify-exec-request.ts +32 -0
- package/src/web-search/grok.ts +7 -1
- package/src/web-search/index.ts +28 -1
- package/src/web-search/provider.ts +4 -0
- package/src/web-search/x-intent.ts +5 -0
- package/src/workflow-runtime/executor.ts +277 -47
- package/src/workflow-runtime/index.ts +1 -0
- package/src/workflow-runtime/nodes/subworkflow.ts +60 -0
- package/src/workflow-runtime/plugin-kind-node.ts +1 -1
- package/src/workflow-runtime/schema.ts +79 -4
- package/src/workflow-runtime/types.ts +35 -4
- package/apps/pwa/out/_next/static/bFI5EEwg7T6aG88ibX4Zq/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1201-09613bce624467c0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1377-1efeffbbac0963ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1470.98a6ab61ccb4131c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/216-5acd01cbde4acda7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2330-c2f07246c8788af1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2375-cb6debea56cce4fb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2929-617f1e66a55be184.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2977-e38442e86289f1d3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2982-2a2df543be3d2e5a.js +0 -25
- package/apps/pwa/out/_next/static/chunks/3403.4bc97b4066945a94.js +0 -5
- package/apps/pwa/out/_next/static/chunks/3441-a1dd4acfeda14488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5080-1613752891fcadb4.js +0 -10
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6090-85a3f05f954e5697.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6284-69db415193d5f8bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6416-505ba707cacafbba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6725-68ab7cc0b1faf00d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6998-a654fe5c58271600.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7647.2fcd30d894d5ee30.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7673-2eda1507d38dadb8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8630-cdfc61e82c653ffc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-e263b332208e3957.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-139040d37c79dbc9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/ae787e4c-f2998de7f1ba72a4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-fb7c2d458f32fcad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-35e2713597dd24d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-918b656f064577ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-5f30e00693527ebd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-7fbae173b3c103a7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-3b4edcce364062ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-c5794f484c251f44.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-1130d8eefcd87e76.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-973996d584f008c1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-016e2bda04eecb1f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-b4d6d9a3b56f2940.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-ec49a466121233ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-f72db356744ceade.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-a6fd9b50d1b85378.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-8b0402a46a749a3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-037f83e2a1edc8e2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-46fedff57d4bec67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-0cf318e9dcee2bbf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-899b327cb8b6845c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-6c1e6ba743a29dfe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1a8e4807292f6b17.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-28deba6bcb834916.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-de68e2205794037d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-c71c33a573230672.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-0df30e0c7e50148a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-4fc56790fe0d2682.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-59df2aebb38a7c1e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-859cea1570b6227a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4647be5dbcbd0795.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e025892bda74b743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a0955de73b89f078.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-369846d2921af584.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-72cbc1c8cdd0f65d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-77b28bfb0996d5ea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3220c4934f49c6c2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-90670589b87abea8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-7df9243ed021a4cc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-4498c3853f4f688d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-35bedde6d885f08f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-ea806bb57f2b8923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-8d2302399c2c8f87.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-6870f30ea99f8346.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-b10928c60f0b8488.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-689039227d443fab.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-e5bea36f183b25e6.js +0 -1
- package/apps/pwa/out/_next/static/css/f92f63a5df0ba229.css +0 -1
- package/src/dashboard/turn-typeahead-echo.ts +0 -23
- /package/apps/pwa/out/_next/static/{bFI5EEwg7T6aG88ibX4Zq → Y4zKzL62sakG5momz3SoQ}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { debug } from '../debug/log.js';
|
|
4
|
+
import { runSeatLoopOnce } from '../seat-loop/seat-loop.js';
|
|
5
|
+
import { getUserConfig } from '../user-config.js';
|
|
6
|
+
import { openMsgStore } from '../msg/msg-store.js';
|
|
7
|
+
import type { EventsConfig } from '../user-config.js';
|
|
8
|
+
import type { QueuedHook } from './queue.js';
|
|
9
|
+
|
|
10
|
+
export type HookWake = (seat: string) => Promise<unknown>;
|
|
11
|
+
|
|
12
|
+
// A crashed wake can be reclaimed after the lease expires; a live wake renews it until completion.
|
|
13
|
+
const WAKE_LEASE_MS = 120_000;
|
|
14
|
+
const DEFAULT_SEAT_ROUTES = [
|
|
15
|
+
{ source: 'linear', kind: 'Issue:update', seat: (event: QueuedHook, config: EventsConfig) =>
|
|
16
|
+
event.task.assigneeId ? config.linearAssignees[event.task.assigneeId] : undefined },
|
|
17
|
+
{ source: 'github', kind: 'pull_request:review_requested', seat: 'TC' },
|
|
18
|
+
{ source: 'github', kind: 'check_run:completed', seat: 'TC' },
|
|
19
|
+
{ source: 'github', kind: 'check_suite:completed', seat: 'TC' },
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
/** Record a work card and inbox delivery atomically. Wake state: 0 = retryable, 2 = leased, 1 = done, 3 = seat loop off (terminal).
|
|
23
|
+
* `events.mode` decides only whether the card is delivered; the wake runs the seat loop under its own `loops.seat` mode.
|
|
24
|
+
* So a delivered card with the seat loop off is not retried (the card waits in the inbox), and a shadow loop's wake is
|
|
25
|
+
* recorded as `wake_status = 'shadow'` — visible as «not executed», not mistaken for a real run. */
|
|
26
|
+
export async function dispatchHook(event: QueuedHook, root: string, config: EventsConfig,
|
|
27
|
+
wake: HookWake = seat => runSeatLoopOnce(seat, { root, config: getUserConfig().loops?.seat ?? { mode: 'off' } }),
|
|
28
|
+
now: () => number = Date.now): Promise<void> {
|
|
29
|
+
const key = `${event.provider}:${event.eventId}`;
|
|
30
|
+
const route = config.routes.find(candidate => candidate.source === event.provider && candidate.kind === event.kind);
|
|
31
|
+
const defaultSeat = DEFAULT_SEAT_ROUTES.find(candidate => candidate.source === event.provider && candidate.kind === event.kind)?.seat;
|
|
32
|
+
const defaultResolvedSeat = typeof defaultSeat === 'function' ? defaultSeat(event, config) : defaultSeat;
|
|
33
|
+
if (event.provider === 'linear' && event.kind === 'Issue:update' && !route && !defaultResolvedSeat)
|
|
34
|
+
throw new Error('Linear issue update assignee is not mapped to a seat');
|
|
35
|
+
const seat = route?.seat ?? defaultResolvedSeat ?? 'OP';
|
|
36
|
+
const store = openMsgStore(join(root, 'msg', 'messages.db'));
|
|
37
|
+
const claim = randomUUID();
|
|
38
|
+
try {
|
|
39
|
+
store.db.exec(`CREATE TABLE IF NOT EXISTS hook_work_cards (
|
|
40
|
+
event_key TEXT PRIMARY KEY, source TEXT NOT NULL, kind TEXT NOT NULL,
|
|
41
|
+
seat TEXT NOT NULL, loop TEXT NOT NULL, mode TEXT NOT NULL, title TEXT NOT NULL,
|
|
42
|
+
message_id INTEGER, woken INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
wake_claim TEXT, wake_lease_until INTEGER
|
|
44
|
+
)`);
|
|
45
|
+
const columns = store.db.query('PRAGMA table_info(hook_work_cards)').all() as Array<{ name: string }>;
|
|
46
|
+
if (!columns.some(column => column.name === 'wake_claim')) store.db.exec('ALTER TABLE hook_work_cards ADD COLUMN wake_claim TEXT');
|
|
47
|
+
if (!columns.some(column => column.name === 'wake_lease_until')) store.db.exec('ALTER TABLE hook_work_cards ADD COLUMN wake_lease_until INTEGER');
|
|
48
|
+
if (!columns.some(column => column.name === 'wake_status')) store.db.exec('ALTER TABLE hook_work_cards ADD COLUMN wake_status TEXT');
|
|
49
|
+
store.db.transaction(() => {
|
|
50
|
+
const existing = store.db.query('SELECT event_key FROM hook_work_cards WHERE event_key = ?').get(key);
|
|
51
|
+
if (existing) return;
|
|
52
|
+
const live = config.mode === 'on';
|
|
53
|
+
let messageId: number | null = null;
|
|
54
|
+
if (live) {
|
|
55
|
+
const body = `[작업 카드 · ${key} · ${event.kind ?? 'unknown'} · loop: seat] ${event.task.title}\n${event.task.external.url ?? event.task.external.ref}`;
|
|
56
|
+
messageId = store.append({ from: 'HOOKS', to: seat, kind: 'hook-task', body }).id;
|
|
57
|
+
}
|
|
58
|
+
store.db.query(`INSERT INTO hook_work_cards(event_key, source, kind, seat, loop, mode, title, message_id)
|
|
59
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(key, event.provider, event.kind ?? 'unknown', seat, route?.loop ?? 'seat', live ? 'on' : 'shadow', event.task.title, messageId);
|
|
60
|
+
})();
|
|
61
|
+
const leaseAt = now();
|
|
62
|
+
const acquired = store.db.query(`UPDATE hook_work_cards SET woken = 2, wake_claim = ?, wake_lease_until = ?
|
|
63
|
+
WHERE event_key = ? AND message_id IS NOT NULL
|
|
64
|
+
AND (woken = 0 OR (woken = 2 AND (wake_lease_until IS NULL OR wake_lease_until <= ?)))`)
|
|
65
|
+
.run(claim, leaseAt + WAKE_LEASE_MS, key, leaseAt).changes === 1;
|
|
66
|
+
if (!acquired) return;
|
|
67
|
+
const renew = () => store.db.query(`UPDATE hook_work_cards SET wake_lease_until = ?
|
|
68
|
+
WHERE event_key = ? AND woken = 2 AND wake_claim = ?`).run(now() + WAKE_LEASE_MS, key, claim);
|
|
69
|
+
const heartbeat = setInterval(renew, WAKE_LEASE_MS / 4);
|
|
70
|
+
try {
|
|
71
|
+
const result = await wake(seat);
|
|
72
|
+
const status = result && typeof result === 'object' && 'status' in result && typeof result.status === 'string' ? result.status : 'woken';
|
|
73
|
+
// Seat loop off is a policy, not a transient failure: keep the card in the inbox and stop retrying.
|
|
74
|
+
const final = status === 'skipped-off' ? 3 : 1;
|
|
75
|
+
store.db.query('UPDATE hook_work_cards SET woken = ?, wake_status = ?, wake_claim = NULL, wake_lease_until = NULL WHERE event_key = ? AND woken = 2 AND wake_claim = ?').run(final, status, key, claim);
|
|
76
|
+
debug.log('hooks.dispatch', 'wake', { key, seat, status, retry: false });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
store.db.query('UPDATE hook_work_cards SET woken = 0, wake_claim = NULL, wake_lease_until = NULL WHERE event_key = ? AND woken = 2 AND wake_claim = ?').run(key, claim);
|
|
79
|
+
throw error;
|
|
80
|
+
} finally { clearInterval(heartbeat); }
|
|
81
|
+
} finally { store.close(); }
|
|
82
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
6
|
+
import { debug } from '../debug/log.js';
|
|
7
|
+
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
8
|
+
|
|
9
|
+
export const PUBLIC_HOOK_PATH = '/hooks/linear';
|
|
10
|
+
export const PUBLIC_GITHUB_HOOK_PATH = '/hooks/github';
|
|
11
|
+
export const PUBLIC_HOOK_PORT = 31481;
|
|
12
|
+
const HTTPS_PORT = 8443;
|
|
13
|
+
export const HOOK_EXPOSURE_REF = 'hooks.expose:/hooks/linear';
|
|
14
|
+
const ON_ARGS = ['funnel', '--bg', `--https=${HTTPS_PORT}`, String(PUBLIC_HOOK_PORT)];
|
|
15
|
+
const OFF_ARGS = ['funnel', '--bg', `--https=${HTTPS_PORT}`, 'off'];
|
|
16
|
+
const PROBE_PATH = '/.hooks-ingress-identity';
|
|
17
|
+
const identityFile = (stateDir?: string) => join(stateDir ?? elanousStateRoot(), 'hooks', 'ingress-identity.json');
|
|
18
|
+
interface IngressIdentity { port: number; secret: string }
|
|
19
|
+
|
|
20
|
+
/** A fresh, private challenge key belongs to the listener that actually bound the port. */
|
|
21
|
+
function readIngressIdentity(stateDir?: string): IngressIdentity {
|
|
22
|
+
const identity = JSON.parse(readFileSync(identityFile(stateDir), 'utf8')) as IngressIdentity;
|
|
23
|
+
if (!Number.isInteger(identity.port) || identity.port < 1 || identity.port > 65535 ||
|
|
24
|
+
typeof identity.secret !== 'string' || !/^[a-f0-9]{64}$/.test(identity.secret)) throw new Error('invalid ingress identity');
|
|
25
|
+
return identity;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function verifyIngress(stateDir?: string, port = PUBLIC_HOOK_PORT): Promise<boolean> {
|
|
29
|
+
let identity: IngressIdentity;
|
|
30
|
+
try { identity = readIngressIdentity(stateDir); } catch { return false; }
|
|
31
|
+
if (identity.port !== port) return false;
|
|
32
|
+
const nonce = randomBytes(32).toString('hex');
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(`http://127.0.0.1:${port}${PROBE_PATH}`, {
|
|
35
|
+
headers: { 'x-hooks-ingress-challenge': nonce,
|
|
36
|
+
'x-hooks-ingress-proof': createHmac('sha256', identity.secret).update(`request:${nonce}`).digest('hex') },
|
|
37
|
+
signal: AbortSignal.timeout(1500),
|
|
38
|
+
});
|
|
39
|
+
if (response.status !== 200) return false;
|
|
40
|
+
const expected = createHmac('sha256', identity.secret).update(`response:${nonce}`).digest();
|
|
41
|
+
const actual = await response.text();
|
|
42
|
+
return /^[a-f0-9]{64}$/.test(actual) && timingSafeEqual(Buffer.from(actual, 'hex'), expected);
|
|
43
|
+
} catch { return false; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface HookExposureState { path: typeof PUBLIC_HOOK_PATH; decisionId: string; openedAt: string }
|
|
47
|
+
export interface HookExposureDeps {
|
|
48
|
+
stateDir?: string;
|
|
49
|
+
decision?: (id: string) => DecisionEntry;
|
|
50
|
+
execute?: (binary: string, args: readonly string[]) => void;
|
|
51
|
+
now?: () => Date;
|
|
52
|
+
ingressPort?: number;
|
|
53
|
+
/** Local port the 8443 Funnel currently proxies to, or null (none / unreadable). Default reads `tailscale funnel status --json`. */
|
|
54
|
+
funnelTarget?: () => number | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The loopback port a 8443 Funnel handler proxies to — the only evidence that an unrecorded Funnel is ours. */
|
|
58
|
+
export function parseFunnelTarget(statusJson: string): number | null {
|
|
59
|
+
let status: unknown;
|
|
60
|
+
try { status = JSON.parse(statusJson); } catch { return null; }
|
|
61
|
+
const web = (status as { Web?: Record<string, { Handlers?: Record<string, { Proxy?: unknown }> }> } | null)?.Web;
|
|
62
|
+
if (!web || typeof web !== 'object') return null;
|
|
63
|
+
for (const [host, entry] of Object.entries(web)) {
|
|
64
|
+
if (!host.endsWith(`:${HTTPS_PORT}`)) continue;
|
|
65
|
+
for (const handler of Object.values(entry?.Handlers ?? {})) {
|
|
66
|
+
const match = typeof handler?.Proxy === 'string' ? /^http:\/\/(?:127\.0\.0\.1|localhost):(\d+)\/?$/.exec(handler.Proxy) : null;
|
|
67
|
+
if (match) return Number(match[1]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readFunnelTarget(): number | null {
|
|
74
|
+
const result = spawnSync('tailscale', ['funnel', 'status', '--json'], { encoding: 'utf8' });
|
|
75
|
+
if (result.error || result.status !== 0) return null;
|
|
76
|
+
return parseFunnelTarget(result.stdout);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function run(binary: string, args: readonly string[]): void {
|
|
80
|
+
const result = spawnSync(binary, [...args], { encoding: 'utf8' });
|
|
81
|
+
if (result.error || result.status !== 0) throw new Error(`tailscale command failed${result.error ? `: ${result.error.message}` : ''}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Funnel targets a dedicated loopback listener, never the multi-route hooks receiver or NEXUS. */
|
|
85
|
+
export function hookExposurePlan(): { path: string; local: string; decisionRef: string; on: string; off: string } {
|
|
86
|
+
const plan = { path: PUBLIC_HOOK_PATH, local: `http://127.0.0.1:${PUBLIC_HOOK_PORT}`,
|
|
87
|
+
decisionRef: HOOK_EXPOSURE_REF, on: `tailscale ${ON_ARGS.join(' ')}`, off: `tailscale ${OFF_ARGS.join(' ')}` };
|
|
88
|
+
debug.log('hooks.expose', 'plan', { path: plan.path, port: PUBLIC_HOOK_PORT });
|
|
89
|
+
return plan;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stateFile(deps: HookExposureDeps): string { return join(deps.stateDir ?? elanousStateRoot(), 'hooks', 'exposure.json'); }
|
|
93
|
+
|
|
94
|
+
/** Portable cross-process lock (macOS has no `flock` binary): an atomic `mkdir` lock directory holding the owner pid.
|
|
95
|
+
* A lock whose owner pid is gone is stale and taken over; otherwise wait up to 30s. */
|
|
96
|
+
async function withExposureLock<T>(deps: HookExposureDeps, action: () => Promise<T> | T): Promise<T> {
|
|
97
|
+
const dir = join(deps.stateDir ?? elanousStateRoot(), 'hooks');
|
|
98
|
+
mkdirSync(dir, { recursive: true });
|
|
99
|
+
const lock = join(dir, 'exposure.lock.d');
|
|
100
|
+
const owner = join(lock, 'owner');
|
|
101
|
+
const deadline = Date.now() + 30_000;
|
|
102
|
+
for (;;) {
|
|
103
|
+
try {
|
|
104
|
+
mkdirSync(lock);
|
|
105
|
+
writeFileSync(owner, String(process.pid));
|
|
106
|
+
break;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
109
|
+
let holder = Number.NaN;
|
|
110
|
+
try { holder = Number(readFileSync(owner, 'utf8').trim()); } catch { /* owner not written yet */ }
|
|
111
|
+
const alive = Number.isInteger(holder) && holder > 0 && (() => { try { process.kill(holder, 0); return true; } catch { return false; } })();
|
|
112
|
+
if (Number.isInteger(holder) && holder > 0 && !alive) {
|
|
113
|
+
// Detach atomically, then re-check: only the dead holder's lock is removed — a lock another process just
|
|
114
|
+
// re-created after its own takeover is put back untouched.
|
|
115
|
+
const detached = `${lock}.stale.${process.pid}.${randomBytes(4).toString('hex')}`;
|
|
116
|
+
try { renameSync(lock, detached); } catch { continue; }
|
|
117
|
+
let detachedHolder = Number.NaN;
|
|
118
|
+
try { detachedHolder = Number(readFileSync(join(detached, 'owner'), 'utf8').trim()); } catch { /* unreadable */ }
|
|
119
|
+
if (detachedHolder === holder) rmSync(detached, { recursive: true, force: true });
|
|
120
|
+
else { try { renameSync(detached, lock); } catch { rmSync(detached, { recursive: true, force: true }); } }
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (Date.now() > deadline) throw new Error('hooks exposure lock unavailable (held for over 30s)');
|
|
124
|
+
await Bun.sleep(25);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
try { return await action(); }
|
|
128
|
+
finally { rmSync(lock, { recursive: true, force: true }); }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Recorded exposure state; an out-of-band tailscale change is not represented by this file. */
|
|
132
|
+
export function hookExposureStatus(deps: HookExposureDeps = {}): HookExposureState | null {
|
|
133
|
+
const file = stateFile(deps);
|
|
134
|
+
if (!existsSync(file)) return null;
|
|
135
|
+
const raw: unknown = JSON.parse(readFileSync(file, 'utf8'));
|
|
136
|
+
if (raw === null) return null;
|
|
137
|
+
if (typeof raw !== 'object' || (raw as HookExposureState).path !== PUBLIC_HOOK_PATH ||
|
|
138
|
+
!/^D-\d{8}-\d+$/.test((raw as HookExposureState).decisionId) ||
|
|
139
|
+
!Number.isFinite(Date.parse((raw as HookExposureState).openedAt))) throw new Error('invalid hooks exposure state');
|
|
140
|
+
return raw as HookExposureState;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function save(deps: HookExposureDeps, state: HookExposureState | null): void {
|
|
144
|
+
const file = stateFile(deps);
|
|
145
|
+
mkdirSync(join(file, '..'), { recursive: true });
|
|
146
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
147
|
+
writeFileSync(temp, JSON.stringify(state) + '\n', { mode: 0o600 });
|
|
148
|
+
renameSync(temp, file);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function turnHookExposureOn(decisionId: string | undefined, deps: HookExposureDeps = {}): Promise<HookExposureState> {
|
|
152
|
+
const refuse = (reason: string): never => {
|
|
153
|
+
debug.log('hooks.expose', 'refused', { reason });
|
|
154
|
+
throw new Error(`hooks expose on refused: ${reason}`);
|
|
155
|
+
};
|
|
156
|
+
if (!decisionId || !/^D-\d{8}-\d+$/.test(decisionId)) return refuse('valid --decision <D-id> required');
|
|
157
|
+
return withExposureLock(deps, async () => {
|
|
158
|
+
if (hookExposureStatus(deps)) return refuse('already on; turn off first');
|
|
159
|
+
let entry: DecisionEntry;
|
|
160
|
+
try { entry = (deps.decision ?? (id => new DecisionLedger({ stateDir: deps.stateDir }).show(id)))(decisionId); }
|
|
161
|
+
catch { return refuse('decision not found or unreadable'); }
|
|
162
|
+
if (entry.id !== decisionId || entry.status !== 'decided') return refuse('decision is not decided');
|
|
163
|
+
if (entry.category !== 'security' || !entry.refs?.includes(HOOK_EXPOSURE_REF)) return refuse('decision is not for hooks exposure');
|
|
164
|
+
if (entry.decidedBy?.kind !== 'human') return refuse('owner decision required');
|
|
165
|
+
if (!entry.choice || entry.options.find(option => option.key === entry.choice)?.label !== '켬') return refuse('selected option is not 켬');
|
|
166
|
+
if (!await verifyIngress(deps.stateDir, deps.ingressPort)) return refuse('dedicated hooks ingress not running or identity mismatch');
|
|
167
|
+
const state: HookExposureState = { path: PUBLIC_HOOK_PATH, decisionId, openedAt: (deps.now ?? (() => new Date()))().toISOString() };
|
|
168
|
+
const execute = deps.execute ?? run;
|
|
169
|
+
execute('tailscale', ON_ARGS);
|
|
170
|
+
try { save(deps, state); }
|
|
171
|
+
catch (saveError) {
|
|
172
|
+
try { execute('tailscale', OFF_ARGS); }
|
|
173
|
+
catch (rollbackError) {
|
|
174
|
+
debug.log('hooks.expose', 'refused', { reason: 'state save and rollback failed', path: state.path, decisionId, openedAt: state.openedAt });
|
|
175
|
+
throw new AggregateError([saveError, rollbackError],
|
|
176
|
+
`hooks exposure state save and rollback failed; public exposure may still be open (decision ${decisionId}, path ${state.path}, opened ${state.openedAt}); run hooks expose off (or hooks expose off --force)`);
|
|
177
|
+
}
|
|
178
|
+
debug.log('hooks.expose', 'refused', { reason: 'state save failed; exposure rolled back', path: state.path, decisionId });
|
|
179
|
+
throw saveError;
|
|
180
|
+
}
|
|
181
|
+
debug.log('hooks.expose', 'on', { path: state.path, decisionId, openedAt: state.openedAt });
|
|
182
|
+
return state;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface HookExposureOffResult { changed: boolean; reason: 'recorded' | 'funnel-targets-hook-ingress' | 'forced' | 'not-ours' }
|
|
187
|
+
|
|
188
|
+
/** Turn off only a Funnel this tool owns: a recorded exposure, or a 8443 Funnel proxying to the dedicated hook ingress
|
|
189
|
+
* (recovery after a failed state save). Anything else is left alone unless `force` — an unrelated Funnel is not ours. */
|
|
190
|
+
export async function turnHookExposureOff(deps: HookExposureDeps = {}, opts: { force?: boolean } = {}): Promise<HookExposureOffResult> {
|
|
191
|
+
return withExposureLock(deps, () => offLocked(deps, opts));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function offLocked(deps: HookExposureDeps, opts: { force?: boolean }): HookExposureOffResult {
|
|
195
|
+
const recorded = hookExposureStatus(deps);
|
|
196
|
+
const reason: HookExposureOffResult['reason'] = recorded ? 'recorded'
|
|
197
|
+
: (deps.funnelTarget ?? readFunnelTarget)() === PUBLIC_HOOK_PORT ? 'funnel-targets-hook-ingress'
|
|
198
|
+
: opts.force ? 'forced' : 'not-ours';
|
|
199
|
+
if (reason === 'not-ours') {
|
|
200
|
+
debug.log('hooks.expose', 'off-skipped', { path: PUBLIC_HOOK_PATH, reason });
|
|
201
|
+
return { changed: false, reason };
|
|
202
|
+
}
|
|
203
|
+
(deps.execute ?? run)('tailscale', OFF_ARGS);
|
|
204
|
+
save(deps, null);
|
|
205
|
+
debug.log('hooks.expose', 'off', { path: PUBLIC_HOOK_PATH, reason });
|
|
206
|
+
return { changed: true, reason };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** `hooks serve` shutdown: under the same lock as `on`, close a recorded exposure first, then the ingress — so a Funnel
|
|
210
|
+
* never outlives the listener it was verified against (another process could bind the port next). */
|
|
211
|
+
export async function closeIngressAndExposure(ingress: { stop: () => void } | undefined, deps: HookExposureDeps = {}): Promise<void> {
|
|
212
|
+
await withExposureLock(deps, () => {
|
|
213
|
+
try {
|
|
214
|
+
if (hookExposureStatus(deps)) offLocked(deps, {});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
debug.log('hooks.expose', 'shutdown-off-failed', { path: PUBLIC_HOOK_PATH, error: String(error).slice(0, 200) });
|
|
217
|
+
console.error(`hooks: public exposure may still be open — run \`elanous hooks expose off\` (${error instanceof Error ? error.message : String(error)})`);
|
|
218
|
+
} finally {
|
|
219
|
+
ingress?.stop();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Only the two exact public hook paths are forwarded; the receiver verifies signatures and queues. */
|
|
225
|
+
export function startPublicHookIngress(receiverUrl: string, port = PUBLIC_HOOK_PORT, stateDir?: string): { url: string; stop: () => void } {
|
|
226
|
+
const receiver = new URL(receiverUrl);
|
|
227
|
+
if (receiver.protocol !== 'http:' || (receiver.hostname !== '127.0.0.1' && receiver.hostname !== 'localhost' && receiver.hostname !== '[::1]'))
|
|
228
|
+
throw new Error('public ingress requires loopback receiver');
|
|
229
|
+
const secret = randomBytes(32).toString('hex');
|
|
230
|
+
const server = Bun.serve({ hostname: '127.0.0.1', port, async fetch(request) {
|
|
231
|
+
const path = new URL(request.url).pathname;
|
|
232
|
+
if (path === PROBE_PATH && request.method === 'GET') {
|
|
233
|
+
const nonce = request.headers.get('x-hooks-ingress-challenge');
|
|
234
|
+
const proof = request.headers.get('x-hooks-ingress-proof');
|
|
235
|
+
if (!nonce || !/^[a-f0-9]{64}$/.test(nonce) || !proof || !/^[a-f0-9]{64}$/.test(proof))
|
|
236
|
+
return new Response('Not Found', { status: 404 });
|
|
237
|
+
const expected = createHmac('sha256', secret).update(`request:${nonce}`).digest();
|
|
238
|
+
if (!timingSafeEqual(Buffer.from(proof, 'hex'), expected)) return new Response('Not Found', { status: 404 });
|
|
239
|
+
return new Response(createHmac('sha256', secret).update(`response:${nonce}`).digest('hex'));
|
|
240
|
+
}
|
|
241
|
+
if (path !== PUBLIC_HOOK_PATH && path !== PUBLIC_GITHUB_HOOK_PATH) return new Response('Not Found', { status: 404 });
|
|
242
|
+
if (request.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });
|
|
243
|
+
return fetch(new URL(path, receiver), { method: 'POST', headers: request.headers, body: request.body });
|
|
244
|
+
} });
|
|
245
|
+
const file = identityFile(stateDir);
|
|
246
|
+
try {
|
|
247
|
+
mkdirSync(join(file, '..'), { recursive: true });
|
|
248
|
+
writeFileSync(file, JSON.stringify({ port: server.port, secret }), { mode: 0o600 });
|
|
249
|
+
} catch (error) { server.stop(); throw error; }
|
|
250
|
+
return { url: server.url.toString(), stop: () => {
|
|
251
|
+
server.stop();
|
|
252
|
+
try { if (readIngressIdentity(stateDir).secret === secret) unlinkSync(file); } catch { /* identity already removed */ }
|
|
253
|
+
} };
|
|
254
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { openMsgStore } from '../msg/msg-store.js';
|
|
4
|
+
import { CardStore, redactSecrets } from '../task-cards/card-store.js';
|
|
5
|
+
import { isGithubSpamText, PUBLIC_GITHUB_REPO } from './providers.js';
|
|
6
|
+
import type { QueuedHook } from './queue.js';
|
|
7
|
+
|
|
8
|
+
export type ShadowVerdict = 'accepted' | 'rejected' | 'human-check';
|
|
9
|
+
export interface ShadowJudgment { verdict: ShadowVerdict; reason: string }
|
|
10
|
+
|
|
11
|
+
const riskyPath = /(^|\/)(?:package\.json$|\.github\/(?:workflows\/|actions\/|CODEOWNERS$)|\.env(?:\.|$)|secrets?(?:\/|\.)|auth(?:\/|\.)|release(?:\/|\.)|scripts\/release-|src\/harness\/|src\/self-implement\/|src\/hooks\/)/i;
|
|
12
|
+
|
|
13
|
+
/** Webhook text is untrusted; a missing PR file list cannot be called safe. */
|
|
14
|
+
export function judgeGithubShadow(title: string, type: 'pull_request' | 'issue', files?: string[], spamSignal = false): ShadowJudgment {
|
|
15
|
+
if (spamSignal || isGithubSpamText(title)) return { verdict: 'rejected', reason: 'spam signal in title or body' };
|
|
16
|
+
if (type === 'pull_request' && (!files || !files.length || files.some(path => !path || riskyPath.test(path))))
|
|
17
|
+
return { verdict: 'human-check', reason: !files?.length ? 'PR file list unavailable' : 'sensitive or invalid file path' };
|
|
18
|
+
return { verdict: 'accepted', reason: 'public contribution candidate (shadow only)' };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Compare the current base tree against GitHub's PR test-merge tree, not the head tree.
|
|
22
|
+
* The test merge includes only changes contributed by this PR onto the current base. Read tree metadata only (no patches). */
|
|
23
|
+
export async function publicPrFileNames(number: number, fetchFn: typeof fetch = fetch): Promise<string[] | undefined> {
|
|
24
|
+
if (!Number.isSafeInteger(number) || number < 1) return undefined;
|
|
25
|
+
const read = async (path: string): Promise<Record<string, unknown> | undefined> => {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetchFn(`https://api.github.com/repos/${PUBLIC_GITHUB_REPO}${path}`, { method: 'GET',
|
|
28
|
+
headers: { accept: 'application/vnd.github+json' }, signal: AbortSignal.timeout(5000) });
|
|
29
|
+
if (!response.ok) return undefined;
|
|
30
|
+
return object(await response.json());
|
|
31
|
+
} catch { return undefined; }
|
|
32
|
+
};
|
|
33
|
+
const pull = await read(`/pulls/${number}`);
|
|
34
|
+
const base = object(pull?.base);
|
|
35
|
+
const head = object(pull?.head);
|
|
36
|
+
const baseSha = base?.sha;
|
|
37
|
+
const headSha = head?.sha;
|
|
38
|
+
const mergeSha = pull?.merge_commit_sha;
|
|
39
|
+
if (pull?.number !== number || object(base?.repo)?.full_name !== PUBLIC_GITHUB_REPO ||
|
|
40
|
+
!gitSha(baseSha) || !gitSha(headSha) || !gitSha(mergeSha)) return undefined;
|
|
41
|
+
const baseCommit = await read(`/git/commits/${baseSha}`);
|
|
42
|
+
const mergeCommit = await read(`/git/commits/${mergeSha}`);
|
|
43
|
+
const parents = mergeCommit?.parents;
|
|
44
|
+
if (mergeCommit?.sha !== mergeSha || !Array.isArray(parents) || parents.length !== 2 ||
|
|
45
|
+
object(parents[0])?.sha !== baseSha || object(parents[1])?.sha !== headSha) return undefined;
|
|
46
|
+
const baseTreeSha = object(baseCommit?.tree)?.sha;
|
|
47
|
+
const mergeTreeSha = object(mergeCommit?.tree)?.sha;
|
|
48
|
+
if (!gitSha(baseTreeSha) || !gitSha(mergeTreeSha)) return undefined;
|
|
49
|
+
const baseTree = await read(`/git/trees/${baseTreeSha}?recursive=1`);
|
|
50
|
+
const mergeTree = await read(`/git/trees/${mergeTreeSha}?recursive=1`);
|
|
51
|
+
const entries = (tree: Record<string, unknown> | undefined): Map<string, string> | undefined => {
|
|
52
|
+
if (!tree || tree.truncated !== false || !Array.isArray(tree.tree) || tree.tree.length > 100_000) return undefined;
|
|
53
|
+
const files = new Map<string, string>();
|
|
54
|
+
for (const row of tree.tree) {
|
|
55
|
+
const entry = object(row);
|
|
56
|
+
if (!entry || typeof entry.path !== 'string' || !gitSha(entry.sha) ||
|
|
57
|
+
!['blob', 'commit', 'tree'].includes(String(entry.type)) ||
|
|
58
|
+
typeof entry.mode !== 'string' || !/^[0-7]{6}$/.test(entry.mode)) return undefined;
|
|
59
|
+
if (entry.type === 'blob' || entry.type === 'commit')
|
|
60
|
+
files.set(entry.path, `${entry.type}:${entry.mode}:${entry.sha}`);
|
|
61
|
+
}
|
|
62
|
+
return files;
|
|
63
|
+
};
|
|
64
|
+
const before = entries(baseTree);
|
|
65
|
+
const after = entries(mergeTree);
|
|
66
|
+
if (!before || !after || baseTree?.sha !== baseTreeSha || mergeTree?.sha !== mergeTreeSha) return undefined;
|
|
67
|
+
return [...new Set([...before.keys(), ...after.keys()])].filter(path => before.get(path) !== after.get(path));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function object(value: unknown): Record<string, unknown> | undefined {
|
|
71
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
|
|
72
|
+
}
|
|
73
|
+
function gitSha(value: unknown): value is string { return typeof value === 'string' && /^[0-9a-f]{40}$/i.test(value); }
|
|
74
|
+
|
|
75
|
+
/** Separate shadow ledger/card path: never enter dispatchHook's seat wake or steward launch/report stages. */
|
|
76
|
+
export async function recordGithubShadow(event: QueuedHook, root: string, fetchFn: typeof fetch = fetch): Promise<void> {
|
|
77
|
+
const github = event.task.github;
|
|
78
|
+
if (event.provider !== 'github' || !github || github.repository !== PUBLIC_GITHUB_REPO ||
|
|
79
|
+
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(event.eventId) ||
|
|
80
|
+
!Number.isSafeInteger(github.number) || github.number < 1 || !['pull_request', 'issue'].includes(github.type))
|
|
81
|
+
throw new Error('invalid GitHub shadow event');
|
|
82
|
+
const files = github.type === 'pull_request' && !github.spamSignal && !isGithubSpamText(event.task.title)
|
|
83
|
+
? await publicPrFileNames(github.number, fetchFn) : undefined;
|
|
84
|
+
const judgment = judgeGithubShadow(event.task.title, github.type, files, github.spamSignal);
|
|
85
|
+
const key = `github:${event.eventId}`;
|
|
86
|
+
const store = openMsgStore(join(root, 'msg', 'messages.db'));
|
|
87
|
+
try {
|
|
88
|
+
store.db.exec(`CREATE TABLE IF NOT EXISTS github_shadow_judgments (
|
|
89
|
+
event_key TEXT PRIMARY KEY, repository TEXT NOT NULL, item_type TEXT NOT NULL, item_number INTEGER NOT NULL,
|
|
90
|
+
verdict TEXT NOT NULL, reason TEXT NOT NULL, url TEXT NOT NULL
|
|
91
|
+
)`);
|
|
92
|
+
store.db.query(`INSERT OR IGNORE INTO github_shadow_judgments
|
|
93
|
+
(event_key, repository, item_type, item_number, verdict, reason, url) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
94
|
+
.run(key, github.repository, github.type, github.number, judgment.verdict, judgment.reason, event.task.external.url ?? '');
|
|
95
|
+
const recorded = store.db.query('SELECT verdict, reason FROM github_shadow_judgments WHERE event_key = ?')
|
|
96
|
+
.get(key) as ShadowJudgment;
|
|
97
|
+
const cards = new CardStore(root);
|
|
98
|
+
try {
|
|
99
|
+
const goalId = `github:${github.repository}:${github.type}:${github.number}`;
|
|
100
|
+
const card = cards.createCard({ goalId, title: redactSecrets(event.task.title).slice(0, 240) });
|
|
101
|
+
const content = JSON.stringify({ event: key, source: goalId, url: event.task.external.url, verdict: recorded.verdict,
|
|
102
|
+
reason: recorded.reason, candidate: recorded.verdict === 'rejected' ? 'intake-only' : 'decision-card' });
|
|
103
|
+
const sectionKey = `intake:${createHash('sha256').update(key).digest('hex')}`;
|
|
104
|
+
if (!card.sections.some(section => section.key === sectionKey))
|
|
105
|
+
cards.appendSection(card.id, { key: sectionKey, owner: 'steward', content: redactSecrets(content) });
|
|
106
|
+
} finally { cards.close(); }
|
|
107
|
+
} finally { store.close(); }
|
|
108
|
+
}
|
package/src/hooks/providers.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
|
|
3
|
-
export type HookProvider = 'linear' | 'asana';
|
|
3
|
+
export type HookProvider = 'linear' | 'asana' | 'github';
|
|
4
|
+
export const PUBLIC_GITHUB_REPO = 'ElanvitalAI/elanous';
|
|
5
|
+
/** Links to documentation and reproductions are not spam signals. */
|
|
6
|
+
export function isGithubSpamText(value: string): boolean {
|
|
7
|
+
return /\b(?:casino|crypto giveaway|free money|seo backlinks|airdrop)\b/i.test(value);
|
|
8
|
+
}
|
|
4
9
|
export interface ExternalTask {
|
|
5
10
|
eventId: string;
|
|
6
11
|
title: string;
|
|
7
12
|
description?: string;
|
|
8
13
|
external: { provider: HookProvider; ref: string; url?: string };
|
|
14
|
+
/** Linear issue assignee ID, resolved by the configured seat mapping; events.routes takes precedence. */
|
|
15
|
+
assigneeId?: string;
|
|
16
|
+
/** L14: an opened PR/issue on the public repository — shadow intake only, never dispatched to seats. */
|
|
17
|
+
github?: { repository: string; number: number; type: 'pull_request' | 'issue'; spamSignal?: boolean };
|
|
9
18
|
}
|
|
10
19
|
export type Verification = { ok: true; body: unknown; eventIds: string[]; ignored?: string } | { ok: false; reason: 'bad-signature' | 'stale' | 'invalid-body' };
|
|
11
20
|
|
|
@@ -29,7 +38,9 @@ export function asanaEventId(event: unknown): string | undefined {
|
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
export function verifyWebhook(provider: HookProvider, raw: Buffer, headers: Headers, secret: string, now = Date.now()): Verification {
|
|
32
|
-
|
|
41
|
+
const signature = headers.get(provider === 'linear' ? 'linear-signature' : provider === 'github' ? 'x-hub-signature-256' : 'x-hook-signature');
|
|
42
|
+
const digest = provider === 'github' ? (signature?.startsWith('sha256=') ? signature.slice(7) : null) : signature;
|
|
43
|
+
if (!equalSignature(raw, secret, digest))
|
|
33
44
|
return { ok: false, reason: 'bad-signature' };
|
|
34
45
|
let parsed: unknown;
|
|
35
46
|
try { parsed = JSON.parse(raw.toString('utf8')); } catch { return { ok: false, reason: 'invalid-body' }; }
|
|
@@ -39,15 +50,45 @@ export function verifyWebhook(provider: HookProvider, raw: Buffer, headers: Head
|
|
|
39
50
|
const timestamp = body.webhookTimestamp;
|
|
40
51
|
if (typeof timestamp !== 'number' || !Number.isFinite(timestamp) || Math.abs(now - timestamp) > 60_000)
|
|
41
52
|
return { ok: false, reason: 'stale' };
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
// Acknowledge signed, out-of-scope events so Linear does not disable the webhook.
|
|
54
|
+
const previous = object(body.updatedFrom);
|
|
55
|
+
const relevantUpdate = body.action === 'update' && previous &&
|
|
56
|
+
('assigneeId' in previous || 'stateId' in previous);
|
|
57
|
+
if (body.type !== 'Issue' || (body.action !== 'create' && !relevantUpdate))
|
|
45
58
|
return { ok: true, body, eventIds: [], ignored: `${String(body.type)}:${String(body.action)}` };
|
|
46
59
|
const eventId = text(headers.get('linear-delivery')) ?? (text(body.webhookId) && text(body.createdAt) ? `${body.webhookId}:${body.createdAt}` : undefined);
|
|
47
60
|
if (!eventId || !object(body.data) || !text(object(body.data)?.id) || !text(object(body.data)?.title))
|
|
48
61
|
return { ok: false, reason: 'invalid-body' };
|
|
49
62
|
return { ok: true, body, eventIds: [eventId] };
|
|
50
63
|
}
|
|
64
|
+
if (provider === 'github' && object(body.repository)?.full_name === PUBLIC_GITHUB_REPO) {
|
|
65
|
+
// L14: the public repository's newly opened PRs/issues go to the shadow intake (no seat dispatch, no outside writes).
|
|
66
|
+
const kind = headers.get('x-github-event');
|
|
67
|
+
if (!['pull_request', 'issues'].includes(kind ?? '') || body.action !== 'opened')
|
|
68
|
+
return { ok: true, body, eventIds: [], ignored: `${kind ?? 'unknown'}:${String(body.action)}` };
|
|
69
|
+
const item = object(body[kind === 'pull_request' ? 'pull_request' : 'issue']);
|
|
70
|
+
const delivery = headers.get('x-github-delivery');
|
|
71
|
+
if (!delivery || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(delivery) || !Number.isSafeInteger(body.number) ||
|
|
72
|
+
(body.number as number) < 1 || !item || item.number !== body.number || !text(item.title) ||
|
|
73
|
+
!text(item.html_url) || !text(object(item.user)?.login) ||
|
|
74
|
+
item.html_url !== `https://github.com/${PUBLIC_GITHUB_REPO}/${kind === 'pull_request' ? 'pull' : 'issues'}/${body.number}`)
|
|
75
|
+
return { ok: false, reason: 'invalid-body' };
|
|
76
|
+
return { ok: true, body: { ...body, githubEvent: kind, publicIntake: true }, eventIds: [delivery] };
|
|
77
|
+
}
|
|
78
|
+
if (provider === 'github') {
|
|
79
|
+
const event = headers.get('x-github-event');
|
|
80
|
+
const action = text(body.action);
|
|
81
|
+
if (!((event === 'pull_request' && action === 'review_requested') ||
|
|
82
|
+
(event === 'check_run' && action === 'completed' && object(body.check_run)?.conclusion === 'failure') ||
|
|
83
|
+
(event === 'check_suite' && action === 'completed' && object(body.check_suite)?.conclusion === 'failure')))
|
|
84
|
+
return { ok: true, body, eventIds: [], ignored: `${String(event)}:${String(action)}` };
|
|
85
|
+
const delivery = text(headers.get('x-github-delivery'));
|
|
86
|
+
const resource = object(event === 'pull_request' ? body.pull_request : event === 'check_run' ? body.check_run : body.check_suite);
|
|
87
|
+
if (!delivery || !resource || typeof resource.id !== 'number' ||
|
|
88
|
+
!(text(resource.html_url) ?? text(resource.details_url) ?? text(object(body.repository)?.html_url)))
|
|
89
|
+
return { ok: false, reason: 'invalid-body' };
|
|
90
|
+
return { ok: true, body: { ...body, githubEvent: event }, eventIds: [delivery] };
|
|
91
|
+
}
|
|
51
92
|
if (!Array.isArray(body.events)) return { ok: false, reason: 'invalid-body' };
|
|
52
93
|
const ids = body.events.map(asanaEventId);
|
|
53
94
|
if (ids.some(id => !id)) return { ok: false, reason: 'invalid-body' };
|
|
@@ -63,9 +104,34 @@ export function toExternalTask(provider: HookProvider, body: unknown): ExternalT
|
|
|
63
104
|
const title = text(issue?.title);
|
|
64
105
|
const eventId = text(row.eventId) ?? (text(row.webhookId) && text(row.createdAt) ? `${row.webhookId}:${row.createdAt}` : undefined);
|
|
65
106
|
if (!ref || !title || !eventId) throw new Error('invalid linear issue');
|
|
107
|
+
const assigneeId = text(issue?.assigneeId) ?? text(object(issue?.assignee)?.id);
|
|
66
108
|
return { eventId, title, ...(text(issue?.description) ? { description: text(issue?.description) } : {}),
|
|
109
|
+
...(assigneeId ? { assigneeId } : {}),
|
|
67
110
|
external: { provider, ref, ...(text(issue?.url) ? { url: text(issue?.url) } : {}) } };
|
|
68
111
|
}
|
|
112
|
+
if (provider === 'github' && row.publicIntake === true) {
|
|
113
|
+
const type = row.githubEvent === 'pull_request' ? 'pull_request' : 'issue';
|
|
114
|
+
const item = object(type === 'pull_request' ? row.pull_request : row.issue);
|
|
115
|
+
const number = row.number;
|
|
116
|
+
const eventId = text(row.eventId);
|
|
117
|
+
if (!Number.isSafeInteger(number) || !item || !eventId || !text(item.title) || !text(item.html_url))
|
|
118
|
+
throw new Error('invalid github event');
|
|
119
|
+
return { eventId, title: item.title as string,
|
|
120
|
+
external: { provider, ref: `${PUBLIC_GITHUB_REPO}#${number}`, url: item.html_url as string },
|
|
121
|
+
github: { repository: PUBLIC_GITHUB_REPO, number: number as number, type,
|
|
122
|
+
spamSignal: typeof item.body === 'string' && isGithubSpamText(item.body) } };
|
|
123
|
+
}
|
|
124
|
+
if (provider === 'github') {
|
|
125
|
+
const event = text(row.githubEvent);
|
|
126
|
+
const resource = object(event === 'pull_request' ? row.pull_request : event === 'check_run' ? row.check_run : row.check_suite);
|
|
127
|
+
const ref = resource?.id;
|
|
128
|
+
const eventId = text(row.eventId);
|
|
129
|
+
const url = text(resource?.html_url) ?? text(resource?.details_url) ?? text(object(row.repository)?.html_url);
|
|
130
|
+
if (typeof ref !== 'number' || !eventId || !url) throw new Error('invalid github event');
|
|
131
|
+
const title = event === 'pull_request' ? `PR review requested: ${text(resource?.title) ?? url}`
|
|
132
|
+
: `Check failed: ${text(resource?.name) ?? text(resource?.head_branch) ?? url}`;
|
|
133
|
+
return { eventId, title, external: { provider, ref: String(ref), url } };
|
|
134
|
+
}
|
|
69
135
|
const event = object(row.event);
|
|
70
136
|
const task = object(row.task);
|
|
71
137
|
const ref = text(object(event?.resource)?.gid);
|
package/src/hooks/queue.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { dirname, join } from 'node:path';
|
|
|
4
4
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
5
5
|
import type { ExternalTask } from './providers.js';
|
|
6
6
|
|
|
7
|
-
export interface QueuedHook { provider: ExternalTask['external']['provider']; eventId: string; task: ExternalTask }
|
|
7
|
+
export interface QueuedHook { provider: ExternalTask['external']['provider']; eventId: string; kind?: string; task: ExternalTask }
|
|
8
8
|
|
|
9
9
|
export class HookQueue {
|
|
10
10
|
readonly directory: string;
|