elanous 0.2.11 → 0.2.13
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/chunks/{1394-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-9da4678f8a310d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-e49b9b76c565a0b1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-a0ea8f5a2341ee6b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6ddd2109b7fc3a80.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-f672536f14f0969f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/context/nightly-condense.yaml +21 -0
- package/graphs/context/recipes.yaml +12 -0
- package/graphs/demo/inside-seed.yaml +20 -0
- package/graphs/demo/recipes.yaml +12 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/plugins/card-followup/examples/sample-card.png +0 -0
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +219 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/lib/telegram-inject-args.ts +31 -0
- package/scripts/measure-pod-memory-by-goal.ts +130 -0
- package/scripts/release-loop/auto-approve-node.ts +1 -1
- package/scripts/release-loop/checklist-gate-node.ts +2 -1
- package/scripts/release-loop/gate-node.ts +61 -17
- package/scripts/release-loop/ops-upgrade-node.ts +59 -6
- package/scripts/release-loop/publish-node.ts +4 -1
- package/scripts/release-loop/version-node.ts +13 -3
- package/scripts/telegram-inject.ts +14 -6
- package/scripts/test-deterministic.ts +53 -3
- package/scripts/test-diet/remeasure-list.ts +112 -0
- package/scripts/ux/demo-rehearsal.ts +17 -13
- package/scripts/ux/demo-seed.ts +108 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +150 -23
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +2 -2
- package/src/autopilot/pty-control-loop.ts +4 -1
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/chat/index.ts +1 -0
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +45 -13
- package/src/cli/pod-cli.ts +27 -3
- package/src/cli/pod-memory-advice.ts +35 -0
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +135 -15
- package/src/context-bus/condense.ts +194 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-surfaces.ts +32 -2
- package/src/context-bus/context-now.ts +2 -2
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +153 -0
- package/src/context-bus/session-events.ts +35 -0
- package/src/dashboard/dashboard-status-lines.ts +2 -0
- package/src/dashboard/index.ts +8 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/domains/outbound-alert.ts +99 -31
- package/src/domains/outbound-log-probe.ts +8 -0
- package/src/execution-loop/launch-gate.ts +17 -8
- package/src/field-note/telegram.ts +142 -0
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow-llm.ts +23 -13
- package/src/graph-runner/graph-grow.ts +69 -13
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +92 -24
- package/src/harness/harness-cli-command.ts +165 -17
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/consult-intake.ts +153 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +82 -3
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/index.ts +232 -16
- package/src/intake-plane/discord-seat-work.ts +36 -3
- package/src/intake-plane/linear-wish.ts +44 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-card.ts +33 -0
- package/src/llm/difficulty.ts +1 -1
- package/src/loops/checker.ts +178 -0
- package/src/loops/loop-cli.ts +37 -3
- package/src/loops/observe.ts +49 -0
- package/src/loops/orchestrator/overlap.ts +141 -0
- package/src/loops/orchestrator/traffic.ts +150 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/verdict.ts +22 -0
- package/src/maturity/feature-maturity.ts +5 -4
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +78 -8
- package/src/nexus/api/outbound-report.ts +40 -7
- package/src/nexus/api/personas.ts +97 -54
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-history.ts +88 -0
- package/src/nexus/api/workflows.ts +52 -0
- package/src/nexus/outbound/delivery-ledger.ts +17 -8
- package/src/nexus/outbound/router.ts +8 -3
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +10 -2
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/plugins/maker/market-bundle.ts +2 -1
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/checklist.ts +15 -3
- package/src/release-loop/feature-store.ts +24 -10
- package/src/release-loop/landed-but-yellow.ts +60 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +253 -18
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/draft-sweep.ts +44 -10
- package/src/self-dev/draft-triage-rules.ts +54 -2
- package/src/self-implement/budget-gate.ts +29 -12
- package/src/self-implement/gate-candidate-ranking.ts +60 -0
- package/src/self-implement/gate-scope.ts +9 -8
- package/src/self-implement/headless-elanous-driver.ts +7 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +15 -0
- package/src/self-implement/test-seams.ts +1 -0
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +78 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
- package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +46 -8
- package/src/telegram-commands.ts +21 -0
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +84 -4
- package/src/telegram.ts +37 -5
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +101 -13
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
- package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
- package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
* X 는 media.variants 로 MP4 직링크를 주므로 yt-dlp 없이 curl 로 바로 받는다.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { execFile as execFileCb
|
|
11
|
+
import { execFile as execFileCb } from 'node:child_process';
|
|
12
12
|
import { mkdir, readdir, readFile, rm } from 'node:fs/promises';
|
|
13
|
-
import { existsSync } from 'node:fs';
|
|
13
|
+
import { accessSync, constants, existsSync } from 'node:fs';
|
|
14
14
|
import { tmpdir } from 'node:os';
|
|
15
|
-
import { join } from 'node:path';
|
|
15
|
+
import { join, delimiter } from 'node:path';
|
|
16
16
|
import { promisify } from 'node:util';
|
|
17
17
|
import { env } from './env.js';
|
|
18
18
|
import type { TweetMedia } from './types.js';
|
|
@@ -25,10 +25,22 @@ const MAX_DURATION_MS = Number(env('OMNI_DIGEST_MEDIA_MAX_MS', String(20 * 60 *
|
|
|
25
25
|
const CHUNK_SECONDS = 600;
|
|
26
26
|
|
|
27
27
|
function has(bin: string): boolean {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
for (const dir of (process.env.PATH ?? '').split(delimiter).filter(Boolean)) {
|
|
29
|
+
try { accessSync(join(dir, bin), constants.X_OK); return true; } catch { /* try next directory */ }
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const missingEngines = new Set<string>();
|
|
35
|
+
|
|
36
|
+
export function mediaEngineAvailable(engine: 'ffmpeg' | 'whisper'): boolean {
|
|
37
|
+
if (has(engine)) return true;
|
|
38
|
+
if (!missingEngines.has(engine)) {
|
|
39
|
+
missingEngines.add(engine);
|
|
40
|
+
if (engine === 'ffmpeg') console.error('ffmpeg 없음 — 영상 오디오 추출·전사 빠짐 · 설치하면 영상 전사 켜짐');
|
|
41
|
+
else console.error('whisper 없음 — 로컬 음성 전사 빠짐 · 설치하면 오프라인 전사 켜짐 (youtube-master 폴백은 계속)');
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
async function run(bin: string, args: string[]): Promise<string> {
|
|
@@ -47,7 +59,7 @@ export async function downloadVideo(url: string, outdir: string): Promise<string
|
|
|
47
59
|
|
|
48
60
|
/** 영상에서 16kHz 모노 mp3 를 추출한다 (STT 엔진 공통 입력 규격). */
|
|
49
61
|
export async function extractAudio(videoPath: string, outdir: string): Promise<string> {
|
|
50
|
-
if (!
|
|
62
|
+
if (!mediaEngineAvailable('ffmpeg')) throw new Error('ffmpeg 가 필요합니다 (brew install ffmpeg)');
|
|
51
63
|
const out = join(outdir, 'audio.mp3');
|
|
52
64
|
await run('ffmpeg', [
|
|
53
65
|
'-hide_banner', '-loglevel', 'error', '-y',
|
|
@@ -61,7 +73,7 @@ export async function extractAudio(videoPath: string, outdir: string): Promise<s
|
|
|
61
73
|
|
|
62
74
|
/** 로컬 whisper CLI 전사. 미설치면 null 을 반환해 폴백을 유도한다. */
|
|
63
75
|
async function transcribeLocalWhisper(audioPath: string, outdir: string, lang: string): Promise<string | null> {
|
|
64
|
-
if (!
|
|
76
|
+
if (!mediaEngineAvailable('whisper')) return null;
|
|
65
77
|
const model = env('OMNI_DIGEST_WHISPER_MODEL', 'small');
|
|
66
78
|
try {
|
|
67
79
|
await run('whisper', [
|
|
@@ -81,11 +93,12 @@ async function transcribeLocalWhisper(audioPath: string, outdir: string, lang: s
|
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
/** youtube-master 의 STT 체인(ElevenLabs → OpenAI → Gemini)으로 폴백. */
|
|
84
|
-
async function transcribeViaYoutubeMaster(audioPath: string, outdir: string): Promise<string | null> {
|
|
96
|
+
export async function transcribeViaYoutubeMaster(audioPath: string, outdir: string): Promise<string | null> {
|
|
85
97
|
const skillRoot = env('YOUTUBE_MASTER_ROOT', join(process.env.HOME || '', '.claude/skills/youtube-master'));
|
|
86
98
|
const script = join(skillRoot, 'scripts/transcribe-local.ts');
|
|
87
99
|
if (!existsSync(script)) return null;
|
|
88
100
|
|
|
101
|
+
if (!mediaEngineAvailable('ffmpeg')) return null;
|
|
89
102
|
// transcribe-local.ts 는 chunk_%03d.mp3 가 든 디렉터리를 입력으로 받는다
|
|
90
103
|
const chunksDir = join(outdir, 'chunks');
|
|
91
104
|
await mkdir(chunksDir, { recursive: true });
|
|
@@ -98,7 +111,7 @@ async function transcribeViaYoutubeMaster(audioPath: string, outdir: string): Pr
|
|
|
98
111
|
|
|
99
112
|
const outFile = join(outdir, 'transcript.txt');
|
|
100
113
|
try {
|
|
101
|
-
await run(
|
|
114
|
+
await run(process.execPath, [script, chunksDir, outFile]);
|
|
102
115
|
const text = (await readFile(outFile, 'utf-8')).trim();
|
|
103
116
|
return text || null;
|
|
104
117
|
} catch {
|
|
@@ -137,6 +150,7 @@ export async function transcribeTweetMedia(
|
|
|
137
150
|
console.log(` 영상 ${Math.round(video.durationMs / 1000)}s — 상한(${Math.round(MAX_DURATION_MS / 1000)}s) 초과로 전사 생략`);
|
|
138
151
|
return null;
|
|
139
152
|
}
|
|
153
|
+
if (!mediaEngineAvailable('ffmpeg')) return null;
|
|
140
154
|
|
|
141
155
|
const workdir = join(tmpdir(), `omni-digest-media-${video.mediaKey}`);
|
|
142
156
|
try {
|
|
@@ -13,7 +13,7 @@ import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'n
|
|
|
13
13
|
import { tmpdir } from 'node:os';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
15
|
import { env, requireEnv } from './env.js';
|
|
16
|
-
import { downloadVideo } from './media.js';
|
|
16
|
+
import { downloadVideo, mediaEngineAvailable } from './media.js';
|
|
17
17
|
import type { TweetMedia } from './types.js';
|
|
18
18
|
|
|
19
19
|
const MAX_FRAMES = 8;
|
|
@@ -101,7 +101,8 @@ export async function absorbTweetVisuals(media: TweetMedia[], opts: { tweetId: s
|
|
|
101
101
|
} catch (e) { log(` [시각] 이미지 ${i + 1} 실패: ${String(e).slice(0, 120)}`); }
|
|
102
102
|
}
|
|
103
103
|
const videos = media.filter((m) => (m.type === 'video' || m.type === 'animated_gif') && (m.bestMp4Url || m.sttMp4Url));
|
|
104
|
-
|
|
104
|
+
const canExtractFrames = !videos.length || mediaEngineAvailable('ffmpeg');
|
|
105
|
+
for (const [i, v] of (canExtractFrames ? videos : []).entries()) {
|
|
105
106
|
if (v.durationMs && v.durationMs > MAX_VIDEO_MS_FOR_FRAMES) { log(` [시각] 영상 ${i + 1} 길어서 프레임 생략`); continue; }
|
|
106
107
|
try {
|
|
107
108
|
const dir = join(work, `v${i + 1}`);
|
|
@@ -5,7 +5,7 @@ import { resolveDateRange } from '../src/date.js';
|
|
|
5
5
|
import { route, listProviders } from '../src/router.js';
|
|
6
6
|
import { render } from '../src/render.js';
|
|
7
7
|
import type { Command } from '../src/types.js';
|
|
8
|
-
import { writeStdoutJson } from '
|
|
8
|
+
import { writeStdoutJson } from './stdout-json.js';
|
|
9
9
|
|
|
10
10
|
initEnv();
|
|
11
11
|
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Write JSON output completely before the process can exit. (Kept inside the skill so the skill runs outside this repository — public pack and lite Pod.) */
|
|
2
|
+
export async function writeStdoutJson(text: string): Promise<void> {
|
|
3
|
+
await new Promise<void>((resolve, reject) => process.stdout.write(text, error => error ? reject(error) : resolve()));
|
|
4
|
+
}
|
|
@@ -21,6 +21,17 @@
|
|
|
21
21
|
import { debug } from '../debug/log.js';
|
|
22
22
|
import { getCapturedEnv, capturedEnvAvailable } from '../shell-env-bootstrap.js';
|
|
23
23
|
import { resolveCurrentInstance } from '../instance/current.js';
|
|
24
|
+
import { treeDerivedRootFor } from '../instance/resolve.js';
|
|
25
|
+
|
|
26
|
+
/** Pin a child state root together with its origin; only reuse a parent's tag for the same root. */
|
|
27
|
+
export function stateDirSourceForChild(root: string, env: NodeJS.ProcessEnv = process.env): string {
|
|
28
|
+
if (env.ELANOUS_STATE_DIR === root && env.ELANOUS_STATE_DIR_SOURCE?.trim()) {
|
|
29
|
+
return env.ELANOUS_STATE_DIR_SOURCE;
|
|
30
|
+
}
|
|
31
|
+
const current = resolveCurrentInstance();
|
|
32
|
+
return root === treeDerivedRootFor(process.cwd()) || (current.root === root && current.layer === 'default')
|
|
33
|
+
? 'derived' : 'explicit';
|
|
34
|
+
}
|
|
24
35
|
|
|
25
36
|
// ── 전파 allowlist ─────────────────────────────────────────────────────────
|
|
26
37
|
//
|
|
@@ -22,8 +22,10 @@ import { streamLLM, type LLMMessage } from '../llm.js';
|
|
|
22
22
|
import { debug } from '../debug/log.js';
|
|
23
23
|
import { emitPtyDecision, type DecisionInput } from './pty-decision.js';
|
|
24
24
|
import { observeExternalEvent } from '../context-bus/external-events.js';
|
|
25
|
+
import { emitSessionEvent, type SessionEventInput } from '../context-bus/session-events.js';
|
|
25
26
|
import { emitDecision } from '../live/detail-switch.js';
|
|
26
27
|
import { resolvePtyWebAddress } from '../cli/pty-web-address.js';
|
|
28
|
+
import { verdictForFinalPtyScreen } from '../cli/pty-drive-verdict.js';
|
|
27
29
|
import { reemitPtyUsage } from '../budget/pty-usage-reemit.js';
|
|
28
30
|
import { classifyAuthError } from '../oauth/codex.js';
|
|
29
31
|
import type { FallbackStep } from '../oauth/fallback-chain.js';
|
|
@@ -451,6 +453,8 @@ export interface AgentMissionSpec {
|
|
|
451
453
|
evidence: EvidenceMode;
|
|
452
454
|
/** RFC 최대 라운드(기본 16). */
|
|
453
455
|
maxRounds?: number;
|
|
456
|
+
/** 화면 무변화 회복 문턱(ms, 기본 45초). */
|
|
457
|
+
recoverAfterMs?: number;
|
|
454
458
|
/** 완료 시 자동 commit(기본 true). */
|
|
455
459
|
commit?: boolean;
|
|
456
460
|
/** 스크린 캡처 저장 디렉토리. */
|
|
@@ -487,6 +491,8 @@ export interface AgentMissionSpec {
|
|
|
487
491
|
memory?: boolean;
|
|
488
492
|
/** PTY 닉네임(휴먼 리더블·goto 로 나중 접근). 기본 branch. */
|
|
489
493
|
nickname?: string;
|
|
494
|
+
/** Concurrent missions may share one decision stream alias without sharing a PTY or worktree. */
|
|
495
|
+
decisionMissionId?: string;
|
|
490
496
|
/** ⭐⭐ 그 브랜치를 이미 쥔 «소유» 워크트리가 있으면 지우지 말고 그대로 재사용하라고 요청한다
|
|
491
497
|
* (`createWorktree` 의 같은 이름 인자로 그대로 내려간다 · 판정은 거기 `gateWorktreeReuse` 가 한다).
|
|
492
498
|
*
|
|
@@ -509,11 +515,14 @@ export interface AgentMissionResult {
|
|
|
509
515
|
paths?: string[];
|
|
510
516
|
rounds: number;
|
|
511
517
|
evidencePath: string | null;
|
|
518
|
+
/** The evidence check outcome, independent of the final mission and DRIVE-OK verdicts. */
|
|
519
|
+
evidenceSatisfied?: boolean;
|
|
512
520
|
committed: boolean;
|
|
513
521
|
usedOmniCrawl: boolean;
|
|
514
522
|
detail: string;
|
|
515
523
|
ptyId?: string;
|
|
516
524
|
webUrl?: string | null;
|
|
525
|
+
driveVerdict?: ReturnType<typeof verdictForFinalPtyScreen>['kind'];
|
|
517
526
|
}
|
|
518
527
|
/** @deprecated codex 특정 이름 — AgentMissionResult 을 쓰라. */
|
|
519
528
|
export type CodexMissionResult = AgentMissionResult;
|
|
@@ -538,6 +547,12 @@ export interface AgentMissionDeps {
|
|
|
538
547
|
checkEvidence?: typeof checkEvidence;
|
|
539
548
|
commitWorktree?: typeof commitWorktree;
|
|
540
549
|
dispatchOpenPullRequest?: typeof dispatchOpenPullRequest;
|
|
550
|
+
/** Optional context-journal sink for isolated missions. */
|
|
551
|
+
emitContextEvent?: (input: SessionEventInput) => void;
|
|
552
|
+
/** Internal continuation retains the original mission's context-event owner. */
|
|
553
|
+
contextEventRun?: boolean;
|
|
554
|
+
/** Carries a PR number back to the outer mission's context event. */
|
|
555
|
+
onMissionPrNumber?: (number: number) => void;
|
|
541
556
|
terminateProcessTree?: typeof terminateMissionProcessTree;
|
|
542
557
|
repoStatus?: (repo: string) => string | Promise<string>;
|
|
543
558
|
emitDecision?: typeof emitDecision;
|
|
@@ -835,18 +850,36 @@ export interface MissionControlBrainOpts {
|
|
|
835
850
|
/** Screen-grounded brain summary for the next backend, never the raw PTY screen. */
|
|
836
851
|
readonly onSummary?: (summary: string) => void;
|
|
837
852
|
readonly onDecision?: (text: string, reply?: { question: string; answer: string }) => void;
|
|
853
|
+
readonly recoverAfterMs?: number;
|
|
854
|
+
readonly initialCommand?: string;
|
|
855
|
+
readonly onRecover?: (blocked: string, action: string) => void;
|
|
838
856
|
}
|
|
839
857
|
|
|
840
858
|
/** 기존 5-action mission 판단을 canonical PTY control-loop 결정으로 축소한다. search는 이 경계에서만
|
|
841
859
|
* 부작용(context 기록)을 끝내고, child에는 context를 읽고 계속하라는 단일 input만 전달한다. */
|
|
842
|
-
export
|
|
860
|
+
export type MissionControlBrain = RunSupervisor & {
|
|
861
|
+
/** Report whether the last returned input was accepted by the PTY; recovery stages and events commit only on acceptance. */
|
|
862
|
+
readonly onInputResult: (text: string, accepted: boolean) => void;
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
export function createMissionControlBrain(opts: MissionControlBrainOpts): MissionControlBrain {
|
|
843
866
|
const history: string[] = [];
|
|
844
867
|
const backendName = (opts.backend ?? codexBackend).name;
|
|
845
868
|
// ★ P4 dedup(제한적 재시도·리뷰) — (layer:spec)별 시도횟수. 성공 시 영구 차단, 실패는 최대 2회까지
|
|
846
869
|
// 허용(일시 네트워크/프로세스 오류는 재시도, 지속 실패는 루프 차단).
|
|
847
870
|
const provisionAttempts = new Map<string, number>();
|
|
848
871
|
const PROVISION_MAX_ATTEMPTS = 2;
|
|
849
|
-
|
|
872
|
+
const recoverAfterMs = opts.recoverAfterMs ?? 45_000;
|
|
873
|
+
if (!Number.isFinite(recoverAfterMs) || recoverAfterMs <= 0) throw new Error('recoverAfterMs must be positive');
|
|
874
|
+
let lastScreen: string | undefined;
|
|
875
|
+
let repeatedQuestion = 0;
|
|
876
|
+
let lastQuestion: string | undefined;
|
|
877
|
+
let recoveryStage = 0;
|
|
878
|
+
let pendingReplayBlocked: string | undefined;
|
|
879
|
+
// The recovery input returned by the last decide(); committed (stage advance + event) only when the PTY accepts it.
|
|
880
|
+
let pendingRecovery: { blocked: string; action: string; text: string; replay?: boolean } | undefined;
|
|
881
|
+
let lastCommand = opts.initialCommand ?? opts.mission;
|
|
882
|
+
const llmBrain = createLlmControlBrain({
|
|
850
883
|
goal: opts.mission,
|
|
851
884
|
stream: opts.stream,
|
|
852
885
|
maxTokens: 500,
|
|
@@ -887,7 +920,10 @@ ${opts.handoffEnabled ? ` - "handoff": 다른 담당자로 넘길 때 to(codex/c
|
|
|
887
920
|
if (decision.reason && decision.action !== 'ask-human') opts.onSummary?.(decision.reason.slice(0, 2000));
|
|
888
921
|
if (decision.action === 'handoff' && decision.to && decision.mission) return { action: 'handoff', to: decision.to, mission: decision.mission, ...(decision.carry ? { carry: decision.carry } : {}) };
|
|
889
922
|
if (decision.action === 'ask-human') return { action: 'ask-human', reason: decision.reason, ...(decision.url ? { url: decision.url } : {}), ...(decision.code ? { code: decision.code } : {}) };
|
|
890
|
-
if (decision.action === 'send')
|
|
923
|
+
if (decision.action === 'send') {
|
|
924
|
+
lastCommand = decision.text || 'continue';
|
|
925
|
+
return { action: 'input', text: `${lastCommand}\r` };
|
|
926
|
+
}
|
|
891
927
|
if (decision.action === 'verify' || decision.action === 'done') return { action: 'done', reason: decision.reason || 'agent completed' };
|
|
892
928
|
if (decision.action === 'search') {
|
|
893
929
|
await opts.search(decision.query || opts.mission.slice(0, 120), obs.step);
|
|
@@ -922,6 +958,57 @@ ${opts.handoffEnabled ? ` - "handoff": 다른 담당자로 넘길 때 to(codex/c
|
|
|
922
958
|
return { action: 'wait' };
|
|
923
959
|
},
|
|
924
960
|
});
|
|
961
|
+
return {
|
|
962
|
+
onInputResult: (text, accepted) => {
|
|
963
|
+
const recovery = pendingRecovery;
|
|
964
|
+
// Only the input this brain returned settles its recovery step (a trust answer in between does not).
|
|
965
|
+
if (!recovery || recovery.text !== text) return;
|
|
966
|
+
pendingRecovery = undefined;
|
|
967
|
+
if (!accepted) return;
|
|
968
|
+
if (recovery.replay) { pendingReplayBlocked = undefined; opts.onRecover?.(recovery.blocked, recovery.action); return; }
|
|
969
|
+
recoveryStage += 1;
|
|
970
|
+
if (recovery.action === 'Ctrl-C and replay previous command') pendingReplayBlocked = recovery.blocked;
|
|
971
|
+
else opts.onRecover?.(recovery.blocked, recovery.action);
|
|
972
|
+
},
|
|
973
|
+
decide: async (obs, signal) => {
|
|
974
|
+
pendingRecovery = undefined;
|
|
975
|
+
if (signal?.aborted) return { action: 'wait' };
|
|
976
|
+
const lines = classifierFrameLines(obs.screen);
|
|
977
|
+
// Only the active bottom line can be a repeated prompt; a question left in
|
|
978
|
+
// scrollback while new work appears is not a request for input.
|
|
979
|
+
const tail = lines.at(-1);
|
|
980
|
+
const question = tail && /\?(?:\s*\[[^\]\r\n]+\])?\s*$/.test(tail) ? tail : undefined;
|
|
981
|
+
const screenChanged = lastScreen !== undefined && obs.screen !== lastScreen;
|
|
982
|
+
if (screenChanged) recoveryStage = 0;
|
|
983
|
+
repeatedQuestion = question ? (!screenChanged && question === lastQuestion ? repeatedQuestion + 1 : 1) : 0;
|
|
984
|
+
lastQuestion = question;
|
|
985
|
+
lastScreen = obs.screen;
|
|
986
|
+
if (pendingReplayBlocked !== undefined) {
|
|
987
|
+
// Ctrl-C consumes its own step; replay on the next observation even when the
|
|
988
|
+
// interrupted child keeps redrawing a non-prompt screen. A rejected replay is retried.
|
|
989
|
+
const text = `${lastCommand}\r`;
|
|
990
|
+
pendingRecovery = { blocked: pendingReplayBlocked, action: 'Ctrl-C and replay previous command', text, replay: true };
|
|
991
|
+
return { action: 'input', text };
|
|
992
|
+
}
|
|
993
|
+
const blocked = repeatedQuestion >= 3 ? `same question repeated three times: ${question}`
|
|
994
|
+
: (obs.sameScreenMs ?? 0) >= recoverAfterMs ? `screen unchanged for ${obs.sameScreenMs}ms` : undefined;
|
|
995
|
+
if (blocked && opts.evidenceReady()) return { action: 'done', reason: 'Evidence ready on stalled screen' };
|
|
996
|
+
if (blocked && !signal?.aborted) {
|
|
997
|
+
const action = ['Enter', 'Esc', 'Ctrl-C and replay previous command', 'Human needed'] as const;
|
|
998
|
+
const next = action[Math.min(recoveryStage, 3)]!;
|
|
999
|
+
if (next !== 'Human needed') {
|
|
1000
|
+
const text = next === 'Enter' ? '\r' : next === 'Esc' ? '\x1b' : '\x03';
|
|
1001
|
+
pendingRecovery = { blocked, action: next, text };
|
|
1002
|
+
return { action: 'input', text };
|
|
1003
|
+
}
|
|
1004
|
+
recoveryStage += 1;
|
|
1005
|
+
opts.onRecover?.(blocked, next);
|
|
1006
|
+
const summary = lines.slice(-3).join(' | ').slice(0, 240) || '(empty screen)';
|
|
1007
|
+
return { action: 'no-progress', terminal: true, reason: `사람 필요 · 마지막 화면 요약: ${summary}` };
|
|
1008
|
+
}
|
|
1009
|
+
return llmBrain.decide(obs, signal);
|
|
1010
|
+
},
|
|
1011
|
+
};
|
|
925
1012
|
}
|
|
926
1013
|
|
|
927
1014
|
/**
|
|
@@ -1273,6 +1360,21 @@ export async function runAgentMission(spec: AgentMissionSpec, deps: AgentMission
|
|
|
1273
1360
|
} finally { signal?.removeEventListener('abort', stopWaiting); }
|
|
1274
1361
|
}
|
|
1275
1362
|
const startedAt = Date.now();
|
|
1363
|
+
const contextOwner = !deps.contextEventRun;
|
|
1364
|
+
const contextRunId = contextOwner ? ensureRunId() : undefined;
|
|
1365
|
+
let contextPrNumber: number | undefined;
|
|
1366
|
+
const emitContext = (kind: 'task-claimed' | 'task-done', detail: string): void => {
|
|
1367
|
+
if (!contextOwner) return;
|
|
1368
|
+
try {
|
|
1369
|
+
(deps.emitContextEvent ?? emitSessionEvent)({ kind, seat: 'TC', text: `Agent mission ${detail}: ${spec.mission.split(/\r?\n/, 1)[0]}`,
|
|
1370
|
+
ref: `runId=${contextRunId}${contextPrNumber === undefined ? '' : ` PR=#${contextPrNumber}`}` });
|
|
1371
|
+
} catch (error) {
|
|
1372
|
+
try { debug.log('agent-mission', 'context-event-failed', { kind, runId: contextRunId, error: String(error) }, { level: 'warn' }); } catch { /* fail-soft */ }
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
emitContext('task-claimed', 'claimed');
|
|
1376
|
+
let result: AgentMissionResult | undefined;
|
|
1377
|
+
try {
|
|
1276
1378
|
const initialFiles = workdir && spec.evidence.kind === 'doc' ? initialEvidenceFiles(workdir, spec.evidence) : undefined;
|
|
1277
1379
|
let handle: PtyHandle | undefined;
|
|
1278
1380
|
let childPid: number | undefined;
|
|
@@ -1306,10 +1408,12 @@ export async function runAgentMission(spec: AgentMissionSpec, deps: AgentMission
|
|
|
1306
1408
|
});
|
|
1307
1409
|
signal?.addEventListener('abort', abort, { once: true });
|
|
1308
1410
|
if (signal?.aborted) abort();
|
|
1309
|
-
let result: AgentMissionResult;
|
|
1310
1411
|
try {
|
|
1311
1412
|
if (cancelled) await aborted;
|
|
1312
|
-
const body = runAgentMissionBody(spec, { ...deps,
|
|
1413
|
+
const body = runAgentMissionBody(spec, { ...deps, contextEventRun: true, onMissionPrNumber: (number) => {
|
|
1414
|
+
contextPrNumber = number;
|
|
1415
|
+
try { deps.onMissionPrNumber?.(number); } catch { /* context propagation never changes a mission result */ }
|
|
1416
|
+
}, onControlActive: (pending) => {
|
|
1313
1417
|
activeSettled = false;
|
|
1314
1418
|
activeControl = pending.then(() => { activeSettled = true; }, () => { activeSettled = true; });
|
|
1315
1419
|
}, onControlPending: (pending) => {
|
|
@@ -1377,10 +1481,12 @@ export async function runAgentMission(spec: AgentMissionSpec, deps: AgentMission
|
|
|
1377
1481
|
if (paths.length) {
|
|
1378
1482
|
debug.log('agent-mission', 'outside-write', { workdir, pids: [], reason: paths.join(', ') });
|
|
1379
1483
|
(deps.emitDecision ?? emitDecision)({ kind: 'ESCALATE', what: '작업 폴더 밖 쓰기', reason: paths.join(', '), purpose: '사람이 확인한다', target: 'human' });
|
|
1380
|
-
|
|
1484
|
+
result = { ...result, ok: false, reason: 'outside-write', paths, committed: false };
|
|
1485
|
+
return result;
|
|
1381
1486
|
}
|
|
1382
1487
|
}
|
|
1383
1488
|
return result;
|
|
1489
|
+
} finally { emitContext('task-done', result?.ok ? 'completed' : 'ended'); }
|
|
1384
1490
|
}
|
|
1385
1491
|
|
|
1386
1492
|
async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDeps, startedAt: number, initialFiles?: ReadonlyMap<string, string>): Promise<AgentMissionResult> {
|
|
@@ -1508,7 +1614,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1508
1614
|
const finalEv = verifyEvidence(wt.path, spec.evidence);
|
|
1509
1615
|
if (!finalEv.ok) return {
|
|
1510
1616
|
ok: false, worktree: wt.path, branch: wt.branch, rounds: result.numTurns ?? 0,
|
|
1511
|
-
evidencePath: null, committed: false, usedOmniCrawl: false,
|
|
1617
|
+
evidencePath: null, evidenceSatisfied: false, committed: false, usedOmniCrawl: false,
|
|
1512
1618
|
detail: `미완(증거 부족): ${finalEv.retry ?? '증거가 아직 부족하다.'}`,
|
|
1513
1619
|
};
|
|
1514
1620
|
if (checklist.length) {
|
|
@@ -1526,13 +1632,13 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1526
1632
|
if (!c.ok) {
|
|
1527
1633
|
debug.log('agent-mission', 'result', { ok: false, rounds: result.numTurns ?? 0, evidencePath: finalEv.path, committed: false, usedOmni: false });
|
|
1528
1634
|
return { ok: false, worktree: wt.path, branch: wt.branch, rounds: result.numTurns ?? 0,
|
|
1529
|
-
evidencePath: finalEv.path, committed: false, usedOmniCrawl: false,
|
|
1635
|
+
evidencePath: finalEv.path, evidenceSatisfied: true, committed: false, usedOmniCrawl: false,
|
|
1530
1636
|
detail: `미완(자동 커밋 실패): ${c.out.slice(0, 120)}` };
|
|
1531
1637
|
}
|
|
1532
1638
|
}
|
|
1533
1639
|
debug.log('agent-mission', 'result', { ok: true, rounds: result.numTurns ?? 0, evidencePath: finalEv.path, committed, usedOmni: false });
|
|
1534
1640
|
return { ok: true, worktree: wt.path, branch: wt.branch, rounds: result.numTurns ?? 0,
|
|
1535
|
-
evidencePath: finalEv.path, committed, usedOmniCrawl: false, detail: '완료(증거 충족)' };
|
|
1641
|
+
evidencePath: finalEv.path, evidenceSatisfied: true, committed, usedOmniCrawl: false, detail: '완료(증거 충족)' };
|
|
1536
1642
|
}
|
|
1537
1643
|
env.TERM = 'xterm-256color';
|
|
1538
1644
|
// ⭐run-identity — runId was resolved before either headless or PTY spawn.
|
|
@@ -1612,7 +1718,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1612
1718
|
process.stderr.write(`[agent-mission] pty=${h.id} watch=${webAddress.webUrl ?? `(web unavailable: ${webAddress.pwaUnavailableReason ?? 'not-resolved'})`}\n`);
|
|
1613
1719
|
debug.log('agent-mission', 'pty-link', { id: h.id, webUrl: webAddress.webUrl, webUrlSource: webAddress.webUrlSource });
|
|
1614
1720
|
|
|
1615
|
-
const decisionIdentity = { missionId: runId, sessionId: runId, terminalId: h.id, agent: backend.name };
|
|
1721
|
+
const decisionIdentity = { missionId: spec.decisionMissionId ?? runId, sessionId: runId, terminalId: h.id, agent: backend.name };
|
|
1616
1722
|
const decisionEvent = (event: Omit<DecisionInput, keyof typeof decisionIdentity>): void => {
|
|
1617
1723
|
try { (deps.emitPtyDecision ?? emitPtyDecision)({ ...decisionIdentity, ...event } as DecisionInput); }
|
|
1618
1724
|
catch { /* Observation must not change mission control or handoff. */ }
|
|
@@ -1779,6 +1885,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1779
1885
|
if (deps.isMissionAborted?.()) throw new Error('MISSION_ABORTED');
|
|
1780
1886
|
// 미션 전송 — 인핸싱/기억으로 가공됐으면(멀티라인) 파일로 떨궈 read 지시(TUI 멀티라인 위험·verbatim 보존).
|
|
1781
1887
|
// 원문 그대로면(단문) 타이핑.
|
|
1888
|
+
let submittedCommand = spec.mission;
|
|
1889
|
+
const submittedInputs: string[] = [];
|
|
1782
1890
|
if (missionText !== spec.mission) {
|
|
1783
1891
|
const pf = join(wt.path, '.mission-prompt.md');
|
|
1784
1892
|
try { writeFileSync(pf, missionText); } catch { /* noop */ }
|
|
@@ -1786,9 +1894,12 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1786
1894
|
? '.mission-prompt.md 파일을 읽고 그 안의 미션을 완수하라. [elanous 인핸싱]의 커버리지 체크리스트 모든 항목을 산출물에 빠짐없이 반영하고(요약·일반화 금지), 완료하면 MISSION-COMPLETE 라고 답하라.'
|
|
1787
1895
|
: '.mission-prompt.md 파일을 읽고 그 안의 미션을 완수하라(원문 그대로·[elanous 기억]은 참조 컨텍스트). 완료하면 MISSION-COMPLETE 라고 답하라.';
|
|
1788
1896
|
debug.log('agent-mission', 'mission-send', { via: 'file', file: '.mission-prompt.md', chars: missionText.length, enhanced: enhanceActive });
|
|
1897
|
+
submittedCommand = note;
|
|
1898
|
+
submittedInputs.push(note);
|
|
1789
1899
|
drive(note); await sleep(800); drive('\r');
|
|
1790
1900
|
} else {
|
|
1791
1901
|
debug.log('agent-mission', 'mission-send', { via: 'type', chars: spec.mission.length });
|
|
1902
|
+
submittedInputs.push(spec.mission);
|
|
1792
1903
|
drive(spec.mission); await sleep(800); drive('\r');
|
|
1793
1904
|
}
|
|
1794
1905
|
|
|
@@ -1803,6 +1914,9 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1803
1914
|
backend,
|
|
1804
1915
|
stream: deps.controlStream,
|
|
1805
1916
|
handoffEnabled: true,
|
|
1917
|
+
recoverAfterMs: spec.recoverAfterMs,
|
|
1918
|
+
initialCommand: submittedCommand,
|
|
1919
|
+
onRecover: (blocked, action) => { decisionEvent({ step: 'recover', text: action, detail: { blocked, action } }); },
|
|
1806
1920
|
onSummary: (summary) => { controllerSummary = summary; },
|
|
1807
1921
|
onDecision: (text, reply) => {
|
|
1808
1922
|
pendingAnswer = reply ? { text: `${reply.answer || 'continue'}\r`, ...reply } : undefined;
|
|
@@ -1824,6 +1938,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1824
1938
|
// before asking the LLM — otherwise the mission text is typed into the prompt and the run stalls.
|
|
1825
1939
|
const trustAwareBrain: RunSupervisor = {
|
|
1826
1940
|
decide: (obs, signal) => {
|
|
1941
|
+
decisionEvent({ step: 'read', text: `step ${obs.step} (${obs.state}): ${classifierFrameLines(obs.screen).at(-1) ?? 'empty screen'}` });
|
|
1827
1942
|
pendingAnswer = undefined;
|
|
1828
1943
|
let answer: string | undefined;
|
|
1829
1944
|
if (backend.handleTrust?.(obs.screen, (bytes) => { answer = bytes; }) && answer !== undefined) {
|
|
@@ -1889,7 +2004,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1889
2004
|
handoffPrSkipped = true;
|
|
1890
2005
|
debug.log('agent-mission', 'handoff-pr-skipped', { reason: 'origin-not-github' });
|
|
1891
2006
|
} else {
|
|
1892
|
-
(deps.dispatchOpenPullRequest ?? dispatchOpenPullRequest)({ title: `agent-mission: ${spec.branch}`, body: deps.originalMission ?? spec.mission, head: spec.branch, base: spec.base ?? 'main' }, { cwd: worktree });
|
|
2007
|
+
const opened = (deps.dispatchOpenPullRequest ?? dispatchOpenPullRequest)({ title: `agent-mission: ${spec.branch}`, body: deps.originalMission ?? spec.mission, head: spec.branch, base: spec.base ?? 'main' }, { cwd: worktree });
|
|
2008
|
+
if (opened.number !== null) deps.onMissionPrNumber?.(opened.number);
|
|
1893
2009
|
}
|
|
1894
2010
|
gatedHandoff = true;
|
|
1895
2011
|
},
|
|
@@ -1916,7 +2032,6 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1916
2032
|
renderPng: () => h.renderScreenPng(),
|
|
1917
2033
|
bus,
|
|
1918
2034
|
ident: { ptyId: h.id, instance: resolveInstanceName(), runId },
|
|
1919
|
-
onPublishedFrame: (obs) => decisionEvent({ step: 'read', text: `step ${obs.step} (${obs.state}): ${classifierFrameLines(obs.screen).at(-1) ?? 'empty screen'}` }),
|
|
1920
2035
|
});
|
|
1921
2036
|
debug.log('agent-mission', 'takeover-wait', { id: h.id, maxWaitMs: AGENT_MISSION_TAKEOVER_WAIT_MS });
|
|
1922
2037
|
const controlRun = runWithControlObserve(
|
|
@@ -1932,7 +2047,12 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1932
2047
|
pendingAnswer = undefined;
|
|
1933
2048
|
const accepted = inject(text);
|
|
1934
2049
|
if (accepted) {
|
|
2050
|
+
submittedInputs.push(text);
|
|
1935
2051
|
decisionEvent({ step: 'input', text: `Sent ${text.startsWith('\x1b') ? 'key' : 'text'}: ${text.startsWith('\x1b') ? 'navigation' : text}` });
|
|
2052
|
+
}
|
|
2053
|
+
// Recovery stages and recover events are committed only after the PTY accepted the input.
|
|
2054
|
+
brain.onInputResult(text, accepted);
|
|
2055
|
+
if (accepted) {
|
|
1936
2056
|
if (reply) decisionEvent({ step: 'answer', text: 'Answered agent question', detail: { question: reply.question, answer: reply.answer } });
|
|
1937
2057
|
}
|
|
1938
2058
|
return accepted;
|
|
@@ -1948,6 +2068,8 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1948
2068
|
} : {}),
|
|
1949
2069
|
awaitOwnership: { maxWaitMs: AGENT_MISSION_TAKEOVER_WAIT_MS },
|
|
1950
2070
|
settle: async () => {
|
|
2071
|
+
// A frozen screen goes quiet after 6 s, so stall recovery is still observed; work that keeps
|
|
2072
|
+
// drawing output keeps the original 360 s settle and round budget.
|
|
1951
2073
|
const quiet = waitForQuiet(h, 6000, 360000);
|
|
1952
2074
|
await (deps.awaitMission ? deps.awaitMission(quiet) : quiet);
|
|
1953
2075
|
},
|
|
@@ -1986,7 +2108,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
1986
2108
|
throw error;
|
|
1987
2109
|
}
|
|
1988
2110
|
},
|
|
1989
|
-
}, { maxSteps: maxRounds });
|
|
2111
|
+
}, { maxSteps: maxRounds, stuckLimit: maxRounds + 1 });
|
|
1990
2112
|
deps.onControlActive?.(pending);
|
|
1991
2113
|
return pending;
|
|
1992
2114
|
},
|
|
@@ -2025,7 +2147,7 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
2025
2147
|
try { h.kill(); } catch { /* noop */ }
|
|
2026
2148
|
return { ok: true, worktree: wt.path, branch: wt.branch, rounds: control.steps, evidencePath: wt.path, committed: true, usedOmniCrawl: usedOmni,
|
|
2027
2149
|
...(handoffPrSkipped ? { pr: 'skipped' as const, reason: 'origin-not-github' as const } : {}),
|
|
2028
|
-
detail: handoffPrSkipped ? '완료(증거 게이트·push·PR 생략)' : '완료(증거 게이트·PR)' };
|
|
2150
|
+
evidenceSatisfied: true, detail: handoffPrSkipped ? '완료(증거 게이트·push·PR 생략)' : '완료(증거 게이트·PR)' };
|
|
2029
2151
|
}
|
|
2030
2152
|
const done = control.termination.kind === 'success';
|
|
2031
2153
|
const round = control.steps;
|
|
@@ -2079,24 +2201,29 @@ async function runAgentMissionBody(spec: AgentMissionSpec, deps: AgentMissionDep
|
|
|
2079
2201
|
if (deps.isMissionAborted?.()) return { ok: false, reason: 'aborted', worktree: wt.path, branch: wt.branch,
|
|
2080
2202
|
rounds: round, evidencePath: null, committed: false, usedOmniCrawl: usedOmni, detail: 'aborted' };
|
|
2081
2203
|
const finalEv = verifyEvidence(wt.path, spec.evidence);
|
|
2082
|
-
const
|
|
2204
|
+
const finalScreen = await h.renderScreen();
|
|
2205
|
+
const driveVerdict = verdictForFinalPtyScreen({ screen: finalScreen, inputHistory: submittedInputs, exitCode: h.exitCode ?? null });
|
|
2206
|
+
debug.log('agent-mission', 'drive-verdict', { kind: driveVerdict.kind, reason: driveVerdict.reason.slice(0, 200) });
|
|
2207
|
+
const recoverExhausted = control.termination.kind === 'stuck' && control.termination.reason.startsWith('사람 필요');
|
|
2208
|
+
const accepted = finalEv.ok && !recoverExhausted && driveVerdict.kind !== 'done-but-failed';
|
|
2209
|
+
const evidencePath = accepted ? finalEv.path : null;
|
|
2083
2210
|
let committed = false;
|
|
2084
|
-
if (!deps.isMissionAborted?.() && !spec.workdir &&
|
|
2211
|
+
if (!deps.isMissionAborted?.() && !spec.workdir && accepted && (spec.commit ?? true)) {
|
|
2085
2212
|
const c = (deps.commitWorktree ?? commitWorktree)(wt.path, `chore(agent-mission): ${spec.branch} — ${backend.name}-in-elanous PTY RFC 산출`);
|
|
2086
2213
|
committed = c.ok;
|
|
2087
2214
|
debug.log('agent-mission', 'commit', { ok: c.ok, out: c.out.slice(0, 120) });
|
|
2088
2215
|
}
|
|
2089
|
-
debug.log('agent-mission', 'result', { ok:
|
|
2090
|
-
decisionEvent({ step: 'done', text:
|
|
2091
|
-
stopLive(
|
|
2216
|
+
debug.log('agent-mission', 'result', { ok: accepted, rounds: round, evidencePath, committed, usedOmni });
|
|
2217
|
+
decisionEvent({ step: 'done', text: accepted ? 'Mission evidence accepted' : 'Mission ended without sufficient evidence', detail: { result: { kind: evidencePath ? 'file' : 'text', ref: evidencePath ?? (control.termination.kind === 'error' ? control.termination.message : recoverExhausted ? control.termination.reason : 'evidence unavailable') } } });
|
|
2218
|
+
stopLive(accepted);
|
|
2092
2219
|
if (h.pid) await terminateMissionProcessTree(h.pid);
|
|
2093
2220
|
try { h.kill(); } catch { /* noop */ }
|
|
2094
2221
|
|
|
2095
2222
|
return {
|
|
2096
|
-
ok:
|
|
2097
|
-
evidencePath, committed, usedOmniCrawl: usedOmni,
|
|
2098
|
-
ptyId: h.id, webUrl: webAddress.webUrl,
|
|
2099
|
-
detail: done ? '완료(증거 충족)' : (finalEv.ok ? '증거 충족(루프 종료)' : '미완(증거 부족)'),
|
|
2223
|
+
ok: accepted, worktree: wt.path, branch: wt.branch, rounds: round,
|
|
2224
|
+
evidencePath, evidenceSatisfied: finalEv.ok, committed, usedOmniCrawl: usedOmni,
|
|
2225
|
+
ptyId: h.id, webUrl: webAddress.webUrl, driveVerdict: driveVerdict.kind,
|
|
2226
|
+
detail: recoverExhausted ? control.termination.reason : driveVerdict.kind === 'done-but-failed' ? `미완(DRIVE-OK: ${driveVerdict.reason})` : done ? '완료(증거 충족)' : (finalEv.ok ? '증거 충족(루프 종료)' : '미완(증거 부족)'),
|
|
2100
2227
|
};
|
|
2101
2228
|
} finally { stopLive(); finishContext(false); }
|
|
2102
2229
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { runAgentMission, resolveBackend, type AgentBackend, type AgentMissionResult, type AgentMissionSpec, type EvidenceMode } from './driver.js';
|
|
4
|
+
|
|
5
|
+
export interface DuoOptions {
|
|
6
|
+
left: string;
|
|
7
|
+
right: string;
|
|
8
|
+
branch: string;
|
|
9
|
+
base?: string;
|
|
10
|
+
missionFile?: string;
|
|
11
|
+
evidence?: string;
|
|
12
|
+
docDir?: string;
|
|
13
|
+
docGlob?: string;
|
|
14
|
+
testPath?: string;
|
|
15
|
+
file?: string;
|
|
16
|
+
maxRounds?: string;
|
|
17
|
+
commit?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DuoSide {
|
|
21
|
+
side: 'left' | 'right';
|
|
22
|
+
backend: string;
|
|
23
|
+
branch: string;
|
|
24
|
+
finished: number;
|
|
25
|
+
result?: AgentMissionResult;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DuoResult {
|
|
30
|
+
missionId: string;
|
|
31
|
+
sides: [DuoSide, DuoSide];
|
|
32
|
+
lines: [string, string];
|
|
33
|
+
exitCode: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DuoDeps {
|
|
37
|
+
resolveBackend?: typeof resolveBackend;
|
|
38
|
+
runMission?: typeof runAgentMission;
|
|
39
|
+
readFile?: (path: string) => string;
|
|
40
|
+
id?: () => string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function evidenceFor(opts: DuoOptions): EvidenceMode {
|
|
44
|
+
if (opts.evidence === 'doc') return { kind: 'doc', dirRel: opts.docDir ?? 'docs/plans', glob: new RegExp(opts.docGlob ?? '^PLAN-.*[.]md$', 'i') };
|
|
45
|
+
if (opts.evidence === 'test') {
|
|
46
|
+
if (!opts.testPath) throw new Error('test 모드엔 --test-path 필요');
|
|
47
|
+
return { kind: 'test', testPath: opts.testPath, ...(opts.file ? { fileRel: opts.file } : {}) };
|
|
48
|
+
}
|
|
49
|
+
if (opts.evidence === undefined || opts.evidence === 'tsc') return { kind: 'tsc' };
|
|
50
|
+
throw new Error('--evidence 는 doc|tsc|test 여야 합니다');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function render(side: DuoSide, first: DuoSide | undefined): string {
|
|
54
|
+
const evidence = side.result?.evidenceSatisfied === true
|
|
55
|
+
? side.result.evidencePath ? `충족(${side.result.evidencePath})` : '충족'
|
|
56
|
+
: side.result?.evidenceSatisfied === false ? '미충족' : '미측정';
|
|
57
|
+
const drive = side.result?.driveVerdict ?? '미측정';
|
|
58
|
+
const detail = side.error ?? (!side.result?.ok ? side.result?.detail : undefined);
|
|
59
|
+
return `${side.side} ${side.backend} · branch=${side.branch} · terminalId=${side.result?.ptyId ?? '없음'} · ${first === side ? '먼저' : '나중'} · 증거 ${evidence} · DRIVE-OK ${drive} · ${side.result?.ok ? '완료' : '실패'}${detail ? ` (${detail})` : ''}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Run two independent existing agent missions; neither branch waits for or cancels its peer. */
|
|
63
|
+
export async function runAgentMissionDuo(textParts: string[], opts: DuoOptions, deps: DuoDeps = {}): Promise<DuoResult> {
|
|
64
|
+
if (!opts.branch?.trim()) throw new Error('--branch 필요');
|
|
65
|
+
if (opts.left !== 'codex' && opts.left !== 'claude') throw new Error('--left 는 codex|claude 여야 합니다');
|
|
66
|
+
if (opts.right !== 'codex' && opts.right !== 'claude') throw new Error('--right 는 codex|claude 여야 합니다');
|
|
67
|
+
if (opts.left === opts.right) throw new Error('duo 에는 서로 다른 backend 둘이 필요합니다');
|
|
68
|
+
const mission = opts.missionFile ? (deps.readFile ?? ((path) => readFileSync(path, 'utf8')))(opts.missionFile) : textParts.join(' ');
|
|
69
|
+
if (!mission.trim()) throw new Error('미션 텍스트가 비었다 — <text...> 또는 --mission-file 필요');
|
|
70
|
+
const evidence = evidenceFor(opts);
|
|
71
|
+
const rounds = opts.maxRounds === undefined ? 16 : Number(opts.maxRounds);
|
|
72
|
+
if (!Number.isSafeInteger(rounds) || rounds <= 0) throw new Error('--max-rounds 는 양의 정수여야 합니다');
|
|
73
|
+
const resolve = deps.resolveBackend ?? resolveBackend;
|
|
74
|
+
const backends: [AgentBackend, AgentBackend] = [resolve(opts.left), resolve(opts.right)];
|
|
75
|
+
if (backends[0].name !== opts.left || backends[1].name !== opts.right) throw new Error('backend 선택이 요청과 다릅니다');
|
|
76
|
+
const id = `duo-${(deps.id ?? randomUUID)()}`;
|
|
77
|
+
const run = deps.runMission ?? runAgentMission;
|
|
78
|
+
let completed = 0;
|
|
79
|
+
const branches = [`${opts.branch}-${opts.left}`, `${opts.branch}-${opts.right}`] as const;
|
|
80
|
+
const tasks = backends.map(async (backend, index): Promise<DuoSide> => {
|
|
81
|
+
const side = index === 0 ? 'left' : 'right';
|
|
82
|
+
const branch = branches[index]!;
|
|
83
|
+
const spec: AgentMissionSpec = {
|
|
84
|
+
mission, branch, agent: backend, decisionMissionId: id, evidence, maxRounds: rounds,
|
|
85
|
+
...(opts.base ? { base: opts.base } : {}), commit: opts.commit !== false,
|
|
86
|
+
enhance: false, memory: false, resources: 'off', entry: 'external-verbatim',
|
|
87
|
+
};
|
|
88
|
+
try { return { side, backend: backend.name, branch, result: await run(spec), finished: ++completed }; }
|
|
89
|
+
catch (error) { return { side, backend: backend.name, branch, error: error instanceof Error ? error.message : String(error), finished: ++completed }; }
|
|
90
|
+
});
|
|
91
|
+
const sides = await Promise.all(tasks) as [DuoSide, DuoSide];
|
|
92
|
+
const first = sides[0].finished <= sides[1].finished ? sides[0] : sides[1];
|
|
93
|
+
return { missionId: id, sides, lines: [render(sides[0], first), render(sides[1], first)], exitCode: sides.every((s) => s.result?.ok) ? 0 : 2 };
|
|
94
|
+
}
|
|
@@ -41,10 +41,10 @@ function safe(value: string): string {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** The only emission point for the inside-PTY decision stream. Never log unsanitized input. */
|
|
44
|
-
export function emitPtyDecision(input: DecisionInput, log: typeof debug.log = debug.log): PtyDecision {
|
|
44
|
+
export function emitPtyDecision(input: DecisionInput, log: typeof debug.log = (category, event, data) => debug.log(category, event, data)): PtyDecision {
|
|
45
45
|
let stream = missionStreams.get(input.missionId);
|
|
46
46
|
if (!stream) {
|
|
47
|
-
stream = { alias:
|
|
47
|
+
stream = { alias: `${input.missionId.startsWith('duo-') ? 'duo' : 'mission'}-${missionStreams.size + 1}`, seq: -1 };
|
|
48
48
|
missionStreams.set(input.missionId, stream);
|
|
49
49
|
}
|
|
50
50
|
stream.seq += 1;
|