elanous 0.2.12 → 0.2.14
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/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-6e7e0377c1db0bdc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
- package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9906-482c6619b3fdda20.js → 8060-cc008cc6fedfa818.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-7b2b4f378354869a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-1512d91c7491ed62.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-a627db94ec6904a0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-fed69f9759d248d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-a67e11ba962337b5.js → page-15374909d8f1aebd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-012a742f81218b01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-99effd9d893a43dc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-938de57ae0315e46.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-629e1ede482ec6eb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-2ce5471926761fdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-27e99b4e54a849d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-32cde713be47cddb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e64caff084f7ce21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-db07eaffba0df7e3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-86f91034bd5cabe5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-ce4d8a1215ce53a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-274703bc41b71333.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-3b7e821dc492bdfd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2f810b18d89ecba6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +1 -0
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +23 -23
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +22 -22
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -0
- package/apps/pwa/out/loops/index.txt +30 -0
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- 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/graphs/orchestrator/orchestrator.yaml +59 -0
- package/graphs/orchestrator/recipes.yaml +18 -0
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -1
- package/graphs/rhythm/daily.yaml +38 -0
- package/graphs/rhythm/recipes.yaml +18 -0
- package/graphs/rhythm/weekly.yaml +39 -0
- package/graphs/video/recipes.yaml +3 -0
- package/graphs/video/youtube-summary.yaml +17 -0
- package/package.json +2 -1
- package/scripts/audit-test-state-writes.ts +72 -17
- package/scripts/brand/check.ts +186 -0
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/coord-post.sh +11 -1
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/docs-cli-check.ts +23 -6
- package/scripts/expose-rubric-check.ts +118 -0
- package/scripts/expose-rubric-suggest.ts +163 -0
- package/scripts/heal/run-role.ts +14 -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 +223 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/lessons/handbook-page.ts +85 -0
- package/scripts/release-loop/gate-node.ts +62 -15
- package/scripts/release-loop/ops-upgrade-node.ts +17 -5
- package/scripts/release-loop/publish-node.ts +14 -1
- package/scripts/release-loop/unattended-release.ts +23 -1
- package/scripts/release-loop/vault-note-node.ts +196 -0
- package/scripts/release-story/draft.ts +172 -0
- package/scripts/rhythm/daily.ts +383 -0
- package/scripts/rhythm/weekly.ts +294 -0
- package/scripts/skills/public-check.ts +169 -0
- package/scripts/test-deterministic.ts +57 -11
- package/scripts/test-diet/effectiveness.ts +195 -0
- package/scripts/test-diet/lib.ts +16 -8
- package/scripts/test-diet/node.ts +77 -9
- package/scripts/ux/demo-rehearsal.ts +2 -2
- package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
- package/scripts/video/youtube-summary.ts +133 -0
- package/skills/diagram-master/SKILL.md +42 -49
- package/skills/diagram-master/references/pyproject.toml +4 -1
- package/skills/diagram-master/references/routing-guide.md +4 -2
- package/skills/grill-me/SKILL.md +1 -0
- 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/skills/youtube-master/SKILL.md +13 -13
- package/skills/youtube-master/package-lock.json +0 -545
- package/skills/youtube-master/package.json +2 -5
- package/skills/youtube-master/scripts/main.ts +2 -2
- package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
- package/skills/youtube-master/src/util.ts +30 -7
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +15 -10
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +1 -1
- package/src/autopilot/build/llm-conflict-merge.ts +45 -8
- package/src/autopilot/build/merge-intent.ts +53 -0
- 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 +2 -0
- package/src/claims/claims-ledger.ts +232 -0
- package/src/claims/claims-render.ts +157 -0
- package/src/cli/claims-cli.ts +94 -0
- package/src/cli/decisions-cli.ts +84 -20
- package/src/cli/docs-cli.ts +2 -0
- package/src/cli/flow-cli.ts +11 -0
- package/src/cli/freeze-cli.ts +45 -0
- package/src/cli/lesson-cli.ts +108 -0
- package/src/cli/logs-cli.ts +104 -2
- package/src/cli/logs-model-input.ts +131 -0
- 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 +44 -2
- package/src/cli/private-ledger-commands.ts +54 -4
- package/src/cli/pty-drive-verdict.ts +11 -2
- package/src/cli/release-cli.ts +248 -45
- package/src/cli/rfc-status.ts +91 -0
- package/src/connectors/linear.ts +5 -3
- package/src/context-bus/condense.ts +189 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-public.ts +86 -0
- package/src/context-bus/context-now-surfaces.ts +70 -4
- package/src/context-bus/context-now.ts +4 -3
- package/src/context-bus/long-term-memory.ts +201 -24
- package/src/context-bus/nightly-condense.ts +1 -1
- package/src/dashboard/index.ts +75 -22
- package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
- package/src/dashboard/slash-runtime/loops-table.ts +77 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +83 -18
- package/src/discord/sprint21-runtime.ts +16 -0
- package/src/discord-self-message.ts +8 -0
- package/src/discord-slash-wire.ts +31 -2
- package/src/domains/outbound-alert.ts +1 -1
- package/src/elanous-config-dir.ts +29 -4
- package/src/execution-loop/launch-gate.ts +29 -14
- package/src/flow/cards.ts +239 -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.ts +64 -11
- package/src/graph-runner/graph-tick.ts +13 -2
- package/src/graph-runner/runner.ts +111 -22
- package/src/harness/harness-cli-command.ts +178 -27
- package/src/harness/harness-incidents.ts +43 -12
- package/src/harness/harness-pod-dispatch.ts +2 -0
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +494 -0
- package/src/harness/helper-scan.ts +139 -0
- package/src/harness/model-input-observation.ts +37 -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/hq/promote.ts +301 -0
- package/src/index.ts +281 -27
- package/src/intake-plane/discord-seat-work.ts +13 -6
- package/src/intake-plane/linear-wish.ts +72 -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-benchmark-ledger.ts +129 -0
- package/src/intake-plane/wish-card.ts +53 -2
- package/src/intake-plane/wish-folder.ts +5 -2
- package/src/intake-plane/wish-reply.ts +72 -0
- package/src/lessons/lesson-import.ts +106 -0
- package/src/lessons/lesson-ledger.ts +206 -0
- package/src/loops/activity.ts +203 -0
- package/src/loops/budget.ts +32 -0
- package/src/loops/checker.ts +180 -0
- package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
- package/src/loops/loop-cli.ts +72 -6
- package/src/loops/neighbors.ts +40 -0
- package/src/loops/observe.ts +59 -0
- package/src/loops/orchestrator/author-depth.ts +164 -0
- package/src/loops/orchestrator/author-input-check.ts +77 -0
- package/src/loops/orchestrator/author-ledger.ts +230 -0
- package/src/loops/orchestrator/finish-rate.ts +175 -0
- package/src/loops/orchestrator/overlap.ts +180 -0
- package/src/loops/orchestrator/resync-shadow.ts +227 -0
- package/src/loops/orchestrator/tick.ts +302 -0
- package/src/loops/orchestrator/traffic.ts +165 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/registry.ts +454 -9
- package/src/loops/status-rows.ts +118 -0
- package/src/loops/verdict.ts +34 -0
- package/src/maturity/bot-slash-catalog.ts +66 -0
- package/src/maturity/feature-maturity.ts +8 -5
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/dashboard.ts +6 -1
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +65 -2
- package/src/nexus/api/merge-approvals-gate.ts +1 -1
- package/src/nexus/api/personas.ts +2 -1
- package/src/nexus/api/schedules-read.ts +5 -1
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +44 -5
- package/src/nexus/api/workflows.ts +2 -1
- 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/auto-start.ts +48 -0
- package/src/release-loop/checklist.ts +13 -4
- package/src/release-loop/feature-store.ts +28 -16
- package/src/release-loop/landed-but-yellow.ts +12 -0
- package/src/release-loop/landing-freeze.ts +121 -0
- package/src/release-loop/placement.ts +242 -0
- package/src/release-loop/release-schedule.ts +3 -1
- package/src/seat-dispatch/ceo-commands.ts +2 -2
- package/src/seat-dispatch/seat-ask.ts +73 -15
- package/src/seat-dispatch/seat-questions.ts +58 -0
- package/src/seat-loop/seat-loop.ts +495 -46
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/dev-pipeline.ts +6 -2
- package/src/self-dev/draft-sweep.ts +76 -0
- package/src/self-dev/run-store.ts +73 -1
- package/src/self-dev/run-supervisor.ts +8 -0
- package/src/self-dev/self-orchestrate-runtime.ts +5 -1
- package/src/self-implement/budget-gate.ts +29 -0
- package/src/self-implement/expose-gate.ts +47 -0
- package/src/self-implement/frozen-merges.ts +294 -0
- package/src/self-implement/gate-cli.ts +10 -4
- package/src/self-implement/gate-scope.ts +46 -9
- package/src/self-implement/goal-archive.ts +222 -0
- package/src/self-implement/goal-lookup.ts +147 -0
- package/src/self-implement/heal-intake.ts +79 -0
- package/src/self-implement/host-regate.ts +25 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +114 -10
- package/src/self-implement/revert-guard.ts +62 -0
- package/src/self-implement/run-ledger.ts +1 -1
- package/src/self-implement/seams.ts +28 -8
- package/src/session/index.ts +2 -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 +86 -0
- package/src/task-cards/card-store.ts +10 -6
- package/src/task-orchestrator/surfaces/pod-lease.ts +23 -8
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +112 -18
- package/src/telegram-commands.ts +27 -3
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-loops-command.ts +44 -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 +204 -16
- 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/storage.ts +31 -13
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/-6XrAoD3eU7NyMHAE4xPS/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1394-ee3795b112907122.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3992-2e522a8ac5f2afde.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
- package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7758-0b018a9e4fc56200.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8605-0259994816604c49.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-cb169620945d1952.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-cefe8169e36cf62a.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/chat/page-813c4a52c9d05ca8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-4731f2c215358937.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-a022b257950f39e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-a2ffc717e04b8b0a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-b59ee4e70fc18fcf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-c172dabd796eb7a3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-1efd0cb9e0ed9d13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-d5dc7ff69a37b4cf.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/market/page-dfca9b7d2de0b135.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-6dd3bfbbde2a5c2c.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/observatory/page-77447356baad75d2.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/outputs/page-6d604227e1664b71.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-4048543b4b7ad5e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-4c3a3126abed551a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-64b0aecb1ca75e75.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/setup/layout-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-9547b5cec7cf76bf.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/tasks/page-2cddeb9e2aeedc55.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-baba2e047872fb0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-e895da4ec9cac345.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/chunks/app/workspace/page-2e9e2fb01c9da1c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-d62ceb13411ebba4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { Database } from 'bun:sqlite';
|
|
4
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
5
|
+
import { LogStore, type LogStoreRow } from '../mss/logging/log-store.js';
|
|
6
|
+
import { checkLoops } from './checker.js';
|
|
7
|
+
import { defaultLoopRoot, listAllLoops, type AllLoopEntry } from './registry.js';
|
|
8
|
+
import type { ScheduleRow } from '../domains/schedule-registry.js';
|
|
9
|
+
|
|
10
|
+
export interface ActivityNode {
|
|
11
|
+
id: string;
|
|
12
|
+
kind: AllLoopEntry['kind'] | 'seat' | 'card' | 'event';
|
|
13
|
+
title: string;
|
|
14
|
+
state: string | null;
|
|
15
|
+
reason: string | null;
|
|
16
|
+
lastAt: string | null;
|
|
17
|
+
events: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ActivityEdge {
|
|
20
|
+
from: string;
|
|
21
|
+
to: string;
|
|
22
|
+
kind: 'delegation' | 'request' | 'reply' | 'card';
|
|
23
|
+
count: number;
|
|
24
|
+
lastAt: string;
|
|
25
|
+
}
|
|
26
|
+
export interface LoopActivityResult {
|
|
27
|
+
since: string;
|
|
28
|
+
until: string;
|
|
29
|
+
nodes: ActivityNode[];
|
|
30
|
+
edges: ActivityEdge[];
|
|
31
|
+
}
|
|
32
|
+
export interface LoopActivityDeps {
|
|
33
|
+
listAllLoops?: (opts: { root: string; stateRoot: string; now: Date; schedules: ScheduleRow[] }) => AllLoopEntry[];
|
|
34
|
+
queryEvents?: (root: string, sinceMs: number, untilMs: number) => LogStoreRow[];
|
|
35
|
+
readJournal?: (root: string, name: string) => readonly Record<string, unknown>[];
|
|
36
|
+
readCards?: (root: string) => readonly { id: string; createdAt: string; sections: readonly { owner: string; createdAt: string }[] }[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readSchedules(root: string): ScheduleRow[] {
|
|
40
|
+
const path = join(root, 'schedules.db');
|
|
41
|
+
if (!existsSync(path)) return [];
|
|
42
|
+
const db = new Database(path, { readonly: true });
|
|
43
|
+
try {
|
|
44
|
+
if (!db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'schedule_registry'").get()) return [];
|
|
45
|
+
const columns = (db.query('PRAGMA table_info(schedule_registry)').all() as Array<{ name: string }>).map(row => row.name);
|
|
46
|
+
if (!columns.includes('run_via')) return [];
|
|
47
|
+
return db.query('SELECT * FROM schedule_registry').all() as ScheduleRow[];
|
|
48
|
+
} finally { db.close(); }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readEvents(root: string, sinceMs: number, untilMs: number): LogStoreRow[] {
|
|
52
|
+
const path = join(root, 'logs', 'logs.db');
|
|
53
|
+
if (!existsSync(path)) return [];
|
|
54
|
+
const store = LogStore.openReadOnly(path);
|
|
55
|
+
try { return store.queryAll({ categories: ['loop.', 'seat.loop'], sinceMs, untilMs }); }
|
|
56
|
+
finally { store.close(); }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseObject(value: string): Record<string, unknown> {
|
|
60
|
+
try {
|
|
61
|
+
const parsed: unknown = JSON.parse(value);
|
|
62
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record<string, unknown> : {};
|
|
63
|
+
} catch { return {}; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readJournal(root: string, name: string): Record<string, unknown>[] {
|
|
67
|
+
const path = join(root, 'seat-requests', name);
|
|
68
|
+
return existsSync(path) ? readFileSync(path, 'utf8').split('\n').filter(Boolean).map(parseObject) : [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readCards(root: string): { id: string; createdAt: string; sections: { owner: string; createdAt: string }[] }[] {
|
|
72
|
+
const dir = join(root, 'task-cards');
|
|
73
|
+
if (!existsSync(dir)) return [];
|
|
74
|
+
return readdirSync(dir).filter(file => file.endsWith('.jsonl')).flatMap(file => {
|
|
75
|
+
const entries = readFileSync(join(dir, file), 'utf8').split('\n').filter(Boolean).map(parseObject);
|
|
76
|
+
const created = entries.find(row => row.type === 'created');
|
|
77
|
+
if (!created || typeof created.id !== 'string' || typeof created.createdAt !== 'string') return [];
|
|
78
|
+
return [{ id: created.id, createdAt: created.createdAt,
|
|
79
|
+
sections: entries.filter(row => row.type === 'section' && typeof row.owner === 'string' && typeof row.createdAt === 'string')
|
|
80
|
+
.map(row => ({ owner: row.owner as string, createdAt: row.createdAt as string })) }];
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function text(value: unknown): string | null {
|
|
85
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function loopId(value: unknown): string | null {
|
|
89
|
+
const id = text(value);
|
|
90
|
+
if (!id) return null;
|
|
91
|
+
const seat = id.toUpperCase();
|
|
92
|
+
if (seat === 'OP' || seat === 'TC' || seat === 'UX') return `${seat.toLowerCase()}-seat`;
|
|
93
|
+
if (seat === 'MK' || seat === 'MK-SEAT') return 'cmo-seat';
|
|
94
|
+
return id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cardNodeId(value: unknown): string | null {
|
|
98
|
+
const id = text(value);
|
|
99
|
+
return id ? (id.startsWith('card:') ? id : `card:${id}`) : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Read-only registry/checker snapshot plus observed edges in the inclusive time window. */
|
|
103
|
+
export function loopActivity({ since, until, root = effectiveInstanceRoot(), deps = {} }: {
|
|
104
|
+
since: string | Date;
|
|
105
|
+
until?: string | Date;
|
|
106
|
+
root?: string;
|
|
107
|
+
deps?: LoopActivityDeps;
|
|
108
|
+
}): LoopActivityResult {
|
|
109
|
+
const end = until === undefined ? new Date() : new Date(until);
|
|
110
|
+
const duration = typeof since === 'string' ? /^(\d+)([mhd])$/.exec(since) : null;
|
|
111
|
+
const durationMs = duration ? Number(duration[1]) * ({ m: 60_000, h: 3_600_000, d: 86_400_000 }[duration[2]!] ?? NaN) : NaN;
|
|
112
|
+
const start = duration ? new Date(end.getTime() - durationMs) : new Date(since);
|
|
113
|
+
if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()) || start > end) throw new Error('invalid loop activity window');
|
|
114
|
+
const inventory = (deps.listAllLoops ?? listAllLoops)({ root: deps.listAllLoops ? root : defaultLoopRoot(), stateRoot: root, now: end, schedules: deps.listAllLoops ? [] : readSchedules(root) });
|
|
115
|
+
const checked = checkLoops(inventory, end);
|
|
116
|
+
const byId = new Map(inventory.map(entry => [entry.id, entry]));
|
|
117
|
+
const nodes = new Map<string, ActivityNode>(checked.map(entry => [entry.id, {
|
|
118
|
+
id: entry.id, kind: byId.get(entry.id)?.kind ?? 'graph',
|
|
119
|
+
title: byId.get(entry.id)?.title ?? entry.id,
|
|
120
|
+
state: entry.state, reason: entry.reason, lastAt: entry.lastRunAt ?? null, events: 0,
|
|
121
|
+
}]));
|
|
122
|
+
const edges = new Map<string, ActivityEdge>();
|
|
123
|
+
const ensure = (id: string, kind: ActivityNode['kind'] = 'event'): ActivityNode => {
|
|
124
|
+
let node = nodes.get(id);
|
|
125
|
+
if (!node) {
|
|
126
|
+
node = { id, kind, title: id, state: null, reason: null, lastAt: null, events: 0 };
|
|
127
|
+
nodes.set(id, node);
|
|
128
|
+
}
|
|
129
|
+
return node;
|
|
130
|
+
};
|
|
131
|
+
const inWindow = (value: unknown): string | null => {
|
|
132
|
+
const ms = typeof value === 'string' ? Date.parse(value) : NaN;
|
|
133
|
+
return Number.isFinite(ms) && ms >= start.getTime() && ms <= end.getTime() ? new Date(ms).toISOString() : null;
|
|
134
|
+
};
|
|
135
|
+
const touch = (id: string, at: string, kind: ActivityNode['kind'] = 'event'): void => {
|
|
136
|
+
const node = ensure(id, kind);
|
|
137
|
+
node.events++;
|
|
138
|
+
if (!node.lastAt || at > node.lastAt) node.lastAt = at;
|
|
139
|
+
};
|
|
140
|
+
const connect = (from: string, to: string, kind: ActivityEdge['kind'], at: string): void => {
|
|
141
|
+
if (from === to) return;
|
|
142
|
+
ensure(from, from.startsWith('card:') ? 'card' : 'event');
|
|
143
|
+
ensure(to, to.startsWith('card:') ? 'card' : 'event');
|
|
144
|
+
const key = JSON.stringify([from, to, kind]);
|
|
145
|
+
const edge = edges.get(key);
|
|
146
|
+
if (edge) { edge.count++; if (at > edge.lastAt) edge.lastAt = at; }
|
|
147
|
+
else edges.set(key, { from, to, kind, count: 1, lastAt: at });
|
|
148
|
+
};
|
|
149
|
+
for (const row of (deps.queryEvents ?? readEvents)(root, start.getTime(), end.getTime())) {
|
|
150
|
+
const timestamp = Number.isFinite(row.ts_ms) ? row.ts_ms : Date.parse(row.ts);
|
|
151
|
+
if (!Number.isFinite(timestamp) || timestamp < start.getTime() || timestamp > end.getTime()) continue;
|
|
152
|
+
const data = parseObject(row.data ?? '');
|
|
153
|
+
const actor = row.category.startsWith('loop.') && !['loop.checker', 'loop.observe', 'loop.registry', 'loop.neighbors', 'loop.package'].includes(row.category)
|
|
154
|
+
? loopId(row.category.slice('loop.'.length)) : row.category === 'seat.loop' ? loopId(data.seat) : null;
|
|
155
|
+
if (!actor) continue;
|
|
156
|
+
const at = new Date(timestamp).toISOString();
|
|
157
|
+
touch(actor, at);
|
|
158
|
+
if (row.event === 'exchange' && text(data.reason) === 'delegation') {
|
|
159
|
+
const to = loopId(data.to ?? data.targetLoopId);
|
|
160
|
+
if (to) connect(loopId(data.from) ?? actor, to, 'delegation', at);
|
|
161
|
+
} else if (row.category === 'loop.orchestrator' && row.event === 'exchange' && data.reason === 'queued') {
|
|
162
|
+
const to = loopId(data.targetLoopId);
|
|
163
|
+
if (to) connect(actor, to, 'delegation', at);
|
|
164
|
+
} else if (row.event === 'exchange' && (data.reason === 'request' || data.reason === 'reply' || data.reason === 'card')) {
|
|
165
|
+
const to = data.reason === 'card' && data.cardId != null
|
|
166
|
+
? cardNodeId(data.cardId) : loopId(data.to ?? data.targetLoopId);
|
|
167
|
+
if (to) connect(loopId(data.from) ?? actor, to, data.reason, at);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const item of (deps.readJournal ?? readJournal)(root, 'requests.jsonl')) {
|
|
171
|
+
const at = inWindow(item.queuedAt);
|
|
172
|
+
const to = loopId(item.loopId ?? item.seat);
|
|
173
|
+
if (!at || !to) continue;
|
|
174
|
+
touch(to, at, 'seat');
|
|
175
|
+
const from = loopId(item.from) ?? (item.source === 'orchestrator' || item.source === 'orchestrator-traffic' ? 'orchestrator'
|
|
176
|
+
: item.source === 'loop-checker' ? 'checker' : null);
|
|
177
|
+
if (from) connect(from, to, 'request', at);
|
|
178
|
+
}
|
|
179
|
+
for (const item of (deps.readJournal ?? readJournal)(root, 'replies.jsonl')) {
|
|
180
|
+
const at = inWindow(item.repliedAt ?? item.createdAt);
|
|
181
|
+
const from = loopId(item.from ?? item.seat);
|
|
182
|
+
if (!at || !from) continue;
|
|
183
|
+
touch(from, at, 'seat');
|
|
184
|
+
const to = loopId(item.to);
|
|
185
|
+
if (to) connect(from, to, 'reply', at);
|
|
186
|
+
}
|
|
187
|
+
for (const card of (deps.readCards ?? readCards)(root)) {
|
|
188
|
+
const id = cardNodeId(card.id);
|
|
189
|
+
if (!id) continue;
|
|
190
|
+
const created = inWindow(card.createdAt);
|
|
191
|
+
if (created) touch(id, created, 'card');
|
|
192
|
+
for (const section of card.sections) {
|
|
193
|
+
const at = inWindow(section.createdAt);
|
|
194
|
+
const owner = loopId(section.owner);
|
|
195
|
+
if (!at || !owner) continue;
|
|
196
|
+
touch(id, at, 'card');
|
|
197
|
+
connect(owner, id, 'card', at);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { since: start.toISOString(), until: end.toISOString(),
|
|
201
|
+
nodes: [...nodes.values()].sort((a, b) => a.id.localeCompare(b.id)),
|
|
202
|
+
edges: [...edges.values()].sort((a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) || a.kind.localeCompare(b.kind)) };
|
|
203
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ORCHESTRATOR_DEFAULTS, type OrchestratorSeat } from '../user-config.js';
|
|
2
|
+
|
|
3
|
+
export interface SpawnBudgetInput {
|
|
4
|
+
seat: OrchestratorSeat;
|
|
5
|
+
/** Current running count for this seat; null/undefined means the ledger could not be measured. */
|
|
6
|
+
running: number | null | undefined;
|
|
7
|
+
/** Configured loops.orchestrator.seatCaps. Missing seats use the orchestrator defaults. */
|
|
8
|
+
caps?: Partial<Record<OrchestratorSeat, number>>;
|
|
9
|
+
/** Release-gate seat limits. Missing seats use the gate default of four. */
|
|
10
|
+
gate?: Partial<Record<OrchestratorSeat, number>>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SpawnBudgetDecision {
|
|
14
|
+
allow: boolean;
|
|
15
|
+
reason: 'within-cap' | 'seat-cap' | 'unknown-running';
|
|
16
|
+
cap: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A release gate can lower, but never raise, the configured per-seat cap. */
|
|
20
|
+
export function decideSpawn({ seat, running, caps, gate }: SpawnBudgetInput): SpawnBudgetDecision {
|
|
21
|
+
const configured = caps?.[seat] ?? ORCHESTRATOR_DEFAULTS.seatCaps[seat];
|
|
22
|
+
const gated = gate?.[seat] ?? 4;
|
|
23
|
+
const cap = Number.isSafeInteger(configured) && configured >= 0 && Number.isSafeInteger(gated) && gated >= 0
|
|
24
|
+
? Math.min(configured, gated)
|
|
25
|
+
: 0;
|
|
26
|
+
if (!Number.isSafeInteger(running) || running === null || running === undefined || running < 0) {
|
|
27
|
+
return { allow: false, reason: 'unknown-running', cap };
|
|
28
|
+
}
|
|
29
|
+
return running < cap
|
|
30
|
+
? { allow: true, reason: 'within-cap', cap }
|
|
31
|
+
: { allow: false, reason: 'seat-cap', cap };
|
|
32
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
ownerSource?: 'header' | 'config' | 'seat' | 'default';
|
|
18
|
+
evidence?: 'log-mtime';
|
|
19
|
+
mode?: string;
|
|
20
|
+
expectEveryMinutes?: number;
|
|
21
|
+
/** The scheduled fire before the latest one (cron loops). A run older than this missed two scheduled fires. */
|
|
22
|
+
dueAt?: string;
|
|
23
|
+
lastRunAt?: string;
|
|
24
|
+
lastStatus?: string;
|
|
25
|
+
recentStatuses?: string[];
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
registered: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface CheckResult extends CheckEntry {
|
|
30
|
+
state: LoopState;
|
|
31
|
+
reason: string;
|
|
32
|
+
}
|
|
33
|
+
export const LOOP_STATES = ['alive', 'late', 'failing', 'off', 'unregistered', 'unknown'] as const;
|
|
34
|
+
export type LoopCounts = Record<LoopState, number>;
|
|
35
|
+
|
|
36
|
+
export function checkLoops(entries: CheckEntry[], now: Date, opts: { failureStatuses?: readonly string[] } = {}): CheckResult[] {
|
|
37
|
+
const failures = opts.failureStatuses ?? ['failed'];
|
|
38
|
+
return entries.map(entry => {
|
|
39
|
+
let state: LoopState;
|
|
40
|
+
let reason: string;
|
|
41
|
+
const age = entry.lastRunAt ? now.getTime() - Date.parse(entry.lastRunAt) : NaN;
|
|
42
|
+
if (!entry.enabled) { state = 'off'; reason = 'disabled'; }
|
|
43
|
+
else if (!entry.registered) { state = 'unregistered'; reason = 'not registered'; }
|
|
44
|
+
else if (entry.recentStatuses?.length && entry.recentStatuses.length >= 3
|
|
45
|
+
&& entry.recentStatuses.slice(0, 3).every(status => failures.includes(status))) {
|
|
46
|
+
state = 'failing'; reason = 'last 3 runs failed';
|
|
47
|
+
} else if (Number.isFinite(age) && entry.dueAt && Number.isFinite(Date.parse(entry.dueAt))) {
|
|
48
|
+
// Judge against the real schedule, not a fixed interval: overnight or weekend gaps are not lateness.
|
|
49
|
+
if (Date.parse(entry.lastRunAt!) < Date.parse(entry.dueAt)) { state = 'late'; reason = `missed the scheduled runs since ${entry.dueAt}`; }
|
|
50
|
+
else { state = 'alive'; reason = 'ran at or after the previous scheduled fire'; }
|
|
51
|
+
} else if (Number.isFinite(age) && entry.expectEveryMinutes !== undefined
|
|
52
|
+
&& entry.expectEveryMinutes > 0 && age > entry.expectEveryMinutes * 2 * 60_000) {
|
|
53
|
+
state = 'late'; reason = `last run exceeds twice the expected ${entry.expectEveryMinutes}m interval`;
|
|
54
|
+
} else if (!entry.lastRunAt || !Number.isFinite(age)) { state = 'unknown'; reason = 'no valid run recorded'; }
|
|
55
|
+
else if (entry.expectEveryMinutes === undefined || !Number.isFinite(entry.expectEveryMinutes) || entry.expectEveryMinutes <= 0) {
|
|
56
|
+
state = 'unknown'; reason = 'expected interval unknown';
|
|
57
|
+
} else { state = 'alive'; reason = 'within expected interval'; }
|
|
58
|
+
return { ...entry, state, reason };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function countLoopStates(results: CheckResult[]): LoopCounts {
|
|
63
|
+
const counts: LoopCounts = { alive: 0, late: 0, failing: 0, off: 0, unregistered: 0, unknown: 0 };
|
|
64
|
+
for (const result of results) counts[result.state]++;
|
|
65
|
+
return counts;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Latest cron fire at or before `at` (minute resolution, scans back up to one year). */
|
|
69
|
+
export function previousLoopFire(cron: string, at: Date, timeZone: string = resolveTimeZone().timeZone): string | null {
|
|
70
|
+
if (cron.trim().split(/\s+/).length !== 5) return null;
|
|
71
|
+
let minute = Math.floor(at.getTime() / 60000) * 60000;
|
|
72
|
+
for (let i = 0; i < 527040; i++, minute -= 60000) {
|
|
73
|
+
if (cronMatches(cron, new Date(minute), { timeZone })) return new Date(minute).toISOString();
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The fire before the latest scheduled fire — a run older than this has missed two scheduled fires. */
|
|
79
|
+
export function dueBefore(cron: string, now: Date, timeZone?: string): string | null {
|
|
80
|
+
const latest = previousLoopFire(cron, now, timeZone);
|
|
81
|
+
return latest ? previousLoopFire(cron, new Date(Date.parse(latest) - 60000), timeZone) : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface RegistryAdapterDeps {
|
|
85
|
+
listLoops?: () => LoopEntry[];
|
|
86
|
+
listAllLoops?: () => CheckEntry[];
|
|
87
|
+
statusForLoop?: (id: string) => { recentRuns: Pick<LoopRun, 'status'>[] };
|
|
88
|
+
now?: Date;
|
|
89
|
+
timeZone?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function entriesFromRegistry(deps: RegistryAdapterDeps = {}): { entries: CheckEntry[]; scope: LoopScope } {
|
|
93
|
+
// An explicitly injected graph inventory remains the checker test/adapter fallback.
|
|
94
|
+
const all = deps.listAllLoops ?? (deps.listLoops ? undefined : registry.listAllLoops);
|
|
95
|
+
if (typeof all === 'function') return { entries: all(), scope: 'registry' };
|
|
96
|
+
const now = deps.now ?? new Date();
|
|
97
|
+
return { scope: 'graph-only', entries: (deps.listLoops ?? registry.listLoops)().map(loop => {
|
|
98
|
+
const first = loop.trigger.cron ? registry.nextLoopFire(loop.trigger.cron, now) : null;
|
|
99
|
+
const second = first && loop.trigger.cron ? registry.nextLoopFire(loop.trigger.cron, new Date(first)) : null;
|
|
100
|
+
const interval = first && second ? (Date.parse(second) - Date.parse(first)) / 60_000 : undefined;
|
|
101
|
+
const statuses = loop.lastRun
|
|
102
|
+
? deps.statusForLoop ? deps.statusForLoop(loop.id).recentRuns.map(run => run.status)
|
|
103
|
+
: deps.listLoops ? [loop.lastRun.status]
|
|
104
|
+
: registry.loopStatus(loop.id).recentRuns.map(run => run.status)
|
|
105
|
+
: [];
|
|
106
|
+
const dueAt = loop.trigger.cron ? dueBefore(loop.trigger.cron, now, deps.timeZone) : null;
|
|
107
|
+
return { id: loop.id, owner: loop.owner ?? undefined, ownerSource: loop.ownerSource, enabled: loop.enabled, registered: true,
|
|
108
|
+
...(interval !== undefined ? { expectEveryMinutes: interval } : {}),
|
|
109
|
+
...(dueAt ? { dueAt } : {}),
|
|
110
|
+
...(loop.lastRun ? { lastRunAt: loop.lastRun.at, lastStatus: loop.lastRun.status } : {}),
|
|
111
|
+
recentStatuses: statuses };
|
|
112
|
+
}) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface CheckerDeps {
|
|
116
|
+
root?: string;
|
|
117
|
+
now?: Date;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Appends only new daily keys to the seat request journal; never rewrites existing lines. */
|
|
121
|
+
export function notifyOwners(results: CheckResult[], deps: CheckerDeps = {}): number {
|
|
122
|
+
const eligible = results.filter(result => result.owner && (result.state === 'late' || result.state === 'failing'));
|
|
123
|
+
if (!eligible.length) return 0;
|
|
124
|
+
const now = deps.now ?? new Date();
|
|
125
|
+
const path = join(deps.root ?? effectiveInstanceRoot(), 'seat-requests', 'requests.jsonl');
|
|
126
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
127
|
+
// SQLite's OS locks are released when a process dies, including after SIGKILL.
|
|
128
|
+
// The journal itself remains append-only; this database only serializes check + append.
|
|
129
|
+
const lock = new Database(`${path}.loop-checker.lock.sqlite`);
|
|
130
|
+
try {
|
|
131
|
+
lock.exec('PRAGMA busy_timeout = 10000');
|
|
132
|
+
lock.exec('BEGIN EXCLUSIVE');
|
|
133
|
+
try {
|
|
134
|
+
const existing = new Set<string>();
|
|
135
|
+
if (existsSync(path)) {
|
|
136
|
+
for (const line of readFileSync(path, 'utf8').split('\n')) {
|
|
137
|
+
if (line.trim()) {
|
|
138
|
+
const row = JSON.parse(line) as { key?: string };
|
|
139
|
+
if (row.key) existing.add(row.key);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let added = 0;
|
|
144
|
+
for (const result of eligible) {
|
|
145
|
+
const key = `loopcheck:${result.id}:${now.toISOString().slice(0, 10)}:${result.state}`;
|
|
146
|
+
if (existing.has(key)) continue;
|
|
147
|
+
const row = { key, receiptId: key, seat: result.owner, text: `${result.id} ${result.state} — 마지막 ${result.lastRunAt ?? '없음'} · ${result.reason}`,
|
|
148
|
+
status: 'queued', queuedAt: now.toISOString(), source: 'loop-checker' };
|
|
149
|
+
appendFileSync(path, JSON.stringify(row) + '\n');
|
|
150
|
+
existing.add(key);
|
|
151
|
+
added++;
|
|
152
|
+
}
|
|
153
|
+
lock.exec('COMMIT');
|
|
154
|
+
return added;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
lock.exec('ROLLBACK');
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
} finally { lock.close(); }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Persist the previous comparison only for explicit all-status checks. Notification is separate. */
|
|
163
|
+
export function observeLoopCheck(results: CheckResult[], scope: LoopScope, deps: CheckerDeps = {}): LoopCounts {
|
|
164
|
+
const path = join(deps.root ?? effectiveInstanceRoot(), 'loop', 'checker', 'last.json');
|
|
165
|
+
let previous: Record<string, LoopState> = {};
|
|
166
|
+
try { previous = JSON.parse(readFileSync(path, 'utf8')) as Record<string, LoopState>; }
|
|
167
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; }
|
|
168
|
+
const current: Record<string, LoopState> = {};
|
|
169
|
+
for (const { id, state } of results) {
|
|
170
|
+
current[id] = state;
|
|
171
|
+
if (previous[id] && previous[id] !== state) debug.log('loop.checker', 'state-change', { id, from: previous[id], to: state });
|
|
172
|
+
}
|
|
173
|
+
const counts = countLoopStates(results);
|
|
174
|
+
debug.log('loop.checker', 'checked', { counts, scope });
|
|
175
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
176
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
177
|
+
try { writeFileSync(tmp, JSON.stringify(current)); renameSync(tmp, path); }
|
|
178
|
+
catch (error) { if (existsSync(tmp)) unlinkSync(tmp); throw error; }
|
|
179
|
+
return counts;
|
|
180
|
+
}
|
|
@@ -2,7 +2,7 @@ id: orchestrator
|
|
|
2
2
|
version: 1.0.0
|
|
3
3
|
kind: orchestrator
|
|
4
4
|
role: docs/roles/OP.md
|
|
5
|
-
graph: graphs/
|
|
5
|
+
graph: graphs/orchestrator/orchestrator.yaml
|
|
6
6
|
orchestration:
|
|
7
7
|
windows:
|
|
8
8
|
- {at: '08:00', delegateTo: [op-seat, cmo-seat, tc-seat, ux-seat], fromLedger: release/checklist-and-seat-requests}
|
package/src/loops/loop-cli.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { writeStdoutJson } from '../cli/stdout-json.js';
|
|
3
|
-
import { listLoops, loopStatus, runLoop, setLoopEnabled } from './registry.js';
|
|
3
|
+
import { listAllLoops, listLoops, loopStatus, runLoop, setLoopEnabled, unregisteredCronLoops, type AllLoopEntry, type LoopRegistryOptions } 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';
|
|
7
|
+
import { loopActivity } from './activity.js';
|
|
4
8
|
|
|
5
9
|
async function output(value: unknown, json: boolean): Promise<void> {
|
|
6
10
|
if (json) await writeStdoutJson(JSON.stringify(value) + '\n');
|
|
@@ -14,12 +18,62 @@ function handle(action: () => Promise<void> | void): Promise<void> {
|
|
|
14
18
|
});
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
function allLoopTable(entries: AllLoopEntry[]): void {
|
|
22
|
+
console.log('id\t층\t주인 자리\t모드\t기대 주기\t호스트\t울타리 역할\t관측 카테고리\t마지막 실행');
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
const interval = entry.expectEveryMinutes === undefined ? '모름'
|
|
25
|
+
: entry.cronIrregular ? `불규칙 · 최대 ${entry.expectEveryMinutes}분` : `${entry.expectEveryMinutes}분`;
|
|
26
|
+
console.log([entry.id, entry.kind, entry.owner ?? '모름', entry.mode ?? '모름', interval,
|
|
27
|
+
entry.host ?? '모름', entry.fenceRole ?? '모름', entry.observationCategory ?? '모름', entry.lastRunAt ?? '모름'].join('\t'));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function registerLoopCommands(program: Command, checkerDeps: RegistryAdapterDeps & { root?: string; registryOptions?: LoopRegistryOptions } = {}): void {
|
|
18
32
|
const loop = program.command('loop').description('Inspect and control graph-backed loop agents');
|
|
19
|
-
loop.command('list').option('--json', 'Print JSON')
|
|
20
|
-
.action((opts: { json?: boolean }) => handle(() =>
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
loop.command('list').option('--all', 'Show the full loop registry').option('--json', 'Print JSON')
|
|
34
|
+
.action((opts: { all?: boolean; json?: boolean }) => handle(async () => {
|
|
35
|
+
if (!opts.all) { await output(listLoops(checkerDeps.registryOptions), !!opts.json); return; }
|
|
36
|
+
const registryOpts = checkerDeps.registryOptions ?? {};
|
|
37
|
+
const entries = listAllLoops(registryOpts);
|
|
38
|
+
if (opts.json) { await output(entries, true); return; }
|
|
39
|
+
allLoopTable(entries);
|
|
40
|
+
const missing = unregisteredCronLoops(entries, registryOpts);
|
|
41
|
+
if (missing.length) console.log(`미등록 ${missing.length}: ${missing.map(row => `${row.id} (${row.command})`).join(', ')}`);
|
|
42
|
+
}));
|
|
43
|
+
loop.command('activity').description('Read loop and seat activity in a time window (default: last 24h)')
|
|
44
|
+
.option('--since <duration-or-date>', 'Start of window: duration (e.g. 24h) or ISO date', '24h')
|
|
45
|
+
.option('--json', 'Print JSON')
|
|
46
|
+
.action((opts: { since: string; json?: boolean }) => handle(async () => {
|
|
47
|
+
const activity = loopActivity({ since: opts.since, root: checkerDeps.root ?? effectiveInstanceRoot() });
|
|
48
|
+
if (opts.json) { await output(activity, true); return; }
|
|
49
|
+
console.log(`Loop activity ${activity.since} → ${activity.until}`);
|
|
50
|
+
console.log(`${activity.nodes.length} nodes · ${activity.edges.length} edges`);
|
|
51
|
+
console.log('Nodes (id · state · events · last activity)');
|
|
52
|
+
for (const node of activity.nodes) console.log(`${node.id} · ${node.state ?? '—'} · ${node.events} · ${node.lastAt ?? '—'}`);
|
|
53
|
+
console.log('Edges (from → to · kind · count · last activity)');
|
|
54
|
+
for (const edge of activity.edges) console.log(`${edge.from} → ${edge.to} · ${edge.kind} · ${edge.count} · ${edge.lastAt}`);
|
|
55
|
+
}));
|
|
56
|
+
loop.command('status [id]').option('--all', 'Check all registered loops')
|
|
57
|
+
.option('--json', 'Print JSON').option('--notify', 'Queue late/failing loops for their owners')
|
|
58
|
+
.action((id: string | undefined, opts: { all?: boolean; json?: boolean; notify?: boolean }) => handle(async () => {
|
|
59
|
+
if (!opts.all) {
|
|
60
|
+
if (!id) throw new Error('loop status requires an id or --all');
|
|
61
|
+
await output(loopStatus(id), !!opts.json);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (id) throw new Error('loop status --all does not take an id');
|
|
65
|
+
const now = checkerDeps.now ?? new Date();
|
|
66
|
+
const root = checkerDeps.root ?? effectiveInstanceRoot();
|
|
67
|
+
const { entries, scope } = entriesFromRegistry({ ...checkerDeps, now });
|
|
68
|
+
const results = checkLoops(entries, now);
|
|
69
|
+
const counts = countLoopStates(results);
|
|
70
|
+
observeLoopCheck(results, scope, { root, now });
|
|
71
|
+
if (opts.notify) notifyOwners(results, { root, now });
|
|
72
|
+
if (opts.json) { await output({ results, counts, scope }, true); return; }
|
|
73
|
+
console.log('id\tstate\tlast run\texpected interval\towner\treason');
|
|
74
|
+
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}`);
|
|
75
|
+
console.log(`${LOOP_STATES.map(state => `${state} ${counts[state]}`).join(' · ')} · scope ${scope}`);
|
|
76
|
+
}));
|
|
23
77
|
for (const [name, enabled] of [['start', true], ['stop', false]] as const) {
|
|
24
78
|
loop.command(`${name} <id>`).option('--yes', 'Apply the crontab change (otherwise preview only)')
|
|
25
79
|
.option('--json', 'Print JSON')
|
|
@@ -34,4 +88,16 @@ export function registerLoopCommands(program: Command): void {
|
|
|
34
88
|
await output(state, !!opts.json);
|
|
35
89
|
if (state.status !== 'done' && state.status !== 'awaiting-approval') process.exitCode = 1;
|
|
36
90
|
}));
|
|
91
|
+
loop.command('package').description('Inspect a loop package without installing it')
|
|
92
|
+
.command('verify <dir>').option('--json', 'Print JSON')
|
|
93
|
+
.action((dir: string, opts: { json?: boolean }) => handle(async () => {
|
|
94
|
+
const result = verifyLoopPackage(dir);
|
|
95
|
+
if (opts.json) await output(result, true);
|
|
96
|
+
else {
|
|
97
|
+
if (result.ok) console.log('OK');
|
|
98
|
+
else for (const error of result.errors) console.log(error);
|
|
99
|
+
console.log(`loop-package ${String(result.merged?.id ?? '?')} ${String(result.merged?.version ?? '?')} ${result.ok ? 'ok' : 'refused'} errors=${result.errors.length}`);
|
|
100
|
+
}
|
|
101
|
+
if (!result.ok) process.exitCode = 1;
|
|
102
|
+
}));
|
|
37
103
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { debug } from '../debug/log.js';
|
|
2
|
+
|
|
3
|
+
/** The neighbor fields validated by src/loops/contract/validate.ts. */
|
|
4
|
+
export interface Neighbor {
|
|
5
|
+
id: string;
|
|
6
|
+
exchange: ('status' | 'request' | 'decision' | 'blocked')[];
|
|
7
|
+
heartbeat: { everyMinutes: number; missedTicks: number };
|
|
8
|
+
onAbsent: { action: 'delegate' | 'defer' | 'escalate'; delegateTo: string };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface NeighborJudgment {
|
|
12
|
+
state: 'present' | 'absent' | 'unknown';
|
|
13
|
+
action?: Neighbor['onAbsent'];
|
|
14
|
+
reason: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** lastSeenAt is the latest ACK of a heartbeat or envelope, not the last send attempt. */
|
|
18
|
+
export function judgeNeighbor({ neighbor, lastSeenAt, lastSendError, now }: {
|
|
19
|
+
neighbor: Neighbor;
|
|
20
|
+
lastSeenAt?: string | null;
|
|
21
|
+
lastSendError?: unknown;
|
|
22
|
+
now: Date;
|
|
23
|
+
}): NeighborJudgment {
|
|
24
|
+
const seen = lastSeenAt ? Date.parse(lastSeenAt) : NaN;
|
|
25
|
+
let result: NeighborJudgment;
|
|
26
|
+
if (lastSendError != null) {
|
|
27
|
+
result = { state: 'unknown', reason: 'send-failed' };
|
|
28
|
+
} else if (!Number.isFinite(seen) || !Number.isFinite(now.getTime()) || seen > now.getTime()) {
|
|
29
|
+
result = { state: 'unknown', reason: 'last-ack-unknown' };
|
|
30
|
+
} else if (now.getTime() - seen >= neighbor.heartbeat.everyMinutes * neighbor.heartbeat.missedTicks * 60_000) {
|
|
31
|
+
result = { state: 'absent', action: neighbor.onAbsent, reason: 'heartbeat-missed' };
|
|
32
|
+
} else {
|
|
33
|
+
result = { state: 'present', reason: 'within-heartbeat-window' };
|
|
34
|
+
}
|
|
35
|
+
// The validated neighbor entry does not carry its owning loop id; do not invent one.
|
|
36
|
+
debug.log('loop.neighbors', 'judged', {
|
|
37
|
+
loop: null, neighbor: neighbor.id, state: result.state, action: result.action, reason: result.reason,
|
|
38
|
+
});
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { debug } from '../debug/log.js';
|
|
2
|
+
import { recordFailureEvent } from '../self-implement/heal-intake.js';
|
|
3
|
+
|
|
4
|
+
/** Event vocabulary of the loop-agent manifest observability contract. */
|
|
5
|
+
export const LOOP_EVENTS = [
|
|
6
|
+
'tick', 'posture-change', 'resolution-change', 'grounding', 'heartbeat',
|
|
7
|
+
'absent', 'exchange', 'spawn', 'reap', 'promote', 'hitl',
|
|
8
|
+
] as const;
|
|
9
|
+
|
|
10
|
+
const LOOP_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
11
|
+
const REQUIRED_CONTEXT = ['runId', 'profile', 'reason', 'sourceRef'] as const;
|
|
12
|
+
|
|
13
|
+
function validId(id: unknown): id is string {
|
|
14
|
+
return typeof id === 'string' && LOOP_ID.test(id);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function reject(reason: 'invalid-loop-id' | 'invalid-event' | 'invalid-data'): void {
|
|
18
|
+
// Never include untrusted identifiers, event names or payloads in a log category.
|
|
19
|
+
debug.log('loop.observe', 'rejected', { reason });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Category matches observability.category in the loop-agent manifest. */
|
|
23
|
+
export function loopCategory(id: string): string {
|
|
24
|
+
if (!validId(id)) {
|
|
25
|
+
reject('invalid-loop-id');
|
|
26
|
+
throw new Error('invalid loop id');
|
|
27
|
+
}
|
|
28
|
+
return `loop.${id}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Emit an RFC loop event, retaining absent context as a measurable list. */
|
|
32
|
+
export function loopEvent(loopId: string, event: string, data: Record<string, unknown>): void {
|
|
33
|
+
if (!validId(loopId)) {
|
|
34
|
+
reject('invalid-loop-id');
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (typeof event !== 'string' || !LOOP_EVENTS.some(name => name === event)) {
|
|
38
|
+
reject('invalid-event');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)
|
|
42
|
+
|| Object.getPrototypeOf(data) !== Object.prototype && Object.getPrototypeOf(data) !== null
|
|
43
|
+
|| (Object.hasOwn(data, 'loopId') && data.loopId !== loopId)) {
|
|
44
|
+
reject('invalid-data');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const missingRequired = REQUIRED_CONTEXT.filter(field =>
|
|
48
|
+
typeof data[field] !== 'string' || !(data[field] as string).trim());
|
|
49
|
+
debug.log(`loop.${loopId}`, event, { ...data, loopId, missingRequired });
|
|
50
|
+
if (event === 'tick' && (data.outcome === 'failed' || data.status === 'failed')) {
|
|
51
|
+
try {
|
|
52
|
+
recordFailureEvent({ source: 'loop-tick', kind: event, ref: loopId,
|
|
53
|
+
summary: `Loop ${loopId} tick failed`, at: new Date().toISOString() });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
try { debug.log('heal.intake', 'record-failed', { source: 'loop-tick', kind: event, ref: loopId, error: String(error) }, { level: 'error' }); }
|
|
56
|
+
catch { /* Heal reporting must not change the tick outcome. */ }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|