elanous 0.2.12 → 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-ee3795b112907122.js → 1394-931c9827067b2276.js} +1 -1
- 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-0b018a9e4fc56200.js → 7758-85c2b53f731da024.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8605-0259994816604c49.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-cb169620945d1952.js → page-e5b4e1b256f41076.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-cefe8169e36cf62a.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-813c4a52c9d05ca8.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-4731f2c215358937.js → page-c85b816e3b269dbb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a022b257950f39e6.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-a2ffc717e04b8b0a.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b59ee4e70fc18fcf.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-c172dabd796eb7a3.js → page-36c6c4183353ac9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-1efd0cb9e0ed9d13.js → page-cb2eb0fd809980da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-d5dc7ff69a37b4cf.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-dfca9b7d2de0b135.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-66c27608f651b866.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-6dd3bfbbde2a5c2c.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-77447356baad75d2.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-6d604227e1664b71.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-4048543b4b7ad5e0.js → page-e7a6c80f288b1948.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4c3a3126abed551a.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-64b0aecb1ca75e75.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +1 -0
- 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-a011dd5b0bff29c5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-9547b5cec7cf76bf.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-2cddeb9e2aeedc55.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-baba2e047872fb0f.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-e895da4ec9cac345.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2e9e2fb01c9da1c9.js → page-2b256feed331546f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-d62ceb13411ebba4.js → page-505bb96d69a8622c.js} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- 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/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- 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/release-loop/gate-node.ts +53 -15
- package/scripts/release-loop/ops-upgrade-node.ts +17 -5
- package/scripts/test-deterministic.ts +53 -3
- 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 +12 -7
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +1 -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/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +29 -0
- package/src/cli/pod-cli.ts +10 -3
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +108 -28
- 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 +15 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +1 -1
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow.ts +64 -11
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +81 -17
- package/src/harness/harness-cli-command.ts +27 -1
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +4 -2
- 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 +199 -7
- package/src/intake-plane/discord-seat-work.ts +13 -6
- 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 +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 +3 -3
- 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 +47 -0
- package/src/nexus/api/personas.ts +2 -1
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +2 -1
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/landed-but-yellow.ts +12 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +90 -16
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-implement/l8-merge-queue.ts +157 -1
- 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-lease.ts +18 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +38 -8
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +10 -4
- package/src/telegram.ts +5 -0
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +94 -10
- 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/1147-466ec15afacc1a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
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
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { debug } from '../../debug/log.js';
|
|
5
|
+
import { loopAgentContractErrors } from '../contract/validate.js';
|
|
6
|
+
|
|
7
|
+
const contractSchema = JSON.parse(readFileSync(resolve(import.meta.dir, '../contract/loop-agent.schema.json'), 'utf8')) as { $id: string };
|
|
8
|
+
const allowedOverlayFields = new Set(['neighbors', 'cadence', 'grounding', 'budget']);
|
|
9
|
+
type RecordValue = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
function record(value: unknown): value is RecordValue {
|
|
12
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function schemaMajor(value: string): number | null {
|
|
16
|
+
const match = /^(?:https:\/\/elanous\.ai\/schemas\/loop-agent\/)?(\d+)\.\d+\.\d+$/.exec(value);
|
|
17
|
+
return match ? Number(match[1]) : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The package version is independent of the declared contract schema version. */
|
|
21
|
+
export function contractMajor(manifest: RecordValue): { ok: boolean; error?: string } {
|
|
22
|
+
const declared = manifest.schema ?? manifest.$schema ?? contractSchema.$id;
|
|
23
|
+
const expected = schemaMajor(contractSchema.$id);
|
|
24
|
+
const actual = typeof declared === 'string' ? schemaMajor(declared) : null;
|
|
25
|
+
const other = manifest.schema !== undefined && manifest.$schema !== undefined ? manifest.$schema : undefined;
|
|
26
|
+
if (actual !== null && expected !== null && actual === expected &&
|
|
27
|
+
(other === undefined || typeof other === 'string' && schemaMajor(other) === expected)) return { ok: true };
|
|
28
|
+
return { ok: false, error: `contract major mismatch: declared ${String(declared)}${other === undefined ? '' : ` / ${String(other)}`}, expected ${contractSchema.$id}` };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Replace only allowlisted top-level fields; refused fields never enter the merged manifest. */
|
|
32
|
+
export function mergeOverlay(manifest: RecordValue, overlay: unknown): { merged: RecordValue; refused: string[] } {
|
|
33
|
+
const merged = { ...manifest };
|
|
34
|
+
const refused: string[] = [];
|
|
35
|
+
if (!record(overlay)) return { merged, refused: ['overlay'] };
|
|
36
|
+
for (const [field, value] of Object.entries(overlay)) {
|
|
37
|
+
if (!allowedOverlayFields.has(field)) refused.push(field);
|
|
38
|
+
else merged[field] = value;
|
|
39
|
+
}
|
|
40
|
+
return { merged, refused };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function inside(root: string, path: string): boolean {
|
|
44
|
+
const part = relative(root, path);
|
|
45
|
+
return part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readReference(root: string, name: string, path: unknown, errors: string[]): void {
|
|
49
|
+
if (typeof path !== 'string' || !path || isAbsolute(path) || /^[a-zA-Z]:[\\/]/.test(path) || path.split(/[\\/]/).includes('..')) {
|
|
50
|
+
errors.push(`${name} path escape: ${String(path)}`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const candidate = resolve(root, path);
|
|
54
|
+
if (!inside(root, candidate)) {
|
|
55
|
+
errors.push(`${name} path escape: ${path}`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const actual = realpathSync(candidate);
|
|
60
|
+
if (!inside(root, actual)) errors.push(`${name} path escape: ${path}`);
|
|
61
|
+
else if (!statSync(actual).isFile()) errors.push(`${name} file missing: ${path}`);
|
|
62
|
+
} catch {
|
|
63
|
+
errors.push(`${name} file missing: ${path}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function readLoopPackage(dir: string): { manifest: RecordValue | null; overlay?: unknown; errors: string[] } {
|
|
68
|
+
const errors: string[] = [];
|
|
69
|
+
let manifest: RecordValue | null = null;
|
|
70
|
+
let overlay: unknown;
|
|
71
|
+
const root = realpathSync(dir);
|
|
72
|
+
if (!statSync(root).isDirectory()) throw new Error(`${dir} is not a directory`);
|
|
73
|
+
const readYaml = (name: string): unknown => {
|
|
74
|
+
const path = resolve(root, name);
|
|
75
|
+
const actual = realpathSync(path);
|
|
76
|
+
if (!inside(root, actual) || !statSync(actual).isFile()) throw new Error(`${name} path escape or not a file`);
|
|
77
|
+
return parseYaml(readFileSync(actual, 'utf8'));
|
|
78
|
+
};
|
|
79
|
+
try {
|
|
80
|
+
const parsed = readYaml('loop.yaml');
|
|
81
|
+
if (!record(parsed)) errors.push('loop.yaml must contain a manifest object');
|
|
82
|
+
else manifest = parsed;
|
|
83
|
+
} catch (error) { errors.push(`loop.yaml: ${error instanceof Error ? error.message : String(error)}`); }
|
|
84
|
+
try { overlay = readYaml('overlay.yaml'); }
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (!(record(error) && error.code === 'ENOENT')) errors.push(`overlay.yaml: ${error instanceof Error ? error.message : String(error)}`);
|
|
87
|
+
}
|
|
88
|
+
if (manifest) {
|
|
89
|
+
readReference(root, 'graph', manifest.graph, errors);
|
|
90
|
+
readReference(root, 'role', manifest.role, errors);
|
|
91
|
+
}
|
|
92
|
+
return { manifest, overlay, errors };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function verifyLoopPackage(dir: string): {
|
|
96
|
+
ok: boolean; errors: string[]; refusedOverlayFields: string[]; merged: RecordValue | null;
|
|
97
|
+
} {
|
|
98
|
+
let manifest: RecordValue | null = null;
|
|
99
|
+
let overlay: unknown;
|
|
100
|
+
const errors: string[] = [];
|
|
101
|
+
try {
|
|
102
|
+
const read = readLoopPackage(dir);
|
|
103
|
+
manifest = read.manifest;
|
|
104
|
+
overlay = read.overlay;
|
|
105
|
+
errors.push(...read.errors);
|
|
106
|
+
} catch (error) { errors.push(`package directory: ${error instanceof Error ? error.message : String(error)}`); }
|
|
107
|
+
let merged: RecordValue | null = null;
|
|
108
|
+
let refusedOverlayFields: string[] = [];
|
|
109
|
+
if (manifest) {
|
|
110
|
+
const major = contractMajor(manifest);
|
|
111
|
+
if (major.error) errors.push(major.error);
|
|
112
|
+
const result = overlay === undefined ? { merged: { ...manifest }, refused: [] } : mergeOverlay(manifest, overlay);
|
|
113
|
+
refusedOverlayFields = result.refused;
|
|
114
|
+
merged = result.merged;
|
|
115
|
+
if (refusedOverlayFields.length) errors.push(`overlay refused: ${refusedOverlayFields.join(', ')}`);
|
|
116
|
+
// Schema declaration is package metadata, not a field in the loop-agent contract.
|
|
117
|
+
const { schema: _schema, $schema: _dollarSchema, ...contractInput } = merged;
|
|
118
|
+
errors.push(...loopAgentContractErrors(contractInput));
|
|
119
|
+
}
|
|
120
|
+
const ok = errors.length === 0;
|
|
121
|
+
debug.log('loop.package', ok ? 'verified' : 'refused', {
|
|
122
|
+
id: manifest?.id, version: manifest?.version, errors: errors.length, refusedOverlayFields,
|
|
123
|
+
});
|
|
124
|
+
return { ok, errors, refusedOverlayFields, merged };
|
|
125
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type LoopCronVerdict = 'alive' | 'late' | 'failed' | 'off';
|
|
2
|
+
|
|
3
|
+
/** Shared display inputs for a graph loop or a cron job. The caller supplies
|
|
4
|
+
* the cadence in milliseconds (or null when it is not known). */
|
|
5
|
+
export interface LoopCronVerdictInput {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
lastRunAt: string | null;
|
|
8
|
+
lastStatus: string | null;
|
|
9
|
+
intervalMs: number | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Pure health classification; a failed last execution takes precedence over age.
|
|
13
|
+
* With no valid timestamp or positive cadence, lateness cannot be established. */
|
|
14
|
+
export function loopCronVerdict(entry: LoopCronVerdictInput, nowMs: number): LoopCronVerdict {
|
|
15
|
+
if (!entry.enabled) return 'off';
|
|
16
|
+
if (entry.lastStatus === 'failed' || entry.lastStatus === 'error' || entry.lastStatus === 'failure') return 'failed';
|
|
17
|
+
const lastRunMs = entry.lastRunAt === null ? NaN : Date.parse(entry.lastRunAt);
|
|
18
|
+
if (Number.isFinite(nowMs) && Number.isFinite(lastRunMs) &&
|
|
19
|
+
entry.intervalMs !== null && Number.isFinite(entry.intervalMs) && entry.intervalMs > 0 &&
|
|
20
|
+
nowMs - lastRunMs > 2 * entry.intervalMs) return 'late';
|
|
21
|
+
return 'alive';
|
|
22
|
+
}
|
|
@@ -57,11 +57,11 @@ export const FEATURE_MATURITY = {
|
|
|
57
57
|
pty: 'beta', pr: 'beta', repo: 'beta', role: 'beta', machine: 'beta',
|
|
58
58
|
where: 'stable', shadow: 'beta', llm: 'beta', storage: 'beta', leader: 'beta',
|
|
59
59
|
pod: 'beta', browser: 'beta', questions: 'beta', usage: 'stable', live: 'beta',
|
|
60
|
-
research: 'beta', release: 'beta', seat: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
|
|
60
|
+
research: 'beta', release: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
|
|
61
61
|
control: 'beta', resources: 'beta', hooks: 'beta', setup: 'stable', start: 'stable',
|
|
62
62
|
grounding: 'beta', graph: 'beta', loop: 'stable', card: 'stable', 'launch-head': 'beta',
|
|
63
63
|
a2a: 'beta', plugin: 'stable', make: 'beta', skills: 'beta', connect: 'beta', import: 'beta',
|
|
64
|
-
persona: 'beta', 'team-set': 'beta', market: 'beta', directive: 'stable', connector: 'beta', storyboard: 'beta',
|
|
64
|
+
persona: 'beta', team: 'beta', 'team-set': 'beta', market: 'beta', directive: 'stable', connector: 'beta', storyboard: 'beta',
|
|
65
65
|
python: 'beta', 'self-update': 'stable', 'measure-fabric-arc-ab': 'beta', mcp: 'beta',
|
|
66
66
|
relay: 'beta', 'telegram-test': 'beta', telegram: 'beta', 'discord-test': 'beta',
|
|
67
67
|
sync: 'beta', status: 'beta', git: 'beta', gh: 'beta', repro: 'system',
|
|
@@ -71,7 +71,7 @@ export const FEATURE_MATURITY = {
|
|
|
71
71
|
provider: 'stable', 'provider:set': 'beta', 'provider:restore': 'beta',
|
|
72
72
|
'provider:rotate': 'beta', 'provider:use': 'beta', 'status-bar': 'beta',
|
|
73
73
|
schedule: 'beta', intake: 'beta', logs: 'stable', ad: 'beta', docs: 'beta',
|
|
74
|
-
decisions: 'beta', directives: 'beta', fleet: 'beta', ops: 'stable', publish: 'beta',
|
|
74
|
+
decisions: 'beta', directives: 'beta', fleet: 'beta', ops: 'stable', outputs: 'beta', publish: 'beta',
|
|
75
75
|
env: 'beta', 'agent-mission': 'beta', dev: 'beta', keys: 'beta', finance: 'beta',
|
|
76
76
|
config: 'stable', tasks: 'stable', labels: 'beta', msg: 'beta', ask: 'stable',
|
|
77
77
|
onboarding: 'beta', login: 'stable', session: 'beta', chat: 'stable', repl: 'stable',
|