elanous 0.2.10 → 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/{8067-87ce253ec2f23697.js → 5845-e6c003ce8d5fd36a.js} +1 -1
- 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-54b32bd27726588d.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-35414e0a007b4082.js +1 -0
- 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 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- 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 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -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/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/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/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 +12 -0
- 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/marketing/measure-site-numbers.ts +169 -0
- package/scripts/measure-gate-shards.ts +309 -0
- package/scripts/mission-request-judge.ts +31 -1
- package/scripts/morning-report.ts +2 -2
- package/scripts/opportunity-scan.ts +1 -1
- package/scripts/release-loop/auto-approve-node.ts +21 -4
- package/scripts/release-loop/cut-branch.ts +84 -0
- 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/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 +61 -10
- package/scripts/signal-digest-cycle.ts +2 -7
- package/scripts/signal-digest-photo.ts +11 -0
- package/scripts/tech1-public-measure.ts +94 -0
- package/scripts/test-diet/node.ts +1 -1
- package/scripts/tui-regress.ts +2 -2
- package/scripts/ux/demo-rehearsal.ts +243 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +73 -0
- 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/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 +2 -2
- package/skills/pitch-deck/template.html +2 -2
- 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/server.ts +5 -2
- 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/autopilot/build/llm-conflict-merge.ts +16 -3
- package/src/autopilot/tool-registry.ts +2 -0
- package/src/boot/daemon-prompt-turn.ts +5 -0
- package/src/boot/daemon-runtime.ts +9 -3
- package/src/boot/daemon-tools/web-search.ts +3 -0
- package/src/card-followup/core.ts +109 -0
- package/src/chat/index.ts +1 -0
- 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/persona-cli.ts +90 -4
- package/src/cli/plugin-cli.ts +31 -22
- package/src/cli/pod-cli.ts +19 -9
- package/src/cli/pr-cli.ts +51 -15
- package/src/cli/pty-drive-cli.ts +41 -7
- package/src/cli/pty-drive-verdict.ts +40 -0
- package/src/cli/release-cli.ts +17 -4
- package/src/cli/setup-cli.ts +43 -1
- package/src/cli/skills-cli.ts +17 -1
- 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 +2 -2
- package/src/context-bus/external-events.ts +52 -0
- package/src/context-bus/long-term-memory.ts +108 -0
- package/src/dashboard/index.ts +9 -4
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +7 -1
- package/src/decisions/decision-cards.ts +10 -3
- package/src/decisions/decision-ledger.ts +13 -5
- 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 +17 -1
- package/src/domains/core-tools.ts +3 -2
- package/src/domains/outbound-alert.ts +6 -1
- package/src/domains/telegram-kind-route.ts +51 -13
- package/src/exec-requests/default-outputs.ts +25 -0
- package/src/exec-requests/planner.ts +10 -2
- package/src/exec-requests/runner.ts +20 -6
- package/src/exec-requests/store.ts +3 -0
- package/src/execution-loop/launch-gate.ts +100 -0
- package/src/field/field-reel.ts +5 -1
- package/src/field-note/index.ts +201 -0
- package/src/field-note/transcribe.ts +248 -0
- package/src/graph-kinds/registry.ts +3 -3
- package/src/graph-runner/graph-cli.ts +61 -1
- package/src/graph-runner/graph-grow-llm.ts +37 -0
- package/src/graph-runner/graph-grow.ts +47 -0
- package/src/graph-runner/runner.ts +317 -25
- package/src/grok/agent-search.ts +8 -2
- package/src/guardian/secret-exposure-scan.ts +101 -0
- package/src/harness/harness-cli-command.ts +247 -7
- package/src/harness/harness-pod-dispatch.ts +43 -23
- package/src/harness/harness-seams.ts +38 -4
- package/src/harness/harness-stop.ts +23 -4
- package/src/harness/launch-registry.ts +91 -0
- package/src/harness/process-garbage.ts +5 -3
- 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 +52 -14
- package/src/intake-plane/categorize.ts +18 -46
- package/src/intake-plane/discord-seat-work.ts +10 -0
- 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/telegram-seat-work.ts +36 -2
- 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/feature-maturity.ts +26 -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/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 +36 -0
- package/src/nexus/api/inside-events.ts +226 -0
- package/src/nexus/api/meta-api.ts +21 -8
- package/src/nexus/api/missions.ts +6 -1
- package/src/nexus/api/ops-seats.ts +33 -0
- package/src/nexus/api/outputs-route.ts +44 -0
- package/src/nexus/api/seat-requests.ts +44 -2
- package/src/nexus/api/sessions-store.ts +17 -38
- 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/workflow-run-event-bridge.ts +1 -0
- package/src/nexus/api/workflows.ts +80 -6
- package/src/nexus/index.ts +2 -0
- 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/voice-ai.ts +3 -3
- 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-suggestion.ts +16 -0
- package/src/project/session-context.ts +24 -0
- package/src/release-loop/checklist.ts +2 -2
- package/src/release-loop/feature-store.ts +5 -3
- package/src/release-loop/next-md-merge.ts +58 -0
- package/src/release-loop/release-note.ts +59 -4
- 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 +548 -29
- package/src/seat-loop/seat-report.ts +37 -2
- 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/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/gate-baseline.ts +1 -1
- package/src/self-implement/gate-cli.ts +40 -1
- 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 +242 -9
- package/src/self-implement/route-consumer-test-index.ts +122 -0
- 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/index.ts +36 -13
- package/src/skills/gift-install.ts +215 -0
- package/src/skills/tools/web-search.ts +4 -1
- package/src/steward/launch.ts +42 -26
- package/src/steward/steward-cards.ts +17 -9
- package/src/steward/triage.ts +46 -14
- package/src/task-cards/card-cli.ts +25 -1
- 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 +156 -19
- 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 +16 -96
- package/src/telegram-commands.ts +10 -1
- package/src/telegram-project-command.ts +71 -0
- package/src/telegram-report.ts +21 -17
- package/src/telegram.ts +29 -3
- package/src/tool-runtime/self-cognition-runtimes.ts +13 -3
- package/src/user-config.ts +102 -20
- package/src/version/packed-revision.json +1 -1
- 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/2DsiDfDCks44g1cPUMhiW/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1105-c473ed46272fba79.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/24-e6d28380efab7faf.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/3615-740dd270764ffa24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/371-4ad6af55dad02ced.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3734-1015fe991eb9b7e7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4114-3e63117452378807.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5012-a7fe6f3c8b6fe45a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5609-012bb3557211f07e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5783-ef40ebfc91d55230.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5791-bb82743fa6299562.js +0 -1
- package/apps/pwa/out/_next/static/chunks/62-ff227c3edb8b248b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6320-baac91d68843a9a6.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/7633-1b3709b0854c9412.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/7951-8b03dbb10b24cd06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7990-e12efba616f46e03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8142-744998d65d7bb17e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8195-6fde3d7a39859206.js +0 -10
- package/apps/pwa/out/_next/static/chunks/8619-4365e03a2862830f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-b5fb36bab837cd29.js +0 -1
- package/apps/pwa/out/_next/static/chunks/920-f7dceb78f1c585ef.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9454-84d0d5d77446bd92.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9651-3cd291a6fa1370d2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/988-5f92e7ab7ccea3dd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9989-5582582f06429840.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-d2253d7e58ef1890.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-5441d4a9ab35e518.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d9b12d680922d980.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e268d6d145827a22.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-f8116559c7424da7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-1085399fd0f32112.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1df686a14f65895a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-54e1f966ab60a4dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-7ed95f296f5ee8e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-11edac668d77e5c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-59e2b9a22cd5a6d5.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-2976df21ad74ab48.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-34d211a67f16d510.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8ff652d5d950639b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-b31610deda76337b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6cb0e1bbefe5fb3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-a286a152cf584576.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-d9237581e7003bcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-83274e761643fdd2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-ceee6cfb1d4e6d68.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-9c9f8bb3e4a64091.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c17217603ea3426a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-efb6177cbf1e8a51.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-d307523703bc4df3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-462f4e4bab45ce7d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8990e96f33af3837.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e168fa1030af0548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1233e05a4068bc9c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-13a1840b1fac337a.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-64a29fc80b11ab55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-68e787339a16b3dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-4fddc90b23feb215.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-886ba8260dc79c79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-0f26507a1722e40b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-109d4ccd42259c9d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-f5c2a4e510726774.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-b2e0039416f42522.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-26102fd7350f171e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-708948bc8a802794.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-5e5611620f78fb0b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-139ba5c367bbf60a.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-202b4e81f33895ea.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/43feca5fd387dcbd.css +0 -1
- /package/apps/pwa/out/_next/static/{2DsiDfDCks44g1cPUMhiW → Y4zKzL62sakG5momz3SoQ}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
5
|
+
import { lastJsonObject, runGraph as defaultRunGraph, type GraphRunState } from '../graph-runner/runner.js';
|
|
6
|
+
|
|
7
|
+
const GRAPH = resolve(import.meta.dir, '../../plugins/card-followup/graphs/card-followup.yaml');
|
|
8
|
+
export type CardDetectDecision = 'card' | 'skip' | 'ambiguous';
|
|
9
|
+
|
|
10
|
+
export function cardShape(width?: number, height?: number): { decision: 'skip' | 'pass'; signal: string } {
|
|
11
|
+
if (!width || !height) return { decision: 'pass', signal: 'ratio:unknown' };
|
|
12
|
+
const ratio = Math.max(width, height) / Math.min(width, height);
|
|
13
|
+
if (ratio < 1.15) return { decision: 'skip', signal: 'ratio:square' };
|
|
14
|
+
if (ratio > 2.0) return { decision: 'skip', signal: 'ratio:screenshot' };
|
|
15
|
+
return { decision: 'pass', signal: `ratio:${ratio.toFixed(2)}` };
|
|
16
|
+
}
|
|
17
|
+
const EMAIL = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i;
|
|
18
|
+
const PHONE = /(?:\+?\d{1,3}[\s.-]?)?\(?\d{2,4}\)?[\s.-]?\d{3,4}[\s.-]?\d{4}/;
|
|
19
|
+
const WEB = /\b(?:https?:\/\/|www\.)\S+|\b[a-z0-9-]+\.(?:com|co\.kr|kr|io|ai|net|org)\b/i;
|
|
20
|
+
const TITLE = /대표|이사|팀장|부장|과장|실장|매니저|주식회사|\(주\)|\b(?:CEO|CTO|COO|CFO|Director|Manager|Founder|Inc|Ltd|Corp)\b/i;
|
|
21
|
+
export function cardText(ocr: string | null): { decision: CardDetectDecision; signal: string } {
|
|
22
|
+
if (ocr === null) return { decision: 'ambiguous', signal: 'ocr:unavailable' };
|
|
23
|
+
const chars = ocr.replace(/\s+/g, '').length;
|
|
24
|
+
if (chars < 15) return { decision: 'ambiguous', signal: 'ocr:chars<15' };
|
|
25
|
+
if (chars > 700) return { decision: 'skip', signal: 'ocr:document' };
|
|
26
|
+
const hits = ([['email', EMAIL], ['phone', PHONE], ['web', WEB], ['title', TITLE]] as const).filter(([, re]) => re.test(ocr)).map(([name]) => name);
|
|
27
|
+
const signal = `ocr:${hits.join(',') || 'none'}`;
|
|
28
|
+
if (hits.length >= 2) return { decision: 'card', signal };
|
|
29
|
+
return { decision: hits.length === 0 ? 'skip' : 'ambiguous', signal };
|
|
30
|
+
}
|
|
31
|
+
export function detectCard({ width, height, ocrText }: { width?: number; height?: number; ocrText: string | null }): { decision: CardDetectDecision; signal: string } {
|
|
32
|
+
const shape = cardShape(width, height);
|
|
33
|
+
return shape.decision === 'skip' ? { decision: 'skip', signal: shape.signal } : cardText(ocrText);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const obj = (value: unknown): Record<string, unknown> => value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
37
|
+
const text = (value: unknown): string => typeof value === 'string' ? value.trim() : '';
|
|
38
|
+
const line = (value: unknown): string => (typeof value === 'number' && Number.isFinite(value) ? String(value) : text(value)).replace(/[\r\n]+/g, ' ') || '—';
|
|
39
|
+
|
|
40
|
+
export class NotACardError extends Error {}
|
|
41
|
+
function isNotACard(state: GraphRunState): boolean {
|
|
42
|
+
if (state.status !== 'failed') return false;
|
|
43
|
+
const read = state.nodes.find((node) => node.nodeId === 'read-card');
|
|
44
|
+
if (!read || !read.ok) return false;
|
|
45
|
+
const output = lastJsonObject(read.output);
|
|
46
|
+
if (output?.outcome === 'ok' && output.card && typeof output.card === 'object') {
|
|
47
|
+
const card = obj(output.card);
|
|
48
|
+
return !['name', 'company', 'title', 'email', 'phone', 'url', 'linkedin'].some((field) => text(card[field]));
|
|
49
|
+
}
|
|
50
|
+
return output?.outcome === 'fail' && typeof output.reason === 'string'
|
|
51
|
+
&& /\bnot a (?:business )?card\b|명함이?\s*아님|명함이?\s*아닙니다/i.test(output.reason);
|
|
52
|
+
}
|
|
53
|
+
function validateReport(report: Record<string, unknown>): void {
|
|
54
|
+
const card = obj(report.card), fit = obj(report.fit), approach = obj(report.approach);
|
|
55
|
+
const next = obj(report.nextAction), draft = obj(report.draft);
|
|
56
|
+
const score = fit.score;
|
|
57
|
+
if (report.sent !== false || !text(card.name) || !text(card.company)
|
|
58
|
+
|| !(score === null || (typeof score === 'number' && Number.isFinite(score)))
|
|
59
|
+
|| !text(fit.label) || !text(approach.problem) || !text(approach.proposal)
|
|
60
|
+
|| !text(approach.channel) || !text(next.what) || !text(next.due)
|
|
61
|
+
|| !text(draft.subject) || !text(draft.body)) {
|
|
62
|
+
throw new Error('incomplete or sent card report');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function runCardFollowup({ imagePath, ocrText, runGraph = defaultRunGraph, rootDir = effectiveInstanceRoot(), outDir, runId = randomUUID(), context }: {
|
|
67
|
+
imagePath: string;
|
|
68
|
+
ocrText: string | null;
|
|
69
|
+
runGraph?: typeof defaultRunGraph;
|
|
70
|
+
rootDir?: string;
|
|
71
|
+
outDir?: string;
|
|
72
|
+
runId?: string;
|
|
73
|
+
context?: string;
|
|
74
|
+
}): Promise<{ replies: [string, string, string]; followupPath?: string }> {
|
|
75
|
+
if (ocrText !== null && cardText(ocrText).decision !== 'card') throw new NotACardError('not a card');
|
|
76
|
+
const base = join(rootDir, 'graph-runs', 'card-followup');
|
|
77
|
+
const dir = outDir ?? join(base, runId);
|
|
78
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
79
|
+
chmodSync(base, 0o700);
|
|
80
|
+
chmodSync(dir, 0o700);
|
|
81
|
+
const statePath = join(base, `${runId}.json`);
|
|
82
|
+
const sealState = () => {
|
|
83
|
+
try { chmodSync(statePath, 0o600); } catch { /* runner may not have written state */ }
|
|
84
|
+
const contexts = `${statePath}.contexts`;
|
|
85
|
+
try {
|
|
86
|
+
chmodSync(contexts, 0o700);
|
|
87
|
+
for (const file of readdirSync(contexts)) chmodSync(join(contexts, file), 0o600);
|
|
88
|
+
} catch { /* no contexts yet */ }
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
const state = await runGraph(GRAPH, { runId, deps: { root: rootDir }, input: {
|
|
92
|
+
image: imagePath, outDir: dir, ...(context ? { context } : {}),
|
|
93
|
+
} });
|
|
94
|
+
if (isNotACard(state)) throw new NotACardError('not a card');
|
|
95
|
+
if (state.status !== 'done') throw new Error('graph did not finish');
|
|
96
|
+
const report = obj(JSON.parse(readFileSync(join(dir, 'followup.json'), 'utf8')));
|
|
97
|
+
validateReport(report);
|
|
98
|
+
const card = obj(report.card), fit = obj(report.fit), approach = obj(report.approach);
|
|
99
|
+
const next = obj(report.nextAction), draft = obj(report.draft);
|
|
100
|
+
const summary = `① 요약\n${line(card.name)} · ${line(card.company)} · ${line(card.title)}\n타겟 판정: ${line(fit.score)} · ${line(fit.label)}`;
|
|
101
|
+
const strategy = `② 전략\n${[approach.problem, approach.proposal, approach.channel].map(line).join('\n')}\n다음 행동: ${line(next.what)} · ${line(next.due)}`;
|
|
102
|
+
const mail = `③ 팔로업 초안 (보내지 않았습니다)\n제목: ${line(draft.subject)}\n${text(draft.body)}`;
|
|
103
|
+
const followupPath = join(dir, 'followup.md');
|
|
104
|
+
readFileSync(followupPath);
|
|
105
|
+
return { replies: [summary, strategy, mail], followupPath };
|
|
106
|
+
} finally {
|
|
107
|
+
if (runGraph === defaultRunGraph) sealState();
|
|
108
|
+
}
|
|
109
|
+
}
|
package/src/chat/index.ts
CHANGED
|
@@ -437,6 +437,7 @@ export const SLASH_COMMANDS: SlashCommand[] = [
|
|
|
437
437
|
{ name: 'model', aliases: ['m'], description: '모델 바꾸기 — /model <codex|terra|sol|luna|opus|sonnet|grok> (list + current when no arg). OpenAI 는 Codex(Responses API)만. effort 는 /reasoning.', subcommands: ['codex', 'terra', 'sol', 'luna', 'opus', 'sonnet', 'grok'] },
|
|
438
438
|
{ name: 'clear', aliases: ['cls'], description: '대화 기록 지우기' },
|
|
439
439
|
{ name: 'status', aliases: ['st'], description: '지금 상태 — 모델 · 연결 · 계정' },
|
|
440
|
+
{ name: 'now', aliases: [], description: '현재 맥락과 출처 표 — /now [주제]' },
|
|
440
441
|
{ name: 'remaining', aliases: [], description: '계정별 남은 사용량' },
|
|
441
442
|
{ name: 'setup', aliases: [], description: '상세 설정 — /setup · /setup reset(다음 실행 때 마법사 다시)', subcommands: ['help', 'reset'] },
|
|
442
443
|
{ name: 'quit', aliases: ['q', 'exit'], description: '나가기' },
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, isAbsolute, relative, sep } from 'node:path';
|
|
4
|
+
|
|
5
|
+
export type DriveInputJailVerdict =
|
|
6
|
+
| { readonly allowed: true }
|
|
7
|
+
| { readonly allowed: false; readonly reason: 'outside-jail'; readonly path: string };
|
|
8
|
+
|
|
9
|
+
/** Resolve each existing component before applying the next '..', including write targets. */
|
|
10
|
+
function canonicalPath(path: string): string {
|
|
11
|
+
let current = isAbsolute(path) ? sep : realpathSync(process.cwd());
|
|
12
|
+
for (const part of path.split(sep)) {
|
|
13
|
+
if (!part || part === '.') continue;
|
|
14
|
+
if (part === '..') {
|
|
15
|
+
current = dirname(current);
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const next = `${current === sep ? '' : current}${sep}${part}`;
|
|
19
|
+
try {
|
|
20
|
+
current = realpathSync(next);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
23
|
+
current = next;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return current;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function insideJail(boundary: string, candidate: string): boolean {
|
|
30
|
+
const rel = relative(boundary, candidate);
|
|
31
|
+
return rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check path literals in brain-generated PTY text without executing or rewriting the text.
|
|
36
|
+
* Absolute paths and home paths (~ and ~/) are checked against the canonical jail, including symlink
|
|
37
|
+
* ancestors. This is a path-literal guard, not a shell interpreter or a sandbox for the child.
|
|
38
|
+
*/
|
|
39
|
+
export function checkDriveInputJail(input: string, boundary: string, home: string = homedir()): DriveInputJailVerdict {
|
|
40
|
+
const jail = canonicalPath(boundary);
|
|
41
|
+
// Quoted path arguments can contain spaces. Inspect those as one literal before scanning
|
|
42
|
+
// unquoted tokens (which also covers option assignments such as --prefix=/opt/homebrew).
|
|
43
|
+
const quotedRanges: Array<[number, number]> = [];
|
|
44
|
+
const literals: string[] = [];
|
|
45
|
+
for (const match of input.matchAll(/(["'])(.*?)\1/gs)) {
|
|
46
|
+
const start = match.index ?? 0;
|
|
47
|
+
if (match[2]?.startsWith('/') || match[2] === '~' || match[2]?.startsWith('~/')) {
|
|
48
|
+
// A double-quoted string still runs $(…) and `…` — keep scanning inside it so a substituted
|
|
49
|
+
// command's own paths (e.g. "/jail/$(cat /etc/passwd)") are checked too.
|
|
50
|
+
const expands = match[1] === '"' && /\$\(|`/.test(match[2]);
|
|
51
|
+
if (!expands) quotedRanges.push([start, start + match[0].length]);
|
|
52
|
+
if (/\bPATH=$/.test(input.slice(0, start))) literals.push(...match[2].split(':').filter(Boolean));
|
|
53
|
+
else literals.push(match[2]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const candidates = /(^|[\s"'`|&=:<>(\[,;])((?:~\/|\/)[^\s"'`|:;&<>()[\]{}]*|~(?=$|[\s"'`|:;&<>()[\]{},]))/gm;
|
|
57
|
+
for (const match of input.matchAll(candidates)) {
|
|
58
|
+
const literal = match[2]!;
|
|
59
|
+
const start = (match.index ?? 0) + match[1]!.length;
|
|
60
|
+
if (quotedRanges.some(([from, to]) => from < start && start < to)) continue;
|
|
61
|
+
if (literal.startsWith('//') && /\w+:$/.test(input.slice(Math.max(0, start - 12), start))) continue;
|
|
62
|
+
literals.push(literal);
|
|
63
|
+
}
|
|
64
|
+
for (const literal of literals) {
|
|
65
|
+
const expanded = literal === '~' ? home : literal.startsWith('~/') ? `${home.replace(/\/$/, '')}/${literal.slice(2)}` : literal;
|
|
66
|
+
if (!insideJail(jail, canonicalPath(expanded))) return { allowed: false, reason: 'outside-jail', path: literal };
|
|
67
|
+
}
|
|
68
|
+
return { allowed: true };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DriveJailDeps {
|
|
72
|
+
inject(text: string): boolean;
|
|
73
|
+
observe(): string | Promise<string>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Put a drive's PTY input behind the jail. A blocked input is never sent to the PTY; the next screen the brain
|
|
78
|
+
* reads carries one line saying why, so it can try another way inside the jail (returning false from inject
|
|
79
|
+
* would end the loop as a human takeover). No jail → the deps come back unchanged.
|
|
80
|
+
*/
|
|
81
|
+
export function withDriveJail<T extends DriveJailDeps>(deps: T, jail: string | undefined, log?: (event: string, data: Record<string, unknown>) => void): T {
|
|
82
|
+
if (!jail) return deps;
|
|
83
|
+
let notice: string | null = null;
|
|
84
|
+
return {
|
|
85
|
+
...deps,
|
|
86
|
+
inject: (text: string) => {
|
|
87
|
+
const verdict = checkDriveInputJail(text, jail);
|
|
88
|
+
if (verdict.allowed) return deps.inject(text);
|
|
89
|
+
notice = `⚠ elanous: input blocked — ${verdict.path} is outside the boundary ${jail} · try another way inside it`;
|
|
90
|
+
log?.('jail-block', { path: verdict.path, input: text.slice(0, 80) });
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
observe: async (): Promise<string> => {
|
|
94
|
+
const frame = await deps.observe();
|
|
95
|
+
if (notice === null) return frame;
|
|
96
|
+
const line = notice;
|
|
97
|
+
notice = null;
|
|
98
|
+
return `${frame}\n${line}`;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
package/src/cli/hooks-cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { startHookReceiver } from '../hooks/receiver.js';
|
|
3
3
|
import { HookQueue } from '../hooks/queue.js';
|
|
4
|
+
import { closeIngressAndExposure, hookExposurePlan, hookExposureStatus, startPublicHookIngress, turnHookExposureOff, turnHookExposureOn, type HookExposureDeps } from '../hooks/expose.js';
|
|
4
5
|
import { getSecretAsync, setSecretAsync } from '../nexus/config/secrets/index.js';
|
|
5
6
|
import { getUserConfig } from '../user-config.js';
|
|
6
7
|
|
|
@@ -9,7 +10,7 @@ export function asanaEnabled(raw: unknown = getUserConfig().raw): boolean {
|
|
|
9
10
|
return tox?.external?.asana?.enabled === true;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
export function registerHooksCommands(program: Command): void {
|
|
13
|
+
export function registerHooksCommands(program: Command, exposureDeps: HookExposureDeps = {}): void {
|
|
13
14
|
const hooks = program.command('hooks').description('외부 웹훅 수신·재전송');
|
|
14
15
|
hooks.command('serve').description('웹훅 수신기를 포그라운드에서 실행')
|
|
15
16
|
.option('--host <host>', '리스닝 주소', '127.0.0.1')
|
|
@@ -20,22 +21,35 @@ export function registerHooksCommands(program: Command): void {
|
|
|
20
21
|
throw new Error('invalid hooks port');
|
|
21
22
|
const secrets = {
|
|
22
23
|
linear: await getSecretAsync('hooks.linear'), asana: await getSecretAsync('hooks.asana'),
|
|
24
|
+
github: await getSecretAsync('hooks.github'),
|
|
23
25
|
// 첫 핸드셰이크의 비밀을 저장하는 것은 «먼저 온 사람이 이긴다» — Asana 를 켠 설치에서만 연다.
|
|
24
26
|
...(asanaEnabled() ? { saveAsana: (value: string) => setSecretAsync('hooks.asana', value) } : {}),
|
|
25
27
|
};
|
|
26
28
|
const server = startHookReceiver({ host: opts.host, port, secrets });
|
|
29
|
+
let ingress: ReturnType<typeof startPublicHookIngress> | undefined;
|
|
30
|
+
try { if (opts.host === '127.0.0.1' && port === 31480) ingress = startPublicHookIngress(server.url); }
|
|
31
|
+
catch (error) { server.stop(); throw error; }
|
|
27
32
|
console.log(`hooks listening ${new URL(server.url).host}`);
|
|
28
33
|
await new Promise<void>(resolve => {
|
|
29
34
|
const stop = () => {
|
|
30
35
|
process.removeListener('SIGINT', stop);
|
|
31
36
|
process.removeListener('SIGTERM', stop);
|
|
32
|
-
server.stop();
|
|
33
|
-
resolve();
|
|
37
|
+
void closeIngressAndExposure(ingress, exposureDeps).finally(() => { server.stop(); resolve(); });
|
|
34
38
|
};
|
|
35
39
|
process.once('SIGINT', stop);
|
|
36
40
|
process.once('SIGTERM', stop);
|
|
37
41
|
});
|
|
38
42
|
});
|
|
43
|
+
const expose = hooks.command('expose').description('결정 원장에 묶인 웹훅 공개 진입');
|
|
44
|
+
expose.command('plan').description('공개 경로와 tailscale 명령만 표시').action(() => console.log(JSON.stringify(hookExposurePlan())));
|
|
45
|
+
expose.command('on').description('결정 뒤 공개 진입 켜기').option('--decision <D-id>')
|
|
46
|
+
.action(async (opts: { decision?: string }) => console.log(JSON.stringify(await turnHookExposureOn(opts.decision, exposureDeps))));
|
|
47
|
+
expose.command('off').description('이 도구가 켠 공개 진입 끄기(결정 불필요)').option('--force', '기록·대상 확인 없이 8443 Funnel 을 끈다')
|
|
48
|
+
.action(async (opts: { force?: boolean }) => {
|
|
49
|
+
const result = await turnHookExposureOff(exposureDeps, { force: opts.force });
|
|
50
|
+
console.log(result.changed ? `hooks exposure off (${result.reason})` : 'hooks exposure: nothing of ours to turn off (8443 Funnel is not recorded and does not target the hook ingress; --force to turn it off anyway)');
|
|
51
|
+
});
|
|
52
|
+
expose.command('status').description('기록된 공개 경로·결정·시각').action(() => console.log(JSON.stringify(hookExposureStatus(exposureDeps))));
|
|
39
53
|
hooks.command('status').description('대기 중인 웹훅 수와 마지막 전달 시각')
|
|
40
54
|
.action(() => {
|
|
41
55
|
const queue = new HookQueue();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { debug } from '../debug/log.js';
|
|
3
|
+
import { judge } from '../llm/judge-layer.js';
|
|
4
|
+
import { runPluginMake } from './plugin-cli.js';
|
|
5
|
+
import { workflowRun } from './workflow.js';
|
|
6
|
+
|
|
7
|
+
type MakeKind = 'plugin' | 'workflow';
|
|
8
|
+
type MakeDeps = {
|
|
9
|
+
decide?: (request: string) => Promise<MakeKind | null>;
|
|
10
|
+
pluginMake?: (request: string) => Promise<void>;
|
|
11
|
+
workflowMake?: (request: string) => Promise<number>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export async function classifyMakeRequest(request: string, call?: (args: { prompt: string; provider: string; model: string }) => Promise<{ text: string }>): Promise<MakeKind | null> {
|
|
15
|
+
if (!request.trim()) return null;
|
|
16
|
+
const result = await judge<MakeKind>({
|
|
17
|
+
site: 'cli.make',
|
|
18
|
+
...(call ? { call } : {}),
|
|
19
|
+
prompt: `Classify this request for an Elanous creator. Choose "plugin" for a reusable connector, integration, skill, or capability (including reading a file format). Choose "workflow" for a sequence of jobs, recurring schedule, automation, or publishing task, even when it uses a connector. If you cannot decide, return {"kind":"unclear"}. Respond with JSON only, exactly {"kind":"plugin"} or {"kind":"workflow"} or {"kind":"unclear"}. Do not execute the request.\nRequest: ${JSON.stringify(request)}`,
|
|
20
|
+
schema: value => {
|
|
21
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
22
|
+
const kind = (value as { kind?: unknown }).kind;
|
|
23
|
+
return kind === 'plugin' || kind === 'workflow' ? kind : null;
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
return result.ok ? result.value : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function registerMakeCommand(program: Command, deps: MakeDeps = {}): void {
|
|
30
|
+
program.command('make <request>')
|
|
31
|
+
.description('Classify a one-line request and make a plugin or build a workflow')
|
|
32
|
+
.action(async (request: string) => {
|
|
33
|
+
const kind = await (deps.decide ?? classifyMakeRequest)(request).catch(() => null);
|
|
34
|
+
debug.log('cli.make', 'route', { kind: kind ?? 'undecided' });
|
|
35
|
+
if (!kind) {
|
|
36
|
+
console.log('플러그인(커넥터·스킬)을 만들까요, 워크플로(잡)를 만들까요?');
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (kind === 'plugin') await (deps.pluginMake ?? runPluginMake)(request);
|
|
41
|
+
else process.exitCode = await (deps.workflowMake ?? ((text: string) => workflowRun('build-workflow', text)))(request);
|
|
42
|
+
});
|
|
43
|
+
}
|
package/src/cli/persona-cli.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// `elanous persona list|add|edit|show`: 프리셋을 보고, 내 페르소나를 만들고 꾸민다.
|
|
2
2
|
// 만든 페르소나는 기존 페르소나 저장소(resolveStatePersonaDir · Discord·PWA 가 읽는 곳)의 `<id>.yaml` 한 파일이다.
|
|
3
3
|
import type { Command } from 'commander';
|
|
4
4
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -7,7 +7,8 @@ import { parse as parseYaml } from 'yaml';
|
|
|
7
7
|
import { debug } from '../debug/log.js';
|
|
8
8
|
import { parsePersonaYaml } from '../persona/loader.js';
|
|
9
9
|
import { resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
|
|
10
|
-
import { choosePersonaId, describePreset, findPreset, loadPresetIndex, loadPresets, presetToProfileYaml } from '../persona/presets.js';
|
|
10
|
+
import { choosePersonaId, clonePreset, describePreset, editPersona, findPreset, loadPresetIndex, loadPresets, presetToProfileYaml } from '../persona/presets.js';
|
|
11
|
+
import { addTeamSet, listTeamSets, removeTeamSet } from '../persona/team-set.js';
|
|
11
12
|
|
|
12
13
|
interface StoredPersona { personaId: string; displayName: string; description?: string; preset?: string; title?: string; path: string }
|
|
13
14
|
|
|
@@ -36,8 +37,52 @@ function takenIds(stateDir: string): Set<string> {
|
|
|
36
37
|
return ids;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
function mapping(value: unknown): value is Record<string, unknown> {
|
|
41
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function setEdit(edits: Record<string, unknown>, assignment: string): void {
|
|
45
|
+
const equals = assignment.indexOf('=');
|
|
46
|
+
if (equals < 1) throw new Error(`--set requires key=value: ${assignment}`);
|
|
47
|
+
const key = assignment.slice(0, equals);
|
|
48
|
+
const parts = key.split('.');
|
|
49
|
+
if (parts.some((part) => !part || ['__proto__', 'prototype', 'constructor'].includes(part))) {
|
|
50
|
+
throw new Error(`invalid --set key: ${key}`);
|
|
51
|
+
}
|
|
52
|
+
const value: unknown = parseYaml(assignment.slice(equals + 1));
|
|
53
|
+
let target = edits;
|
|
54
|
+
for (const part of parts.slice(0, -1)) {
|
|
55
|
+
if (!Object.hasOwn(target, part)) target[part] = {};
|
|
56
|
+
if (!mapping(target[part])) throw new Error(`--set key conflicts with a scalar: ${key}`);
|
|
57
|
+
target = target[part];
|
|
58
|
+
}
|
|
59
|
+
target[parts.at(-1)!] = value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function editOptions(fromFile: string | undefined, assignments: readonly string[]): Record<string, unknown> {
|
|
63
|
+
const parsed: unknown = fromFile ? parseYaml(readFileSync(fromFile, 'utf8')) : {};
|
|
64
|
+
if (!mapping(parsed)) throw new Error('--from-file must contain a YAML mapping');
|
|
65
|
+
const edits = { ...parsed };
|
|
66
|
+
for (const assignment of assignments) setEdit(edits, assignment);
|
|
67
|
+
return edits;
|
|
68
|
+
}
|
|
69
|
+
|
|
39
70
|
export function registerPersonaCommands(program: Command): void {
|
|
40
|
-
const
|
|
71
|
+
const team = program.command('team-set').description('Install, remove and list persona team sets');
|
|
72
|
+
team.command('add <file>').description('Install a YAML team set').action((file: string) => {
|
|
73
|
+
try { const result = addTeamSet(file); console.log(`Installed team-set ${result['team-set']}`); }
|
|
74
|
+
catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; }
|
|
75
|
+
});
|
|
76
|
+
team.command('remove <name>').description('Remove an installed team set').action((name: string) => {
|
|
77
|
+
try { removeTeamSet(name); console.log(`Removed team-set ${name}`); }
|
|
78
|
+
catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; }
|
|
79
|
+
});
|
|
80
|
+
team.command('list').description('List installed team sets').action(() => {
|
|
81
|
+
try { for (const item of listTeamSets()) console.log(`${item['team-set']} ${item.members.map((member) => `${member.persona} (${member.title})`).join(', ')}`); }
|
|
82
|
+
catch (error) { console.error(error instanceof Error ? error.message : String(error)); process.exitCode = 1; }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const persona = program.command('persona').description('Persona presets — AI coworkers you pick and customise (list · add · edit · show)');
|
|
41
86
|
|
|
42
87
|
persona.command('list')
|
|
43
88
|
.description('List the persona presets, then the personas you have made')
|
|
@@ -63,9 +108,10 @@ export function registerPersonaCommands(program: Command): void {
|
|
|
63
108
|
persona.command('add <preset>')
|
|
64
109
|
.description('Make your own persona from a preset (id, name or role) — the preset file is not changed')
|
|
65
110
|
.option('--name <name>', 'What to call it (default: the preset’s first name)')
|
|
111
|
+
.option('--as <name>', 'Clone this preset under a new, unique name')
|
|
66
112
|
.option('--title <title>', 'Optional title, e.g. CMO')
|
|
67
113
|
.option('--json', 'JSON output')
|
|
68
|
-
.action((query: string, opts: { name?: string; title?: string; json?: boolean }) => {
|
|
114
|
+
.action((query: string, opts: { name?: string; as?: string; title?: string; json?: boolean }) => {
|
|
69
115
|
const presets = loadPresets();
|
|
70
116
|
const preset = findPreset(presets, query);
|
|
71
117
|
if (!preset) {
|
|
@@ -73,6 +119,23 @@ export function registerPersonaCommands(program: Command): void {
|
|
|
73
119
|
process.exitCode = 1;
|
|
74
120
|
return;
|
|
75
121
|
}
|
|
122
|
+
if (opts.as !== undefined) {
|
|
123
|
+
try {
|
|
124
|
+
if (opts.name !== undefined || opts.title !== undefined) throw new Error('--as cannot be combined with --name or --title');
|
|
125
|
+
const result = clonePreset(preset, opts.as);
|
|
126
|
+
debug.log('persona.cli', 'cloned', { preset: preset.personaId, personaId: result.personaId });
|
|
127
|
+
if (opts.json) console.log(JSON.stringify({ personaId: result.personaId, displayName: result.displayName, preset: preset.personaId, path: result.path }, null, 2));
|
|
128
|
+
else {
|
|
129
|
+
console.log(`만들었습니다 — ${result.displayName} (id ${result.personaId} · 프리셋 ${preset.personaId})`);
|
|
130
|
+
console.log(` 처음 묻는 것: ${preset.firstQuestions.join(' / ')}`);
|
|
131
|
+
console.log(` 보기: elanous persona show ${result.personaId}`);
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
135
|
+
process.exitCode = 1;
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
76
139
|
const dir = resolveStatePersonaDir();
|
|
77
140
|
const personaId = choosePersonaId(preset, opts.name, takenIds(dir));
|
|
78
141
|
const yaml = presetToProfileYaml(preset, loadPresetIndex().common, { personaId, ...(opts.name ? { name: opts.name } : {}), ...(opts.title ? { title: opts.title } : {}) });
|
|
@@ -89,6 +152,29 @@ export function registerPersonaCommands(program: Command): void {
|
|
|
89
152
|
console.log(` 보기: elanous persona show ${personaId}`);
|
|
90
153
|
});
|
|
91
154
|
|
|
155
|
+
persona.command('edit <name>')
|
|
156
|
+
.description('Edit a saved persona (schema fields only; the preset source is not changed)')
|
|
157
|
+
.option('--set <key=value>', 'Set a field (repeatable; YAML values)', (value: string, values: string[]) => [...values, value], [] as string[])
|
|
158
|
+
.option('--from-file <path>', 'Load edits from a YAML mapping')
|
|
159
|
+
.option('--dry-run', 'Validate and preview without writing')
|
|
160
|
+
.option('--json', 'JSON output')
|
|
161
|
+
.action((name: string, opts: { set: string[]; fromFile?: string; dryRun?: boolean; json?: boolean }) => {
|
|
162
|
+
try {
|
|
163
|
+
if (!opts.fromFile && !opts.set.length) throw new Error('persona edit requires --set or --from-file');
|
|
164
|
+
const result = editPersona(name, editOptions(opts.fromFile, opts.set), { dryRun: opts.dryRun });
|
|
165
|
+
debug.log('persona.cli', opts.dryRun ? 'edit-dry-run' : 'edited', { personaId: result.personaId });
|
|
166
|
+
if (opts.json) console.log(JSON.stringify(result, null, 2));
|
|
167
|
+
else {
|
|
168
|
+
console.log(`${opts.dryRun ? '미리 보기' : '수정했습니다'} — ${result.displayName} (id ${result.personaId})`);
|
|
169
|
+
console.log(result.yaml);
|
|
170
|
+
console.log(` 파일: ${result.path}`);
|
|
171
|
+
}
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
92
178
|
persona.command('show <name>')
|
|
93
179
|
.description('Show one of your personas (id or name), or a preset')
|
|
94
180
|
.option('--json', 'JSON output')
|
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { OFFICIAL_INDEX_KEYS } from '../market/official-keys.js';
|
|
|
12
12
|
import { installPlugin, listInstalledPlugins, PluginInstallError, removePlugin, type InstallEvent } from '../plugins/install/plugin-install.js';
|
|
13
13
|
import { credentialStatus, setPluginCredentials } from '../plugins/install/plugin-credentials.js';
|
|
14
14
|
import { makePlugin } from '../plugins/maker/plugin-maker.js';
|
|
15
|
+
import { registerStandaloneLogSink } from '../domains/standalone-log-sink.js';
|
|
15
16
|
import { addAndInstallNode } from '../plugins/maker/node-install.js';
|
|
16
17
|
|
|
17
18
|
function keys(): ReadonlyArray<{ keyId: string; publicKey: string }> {
|
|
@@ -56,34 +57,42 @@ function safeCause(error: unknown): string {
|
|
|
56
57
|
return redactSecretText(error.message.replace(/https?:\/\/[^\s"'<>]+/gi, '[redacted URL]'));
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
export async function runPluginMake(request: string, opts: { name?: string; dir?: string; draftFile?: string; run?: boolean; marketBundleDir?: string; input?: string; json?: boolean } = {}): Promise<void> {
|
|
61
|
+
try {
|
|
62
|
+
await registerStandaloneLogSink('cli');
|
|
63
|
+
const result = await makePlugin({ request, name: opts.name, parentDir: opts.dir, draftFile: opts.draftFile, run: opts.run, marketBundleDir: opts.marketBundleDir,
|
|
64
|
+
...(opts.input === undefined ? {} : { input: JSON.parse(opts.input) as unknown }) });
|
|
65
|
+
debug.flush();
|
|
66
|
+
if (opts.json) stdout.write(JSON.stringify(result) + '\n');
|
|
67
|
+
else {
|
|
68
|
+
console.log(`뼈대 ${result.timings.scaffold}ms · 작성 ${result.timings.write}ms`);
|
|
69
|
+
console.log(`검증 오류 ${result.errors.length} · ${result.timings.validate}ms${result.timings.repair === undefined ? '' : ` · 수리 ${result.timings.repair}ms`}`);
|
|
70
|
+
console.log(`설치 ${result.status === 'draft' || result.timings.install === 0 ? '미실행' : result.status === 'failed' && result.timings.run === undefined ? '실패' : '완료'} · ${result.timings.install}ms (${result.dir})`);
|
|
71
|
+
if (result.status === 'draft') console.log('연구 초안만 생성됨 — 요청 처리 단계 구현 후 별도로 검증·설치 필요');
|
|
72
|
+
if (result.marketBundle) console.log(`마켓 꾸러미 ${result.marketBundle.path} · ${result.marketBundle.signature} · 게시 안 함`);
|
|
73
|
+
if (opts.run) console.log(`실행 ${result.runStatus ?? '미실행'} · ${result.timings.run ?? 0}ms`);
|
|
74
|
+
for (const error of result.errors) console.error(error);
|
|
75
|
+
}
|
|
76
|
+
if (result.status === 'failed') process.exitCode = 1;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
if (opts.json) stdout.write(JSON.stringify({ status: 'failed', errors: [message] }) + '\n');
|
|
80
|
+
else console.error(`plugin make failed: ${message}`);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
export function registerPluginCommands(program: Command): void {
|
|
60
86
|
const plugin = program.command('plugin').description('Install and manage Elanous plugins');
|
|
61
|
-
plugin.command('make <request>').description('Write, validate and install a local graph plugin
|
|
87
|
+
plugin.command('make <request>').description('Write, validate and install a local graph plugin, or generate an uninstalled research draft')
|
|
62
88
|
.option('--name <slug>', 'Local plugin name')
|
|
63
89
|
.option('--dir <parent>', 'Parent directory (default: instance plugins-local)')
|
|
64
|
-
.option('--
|
|
90
|
+
.option('--draft-file <path>', 'Generate an uninstalled research draft with connector and skill definitions')
|
|
91
|
+
.option('--run', 'Run the installed graph (not available for research drafts)')
|
|
92
|
+
.option('--market-bundle-dir <path>', 'Bundle installed plugin for human review; never publish')
|
|
65
93
|
.option('--input <json>', 'JSON input for --run')
|
|
66
94
|
.option('--json', 'Print the result as JSON')
|
|
67
|
-
.action(
|
|
68
|
-
try {
|
|
69
|
-
const result = await makePlugin({ request, name: opts.name, parentDir: opts.dir, run: opts.run,
|
|
70
|
-
...(opts.input === undefined ? {} : { input: JSON.parse(opts.input) as unknown }) });
|
|
71
|
-
if (opts.json) stdout.write(JSON.stringify(result) + '\n');
|
|
72
|
-
else {
|
|
73
|
-
console.log(`뼈대 ${result.timings.scaffold}ms · 작성 ${result.timings.write}ms`);
|
|
74
|
-
console.log(`검증 오류 ${result.errors.length} · ${result.timings.validate}ms${result.timings.repair === undefined ? '' : ` · 수리 ${result.timings.repair}ms`}`);
|
|
75
|
-
console.log(`설치 ${result.timings.install === 0 && result.status === 'failed' ? '미실행' : result.status === 'failed' && result.timings.run === undefined ? '실패' : '완료'} · ${result.timings.install}ms (${result.dir})`);
|
|
76
|
-
if (opts.run) console.log(`실행 ${result.runStatus ?? '미실행'} · ${result.timings.run ?? 0}ms`);
|
|
77
|
-
for (const error of result.errors) console.error(error);
|
|
78
|
-
}
|
|
79
|
-
if (result.status === 'failed') process.exitCode = 1;
|
|
80
|
-
} catch (error) {
|
|
81
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
82
|
-
if (opts.json) stdout.write(JSON.stringify({ status: 'failed', errors: [message] }) + '\n');
|
|
83
|
-
else console.error(`plugin make failed: ${message}`);
|
|
84
|
-
process.exitCode = 1;
|
|
85
|
-
}
|
|
86
|
-
});
|
|
95
|
+
.action(runPluginMake);
|
|
87
96
|
plugin.command('node').description('Manage plugin workflow nodes')
|
|
88
97
|
.command('add <dir> <request>').description('Write, validate and install a workflow node in a plugin')
|
|
89
98
|
.option('--kind <slug>', 'Workflow node kind')
|
package/src/cli/pod-cli.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { debug } from '../debug/log.js';
|
|
|
6
6
|
import { getUserConfig } from '../user-config.js';
|
|
7
7
|
import { listCodexAccountsInStore } from '../oauth/codex-account-store.js';
|
|
8
8
|
import { parsePodPool, resolvePodPoolSpec, type PoolKubectl } from '../task-orchestrator/surfaces/pod-pool.js';
|
|
9
|
-
import { leaseKubectl, measurePoolLease, recommendConcurrency, type PoolLeaseMeasure } from '../task-orchestrator/surfaces/pod-lease.js';
|
|
9
|
+
import { leaseKubectl, measurePoolLease, probePoolDns, recommendConcurrency, type PoolLeaseMeasure, type PoolDnsProbe } from '../task-orchestrator/surfaces/pod-lease.js';
|
|
10
10
|
import { POD_COMMAND_DEADLINE_SECONDS, runPodCommand, type PodCommandResult, type RunPodCommandOptions } from '../task-orchestrator/surfaces/pod-command-job.js';
|
|
11
11
|
|
|
12
12
|
export interface PodCliIo {
|
|
@@ -19,9 +19,11 @@ export interface PodCliDeps {
|
|
|
19
19
|
io?: PodCliIo;
|
|
20
20
|
run?: (options: RunPodCommandOptions) => Promise<PodCommandResult>;
|
|
21
21
|
kubectl?: PoolKubectl;
|
|
22
|
+
dns?: (context: string, kubectl: PoolKubectl) => PoolDnsProbe;
|
|
22
23
|
accounts?: () => number;
|
|
23
24
|
perAccount?: () => number;
|
|
24
25
|
poolSpec?: (explicit?: string) => string | null;
|
|
26
|
+
harnessPool?: () => string | undefined;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const HELP = `elanous pod run [--pool <스펙>] [--skill <이름,…>] [--llm grok] [--clone] [--deadline <초>] -- <명령…>
|
|
@@ -36,19 +38,22 @@ export function registerPodCommands(program: Command, deps: PodCliDeps = {}): vo
|
|
|
36
38
|
};
|
|
37
39
|
const run = deps.run ?? ((options: RunPodCommandOptions) => runPodCommand(options));
|
|
38
40
|
const pod = program.command('pod').description('Pod 명령 Job — 하니스 없이 명령 하나를 돌리고 산출을 돌려받는다');
|
|
39
|
-
pod.command('lease').description('Pod
|
|
41
|
+
pod.command('lease').description('Pod 풀 동시 실행 권장 (DNS 점검용 Pod 를 잠깐 만들었다 지움)')
|
|
40
42
|
.command('status').description('지금 추가로 쏠 수 있는 Pod 골과 제한 근거')
|
|
41
|
-
.option('--pool <spec>', '풀 스펙 — 컨텍스트[@ssh호스트][:상한] 쉼표')
|
|
43
|
+
.option('--pool <spec>', '풀 스펙 — 컨텍스트[@ssh호스트][:상한][#레지스트리:포트] 쉼표')
|
|
42
44
|
.option('--json', '표 대신 동일 측정값을 JSON 으로')
|
|
43
45
|
.action((opts: { pool?: string; json?: boolean }) => {
|
|
44
46
|
try {
|
|
45
47
|
const kubectl = deps.kubectl ?? leaseKubectl;
|
|
46
|
-
const spec = (deps.poolSpec ?? ((explicit?: string) => resolvePodPoolSpec(explicit
|
|
48
|
+
const spec = (deps.poolSpec ?? ((explicit?: string) => resolvePodPoolSpec(explicit, process.env, () => {
|
|
49
|
+
const config = deps.harnessPool ? null : getUserConfig();
|
|
50
|
+
return (deps.harnessPool?.() ?? config?.harness?.podPool) || config?.pod?.pool;
|
|
51
|
+
})))(opts.pool);
|
|
47
52
|
// No pool configured: mirror the Pod launch fallback to kubectl's current context.
|
|
48
53
|
const current = spec ? null : kubectl(['config', 'current-context']);
|
|
49
54
|
const context = spec ?? (current?.status === 0 ? current.stdout.trim() : '');
|
|
50
55
|
const members = context ? parsePodPool(context) : [];
|
|
51
|
-
const measure: PoolLeaseMeasure = members.length ? measurePoolLease(members, { kubectl }) : { members: [] };
|
|
56
|
+
const measure: PoolLeaseMeasure = members.length ? measurePoolLease(members, { kubectl, dns: (context) => (deps.dns ?? probePoolDns)(context, kubectl) }) : { members: [] };
|
|
52
57
|
let accounts: number | null = null;
|
|
53
58
|
let accountReason: string | null = null;
|
|
54
59
|
try { accounts = (deps.accounts ?? (() => listCodexAccountsInStore().length))(); }
|
|
@@ -57,17 +62,20 @@ export function registerPodCommands(program: Command, deps: PodCliDeps = {}): vo
|
|
|
57
62
|
const decision = recommendConcurrency(measure, { capacity: members.reduce((sum, m) => sum + m.capacity, 0), accounts: accounts ?? -1, perAccount });
|
|
58
63
|
if (!context) { decision.recommended = null; decision.limitedBy = null; decision.reason = '측정 불가: cluster (current context)'; }
|
|
59
64
|
const result = { pool: spec, ...measure, accounts, perAccount, ...decision };
|
|
60
|
-
debug.log('pod.lease', 'status', { pool: spec ? members.map((m) => ({ capacity: m.capacity })) : null, running: decision.running, pending: decision.pending, recommended: decision.recommended, limitedBy: decision.limitedBy });
|
|
65
|
+
debug.log('pod.lease', 'status', { pool: spec ? members.map((m) => ({ capacity: m.capacity })) : null, running: decision.running, pending: decision.pending, unleasedRunning: decision.unleasedRunning, recommended: decision.recommended, limitedBy: decision.limitedBy });
|
|
61
66
|
if (opts.json) io.log(JSON.stringify(result));
|
|
62
67
|
else {
|
|
68
|
+
io.log(`풀: ${(spec ?? context) || '?'}`);
|
|
63
69
|
io.log('컨텍스트 | 상한 | Running | Pending | 메모리(상한 합/할당 가능) | CPU(할당 가능)');
|
|
64
70
|
for (const m of measure.members) {
|
|
65
71
|
const mem = (v: number | null) => v === null ? '?' : `${(v / 1024 ** 3).toFixed(1)}Gi`;
|
|
66
72
|
io.log(`${m.context} | ${m.capacity} | ${m.running ?? '?'} | ${m.pending ?? '?'} | ${mem(m.memoryLimitBytes)}/${mem(m.allocatableMemoryBytes)} | ${m.allocatableCpuMillicores === null ? '?' : m.allocatableCpuMillicores / 1000}${m.reason ? ` (${m.reason})` : ''}`);
|
|
67
73
|
}
|
|
68
74
|
io.log(`권장 지금 ${decision.recommended ?? '?'} 개 더 (limitedBy=${decision.limitedBy ?? 'unknown'})${decision.reason ? ` · ${decision.reason}` : ''}`);
|
|
69
|
-
io.log(
|
|
75
|
+
io.log(`임대 없는 실행 ${decision.unleasedRunning ?? '?'}`);
|
|
76
|
+
io.log(`capacity: ${decision.capacitySlots ?? '?'} 칸 (상한 ${members.reduce((sum, m) => sum + m.capacity, 0)} − Running ${decision.running ?? '?'} − Pending ${decision.pending ?? '?'}; 권장 수에서 건강한 멤버의 임대 없는 실행 차감)`);
|
|
70
77
|
io.log(`memory: ${decision.memorySlots ?? '?'} 칸 (할당 가능 − Running 하니스 상한 합; 골당 16Gi · 모든 네임스페이스의 노드별 기존 예약을 반영해 배치 가능 ${decision.placeableSlots ?? '?'} 칸)`);
|
|
78
|
+
io.log(`lite: ${decision.liteMemorySlots ?? '?'} 칸 (같은 여유를 lite 골 2Gi 로 센 값 · 관측만 — 권장 수는 위 standard 기준)`);
|
|
71
79
|
io.log(`accounts (관측만 · 상한 아님): ${decision.accountSlots ?? '?'} 칸 (${accounts ?? '?'} × ${perAccount} − Running ${decision.running ?? '?'})${accountReason ? ` · ${accountReason}` : ''}`);
|
|
72
80
|
}
|
|
73
81
|
io.exit(0);
|
|
@@ -78,14 +86,15 @@ export function registerPodCommands(program: Command, deps: PodCliDeps = {}): vo
|
|
|
78
86
|
});
|
|
79
87
|
pod.command('run')
|
|
80
88
|
.description(HELP)
|
|
81
|
-
.option('--pool <spec>', '풀 스펙 — 컨텍스트[@ssh호스트][:상한] 쉼표')
|
|
89
|
+
.option('--pool <spec>', '풀 스펙 — 컨텍스트[@ssh호스트][:상한][#레지스트리:포트] 쉼표')
|
|
82
90
|
.option('--skill <names>', '스킬 이름(쉼표). 그 스킬 .env 만 Secret 으로 싣는다')
|
|
83
91
|
.option('--llm <provider>', 'grok 만 허용 — access 사본(refresh 없음)')
|
|
84
92
|
.option('--clone', '저장소를 clone 한다(비공개 · GitHub 토큰이 런 Secret 으로 간다 · 명시할 때만). 기본은 clone 없이 ~/work 에서 이미지의 elanous 로')
|
|
85
93
|
.option('--deadline <seconds>', `Job 수명 상한(초). 생략 시 ${POD_COMMAND_DEADLINE_SECONDS}`)
|
|
86
94
|
.allowUnknownOption(true)
|
|
87
95
|
.allowExcessArguments(true)
|
|
88
|
-
.
|
|
96
|
+
.option('--lite', '네트워크 스킬(omni-crawl·omni-digest)만 쓰는 명령 — 작은 이미지 ⊕ 2Gi 한도(POD7 · clone 불가)')
|
|
97
|
+
.action(async (opts: { pool?: string; skill?: string; llm?: string; clone?: boolean; deadline?: string; lite?: boolean }, command: Command) => {
|
|
89
98
|
const argv = process.argv.slice(2);
|
|
90
99
|
const sep = argv.indexOf('--');
|
|
91
100
|
const fromArgv = sep >= 0 ? argv.slice(sep + 1) : [];
|
|
@@ -117,6 +126,7 @@ export function registerPodCommands(program: Command, deps: PodCliDeps = {}): vo
|
|
|
117
126
|
...(skills.length ? { skills } : {}),
|
|
118
127
|
...(opts.llm === 'grok' ? { llm: 'grok' as const } : {}),
|
|
119
128
|
...(opts.clone ? { clone: true } : {}),
|
|
129
|
+
...(opts.lite ? { lite: true } : {}),
|
|
120
130
|
...(deadlineSeconds !== undefined ? { deadlineSeconds } : {}),
|
|
121
131
|
});
|
|
122
132
|
if (result.image) io.error(`[pod] image ${result.image} · job ${result.job}`);
|