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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { Database } from 'bun:sqlite';
|
|
5
|
+
import { debug } from '../debug/log.js';
|
|
6
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
7
|
+
import * as registry from './registry.js';
|
|
8
|
+
import { cronMatches } from '../domains/cron-match.js';
|
|
9
|
+
import { resolveTimeZone } from '../time/format.js';
|
|
10
|
+
import type { LoopEntry, LoopRun } from './registry.js';
|
|
11
|
+
|
|
12
|
+
type LoopState = 'off' | 'unregistered' | 'failing' | 'late' | 'unknown' | 'alive';
|
|
13
|
+
export type LoopScope = 'graph-only' | 'registry';
|
|
14
|
+
export interface CheckEntry {
|
|
15
|
+
id: string;
|
|
16
|
+
owner?: string;
|
|
17
|
+
mode?: string;
|
|
18
|
+
expectEveryMinutes?: number;
|
|
19
|
+
/** The scheduled fire before the latest one (cron loops). A run older than this missed two scheduled fires. */
|
|
20
|
+
dueAt?: string;
|
|
21
|
+
lastRunAt?: string;
|
|
22
|
+
lastStatus?: string;
|
|
23
|
+
recentStatuses?: string[];
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
registered: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface CheckResult extends CheckEntry {
|
|
28
|
+
state: LoopState;
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
export const LOOP_STATES = ['alive', 'late', 'failing', 'off', 'unregistered', 'unknown'] as const;
|
|
32
|
+
export type LoopCounts = Record<LoopState, number>;
|
|
33
|
+
|
|
34
|
+
export function checkLoops(entries: CheckEntry[], now: Date, opts: { failureStatuses?: readonly string[] } = {}): CheckResult[] {
|
|
35
|
+
const failures = opts.failureStatuses ?? ['failed'];
|
|
36
|
+
return entries.map(entry => {
|
|
37
|
+
let state: LoopState;
|
|
38
|
+
let reason: string;
|
|
39
|
+
const age = entry.lastRunAt ? now.getTime() - Date.parse(entry.lastRunAt) : NaN;
|
|
40
|
+
if (!entry.enabled) { state = 'off'; reason = 'disabled'; }
|
|
41
|
+
else if (!entry.registered) { state = 'unregistered'; reason = 'not registered'; }
|
|
42
|
+
else if (entry.recentStatuses?.length && entry.recentStatuses.length >= 3
|
|
43
|
+
&& entry.recentStatuses.slice(0, 3).every(status => failures.includes(status))) {
|
|
44
|
+
state = 'failing'; reason = 'last 3 runs failed';
|
|
45
|
+
} else if (Number.isFinite(age) && entry.dueAt && Number.isFinite(Date.parse(entry.dueAt))) {
|
|
46
|
+
// Judge against the real schedule, not a fixed interval: overnight or weekend gaps are not lateness.
|
|
47
|
+
if (Date.parse(entry.lastRunAt!) < Date.parse(entry.dueAt)) { state = 'late'; reason = `missed the scheduled runs since ${entry.dueAt}`; }
|
|
48
|
+
else { state = 'alive'; reason = 'ran at or after the previous scheduled fire'; }
|
|
49
|
+
} else if (Number.isFinite(age) && entry.expectEveryMinutes !== undefined
|
|
50
|
+
&& entry.expectEveryMinutes > 0 && age > entry.expectEveryMinutes * 2 * 60_000) {
|
|
51
|
+
state = 'late'; reason = `last run exceeds twice the expected ${entry.expectEveryMinutes}m interval`;
|
|
52
|
+
} else if (!entry.lastRunAt || !Number.isFinite(age)) { state = 'unknown'; reason = 'no valid run recorded'; }
|
|
53
|
+
else if (entry.expectEveryMinutes === undefined || !Number.isFinite(entry.expectEveryMinutes) || entry.expectEveryMinutes <= 0) {
|
|
54
|
+
state = 'unknown'; reason = 'expected interval unknown';
|
|
55
|
+
} else { state = 'alive'; reason = 'within expected interval'; }
|
|
56
|
+
return { ...entry, state, reason };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function countLoopStates(results: CheckResult[]): LoopCounts {
|
|
61
|
+
const counts: LoopCounts = { alive: 0, late: 0, failing: 0, off: 0, unregistered: 0, unknown: 0 };
|
|
62
|
+
for (const result of results) counts[result.state]++;
|
|
63
|
+
return counts;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Latest cron fire at or before `at` (minute resolution, scans back up to one year). */
|
|
67
|
+
export function previousLoopFire(cron: string, at: Date, timeZone: string = resolveTimeZone().timeZone): string | null {
|
|
68
|
+
if (cron.trim().split(/\s+/).length !== 5) return null;
|
|
69
|
+
let minute = Math.floor(at.getTime() / 60000) * 60000;
|
|
70
|
+
for (let i = 0; i < 527040; i++, minute -= 60000) {
|
|
71
|
+
if (cronMatches(cron, new Date(minute), { timeZone })) return new Date(minute).toISOString();
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The fire before the latest scheduled fire — a run older than this has missed two scheduled fires. */
|
|
77
|
+
export function dueBefore(cron: string, now: Date, timeZone?: string): string | null {
|
|
78
|
+
const latest = previousLoopFire(cron, now, timeZone);
|
|
79
|
+
return latest ? previousLoopFire(cron, new Date(Date.parse(latest) - 60000), timeZone) : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RegistryAdapterDeps {
|
|
83
|
+
listLoops?: () => LoopEntry[];
|
|
84
|
+
listAllLoops?: () => CheckEntry[];
|
|
85
|
+
statusForLoop?: (id: string) => { recentRuns: Pick<LoopRun, 'status'>[] };
|
|
86
|
+
now?: Date;
|
|
87
|
+
timeZone?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function entriesFromRegistry(deps: RegistryAdapterDeps = {}): { entries: CheckEntry[]; scope: LoopScope } {
|
|
91
|
+
// The registry integration is optional until LOOP-REG1 lands. Do not import a missing named export.
|
|
92
|
+
const all = deps.listAllLoops ?? (registry as typeof registry & { listAllLoops?: () => CheckEntry[] }).listAllLoops;
|
|
93
|
+
if (typeof all === 'function') return { entries: all(), scope: 'registry' };
|
|
94
|
+
const now = deps.now ?? new Date();
|
|
95
|
+
return { scope: 'graph-only', entries: (deps.listLoops ?? registry.listLoops)().map(loop => {
|
|
96
|
+
const first = loop.trigger.cron ? registry.nextLoopFire(loop.trigger.cron, now) : null;
|
|
97
|
+
const second = first && loop.trigger.cron ? registry.nextLoopFire(loop.trigger.cron, new Date(first)) : null;
|
|
98
|
+
const interval = first && second ? (Date.parse(second) - Date.parse(first)) / 60_000 : undefined;
|
|
99
|
+
const statuses = loop.lastRun
|
|
100
|
+
? deps.statusForLoop ? deps.statusForLoop(loop.id).recentRuns.map(run => run.status)
|
|
101
|
+
: deps.listLoops ? [loop.lastRun.status]
|
|
102
|
+
: registry.loopStatus(loop.id).recentRuns.map(run => run.status)
|
|
103
|
+
: [];
|
|
104
|
+
const dueAt = loop.trigger.cron ? dueBefore(loop.trigger.cron, now, deps.timeZone) : null;
|
|
105
|
+
return { id: loop.id, enabled: loop.enabled, registered: true,
|
|
106
|
+
...(interval !== undefined ? { expectEveryMinutes: interval } : {}),
|
|
107
|
+
...(dueAt ? { dueAt } : {}),
|
|
108
|
+
...(loop.lastRun ? { lastRunAt: loop.lastRun.at, lastStatus: loop.lastRun.status } : {}),
|
|
109
|
+
recentStatuses: statuses };
|
|
110
|
+
}) };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CheckerDeps {
|
|
114
|
+
root?: string;
|
|
115
|
+
now?: Date;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Appends only new daily keys to the seat request journal; never rewrites existing lines. */
|
|
119
|
+
export function notifyOwners(results: CheckResult[], deps: CheckerDeps = {}): number {
|
|
120
|
+
const eligible = results.filter(result => result.owner && (result.state === 'late' || result.state === 'failing'));
|
|
121
|
+
if (!eligible.length) return 0;
|
|
122
|
+
const now = deps.now ?? new Date();
|
|
123
|
+
const path = join(deps.root ?? effectiveInstanceRoot(), 'seat-requests', 'requests.jsonl');
|
|
124
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
125
|
+
// SQLite's OS locks are released when a process dies, including after SIGKILL.
|
|
126
|
+
// The journal itself remains append-only; this database only serializes check + append.
|
|
127
|
+
const lock = new Database(`${path}.loop-checker.lock.sqlite`);
|
|
128
|
+
try {
|
|
129
|
+
lock.exec('PRAGMA busy_timeout = 10000');
|
|
130
|
+
lock.exec('BEGIN EXCLUSIVE');
|
|
131
|
+
try {
|
|
132
|
+
const existing = new Set<string>();
|
|
133
|
+
if (existsSync(path)) {
|
|
134
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
135
|
+
if (line.trim()) {
|
|
136
|
+
const row = JSON.parse(line) as { key?: string };
|
|
137
|
+
if (row.key) existing.add(row.key);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let added = 0;
|
|
142
|
+
for (const result of eligible) {
|
|
143
|
+
const key = `loopcheck:${result.id}:${now.toISOString().slice(0, 10)}:${result.state}`;
|
|
144
|
+
if (existing.has(key)) continue;
|
|
145
|
+
const row = { key, receiptId: key, seat: result.owner, text: `${result.id} ${result.state} — 마지막 ${result.lastRunAt ?? '없음'} · ${result.reason}`,
|
|
146
|
+
status: 'queued', queuedAt: now.toISOString(), source: 'loop-checker' };
|
|
147
|
+
appendFileSync(path, JSON.stringify(row) + '\n');
|
|
148
|
+
existing.add(key);
|
|
149
|
+
added++;
|
|
150
|
+
}
|
|
151
|
+
lock.exec('COMMIT');
|
|
152
|
+
return added;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
lock.exec('ROLLBACK');
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
} finally { lock.close(); }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Persist the previous comparison only for explicit all-status checks. Notification is separate. */
|
|
161
|
+
export function observeLoopCheck(results: CheckResult[], scope: LoopScope, deps: CheckerDeps = {}): LoopCounts {
|
|
162
|
+
const path = join(deps.root ?? effectiveInstanceRoot(), 'loop', 'checker', 'last.json');
|
|
163
|
+
let previous: Record<string, LoopState> = {};
|
|
164
|
+
try { previous = JSON.parse(readFileSync(path, 'utf8')) as Record<string, LoopState>; }
|
|
165
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
166
|
+
const current: Record<string, LoopState> = {};
|
|
167
|
+
for (const { id, state } of results) {
|
|
168
|
+
current[id] = state;
|
|
169
|
+
if (previous[id] && previous[id] !== state) debug.log('loop.checker', 'state-change', { id, from: previous[id], to: state });
|
|
170
|
+
}
|
|
171
|
+
const counts = countLoopStates(results);
|
|
172
|
+
debug.log('loop.checker', 'checked', { counts, scope });
|
|
173
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
174
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
175
|
+
try { writeFileSync(tmp, JSON.stringify(current)); renameSync(tmp, path); }
|
|
176
|
+
catch (error) { if (existsSync(tmp)) unlinkSync(tmp); throw error; }
|
|
177
|
+
return counts;
|
|
178
|
+
}
|
package/src/loops/loop-cli.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { writeStdoutJson } from '../cli/stdout-json.js';
|
|
3
3
|
import { listLoops, loopStatus, runLoop, setLoopEnabled } from './registry.js';
|
|
4
|
+
import { checkLoops, countLoopStates, entriesFromRegistry, LOOP_STATES, notifyOwners, observeLoopCheck, type RegistryAdapterDeps } from './checker.js';
|
|
5
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
6
|
+
import { verifyLoopPackage } from './package/verify.js';
|
|
4
7
|
|
|
5
8
|
async function output(value: unknown, json: boolean): Promise<void> {
|
|
6
9
|
if (json) await writeStdoutJson(JSON.stringify(value) + '\n');
|
|
@@ -14,12 +17,31 @@ function handle(action: () => Promise<void> | void): Promise<void> {
|
|
|
14
17
|
});
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
export function registerLoopCommands(program: Command): void {
|
|
20
|
+
export function registerLoopCommands(program: Command, checkerDeps: RegistryAdapterDeps & { root?: string } = {}): void {
|
|
18
21
|
const loop = program.command('loop').description('Inspect and control graph-backed loop agents');
|
|
19
22
|
loop.command('list').option('--json', 'Print JSON')
|
|
20
23
|
.action((opts: { json?: boolean }) => handle(() => output(listLoops(), !!opts.json)));
|
|
21
|
-
loop.command('status
|
|
22
|
-
.
|
|
24
|
+
loop.command('status [id]').option('--all', 'Check all registered loops')
|
|
25
|
+
.option('--json', 'Print JSON').option('--notify', 'Queue late/failing loops for their owners')
|
|
26
|
+
.action((id: string | undefined, opts: { all?: boolean; json?: boolean; notify?: boolean }) => handle(async () => {
|
|
27
|
+
if (!opts.all) {
|
|
28
|
+
if (!id) throw new Error('loop status requires an id or --all');
|
|
29
|
+
await output(loopStatus(id), !!opts.json);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (id) throw new Error('loop status --all does not take an id');
|
|
33
|
+
const now = checkerDeps.now ?? new Date();
|
|
34
|
+
const root = checkerDeps.root ?? effectiveInstanceRoot();
|
|
35
|
+
const { entries, scope } = entriesFromRegistry({ ...checkerDeps, now });
|
|
36
|
+
const results = checkLoops(entries, now);
|
|
37
|
+
const counts = countLoopStates(results);
|
|
38
|
+
observeLoopCheck(results, scope, { root, now });
|
|
39
|
+
if (opts.notify) notifyOwners(results, { root, now });
|
|
40
|
+
if (opts.json) { await output({ results, counts, scope }, true); return; }
|
|
41
|
+
console.log('id\tstate\tlast run\texpected interval\towner\treason');
|
|
42
|
+
for (const result of results) console.log(`${result.id}\t${result.state}\t${result.lastRunAt ?? '—'}\t${result.expectEveryMinutes === undefined ? '—' : `${result.expectEveryMinutes}m`}\t${result.owner ?? '—'}\t${result.reason}`);
|
|
43
|
+
console.log(`${LOOP_STATES.map(state => `${state} ${counts[state]}`).join(' · ')} · scope ${scope}`);
|
|
44
|
+
}));
|
|
23
45
|
for (const [name, enabled] of [['start', true], ['stop', false]] as const) {
|
|
24
46
|
loop.command(`${name} <id>`).option('--yes', 'Apply the crontab change (otherwise preview only)')
|
|
25
47
|
.option('--json', 'Print JSON')
|
|
@@ -34,4 +56,16 @@ export function registerLoopCommands(program: Command): void {
|
|
|
34
56
|
await output(state, !!opts.json);
|
|
35
57
|
if (state.status !== 'done' && state.status !== 'awaiting-approval') process.exitCode = 1;
|
|
36
58
|
}));
|
|
59
|
+
loop.command('package').description('Inspect a loop package without installing it')
|
|
60
|
+
.command('verify <dir>').option('--json', 'Print JSON')
|
|
61
|
+
.action((dir: string, opts: { json?: boolean }) => handle(async () => {
|
|
62
|
+
const result = verifyLoopPackage(dir);
|
|
63
|
+
if (opts.json) await output(result, true);
|
|
64
|
+
else {
|
|
65
|
+
if (result.ok) console.log('OK');
|
|
66
|
+
else for (const error of result.errors) console.log(error);
|
|
67
|
+
console.log(`loop-package ${String(result.merged?.id ?? '?')} ${String(result.merged?.version ?? '?')} ${result.ok ? 'ok' : 'refused'} errors=${result.errors.length}`);
|
|
68
|
+
}
|
|
69
|
+
if (!result.ok) process.exitCode = 1;
|
|
70
|
+
}));
|
|
37
71
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { debug } from '../debug/log.js';
|
|
2
|
+
|
|
3
|
+
/** Event vocabulary of the loop-agent manifest observability contract. */
|
|
4
|
+
export const LOOP_EVENTS = [
|
|
5
|
+
'tick', 'posture-change', 'resolution-change', 'grounding', 'heartbeat',
|
|
6
|
+
'absent', 'exchange', 'spawn', 'reap', 'promote', 'hitl',
|
|
7
|
+
] as const;
|
|
8
|
+
|
|
9
|
+
const LOOP_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
10
|
+
const REQUIRED_CONTEXT = ['runId', 'profile', 'reason', 'sourceRef'] as const;
|
|
11
|
+
|
|
12
|
+
function validId(id: unknown): id is string {
|
|
13
|
+
return typeof id === 'string' && LOOP_ID.test(id);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function reject(reason: 'invalid-loop-id' | 'invalid-event' | 'invalid-data'): void {
|
|
17
|
+
// Never include untrusted identifiers, event names or payloads in a log category.
|
|
18
|
+
debug.log('loop.observe', 'rejected', { reason });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Category matches observability.category in the loop-agent manifest. */
|
|
22
|
+
export function loopCategory(id: string): string {
|
|
23
|
+
if (!validId(id)) {
|
|
24
|
+
reject('invalid-loop-id');
|
|
25
|
+
throw new Error('invalid loop id');
|
|
26
|
+
}
|
|
27
|
+
return `loop.${id}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Emit an RFC loop event, retaining absent context as a measurable list. */
|
|
31
|
+
export function loopEvent(loopId: string, event: string, data: Record<string, unknown>): void {
|
|
32
|
+
if (!validId(loopId)) {
|
|
33
|
+
reject('invalid-loop-id');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (typeof event !== 'string' || !LOOP_EVENTS.some(name => name === event)) {
|
|
37
|
+
reject('invalid-event');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)
|
|
41
|
+
|| Object.getPrototypeOf(data) !== Object.prototype && Object.getPrototypeOf(data) !== null
|
|
42
|
+
|| (Object.hasOwn(data, 'loopId') && data.loopId !== loopId)) {
|
|
43
|
+
reject('invalid-data');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const missingRequired = REQUIRED_CONTEXT.filter(field =>
|
|
47
|
+
typeof data[field] !== 'string' || !(data[field] as string).trim());
|
|
48
|
+
debug.log(`loop.${loopId}`, event, { ...data, loopId, missingRequired });
|
|
49
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, isAbsolute, resolve } from 'node:path';
|
|
4
|
+
import { debug } from '../../debug/log.js';
|
|
5
|
+
import { defaultListHarnessProcesses, type HarnessProcessListObservation } from '../../harness/harness-cli-command.js';
|
|
6
|
+
import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorLoopConfig, type OrchestratorSeat } from '../../user-config.js';
|
|
7
|
+
import { seatOfTree, TRAFFIC_SEATS } from './traffic.js';
|
|
8
|
+
|
|
9
|
+
export type WorkItem = { kind: 'pr' | 'goal'; ref: string; seat: OrchestratorSeat | null; files: string[]; cells: string[]; unreadable?: true };
|
|
10
|
+
export type WorkRef = Pick<WorkItem, 'kind' | 'ref' | 'seat'>;
|
|
11
|
+
export type Overlap = ({ type: 'file'; path: string } | { type: 'cell'; cell: string }) & { refs: WorkRef[]; crossSeat: boolean };
|
|
12
|
+
|
|
13
|
+
type OpenPr = { number: number; title: string; body?: string | null; headRefName: string; files: Array<{ path: string }>; isDraft: boolean };
|
|
14
|
+
export interface CollectWorkDeps {
|
|
15
|
+
runGh?: (args: string[]) => string;
|
|
16
|
+
listProcesses?: () => HarnessProcessListObservation;
|
|
17
|
+
readGoal?: (path: string) => string;
|
|
18
|
+
config?: Pick<OrchestratorLoopConfig, 'seatTrees'>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const repoRoot = resolve(import.meta.dir, '../../..');
|
|
22
|
+
const defaultRunGh = (args: string[]): string => execFileSync('bun', ['bin/elanous.mjs', '--test', 'gh', ...args], {
|
|
23
|
+
cwd: repoRoot, encoding: 'utf8', timeout: 60_000, maxBuffer: 20 * 1024 * 1024,
|
|
24
|
+
stdio: ['ignore', 'pipe', 'pipe'], env: process.env,
|
|
25
|
+
});
|
|
26
|
+
const cellsOf = (text: string): string[] => [...new Set([...text.matchAll(/\b0\.2\.\d+\s+칸\s+([A-Za-z][A-Za-z0-9-]*)/g)].map(match => match[1]!))];
|
|
27
|
+
const seatOfPr = (title: string): OrchestratorSeat | null => {
|
|
28
|
+
const seat = /^\s*\[(OP|TC|MK|UX)\]/.exec(title)?.[1];
|
|
29
|
+
return TRAFFIC_SEATS.find(id => id === seat) ?? null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function goalPath(command: string): string | null {
|
|
33
|
+
// ps supplies an argv-shaped command, not a shell to execute. Only inspect explicit goal-file arguments.
|
|
34
|
+
const argv = [...command.matchAll(/"([^"]*)"|'([^']*)'|([^\s]+)/g)].map(match => match[1] ?? match[2] ?? match[3]!);
|
|
35
|
+
const entry = argv.findIndex(arg => /(?:^|\/)elanous\.mjs$/.test(arg));
|
|
36
|
+
if (entry < 0) return null;
|
|
37
|
+
let commandAt = entry + 1;
|
|
38
|
+
while (argv[commandAt] === '--test' || argv[commandAt] === '--config-dir') {
|
|
39
|
+
commandAt += argv[commandAt] === '--config-dir' ? 2 : 1;
|
|
40
|
+
}
|
|
41
|
+
const launch = argv[commandAt] === 'harness' && ['ask', 'say'].includes(argv[commandAt + 1] ?? '') ? commandAt : -1;
|
|
42
|
+
const dev = argv[commandAt] === 'dev' ? commandAt : -1;
|
|
43
|
+
if (launch < 0 && dev < 0) return null;
|
|
44
|
+
const fileFlag = argv.indexOf('--file', (launch >= 0 ? launch : dev) + 1);
|
|
45
|
+
if (fileFlag >= 0) return argv[fileFlag + 1] ?? null;
|
|
46
|
+
if (launch >= 0) {
|
|
47
|
+
const candidate = argv[launch + 2];
|
|
48
|
+
return candidate && !candidate.startsWith('-') && /\.(?:txt|md)$/.test(candidate) ? candidate : null;
|
|
49
|
+
}
|
|
50
|
+
const implement = argv.indexOf('--implement', dev + 1);
|
|
51
|
+
const candidate = implement >= 0 ? argv[implement + 1] : undefined;
|
|
52
|
+
return candidate && !candidate.startsWith('-') && /\.(?:txt|md)$/.test(candidate) ? candidate : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function goalFiles(text: string): string[] {
|
|
56
|
+
const line = text.split(/\r?\n/, 1)[0] ?? '';
|
|
57
|
+
const match = /^대상 경로:\s*(.*)$/.exec(line);
|
|
58
|
+
if (!match) return [];
|
|
59
|
+
return [...new Set(match[1]!.split(/\s*[·,]\s*/).map(file => file.trim().replace(/^`|`$/g, '')).filter(Boolean))];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function collectWork(deps: CollectWorkDeps = {}): WorkItem[] {
|
|
63
|
+
const prs = JSON.parse((deps.runGh ?? defaultRunGh)(['pr', 'list', '--state', 'open', '--limit', '1000', '--json', 'number,title,body,headRefName,files,isDraft'])) as OpenPr[];
|
|
64
|
+
if (!Array.isArray(prs)) throw new Error('PR observation is not a list');
|
|
65
|
+
const observation = (deps.listProcesses ?? defaultListHarnessProcesses)();
|
|
66
|
+
if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
|
|
67
|
+
const cfg = deps.config ?? getUserConfig().loops?.orchestrator ?? ORCHESTRATOR_DEFAULTS;
|
|
68
|
+
const work: WorkItem[] = prs.map(pr => ({ kind: 'pr', ref: `#${pr.number}`, seat: seatOfPr(pr.title),
|
|
69
|
+
files: pr.files.map(file => file.path), cells: cellsOf(`${pr.title}\n${pr.body ?? ''}`) }));
|
|
70
|
+
for (const process of observation.records) {
|
|
71
|
+
const path = goalPath(process.command);
|
|
72
|
+
if (path === null) continue;
|
|
73
|
+
const ref = basename(path);
|
|
74
|
+
const cwd = process.cwdStatus === 'observed' && process.cwd && isAbsolute(process.cwd) ? process.cwd : undefined;
|
|
75
|
+
const seat = seatOfTree(cwd, cfg);
|
|
76
|
+
if (!isAbsolute(path) && !cwd) {
|
|
77
|
+
work.push({ kind: 'goal', ref, seat, files: [], cells: [], unreadable: true });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
let text: string;
|
|
81
|
+
try {
|
|
82
|
+
text = (deps.readGoal ?? ((file: string) => readFileSync(file, 'utf8')))(isAbsolute(path) ? path : resolve(cwd!, path));
|
|
83
|
+
} catch {
|
|
84
|
+
work.push({ kind: 'goal', ref, seat, files: [], cells: [], unreadable: true });
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
work.push({ kind: 'goal', ref, seat, files: goalFiles(text), cells: cellsOf(text) });
|
|
88
|
+
}
|
|
89
|
+
return work;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function findOverlaps(work: readonly WorkItem[], excludedFiles: readonly string[] = ['release/next.md']): Overlap[] {
|
|
93
|
+
const excluded = new Set(excludedFiles);
|
|
94
|
+
const files = new Map<string, WorkRef[]>();
|
|
95
|
+
const cells = new Map<string, WorkRef[]>();
|
|
96
|
+
for (const item of work) {
|
|
97
|
+
const ref = { kind: item.kind, ref: item.ref, seat: item.seat };
|
|
98
|
+
for (const path of new Set(item.files)) {
|
|
99
|
+
if (!excluded.has(path)) files.set(path, [...(files.get(path) ?? []), ref]);
|
|
100
|
+
}
|
|
101
|
+
for (const cell of new Set(item.cells)) cells.set(cell, [...(cells.get(cell) ?? []), ref]);
|
|
102
|
+
}
|
|
103
|
+
const details = (refs: WorkRef[]) => ({ refs, crossSeat: new Set(refs.map(ref => ref.seat).filter(Boolean)).size > 1 });
|
|
104
|
+
return [
|
|
105
|
+
...[...files.entries()].filter(([, refs]) => refs.length > 1).map(([path, refs]): Overlap => ({ type: 'file', path, ...details(refs) })),
|
|
106
|
+
...[...cells.entries()].filter(([, refs]) => refs.length > 1).map(([cell, refs]): Overlap => ({ type: 'cell', cell, ...details(refs) })),
|
|
107
|
+
];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function renderOverlaps(overlaps: readonly Overlap[]): string[] {
|
|
111
|
+
return overlaps.map(overlap => {
|
|
112
|
+
const refs = overlap.refs.map(ref => `${ref.kind === 'pr' ? 'PR ' : 'goal '}${ref.ref}(${ref.seat ?? '?'})`).join(' · ');
|
|
113
|
+
return overlap.type === 'file'
|
|
114
|
+
? `⚠ 겹침 file ${overlap.path} — ${refs} · 먼저 착지하는 쪽 뒤에 다른 쪽 rebase`
|
|
115
|
+
: `⚠ 같은 칸 ${overlap.cell} — ${refs}`;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (import.meta.main) {
|
|
120
|
+
try {
|
|
121
|
+
if (process.argv.slice(2).some(arg => arg !== '--json')) throw new Error('usage: overlap.ts [--json]');
|
|
122
|
+
const work = collectWork();
|
|
123
|
+
const overlaps = findOverlaps(work);
|
|
124
|
+
const files = overlaps.filter(overlap => overlap.type === 'file').length;
|
|
125
|
+
const cells = overlaps.filter(overlap => overlap.type === 'cell').length;
|
|
126
|
+
const crossSeat = overlaps.filter(overlap => overlap.crossSeat).length;
|
|
127
|
+
const unreadable = work.filter(item => item.unreadable).length;
|
|
128
|
+
debug.log('loop.orchestrator', 'exchange', { node: 'overlap', files, cells, crossSeat });
|
|
129
|
+
const summary = `overlap work=${work.length} files=${files} cells=${cells} crossSeat=${crossSeat} unreadable=${unreadable}`;
|
|
130
|
+
if (process.argv.includes('--json')) {
|
|
131
|
+
console.log(JSON.stringify({ work, overlaps }));
|
|
132
|
+
console.error(summary);
|
|
133
|
+
} else {
|
|
134
|
+
for (const line of renderOverlaps(overlaps)) console.log(line);
|
|
135
|
+
console.log(summary);
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error(`overlap: ${error instanceof Error ? error.message : String(error)}`);
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
|
+
import { debug } from '../../debug/log.js';
|
|
4
|
+
import { defaultListHarnessProcesses, type HarnessProcessRecord } from '../../harness/harness-cli-command.js';
|
|
5
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
6
|
+
import { devVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
|
|
7
|
+
import { withFileLockSync } from '../../storage/file-lock.js';
|
|
8
|
+
import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorLoopConfig, type OrchestratorSeat } from '../../user-config.js';
|
|
9
|
+
|
|
10
|
+
export const TRAFFIC_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
|
|
11
|
+
export type TrafficCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status'>;
|
|
12
|
+
export type TrafficProcess = Pick<HarnessProcessRecord, 'command' | 'elapsedSeconds' | 'cwd'> & { seat: OrchestratorSeat | null };
|
|
13
|
+
export type TrafficSeatRow = { seat: OrchestratorSeat; running: number; cap: number; idleFor: number | null; idle: boolean; nextCell: TrafficCell | null };
|
|
14
|
+
export type TrafficResult = { seats: TrafficSeatRow[]; unassigned: number; now: Date; idleMinutes: number };
|
|
15
|
+
|
|
16
|
+
/** Same bounded walk as the CTX1 hook: a marker at the git root is checked before stopping. */
|
|
17
|
+
export function seatOfTree(cwd: string | undefined, cfg: Pick<OrchestratorLoopConfig, 'seatTrees'>): OrchestratorSeat | null {
|
|
18
|
+
if (!cwd || !isAbsolute(cwd)) return null;
|
|
19
|
+
let dir = resolve(cwd);
|
|
20
|
+
for (let level = 0; level <= 6; level++) {
|
|
21
|
+
const marker = join(dir, '.claude', 'seat');
|
|
22
|
+
if (existsSync(marker)) {
|
|
23
|
+
const seat = readFileSync(marker, 'utf8').trim();
|
|
24
|
+
return TRAFFIC_SEATS.some(id => id === seat) ? seat as OrchestratorSeat : null;
|
|
25
|
+
}
|
|
26
|
+
if (existsSync(join(dir, '.git'))) break;
|
|
27
|
+
const parent = dirname(dir);
|
|
28
|
+
if (parent === dir) break;
|
|
29
|
+
dir = parent;
|
|
30
|
+
}
|
|
31
|
+
// Compare real paths: on macOS a process cwd under /var reports as /private/var (10-04 MK: UX trees under $TMPDIR
|
|
32
|
+
// never matched their configured /var/folders/... paths).
|
|
33
|
+
const path = realOrResolved(cwd);
|
|
34
|
+
for (const seat of TRAFFIC_SEATS) {
|
|
35
|
+
if (cfg.seatTrees[seat]?.some(tree => {
|
|
36
|
+
if (!isAbsolute(tree)) return false;
|
|
37
|
+
const root = realOrResolved(tree);
|
|
38
|
+
return path === root || path.startsWith(root + sep);
|
|
39
|
+
})) return seat;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function realOrResolved(path: string): string {
|
|
45
|
+
try { return realpathSync(path); } catch { return resolve(path); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const IS_LAUNCH = /(?:^|\s)(?:\S*\/)?elanous\.mjs\s+(?:(?:--test|--config-dir\s+\S+)\s+)?harness\s+(?:ask|say)(?=\s|$)/;
|
|
49
|
+
|
|
50
|
+
/** Process-to-seat resolution occurs at the boundary; this decision does no IO. */
|
|
51
|
+
export function trafficTick({ processes, now, caps, lastLaunchAt, openCells, idleMinutes = 30 }: {
|
|
52
|
+
processes: readonly TrafficProcess[];
|
|
53
|
+
now: Date;
|
|
54
|
+
caps: Readonly<Record<OrchestratorSeat, number>>;
|
|
55
|
+
lastLaunchAt?: Partial<Record<OrchestratorSeat, Date | null>>;
|
|
56
|
+
openCells: readonly TrafficCell[];
|
|
57
|
+
idleMinutes?: number;
|
|
58
|
+
}): TrafficResult {
|
|
59
|
+
const launches = processes.filter(process => IS_LAUNCH.test(process.command));
|
|
60
|
+
const seats = TRAFFIC_SEATS.map((seat): TrafficSeatRow => {
|
|
61
|
+
const owned = launches.filter(process => process.seat === seat);
|
|
62
|
+
const latest = owned.reduce<number | null>((current, process) => {
|
|
63
|
+
if (!Number.isFinite(process.elapsedSeconds) || process.elapsedSeconds < 0) return current;
|
|
64
|
+
const start = now.getTime() - process.elapsedSeconds * 1000;
|
|
65
|
+
return current === null ? start : Math.max(current, start);
|
|
66
|
+
}, null);
|
|
67
|
+
const fallback = owned.length ? lastLaunchAt?.[seat]?.getTime() : undefined;
|
|
68
|
+
const last = latest ?? (fallback !== undefined && Number.isFinite(fallback) ? fallback : null);
|
|
69
|
+
const idleFor = last === null ? null : Math.max(0, Math.floor((now.getTime() - last) / 60_000));
|
|
70
|
+
const idle = owned.length < caps[seat] && last !== null && now.getTime() - last > idleMinutes * 60_000;
|
|
71
|
+
return { seat, running: owned.length, cap: caps[seat], idleFor, idle,
|
|
72
|
+
nextCell: idle ? openCells.find(cell => cell.status !== 'done' && cell.owner?.split('/')[0] === seat) ?? null : null };
|
|
73
|
+
});
|
|
74
|
+
return { seats, unassigned: launches.filter(process => process.seat === null).length, now, idleMinutes };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type TrafficRequest = { key: string; receiptId: string; seat: OrchestratorSeat; text: string; status: 'queued'; queuedAt: string; source: 'orchestrator-traffic' };
|
|
78
|
+
export interface TrafficApplyDeps {
|
|
79
|
+
mode?: 'shadow' | 'live';
|
|
80
|
+
root?: string;
|
|
81
|
+
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Only the live branch opens the journal. The lock serializes hourly-key checks across CLI processes. */
|
|
85
|
+
export function applyTraffic(result: TrafficResult, deps: TrafficApplyDeps = {}): number {
|
|
86
|
+
const log = deps.log ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data));
|
|
87
|
+
const mode = deps.mode ?? 'shadow';
|
|
88
|
+
log('loop.orchestrator', 'tick', { mode, seats: result.seats.map(({ seat, running, cap, idleFor, idle }) => ({ seat, running, cap, idleFor, idle })), unassigned: result.unassigned });
|
|
89
|
+
let queued = 0;
|
|
90
|
+
for (const row of result.seats) {
|
|
91
|
+
if (!row.idle) continue;
|
|
92
|
+
const data = { seat: row.seat, running: row.running, cap: row.cap, idleMinutes: row.idleFor, nextCell: row.nextCell };
|
|
93
|
+
if (mode !== 'live') {
|
|
94
|
+
log('loop.orchestrator', 'would-nudge', data);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!row.nextCell) {
|
|
98
|
+
log('loop.orchestrator', 'exchange', { ...data, outcome: 'no-cell' });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const path = join(deps.root ?? effectiveInstanceRoot(), 'seat-requests', 'requests.jsonl');
|
|
102
|
+
const key = `orch-traffic:${row.seat}:${result.now.toISOString().slice(0, 13)}`;
|
|
103
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
104
|
+
const appended = withFileLockSync(`${path}.lock`, () => {
|
|
105
|
+
const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
106
|
+
if (previous.split('\n').some(line => {
|
|
107
|
+
if (!line) return false;
|
|
108
|
+
try { return (JSON.parse(line) as { key?: string }).key === key; } catch { throw new Error('invalid seat request journal'); }
|
|
109
|
+
})) return false;
|
|
110
|
+
const request: TrafficRequest = { key, receiptId: key, seat: row.seat,
|
|
111
|
+
text: `다음 칸 쏘라: ${row.nextCell!.id} ${row.nextCell!.title.slice(0, 60)} (지금 ${row.running}/${row.cap} · ${row.idleFor}분 놂)`,
|
|
112
|
+
status: 'queued', queuedAt: result.now.toISOString(), source: 'orchestrator-traffic' };
|
|
113
|
+
appendFileSync(path, JSON.stringify(request) + '\n');
|
|
114
|
+
return true;
|
|
115
|
+
});
|
|
116
|
+
if (appended) queued++;
|
|
117
|
+
log('loop.orchestrator', 'exchange', { ...data, key, outcome: appended ? 'queued' : 'duplicate' });
|
|
118
|
+
}
|
|
119
|
+
return queued;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function trafficLine(result: TrafficResult, mode: 'shadow' | 'live'): string {
|
|
123
|
+
const parts = result.seats.map(row => `${row.seat} ${row.running}/${row.cap}`);
|
|
124
|
+
const idle = result.seats.filter(row => row.idle).map(row => row.seat);
|
|
125
|
+
return `traffic ${parts.join(' · ')} · idle=${idle.length ? idle.join(',') : '없음'} · mode=${mode}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (import.meta.main) {
|
|
129
|
+
try {
|
|
130
|
+
const args = process.argv.slice(2);
|
|
131
|
+
const versionFlag = args.indexOf('--version');
|
|
132
|
+
const version = versionFlag < 0 ? devVersion().replace(/-dev\.\d+$/, '') : args[versionFlag + 1];
|
|
133
|
+
if (!version || version.startsWith('--')
|
|
134
|
+
|| args.some((arg, i) => arg !== '--json' && i !== versionFlag && (versionFlag < 0 || i !== versionFlag + 1))) {
|
|
135
|
+
throw new Error('usage: traffic.ts [--version <판>] [--json]');
|
|
136
|
+
}
|
|
137
|
+
const cfg = getUserConfig().loops?.orchestrator ?? ORCHESTRATOR_DEFAULTS;
|
|
138
|
+
const observation = defaultListHarnessProcesses();
|
|
139
|
+
if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
|
|
140
|
+
const now = new Date();
|
|
141
|
+
const processes = observation.records.map(process => ({ ...process, seat: seatOfTree(process.cwdStatus === 'unknown' ? undefined : process.cwd, cfg) }));
|
|
142
|
+
const result = trafficTick({ processes, now, caps: cfg.seatCaps, openCells: listChecklist(version).items });
|
|
143
|
+
const queued = applyTraffic(result, { mode: cfg.trafficMode });
|
|
144
|
+
if (args.includes('--json')) console.log(JSON.stringify({ ...result, mode: cfg.trafficMode, queued, version }));
|
|
145
|
+
else console.log(trafficLine(result, cfg.trafficMode));
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(`traffic: ${error instanceof Error ? error.message : String(error)}`);
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
}
|
|
150
|
+
}
|