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
|
@@ -27,6 +27,10 @@ export interface DraftSweepAdapters {
|
|
|
27
27
|
closeDraft(repository: string, number: number, comment: string): Promise<void>;
|
|
28
28
|
/** Resolve the latest claim comment owner; a failed lookup must not authorize a close. */
|
|
29
29
|
getClaimOwner?(repository: string, number: number): Promise<string | undefined>;
|
|
30
|
+
/** Whether the current owned run has a self-implement.result final observation, even if its worktree remains. */
|
|
31
|
+
hasFinalRunResult?(draft: SweepDraft, repository: string): Promise<boolean | undefined>;
|
|
32
|
+
/** Per-file last change on the draft branch; undefined means file coverage is unverified. */
|
|
33
|
+
getLatestFileChanges?(draft: SweepDraft, repository: string): Promise<Readonly<Record<string, string>> | undefined>;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
export interface DraftSweepOptions {
|
|
@@ -120,9 +124,23 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
|
|
|
120
124
|
return finish();
|
|
121
125
|
}
|
|
122
126
|
let statuses: Map<number, string | undefined>;
|
|
127
|
+
let finality: Map<number, boolean | undefined>;
|
|
128
|
+
let latestFileChanges: Map<number, Readonly<Record<string, string>> | undefined>;
|
|
123
129
|
try {
|
|
124
130
|
statuses = new Map<number, string | undefined>();
|
|
125
|
-
|
|
131
|
+
finality = new Map<number, boolean | undefined>();
|
|
132
|
+
latestFileChanges = new Map<number, Readonly<Record<string, string>> | undefined>();
|
|
133
|
+
for (const draft of drafts) {
|
|
134
|
+
const status = await adapters.getRunStatus(draft, repository);
|
|
135
|
+
// A prior final cannot terminate a new active run on the same draft.
|
|
136
|
+
const active = status === 'running' || status === 'probable-running';
|
|
137
|
+
const final = active ? false : await adapters.hasFinalRunResult?.(draft, repository);
|
|
138
|
+
finality.set(draft.number, final);
|
|
139
|
+
statuses.set(draft.number, final ? 'self-implement.result final' : status);
|
|
140
|
+
if (draft.changedFiles?.length && adapters.getLatestFileChanges) {
|
|
141
|
+
latestFileChanges.set(draft.number, await adapters.getLatestFileChanges(draft, repository));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
126
144
|
} catch (error) {
|
|
127
145
|
result.error = String(error);
|
|
128
146
|
return finish();
|
|
@@ -132,8 +150,10 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
|
|
|
132
150
|
let unobserved = 0;
|
|
133
151
|
let claimed = 0;
|
|
134
152
|
let claimExpired = 0;
|
|
135
|
-
for (const
|
|
153
|
+
for (const listedDraft of drafts) {
|
|
154
|
+
const draft = { ...listedDraft, latestFileChanges: latestFileChanges.get(listedDraft.number) };
|
|
136
155
|
const runStatus = statuses.get(draft.number);
|
|
156
|
+
const finalRunResult = finality.get(draft.number);
|
|
137
157
|
const states = stateLabels.filter((state) => draft.labels.includes(state.name));
|
|
138
158
|
const contradiction = states.length > 1 || draft.labels.includes(approvalLabel ?? '');
|
|
139
159
|
if (draft.labels.includes(releaseHoldLabel ?? '') || (!contradiction && draft.labels.includes(keepLabel ?? ''))) {
|
|
@@ -147,8 +167,14 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
|
|
|
147
167
|
? (now.getTime() - Date.parse(draft.updatedAt)) / 3_600_000 : NaN;
|
|
148
168
|
// An unobserved claim may still have a merged twin: liveness/holds retain their priority,
|
|
149
169
|
// but a superseded draft must not be hidden by the six-hour claim window.
|
|
150
|
-
const mergedDecision = isClaimed ? decideDraft({ draft, runStatus, mergedTwins: merged,
|
|
151
|
-
|
|
170
|
+
const mergedDecision = isClaimed ? decideDraft({ draft, runStatus, mergedTwins: merged, openDrafts: drafts, liveBranches,
|
|
171
|
+
finalRunResult, ageHours: idleHours }) : undefined;
|
|
172
|
+
const supersededClaim = mergedDecision?.reason.startsWith('superseded-by #') === true
|
|
173
|
+
|| mergedDecision?.reason.startsWith('duplicate-of-open #') === true;
|
|
174
|
+
if (!contradiction && mergedDecision?.reason === 'branch-finality-unobserved') {
|
|
175
|
+
record({ number: draft.number, action: 'keep', reason: mergedDecision.reason, applied: false });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
152
178
|
// Without an observed update time, neither expiration nor a fallback to creation time can authorize mutation —
|
|
153
179
|
// but a merged twin does not need the update time, so superseded keeps its priority over the unobserved claim.
|
|
154
180
|
if (isClaimed && !supersededClaim && (typeof draft.updatedAt !== 'string' || !Number.isFinite(Date.parse(draft.updatedAt)))) {
|
|
@@ -162,13 +188,21 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
|
|
|
162
188
|
}
|
|
163
189
|
if (isClaimed && !supersededClaim && Number.isFinite(idleHours) && idleHours >= CLAIM_IDLE_HOURS) {
|
|
164
190
|
claimExpired += 1;
|
|
165
|
-
if (
|
|
191
|
+
if (liveBranches.has(draft.branch) && finalRunResult === undefined
|
|
192
|
+
&& runStatus !== 'running' && runStatus !== 'probable-running') {
|
|
193
|
+
record({ number: draft.number, action: 'keep', reason: 'branch-finality-unobserved', applied: false });
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (runStatus === 'running' || runStatus === 'probable-running' ||
|
|
197
|
+
(runStatus !== 'self-implement.result final' && liveBranches.has(draft.branch))) {
|
|
166
198
|
record({ number: draft.number, action: 'keep', reason: 'claim-expired-but-live', applied: false });
|
|
167
199
|
continue;
|
|
168
200
|
}
|
|
169
201
|
const action = closes >= DRAFT_SWEEP_CLOSE_CAP ? 'keep' : 'close';
|
|
170
202
|
if (action === 'close') closes += 1;
|
|
171
|
-
const entry: DraftSweepEntry = { number: draft.number, action,
|
|
203
|
+
const entry: DraftSweepEntry = { number: draft.number, action,
|
|
204
|
+
reason: action === 'close' ? runStatus === 'self-implement.result final'
|
|
205
|
+
? 'claim-expired (self-implement.result final; worktree is not live)' : 'claim-expired' : 'close-cap',
|
|
172
206
|
...(action === 'close' ? { statusLabel: stalledLabel } : {}), applied: false };
|
|
173
207
|
record(entry);
|
|
174
208
|
if (!apply || action !== 'close') continue;
|
|
@@ -178,7 +212,7 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
|
|
|
178
212
|
await adapters.setLabels(repository, draft.number, { add: stalledLabel, remove: [runningLabel] });
|
|
179
213
|
labelsChanged = true;
|
|
180
214
|
await adapters.closeDraft(repository, draft.number,
|
|
181
|
-
`Draft sweep: 처리 중 표식이 ${CLAIM_IDLE_HOURS}시간 갱신 없음 — 런·워크트리 없음 · 주인 ${owner}. Closed as stalled. Branch preserved; reopen to restore.`);
|
|
215
|
+
`Draft sweep: 처리 중 표식이 ${CLAIM_IDLE_HOURS}시간 갱신 없음 — ${runStatus === 'self-implement.result final' ? 'self-implement.result final · 남은 워크트리는 live 아님' : '런·워크트리 없음'} · 주인 ${owner}. Closed as stalled. Branch preserved; reopen to restore.`);
|
|
182
216
|
entry.applied = true;
|
|
183
217
|
} catch (error) {
|
|
184
218
|
if (labelsChanged) entry.partialApplied = true;
|
|
@@ -191,8 +225,8 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
|
|
|
191
225
|
record({ number: draft.number, action: 'keep', reason: 'outside-harness', applied: false });
|
|
192
226
|
continue;
|
|
193
227
|
}
|
|
194
|
-
let decision = supersededClaim ? mergedDecision! : decideDraft({ draft, runStatus, mergedTwins: merged, liveBranches,
|
|
195
|
-
ageHours: runStatus ? ageHours : idleHours });
|
|
228
|
+
let decision = supersededClaim ? mergedDecision! : decideDraft({ draft, runStatus, mergedTwins: merged, openDrafts: drafts, liveBranches,
|
|
229
|
+
finalRunResult, ageHours: runStatus ? ageHours : idleHours });
|
|
196
230
|
if (!runStatus && (decision.reason === 'unobserved' || decision.reason === 'stale-unobserved')) unobserved += 1;
|
|
197
231
|
if (!contradiction && decision.action === 'close') {
|
|
198
232
|
if (closes >= DRAFT_SWEEP_CLOSE_CAP) decision = { action: 'keep', reason: 'close-cap' };
|
|
@@ -204,7 +238,7 @@ export async function runDraftSweep({ repository, adapters, apply = false, now =
|
|
|
204
238
|
: decision.reason;
|
|
205
239
|
const isLive = decision.reason === 'live';
|
|
206
240
|
const target = action === 'report' ? undefined : action === 'close'
|
|
207
|
-
? decision.reason.startsWith('superseded-by #') ? supersededLabel : stalledLabel
|
|
241
|
+
? decision.reason.startsWith('superseded-by #') || decision.reason.startsWith('duplicate-of-open #') ? supersededLabel : stalledLabel
|
|
208
242
|
: isLive ? (states.length ? runningLabel : undefined) : runStatus && (states[0]?.name === runningLabel || states.length === 0) ? stalledLabel : undefined;
|
|
209
243
|
const entry: DraftSweepEntry = { number: draft.number, action, reason, ...(target ? { statusLabel: target } : {}), applied: false };
|
|
210
244
|
record(entry);
|
|
@@ -6,14 +6,25 @@ export interface DraftTriagePr {
|
|
|
6
6
|
title?: string;
|
|
7
7
|
branch: string;
|
|
8
8
|
labels?: readonly string[];
|
|
9
|
+
body?: string;
|
|
10
|
+
mergeCommitMessage?: string;
|
|
11
|
+
landingVerifiedComments?: readonly string[];
|
|
12
|
+
createdAt?: string;
|
|
13
|
+
mergedAt?: string;
|
|
14
|
+
changedFiles?: readonly string[];
|
|
15
|
+
/** Latest change to each draft file, from the draft branch's commit history. Missing is not creation time. */
|
|
16
|
+
latestFileChanges?: Readonly<Record<string, string>>;
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
export interface DraftTriageInput {
|
|
12
20
|
draft: DraftTriagePr;
|
|
13
21
|
runStatus: string | undefined;
|
|
14
22
|
mergedTwins: readonly DraftTriagePr[];
|
|
23
|
+
openDrafts?: readonly DraftTriagePr[];
|
|
15
24
|
ageHours: number;
|
|
16
25
|
liveBranches: ReadonlySet<string>;
|
|
26
|
+
/** false only when the current run's final result has actually been checked and ruled out. */
|
|
27
|
+
finalRunResult?: boolean;
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
export type DraftDecision = { action: 'close' | 'keep'; reason: string };
|
|
@@ -25,11 +36,22 @@ const PROTECTED_LABELS: ReadonlySet<string> = new Set<string>([...PR_LABELS
|
|
|
25
36
|
.map((label) => label.name), RELEASE_PATH_LABEL]);
|
|
26
37
|
|
|
27
38
|
const goalId = (branch: string): string | undefined => /(?:^|[-/])goalid-([a-f0-9]+)(?=-|\/|$)/i.exec(branch)?.[1]?.toLowerCase();
|
|
39
|
+
export const branchStem = (branch: string): string | undefined => /^self-impl\/(.+)-[a-f0-9]{8}-r[a-f0-9]+$/i.exec(branch)?.[1];
|
|
40
|
+
const slot = (body?: string): string | undefined => /^칸:\s*(.+?)\s*$/m.exec(body ?? '')?.[1];
|
|
41
|
+
const referencesDraft = (text: string, number: number): boolean =>
|
|
42
|
+
new RegExp(`(?:\\(수확\\s*#${number}\\s*\\)|\\bsuperseded\\s*#${number}(?!\\d))`, 'i').test(text);
|
|
43
|
+
const landingCommentReferencesDraft = (text: string, number: number): boolean =>
|
|
44
|
+
new RegExp(`\\blanding-verified\\s*:\\s*(?:draft|superseded|수확)\\s*#${number}(?!\\d)\\b`, 'i').test(text);
|
|
28
45
|
|
|
29
|
-
export function decideDraft({ draft, runStatus, mergedTwins, ageHours, liveBranches }: DraftTriageInput): DraftDecision {
|
|
46
|
+
export function decideDraft({ draft, runStatus, mergedTwins, openDrafts, ageHours, liveBranches, finalRunResult }: DraftTriageInput): DraftDecision {
|
|
30
47
|
const held = draft.labels?.find((label) => PROTECTED_LABELS.has(label));
|
|
31
48
|
if (held) return { action: 'keep', reason: `label:${held}` };
|
|
32
|
-
if (runStatus
|
|
49
|
+
if (runStatus !== 'self-implement.result final' && draft.branch && liveBranches.has(draft.branch)
|
|
50
|
+
&& finalRunResult === undefined && runStatus !== 'running' && runStatus !== 'probable-running') {
|
|
51
|
+
return { action: 'keep', reason: 'branch-finality-unobserved' };
|
|
52
|
+
}
|
|
53
|
+
if (runStatus === 'running' || runStatus === 'probable-running' ||
|
|
54
|
+
(runStatus !== 'self-implement.result final' && draft.branch && liveBranches.has(draft.branch))) {
|
|
33
55
|
return { action: 'keep', reason: 'live' };
|
|
34
56
|
}
|
|
35
57
|
const lineage = goalId(draft.branch);
|
|
@@ -37,9 +59,39 @@ export function decideDraft({ draft, runStatus, mergedTwins, ageHours, liveBranc
|
|
|
37
59
|
(draft.title && pr.title === draft.title) || (lineage && goalId(pr.branch) === lineage)
|
|
38
60
|
));
|
|
39
61
|
if (twin) return { action: 'close', reason: `superseded-by #${twin.number}` };
|
|
62
|
+
const stem = branchStem(draft.branch);
|
|
63
|
+
const createdAt = Date.parse(draft.createdAt ?? '');
|
|
64
|
+
const newer = stem && draft.title && Number.isFinite(createdAt) ? openDrafts
|
|
65
|
+
?.filter((pr) => pr.number !== draft.number && pr.title === draft.title && branchStem(pr.branch) === stem
|
|
66
|
+
&& Number.isFinite(Date.parse(pr.createdAt ?? '')) && Date.parse(pr.createdAt!) > createdAt)
|
|
67
|
+
.sort((a, b) => Date.parse(b.createdAt!) - Date.parse(a.createdAt!) || b.number - a.number)[0] : undefined;
|
|
68
|
+
if (newer) return { action: 'close', reason: `duplicate-of-open #${newer.number}` };
|
|
69
|
+
const harvest = mergedTwins.find((pr) => pr.number !== draft.number && (
|
|
70
|
+
[pr.mergeCommitMessage, pr.body].some((text) => text && referencesDraft(text, draft.number))
|
|
71
|
+
|| pr.landingVerifiedComments?.some((text) => landingCommentReferencesDraft(text, draft.number))
|
|
72
|
+
));
|
|
73
|
+
if (harvest) return { action: 'close', reason: `superseded-by #${harvest.number} (harvest #${draft.number})` };
|
|
74
|
+
const created = Date.parse(draft.createdAt ?? '');
|
|
75
|
+
const files = draft.changedFiles;
|
|
76
|
+
const draftSlot = slot(draft.body);
|
|
77
|
+
const later = mergedTwins.filter((pr) => pr.number !== draft.number && Number.isFinite(created)
|
|
78
|
+
&& Number.isFinite(Date.parse(pr.mergedAt ?? '')) && Date.parse(pr.mergedAt!) > created
|
|
79
|
+
&& ((lineage && goalId(pr.branch) === lineage) || (draftSlot && slot(pr.body) === draftSlot)));
|
|
80
|
+
if (files?.length && files.every((file) => {
|
|
81
|
+
const changed = Date.parse(draft.latestFileChanges?.[file] ?? '');
|
|
82
|
+
return Number.isFinite(changed) && changed >= created && later.some((pr) =>
|
|
83
|
+
pr.changedFiles?.includes(file) && Date.parse(pr.mergedAt!) > changed);
|
|
84
|
+
})) {
|
|
85
|
+
const covering = later.filter((pr) => pr.changedFiles?.some((file) => files.includes(file)
|
|
86
|
+
&& Date.parse(pr.mergedAt!) > Date.parse(draft.latestFileChanges![file]!)));
|
|
87
|
+
return { action: 'close', reason: `superseded-by #${covering.map((pr) => pr.number).join(', #')} (all-files-landed)` };
|
|
88
|
+
}
|
|
40
89
|
if (!runStatus) return Number.isFinite(ageHours) && ageHours >= STALLED_DRAFT_HOURS
|
|
41
90
|
? { action: 'close', reason: 'stale-unobserved' }
|
|
42
91
|
: { action: 'keep', reason: 'unobserved' };
|
|
92
|
+
if (runStatus === 'self-implement.result final') return Number.isFinite(ageHours) && ageHours >= STALLED_DRAFT_HOURS
|
|
93
|
+
? { action: 'close', reason: 'stale-ended-run (self-implement.result final; worktree is not live)' }
|
|
94
|
+
: { action: 'keep', reason: 'recent (self-implement.result final; worktree is not live)' };
|
|
43
95
|
if (Number.isFinite(ageHours) && ageHours >= STALLED_DRAFT_HOURS) return { action: 'close', reason: 'stale-ended-run' };
|
|
44
96
|
return { action: 'keep', reason: 'recent' };
|
|
45
97
|
}
|
|
@@ -101,21 +101,27 @@ function codexCandidateUsable(candidate: BudgetCodexCandidate, cap: number | und
|
|
|
101
101
|
function slotUsable(
|
|
102
102
|
provider: string,
|
|
103
103
|
input: DecideBudgetInput,
|
|
104
|
-
|
|
104
|
+
allowUnmeasured = false,
|
|
105
|
+
): { usable: boolean; reason: string; unmeasured?: boolean } {
|
|
105
106
|
if (provider === CODEX_PROVIDER) {
|
|
106
107
|
const cap = capFor(input.maxUsedPercent, CODEX_PROVIDER);
|
|
107
|
-
const
|
|
108
|
-
|
|
108
|
+
const measuredUsable = input.codexCandidates.some((candidate) => codexCandidateUsable(candidate, cap));
|
|
109
|
+
const unmeasured = allowUnmeasured && !measuredUsable && (input.codexCandidates.length === 0
|
|
110
|
+
|| input.codexCandidates.some((candidate) => candidate.reached !== true
|
|
111
|
+
&& (typeof candidate.usedPercent !== 'number' || !Number.isFinite(candidate.usedPercent))));
|
|
112
|
+
return { usable: measuredUsable || unmeasured, reason: formatCodexReason(input.codexCandidates, cap), ...(unmeasured ? { unmeasured: true } : {}) };
|
|
109
113
|
}
|
|
110
114
|
if (provider === GROK_PROVIDER) {
|
|
111
115
|
const cap = capFor(input.maxUsedPercent, GROK_PROVIDER);
|
|
112
116
|
const known = typeof input.grokUsedPercent === 'number' && Number.isFinite(input.grokUsedPercent);
|
|
113
|
-
const
|
|
117
|
+
const unmeasured = allowUnmeasured && !known;
|
|
118
|
+
const usable = unmeasured || (cap === undefined
|
|
114
119
|
? known
|
|
115
|
-
: known && (input.grokUsedPercent as number) < cap;
|
|
120
|
+
: known && (input.grokUsedPercent as number) < cap);
|
|
116
121
|
return {
|
|
117
122
|
usable,
|
|
118
123
|
reason: formatGrokReason(input.grokUsedPercent, cap),
|
|
124
|
+
...(unmeasured ? { unmeasured: true } : {}),
|
|
119
125
|
};
|
|
120
126
|
}
|
|
121
127
|
return { usable: false, reason: `${provider}: 예산 판정 없음` };
|
|
@@ -127,6 +133,15 @@ function slotUsable(
|
|
|
127
133
|
* 아무것도 못 쓰면 onShortfall 이 wait-reset 일 때만 wait-reset, 아니면 stop.
|
|
128
134
|
*/
|
|
129
135
|
export function decideBudget(input: DecideBudgetInput): BudgetDecision {
|
|
136
|
+
return decideBudgetWithPolicy(input, false).decision;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Only the harness launch gate treats an unmeasured provider as launchable. */
|
|
140
|
+
export function decideLaunchBudget(input: DecideBudgetInput): { decision: BudgetDecision; unmeasuredProvider?: string } {
|
|
141
|
+
return decideBudgetWithPolicy(input, true);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function decideBudgetWithPolicy(input: DecideBudgetInput, allowUnmeasured: boolean): { decision: BudgetDecision; unmeasuredProvider?: string } {
|
|
130
145
|
const reasons: string[] = [];
|
|
131
146
|
if (input.preference.chain.length === 0) {
|
|
132
147
|
reasons.push('chain: 비었음 — tools.selfImplement.childLlm.chain 도 llm.fallbackChain 도 없다');
|
|
@@ -134,7 +149,7 @@ export function decideBudget(input: DecideBudgetInput): BudgetDecision {
|
|
|
134
149
|
let firstUnusable = false;
|
|
135
150
|
for (let index = 0; index < input.preference.chain.length; index++) {
|
|
136
151
|
const slot = input.preference.chain[index]!;
|
|
137
|
-
const judged = slotUsable(slot.provider, input);
|
|
152
|
+
const judged = slotUsable(slot.provider, input, allowUnmeasured);
|
|
138
153
|
reasons.push(judged.reason);
|
|
139
154
|
if (!judged.usable) {
|
|
140
155
|
if (index === 0) firstUnusable = true;
|
|
@@ -142,16 +157,18 @@ export function decideBudget(input: DecideBudgetInput): BudgetDecision {
|
|
|
142
157
|
}
|
|
143
158
|
const action: BudgetAction = index === 0 || !firstUnusable ? 'proceed' : 'next-provider';
|
|
144
159
|
return {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
160
|
+
decision: {
|
|
161
|
+
action,
|
|
162
|
+
provider: slot.provider,
|
|
163
|
+
...(slot.model !== undefined ? { model: slot.model } : {}),
|
|
164
|
+
reasons,
|
|
165
|
+
},
|
|
166
|
+
...(judged.unmeasured ? { unmeasuredProvider: slot.provider } : {}),
|
|
149
167
|
};
|
|
150
168
|
}
|
|
151
169
|
const onShortfall = input.preference.budgetGate.onShortfall;
|
|
152
170
|
return {
|
|
153
|
-
action: onShortfall === 'wait-reset' ? 'wait-reset' : 'stop',
|
|
154
|
-
reasons,
|
|
171
|
+
decision: { action: onShortfall === 'wait-reset' ? 'wait-reset' : 'stop', reasons },
|
|
155
172
|
};
|
|
156
173
|
}
|
|
157
174
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { GateCallerReason } from './gate-scope.js';
|
|
2
|
+
|
|
3
|
+
export interface GateCallerCandidate {
|
|
4
|
+
readonly file: string;
|
|
5
|
+
readonly reasons: readonly GateCallerReason[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Path segments between `file` and the nearest changed path's directory (0 = same directory). */
|
|
9
|
+
function directoryDistance(file: string, changedPaths: readonly string[]): number {
|
|
10
|
+
const dir = file.split('/').slice(0, -1);
|
|
11
|
+
let best = Infinity;
|
|
12
|
+
for (const changed of changedPaths) {
|
|
13
|
+
const other = changed.split('/').slice(0, -1);
|
|
14
|
+
let shared = 0;
|
|
15
|
+
while (shared < dir.length && shared < other.length && dir[shared] === other[shared]) shared += 1;
|
|
16
|
+
best = Math.min(best, dir.length - shared + other.length - shared);
|
|
17
|
+
}
|
|
18
|
+
return best;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Rank gate caller tests by closeness to the change, nearest first (GATE-CALLERS2 — the cap keeps the head).
|
|
23
|
+
* A `route` candidate names a route on a changed diff line, so it ranks with touched tests (distance 0);
|
|
24
|
+
* otherwise the distance is the shortest reverse-import path from a changed file (direct importer = 1,
|
|
25
|
+
* unreachable = after every reachable candidate). Equal distances prefer the test nearest a changed file
|
|
26
|
+
* in the directory tree, then numeric-aware path order. Inputs are not modified.
|
|
27
|
+
* `importersByFile` maps each imported file to its immediate importers (tests or sources).
|
|
28
|
+
*/
|
|
29
|
+
export function rankGateCallerCandidates<T extends GateCallerCandidate>(
|
|
30
|
+
changedPaths: readonly string[],
|
|
31
|
+
candidates: readonly T[],
|
|
32
|
+
importersByFile: ReadonlyMap<string, readonly string[]>,
|
|
33
|
+
): T[] {
|
|
34
|
+
const distances = new Map<string, number>();
|
|
35
|
+
const queue: string[] = [];
|
|
36
|
+
for (const path of changedPaths) {
|
|
37
|
+
if (distances.has(path)) continue;
|
|
38
|
+
distances.set(path, 0);
|
|
39
|
+
queue.push(path);
|
|
40
|
+
}
|
|
41
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
42
|
+
const source = queue[index]!;
|
|
43
|
+
const nextDistance = distances.get(source)! + 1;
|
|
44
|
+
for (const importer of importersByFile.get(source) ?? []) {
|
|
45
|
+
if (distances.has(importer)) continue;
|
|
46
|
+
distances.set(importer, nextDistance);
|
|
47
|
+
queue.push(importer);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const keys = new Map(candidates.map((candidate) => [candidate, {
|
|
51
|
+
distance: candidate.reasons.includes('route') ? 0 : distances.get(candidate.file) ?? Infinity,
|
|
52
|
+
directory: directoryDistance(candidate.file, changedPaths),
|
|
53
|
+
}]));
|
|
54
|
+
return [...candidates].sort((left, right) => {
|
|
55
|
+
const a = keys.get(left)!;
|
|
56
|
+
const b = keys.get(right)!;
|
|
57
|
+
return (a.distance - b.distance || 0) || (a.directory - b.directory || 0)
|
|
58
|
+
|| left.file.localeCompare(right.file, undefined, { numeric: true });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
import { importerTestsNotInRunSet, type ImporterTestIndex } from './importer-test-index.js';
|
|
32
32
|
import type { RouteConsumerTestIndex } from './route-consumer-test-index.js';
|
|
33
|
+
import { rankGateCallerCandidates } from './gate-candidate-ranking.js';
|
|
33
34
|
|
|
34
35
|
export const GATE_CALLER_TEST_LIMIT = 30;
|
|
35
36
|
export type GateCallerReason = 'import' | 'route';
|
|
@@ -249,11 +250,7 @@ interface GateScopeDecision {
|
|
|
249
250
|
readonly unverified: readonly string[];
|
|
250
251
|
/** elanous 자체가 남긴 런타임 산출물. 미검증 사용자 변경에서 제외하되 경로를 보존한다. */
|
|
251
252
|
readonly elanousRuntimeArtifacts: readonly string[];
|
|
252
|
-
/** ⭐
|
|
253
|
-
* ⚠️ 직전 판본은 이 자리에 `unrunRelatedTests`(=실행집합에 없는 연관 테스트)를 뒀는데,
|
|
254
|
-
* 실행집합이 연관 테스트를 **전부 포함**하도록 바뀌었으므로 그 값은 **정의상 항상 빈 배열**이었다.
|
|
255
|
-
* 빈 배열을 보고 *"안 돈 게 없다"* 고 읽게 되지만 그건 사실 확인이 아니라 계산의 결과였다
|
|
256
|
-
* (관측이 사실 아닌 값을 나르는 형태). ⇒ **실제로 일어난 일**을 센다: 합집합이 구제한 파일들. */
|
|
253
|
+
/** ⭐ 편집되지 않았는데 함께 끌어와 돌린 연관 테스트(`changed-tests` 분기) — 합집합이 실제로 구제한 파일들. */
|
|
257
254
|
readonly pulledInRelatedTests: readonly string[];
|
|
258
255
|
/** 실행 집합에 없는, 변경 소스를 상대 import 하는 테스트. 색인이 주입되지 않으면 null. */
|
|
259
256
|
readonly importerTestsNotRun: {
|
|
@@ -352,8 +349,9 @@ export function resolveGateScope(
|
|
|
352
349
|
}
|
|
353
350
|
}
|
|
354
351
|
}
|
|
355
|
-
|
|
356
|
-
|
|
352
|
+
// GATE-CALLERS2 — the cap keeps the candidates closest to the change, not the alphabetically first.
|
|
353
|
+
const candidates = rankGateCallerCandidates(userChanged, [...callers].map(([file, reasons]) => ({ file, reasons: [...reasons] })),
|
|
354
|
+
importerTestIndex?.testsBySource ?? new Map());
|
|
357
355
|
const callerTests = candidates.slice(0, GATE_CALLER_TEST_LIMIT);
|
|
358
356
|
const callerTestsOverflow = candidates.slice(GATE_CALLER_TEST_LIMIT);
|
|
359
357
|
const runWithCallers = (tests: readonly string[]) => [...new Set([...tests, ...callerTests.map(({ file }) => file)])];
|
|
@@ -364,11 +362,14 @@ export function resolveGateScope(
|
|
|
364
362
|
});
|
|
365
363
|
|
|
366
364
|
if (testFiles.length > 0) {
|
|
365
|
+
// Edited tests ⊕ every related test of the changed sources ⊕ capped callers (restored 10-04: #23467 narrowed this
|
|
366
|
+
// to edited tests only, so a co-located sibling of a changed source stopped running — 0.2.12 gate introduced).
|
|
367
367
|
const allRelatedTests = conventional;
|
|
368
368
|
const runSet = runWithCallers([...testFiles, ...allRelatedTests]);
|
|
369
369
|
// 편집된 테스트만 돌렸다면 빠졌을 것들 — 이 목록이 비어있지 않다는 것은 합집합이 실제로 구제했다는 뜻이다.
|
|
370
370
|
const pulledInRelatedTests = allRelatedTests.filter((f) => !testFiles.includes(f));
|
|
371
|
-
return { ...base, ...withDocumentNonContribution(runSet), testArgs: runSet, unverified: unverifiedFor(runSet), pulledInRelatedTests,
|
|
371
|
+
return { ...base, ...withDocumentNonContribution(runSet), testArgs: runSet, unverified: unverifiedFor(runSet), pulledInRelatedTests,
|
|
372
|
+
importerTestsNotRun: observationFor(runSet), skipTestStep: false, reason: 'changed-tests' };
|
|
372
373
|
}
|
|
373
374
|
if (userChanged.length === 0) {
|
|
374
375
|
// postsync 빈 범위는 기존 no-changes(깨끗함)와 다른 값이다. 일반 호출자는 계속 no-changes를 읽는다.
|
|
@@ -1170,11 +1170,16 @@ export interface HeadlessGoalLoopPtyResult {
|
|
|
1170
1170
|
|
|
1171
1171
|
export function pickImplementRoleForGoal(document: string): ReturnType<typeof resolveRoleLlm> | undefined {
|
|
1172
1172
|
try {
|
|
1173
|
-
const
|
|
1173
|
+
const config = getUserConfig();
|
|
1174
|
+
const role = resolveRoleLlm('implement', { config });
|
|
1175
|
+
if (config.harness?.difficultyPlacement !== true || role.source !== 'default') return role;
|
|
1174
1176
|
const signals = goalDifficultySignals(document);
|
|
1175
1177
|
const level = classifyGoalDifficulty(signals);
|
|
1176
1178
|
const picked = resolveImplementDifficulty(role, level);
|
|
1177
|
-
try {
|
|
1179
|
+
try {
|
|
1180
|
+
debug.log('llm.difficulty', 'pick', { level, tier: picked.tier ?? null, signals });
|
|
1181
|
+
debug.log('harness.role-llm', 'difficulty-placement', { difficulty: level, tier: picked.tier, source: role.source });
|
|
1182
|
+
} catch { /* observation is fail-soft */ }
|
|
1178
1183
|
return picked;
|
|
1179
1184
|
} catch { return undefined; }
|
|
1180
1185
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { NEXT_MD_PATH, resolveNextMdConflict } from '../release-loop/next-md-merge.js';
|
|
6
|
+
import { getDefaultLogStore } from '../mss/logging/log-store.js';
|
|
6
7
|
|
|
7
8
|
type Command = (bin: string, args: readonly string[], cwd: string) => { status: number | null; stdout: string; stderr: string };
|
|
8
9
|
export type L8MergeQueueInput = { number: number; cwd: string; matchHeadCommit: string };
|
|
@@ -13,6 +14,161 @@ export type L8MergeQueueDeps = {
|
|
|
13
14
|
acquire?: (cwd: string) => Promise<() => void>;
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
export type L8ShadowVerdict = { number: number; head: string; base?: string; verdict: 'pass' | 'conflict' | 'fail'; detail?: string; tests: string[]; at: string };
|
|
18
|
+
export type L8ShadowQueueState = { pending: { number: number; head: string }[]; verdicts: L8ShadowVerdict[] };
|
|
19
|
+
export type L8ShadowQueueDeps = L8MergeQueueDeps & { observe?: (verdict: L8ShadowVerdict) => void | Promise<void> };
|
|
20
|
+
|
|
21
|
+
function shadowCommand(execute: Command, bin: string, args: string[], cwd: string): string {
|
|
22
|
+
const result = execute(bin, args, cwd);
|
|
23
|
+
if (result.status !== 0) throw new Error(`${bin} ${args.join(' ')}: ${(result.stderr || result.stdout || `exit ${result.status}`).trim().slice(0, 500)}`);
|
|
24
|
+
return args.includes('-z') ? result.stdout : result.stdout.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shadowQueuePath(cwd: string, execute: Command): string {
|
|
28
|
+
const dir = shadowCommand(execute, 'git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);
|
|
29
|
+
if (!dir) throw new Error('merge queue common git directory unavailable');
|
|
30
|
+
return join(dir, 'elanous-l8-shadow-queue.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readShadowQueue(path: string): L8ShadowQueueState {
|
|
34
|
+
if (!existsSync(path)) return { pending: [], verdicts: [] };
|
|
35
|
+
const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
36
|
+
if (!value || typeof value !== 'object' || !('pending' in value) || !Array.isArray(value.pending)
|
|
37
|
+
|| !value.pending.every((p: { number?: number; head?: string }) => Number.isSafeInteger(p?.number) && p.number! > 0 && typeof p.head === 'string' && /^[0-9a-f]{40}$/i.test(p.head))
|
|
38
|
+
|| !('verdicts' in value) || !Array.isArray(value.verdicts)) throw new Error('invalid shadow queue ledger');
|
|
39
|
+
return value as L8ShadowQueueState;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeShadowQueue(path: string, state: L8ShadowQueueState): void {
|
|
43
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(temp, JSON.stringify(state, null, 2) + '\n', { flag: 'wx' });
|
|
46
|
+
renameSync(temp, path);
|
|
47
|
+
} finally { rmSync(temp, { force: true }); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function withShadowQueue<T>(cwd: string, deps: L8ShadowQueueDeps, fn: (state: L8ShadowQueueState, path: string) => Promise<T>): Promise<T> {
|
|
51
|
+
const release = await (deps.acquire ?? acquireL8MergeQueue)(cwd);
|
|
52
|
+
try {
|
|
53
|
+
const path = shadowQueuePath(cwd, deps.command ?? command);
|
|
54
|
+
return await fn(readShadowQueue(path), path);
|
|
55
|
+
} finally { release(); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function shadowPrNumber(raw: string): number {
|
|
59
|
+
const text = raw.replace(/^#/, '');
|
|
60
|
+
const number = Number(text);
|
|
61
|
+
if (!/^[1-9]\d*$/.test(text) || !Number.isSafeInteger(number)) throw new Error('expected a positive PR number');
|
|
62
|
+
return number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function statusL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowQueueState> {
|
|
66
|
+
return withShadowQueue(cwd, deps, async (state) => state);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function enqueueL8ShadowQueue(cwd: string, number: number, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowQueueState> {
|
|
70
|
+
if (!Number.isSafeInteger(number) || number <= 0) throw new Error('expected a positive PR number');
|
|
71
|
+
return withShadowQueue(cwd, deps, async (state, path) => {
|
|
72
|
+
const execute = deps.command ?? command;
|
|
73
|
+
const view = JSON.parse(shadowCommand(execute, 'gh', ['pr', 'view', String(number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'], cwd)) as {
|
|
74
|
+
headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
|
|
75
|
+
};
|
|
76
|
+
if (!view.headRefOid || !/^[0-9a-f]{40}$/i.test(view.headRefOid) || view.baseRefName !== 'main'
|
|
77
|
+
|| view.state !== 'OPEN' || view.isDraft !== false || view.isCrossRepository !== false) throw new Error('PR is not an open, ready, same-repository main PR');
|
|
78
|
+
if (state.pending.some((p) => p.number === number)) throw new Error(`PR #${number} already queued`);
|
|
79
|
+
state.pending.push({ number, head: view.headRefOid });
|
|
80
|
+
writeShadowQueue(path, state);
|
|
81
|
+
return state;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Evaluate one pinned queue head against freshly fetched main. Never push or call a merge API. */
|
|
86
|
+
export async function runL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowVerdict | null> {
|
|
87
|
+
return withShadowQueue(cwd, deps, async (state, path) => {
|
|
88
|
+
const item = state.pending[0];
|
|
89
|
+
if (!item) return null;
|
|
90
|
+
const execute = deps.command ?? command;
|
|
91
|
+
const run = (bin: string, args: string[], where = cwd) => shadowCommand(execute, bin, args, where);
|
|
92
|
+
const verdict: L8ShadowVerdict = { number: item.number, head: item.head, verdict: 'fail', tests: [], at: new Date().toISOString() };
|
|
93
|
+
const view = JSON.parse(run('gh', ['pr', 'view', String(item.number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'])) as {
|
|
94
|
+
headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
|
|
95
|
+
};
|
|
96
|
+
if (view.headRefOid !== item.head || view.baseRefName !== 'main' || view.state !== 'OPEN'
|
|
97
|
+
|| view.isDraft !== false || view.isCrossRepository !== false) throw new Error('queued PR changed or is not ready');
|
|
98
|
+
run('git', ['fetch', 'origin', 'refs/heads/main']);
|
|
99
|
+
const base = run('git', ['rev-parse', 'FETCH_HEAD']);
|
|
100
|
+
if (!/^[0-9a-f]{40}$/i.test(base)) throw new Error('main commit unavailable');
|
|
101
|
+
verdict.base = base;
|
|
102
|
+
run('git', ['fetch', 'origin', `refs/pull/${item.number}/head`]);
|
|
103
|
+
if (run('git', ['rev-parse', 'FETCH_HEAD']) !== item.head) throw new Error('fetched PR head changed');
|
|
104
|
+
let temp: string | undefined;
|
|
105
|
+
let attached = false;
|
|
106
|
+
try {
|
|
107
|
+
temp = mkdtempSync(join(tmpdir(), 'elanous-l8-shadow-'));
|
|
108
|
+
run('git', ['worktree', 'add', '--detach', temp, base]);
|
|
109
|
+
attached = true;
|
|
110
|
+
const fork = run('git', ['merge-base', base, item.head], temp);
|
|
111
|
+
const changed = run('git', ['diff', '--name-only', '-z', fork, item.head], temp).split('\0').filter(Boolean);
|
|
112
|
+
if (!changed.length) throw new Error('PR changed-file paths unavailable');
|
|
113
|
+
const diff = run('git', ['diff', '--name-only', '-z', '--diff-filter=ACMRT', fork, item.head], temp);
|
|
114
|
+
const files = diff.split('\0').filter(Boolean);
|
|
115
|
+
if (changed.some((file) => file.startsWith('-') || file.startsWith('/') || file.split('/').includes('..') || file.includes('\\'))) throw new Error('unsafe changed-file path');
|
|
116
|
+
verdict.tests = files.filter((file) => /(?:^|\/)[^/]+\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/.test(file));
|
|
117
|
+
const merge = execute('git', ['-c', 'user.name=elanous shadow', '-c', 'user.email=shadow@localhost', 'merge', '--no-ff', '--no-edit', item.head], temp);
|
|
118
|
+
if (merge.status !== 0) {
|
|
119
|
+
const unresolved = run('git', ['diff', '--name-only', '--diff-filter=U'], temp);
|
|
120
|
+
if (!unresolved) throw new Error(`git merge failed: ${merge.stderr.trim() || `exit ${merge.status}`}`);
|
|
121
|
+
verdict.verdict = 'conflict';
|
|
122
|
+
verdict.detail = unresolved;
|
|
123
|
+
} else {
|
|
124
|
+
if (verdict.tests.length) {
|
|
125
|
+
if (verdict.tests.some((file) => !existsSync(join(temp!, file)))) throw new Error('changed test path unavailable');
|
|
126
|
+
run('bun', ['install', '--frozen-lockfile'], temp);
|
|
127
|
+
const result = execute('bun', ['run', 'test:deterministic', ...verdict.tests], temp);
|
|
128
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
129
|
+
const counts = /\b(\d+) pass\b[\s\S]*?\b(\d+) fail\b[\s\S]*?Ran ([1-9]\d*) tests? across ([1-9]\d*) files?/.exec(output);
|
|
130
|
+
if (!counts || Number(counts[1]) + Number(counts[2]) !== Number(counts[3])
|
|
131
|
+
|| Number(counts[4]) !== verdict.tests.length || (result.status !== 0 && Number(counts[2]) === 0)) {
|
|
132
|
+
throw new Error(`changed tests unmeasured: ${output.slice(-400)}`);
|
|
133
|
+
}
|
|
134
|
+
if (Number(counts[2]) > 0 || Number(counts[1]) === 0) {
|
|
135
|
+
verdict.detail = `changed tests failed: ${output.slice(-400)}`;
|
|
136
|
+
} else {
|
|
137
|
+
verdict.verdict = 'pass';
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
verdict.verdict = 'pass';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
try {
|
|
145
|
+
if (attached && temp) run('git', ['worktree', 'remove', '--force', temp]);
|
|
146
|
+
} finally {
|
|
147
|
+
if (temp) rmSync(temp, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (verdict.verdict === 'pass') {
|
|
151
|
+
run('git', ['fetch', 'origin', 'refs/heads/main']);
|
|
152
|
+
if (run('git', ['rev-parse', 'FETCH_HEAD']) !== verdict.base) throw new Error('main advanced during shadow verification');
|
|
153
|
+
const current = JSON.parse(run('gh', ['pr', 'view', String(item.number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'])) as {
|
|
154
|
+
headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
|
|
155
|
+
};
|
|
156
|
+
if (current.headRefOid !== item.head || current.baseRefName !== 'main' || current.state !== 'OPEN'
|
|
157
|
+
|| current.isDraft !== false || current.isCrossRepository !== false) throw new Error('PR changed during shadow verification');
|
|
158
|
+
}
|
|
159
|
+
// A rejected insert leaves the pinned item queued for a later evaluation.
|
|
160
|
+
await (deps.observe ?? ((entry) => {
|
|
161
|
+
const store = getDefaultLogStore();
|
|
162
|
+
if (!store) throw new Error('merge-queue observation store unavailable');
|
|
163
|
+
store.insertBatch([{ rec: { ts: new Date().toISOString(), category: 'merge-queue', event: 'shadow-verdict', data: entry }, surface: 'merge-queue' }]);
|
|
164
|
+
}))(verdict);
|
|
165
|
+
state.pending.shift();
|
|
166
|
+
state.verdicts.push(verdict);
|
|
167
|
+
writeShadowQueue(path, state);
|
|
168
|
+
return verdict;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
16
172
|
const command: Command = (bin, args, cwd) => {
|
|
17
173
|
const r = spawnSync(bin, [...args], { cwd, encoding: 'utf8', timeout: 600_000, maxBuffer: 32 * 1024 * 1024 });
|
|
18
174
|
return { status: r.status, stdout: r.stdout ?? '', stderr: (r.stderr ?? '') + (r.error?.message ?? '') };
|
|
@@ -63,6 +63,7 @@ import { GoalRunStore, insertGoalRunRecord, type GoalPriorRuns } from './goal-ru
|
|
|
63
63
|
import { writeGoalRunRecordFragment } from './goal-execution-records.js';
|
|
64
64
|
import { getUserConfig } from '../user-config.js';
|
|
65
65
|
import { appendRunLedgerEntry, loadRunLedger, parseRunShardIdentity, queryRunChain, type RunChainShardSibling, type RunLedgerEntry, type RunLedgerWriter, type RunOriginData, type RunShardIdentity } from './run-ledger.js';
|
|
66
|
+
import { emitSessionEvent, type SessionEventInput } from '../context-bus/session-events.js';
|
|
66
67
|
import { decideLineageSupersede, lineageSupersedeCloseComment, type LineageSupersedeOpenDraft } from './lineage-supersede.js';
|
|
67
68
|
import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
68
69
|
import { enqueueControlMemo, readSoftStopRequestStatus, type SoftStopRequestRead } from '../harness/control-inbox.js';
|
|
@@ -874,6 +875,8 @@ export type ImplementTerminalStatus = {
|
|
|
874
875
|
export interface SelfImplementSeams {
|
|
875
876
|
/** Run-observer ledger sink. Tests inject an in-memory sink to avoid touching user state. */
|
|
876
877
|
writeRunLedger?: RunLedgerWriter;
|
|
878
|
+
/** Optional context-journal sink for isolated runs. */
|
|
879
|
+
emitContextEvent?: (input: SessionEventInput) => void;
|
|
877
880
|
/** Registers this run as an ephemeral loop-agent asset; failures are observed but never affect execution. */
|
|
878
881
|
registerLoopAgent?: (input: LoopAgentInput) => void;
|
|
879
882
|
/** Wraps implementation and gate promises with their effective wall-clock timeout; tests observe the actual stage budgets. */
|
|
@@ -3384,6 +3387,16 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
|
|
|
3384
3387
|
detail: 'observe-only: SelfImplement execution skipped before child boot',
|
|
3385
3388
|
};
|
|
3386
3389
|
}
|
|
3390
|
+
const emitContext = (kind: 'task-claimed' | 'task-done', text: string, pr?: number): void => {
|
|
3391
|
+
try {
|
|
3392
|
+
(opts.seams.emitContextEvent ?? emitSessionEvent)({ kind, seat: 'TC', text,
|
|
3393
|
+
ref: `runId=${runId}${pr === undefined ? '' : ` PR=#${pr}`}` });
|
|
3394
|
+
} catch (error) {
|
|
3395
|
+
try { logRunAwareFailSoft(runId, 'context-event-failed', { kind, error: safeErrorDescription(error) }); }
|
|
3396
|
+
catch { /* context publication never changes the run result */ }
|
|
3397
|
+
}
|
|
3398
|
+
};
|
|
3399
|
+
emitContext('task-claimed', `Self-implement claimed: ${opts.feature.split(/\r?\n/, 1)[0]}`);
|
|
3387
3400
|
const parentHarnessSpaceId = getHarnessSpace()?.id || undefined;
|
|
3388
3401
|
const configRoot = getElanousConfigDir() || undefined;
|
|
3389
3402
|
const stateRoot = elanousStateRoot() || undefined;
|
|
@@ -4091,6 +4104,8 @@ export async function runSelfImplement(opts: SelfImplementOptions): Promise<Self
|
|
|
4091
4104
|
}, { level: 'warn' });
|
|
4092
4105
|
} catch { /* fail-soft — 종결 기록 실패가 원래 예외를 덮으면 안 된다 */ }
|
|
4093
4106
|
}
|
|
4107
|
+
emitContext('task-done', `Self-implement ${terminalResult?.stage ?? 'crashed'}: ${opts.feature.split(/\r?\n/, 1)[0]}`,
|
|
4108
|
+
terminalResult?.prNumber ?? capturedPrNumber);
|
|
4094
4109
|
finalizeTraversal();
|
|
4095
4110
|
try { unregisterProviderErrorSink(); } catch { /* fail-soft — cleanup must not change the terminal result */ }
|
|
4096
4111
|
}
|