elanous 0.2.13 → 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/8060-cc008cc6fedfa818.js +1 -0
- 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-f14987d5850a31e0.js → page-1abfe3aec3be6bdc.js} +1 -1
- 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-15374909d8f1aebd.js +1 -0
- 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-66c27608f651b866.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-78b40a344ed22f24.js → page-e1e47d8d940aba3d.js} +1 -1
- 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-63d14a4d8fe1e1c1.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/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 +1 -1
- package/scripts/audit-test-state-writes.ts +72 -17
- package/scripts/brand/check.ts +186 -0
- package/scripts/coord-post.sh +11 -1
- 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/standby-verify.ts +11 -7
- package/scripts/lessons/handbook-page.ts +85 -0
- package/scripts/release-loop/gate-node.ts +9 -0
- 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 +4 -8
- 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/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-mission/driver.ts +3 -3
- package/src/autopilot/build/llm-conflict-merge.ts +45 -8
- package/src/autopilot/build/merge-intent.ts +53 -0
- 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 +35 -3
- 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/pr-cli.ts +29 -1
- package/src/cli/private-ledger-commands.ts +54 -4
- package/src/cli/pty-drive-verdict.ts +11 -2
- package/src/cli/release-cli.ts +144 -21
- package/src/cli/rfc-status.ts +91 -0
- package/src/connectors/linear.ts +5 -3
- package/src/context-bus/condense.ts +15 -20
- package/src/context-bus/context-now-public.ts +86 -0
- package/src/context-bus/context-now-surfaces.ts +55 -2
- package/src/context-bus/context-now.ts +4 -3
- package/src/context-bus/long-term-memory.ts +92 -11
- 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-ledger.ts +40 -1
- 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-runner/graph-tick.ts +4 -0
- package/src/graph-runner/runner.ts +30 -5
- package/src/harness/harness-cli-command.ts +152 -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 +3 -3
- package/src/harness/harness-queue.ts +216 -28
- package/src/harness/helper-scan.ts +139 -0
- package/src/harness/model-input-observation.ts +37 -0
- package/src/hq/hq.ts +1 -1
- package/src/hq/promote.ts +301 -0
- package/src/index.ts +83 -21
- package/src/intake-plane/linear-wish.ts +39 -11
- 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 +5 -3
- package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
- package/src/loops/loop-cli.ts +36 -4
- package/src/loops/neighbors.ts +40 -0
- package/src/loops/observe.ts +10 -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 +51 -12
- package/src/loops/orchestrator/resync-shadow.ts +227 -0
- package/src/loops/orchestrator/tick.ts +302 -0
- package/src/loops/orchestrator/traffic.ts +18 -3
- package/src/loops/registry.ts +454 -9
- package/src/loops/status-rows.ts +118 -0
- package/src/loops/verdict.ts +17 -5
- package/src/maturity/bot-slash-catalog.ts +66 -0
- package/src/maturity/feature-maturity.ts +7 -4
- package/src/nexus/api/dashboard.ts +6 -1
- package/src/nexus/api/http-server.ts +18 -2
- package/src/nexus/api/merge-approvals-gate.ts +1 -1
- package/src/nexus/api/schedules-read.ts +5 -1
- package/src/nexus/api/task-cards-api.ts +44 -5
- package/src/nexus/api/workflows.ts +2 -1
- 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/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 +16 -3
- package/src/seat-dispatch/seat-questions.ts +58 -0
- package/src/seat-loop/seat-loop.ts +406 -31
- 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/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/task-cards/card-cli.ts +10 -2
- package/src/task-cards/card-store.ts +10 -6
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +80 -16
- package/src/telegram-commands.ts +27 -3
- package/src/telegram-loops-command.ts +44 -0
- package/src/user-config.ts +120 -16
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/storage.ts +31 -13
- package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.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-fe5c06d6b31f8b25.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/5250-c78ca295585bbf95.js +0 -1
- 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/6511-ff8360be3d65aaa4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.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/7551-a0e0707b79164ddd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.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-9da4678f8a310d18.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/9906-e7b8b5310a169dfe.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-e5b4e1b256f41076.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.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-cb2eb0fd809980da.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.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-d92e32d51ab5157e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.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/{D51l8jUBplJrIpWvENsDK → x6mjI6vqOkVVu0jFuDkz1}/_ssgManifest.js +0 -0
package/src/loops/registry.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
2
3
|
import { isAbsolute, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
3
5
|
import { parse as parseYaml } from 'yaml';
|
|
4
6
|
import type { Database } from 'bun:sqlite';
|
|
5
7
|
import { debug } from '../debug/log.js';
|
|
@@ -8,7 +10,10 @@ import { cronMatches } from '../domains/cron-match.js';
|
|
|
8
10
|
import { resolveTimeZone } from '../time/format.js';
|
|
9
11
|
import { inventoryCrontab, listSchedules, openSchedulesDb, type ScheduleRow } from '../domains/schedule-registry.js';
|
|
10
12
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
13
|
+
import { getUserConfig, type UserConfig } from '../user-config.js';
|
|
11
14
|
import { runGraph, type GraphRunState } from '../graph-runner/runner.js';
|
|
15
|
+
import { FENCE_ROLES } from '../hq/hq.js';
|
|
16
|
+
import { dueBefore, type CheckEntry } from './checker.js';
|
|
12
17
|
|
|
13
18
|
export interface LoopRun {
|
|
14
19
|
at: string;
|
|
@@ -22,6 +27,9 @@ export interface LoopEntry {
|
|
|
22
27
|
id: string;
|
|
23
28
|
title: string;
|
|
24
29
|
description: string | null;
|
|
30
|
+
owner?: string | null;
|
|
31
|
+
ownerSource?: 'header' | 'config' | 'seat' | 'default';
|
|
32
|
+
mode?: string | null;
|
|
25
33
|
file: string;
|
|
26
34
|
trigger: { cron: string | null; events: string[] };
|
|
27
35
|
jobs: Array<{ id: string; cron: string | null; enabled: boolean; entryScript?: boolean }>;
|
|
@@ -33,6 +41,8 @@ export interface LoopRegistryOptions {
|
|
|
33
41
|
root?: string;
|
|
34
42
|
stateRoot?: string;
|
|
35
43
|
now?: Date;
|
|
44
|
+
/** Inject a parsed config for isolated inventory checks. */
|
|
45
|
+
config?: UserConfig;
|
|
36
46
|
/** An already-inventoried registry; keeps focused tests isolated. */
|
|
37
47
|
schedules?: ScheduleRow[];
|
|
38
48
|
scheduleAction?: (action: 'create' | 'enable' | 'disable', args: Record<string, unknown>) => Promise<unknown>;
|
|
@@ -55,10 +65,16 @@ const LOOP_MARKER = /#\s*elanous-loop=([A-Za-z0-9][\w.-]*)\s*$/;
|
|
|
55
65
|
export function graphJobMatches(command: string | null, file: string, root: string, graphId?: string): boolean {
|
|
56
66
|
if (!command) return false;
|
|
57
67
|
if (graphId && command.match(LOOP_MARKER)?.[1] === graphId) return true;
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
68
|
+
for (const segment of shellSegments(command)) {
|
|
69
|
+
for (const words of launchedCommands(segment)) {
|
|
70
|
+
const { script, args } = launchedScript(words);
|
|
71
|
+
if (!isElanousExecutable(script)) continue;
|
|
72
|
+
const graph = args[0]?.text === '--test' ? (args[1]?.text === 'graph' ? 1 : 2)
|
|
73
|
+
: args[0]?.text.startsWith('--test=') ? 1 : 0;
|
|
74
|
+
if (args[graph]?.text !== 'graph' || args[graph + 1]?.text !== 'run' || !args[graph + 2]) continue;
|
|
75
|
+
const arg = args[graph + 2]!.text;
|
|
76
|
+
if (resolve(root, arg) === file || (isAbsolute(arg) && resolve(arg) === file)) return true;
|
|
77
|
+
}
|
|
62
78
|
}
|
|
63
79
|
return false;
|
|
64
80
|
}
|
|
@@ -66,9 +82,16 @@ export function graphJobMatches(command: string | null, file: string, root: stri
|
|
|
66
82
|
/** `cron_entry` identifies the script actually launched by cron, not text in another command's arguments. */
|
|
67
83
|
function cronEntryMatches(command: string | null, entry: string): boolean {
|
|
68
84
|
if (!command) return false;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
return shellSegments(command).some(segment => launchedCommands(segment).some(words => {
|
|
86
|
+
let index = 0;
|
|
87
|
+
if (/(?:^|\/)(?:bun|node)$/.test(words[0]?.text ?? '') && /(?:^|\/)cron-run\.ts$/.test(words[1]?.text ?? '')) {
|
|
88
|
+
index = words.findIndex(word => word.text === '--shell') + 1;
|
|
89
|
+
if (index === 0) return false;
|
|
90
|
+
}
|
|
91
|
+
if (/(?:^|\/)(?:sh|bash|zsh)$/.test(words[index]?.text ?? '')) index++;
|
|
92
|
+
const script = words[index]?.text;
|
|
93
|
+
return !!script && /\.sh$/.test(script) && (script === entry || script.endsWith(`/${entry}`));
|
|
94
|
+
}));
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
function recentRuns(id: string, stateRoot: string, count = 5): LoopRun[] {
|
|
@@ -112,6 +135,32 @@ function schedules(opts: LoopRegistryOptions): ScheduleRow[] {
|
|
|
112
135
|
} finally { db.close(); }
|
|
113
136
|
}
|
|
114
137
|
|
|
138
|
+
type OwnerSource = NonNullable<LoopEntry['ownerSource']>;
|
|
139
|
+
function resolveOwner(id: string, title: string, config: UserConfig, header?: unknown, seat?: string): { owner: string; ownerSource: OwnerSource } {
|
|
140
|
+
if (typeof header === 'string' && /^(OP|TC|MK|UX)$/.test(header)) return { owner: header, ownerSource: 'header' };
|
|
141
|
+
const owners = config.loops?.owners;
|
|
142
|
+
const configured = owners && (Object.hasOwn(owners, id) ? owners[id] : Object.hasOwn(owners, title) ? owners[title] : undefined);
|
|
143
|
+
if (configured) return { owner: configured, ownerSource: 'config' };
|
|
144
|
+
if (seat) return { owner: seat, ownerSource: 'seat' };
|
|
145
|
+
return { owner: config.loops?.defaultOwner ?? 'OP', ownerSource: 'default' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** An append redirection on the installed command is a run estimate, not a success record. */
|
|
149
|
+
function cronLogMtime(command: string): string | undefined {
|
|
150
|
+
const match = command.match(/(?:^|\s)>>\s*(?:'([^']+)'|"([^"]+)"|([^\s;&|]+))/);
|
|
151
|
+
if (!match) return undefined;
|
|
152
|
+
const path = (match[1] ?? match[2] ?? match[3]!).replace(/^~(?=\/)/, homedir()).replace(/^\$HOME(?=\/)/, homedir());
|
|
153
|
+
const cd = command.match(/^cd\s+(?:'([^']+)'|"([^"]+)"|([^\s;&|]+))\s*&&/);
|
|
154
|
+
const cwd = (cd?.[1] ?? cd?.[2] ?? cd?.[3] ?? homedir()).replace(/^~(?=\/)/, homedir()).replace(/^\$HOME(?=\/)/, homedir());
|
|
155
|
+
try {
|
|
156
|
+
const stat = statSync(resolve(cwd, path));
|
|
157
|
+
return stat.isFile() ? stat.mtime.toISOString() : undefined;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
debug.log('loops.registry', 'cron-log-unavailable', { path: resolve(cwd, path), error: String(error) });
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
115
164
|
/** Where loop graphs live and where their cron line `cd`s — the same rule as the other elanous crons:
|
|
116
165
|
* the installed package when there is one, else this checkout. Never the caller's cwd: a loop started
|
|
117
166
|
* from a disposable worktree would schedule a cron into a directory that is about to disappear (🅣 09-29). */
|
|
@@ -125,6 +174,7 @@ export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
|
|
|
125
174
|
const root = resolve(opts.root ?? defaultLoopRoot());
|
|
126
175
|
const stateRoot = opts.stateRoot ?? effectiveInstanceRoot();
|
|
127
176
|
const rows = schedules(opts);
|
|
177
|
+
const config = opts.config ?? getUserConfig();
|
|
128
178
|
const loops: LoopEntry[] = [];
|
|
129
179
|
for (const file of graphFiles(join(root, 'graphs'))) {
|
|
130
180
|
let doc: Record<string, unknown> | null;
|
|
@@ -149,8 +199,11 @@ export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
|
|
|
149
199
|
...(entryRows.includes(row) ? { entryScript: true } : {}) }));
|
|
150
200
|
const active = matched.filter(row => !!row.enabled && !!row.cron);
|
|
151
201
|
const next = active.map(row => nextLoopFire(row.cron!, opts.now ?? new Date())).filter((v): v is string => v !== null).sort()[0] ?? null;
|
|
152
|
-
const
|
|
153
|
-
|
|
202
|
+
const title = typeof header?.title === 'string' ? header.title : doc.graph_id;
|
|
203
|
+
const entry: LoopEntry = { id: doc.graph_id, title,
|
|
204
|
+
description: typeof header?.description === 'string' ? header.description : null,
|
|
205
|
+
...resolveOwner(doc.graph_id, title, config, header?.owner),
|
|
206
|
+
mode: typeof header?.mode === 'string' ? header.mode : null, file: relative(root, file),
|
|
154
207
|
trigger: { cron: declaredCron, events }, jobs, enabled: matched.some(row => !!row.enabled),
|
|
155
208
|
lastRun: recentRuns(doc.graph_id, stateRoot, 1)[0] ?? null, nextRun: next };
|
|
156
209
|
loops.push(entry);
|
|
@@ -159,6 +212,398 @@ export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
|
|
|
159
212
|
return loops;
|
|
160
213
|
}
|
|
161
214
|
|
|
215
|
+
export interface AllLoopEntry extends CheckEntry {
|
|
216
|
+
kind: 'graph' | 'seat' | 'cron-shell' | 'orchestrator';
|
|
217
|
+
title: string;
|
|
218
|
+
cron?: string;
|
|
219
|
+
command?: string;
|
|
220
|
+
/** True when the cron's fire gaps vary (e.g. `0 9,17 * * *`); `expectEveryMinutes` is then the longest gap. */
|
|
221
|
+
cronIrregular?: boolean;
|
|
222
|
+
fenceRole?: string;
|
|
223
|
+
host?: string;
|
|
224
|
+
observationCategory?: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Inspect only words of the launched command. Quoted arguments are kept whole, not searched for flags. */
|
|
228
|
+
function launchedWords(command: string): Array<{ text: string; quoted: boolean }> {
|
|
229
|
+
const launched = command.replace(/^cd\s+(?:'[^']*'|"[^"]*"|\S+)\s*&&\s*/, '');
|
|
230
|
+
const words: Array<{ text: string; quoted: boolean }> = [];
|
|
231
|
+
let text = '';
|
|
232
|
+
let quoted = false;
|
|
233
|
+
let quote: string | null = null;
|
|
234
|
+
for (let i = 0; i < launched.length; i++) {
|
|
235
|
+
const ch = launched[i]!;
|
|
236
|
+
if (quote) {
|
|
237
|
+
if (ch === quote) quote = null;
|
|
238
|
+
else if (ch === '\\' && quote === '"' && i + 1 < launched.length) text += launched[++i];
|
|
239
|
+
else text += ch;
|
|
240
|
+
} else if (ch === "'" || ch === '"') { quote = ch; quoted = true; }
|
|
241
|
+
else if (ch === '\\' && i + 1 < launched.length) { text += launched[++i]; }
|
|
242
|
+
else if (ch === ';' || ch === '|' || ch === '&') break;
|
|
243
|
+
else if (/\s/.test(ch)) {
|
|
244
|
+
if (text || quoted) { words.push({ text, quoted }); text = ''; quoted = false; }
|
|
245
|
+
} else text += ch;
|
|
246
|
+
}
|
|
247
|
+
if (text || quoted) words.push({ text, quoted });
|
|
248
|
+
return words;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
type LaunchWord = { text: string; quoted: boolean };
|
|
252
|
+
|
|
253
|
+
function isElanousExecutable(script: string): boolean {
|
|
254
|
+
return /^(?:eln|elanous|elanous\.mjs)$/.test(script.split('/').pop() ?? '');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Skip only leading global CLI options, leaving the action words untouched. */
|
|
258
|
+
function elanousActionStart(args: LaunchWord[]): number {
|
|
259
|
+
let start = 0;
|
|
260
|
+
while (args[start] && !args[start]!.quoted) {
|
|
261
|
+
const option = args[start]!.text;
|
|
262
|
+
if (option === '--test' || option.startsWith('--test=')) { start++; continue; }
|
|
263
|
+
if (option === '--config-dir' || option === '--state-dir') { start += 2; continue; }
|
|
264
|
+
if (option.startsWith('--config-dir=') || option.startsWith('--state-dir=')) { start++; continue; }
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
return start;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Include every launched segment of a wrapper's quoted shell payload, in execution order. */
|
|
271
|
+
function launchedCommands(command: string, onFence?: (role: string) => void): LaunchWord[][] {
|
|
272
|
+
const launches: LaunchWord[][] = [];
|
|
273
|
+
const last = launchedCommand(command, onFence, words => { if (words.length) launches.push(words); });
|
|
274
|
+
if (last.length) launches.push(last);
|
|
275
|
+
return launches;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Only executable-prefix wrappers are stripped; a quoted argument mentioning a loop is not a launch. */
|
|
279
|
+
function launchedCommand(command: string, onFence?: (role: string) => void, onLaunch?: (words: LaunchWord[]) => void): LaunchWord[] {
|
|
280
|
+
const words = launchedWords(command);
|
|
281
|
+
const wrappers: Record<string, { values: readonly string[]; positionals: number }> = {
|
|
282
|
+
env: { values: ['-u', '--unset', '-C', '--chdir', '-S', '--split-string'], positionals: 0 },
|
|
283
|
+
flock: { values: ['-w', '--wait', '-E', '--conflict-exit-code'], positionals: 1 },
|
|
284
|
+
nice: { values: ['-n', '--adjustment'], positionals: 0 },
|
|
285
|
+
timeout: { values: ['-s', '--signal', '-k', '--kill-after'], positionals: 1 },
|
|
286
|
+
'hq-fence': { values: [], positionals: 0 },
|
|
287
|
+
};
|
|
288
|
+
let offset = 0;
|
|
289
|
+
while (offset < words.length) {
|
|
290
|
+
if (/(?:^|\/)(?:bun|node)$/.test(words[offset]?.text ?? '')
|
|
291
|
+
&& /(?:^|\/)cron-run\.ts$/.test(words[offset + 1]?.text ?? '')
|
|
292
|
+
&& words[offset + 2]?.text === '--schedule-id' && words[offset + 4]?.text === '--shell') {
|
|
293
|
+
offset += 5;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const name = words[offset]!.text.split('/').pop()!;
|
|
297
|
+
const spec = wrappers[name];
|
|
298
|
+
if (!spec || words[offset]!.quoted) {
|
|
299
|
+
const { script, args } = launchedScript(words.slice(offset));
|
|
300
|
+
if (!isElanousExecutable(script) || args[0]?.text !== 'hq' || args[1]?.text !== 'fence') break;
|
|
301
|
+
const separator = args.findIndex(word => word.text === '--' && !word.quoted);
|
|
302
|
+
if (separator < 0) break;
|
|
303
|
+
for (let i = 2; i < separator; i++) {
|
|
304
|
+
const role = args[i]?.text === '--role' && i + 1 < separator ? args[i + 1]?.text
|
|
305
|
+
: args[i]?.text.startsWith('--role=') ? args[i]!.text.slice('--role='.length) : undefined;
|
|
306
|
+
if (role) { onFence?.(role); break; }
|
|
307
|
+
}
|
|
308
|
+
offset = words.length - args.length + separator + 1;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (name === 'hq-fence') {
|
|
312
|
+
let commandAt = offset + 1;
|
|
313
|
+
if (words[commandAt]?.text === '--role' && !words[commandAt]?.quoted) {
|
|
314
|
+
const role = words[commandAt + 1]?.text;
|
|
315
|
+
if (role && FENCE_ROLES.includes(role as typeof FENCE_ROLES[number])) {
|
|
316
|
+
onFence?.(role);
|
|
317
|
+
commandAt += words[commandAt + 2]?.text === '--' ? 3 : 2;
|
|
318
|
+
}
|
|
319
|
+
} else if (FENCE_ROLES.includes(words[commandAt]?.text as typeof FENCE_ROLES[number]) && !words[commandAt]?.quoted) {
|
|
320
|
+
onFence?.(words[commandAt]!.text);
|
|
321
|
+
commandAt++;
|
|
322
|
+
}
|
|
323
|
+
if (words[commandAt]?.quoted && words[commandAt]!.text.includes(' ')) {
|
|
324
|
+
const parts = shellSegments(words[commandAt]!.text);
|
|
325
|
+
for (const part of parts.slice(0, -1)) {
|
|
326
|
+
const launch = launchedCommand(part, onFence, onLaunch);
|
|
327
|
+
if (launch.length) onLaunch?.(launch);
|
|
328
|
+
}
|
|
329
|
+
return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch);
|
|
330
|
+
}
|
|
331
|
+
offset = commandAt;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
let index = offset + 1;
|
|
335
|
+
while (index < words.length) {
|
|
336
|
+
const word = words[index]!;
|
|
337
|
+
if (word.text === '--' && !word.quoted) { index++; break; }
|
|
338
|
+
if (name === 'env' && /^[A-Za-z_][A-Za-z_0-9]*=/.test(word.text)) { index++; continue; }
|
|
339
|
+
if (name === 'nice' && /^-\d+$/.test(word.text) && !word.quoted) { index++; continue; }
|
|
340
|
+
if (!word.text.startsWith('-') || word.quoted) break;
|
|
341
|
+
const flag = word.text.split('=')[0]!;
|
|
342
|
+
index += spec.values.includes(flag) && !word.text.includes('=') ? 2 : 1;
|
|
343
|
+
}
|
|
344
|
+
index += spec.positionals;
|
|
345
|
+
if (name === 'flock' && words[index]?.text === '-c' && !words[index]?.quoted) {
|
|
346
|
+
const payload = words[index + 1]?.text;
|
|
347
|
+
if (!payload) return [];
|
|
348
|
+
// The payload is its own shell line: each segment may launch a loop.
|
|
349
|
+
const parts = shellSegments(payload);
|
|
350
|
+
for (const part of parts.slice(0, -1)) {
|
|
351
|
+
const launch = launchedCommand(part, onFence, onLaunch);
|
|
352
|
+
if (launch.length) onLaunch?.(launch);
|
|
353
|
+
}
|
|
354
|
+
return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch);
|
|
355
|
+
}
|
|
356
|
+
if (index >= words.length) break;
|
|
357
|
+
offset = index;
|
|
358
|
+
}
|
|
359
|
+
return words.slice(offset);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Split a shell line at top-level `&&`, `||`, `;` and `|` only — separators inside quotes stay in their word. */
|
|
363
|
+
export function shellSegments(command: string): string[] {
|
|
364
|
+
const segments: string[] = [];
|
|
365
|
+
let current = '';
|
|
366
|
+
let quote: '"' | "'" | null = null;
|
|
367
|
+
for (let i = 0; i < command.length; i++) {
|
|
368
|
+
const ch = command[i]!;
|
|
369
|
+
if (quote) {
|
|
370
|
+
current += ch;
|
|
371
|
+
if (ch === '\\' && quote === '"' && i + 1 < command.length) { current += command[++i]; continue; }
|
|
372
|
+
if (ch === quote) quote = null;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (ch === '\\' && i + 1 < command.length) { current += ch + command[++i]; continue; }
|
|
376
|
+
if (ch === '"' || ch === "'") { quote = ch; current += ch; continue; }
|
|
377
|
+
const two = command.slice(i, i + 2);
|
|
378
|
+
if (two === '&&' || two === '||') { segments.push(current); current = ''; i++; continue; }
|
|
379
|
+
if (ch === ';' || ch === '|' || ch === '\n') { segments.push(current); current = ''; continue; }
|
|
380
|
+
current += ch;
|
|
381
|
+
}
|
|
382
|
+
segments.push(current);
|
|
383
|
+
return segments.map(segment => segment.trim()).filter(Boolean);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function cronFenceRole(command: string): string | undefined {
|
|
387
|
+
for (const segment of shellSegments(command)) {
|
|
388
|
+
let role: string | undefined;
|
|
389
|
+
launchedCommand(segment, value => { role = value; });
|
|
390
|
+
if (role) return role;
|
|
391
|
+
}
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function fenceCommandTitle(command: string, role: string): string | undefined {
|
|
396
|
+
const launches = shellSegments(command).flatMap(segment => launchedCommands(segment));
|
|
397
|
+
const inner = launches.at(-1);
|
|
398
|
+
if (!inner?.length) return undefined;
|
|
399
|
+
const { script, args } = launchedScript(inner);
|
|
400
|
+
const executable = script.split('/').pop();
|
|
401
|
+
const action = elanousCronName(command) ?? (executable === 'git' ? [executable, args[0]?.text].filter(Boolean).join(' ')
|
|
402
|
+
: /^(?:sh|bash|zsh)$/.test(executable ?? '') ? args[0]?.text.split('/').pop() : executable);
|
|
403
|
+
return action ? `${action} (hq-fence ${role})` : undefined;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function launchedScript(words: LaunchWord[]): { script: string; args: LaunchWord[] } {
|
|
407
|
+
const offset = /^(?:.*\/)?(?:bun|node)$/.test(words[0]?.text ?? '') ? 1 : 0;
|
|
408
|
+
return { script: words[offset]?.text ?? '', args: words.slice(offset + 1) };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Recognize direct .sh execution or sh/bash/zsh with option flags and a script or -c payload. */
|
|
412
|
+
function isCronShellCommand(command: string): boolean {
|
|
413
|
+
return shellSegments(command).some(segment => launchedCommands(segment).some(words => {
|
|
414
|
+
const executable = words[0]?.text ?? '';
|
|
415
|
+
if (/\.sh$/.test(executable)) return true;
|
|
416
|
+
if (!/(?:^|\/)(?:sh|bash|zsh)$/.test(executable)) return false;
|
|
417
|
+
let index = 1;
|
|
418
|
+
let inline = false;
|
|
419
|
+
while (words[index] && /^-[a-zA-Z]+$/.test(words[index]!.text) && !words[index]!.quoted) {
|
|
420
|
+
inline ||= words[index]!.text.includes('c');
|
|
421
|
+
index++;
|
|
422
|
+
}
|
|
423
|
+
if (words[index]?.text === '--') index++;
|
|
424
|
+
return !!words[index] && (inline || /\.sh$/.test(words[index]!.text));
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function launchedSeat(command: string): string | undefined {
|
|
429
|
+
for (const segment of shellSegments(command)) {
|
|
430
|
+
for (const words of launchedCommands(segment)) {
|
|
431
|
+
const { script, args } = launchedScript(words);
|
|
432
|
+
const named = script.match(/(?:^|\/)(op|tc|mk|ux)-seat(?:\.[\w-]+)?$/i)?.[1];
|
|
433
|
+
if (named) return named.toUpperCase();
|
|
434
|
+
if (!/(?:^|\/)seat-loop(?:\.[\w-]+)?$/i.test(script)) continue;
|
|
435
|
+
for (let i = 0; i < args.length; i++) {
|
|
436
|
+
const arg = args[i]!;
|
|
437
|
+
if (arg.quoted) continue;
|
|
438
|
+
const seat = arg.text === '--seat' ? (!args[i + 1]?.quoted ? args[i + 1]?.text : undefined)
|
|
439
|
+
: arg.text.startsWith('--seat=') ? arg.text.slice('--seat='.length) : undefined;
|
|
440
|
+
if (seat && /^(OP|TC|MK|UX)$/i.test(seat)) return seat.toUpperCase();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return undefined;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function launchedOrchestrator(command: string): boolean {
|
|
448
|
+
return shellSegments(command).some(segment => launchedCommands(segment).some(words =>
|
|
449
|
+
/(?:^|\/)(?:\w+-)?orchestrator(?:\.[\w-]+)?$/i.test(launchedScript(words).script)));
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** A launched two-word elanous action, not a mention in an argument or a read-only CLI query. */
|
|
453
|
+
function elanousCronName(command: string): string | undefined {
|
|
454
|
+
for (const segment of shellSegments(command)) {
|
|
455
|
+
for (const words of launchedCommands(segment)) {
|
|
456
|
+
const { script, args } = launchedScript(words);
|
|
457
|
+
if (!isElanousExecutable(script)) continue;
|
|
458
|
+
const start = elanousActionStart(args);
|
|
459
|
+
const first = args[start];
|
|
460
|
+
const second = args[start + 1];
|
|
461
|
+
if (!first || !second || first.quoted || second.quoted || first.text.startsWith('-') || second.text.startsWith('-')) continue;
|
|
462
|
+
const actions: Record<string, readonly string[]> = {
|
|
463
|
+
hq: ['heartbeat', 'arbiter-check'],
|
|
464
|
+
harness: ['queue'],
|
|
465
|
+
card: ['intake-scan'],
|
|
466
|
+
loop: ['status', 'list'],
|
|
467
|
+
};
|
|
468
|
+
if (actions[first.text]?.includes(second.text)
|
|
469
|
+
&& (first.text !== 'harness' || args[start + 2]?.text === 'tick')) return `${first.text} ${second.text}`;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Number only unclaimed named jobs, in installed schedule order, for inventory and warning lookup alike. */
|
|
476
|
+
function namedCronIds(rows: ScheduleRow[], graphClaimed: Set<string>): Map<string, string> {
|
|
477
|
+
const counts = new Map<string, number>();
|
|
478
|
+
const ids = new Map<string, string>();
|
|
479
|
+
for (const row of rows) {
|
|
480
|
+
if (graphClaimed.has(row.id) || row.source !== 'crontab' || row.run_via !== 'crontab' || row.disabled_reason === 'vanished') continue;
|
|
481
|
+
const command = row.command ?? '';
|
|
482
|
+
if (launchedSeat(command) || launchedOrchestrator(command) || isCronShellCommand(command)) continue;
|
|
483
|
+
const name = elanousCronName(command);
|
|
484
|
+
if (!name) continue;
|
|
485
|
+
const base = `elanous:${name.replace(/\s+/g, '-')}`;
|
|
486
|
+
const count = (counts.get(base) ?? 0) + 1;
|
|
487
|
+
counts.set(base, count);
|
|
488
|
+
ids.set(row.id, `${base}${count > 1 ? `-${count}` : ''}`);
|
|
489
|
+
}
|
|
490
|
+
return ids;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Gaps between consecutive actual fires, rather than a guess based on cron text. A cron whose gaps vary
|
|
494
|
+
* (e.g. `0 9,17 * * *` = 8h then 16h) is irregular: report the longest gap so «late» never fires early. */
|
|
495
|
+
const CRON_PERIOD_SAMPLES = 8;
|
|
496
|
+
function cronPeriod(cron: string | null, now: Date): { minutes: number; irregular: boolean } | undefined {
|
|
497
|
+
if (!cron) return undefined;
|
|
498
|
+
const fires: number[] = [];
|
|
499
|
+
let at = now;
|
|
500
|
+
for (let i = 0; i <= CRON_PERIOD_SAMPLES; i++) {
|
|
501
|
+
const next = nextLoopFire(cron, at);
|
|
502
|
+
if (!next) break;
|
|
503
|
+
fires.push(Date.parse(next));
|
|
504
|
+
at = new Date(next);
|
|
505
|
+
}
|
|
506
|
+
if (fires.length < 2) return undefined;
|
|
507
|
+
const gaps = fires.slice(1).map((fire, i) => (fire - fires[i]!) / 60_000);
|
|
508
|
+
const longest = Math.max(...gaps);
|
|
509
|
+
return { minutes: longest, irregular: gaps.some(gap => gap !== gaps[0]) };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function periodFields(period: { minutes: number; irregular: boolean } | undefined): Pick<AllLoopEntry, 'expectEveryMinutes' | 'cronIrregular'> {
|
|
513
|
+
if (!period) return {};
|
|
514
|
+
return { expectEveryMinutes: period.minutes, ...(period.irregular ? { cronIrregular: true } : {}) };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/** Inventory all installed loop agents without changing the graph-only `listLoops` contract. */
|
|
518
|
+
export function listAllLoops(opts: LoopRegistryOptions = {}): AllLoopEntry[] {
|
|
519
|
+
const now = opts.now ?? new Date();
|
|
520
|
+
const config = opts.config ?? getUserConfig();
|
|
521
|
+
const rows = schedules(opts).filter(row => row.source === 'crontab' && row.run_via === 'crontab' && row.disabled_reason !== 'vanished');
|
|
522
|
+
const graphOpts = { ...opts, now, schedules: rows };
|
|
523
|
+
const graphLoops = listLoops(graphOpts);
|
|
524
|
+
const claimed = new Set<string>();
|
|
525
|
+
const all: AllLoopEntry[] = graphLoops.map(loop => {
|
|
526
|
+
for (const job of loop.jobs) claimed.add(job.id);
|
|
527
|
+
// The installed job's cron is what actually runs — a disabled install still beats the declared trigger.
|
|
528
|
+
const cron = loop.jobs.find(job => job.enabled && job.cron)?.cron ?? loop.jobs.find(job => job.cron)?.cron ?? loop.trigger.cron;
|
|
529
|
+
const runs = recentRuns(loop.id, opts.stateRoot ?? effectiveInstanceRoot());
|
|
530
|
+
const lastRun = runs[0] ?? null;
|
|
531
|
+
const period = cronPeriod(cron, now);
|
|
532
|
+
const dueAt = cron ? dueBefore(cron, now) : null;
|
|
533
|
+
const installed = rows.filter(row => loop.jobs.some(item => item.id === row.id));
|
|
534
|
+
const fenceRole = installed.map(row => cronFenceRole(row.command ?? '')).find((role): role is string => !!role);
|
|
535
|
+
const graphDoc = parseYaml(readFileSync(join(resolve(opts.root ?? defaultLoopRoot()), loop.file), 'utf8')) as Record<string, unknown> | null;
|
|
536
|
+
const header = graphDoc?.loop && typeof graphDoc.loop === 'object' && !Array.isArray(graphDoc.loop)
|
|
537
|
+
? graphDoc.loop as Record<string, unknown> : {};
|
|
538
|
+
const declaredHost = typeof header.host === 'string' && header.host ? header.host : undefined;
|
|
539
|
+
const declaredCategory = typeof header.observationCategory === 'string' && header.observationCategory ? header.observationCategory : undefined;
|
|
540
|
+
return { kind: 'graph', id: loop.id, title: loop.title, owner: loop.owner ?? undefined, ownerSource: loop.ownerSource,
|
|
541
|
+
...(declaredHost || installed.length ? { host: declaredHost ?? hostname() } : {}),
|
|
542
|
+
observationCategory: declaredCategory ?? 'graph.runner',
|
|
543
|
+
...(fenceRole ? { fenceRole } : {}),
|
|
544
|
+
mode: loop.mode ?? undefined, enabled: loop.enabled, registered: loop.jobs.length > 0,
|
|
545
|
+
...(cron ? { cron } : {}), ...periodFields(period),
|
|
546
|
+
...(dueAt ? { dueAt } : {}),
|
|
547
|
+
...(lastRun ? { lastRunAt: lastRun.at, lastStatus: lastRun.status } : {}),
|
|
548
|
+
recentStatuses: runs.map(run => run.status) };
|
|
549
|
+
});
|
|
550
|
+
const namedIds = namedCronIds(rows, claimed);
|
|
551
|
+
for (const row of rows) {
|
|
552
|
+
if (claimed.has(row.id)) continue;
|
|
553
|
+
const command = row.command ?? '';
|
|
554
|
+
const seat = launchedSeat(command);
|
|
555
|
+
const orchestrator = launchedOrchestrator(command);
|
|
556
|
+
const shell = isCronShellCommand(command);
|
|
557
|
+
const namedId = namedIds.get(row.id);
|
|
558
|
+
const fenceRole = cronFenceRole(command);
|
|
559
|
+
const fencedGitPush = fenceRole === 'git-push' && fenceCommandTitle(command, fenceRole) === 'git push (hq-fence git-push)';
|
|
560
|
+
const kind: AllLoopEntry['kind'] = seat ? 'seat' : orchestrator ? 'orchestrator' : 'cron-shell';
|
|
561
|
+
if (kind === 'cron-shell' && !shell && !namedId && !fencedGitPush) continue;
|
|
562
|
+
const cron = row.cron;
|
|
563
|
+
const period = cronPeriod(cron, now);
|
|
564
|
+
const dueAt = cron ? dueBefore(cron, now) : null;
|
|
565
|
+
const id = seat ? `${seat.toLowerCase()}-seat:${row.id}` : namedId ?? row.id;
|
|
566
|
+
const title = seat ? `${seat} seat` : row.note || (fenceRole ? fenceCommandTitle(command, fenceRole) : undefined) || (namedId ? elanousCronName(command) : undefined) || row.name;
|
|
567
|
+
const logMtime = kind === 'cron-shell' && command ? cronLogMtime(command) : undefined;
|
|
568
|
+
all.push({ kind, id, title,
|
|
569
|
+
...resolveOwner(id, title, config, undefined, seat), ...(fenceRole ? { fenceRole } : {}),
|
|
570
|
+
host: hostname(), observationCategory: row.category,
|
|
571
|
+
enabled: !!row.enabled, registered: true,
|
|
572
|
+
...(cron ? { cron } : {}), ...(command ? { command } : {}),
|
|
573
|
+
...periodFields(period),
|
|
574
|
+
...(dueAt ? { dueAt } : {}),
|
|
575
|
+
...(logMtime ? { lastRunAt: logMtime, evidence: 'log-mtime' as const }
|
|
576
|
+
: row.last_run && Number.isFinite(Date.parse(row.last_run)) ? { lastRunAt: row.last_run } : {}),
|
|
577
|
+
...(row.last_status ? { lastStatus: row.last_status } : {}),
|
|
578
|
+
recentStatuses: row.last_status ? [row.last_status] : [] });
|
|
579
|
+
}
|
|
580
|
+
const bySource: Record<OwnerSource, number> = { header: 0, config: 0, seat: 0, default: 0 };
|
|
581
|
+
for (const loop of all) bySource[loop.ownerSource!]++;
|
|
582
|
+
debug.log('loop.check', 'ownership', { total: all.length, bySource });
|
|
583
|
+
debug.log('loops.registry', 'list-all', { count: all.length });
|
|
584
|
+
return all;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** An unclaimed cron line with any elanous marker must surface as an unregistered warning. */
|
|
588
|
+
function looksElanousRelated(command: string): boolean {
|
|
589
|
+
return /(?:^|[^\w.-])(?:elanous(?:\.mjs)?|eln|hq-fence)(?=$|[^\w.-])|(?:^|\/)\.elanous(?:\/|$)/i.test(command);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Crontab elanous loop launches not represented by any graph, seat, shell or orchestrator row. */
|
|
593
|
+
export function unregisteredCronLoops(entries: AllLoopEntry[], opts: LoopRegistryOptions = {}): ScheduleRow[] {
|
|
594
|
+
const graphJobs = new Map(listLoops(opts).map(loop => [loop.id, loop.jobs.map(job => job.id)]));
|
|
595
|
+
const rows = schedules(opts);
|
|
596
|
+
const graphClaimed = new Set([...graphJobs.values()].flat());
|
|
597
|
+
const namedRows = new Map([...namedCronIds(rows, graphClaimed)].map(([rowId, name]) => [name, rowId]));
|
|
598
|
+
const claimed = new Set(entries.flatMap(entry => entry.kind === 'graph'
|
|
599
|
+
? graphJobs.get(entry.id) ?? []
|
|
600
|
+
: [entry.kind === 'seat' ? entry.id.slice(entry.id.indexOf(':') + 1)
|
|
601
|
+
: namedRows.get(entry.id) ?? entry.id]));
|
|
602
|
+
return rows.filter(row => row.source === 'crontab' && row.run_via === 'crontab'
|
|
603
|
+
&& row.disabled_reason !== 'vanished' && !claimed.has(row.id)
|
|
604
|
+
&& looksElanousRelated(row.command ?? ''));
|
|
605
|
+
}
|
|
606
|
+
|
|
162
607
|
export function loopStatus(id: string, opts: LoopRegistryOptions = {}): LoopEntry & { recentRuns: LoopRun[] } {
|
|
163
608
|
const loop = listLoops(opts).find(entry => entry.id === id);
|
|
164
609
|
if (!loop) throw new Error(`loop not found: ${id}`);
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { loopCronVerdict, type LoopCronVerdict } from './verdict';
|
|
2
|
+
|
|
3
|
+
const VERDICT_LABEL: Record<LoopCronVerdict, LoopRow['verdict']> = {
|
|
4
|
+
alive: '살아 있음', late: '늦음', failed: '실패', off: '꺼짐',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export interface LoopSchedule {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
source: string;
|
|
11
|
+
category: string | null;
|
|
12
|
+
domain: string | null;
|
|
13
|
+
runVia: string | null;
|
|
14
|
+
cron: string | null;
|
|
15
|
+
intervalMs: number | null;
|
|
16
|
+
state: 'live' | 'firing' | 'stale' | 'off';
|
|
17
|
+
next: string[];
|
|
18
|
+
lastRun: { at: string; status: string | null; exit: number | null } | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LoopRow {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
layer: string;
|
|
25
|
+
owner: string;
|
|
26
|
+
mode: string;
|
|
27
|
+
lastRun: string | null;
|
|
28
|
+
verdict: '살아 있음' | '늦음' | '실패' | '꺼짐' | '판정 불가';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const LOOP_SCHEDULES_PATH = '/v1/schedules?includeOff=1';
|
|
32
|
+
export const LOOPS_PATH = '/v1/dashboard/loops';
|
|
33
|
+
|
|
34
|
+
interface RecordedLoop {
|
|
35
|
+
name: string;
|
|
36
|
+
label: string;
|
|
37
|
+
category?: string;
|
|
38
|
+
armed: boolean | null;
|
|
39
|
+
last: { at: string; status: string } | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function loadLoopRows(fetchJson: (path: string) => Promise<unknown>, now: number): Promise<LoopRow[]> {
|
|
43
|
+
const [schedulesResult, loopsResult] = await Promise.allSettled([
|
|
44
|
+
fetchJson(LOOP_SCHEDULES_PATH), fetchJson(LOOPS_PATH),
|
|
45
|
+
]);
|
|
46
|
+
const schedules = schedulesResult.status === 'fulfilled' ? (schedulesResult.value as { schedules?: unknown })?.schedules : null;
|
|
47
|
+
const loops = loopsResult.status === 'fulfilled' ? (loopsResult.value as { loops?: { loops?: unknown } | null })?.loops?.loops : null;
|
|
48
|
+
if (!Array.isArray(schedules) && !Array.isArray(loops)) throw new Error('loop registries unavailable');
|
|
49
|
+
if (!Array.isArray(schedules)) throw new Error('스케줄 레지스트리 조회 실패');
|
|
50
|
+
if (!Array.isArray(loops)) throw new Error('루프 레지스트리 조회 실패');
|
|
51
|
+
// These registries have different ID namespaces even when labels match.
|
|
52
|
+
const rows = loopRows(schedules as LoopSchedule[], now).map((row) => ({ ...row, id: `schedule:${row.id}` }));
|
|
53
|
+
for (const loop of loops as RecordedLoop[]) {
|
|
54
|
+
const last = loop.last ? Date.parse(loop.last.at) : NaN;
|
|
55
|
+
const verdict = loopCronVerdict({
|
|
56
|
+
enabled: loop.armed !== false,
|
|
57
|
+
lastRunAt: loop.last?.at ?? null,
|
|
58
|
+
lastStatus: loop.last?.status.toLowerCase() ?? null,
|
|
59
|
+
intervalMs: null,
|
|
60
|
+
}, now);
|
|
61
|
+
rows.push({
|
|
62
|
+
id: `loop:${loop.name}`, name: loop.label, layer: loop.category ?? '루프', owner: '미지정',
|
|
63
|
+
mode: loop.armed === null ? '관측' : loop.armed ? 'armed' : 'off',
|
|
64
|
+
lastRun: Number.isFinite(last) ? loop.last!.at : null,
|
|
65
|
+
verdict: verdict === 'alive' ? '판정 불가' : VERDICT_LABEL[verdict],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return rows;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createLoopRowsRefresh(
|
|
72
|
+
fetchJson: (path: string) => Promise<unknown>,
|
|
73
|
+
onResult: (result: { rows: LoopRow[]; state: 'ready' | 'error' }) => void,
|
|
74
|
+
clock: () => number = Date.now,
|
|
75
|
+
) {
|
|
76
|
+
let sequence = 0;
|
|
77
|
+
let active = true;
|
|
78
|
+
return {
|
|
79
|
+
async refresh() {
|
|
80
|
+
const request = ++sequence;
|
|
81
|
+
try {
|
|
82
|
+
const rows = await loadLoopRows(fetchJson, clock());
|
|
83
|
+
if (active && request === sequence) onResult({ rows, state: 'ready' });
|
|
84
|
+
} catch {
|
|
85
|
+
if (active && request === sequence) onResult({ rows: [], state: 'error' });
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
dispose() { active = false; },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function loopRows(schedules: readonly LoopSchedule[], now: number): LoopRow[] {
|
|
93
|
+
return schedules.map((schedule) => {
|
|
94
|
+
const last = schedule.lastRun ? Date.parse(schedule.lastRun.at) : NaN;
|
|
95
|
+
const sharedVerdict = loopCronVerdict({
|
|
96
|
+
enabled: schedule.state !== 'off',
|
|
97
|
+
lastRunAt: schedule.lastRun?.at ?? null,
|
|
98
|
+
lastStatus: schedule.lastRun?.exit != null && schedule.lastRun.exit !== 0
|
|
99
|
+
? 'failed' : schedule.lastRun?.status != null && !['ok', 'success', 'completed'].includes(schedule.lastRun.status.toLowerCase())
|
|
100
|
+
? 'failed' : schedule.lastRun?.status?.toLowerCase() ?? null,
|
|
101
|
+
intervalMs: schedule.cron ? null : schedule.intervalMs,
|
|
102
|
+
// A cron uses daemon calendar state; fixed intervals use the 2× interval rule.
|
|
103
|
+
...(schedule.cron ? { scheduleState: schedule.state } : {}),
|
|
104
|
+
}, now);
|
|
105
|
+
const noCadence = !schedule.cron && !(schedule.intervalMs && schedule.intervalMs > 0);
|
|
106
|
+
const verdict = sharedVerdict === 'alive' && (!Number.isFinite(last) || noCadence)
|
|
107
|
+
? '판정 불가' : VERDICT_LABEL[sharedVerdict];
|
|
108
|
+
return {
|
|
109
|
+
id: schedule.id,
|
|
110
|
+
name: schedule.name,
|
|
111
|
+
layer: schedule.domain || schedule.category || schedule.source,
|
|
112
|
+
owner: '미지정',
|
|
113
|
+
mode: schedule.runVia || schedule.source,
|
|
114
|
+
lastRun: Number.isFinite(last) ? schedule.lastRun!.at : null,
|
|
115
|
+
verdict,
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
}
|