elanous 0.2.14 → 0.2.15
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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -0
- package/apps/pwa/out/ceo/index.txt +33 -0
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -2
- package/package.json +1 -1
- package/scripts/brand/check.ts +7 -5
- package/scripts/ci-typecheck-changed.ts +50 -18
- package/scripts/docs/generate-graph-anatomy.ts +315 -0
- package/scripts/expose-rubric-suggest.ts +18 -12
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +30 -3
- package/scripts/marketing/generate-ax-brief.ts +140 -0
- package/scripts/marketing/generate-feature-map.ts +118 -0
- package/scripts/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/gate-node.ts +80 -14
- package/scripts/release-loop/vault-note-node.ts +240 -8
- package/scripts/release-story/draft.ts +57 -9
- package/scripts/rhythm/daily.ts +263 -76
- package/scripts/rhythm/weekly.ts +30 -9
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/src/agent-env/profile.ts +161 -0
- package/src/agent-substrate/pr-reviewer.ts +2 -0
- package/src/ask-user-question/pending-questions.ts +5 -0
- package/src/auth/github-app-token.ts +112 -13
- package/src/autopilot/build/llm-conflict-merge.ts +32 -6
- package/src/briefing/brief-items.ts +121 -0
- package/src/claims/claims-render.ts +14 -5
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/decisions-cli.ts +9 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/intake-cli.ts +46 -17
- package/src/cli/pod-cli.ts +2 -0
- package/src/cli/release-cli.ts +61 -27
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/context-bus/context-now-public.ts +16 -6
- package/src/context-bus/context-now.ts +3 -1
- package/src/context-bus/coord-events.ts +6 -4
- package/src/decisions/decision-cards.ts +33 -7
- package/src/decisions/proact-meter.ts +286 -0
- package/src/decisions/proact1-lite.ts +86 -0
- package/src/decisions/web-push-decision-cards.ts +1 -1
- package/src/domains/alert-audit.ts +271 -0
- package/src/domains/core-tools.ts +4 -3
- package/src/domains/ops-facts-tool.ts +31 -1
- package/src/harness/harness-cli-command.ts +111 -15
- package/src/harness/harness-incidents.ts +2 -0
- package/src/harness/harness-pod-dispatch.ts +100 -11
- package/src/harness/harness-queue.ts +380 -50
- package/src/harness/helper-repair.ts +87 -0
- package/src/harness/helper-scan.ts +16 -4
- package/src/harness/launch-stamp.ts +236 -0
- package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
- package/src/harness/ledger-write-guard.ts +35 -0
- package/src/harness/pod-memory-policy.ts +15 -0
- package/src/harness/repository-name.ts +55 -0
- package/src/hq/fence-audit.ts +90 -0
- package/src/hq/hq.ts +62 -4
- package/src/hq/lease.ts +4 -4
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +31 -0
- package/src/index.ts +77 -7
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +37 -15
- package/src/intake-plane/front-classifier.ts +6 -0
- package/src/intake-plane/items.ts +3 -2
- package/src/intake-plane/lens.ts +177 -0
- package/src/intake-plane/route.ts +145 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
- package/src/launch-head/seat-goal-queue.ts +198 -0
- package/src/loops/budget.ts +36 -5
- package/src/loops/checker.ts +2 -2
- package/src/loops/orchestrator/author-depth.ts +65 -9
- package/src/loops/orchestrator/author-input-check.ts +8 -3
- package/src/loops/orchestrator/author-ledger.ts +3 -1
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +11 -4
- package/src/loops/orchestrator/traffic.ts +117 -11
- package/src/loops/registry.ts +142 -61
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/maturity/tui-slash-availability.ts +31 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/http-server.ts +28 -14
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/schedules-read.ts +13 -0
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +6 -3
- package/src/release-loop/checklist.ts +13 -4
- package/src/release-loop/feature-store.ts +58 -31
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +86 -26
- package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
- package/src/seat-dispatch/seat-request-ledger.ts +133 -0
- package/src/seat-loop/seat-loop.ts +126 -10
- package/src/self-dev/dev-pipeline.ts +8 -4
- package/src/self-dev/draft-sweep.ts +90 -10
- package/src/self-dev/draft-triage-rules.ts +25 -8
- package/src/self-dev/orchestrate.ts +15 -1
- package/src/self-dev/review-provider-fallback.ts +6 -1
- package/src/self-dev/run-supervisor.ts +49 -2
- package/src/self-implement/frozen-merges.ts +3 -2
- package/src/self-implement/goal-author.ts +43 -5
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/heal-intake.ts +120 -2
- package/src/self-implement/host-regate.ts +9 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +187 -40
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +42 -7
- package/src/self-implement/run-ledger.ts +2 -0
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +1 -1
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
- package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
- package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
- package/src/telegram.ts +25 -4
- package/src/user-config.ts +29 -5
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { join } from 'node:path';
|
|
4
|
+
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
3
5
|
import { debug } from '../../debug/log.js';
|
|
4
6
|
import { defaultListHarnessProcesses } from '../../harness/harness-cli-command.js';
|
|
5
7
|
import { listHarnessQueue, type QueueItem } from '../../harness/harness-queue.js';
|
|
@@ -7,15 +9,19 @@ import { releaseLedgerRoot } from '../../instance/resolve.js';
|
|
|
7
9
|
import { listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
|
|
8
10
|
import { listSchedules } from '../../release-loop/release-schedule.js';
|
|
9
11
|
import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorSeat } from '../../user-config.js';
|
|
12
|
+
import { checkAuthorInput } from './author-input-check.js';
|
|
13
|
+
import { AuthorLedger } from './author-ledger.js';
|
|
10
14
|
import { collectWork, findOverlaps, type WorkItem } from './overlap.js';
|
|
11
15
|
import { seatOfTree, trafficTick, TRAFFIC_SEATS } from './traffic.js';
|
|
12
16
|
|
|
13
|
-
export type AuthorCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status'> & { version: string };
|
|
17
|
+
export type AuthorCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'> & { version: string };
|
|
14
18
|
export type AuthorQueueRow = Pick<QueueItem, 'seat' | 'status'> & Partial<Pick<QueueItem, 'input' | 'kind'>>;
|
|
15
19
|
export type AuthorDepthResult = {
|
|
16
20
|
seats: Array<{ seat: OrchestratorSeat; cap: number | null; running: number | null; queued: number | null;
|
|
17
21
|
target: number | null; short: number | null; wouldAuthor: Array<{ cellId: string; version: string; title: string }> }>;
|
|
18
22
|
unreadable: Array<{ source: string; reason: string }>;
|
|
23
|
+
receiptCounts?: { held: number; queuedForAuthor: number };
|
|
24
|
+
receiptFailures?: number;
|
|
19
25
|
};
|
|
20
26
|
export type AuthorDepthInput = {
|
|
21
27
|
seats: readonly OrchestratorSeat[];
|
|
@@ -84,6 +90,7 @@ export function authorDepth({ seats, caps, running, queued, cells, overlaps, now
|
|
|
84
90
|
}
|
|
85
91
|
|
|
86
92
|
export interface CollectAuthorDepthDeps {
|
|
93
|
+
root?: string;
|
|
87
94
|
seats?: readonly OrchestratorSeat[];
|
|
88
95
|
now?: Date;
|
|
89
96
|
caps?: () => Partial<Record<OrchestratorSeat, number>>;
|
|
@@ -92,12 +99,13 @@ export interface CollectAuthorDepthDeps {
|
|
|
92
99
|
cells?: (version: string) => readonly AuthorCell[];
|
|
93
100
|
overlaps?: () => readonly WorkItem[];
|
|
94
101
|
versions?: () => readonly [string, string];
|
|
102
|
+
ledger?: Pick<AuthorLedger, 'request'>;
|
|
95
103
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
96
104
|
}
|
|
97
105
|
|
|
98
|
-
function defaultVersions(now: Date): readonly [string, string] {
|
|
99
|
-
if (!existsSync(join(
|
|
100
|
-
const upcoming = listSchedules().filter(row => row.cutAt && Date.parse(row.cutAt) > now.getTime())
|
|
106
|
+
function defaultVersions(now: Date, root = releaseLedgerRoot()): readonly [string, string] {
|
|
107
|
+
if (!existsSync(join(root, 'release', 'features.sqlite'))) throw new Error('release schedule ledger unavailable');
|
|
108
|
+
const upcoming = listSchedules(root).filter(row => row.cutAt && Date.parse(row.cutAt) > now.getTime())
|
|
101
109
|
.sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
|
|
102
110
|
if (upcoming.length < 2) throw new Error('current and next release schedules unavailable');
|
|
103
111
|
return [upcoming[0]!.version, upcoming[1]!.version];
|
|
@@ -123,12 +131,13 @@ export function collectAuthorDepth(deps: CollectAuthorDepthDeps = {}): AuthorDep
|
|
|
123
131
|
if (measured.unassigned) throw new Error(`${measured.unassigned} unassigned harness launch process(es)`);
|
|
124
132
|
return Object.fromEntries(measured.seats.map(row => [row.seat, row.running]));
|
|
125
133
|
});
|
|
126
|
-
const queued = observe('queue', deps.queued ?? (() => listHarnessQueue()));
|
|
127
|
-
const
|
|
134
|
+
const queued = observe('queue', deps.queued ?? (() => listHarnessQueue({ root: deps.root })));
|
|
135
|
+
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
136
|
+
const versions = observe('versions', deps.versions ?? (() => defaultVersions(now, releaseRoot)));
|
|
128
137
|
const cells = versions === null ? null : observe('checklist', () => versions.flatMap(version => {
|
|
129
|
-
if (!deps.cells && !existsSync(join(
|
|
138
|
+
if (!deps.cells && !existsSync(join(releaseRoot, 'release', 'features.sqlite')))
|
|
130
139
|
throw new Error('checklist ledger unavailable');
|
|
131
|
-
return deps.cells?.(version) ?? listChecklist(version).items.map(item => ({ ...item, version }));
|
|
140
|
+
return deps.cells?.(version) ?? listChecklist(version, deps.root).items.map(item => ({ ...item, version }));
|
|
132
141
|
}));
|
|
133
142
|
const overlaps = observe('overlap', deps.overlaps ?? (() => {
|
|
134
143
|
const observed = collectWork();
|
|
@@ -141,9 +150,56 @@ export function collectAuthorDepth(deps: CollectAuthorDepthDeps = {}): AuthorDep
|
|
|
141
150
|
for (const row of result.seats) log('loop.orchestrator', 'would-author', {
|
|
142
151
|
seat: row.seat, target: row.target, have: row.queued, short: row.short, cells: row.wouldAuthor,
|
|
143
152
|
});
|
|
153
|
+
const ledger = deps.ledger ?? new AuthorLedger(deps.root === undefined ? {} : { path: join(deps.root, 'orchestrator', 'author-ledger.sqlite') });
|
|
154
|
+
const receiptCounts = { held: 0, queuedForAuthor: 0 };
|
|
155
|
+
let receiptFailures = 0;
|
|
156
|
+
for (const row of result.seats) for (const candidate of row.wouldAuthor) {
|
|
157
|
+
const cell = cells?.find(item => item.id === candidate.cellId && item.version === candidate.version);
|
|
158
|
+
try {
|
|
159
|
+
if (!cell) throw new Error('selected cell unavailable');
|
|
160
|
+
const text = cell.evidence ?? '';
|
|
161
|
+
const check = checkAuthorInput({ title: cell.title, text, cellId: cell.id, version: cell.version });
|
|
162
|
+
const receipt = ledger.request({ seat: row.seat, cellId: cell.id, version: cell.version, title: cell.title,
|
|
163
|
+
text, check: { verdict: check.verdict, signals: check.signals, ratio: check.implementationRatio } });
|
|
164
|
+
if (receipt.status === 'held') receiptCounts.held++;
|
|
165
|
+
if (receipt.status === 'queued-for-author') receiptCounts.queuedForAuthor++;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
receiptFailures++;
|
|
168
|
+
result.unreadable.push({ source: 'author-ledger', reason: `${candidate.version} ${candidate.cellId}: ${error instanceof Error ? error.message : String(error)}` });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
result.receiptCounts = receiptCounts;
|
|
172
|
+
result.receiptFailures = receiptFailures;
|
|
144
173
|
return result;
|
|
145
174
|
}
|
|
146
175
|
|
|
176
|
+
const SHADOW_INTERVAL_MS = 30 * 60_000;
|
|
177
|
+
|
|
178
|
+
/** The queue tick serializes callers; stamp before collection so even a failing shadow cannot run every two minutes. */
|
|
179
|
+
export function runAuthorDepthShadow(root = effectiveInstanceRoot(), now = new Date(), collect = collectAuthorDepth): void {
|
|
180
|
+
const path = join(root, 'orchestrator', 'author-depth-shadow-at');
|
|
181
|
+
const time = now.getTime();
|
|
182
|
+
if (!Number.isFinite(time)) throw new Error('invalid author-depth shadow clock');
|
|
183
|
+
if (existsSync(path)) {
|
|
184
|
+
const last = Date.parse(readFileSync(path, 'utf8').trim());
|
|
185
|
+
if (!Number.isFinite(last)) throw new Error('invalid author-depth shadow marker');
|
|
186
|
+
if (time - last < SHADOW_INTERVAL_MS) return;
|
|
187
|
+
}
|
|
188
|
+
mkdirSync(join(root, 'orchestrator'), { recursive: true });
|
|
189
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
190
|
+
try {
|
|
191
|
+
writeFileSync(temporary, now.toISOString(), { flag: 'wx', mode: 0o600 });
|
|
192
|
+
renameSync(temporary, path);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
try { unlinkSync(temporary); } catch { /* No temporary file to remove. */ }
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
const result = collect({ root, now });
|
|
198
|
+
if (result.unreadable.length || result.receiptFailures || !result.receiptCounts) {
|
|
199
|
+
throw new Error(`author-depth shadow incomplete: ${JSON.stringify({ unreadable: result.unreadable, receiptFailures: result.receiptFailures })}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
147
203
|
export function authorDepthLines(result: AuthorDepthResult): string[] {
|
|
148
204
|
return result.seats.map(row => `${row.seat} ${row.running ?? '?'}/${row.cap ?? '?'} · 대기 ${row.queued ?? '?'} · 목표 ${row.target ?? '?'} · 모자람 ${row.short ?? '?'} → ${row.wouldAuthor.map(cell => cell.cellId).join(', ') || '없음'}`);
|
|
149
205
|
}
|
|
@@ -8,7 +8,7 @@ export interface AuthorInput {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export interface AuthorInputSignal {
|
|
11
|
-
kind: 'path' | 'symbol' | 'command' | 'marker';
|
|
11
|
+
kind: 'path' | 'symbol' | 'command' | 'marker' | 'missing';
|
|
12
12
|
match: string;
|
|
13
13
|
field: 'title' | 'text';
|
|
14
14
|
reason: string;
|
|
@@ -40,15 +40,19 @@ function sentences(value: string): string[] {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export function checkAuthorInput({ title, text, cellId, version }: AuthorInput): AuthorInputCheck {
|
|
43
|
-
const
|
|
43
|
+
const validIdentity = [title, cellId, version].every(value => typeof value === 'string' && !!value.trim())
|
|
44
44
|
&& /^\d+\.\d+\.\d+$/.test(version);
|
|
45
|
+
const missingText = typeof text !== 'string' || !text.trim();
|
|
46
|
+
const valid = validIdentity && !missingText;
|
|
45
47
|
const signals: AuthorInputSignal[] = [];
|
|
48
|
+
if (validIdentity && missingText) signals.push({ kind: 'missing', match: '', field: 'text', reason: 'missing cell text' });
|
|
46
49
|
let checked = 0;
|
|
47
50
|
let candidates = 0;
|
|
48
51
|
let directed = false;
|
|
49
52
|
|
|
50
|
-
if (
|
|
53
|
+
if (validIdentity) {
|
|
51
54
|
for (const field of ['title', 'text'] as const) {
|
|
55
|
+
if (field === 'text' && missingText) continue;
|
|
52
56
|
for (const sentence of sentences(field === 'title' ? title : text)) {
|
|
53
57
|
checked++;
|
|
54
58
|
let found = false;
|
|
@@ -70,6 +74,7 @@ export function checkAuthorInput({ title, text, cellId, version }: AuthorInput):
|
|
|
70
74
|
const verdict: AuthorInputCheck['verdict'] = !valid ? 'uncheckable'
|
|
71
75
|
: signals.length === 0 ? 'approved' : directed ? 'resubmit' : 'confirm';
|
|
72
76
|
const implementationRatio = valid ? candidates / checked : null;
|
|
77
|
+
if (!valid) { checked = 0; candidates = 0; }
|
|
73
78
|
debug.log('author.par', 'input-checked', {
|
|
74
79
|
cellId, version, verdict, signals: signals.length, ratio: implementationRatio,
|
|
75
80
|
});
|
|
@@ -120,9 +120,11 @@ export class AuthorLedger {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
request(input: AuthorRequest): AuthorRow & { duplicate: boolean } {
|
|
123
|
-
for (const key of ['seat', 'cellId', 'version', 'title'
|
|
123
|
+
for (const key of ['seat', 'cellId', 'version', 'title'] as const) {
|
|
124
124
|
if (typeof input[key] !== 'string' || !input[key].trim()) throw new Error(`${key} is required`);
|
|
125
125
|
}
|
|
126
|
+
if (typeof input.text !== 'string' || (!input.text.trim() && input.check?.verdict !== 'uncheckable'))
|
|
127
|
+
throw new Error('text is required unless uncheckable');
|
|
126
128
|
if (!['approved', 'resubmit', 'confirm', 'uncheckable'].includes(input.check?.verdict)
|
|
127
129
|
|| !Array.isArray(input.check.signals)
|
|
128
130
|
|| (input.check.ratio !== null && (typeof input.check.ratio !== 'number' || !Number.isFinite(input.check.ratio))))
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ChecklistItem } from '../../release-loop/checklist.js';
|
|
2
|
+
import type { OrchestratorSeat } from '../../user-config.js';
|
|
3
|
+
|
|
4
|
+
const SEATS: readonly OrchestratorSeat[] = ['OP', 'TC', 'MK', 'UX'];
|
|
5
|
+
type WorkCell = Pick<ChecklistItem, 'owner' | 'status'>;
|
|
6
|
+
|
|
7
|
+
/** Allocate whole base slots by remaining work; this calculation does not decide launches. */
|
|
8
|
+
export function calculateSeatBaseShares({ totalSlots, currentRound, nextRound, seatCaps }: {
|
|
9
|
+
totalSlots: number;
|
|
10
|
+
currentRound: readonly WorkCell[];
|
|
11
|
+
nextRound: readonly WorkCell[];
|
|
12
|
+
seatCaps: Readonly<Record<OrchestratorSeat, number>>;
|
|
13
|
+
}): Record<OrchestratorSeat, number> {
|
|
14
|
+
if (!Number.isSafeInteger(totalSlots) || totalSlots < 0 ||
|
|
15
|
+
SEATS.some(seat => !Number.isSafeInteger(seatCaps[seat]) || seatCaps[seat] < 0)) {
|
|
16
|
+
throw new RangeError('totalSlots and seatCaps must be nonnegative safe integers');
|
|
17
|
+
}
|
|
18
|
+
const shares: Record<OrchestratorSeat, number> = { OP: 0, TC: 0, MK: 0, UX: 0 };
|
|
19
|
+
const weights: Record<OrchestratorSeat, number> = { OP: 0, TC: 0, MK: 0, UX: 0 };
|
|
20
|
+
for (const [cells, weight] of [[currentRound, 2], [nextRound, 1]] as const) {
|
|
21
|
+
for (const cell of cells) {
|
|
22
|
+
if (cell.status !== 'yellow') continue;
|
|
23
|
+
const seat = cell.owner?.split('/')[0];
|
|
24
|
+
if (SEATS.some(id => id === seat)) weights[seat as OrchestratorSeat] += weight;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let available = Math.min(totalSlots, SEATS.reduce((sum, seat) => sum + seatCaps[seat], 0));
|
|
29
|
+
let eligible = SEATS.filter(seat => weights[seat] > 0 && seatCaps[seat] > 0);
|
|
30
|
+
while (available > 0 && eligible.length > 0) {
|
|
31
|
+
const totalWeight = eligible.reduce((sum, seat) => sum + weights[seat], 0);
|
|
32
|
+
const ideal = (seat: OrchestratorSeat) => available * weights[seat] / totalWeight;
|
|
33
|
+
const capped = eligible.filter(seat => ideal(seat) >= seatCaps[seat]);
|
|
34
|
+
if (capped.length > 0) {
|
|
35
|
+
for (const seat of capped) {
|
|
36
|
+
shares[seat] = seatCaps[seat];
|
|
37
|
+
available -= seatCaps[seat];
|
|
38
|
+
}
|
|
39
|
+
eligible = eligible.filter(seat => !capped.includes(seat));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const remainders = eligible.map(seat => ({ seat, exact: ideal(seat) }));
|
|
43
|
+
for (const { seat, exact } of remainders) {
|
|
44
|
+
shares[seat] = Math.floor(exact);
|
|
45
|
+
available -= shares[seat];
|
|
46
|
+
}
|
|
47
|
+
remainders.sort((a, b) => (b.exact - Math.floor(b.exact)) - (a.exact - Math.floor(a.exact)));
|
|
48
|
+
for (const { seat } of remainders) {
|
|
49
|
+
if (available === 0) break;
|
|
50
|
+
shares[seat]++;
|
|
51
|
+
available--;
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
return shares;
|
|
56
|
+
}
|
|
@@ -16,7 +16,7 @@ const SEAT_LOOP_IDS: Readonly<Record<string, string>> = { OP: 'op-seat', MK: 'cm
|
|
|
16
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
17
|
export interface TickState {
|
|
18
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;
|
|
19
|
+
cards: TaskCard[]; skippedCards: Array<{ id: string; reason: string }>; cells: Cell[]; placed: number; unplaced?: number; delegated: number; wouldDelegate: number; rebalanced?: number; rebalanceBlocked?: Array<{ id: string; reason: string }>;
|
|
20
20
|
reconciled: { reached: number; progressing: number; blocked: number; unknown: number };
|
|
21
21
|
runSummaries?: string[];
|
|
22
22
|
nodes: Partial<Record<Node, 'ok' | 'skipped'>>;
|
|
@@ -27,7 +27,7 @@ export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Prom
|
|
|
27
27
|
/** RELPLAN1 `placeCell(input: PlacementCell)` — owner·priority·predecessors are required; the result carries no seat. */
|
|
28
28
|
export type PlacementInput = { id: string; title: string; owner: string; priority: 'P0' | 'P1' | 'P2'; predecessors: string[] };
|
|
29
29
|
export type PlaceAdapter = (cell: PlacementInput) => Promise<{ version?: string } | null> | { version?: string } | null;
|
|
30
|
-
export type RebalanceAdapter = (version: string) => Promise<unknown[]> | unknown[];
|
|
30
|
+
export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown[]; blocked: Array<{ id: string; reason: string }> }> | { decisions: unknown[]; blocked: Array<{ id: string; reason: string }> };
|
|
31
31
|
export interface TickDeps {
|
|
32
32
|
root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
|
|
33
33
|
cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
|
|
@@ -72,7 +72,8 @@ export function reconciliation(cells: readonly Cell[], requests: readonly { key:
|
|
|
72
72
|
export function reportLine(state: TickState): string {
|
|
73
73
|
const r = state.reconciled;
|
|
74
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
|
-
|
|
75
|
+
const details = [...(state.rebalanceBlocked ?? []).map(row => `⛔ 이월 거부 ${row.id}: ${row.reason}`), ...(state.runSummaries ?? [])];
|
|
76
|
+
return details.length ? `${line}\n${details.join('\n')}` : line;
|
|
76
77
|
}
|
|
77
78
|
function readCardSnapshot(root: string): { cards: TaskCard[]; skippedCards: TickState['skippedCards'] } {
|
|
78
79
|
const dir = taskCardsDir(root);
|
|
@@ -268,7 +269,13 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
268
269
|
if (mode === 'shadow') observe('exchange', 'rebalance-shadow-not-invoked', versions.length);
|
|
269
270
|
else {
|
|
270
271
|
state.rebalanced = 0;
|
|
271
|
-
|
|
272
|
+
state.rebalanceBlocked = [];
|
|
273
|
+
for (const version of versions) {
|
|
274
|
+
const result = await rebalance(version);
|
|
275
|
+
state.rebalanced += result.decisions.length;
|
|
276
|
+
state.rebalanceBlocked.push(...result.blocked);
|
|
277
|
+
for (const blocked of result.blocked) observe('exchange', `rebalance-blocked: ${blocked.id} ${blocked.reason}`, 1);
|
|
278
|
+
}
|
|
272
279
|
}
|
|
273
280
|
}
|
|
274
281
|
}
|
|
@@ -2,16 +2,19 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, realpathSync } fro
|
|
|
2
2
|
import { dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
3
3
|
import { debug } from '../../debug/log.js';
|
|
4
4
|
import { defaultListHarnessProcesses, type HarnessProcessRecord } from '../../harness/harness-cli-command.js';
|
|
5
|
-
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
5
|
+
import { effectiveInstanceRoot, releaseLedgerRoot } from '../../instance/resolve.js';
|
|
6
6
|
import { devVersion, listChecklist, type ChecklistItem } from '../../release-loop/checklist.js';
|
|
7
|
+
import { listSchedules } from '../../release-loop/release-schedule.js';
|
|
7
8
|
import { withFileLockSync } from '../../storage/file-lock.js';
|
|
8
9
|
import { getUserConfig, ORCHESTRATOR_DEFAULTS, type OrchestratorLoopConfig, type OrchestratorSeat } from '../../user-config.js';
|
|
9
10
|
import { finishAdvice, measureFinish, type FinishAdvice, type FinishMetrics, type MeasureFinishDeps } from './finish-rate.js';
|
|
11
|
+
import { collectAuthorDepth, type AuthorDepthResult, type CollectAuthorDepthDeps } from './author-depth.js';
|
|
12
|
+
import { calculateSeatBaseShares } from './seat-share.js';
|
|
10
13
|
|
|
11
14
|
export const TRAFFIC_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
|
|
12
15
|
export type TrafficCell = Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status'>;
|
|
13
16
|
export type TrafficProcess = Pick<HarnessProcessRecord, 'command' | 'elapsedSeconds' | 'cwd'> & { seat: OrchestratorSeat | null };
|
|
14
|
-
export type TrafficSeatRow = { seat: OrchestratorSeat; running: number; cap: number; idleFor: number | null; idle: boolean; nextCell: TrafficCell | null };
|
|
17
|
+
export type TrafficSeatRow = { seat: OrchestratorSeat; running: number; cap: number; baseShare: number | null; borrowed: number; lent: number; launchCap: number; idleFor: number | null; idle: boolean; nextCell: TrafficCell | null };
|
|
15
18
|
export type TrafficResult = { seats: TrafficSeatRow[]; unassigned: number; now: Date; idleMinutes: number; finish?: { metrics: FinishMetrics; advice: FinishAdvice } };
|
|
16
19
|
|
|
17
20
|
/** Same bounded walk as the CTX1 hook: a marker at the git root is checked before stopping. */
|
|
@@ -49,15 +52,23 @@ function realOrResolved(path: string): string {
|
|
|
49
52
|
const IS_LAUNCH = /(?:^|\s)(?:\S*\/)?elanous\.mjs\s+(?:(?:--test|--config-dir\s+\S+)\s+)?harness\s+(?:ask|say)(?=\s|$)/;
|
|
50
53
|
|
|
51
54
|
/** Process-to-seat resolution occurs at the boundary; this decision does no IO. */
|
|
52
|
-
export function trafficTick({ processes, now, caps, lastLaunchAt, openCells, idleMinutes = 30 }: {
|
|
55
|
+
export function trafficTick({ processes, now, caps, lastLaunchAt, idleSince, openCells, nextRound, totalSlots, idleMinutes = 30 }: {
|
|
53
56
|
processes: readonly TrafficProcess[];
|
|
54
57
|
now: Date;
|
|
55
58
|
caps: Readonly<Record<OrchestratorSeat, number>>;
|
|
56
59
|
lastLaunchAt?: Partial<Record<OrchestratorSeat, Date | null>>;
|
|
60
|
+
idleSince?: Partial<Record<OrchestratorSeat, Date | null>>;
|
|
57
61
|
openCells: readonly TrafficCell[];
|
|
62
|
+
nextRound?: readonly TrafficCell[];
|
|
63
|
+
totalSlots?: number;
|
|
58
64
|
idleMinutes?: number;
|
|
59
65
|
}): TrafficResult {
|
|
60
66
|
const launches = processes.filter(process => IS_LAUNCH.test(process.command));
|
|
67
|
+
const slotBudget = totalSlots ?? TRAFFIC_SEATS.reduce((sum, seat) => sum + caps[seat], 0);
|
|
68
|
+
// A missing next round is unknown, not an empty round with zero remaining work.
|
|
69
|
+
const baseShares = nextRound === undefined ? null : calculateSeatBaseShares({
|
|
70
|
+
totalSlots: slotBudget, currentRound: openCells, nextRound, seatCaps: caps,
|
|
71
|
+
});
|
|
61
72
|
const seats = TRAFFIC_SEATS.map((seat): TrafficSeatRow => {
|
|
62
73
|
const owned = launches.filter(process => process.seat === seat);
|
|
63
74
|
const latest = owned.reduce<number | null>((current, process) => {
|
|
@@ -66,12 +77,57 @@ export function trafficTick({ processes, now, caps, lastLaunchAt, openCells, idl
|
|
|
66
77
|
return current === null ? start : Math.max(current, start);
|
|
67
78
|
}, null);
|
|
68
79
|
const fallback = owned.length ? lastLaunchAt?.[seat]?.getTime() : undefined;
|
|
69
|
-
const
|
|
80
|
+
const idleStart = idleSince?.[seat]?.getTime();
|
|
81
|
+
const last = idleStart !== undefined && Number.isFinite(idleStart) ? idleStart
|
|
82
|
+
: latest ?? (fallback !== undefined && Number.isFinite(fallback) ? fallback : null);
|
|
70
83
|
const idleFor = last === null ? null : Math.max(0, Math.floor((now.getTime() - last) / 60_000));
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
const nextCell = openCells.find(cell => cell.status !== 'done' && cell.owner?.split('/')[0] === seat) ?? null;
|
|
85
|
+
const baseShare = baseShares?.[seat] ?? null;
|
|
86
|
+
return { seat, running: owned.length, cap: caps[seat], baseShare, borrowed: 0, lent: 0,
|
|
87
|
+
launchCap: baseShare === null ? caps[seat] : Math.max(owned.length, baseShare), idleFor,
|
|
88
|
+
idle: owned.length < caps[seat] && last !== null && now.getTime() - last > idleMinutes * 60_000, nextCell };
|
|
74
89
|
});
|
|
90
|
+
if (baseShares !== null) {
|
|
91
|
+
// Account for outstanding loans before lending new capacity. A returning
|
|
92
|
+
// lender cannot launch into a slot still occupied by the borrower's run.
|
|
93
|
+
for (const borrower of seats) {
|
|
94
|
+
let excess = Math.max(0, borrower.running - borrower.baseShare!);
|
|
95
|
+
borrower.borrowed += excess;
|
|
96
|
+
for (const lender of seats) {
|
|
97
|
+
if (!excess) break;
|
|
98
|
+
if (lender === borrower) continue;
|
|
99
|
+
const amount = Math.min(excess, Math.max(0, lender.baseShare! - lender.running - lender.lent));
|
|
100
|
+
lender.lent += amount;
|
|
101
|
+
excess -= amount;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
for (const lender of seats) {
|
|
105
|
+
if (lender.nextCell) continue;
|
|
106
|
+
let available = Math.max(0, lender.baseShare! - lender.running - lender.lent);
|
|
107
|
+
for (const borrower of seats) {
|
|
108
|
+
if (!available) break;
|
|
109
|
+
if (borrower === lender || !borrower.nextCell) continue;
|
|
110
|
+
const needed = Math.max(0, Math.min(borrower.cap, borrower.running + 1) - borrower.launchCap);
|
|
111
|
+
const amount = Math.min(available, needed);
|
|
112
|
+
lender.lent += amount;
|
|
113
|
+
borrower.borrowed += amount;
|
|
114
|
+
borrower.launchCap += amount;
|
|
115
|
+
available -= amount;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const lender of seats) lender.launchCap = Math.max(lender.running, lender.launchCap - lender.lent);
|
|
119
|
+
}
|
|
120
|
+
// Reserve one available global slot per possible request, including unknown-seat
|
|
121
|
+
// launches in the occupied count; returning a lent share does not end its old run.
|
|
122
|
+
let freeSlots = Math.max(0, slotBudget - launches.length);
|
|
123
|
+
for (const row of seats) {
|
|
124
|
+
if (row.nextCell && row.idle && row.running < row.launchCap) {
|
|
125
|
+
if (freeSlots > 0) freeSlots--;
|
|
126
|
+
else row.launchCap = row.running;
|
|
127
|
+
}
|
|
128
|
+
row.idle = row.idle && (row.nextCell === null || row.running < row.launchCap);
|
|
129
|
+
if (!row.idle) row.nextCell = null;
|
|
130
|
+
}
|
|
75
131
|
return { seats, unassigned: launches.filter(process => process.seat === null).length, now, idleMinutes };
|
|
76
132
|
}
|
|
77
133
|
|
|
@@ -92,7 +148,8 @@ export interface TrafficApplyDeps {
|
|
|
92
148
|
export function applyTraffic(result: TrafficResult, deps: TrafficApplyDeps = {}): number {
|
|
93
149
|
const log = deps.log ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data));
|
|
94
150
|
const mode = deps.mode ?? 'shadow';
|
|
95
|
-
log('loop.orchestrator', 'tick', { mode, seats: result.seats.map(({ seat, running, cap,
|
|
151
|
+
log('loop.orchestrator', 'tick', { mode, seats: result.seats.map(({ seat, running, cap, baseShare, borrowed, lent, launchCap, idleFor, idle }) =>
|
|
152
|
+
({ seat, running, cap, baseShare, borrowed, lent, launchCap, idleFor, idle })), unassigned: result.unassigned });
|
|
96
153
|
if (result.finish) {
|
|
97
154
|
const { finishSlots, launchSlots, state, reasons } = result.finish.advice;
|
|
98
155
|
log('loop.orchestrator', 'would-rebalance', { finishSlots, launchSlots, state, reasons });
|
|
@@ -140,6 +197,49 @@ export function trafficLine(result: TrafficResult, mode: 'shadow' | 'live'): str
|
|
|
140
197
|
return `${prefix} · finish rate=${rate(metrics.landingRate)} stale=${metrics.staleDrafts ?? '?'} conflict=${rate(metrics.conflictRatio)} → finish=${advice.finishSlots}/${advice.finishSlots + advice.launchSlots}`;
|
|
141
198
|
}
|
|
142
199
|
|
|
200
|
+
export type AuthorShadow = { cells: number; held: number; queuedForAuthor: number; status: 'complete' | 'incomplete'; receiptFailures: number };
|
|
201
|
+
|
|
202
|
+
/** Traffic stays authoritative: shadow collection and its ledger cannot change its decision or exit status. */
|
|
203
|
+
export function runTrafficTick({ processes, now, caps, openCells, nextRound, totalSlots, mode, finishDeps, applyDeps, authorDeps, collectAuthor = collectAuthorDepth, log = debug.log.bind(debug) }: {
|
|
204
|
+
processes: readonly TrafficProcess[];
|
|
205
|
+
now: Date;
|
|
206
|
+
caps: Readonly<Record<OrchestratorSeat, number>>;
|
|
207
|
+
openCells: readonly TrafficCell[];
|
|
208
|
+
nextRound?: readonly TrafficCell[];
|
|
209
|
+
totalSlots?: number;
|
|
210
|
+
mode: 'shadow' | 'live';
|
|
211
|
+
finishDeps?: MeasureFinishDeps;
|
|
212
|
+
applyDeps?: TrafficApplyDeps;
|
|
213
|
+
authorDeps?: Omit<CollectAuthorDepthDeps, 'caps' | 'running' | 'now'>;
|
|
214
|
+
collectAuthor?: (deps: CollectAuthorDepthDeps) => AuthorDepthResult;
|
|
215
|
+
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
216
|
+
}): { result: TrafficResult; queued: number; authorShadow: AuthorShadow | null } {
|
|
217
|
+
const metrics = measureFinish(finishDeps, now);
|
|
218
|
+
const advice = finishAdvice(metrics, TRAFFIC_SEATS.reduce((sum, seat) => sum + caps[seat], 0));
|
|
219
|
+
const result = { ...trafficTick({ processes, now, caps, openCells, nextRound, totalSlots: totalSlots ?? advice.launchSlots }),
|
|
220
|
+
finish: { metrics, advice } };
|
|
221
|
+
const queued = applyTraffic(result, { ...applyDeps, mode });
|
|
222
|
+
let authorShadow: AuthorShadow | null = null;
|
|
223
|
+
let shadowFailure: Record<string, unknown> | null = null;
|
|
224
|
+
try {
|
|
225
|
+
const depth = collectAuthor({ ...authorDeps, now, caps: () => caps,
|
|
226
|
+
running: () => Object.fromEntries(result.seats.map(row => [row.seat, row.running])) });
|
|
227
|
+
const receiptFailures = depth.receiptFailures ?? depth.unreadable.filter(item => item.source === 'author-ledger').length;
|
|
228
|
+
const incomplete = depth.unreadable.length > 0 || !depth.receiptCounts;
|
|
229
|
+
authorShadow = { cells: depth.seats.reduce((total, row) => total + row.wouldAuthor.length, 0),
|
|
230
|
+
held: depth.receiptCounts?.held ?? 0, queuedForAuthor: depth.receiptCounts?.queuedForAuthor ?? 0,
|
|
231
|
+
status: incomplete ? 'incomplete' : 'complete', receiptFailures };
|
|
232
|
+
if (incomplete) shadowFailure = { unreadable: depth.unreadable, ...(depth.receiptCounts ? {} : { reason: 'receipt counts unavailable' }) };
|
|
233
|
+
} catch (error) {
|
|
234
|
+
shadowFailure = { reason: error instanceof Error ? error.message : String(error) };
|
|
235
|
+
}
|
|
236
|
+
if (shadowFailure) {
|
|
237
|
+
try { log('loops.author-depth', 'shadow-failed', shadowFailure); }
|
|
238
|
+
catch { /* Shadow logging cannot change traffic's outcome. */ }
|
|
239
|
+
}
|
|
240
|
+
return { result, queued, authorShadow };
|
|
241
|
+
}
|
|
242
|
+
|
|
143
243
|
if (import.meta.main) {
|
|
144
244
|
try {
|
|
145
245
|
const args = process.argv.slice(2);
|
|
@@ -154,9 +254,15 @@ if (import.meta.main) {
|
|
|
154
254
|
if (observation.status !== 'ok') throw new Error(`process observation ${observation.status}`);
|
|
155
255
|
const now = new Date();
|
|
156
256
|
const processes = observation.records.map(process => ({ ...process, seat: seatOfTree(process.cwdStatus === 'unknown' ? undefined : process.cwd, cfg) }));
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
257
|
+
const upcoming = listSchedules(releaseLedgerRoot()).filter(row => Date.parse(row.cutAt) > now.getTime())
|
|
258
|
+
.sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
|
|
259
|
+
const currentIndex = upcoming.findIndex(row => row.version === version);
|
|
260
|
+
const nextVersion = currentIndex < 0 ? upcoming[0]?.version : upcoming[currentIndex + 1]?.version;
|
|
261
|
+
const { result, queued, authorShadow } = runTrafficTick({
|
|
262
|
+
processes, now, caps: cfg.seatCaps, openCells: listChecklist(version).items,
|
|
263
|
+
...(nextVersion ? { nextRound: listChecklist(nextVersion).items } : {}), mode: cfg.trafficMode,
|
|
264
|
+
});
|
|
265
|
+
if (args.includes('--json')) console.log(JSON.stringify({ ...result, mode: cfg.trafficMode, queued, version, authorShadow }));
|
|
160
266
|
else console.log(trafficLine(result, cfg.trafficMode));
|
|
161
267
|
} catch (error) {
|
|
162
268
|
console.error(`traffic: ${error instanceof Error ? error.message : String(error)}`);
|