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
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { execFileSync, spawnSync, type SpawnSyncOptionsWithStringEncoding } from 'node:child_process';
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync } from 'node:fs';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
6
|
+
import { debug } from '../../debug/log.js';
|
|
7
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
8
|
+
import {
|
|
9
|
+
defaultGitMergeSeam, defaultLlmResolve, mergeMainWithLlmResolve,
|
|
10
|
+
type LlmMergeOutcome, type MergeGitSeam,
|
|
11
|
+
} from '../../autopilot/build/llm-conflict-merge.js';
|
|
12
|
+
|
|
13
|
+
type ResyncVerdict = 'saved' | 'merge-ok-tests-failed' | 'merge-ok-no-tests' | 'unresolved' | 'guard-tripped' | 'error';
|
|
14
|
+
interface ResyncShadowEntry {
|
|
15
|
+
at: string;
|
|
16
|
+
pr: number;
|
|
17
|
+
head: string;
|
|
18
|
+
verdict: ResyncVerdict;
|
|
19
|
+
status: LlmMergeOutcome['status'] | 'error';
|
|
20
|
+
resolvedFiles: string[];
|
|
21
|
+
testFiles: string[];
|
|
22
|
+
failedTests?: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface ResyncShadowSummary { candidates: number; tried: number; saved: number; savedRatio: number | null }
|
|
25
|
+
|
|
26
|
+
type OpenPr = { number: number; headRefName: string; mergeable: string; isDraft: boolean; labels: { name: string }[] };
|
|
27
|
+
export interface ResyncShadowDeps {
|
|
28
|
+
repoRoot?: string;
|
|
29
|
+
instanceRoot?: string;
|
|
30
|
+
runGh?: (args: string[]) => string;
|
|
31
|
+
resolve?: (file: string, conflicted: string) => Promise<string>;
|
|
32
|
+
gitSeam?: MergeGitSeam;
|
|
33
|
+
runTests?: (cwd: string, files: string[]) => { ok: boolean; failedTests?: string[]; noTests?: boolean };
|
|
34
|
+
now?: () => Date;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function command(file: string, args: string[], cwd: string, timeout = 60_000): string {
|
|
38
|
+
return execFileSync(file, args, { cwd, encoding: 'utf8', timeout, maxBuffer: 20 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function changedTests(gh: (args: string[]) => string, number: number): string[] {
|
|
42
|
+
const observed: unknown = JSON.parse(gh(['pr', 'view', String(number), '--json', 'files']));
|
|
43
|
+
if (!observed || typeof observed !== 'object' || !('files' in observed) || !Array.isArray(observed.files)
|
|
44
|
+
|| observed.files.some(file => !file || typeof file.path !== 'string')) throw new Error('invalid PR files observation');
|
|
45
|
+
return [...new Set((observed.files as { path: string }[]).map(file => file.path).filter(path =>
|
|
46
|
+
path.endsWith('.test.ts') && !isAbsolute(path) && path !== '' && !path.split('/').some(part => part === '..' || part === '.' || part === '')))];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A PR test must not inherit credentials, reach the network or write outside its own worktree.
|
|
50
|
+
// If the kernel does not provide the required isolation, fail closed before starting bun.
|
|
51
|
+
const TEST_SANDBOX = `import ctypes, os, sys
|
|
52
|
+
libc = ctypes.CDLL(None, use_errno=True)
|
|
53
|
+
class Ruleset(ctypes.Structure):
|
|
54
|
+
_fields_ = [('handled_access_fs', ctypes.c_uint64)]
|
|
55
|
+
class PathRule(ctypes.Structure):
|
|
56
|
+
_fields_ = [('allowed_access', ctypes.c_uint64), ('parent_fd', ctypes.c_int32)]
|
|
57
|
+
EXEC, WRITE, READ_FILE, READ_DIR, REMOVE_DIR, REMOVE_FILE, MAKE_CHAR, MAKE_DIR, MAKE_REG, MAKE_SOCK, MAKE_FIFO, MAKE_BLOCK, MAKE_SYM = (1 << i for i in range(13))
|
|
58
|
+
REFER, TRUNCATE = 1 << 13, 1 << 14
|
|
59
|
+
READ = EXEC | READ_FILE | READ_DIR
|
|
60
|
+
WRITE_BITS = WRITE | REMOVE_DIR | REMOVE_FILE | MAKE_CHAR | MAKE_DIR | MAKE_REG | MAKE_SOCK | MAKE_FIFO | MAKE_BLOCK | MAKE_SYM | REFER | TRUNCATE
|
|
61
|
+
ALL = READ | WRITE_BITS
|
|
62
|
+
def syscall(num, *args):
|
|
63
|
+
result = libc.syscall(num, *args)
|
|
64
|
+
if result < 0: raise OSError(ctypes.get_errno(), 'syscall ' + str(num) + ': ' + os.strerror(ctypes.get_errno()))
|
|
65
|
+
return result
|
|
66
|
+
try:
|
|
67
|
+
cwd, bun, modules, report, *tests = sys.argv[1:]
|
|
68
|
+
version = syscall(444, 0, 0, 1)
|
|
69
|
+
rights = ALL & (~TRUNCATE if version < 3 else ~0) & (~REFER if version < 2 else ~0)
|
|
70
|
+
rules = Ruleset(rights)
|
|
71
|
+
fd = syscall(444, ctypes.byref(rules), ctypes.sizeof(rules), 0)
|
|
72
|
+
def allow(path, rights):
|
|
73
|
+
if not os.path.exists(path): return
|
|
74
|
+
pathfd = os.open(path, os.O_PATH | os.O_CLOEXEC)
|
|
75
|
+
try:
|
|
76
|
+
rule = PathRule(rights & rules.handled_access_fs, pathfd)
|
|
77
|
+
try: syscall(445, fd, 1, ctypes.byref(rule), 0)
|
|
78
|
+
except OSError as error: raise OSError(error.errno, path + ': ' + str(error))
|
|
79
|
+
finally: os.close(pathfd)
|
|
80
|
+
for path in ('/usr', '/lib', '/lib64', '/bin', '/etc', '/dev', '/proc/self', '/proc/thread-self', '/proc/cpuinfo', '/proc/meminfo', '/proc/stat', '/sys'):
|
|
81
|
+
allow(path, (EXEC | READ_FILE) if os.path.isfile(path) else READ)
|
|
82
|
+
allow(bun, EXEC | READ_FILE)
|
|
83
|
+
if modules != '-': allow(modules, READ)
|
|
84
|
+
allow(cwd, READ | WRITE_BITS)
|
|
85
|
+
if libc.prctl(38, 1, 0, 0, 0) != 0: raise OSError(ctypes.get_errno(), 'no_new_privs')
|
|
86
|
+
syscall(446, fd, 0)
|
|
87
|
+
os.close(fd)
|
|
88
|
+
os.environ.clear()
|
|
89
|
+
os.environ.update(PATH='/usr/bin:/bin', HOME=cwd, GH_CONFIG_DIR=cwd, TMPDIR=cwd, XDG_CONFIG_HOME=cwd, GIT_CONFIG_NOSYSTEM='1', GIT_CONFIG_GLOBAL='/dev/null', GIT_TERMINAL_PROMPT='0', GH_PROMPT_DISABLED='1')
|
|
90
|
+
print('RESYNC_SANDBOX_READY', file=sys.stderr, flush=True)
|
|
91
|
+
os.execv(bun, [bun, 'test', '--reporter=junit', '--reporter-outfile=' + report, *tests])
|
|
92
|
+
except Exception as error:
|
|
93
|
+
print('resync test sandbox: ' + str(error), file=sys.stderr)
|
|
94
|
+
sys.exit(125)`;
|
|
95
|
+
|
|
96
|
+
/** Bun's own JUnit report is the run evidence — a test file can print «1 pass» to stdout, but it cannot make the runner record a testcase. */
|
|
97
|
+
export function junitCounts(xml: string): { tests: number; failures: number; failed: string[] } | null {
|
|
98
|
+
const suites = /<testsuites\b[^>]*\btests="(\d+)"[^>]*\bfailures="(\d+)"/.exec(xml);
|
|
99
|
+
if (!suites) return null;
|
|
100
|
+
const failed = [...xml.matchAll(/<testcase\b[^>]*\bname="([^"]*)"[^>]*>\s*<failure/g)].map(match => match[1]!);
|
|
101
|
+
return { tests: Number(suites[1]), failures: Number(suites[2]), failed };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function defaultRunTests(cwd: string, files: string[]): { ok: boolean; failedTests?: string[]; noTests?: boolean } {
|
|
105
|
+
const modules = existsSync(join(cwd, 'node_modules')) ? realpathSync(join(cwd, 'node_modules')) : '-';
|
|
106
|
+
const deadline = Date.now() + 300_000;
|
|
107
|
+
const failedTests: string[] = [];
|
|
108
|
+
// Review round 3 ②: a test file the PR deleted is not runnable — drop it; none left = «no tests», not a measurement error.
|
|
109
|
+
const present = files.filter(file => existsSync(resolve(cwd, file)));
|
|
110
|
+
if (present.length === 0) return { ok: true, noTests: true };
|
|
111
|
+
// Run each declared file separately: Bun's combined summary counts files loaded, not files with tests.
|
|
112
|
+
for (const file of present) {
|
|
113
|
+
const abs = resolve(cwd, file);
|
|
114
|
+
if (!abs.startsWith(`${cwd}/`) || !realpathSync(abs).startsWith(`${cwd}/`)) throw new Error(`test path outside worktree: ${file}`);
|
|
115
|
+
const remaining = deadline - Date.now();
|
|
116
|
+
if (remaining <= 0) throw new Error(`test runner timed out before ${file}`);
|
|
117
|
+
const report = join(cwd, `.resync-junit-${randomUUID()}.xml`);
|
|
118
|
+
rmSync(report, { force: true });
|
|
119
|
+
const args: string[] = ['--user', '--map-root-user', '--net', '--fork',
|
|
120
|
+
'python3', '-c', TEST_SANDBOX, cwd, realpathSync(process.execPath), modules, report, abs];
|
|
121
|
+
const options: SpawnSyncOptionsWithStringEncoding = { cwd, env: { PATH: '/usr/bin:/bin', LANG: 'C', NODE_ENV: 'test' },
|
|
122
|
+
encoding: 'utf8', timeout: remaining, maxBuffer: 20 * 1024 * 1024 };
|
|
123
|
+
const result = spawnSync('unshare', args, options);
|
|
124
|
+
const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`;
|
|
125
|
+
if (!result.stderr?.includes('RESYNC_SANDBOX_READY')) throw new Error(`test sandbox unavailable: ${result.error?.message ?? output.trim()}`);
|
|
126
|
+
if (result.error || result.signal) throw new Error(`test runner unavailable for ${file}: ${result.error?.message ?? output.trim()}`);
|
|
127
|
+
// Review round 3 ①: never trust stdout text — read the runner's JUnit report; no report or zero testcases is not a pass.
|
|
128
|
+
const counts = existsSync(report) ? junitCounts(readFileSync(report, 'utf8')) : null;
|
|
129
|
+
rmSync(report, { force: true });
|
|
130
|
+
if (!counts || counts.tests === 0) { failedTests.push(`${file}: no test cases recorded by the runner`); continue; }
|
|
131
|
+
if (result.status === 0 && counts.failures === 0) continue;
|
|
132
|
+
failedTests.push(...(counts.failed.length ? counts.failed : [`${file}: ${counts.failures} failure(s)`]));
|
|
133
|
+
}
|
|
134
|
+
return failedTests.length ? { ok: false, failedTests } : { ok: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Re-merge open conflicts only in disposable detached worktrees; no remote writes. */
|
|
138
|
+
export async function runResyncShadow(max = 3, deps: ResyncShadowDeps = {}): Promise<ResyncShadowSummary> {
|
|
139
|
+
if (!Number.isSafeInteger(max) || max < 0) throw new Error('--max must be a nonnegative integer');
|
|
140
|
+
const repoRoot = deps.repoRoot ?? resolve(import.meta.dir, '../../..');
|
|
141
|
+
const gh = deps.runGh ?? ((args: string[]) => command('gh', args, repoRoot));
|
|
142
|
+
const observed: unknown = JSON.parse(gh(['pr', 'list', '--state', 'open', '--json', 'number,headRefName,mergeable,isDraft,labels', '--limit', '1000']));
|
|
143
|
+
if (!Array.isArray(observed) || observed.length >= 1000 || observed.some(pr => !pr || !Number.isSafeInteger(pr.number)
|
|
144
|
+
|| typeof pr.headRefName !== 'string' || typeof pr.mergeable !== 'string' || typeof pr.isDraft !== 'boolean'
|
|
145
|
+
|| !Array.isArray(pr.labels) || pr.labels.some((label: unknown) => !label || typeof label !== 'object' || typeof (label as { name?: unknown }).name !== 'string')))
|
|
146
|
+
throw new Error('incomplete or invalid PR observation');
|
|
147
|
+
const candidates = (observed as OpenPr[]).filter(pr => pr.headRefName.startsWith('self-impl/')
|
|
148
|
+
&& pr.mergeable === 'CONFLICTING'
|
|
149
|
+
&& !pr.labels.some(label => label.name === 'elanous:stalled' || label.name === 'elanous:superseded'))
|
|
150
|
+
.sort((a, b) => a.number - b.number).slice(0, max);
|
|
151
|
+
const ledger = join(deps.instanceRoot ?? effectiveInstanceRoot(), 'orchestrator', 'resync-shadow.jsonl');
|
|
152
|
+
let saved = 0;
|
|
153
|
+
let tried = 0;
|
|
154
|
+
for (const pr of candidates) {
|
|
155
|
+
const entry: ResyncShadowEntry = {
|
|
156
|
+
at: (deps.now?.() ?? new Date()).toISOString(), pr: pr.number, head: pr.headRefName,
|
|
157
|
+
verdict: 'error', status: 'error', resolvedFiles: [], testFiles: [],
|
|
158
|
+
};
|
|
159
|
+
let tmp: string | undefined;
|
|
160
|
+
try {
|
|
161
|
+
// Explicit destination refspecs make origin/<head> and origin/main fresh even without remote.origin.fetch.
|
|
162
|
+
command('git', ['fetch', 'origin', `+refs/heads/${pr.headRefName}:refs/remotes/origin/${pr.headRefName}`,
|
|
163
|
+
'+refs/heads/main:refs/remotes/origin/main'], repoRoot);
|
|
164
|
+
entry.testFiles = changedTests(gh, pr.number);
|
|
165
|
+
tmp = realpathSync(mkdtempSync(join(tmpdir(), 'resync-shadow-')));
|
|
166
|
+
command('git', ['worktree', 'add', '--detach', tmp, `origin/${pr.headRefName}`], repoRoot);
|
|
167
|
+
// Focused tests may import packages from the checkout; worktrees do not contain node_modules.
|
|
168
|
+
if (existsSync(join(repoRoot, 'node_modules'))) symlinkSync(realpathSync(join(repoRoot, 'node_modules')), join(tmp, 'node_modules'), 'dir');
|
|
169
|
+
const outcome = await mergeMainWithLlmResolve(tmp, 'origin/main', deps.resolve ?? ((file, content) => defaultLlmResolve(file, content, 'origin/main')), deps.gitSeam ?? defaultGitMergeSeam());
|
|
170
|
+
entry.status = outcome.status;
|
|
171
|
+
entry.resolvedFiles = outcome.resolvedFiles ?? [];
|
|
172
|
+
if (outcome.sizeCollapse?.length || outcome.providerFailure?.length || outcome.testDeclarationLoss?.length) {
|
|
173
|
+
entry.verdict = 'guard-tripped';
|
|
174
|
+
} else if (outcome.status === 'merged' || outcome.status === 'llm-resolved') {
|
|
175
|
+
if (entry.testFiles.length === 0) entry.verdict = 'merge-ok-no-tests';
|
|
176
|
+
else {
|
|
177
|
+
const test = (deps.runTests ?? defaultRunTests)(tmp, entry.testFiles);
|
|
178
|
+
entry.verdict = test.noTests ? 'merge-ok-no-tests' : test.ok ? 'saved' : 'merge-ok-tests-failed';
|
|
179
|
+
if (!test.ok) entry.failedTests = test.failedTests?.length ? test.failedTests : ['test runner failed'];
|
|
180
|
+
}
|
|
181
|
+
} else entry.verdict = outcome.status === 'conflict-unresolved' || outcome.status === 'up-to-date' ? 'unresolved' : 'error';
|
|
182
|
+
} catch (error) {
|
|
183
|
+
entry.verdict = 'error';
|
|
184
|
+
entry.failedTests = [error instanceof Error ? error.message : String(error)];
|
|
185
|
+
} finally {
|
|
186
|
+
if (tmp) {
|
|
187
|
+
try {
|
|
188
|
+
// Even a failed add can have registered a partially created worktree.
|
|
189
|
+
command('git', ['worktree', 'remove', '--force', tmp], repoRoot);
|
|
190
|
+
} catch (error) {
|
|
191
|
+
entry.verdict = 'error';
|
|
192
|
+
entry.failedTests = [`worktree cleanup: ${String(error)}`];
|
|
193
|
+
} finally {
|
|
194
|
+
try { rmSync(tmp, { recursive: true, force: true }); }
|
|
195
|
+
catch (error) { entry.verdict = 'error'; entry.failedTests = [`temporary directory cleanup: ${String(error)}`]; }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
mkdirSync(dirname(ledger), { recursive: true });
|
|
200
|
+
appendFileSync(ledger, `${JSON.stringify(entry)}\n`);
|
|
201
|
+
debug.log('loop.orchestrator', 'resync-shadow', { pr: entry.pr, verdict: entry.verdict, status: entry.status });
|
|
202
|
+
if (entry.verdict !== 'error') tried++;
|
|
203
|
+
if (entry.verdict === 'saved') saved++;
|
|
204
|
+
}
|
|
205
|
+
const summary = { candidates: candidates.length, tried, saved, savedRatio: tried ? saved / tried : null };
|
|
206
|
+
debug.log('loop.orchestrator', 'resync-shadow-summary', { candidates: summary.candidates, tried: summary.tried, saved });
|
|
207
|
+
return summary;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (import.meta.main) {
|
|
211
|
+
try {
|
|
212
|
+
const args = process.argv.slice(2);
|
|
213
|
+
if (args.includes('--help')) {
|
|
214
|
+
console.log('Usage: bun src/loops/orchestrator/resync-shadow.ts [--max N] [--json]');
|
|
215
|
+
} else {
|
|
216
|
+
const index = args.indexOf('--max');
|
|
217
|
+
if (args.some((arg, i) => arg !== '--json' && arg !== '--max' && (index < 0 || i !== index + 1))
|
|
218
|
+
|| (index >= 0 && (index === args.length - 1 || args.lastIndexOf('--max') !== index))) throw new Error('usage: resync-shadow.ts [--max N] [--json]');
|
|
219
|
+
const max = index < 0 ? 3 : Number(args[index + 1]);
|
|
220
|
+
const summary = await runResyncShadow(max);
|
|
221
|
+
console.log(process.argv.includes('--json') ? JSON.stringify(summary) : `resync-shadow candidates=${summary.candidates} tried=${summary.tried} saved=${summary.saved} savedRatio=${summary.savedRatio === null ? 'n/a' : `${(summary.savedRatio * 100).toFixed(1)}%`}`);
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(`resync-shadow: ${error instanceof Error ? error.message : String(error)}`);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { withFileLockSync } from '../../storage/file-lock.js';
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { CardStore, taskCardsDir, type TaskCard } from '../../task-cards/card-store.js';
|
|
5
|
+
import { Database } from 'bun:sqlite';
|
|
6
|
+
import { type ChecklistItem } from '../../release-loop/checklist.js';
|
|
7
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
8
|
+
import { getUserConfig } from '../../user-config.js';
|
|
9
|
+
import { debug } from '../../debug/log.js';
|
|
10
|
+
import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../self-dev/run-store.js';
|
|
11
|
+
|
|
12
|
+
export type Window = '08' | '12' | '18';
|
|
13
|
+
export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'reconcile' | 'report';
|
|
14
|
+
export type Mode = 'off' | 'shadow' | 'live';
|
|
15
|
+
const SEAT_LOOP_IDS: Readonly<Record<string, string>> = { OP: 'op-seat', MK: 'cmo-seat', TC: 'tc-seat', UX: 'ux-seat' };
|
|
16
|
+
export interface Cell { id: string; title: string; seat?: string; host?: string; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2'; predecessors?: string[] }
|
|
17
|
+
export interface TickState {
|
|
18
|
+
runId: string; window: Window; mode: Mode; day: string;
|
|
19
|
+
cards: TaskCard[]; skippedCards: Array<{ id: string; reason: string }>; cells: Cell[]; placed: number; unplaced?: number; delegated: number; wouldDelegate: number; rebalanced?: number;
|
|
20
|
+
reconciled: { reached: number; progressing: number; blocked: number; unknown: number };
|
|
21
|
+
runSummaries?: string[];
|
|
22
|
+
nodes: Partial<Record<Node, 'ok' | 'skipped'>>;
|
|
23
|
+
missing: string[];
|
|
24
|
+
}
|
|
25
|
+
/** FLOW1a contract: with `{ shadow: true }` the splitter only proposes cells and writes nothing (cards, ledgers, LLM side effects excepted). */
|
|
26
|
+
export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Promise<Array<{ id: string; title: string; seat?: string; host?: string }>> | Array<{ id: string; title: string; seat?: string; host?: string }>;
|
|
27
|
+
/** RELPLAN1 `placeCell(input: PlacementCell)` — owner·priority·predecessors are required; the result carries no seat. */
|
|
28
|
+
export type PlacementInput = { id: string; title: string; owner: string; priority: 'P0' | 'P1' | 'P2'; predecessors: string[] };
|
|
29
|
+
export type PlaceAdapter = (cell: PlacementInput) => Promise<{ version?: string } | null> | { version?: string } | null;
|
|
30
|
+
export type RebalanceAdapter = (version: string) => Promise<unknown[]> | unknown[];
|
|
31
|
+
export interface TickDeps {
|
|
32
|
+
root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
|
|
33
|
+
cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
|
|
34
|
+
loadAdapter?: (path: string, name: string) => Promise<unknown>;
|
|
35
|
+
checklist?: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[];
|
|
36
|
+
observe?: (event: string, data: { loopId: string; runId: string; window: Window; node: Node; reason: string; count: number; targetLoopId?: string }) => void;
|
|
37
|
+
print?: (line: string) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function kstWindow(now: Date): Window {
|
|
41
|
+
const hour = Number(new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Seoul', hour: '2-digit', hourCycle: 'h23' }).format(now));
|
|
42
|
+
return hour < 12 ? '08' : hour < 18 ? '12' : '18';
|
|
43
|
+
}
|
|
44
|
+
function kstDay(now: Date): string {
|
|
45
|
+
return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
|
|
46
|
+
}
|
|
47
|
+
export function activeNode(node: Node, window: Window): boolean {
|
|
48
|
+
return node === 'report' || (window === '08' ? node !== 'reconcile' : node === 'reconcile');
|
|
49
|
+
}
|
|
50
|
+
function validVersion(version: unknown): version is string {
|
|
51
|
+
return typeof version === 'string' && /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/.test(version);
|
|
52
|
+
}
|
|
53
|
+
export function wishCards(cards: readonly TaskCard[]): TaskCard[] {
|
|
54
|
+
return cards.filter(card => card.status === 'open' && card.goalId.startsWith('wish:') &&
|
|
55
|
+
!card.sections.some(section => section.key === 'orch:split' || section.key.startsWith('orch:split:')));
|
|
56
|
+
}
|
|
57
|
+
export function reconciliation(cells: readonly Cell[], requests: readonly { key: string; status: string }[], checklist: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[]): TickState['reconciled'] {
|
|
58
|
+
const result = { reached: 0, progressing: 0, blocked: 0, unknown: 0 };
|
|
59
|
+
const byVersion = new Map<string, Pick<ChecklistItem, 'id' | 'status'>[]>();
|
|
60
|
+
const keys = new Set(requests.filter(row => row.status === 'queued').map(row => row.key));
|
|
61
|
+
for (const cell of cells) {
|
|
62
|
+
if (!keys.has(`orch:${cell.cardId}:${cell.id}`) || !cell.version) { result.unknown++; continue; }
|
|
63
|
+
if (!byVersion.has(cell.version)) byVersion.set(cell.version, checklist(cell.version));
|
|
64
|
+
const status = byVersion.get(cell.version)!.find(item => item.id === cell.id)?.status;
|
|
65
|
+
if (status === 'green' || status === 'done') result.reached++;
|
|
66
|
+
else if (status === 'yellow') result.progressing++;
|
|
67
|
+
else if (status === 'red') result.blocked++;
|
|
68
|
+
else result.unknown++;
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
export function reportLine(state: TickState): string {
|
|
73
|
+
const r = state.reconciled;
|
|
74
|
+
const line = `orchestrator ${state.window} cards=${state.cards.length} cells=${state.cells.length} placed=${state.missing.includes('relplan1-absent') ? 'missing' : state.placed} delegated=${state.mode === 'shadow' ? `would ${state.wouldDelegate}` : state.delegated} reconciled=${r.reached}/${r.progressing}/${r.blocked}/${r.unknown} mode=${state.mode}`;
|
|
75
|
+
return state.runSummaries?.length ? `${line}\n${state.runSummaries.join('\n')}` : line;
|
|
76
|
+
}
|
|
77
|
+
function readCardSnapshot(root: string): { cards: TaskCard[]; skippedCards: TickState['skippedCards'] } {
|
|
78
|
+
const dir = taskCardsDir(root);
|
|
79
|
+
if (!existsSync(dir)) return { cards: [], skippedCards: [] };
|
|
80
|
+
const cards: TaskCard[] = [];
|
|
81
|
+
const skippedCards: TickState['skippedCards'] = [];
|
|
82
|
+
for (const name of readdirSync(dir).filter(name => /^[a-zA-Z0-9_-]+\.jsonl$/.test(name))) {
|
|
83
|
+
const id = name.slice(0, -'.jsonl'.length);
|
|
84
|
+
try {
|
|
85
|
+
let card: TaskCard | undefined;
|
|
86
|
+
const raw = readFileSync(join(dir, name), 'utf8');
|
|
87
|
+
if (raw && !raw.endsWith('\n')) throw new Error(`incomplete card journal: ${name}`);
|
|
88
|
+
for (const line of raw.split('\n').filter(Boolean)) {
|
|
89
|
+
const event = JSON.parse(line) as { type: string; id?: string; goalId?: string; title?: string; createdAt: string; key?: string; owner?: string; content?: string };
|
|
90
|
+
if (event.type === 'created' && event.id && event.goalId && event.title) card = { id: event.id, goalId: event.goalId, title: event.title, createdAt: event.createdAt, status: 'open', sections: [] };
|
|
91
|
+
else if (event.type === 'section' && card && event.key && event.owner && event.content) card.sections.push({ key: event.key, owner: event.owner, content: event.content, createdAt: event.createdAt });
|
|
92
|
+
else if (event.type === 'closed' && card) card.status = 'closed';
|
|
93
|
+
}
|
|
94
|
+
if (card) {
|
|
95
|
+
if (card.id !== id) throw new Error(`card journal id mismatch: ${name}`);
|
|
96
|
+
cards.push(card);
|
|
97
|
+
}
|
|
98
|
+
} catch (error) { skippedCards.push({ id, reason: String(error) }); }
|
|
99
|
+
}
|
|
100
|
+
return { cards: cards.sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id)), skippedCards };
|
|
101
|
+
}
|
|
102
|
+
function readChecklist(root: string, version: string): Pick<ChecklistItem, 'id' | 'status'>[] {
|
|
103
|
+
const path = join(root, 'release', 'features.sqlite');
|
|
104
|
+
if (!existsSync(path)) return [];
|
|
105
|
+
const db = new Database(path, { readonly: true, strict: true });
|
|
106
|
+
try { return db.query('SELECT feature_id AS id, status FROM assignments WHERE version = ?').all(version) as Pick<ChecklistItem, 'id' | 'status'>[]; }
|
|
107
|
+
finally { db.close(); }
|
|
108
|
+
}
|
|
109
|
+
function journal(root: string): string { return join(root, 'seat-requests', 'requests.jsonl'); }
|
|
110
|
+
function readRequests(root: string): Array<{ key: string; status: string }> {
|
|
111
|
+
const path = journal(root);
|
|
112
|
+
return existsSync(path) ? readFileSync(path, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line) as { key: string; status: string }) : [];
|
|
113
|
+
}
|
|
114
|
+
function stateFile(root: string, runId: string): string {
|
|
115
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(runId) || runId === '.' || runId === '..') throw new Error('Invalid orchestrator runId');
|
|
116
|
+
return join(root, 'loop', 'orchestrator', `${runId}.json`);
|
|
117
|
+
}
|
|
118
|
+
function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState, 'cards' | 'cells'> | null {
|
|
119
|
+
const dir = join(root, 'loop', 'orchestrator');
|
|
120
|
+
if (!existsSync(dir)) return null;
|
|
121
|
+
const states = readdirSync(dir).filter(name => /^[a-zA-Z0-9._-]+\.json$/.test(name))
|
|
122
|
+
.map(name => ({ name, state: JSON.parse(readFileSync(join(dir, name), 'utf8')) as TickState }))
|
|
123
|
+
.filter(({ state }) => state.day === day && state.mode === mode && state.window === '08' && state.nodes.delegate === 'ok');
|
|
124
|
+
if (!states.length) return null;
|
|
125
|
+
states.sort((a, b) => a.name.localeCompare(b.name));
|
|
126
|
+
const cards = new Map<string, TaskCard>();
|
|
127
|
+
const cells = new Map<string, Cell>();
|
|
128
|
+
for (const { state } of states) {
|
|
129
|
+
for (const card of state.cards) cards.set(card.id, card);
|
|
130
|
+
for (const cell of state.cells) cells.set(`${cell.cardId}:${cell.id}`, cell);
|
|
131
|
+
}
|
|
132
|
+
return { cards: [...cards.values()], cells: [...cells.values()] };
|
|
133
|
+
}
|
|
134
|
+
function initial(runId: string, window: Window, mode: Mode, day: string): TickState {
|
|
135
|
+
return { runId, window, mode, day, cards: [], skippedCards: [], cells: [], placed: 0, delegated: 0, wouldDelegate: 0,
|
|
136
|
+
reconciled: { reached: 0, progressing: 0, blocked: 0, unknown: 0 }, nodes: {}, missing: [] };
|
|
137
|
+
}
|
|
138
|
+
function graphRunId(): string | undefined {
|
|
139
|
+
const context = process.env.ELANOUS_GRAPH_CONTEXT;
|
|
140
|
+
if (!context) return undefined;
|
|
141
|
+
try {
|
|
142
|
+
const value = JSON.parse(readFileSync(context, 'utf8')) as { graphId?: string; runId?: string };
|
|
143
|
+
if (value.graphId !== 'orchestrator' || typeof value.runId !== 'string') throw new Error('invalid orchestrator graph context');
|
|
144
|
+
return value.runId;
|
|
145
|
+
} catch (error) { throw new Error(`orchestrator graph context unavailable: ${String(error)}`); }
|
|
146
|
+
}
|
|
147
|
+
async function optionalAdapter<T>(path: string, name: string): Promise<T | undefined> {
|
|
148
|
+
try { return (await import(path) as Record<string, unknown>)[name] as T | undefined; }
|
|
149
|
+
catch (error) {
|
|
150
|
+
if ((error as { code?: string }).code === 'ERR_MODULE_NOT_FOUND' || (error as { code?: string }).code === 'MODULE_NOT_FOUND') return undefined;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** A graph command invokes one node; only the per-run file carries data across processes. */
|
|
156
|
+
export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Promise<TickState> {
|
|
157
|
+
const now = deps.now ?? new Date();
|
|
158
|
+
const window = deps.window ?? kstWindow(now);
|
|
159
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
160
|
+
const runId = deps.runId ?? graphRunId() ?? `${kstDay(now)}-${window}`;
|
|
161
|
+
const path = stateFile(root, runId);
|
|
162
|
+
const configured = deps.mode ?? getUserConfig().loops?.orchestrator?.mode;
|
|
163
|
+
const mode: Mode = configured === 'off' || configured === 'live' ? configured : 'shadow';
|
|
164
|
+
const state: TickState = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as TickState : initial(runId, window, mode, kstDay(now));
|
|
165
|
+
if (state.window !== window || state.mode !== mode) throw new Error('orchestrator run window/mode changed');
|
|
166
|
+
const predecessor: Partial<Record<Node, Node>> = { split: 'intake', place: 'split', delegate: 'place', reconcile: 'delegate', report: 'reconcile' };
|
|
167
|
+
const preceding = predecessor[node];
|
|
168
|
+
if (preceding && !state.nodes[preceding]) throw new Error(`orchestrator ${node}: missing ${preceding} result`);
|
|
169
|
+
const loadAdapter = deps.loadAdapter ?? optionalAdapter;
|
|
170
|
+
const adapter = async <T>(module: string, name: string): Promise<T | undefined> => {
|
|
171
|
+
const value = await loadAdapter(module, name);
|
|
172
|
+
return typeof value === 'function' ? value as T : undefined;
|
|
173
|
+
};
|
|
174
|
+
const observe = (event: string, reason = '', count = 0, targetLoopId?: string): void => {
|
|
175
|
+
const data = { loopId: 'orchestrator', runId, window, node, reason, count, ...(targetLoopId ? { targetLoopId } : {}) };
|
|
176
|
+
try { (deps.observe ?? ((name, payload) => debug.log('loop.orchestrator', name, payload)))(event, data); }
|
|
177
|
+
catch { /* observation must not change the run */ }
|
|
178
|
+
};
|
|
179
|
+
if (mode !== 'off' && activeNode(node, window) && state.nodes[node] !== 'ok') {
|
|
180
|
+
if (node === 'intake') {
|
|
181
|
+
if (mode === 'live' && deps.cards) { state.cards = wishCards(deps.cards()); state.skippedCards = []; }
|
|
182
|
+
else {
|
|
183
|
+
const snapshot = readCardSnapshot(root);
|
|
184
|
+
state.cards = wishCards(snapshot.cards);
|
|
185
|
+
state.skippedCards = snapshot.skippedCards;
|
|
186
|
+
state.skippedCards.forEach(() => observe('exchange', 'card-unreadable', 1));
|
|
187
|
+
}
|
|
188
|
+
observe('exchange', 'wish-cards', state.cards.length);
|
|
189
|
+
} else if (node === 'split') {
|
|
190
|
+
// Shadow also asks the splitter (it only proposes cells); only live records `orch:split` on the card.
|
|
191
|
+
const split = deps.split ?? await adapter<SplitAdapter>('../../flow-loop/split.js', 'splitCard');
|
|
192
|
+
if (!split) { state.missing.push('flow1a-absent'); observe('node-missing', 'flow1a-absent', state.cards.length); }
|
|
193
|
+
state.cells = [];
|
|
194
|
+
for (const card of state.cards) {
|
|
195
|
+
const cells = split ? await split(card, { shadow: mode === 'shadow' }) : [{ id: card.id, title: card.title }];
|
|
196
|
+
for (const cell of cells) {
|
|
197
|
+
if (!cell.id?.trim() || !cell.title?.trim()) throw new Error('invalid split cell');
|
|
198
|
+
state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate', ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
|
|
199
|
+
}
|
|
200
|
+
if (split && cells.length && mode === 'live') {
|
|
201
|
+
const store = new CardStore(root);
|
|
202
|
+
try {
|
|
203
|
+
store.appendSection(card.id, { key: 'orch:split', owner: 'orchestrator', content: JSON.stringify(cells) });
|
|
204
|
+
} finally { store.close(); }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} else if (node === 'place') {
|
|
208
|
+
const place = deps.placeCell ?? await adapter<PlaceAdapter>('../../release-loop/placement.js', 'placeCell');
|
|
209
|
+
const available = typeof place === 'function';
|
|
210
|
+
if (!available) { state.missing.push('relplan1-absent'); observe('node-missing', 'relplan1-absent', state.cells.length); }
|
|
211
|
+
else if (mode === 'shadow') observe('exchange', 'relplan1-shadow-not-invoked', state.cells.length);
|
|
212
|
+
if (mode === 'live' && available) for (const cell of state.cells) {
|
|
213
|
+
if (cell.origin !== 'flow1a') continue;
|
|
214
|
+
// TC review must-fix ②: map to the placer's required shape; a cell without an owner is unplaced, not a crash.
|
|
215
|
+
if (!cell.seat) { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-owner', 1); continue; }
|
|
216
|
+
const placed = await place({ id: cell.id, title: cell.title, owner: cell.seat, priority: cell.priority ?? 'P2', predecessors: cell.predecessors ?? [] });
|
|
217
|
+
if (validVersion(placed?.version)) {
|
|
218
|
+
cell.version = placed.version;
|
|
219
|
+
state.placed++;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} else if (node === 'delegate') {
|
|
223
|
+
const path = journal(root);
|
|
224
|
+
const keys = new Set(readRequests(root).map(row => row.key));
|
|
225
|
+
for (const cell of state.cells) {
|
|
226
|
+
if (cell.origin !== 'flow1a' || !cell.seat) continue;
|
|
227
|
+
const targetLoopId = SEAT_LOOP_IDS[cell.seat];
|
|
228
|
+
if (!targetLoopId) continue;
|
|
229
|
+
const key = `orch:${cell.cardId}:${cell.id}`;
|
|
230
|
+
if (keys.has(key)) continue;
|
|
231
|
+
// Shadow shows what it would hand over even before a release is chosen (placement is not invoked in shadow).
|
|
232
|
+
if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', validVersion(cell.version) ? key : `${key} (unplaced)`, 1, targetLoopId); continue; }
|
|
233
|
+
if (!validVersion(cell.version)) continue;
|
|
234
|
+
keys.add(key);
|
|
235
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
236
|
+
// TC review must-fix ①: the seat-requests journal contract needs receiptId, and appends are serialized under the
|
|
237
|
+
// same lock as the traffic node — an invalid row stops request intake for every seat.
|
|
238
|
+
const appended = withFileLockSync(`${path}.lock`, () => {
|
|
239
|
+
const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
240
|
+
if (existing.split('\n').some(line => {
|
|
241
|
+
if (!line) return false;
|
|
242
|
+
try { return (JSON.parse(line) as { key?: string }).key === key; } catch { throw new Error('invalid seat request journal'); }
|
|
243
|
+
})) return false;
|
|
244
|
+
appendFileSync(path, `${JSON.stringify({ key, receiptId: key, seat: cell.seat, ...(cell.host !== undefined ? { host: cell.host } : {}), loopId: targetLoopId, text: cell.title, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator', cell: cell.id, version: cell.version })}\n`);
|
|
245
|
+
return true;
|
|
246
|
+
});
|
|
247
|
+
if (!appended) continue;
|
|
248
|
+
state.delegated++;
|
|
249
|
+
observe('exchange', 'queued', 1, targetLoopId);
|
|
250
|
+
}
|
|
251
|
+
} else if (node === 'reconcile') {
|
|
252
|
+
if (window !== '08' && state.cells.length === 0) {
|
|
253
|
+
const previous = restorePrevious(root, state.day, mode);
|
|
254
|
+
if (previous) { state.cards = previous.cards; state.cells = previous.cells; }
|
|
255
|
+
}
|
|
256
|
+
const requests = readRequests(root);
|
|
257
|
+
state.reconciled = reconciliation(state.cells, requests, mode === 'shadow' ? version => readChecklist(root, version) : (deps.checklist ?? (version => readChecklist(root, version))));
|
|
258
|
+
const from = new Date(`${state.day}T00:00:00+09:00`).getTime();
|
|
259
|
+
state.runSummaries = listSelfDevRuns(selfDevRunsDir(root))
|
|
260
|
+
.filter(run => run.updatedAt >= from && run.updatedAt < from + 86_400_000)
|
|
261
|
+
.map(run => runSummaryLine(run));
|
|
262
|
+
observe('exchange', 'reconciled', state.cells.length);
|
|
263
|
+
if (window === '18') {
|
|
264
|
+
const rebalance = await adapter<RebalanceAdapter>('../../release-loop/placement.js', 'rebalance');
|
|
265
|
+
if (!rebalance) { state.missing.push('k1b-absent'); observe('node-missing', 'k1b-absent', 1); }
|
|
266
|
+
else {
|
|
267
|
+
const versions = [...new Set(state.cells.map(cell => cell.version).filter(validVersion))];
|
|
268
|
+
if (mode === 'shadow') observe('exchange', 'rebalance-shadow-not-invoked', versions.length);
|
|
269
|
+
else {
|
|
270
|
+
state.rebalanced = 0;
|
|
271
|
+
for (const version of versions) state.rebalanced += (await rebalance(version)).length;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} else if (node === 'report') (deps.print ?? console.log)(reportLine(state));
|
|
276
|
+
state.nodes[node] = 'ok';
|
|
277
|
+
} else if (state.nodes[node] !== 'ok') {
|
|
278
|
+
// A rerun of a node that already completed keeps its result (review round 3): only never-run nodes become skipped.
|
|
279
|
+
state.nodes[node] = 'skipped';
|
|
280
|
+
}
|
|
281
|
+
observe('tick', state.nodes[node], state.cells.length);
|
|
282
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
283
|
+
writeFileSync(path, JSON.stringify(state));
|
|
284
|
+
return state;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (import.meta.main) {
|
|
288
|
+
const node = process.argv[2];
|
|
289
|
+
const index = process.argv.indexOf('--window');
|
|
290
|
+
const window = index < 0 ? undefined : process.argv[index + 1];
|
|
291
|
+
if (!['intake', 'split', 'place', 'delegate', 'reconcile', 'report'].includes(node ?? '') ||
|
|
292
|
+
(window !== undefined && !['08', '12', '18'].includes(window))) {
|
|
293
|
+
console.error('orchestrator: expected <intake|split|place|delegate|reconcile|report> [--window 08|12|18]');
|
|
294
|
+
process.exitCode = 2;
|
|
295
|
+
} else {
|
|
296
|
+
void (async () => {
|
|
297
|
+
try { await (await import('../../domains/standalone-log-sink.js')).registerStandaloneLogSink('orchestrator'); }
|
|
298
|
+
catch { /* local execution can still use the per-run file */ }
|
|
299
|
+
await runOrchestratorNode(node as Node, { window: window as Window | undefined });
|
|
300
|
+
})().catch(error => { console.error(`orchestrator ${node}: ${String(error)}`); process.exitCode = 1; });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -6,12 +6,13 @@ import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
|
6
6
|
import { devVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
|
|
7
7
|
import { withFileLockSync } from '../../storage/file-lock.js';
|
|
8
8
|
import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorLoopConfig, type OrchestratorSeat } from '../../user-config.js';
|
|
9
|
+
import { finishAdvice, measureFinish, type FinishAdvice, type FinishMetrics, type MeasureFinishDeps } from './finish-rate.js';
|
|
9
10
|
|
|
10
11
|
export const TRAFFIC_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
|
|
11
12
|
export type TrafficCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status'>;
|
|
12
13
|
export type TrafficProcess = Pick<HarnessProcessRecord, 'command' | 'elapsedSeconds' | 'cwd'> & { seat: OrchestratorSeat | null };
|
|
13
14
|
export type TrafficSeatRow = { seat: OrchestratorSeat; running: number; cap: number; idleFor: number | null; idle: boolean; nextCell: TrafficCell | null };
|
|
14
|
-
export type TrafficResult = { seats: TrafficSeatRow[]; unassigned: number; now: Date; idleMinutes: number };
|
|
15
|
+
export type TrafficResult = { seats: TrafficSeatRow[]; unassigned: number; now: Date; idleMinutes: number; finish?: { metrics: FinishMetrics; advice: FinishAdvice } };
|
|
15
16
|
|
|
16
17
|
/** Same bounded walk as the CTX1 hook: a marker at the git root is checked before stopping. */
|
|
17
18
|
export function seatOfTree(cwd: string | undefined, cfg: Pick<OrchestratorLoopConfig, 'seatTrees'>): OrchestratorSeat | null {
|
|
@@ -74,6 +75,12 @@ export function trafficTick({ processes, now, caps, lastLaunchAt, openCells, idl
|
|
|
74
75
|
return { seats, unassigned: launches.filter(process => process.seat === null).length, now, idleMinutes };
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
export function withFinishAdvice(result: TrafficResult, deps: MeasureFinishDeps = {}): TrafficResult {
|
|
79
|
+
const metrics = measureFinish(deps, result.now);
|
|
80
|
+
const totalSlots = result.seats.reduce((total, row) => total + row.cap, 0);
|
|
81
|
+
return { ...result, finish: { metrics, advice: finishAdvice(metrics, totalSlots) } };
|
|
82
|
+
}
|
|
83
|
+
|
|
77
84
|
export type TrafficRequest = { key: string; receiptId: string; seat: OrchestratorSeat; text: string; status: 'queued'; queuedAt: string; source: 'orchestrator-traffic' };
|
|
78
85
|
export interface TrafficApplyDeps {
|
|
79
86
|
mode?: 'shadow' | 'live';
|
|
@@ -86,6 +93,10 @@ export function applyTraffic(result: TrafficResult, deps: TrafficApplyDeps = {})
|
|
|
86
93
|
const log = deps.log ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data));
|
|
87
94
|
const mode = deps.mode ?? 'shadow';
|
|
88
95
|
log('loop.orchestrator', 'tick', { mode, seats: result.seats.map(({ seat, running, cap, idleFor, idle }) => ({ seat, running, cap, idleFor, idle })), unassigned: result.unassigned });
|
|
96
|
+
if (result.finish) {
|
|
97
|
+
const { finishSlots, launchSlots, state, reasons } = result.finish.advice;
|
|
98
|
+
log('loop.orchestrator', 'would-rebalance', { finishSlots, launchSlots, state, reasons });
|
|
99
|
+
}
|
|
89
100
|
let queued = 0;
|
|
90
101
|
for (const row of result.seats) {
|
|
91
102
|
if (!row.idle) continue;
|
|
@@ -122,7 +133,11 @@ export function applyTraffic(result: TrafficResult, deps: TrafficApplyDeps = {})
|
|
|
122
133
|
export function trafficLine(result: TrafficResult, mode: 'shadow' | 'live'): string {
|
|
123
134
|
const parts = result.seats.map(row => `${row.seat} ${row.running}/${row.cap}`);
|
|
124
135
|
const idle = result.seats.filter(row => row.idle).map(row => row.seat);
|
|
125
|
-
|
|
136
|
+
const prefix = `traffic ${parts.join(' · ')} · idle=${idle.length ? idle.join(',') : '없음'} · mode=${mode}`;
|
|
137
|
+
if (!result.finish) return prefix;
|
|
138
|
+
const { metrics, advice } = result.finish;
|
|
139
|
+
const rate = (value: number | null) => value === null ? '?' : value.toFixed(2);
|
|
140
|
+
return `${prefix} · finish rate=${rate(metrics.landingRate)} stale=${metrics.staleDrafts ?? '?'} conflict=${rate(metrics.conflictRatio)} → finish=${advice.finishSlots}/${advice.finishSlots + advice.launchSlots}`;
|
|
126
141
|
}
|
|
127
142
|
|
|
128
143
|
if (import.meta.main) {
|
|
@@ -139,7 +154,7 @@ if (import.meta.main) {
|
|
|
139
154
|
if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
|
|
140
155
|
const now = new Date();
|
|
141
156
|
const processes = observation.records.map(process => ({ ...process, seat: seatOfTree(process.cwdStatus === 'unknown' ? undefined : process.cwd, cfg) }));
|
|
142
|
-
const result = trafficTick({ processes, now, caps: cfg.seatCaps, openCells: listChecklist(version).items });
|
|
157
|
+
const result = withFinishAdvice(trafficTick({ processes, now, caps: cfg.seatCaps, openCells: listChecklist(version).items }));
|
|
143
158
|
const queued = applyTraffic(result, { mode: cfg.trafficMode });
|
|
144
159
|
if (args.includes('--json')) console.log(JSON.stringify({ ...result, mode: cfg.trafficMode, queued, version }));
|
|
145
160
|
else console.log(trafficLine(result, cfg.trafficMode));
|