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,26 +1,31 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readlinkSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
5
|
import { Database } from 'bun:sqlite';
|
|
6
6
|
import { debug } from '../debug/log.js';
|
|
7
|
-
import { effectiveInstanceRoot, prodInstanceRoot } from '../instance/resolve.js';
|
|
8
|
-
import {
|
|
7
|
+
import { effectiveInstanceRoot, prodInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
|
|
8
|
+
import { listChecklist, type ChecklistItem } from '../release-loop/checklist.js';
|
|
9
|
+
import { listSchedules } from '../release-loop/release-schedule.js';
|
|
10
|
+
import { withFileLockSync } from '../storage/file-lock.js';
|
|
11
|
+
import { trafficTick, TRAFFIC_SEATS, type TrafficCell } from '../loops/orchestrator/traffic.js';
|
|
12
|
+
import { decideSpawn, seatCapDetails, seatCapReason } from '../loops/budget.js';
|
|
9
13
|
import { listSelfDevRuns, processBirthId, selfDevRunsDir } from '../self-dev/run-store.js';
|
|
10
|
-
import { getUserConfig } from '../user-config.js';
|
|
14
|
+
import { getUserConfig, userConfigPath, type OrchestratorLoopConfig } from '../user-config.js';
|
|
11
15
|
import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
|
|
12
16
|
import { leaseKubectl, measurePoolLease } from '../task-orchestrator/surfaces/pod-lease.js';
|
|
13
17
|
import { parsePodPool, podPoolHostLease, resolvePodPoolSpec } from '../task-orchestrator/surfaces/pod-pool.js';
|
|
14
18
|
import { writeHarnessQueueReceipt } from './harness-queue-child.js';
|
|
19
|
+
import { finishAdvice, measureFinish, type FinishMetrics } from '../loops/orchestrator/finish-rate.js';
|
|
15
20
|
|
|
16
21
|
export type QueueSeat = 'OP' | 'TC' | 'MK' | 'UX';
|
|
17
22
|
export type QueueItem = {
|
|
18
23
|
id: string; seat: QueueSeat; kind: 'say' | 'ask'; input: string; hold: boolean; heavy: boolean;
|
|
19
|
-
at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string;
|
|
24
|
+
at: string; status: 'queued' | 'launching' | 'launched' | 'finished'; pid?: number; launchId?: string; idempotencyKey?: string; waitingReason?: string;
|
|
20
25
|
};
|
|
21
26
|
export type QueuePool = { running: number; pending: number; reserved: number; limit: number };
|
|
22
27
|
export type QueueTick = { outcome: 'launched' | 'waiting' | 'skipped'; item?: QueueItem; reason: string };
|
|
23
|
-
export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string };
|
|
28
|
+
export type QueueProcess = { pid: number; seat?: QueueSeat; launchId?: string; runId?: string; rootPid?: number };
|
|
24
29
|
type ProcessProbe = {
|
|
25
30
|
run?: typeof spawnSync; platform?: NodeJS.Platform;
|
|
26
31
|
cwd?: (pid: number) => string;
|
|
@@ -51,7 +56,71 @@ export interface HarnessQueueDeps {
|
|
|
51
56
|
alive?: (pid: number) => boolean;
|
|
52
57
|
processes?: () => readonly QueueProcess[];
|
|
53
58
|
receipt?: (root: string, launchId: string) => 'started' | 'finished' | 'not-started' | null;
|
|
59
|
+
now?: () => Date;
|
|
60
|
+
authorShadow?: (root: string, now: Date) => void | Promise<void>;
|
|
61
|
+
idleRequest?: (root: string, now: Date, items: readonly QueueItem[], deps: HarnessQueueDeps) => void | Promise<void>;
|
|
62
|
+
idleCells?: (now: Date) => { current: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[];
|
|
63
|
+
next: readonly Pick<ChecklistItem, 'id' | 'title' | 'owner' | 'status' | 'evidence'>[] };
|
|
64
|
+
idleLog?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
54
65
|
log?: (event: 'enqueued' | 'launched' | 'waiting' | 'skipped', data: Record<string, unknown>) => void;
|
|
66
|
+
/** FINISH-RATE metrics source; a test process without it skips the real measurement. */
|
|
67
|
+
finishMetrics?: (now: Date) => FinishMetrics | Promise<FinishMetrics>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const FINISH_CACHE_MS = 10 * 60 * 1_000;
|
|
71
|
+
const FINISH_KEYS = ['launched', 'landed', 'landingRate', 'staleDrafts', 'conflictRatio', 'unknownMergeable'] as const;
|
|
72
|
+
|
|
73
|
+
/** A cache row missing any metric or its reasons map is re-measured, never fed to finishAdvice. */
|
|
74
|
+
function validFinishMetrics(value: unknown): value is FinishMetrics {
|
|
75
|
+
if (!value || typeof value !== 'object') return false;
|
|
76
|
+
const row = value as Record<string, unknown>;
|
|
77
|
+
return FINISH_KEYS.every((key) => row[key] === null || (typeof row[key] === 'number' && Number.isFinite(row[key])))
|
|
78
|
+
&& !!row.reasons && typeof row.reasons === 'object' && !Array.isArray(row.reasons);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Reads finish metrics at most once per 10 minutes per root; unreadable or missing → null (never a hold). */
|
|
82
|
+
async function cachedFinishMetrics(root: string, now: Date, deps: HarnessQueueDeps): Promise<FinishMetrics | null> {
|
|
83
|
+
// Checked before the cache: a cached real measurement must not reach an uninjected test tick either.
|
|
84
|
+
if (!deps.finishMetrics && (process.env.NODE_ENV === 'test' || process.env.ELANOUS_TEST_HOME)) {
|
|
85
|
+
debug.log('loop.orchestrator', 'finish-skipped-test', {});
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const path = join(root, 'harness', 'finish-metrics.json');
|
|
89
|
+
try {
|
|
90
|
+
if (existsSync(path)) {
|
|
91
|
+
const cached = JSON.parse(readFileSync(path, 'utf8')) as { at?: unknown; metrics?: unknown };
|
|
92
|
+
const at = typeof cached.at === 'string' ? Date.parse(cached.at) : NaN;
|
|
93
|
+
if (Number.isFinite(at) && now.getTime() - at >= 0 && now.getTime() - at < FINISH_CACHE_MS
|
|
94
|
+
&& validFinishMetrics(cached.metrics)) return cached.metrics;
|
|
95
|
+
}
|
|
96
|
+
} catch { /* A broken cache is re-measured. */ }
|
|
97
|
+
const measure = deps.finishMetrics ?? (() => measureFinish());
|
|
98
|
+
const metrics = await measure(now);
|
|
99
|
+
try {
|
|
100
|
+
mkdirSync(join(root, 'harness'), { recursive: true });
|
|
101
|
+
writeFileSync(path, JSON.stringify({ at: now.toISOString(), metrics }));
|
|
102
|
+
} catch (error) { debug.log('loop.orchestrator', 'finish-cache-write-failed', { reason: String(error) }); }
|
|
103
|
+
return metrics;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** FINISH-RATE: a backlogged board holds this launch when the pool already fills the launch share. */
|
|
107
|
+
async function finishGateReason(root: string, now: Date, mode: 'off' | 'shadow' | 'on', running: number, totalSlots: number,
|
|
108
|
+
deps: HarnessQueueDeps): Promise<string | null> {
|
|
109
|
+
if (mode === 'off') return null;
|
|
110
|
+
let metrics: FinishMetrics | null;
|
|
111
|
+
try { metrics = await cachedFinishMetrics(root, now, deps); }
|
|
112
|
+
catch (error) {
|
|
113
|
+
debug.log('loop.orchestrator', 'finish-measure-failed', { reason: String(error) });
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if (!metrics) return null;
|
|
117
|
+
const advice = finishAdvice(metrics, totalSlots);
|
|
118
|
+
if (advice.state !== 'backlogged' || running < advice.launchSlots) return null;
|
|
119
|
+
debug.log('loop.orchestrator', 'finish-rebalanced', {
|
|
120
|
+
finishSlots: advice.finishSlots, launchSlots: advice.launchSlots, running, reasons: advice.reasons, mode,
|
|
121
|
+
});
|
|
122
|
+
if (mode === 'shadow') return null;
|
|
123
|
+
return `마무리 우선 — finish=${advice.finishSlots}/${totalSlots} · 사유 ${advice.reasons.join(', ')}`;
|
|
55
124
|
}
|
|
56
125
|
|
|
57
126
|
export function harnessQueuePath(root = effectiveInstanceRoot()): string {
|
|
@@ -73,7 +142,8 @@ function read(path: string): QueueItem[] {
|
|
|
73
142
|
&& ['queued', 'launching', 'launched', 'finished'].includes(row.status)
|
|
74
143
|
&& (row.pid === undefined || Number.isSafeInteger(row.pid))
|
|
75
144
|
&& (row.launchId === undefined || typeof row.launchId === 'string')
|
|
76
|
-
&& (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
|
|
145
|
+
&& (row.idempotencyKey === undefined || typeof row.idempotencyKey === 'string')
|
|
146
|
+
&& (row.waitingReason === undefined || typeof row.waitingReason === 'string'))) {
|
|
77
147
|
throw new Error('harness queue: invalid queue file (no launch)');
|
|
78
148
|
}
|
|
79
149
|
return value as QueueItem[];
|
|
@@ -90,6 +160,29 @@ function save(path: string, items: QueueItem[]): void {
|
|
|
90
160
|
}
|
|
91
161
|
}
|
|
92
162
|
|
|
163
|
+
function lastLaunchedSeat(path: string): QueueSeat | undefined {
|
|
164
|
+
const marker = `${path}.round-robin.json`;
|
|
165
|
+
if (!existsSync(marker)) return undefined;
|
|
166
|
+
const value: unknown = JSON.parse(readFileSync(marker, 'utf8'));
|
|
167
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
168
|
+
|| !queueSeatNames.some((name) => name === (value as { lastSeat?: unknown }).lastSeat)) {
|
|
169
|
+
throw new Error('harness queue: invalid round-robin state (no launch)');
|
|
170
|
+
}
|
|
171
|
+
return (value as { lastSeat: QueueSeat }).lastSeat;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function saveLastLaunchedSeat(path: string, lastSeat: QueueSeat): void {
|
|
175
|
+
const marker = `${path}.round-robin.json`;
|
|
176
|
+
const temporary = `${marker}.${randomUUID()}.tmp`;
|
|
177
|
+
try {
|
|
178
|
+
writeFileSync(temporary, JSON.stringify({ lastSeat }), { flag: 'wx', mode: 0o600 });
|
|
179
|
+
renameSync(temporary, marker);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
try { unlinkSync(temporary); } catch { /* write did not finish */ }
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
93
186
|
// SQLite OS locks release on process death, including death while awaiting a launch.
|
|
94
187
|
async function locked<T>(root: string, action: (path: string) => Promise<T>): Promise<T> {
|
|
95
188
|
const path = harnessQueuePath(root);
|
|
@@ -309,6 +402,7 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
|
|
|
309
402
|
const attributed: { seat: QueueSeat; depth: number; source: 'env' | 'flag' | 'ledger' | 'cwd'; priority: number }[] = [];
|
|
310
403
|
const worktrees: { seat: QueueSeat; depth: number }[] = [];
|
|
311
404
|
let launchId: string | undefined;
|
|
405
|
+
let runId: string | undefined;
|
|
312
406
|
for (const { process: { pid, command }, depth } of members) {
|
|
313
407
|
let envSeat: string | undefined;
|
|
314
408
|
if (platform === 'linux') {
|
|
@@ -316,6 +410,7 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
|
|
|
316
410
|
const env = (probe.environ ?? ((id: number) => readFileSync(`/proc/${id}/environ`, 'utf8')))(pid).split('\0');
|
|
317
411
|
envSeat = env.find((entry) => entry.startsWith('ELANOUS_HARNESS_SEAT='))?.slice('ELANOUS_HARNESS_SEAT='.length);
|
|
318
412
|
launchId ??= env.find((entry) => entry.startsWith('ELANOUS_HARNESS_QUEUE_LAUNCH='))?.slice('ELANOUS_HARNESS_QUEUE_LAUNCH='.length);
|
|
413
|
+
runId ??= env.find((entry) => entry.startsWith('ELANOUS_RUN_ID='))?.slice('ELANOUS_RUN_ID='.length);
|
|
319
414
|
} catch { /* Unknown attribution is charged to all seats. */ }
|
|
320
415
|
}
|
|
321
416
|
const flag = /--seat[=\s]+(OP|TC|MK|UX)(?:\s|$)/.exec(command)?.[1] as QueueSeat | undefined;
|
|
@@ -352,7 +447,8 @@ export function readHarnessQueueProcesses(probe: ProcessProbe = {}): readonly Qu
|
|
|
352
447
|
const assigned = seats.size === 1 ? top[0] : undefined;
|
|
353
448
|
try { debug.log('harness.queue', 'attributed', { pid: root.pid, seat: assigned?.seat ?? null, source: assigned?.source ?? 'all' }); }
|
|
354
449
|
catch { /* Observation does not block inventory. */ }
|
|
355
|
-
rows.push({ pid: root.pid, ...(assigned ? { seat: assigned.seat } : {}), ...(launchId ? { launchId } : {})
|
|
450
|
+
rows.push({ pid: root.pid, ...(assigned ? { seat: assigned.seat } : {}), ...(launchId ? { launchId } : {}),
|
|
451
|
+
...(runId ? { runId } : {}) });
|
|
356
452
|
}
|
|
357
453
|
return rows;
|
|
358
454
|
}
|
|
@@ -421,34 +517,241 @@ export async function reconcileHarnessQueue(id: string, deps: HarnessQueueDeps =
|
|
|
421
517
|
});
|
|
422
518
|
}
|
|
423
519
|
|
|
520
|
+
const warnedLegacySeatCapPaths = new Set<string>();
|
|
521
|
+
|
|
522
|
+
function warnLegacySeatCap(path: string): void {
|
|
523
|
+
if (warnedLegacySeatCapPaths.has(path)) return;
|
|
524
|
+
try {
|
|
525
|
+
const raw: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
526
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return;
|
|
527
|
+
const harness = (raw as Record<string, unknown>).harness;
|
|
528
|
+
if (!harness || typeof harness !== 'object' || Array.isArray(harness)) return;
|
|
529
|
+
const queue = (harness as Record<string, unknown>).queue;
|
|
530
|
+
if (!queue || typeof queue !== 'object' || Array.isArray(queue)
|
|
531
|
+
|| !Object.hasOwn(queue, 'seatCap')) return;
|
|
532
|
+
warnedLegacySeatCapPaths.add(path);
|
|
533
|
+
debug.log('harness.queue', 'legacy-seat-cap-ignored', {
|
|
534
|
+
key: 'harness.queue.seatCap', replacement: 'loops.orchestrator.seatCaps', path,
|
|
535
|
+
}, { level: 'warn' });
|
|
536
|
+
} catch { /* An unreadable config cannot affect dispatch or observation. */ }
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function groupQueueRuns(processes: readonly QueueProcess[]): { seat?: QueueSeat; launchIds: Set<string> }[] {
|
|
540
|
+
const groups: { seats: Set<QueueSeat>; unknown: boolean; launchIds: Set<string>; keys: Set<string> }[] = [];
|
|
541
|
+
for (const row of processes) {
|
|
542
|
+
const keys = [row.runId && `run:${row.runId}`, `root:${row.rootPid ?? row.pid}`,
|
|
543
|
+
row.launchId && `launch:${row.launchId}`, `pid:${row.pid}`].filter((key): key is string => Boolean(key));
|
|
544
|
+
const matching = groups.filter((group) => keys.some((key) => group.keys.has(key)));
|
|
545
|
+
const group = matching[0] ?? { seats: new Set<QueueSeat>(), unknown: false,
|
|
546
|
+
launchIds: new Set<string>(), keys: new Set<string>() };
|
|
547
|
+
for (const other of matching.slice(1)) {
|
|
548
|
+
for (const key of other.keys) group.keys.add(key);
|
|
549
|
+
for (const seat of other.seats) group.seats.add(seat);
|
|
550
|
+
for (const id of other.launchIds) group.launchIds.add(id);
|
|
551
|
+
group.unknown ||= other.unknown;
|
|
552
|
+
groups.splice(groups.indexOf(other), 1);
|
|
553
|
+
}
|
|
554
|
+
for (const key of keys) group.keys.add(key);
|
|
555
|
+
if (row.seat) group.seats.add(row.seat);
|
|
556
|
+
else group.unknown = true;
|
|
557
|
+
if (row.launchId) group.launchIds.add(row.launchId);
|
|
558
|
+
if (!matching.length) groups.push(group);
|
|
559
|
+
}
|
|
560
|
+
return groups.map((group) => ({
|
|
561
|
+
...(group.unknown || group.seats.size !== 1 ? {} : { seat: [...group.seats][0] }),
|
|
562
|
+
launchIds: group.launchIds,
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const IDLE_REQUEST_INTERVAL_MS = 15 * 60_000;
|
|
567
|
+
const IDLE_REQUEST_MINUTES = 30;
|
|
568
|
+
type IdleRequestState = { checkedAt: string; idleSince: Partial<Record<QueueSeat, string>>; notified?: string[] };
|
|
569
|
+
|
|
570
|
+
function idleRequestCells(now: Date, root: string): ReturnType<NonNullable<HarnessQueueDeps['idleCells']>> {
|
|
571
|
+
const ledger = root === effectiveInstanceRoot() ? releaseLedgerRoot() : root;
|
|
572
|
+
if (!existsSync(join(ledger, 'release', 'features.sqlite'))) throw new Error('release checklist ledger unavailable');
|
|
573
|
+
const upcoming = listSchedules(ledger).filter(row => Date.parse(row.cutAt) > now.getTime())
|
|
574
|
+
.sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
|
|
575
|
+
if (!upcoming[0]) throw new Error('current release schedule unavailable');
|
|
576
|
+
return { current: listChecklist(upcoming[0].version, ledger).items,
|
|
577
|
+
next: upcoming[1] ? listChecklist(upcoming[1].version, ledger).items : [] };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function hasRunEvidence(cell: Pick<ChecklistItem, 'evidence'>): boolean {
|
|
581
|
+
return /(?:\brun[-_][a-z0-9-]+\b|\b(?:런|run)(?:\s*(?:근거|id|ID))?\s*[::#]\s*\S+)/i.test(cell.evidence ?? '');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** The queue lock serializes the observation marker; request journal lock also covers external traffic writers. */
|
|
585
|
+
export function requestIdleSeats(root: string, now: Date, items: readonly QueueItem[], deps: HarnessQueueDeps = {}): void {
|
|
586
|
+
const config: OrchestratorLoopConfig | undefined = (deps.configPath ? getUserConfig(deps.configPath) : getUserConfig()).loops?.orchestrator;
|
|
587
|
+
const mode = config?.idleRequest ?? 'shadow';
|
|
588
|
+
if (mode === 'off') return;
|
|
589
|
+
if (!Number.isFinite(now.getTime())) throw new Error('invalid idle request clock');
|
|
590
|
+
const marker = join(root, 'orchestrator', 'idle-request.json');
|
|
591
|
+
const previous: IdleRequestState = existsSync(marker) ? JSON.parse(readFileSync(marker, 'utf8')) as IdleRequestState
|
|
592
|
+
: { checkedAt: new Date(now.getTime() - IDLE_REQUEST_INTERVAL_MS).toISOString(), idleSince: {} };
|
|
593
|
+
if (!previous || typeof previous !== 'object' || !previous.idleSince || typeof previous.idleSince !== 'object'
|
|
594
|
+
|| !Number.isFinite(Date.parse(previous.checkedAt)) || (previous.notified !== undefined
|
|
595
|
+
&& (!Array.isArray(previous.notified) || previous.notified.some(key => typeof key !== 'string')))
|
|
596
|
+
|| Object.entries(previous.idleSince).some(([seat, value]) =>
|
|
597
|
+
!TRAFFIC_SEATS.some(name => name === seat) || typeof value !== 'string' || !Number.isFinite(Date.parse(value)))) {
|
|
598
|
+
throw new Error('invalid idle request marker');
|
|
599
|
+
}
|
|
600
|
+
const processes = (deps.processes ?? readHarnessQueueProcesses)();
|
|
601
|
+
const groups = groupQueueRuns(processes);
|
|
602
|
+
const reservations = items.filter(item => item.status === 'launching' || item.status === 'launched').filter(item => {
|
|
603
|
+
if (item.launchId && groups.some(group => group.launchIds.has(item.launchId!))) return false;
|
|
604
|
+
const state = item.launchId ? (deps.receipt ?? receipt)(root, item.launchId) : null;
|
|
605
|
+
return state !== 'finished' && state !== 'not-started';
|
|
606
|
+
});
|
|
607
|
+
const counts = Object.fromEntries(TRAFFIC_SEATS.map(seat => [seat,
|
|
608
|
+
groups.filter(group => !group.seat || group.seat === seat).length
|
|
609
|
+
+ reservations.filter(item => item.seat === seat).length])) as Record<QueueSeat, number>;
|
|
610
|
+
const idleSince = { ...previous.idleSince };
|
|
611
|
+
for (const seat of TRAFFIC_SEATS) {
|
|
612
|
+
if (counts[seat] > 0) delete idleSince[seat];
|
|
613
|
+
else if (!idleSince[seat]) idleSince[seat] = now.toISOString();
|
|
614
|
+
}
|
|
615
|
+
const due = now.getTime() - Date.parse(previous.checkedAt) >= IDLE_REQUEST_INTERVAL_MS;
|
|
616
|
+
if (!due && Object.keys(idleSince).length === Object.keys(previous.idleSince).length) return;
|
|
617
|
+
const day = new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit' }).format(now);
|
|
618
|
+
const state: IdleRequestState = { checkedAt: due ? now.toISOString() : previous.checkedAt, idleSince,
|
|
619
|
+
notified: (previous.notified ?? []).filter(key => key.startsWith('orch-idle:') && key.split(':')[2] === day) };
|
|
620
|
+
mkdirSync(join(root, 'orchestrator'), { recursive: true });
|
|
621
|
+
const temporary = `${marker}.${randomUUID()}.tmp`;
|
|
622
|
+
try {
|
|
623
|
+
writeFileSync(temporary, JSON.stringify(state), { flag: 'wx', mode: 0o600 });
|
|
624
|
+
renameSync(temporary, marker);
|
|
625
|
+
} catch (error) {
|
|
626
|
+
try { unlinkSync(temporary); } catch { /* no temporary file */ }
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
if (!due) return;
|
|
630
|
+
const cells = (deps.idleCells ?? ((clock: Date) => idleRequestCells(clock, root)))(now);
|
|
631
|
+
const eligible = (rows: typeof cells.current, seat: QueueSeat) => rows.filter(cell =>
|
|
632
|
+
cell.owner?.split('/')[0] === seat && cell.status === 'yellow' && !hasRunEvidence(cell));
|
|
633
|
+
const candidates: TrafficCell[] = TRAFFIC_SEATS.flatMap(seat => {
|
|
634
|
+
const current = eligible(cells.current, seat);
|
|
635
|
+
return (current.length ? current : eligible(cells.next, seat)).slice(0, 1);
|
|
636
|
+
});
|
|
637
|
+
const measured = trafficTick({ now, processes: TRAFFIC_SEATS.flatMap(seat => Array.from({ length: counts[seat] }, () =>
|
|
638
|
+
({ seat, command: 'bun bin/elanous.mjs harness say observed', elapsedSeconds: 0 }))),
|
|
639
|
+
caps: config?.seatCaps ?? { OP: 4, TC: 8, MK: 6, UX: 6 }, idleSince: Object.fromEntries(TRAFFIC_SEATS.map(seat =>
|
|
640
|
+
[seat, idleSince[seat] ? new Date(idleSince[seat]) : null])), openCells: candidates, idleMinutes: IDLE_REQUEST_MINUTES });
|
|
641
|
+
const log = deps.idleLog ?? ((category: string, event: string, data: Record<string, unknown>) => debug.log(category, event, data));
|
|
642
|
+
for (const row of measured.seats) {
|
|
643
|
+
if (!row.idle || row.running !== 0) continue;
|
|
644
|
+
const cell = row.nextCell ? `${row.nextCell.id} ${row.nextCell.title.replace(/\s+/g, ' ').trim()}` : '칸 없음';
|
|
645
|
+
let outcome: 'shadow' | 'queued' | 'duplicate' | 'no-cell' = mode === 'live' ? 'no-cell' : 'shadow';
|
|
646
|
+
const key = `orch-idle:${row.seat}:${day}:${row.nextCell?.id ?? 'no-cell'}`;
|
|
647
|
+
if (state.notified!.includes(key)) continue;
|
|
648
|
+
if (mode === 'live' && row.nextCell) {
|
|
649
|
+
const path = join(root, 'seat-requests', 'requests.jsonl');
|
|
650
|
+
mkdirSync(join(root, 'seat-requests'), { recursive: true });
|
|
651
|
+
outcome = withFileLockSync(`${path}.lock`, () => {
|
|
652
|
+
if (existsSync(path) && readFileSync(path, 'utf8').split('\n').some(line => {
|
|
653
|
+
if (!line) return false;
|
|
654
|
+
try { return (JSON.parse(line) as { key?: string }).key === key; }
|
|
655
|
+
catch { throw new Error('invalid seat request journal'); }
|
|
656
|
+
})) return 'duplicate';
|
|
657
|
+
appendFileSync(path, JSON.stringify({ key, receiptId: key, seat: row.seat,
|
|
658
|
+
text: `다음 칸: ${cell} — 30분 놀았다`, status: 'queued', queuedAt: now.toISOString(), source: 'orchestrator-idle' }) + '\n');
|
|
659
|
+
return 'queued';
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
if (outcome !== 'duplicate') {
|
|
663
|
+
state.notified!.push(key);
|
|
664
|
+
try { log('loop.orchestrator', 'idle-requested', {
|
|
665
|
+
seat: row.seat, idleMinutes: IDLE_REQUEST_MINUTES, cell, mode, outcome }); }
|
|
666
|
+
catch { /* Observation cannot change the request or queue decision. */ }
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
writeFileSync(temporary, JSON.stringify(state), { flag: 'wx', mode: 0o600 });
|
|
671
|
+
renameSync(temporary, marker);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
try { unlinkSync(temporary); } catch { /* no temporary file */ }
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
424
678
|
export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<QueueTick> {
|
|
425
679
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
426
680
|
return locked(root, async (path) => {
|
|
681
|
+
try {
|
|
682
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
683
|
+
// A test process must not run the real author shadow — it reads operational ledgers and blew the 5s test budget (10-05 P0).
|
|
684
|
+
if (!deps.authorShadow && process.env.ELANOUS_AUTHOR_SHADOW_LIVE !== '1' && (process.env.NODE_ENV === 'test' || process.env.ELANOUS_TEST_HOME)) {
|
|
685
|
+
debug.log('loops.author-depth', 'shadow-skipped-test', {});
|
|
686
|
+
} else {
|
|
687
|
+
const shadow = deps.authorShadow ?? (await import('../loops/orchestrator/author-depth.js')).runAuthorDepthShadow;
|
|
688
|
+
await shadow(root, now);
|
|
689
|
+
}
|
|
690
|
+
} catch (error) {
|
|
691
|
+
try { debug.log('loops.author-depth', 'shadow-failed', { reason: String(error) }); }
|
|
692
|
+
catch { /* Shadow observation cannot affect queue dispatch. */ }
|
|
693
|
+
}
|
|
427
694
|
const items = read(path);
|
|
428
|
-
|
|
429
|
-
|
|
695
|
+
try { await (deps.idleRequest ?? requestIdleSeats)(root, (deps.now ?? (() => new Date()))(), items, deps); }
|
|
696
|
+
catch (error) {
|
|
697
|
+
try { debug.log('loop.orchestrator', 'idle-request-failed', { reason: String(error) }); }
|
|
698
|
+
catch { /* Idle observation cannot affect queue dispatch. */ }
|
|
699
|
+
}
|
|
700
|
+
const heads = new Map<QueueSeat, QueueItem>();
|
|
701
|
+
for (const row of items) if (row.status === 'queued' && !heads.has(row.seat)) heads.set(row.seat, row);
|
|
702
|
+
if (!heads.size) { observe('skipped', { reason: 'empty' }, deps); return { outcome: 'skipped', reason: 'empty' }; }
|
|
703
|
+
const lastSeat = lastLaunchedSeat(path);
|
|
704
|
+
const first = lastSeat ? (queueSeatNames.indexOf(lastSeat) + 1) % queueSeatNames.length
|
|
705
|
+
: queueSeatNames.indexOf(items.find((row) => row.status === 'queued')!.seat);
|
|
706
|
+
const candidates = [...queueSeatNames.slice(first), ...queueSeatNames.slice(0, first)]
|
|
707
|
+
.flatMap((name) => heads.get(name) ? [heads.get(name)!] : []);
|
|
430
708
|
const config = deps.configPath === undefined ? getUserConfig() : getUserConfig(deps.configPath);
|
|
431
|
-
|
|
432
|
-
if (!Number.isSafeInteger(cap) || cap < 1) throw new Error(`harness queue: invalid seat cap for ${item.seat}`);
|
|
709
|
+
warnLegacySeatCap(deps.configPath ?? userConfigPath());
|
|
433
710
|
const caps = config.loops?.orchestrator?.seatCaps;
|
|
434
711
|
const gate = config.loops?.orchestrator?.releaseGate;
|
|
435
712
|
const attributed: Record<QueueSeat, number> = { OP: 0, TC: 0, MK: 0, UX: 0 };
|
|
436
713
|
let unattributed = 0;
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
714
|
+
const blocked = new Map<QueueSeat, { reason: string; active: number }>();
|
|
715
|
+
const waiting = (): QueueTick => {
|
|
716
|
+
const reason = candidates.map((item) => blocked.get(item.seat)!.reason).join('\n');
|
|
717
|
+
const updated = current.map((row) => {
|
|
718
|
+
const block = blocked.get(row.seat);
|
|
719
|
+
return heads.get(row.seat)?.id === row.id && block && row.waitingReason !== block.reason
|
|
720
|
+
? { ...row, waitingReason: block.reason } : row;
|
|
721
|
+
});
|
|
722
|
+
if (updated.some((row, index) => row !== current[index])) save(path, updated);
|
|
723
|
+
for (const item of candidates) {
|
|
724
|
+
const { reason: seatReason, active } = blocked.get(item.seat)!;
|
|
725
|
+
const injectedCap = deps.cap?.(item.seat);
|
|
726
|
+
const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
|
|
727
|
+
observe('waiting', { id: item.id, seat: item.seat, reason: seatReason, active, cap: details.cap,
|
|
728
|
+
seatCaps: details.seatCaps, releaseGate: details.releaseGate,
|
|
729
|
+
...(details.injectedCap === undefined ? {} : { injectedCap: details.injectedCap }), capKeys: details.winners,
|
|
730
|
+
attributed, unattributed }, deps);
|
|
731
|
+
}
|
|
732
|
+
return { outcome: 'waiting', item: { ...candidates[0]!, waitingReason: blocked.get(candidates[0]!.seat)!.reason }, reason };
|
|
440
733
|
};
|
|
734
|
+
for (const item of candidates) {
|
|
735
|
+
const injectedCap = deps.cap?.(item.seat);
|
|
736
|
+
if (injectedCap !== undefined && (!Number.isSafeInteger(injectedCap) || injectedCap < 0)) throw new Error(`harness queue: invalid seat cap for ${item.seat}`);
|
|
737
|
+
}
|
|
738
|
+
let current = items;
|
|
441
739
|
let processes: readonly QueueProcess[];
|
|
442
740
|
try { processes = (deps.processes ?? readHarnessQueueProcesses)(); }
|
|
443
741
|
catch (error) {
|
|
444
|
-
|
|
445
|
-
|
|
742
|
+
for (const item of candidates) {
|
|
743
|
+
const injectedCap = deps.cap?.(item.seat);
|
|
744
|
+
const budget = decideSpawn({ seat: item.seat, running: null, caps, gate, injectedCap });
|
|
745
|
+
blocked.set(item.seat, { reason: `${budget.reason}: harness process inventory unavailable: ${String(error)}`, active: 0 });
|
|
746
|
+
}
|
|
747
|
+
return waiting();
|
|
446
748
|
}
|
|
447
|
-
|
|
448
|
-
|
|
749
|
+
const runs = groupQueueRuns(processes);
|
|
750
|
+
for (const run of runs) {
|
|
751
|
+
if (run.seat) attributed[run.seat]++;
|
|
449
752
|
else unattributed++;
|
|
450
753
|
}
|
|
451
|
-
|
|
754
|
+
current = items.flatMap((row) => {
|
|
452
755
|
if ((row.status !== 'launched' && row.status !== 'launching') || !row.launchId) return [row];
|
|
453
756
|
const state = (deps.receipt ?? receipt)(root, row.launchId);
|
|
454
757
|
if (state !== 'finished' && state !== 'not-started'
|
|
@@ -459,36 +762,63 @@ export async function tickHarnessQueue(deps: HarnessQueueDeps = {}): Promise<Que
|
|
|
459
762
|
return [];
|
|
460
763
|
});
|
|
461
764
|
if (current.length !== items.length || current.some((row, index) => row !== items[index])) save(path, current);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
765
|
+
let pool: QueuePool | undefined;
|
|
766
|
+
let poolReason: string | undefined;
|
|
767
|
+
let finishHeld: string | null | undefined;
|
|
768
|
+
for (const item of candidates) {
|
|
769
|
+
const injectedCap = deps.cap?.(item.seat);
|
|
770
|
+
const details = seatCapDetails({ seat: item.seat, caps, gate, injectedCap });
|
|
771
|
+
const charged = runs.filter((run) => !run.seat || run.seat === item.seat);
|
|
772
|
+
const launchIds = new Set(charged.flatMap((run) => [...run.launchIds]));
|
|
773
|
+
const active = charged.length + current.filter((row) => row.seat === item.seat &&
|
|
774
|
+
(row.status === 'launching' || row.status === 'launched')
|
|
775
|
+
&& (!row.launchId || !launchIds.has(row.launchId))).length;
|
|
776
|
+
if (active >= details.cap) { blocked.set(item.seat, { reason: seatCapReason(item.seat, active, details), active }); continue; }
|
|
777
|
+
if (!pool && !poolReason) {
|
|
778
|
+
try { pool = (deps.pool ?? readHarnessQueuePool)(); }
|
|
779
|
+
catch (error) { poolReason = `pod lease status unavailable: ${String(error)}`; }
|
|
780
|
+
if (pool && (![pool.running, pool.pending, pool.reserved, pool.limit].every((n) => Number.isSafeInteger(n) && n >= 0) || pool.limit < 1)) {
|
|
781
|
+
poolReason = 'pod lease status incomplete';
|
|
782
|
+
}
|
|
783
|
+
if (pool && !poolReason) {
|
|
784
|
+
const queueLaunching = current.filter((row) => row.status === 'launching' || row.status === 'launched').length;
|
|
785
|
+
if (pool.running + pool.pending + pool.reserved + queueLaunching >= pool.limit) {
|
|
786
|
+
poolReason = `pool: ${pool.running}+${pool.pending}+${pool.reserved}+${queueLaunching}/${pool.limit}`;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (poolReason) { blocked.set(item.seat, { reason: poolReason, active }); continue; }
|
|
791
|
+
const budget = decideSpawn({ seat: item.seat, running: active, caps, gate, injectedCap });
|
|
792
|
+
if (!budget.allow) {
|
|
793
|
+
blocked.set(item.seat, { reason: `${budget.reason}: ${seatCapReason(item.seat, active, details)}`, active });
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
if (finishHeld === undefined) {
|
|
797
|
+
finishHeld = await finishGateReason(root, (deps.now ?? (() => new Date()))(), config.loops?.orchestrator?.finishGate ?? 'shadow',
|
|
798
|
+
pool!.running + pool!.pending + pool!.reserved
|
|
799
|
+
+ current.filter((row) => row.status === 'launching' || row.status === 'launched').length, pool!.limit, deps);
|
|
800
|
+
}
|
|
801
|
+
if (finishHeld) { blocked.set(item.seat, { reason: finishHeld, active }); continue; }
|
|
802
|
+
const launching: QueueItem = { ...item, status: 'launching', launchId: `hq-${randomUUID()}` };
|
|
803
|
+
delete launching.waitingReason;
|
|
804
|
+
save(path, current.map((row) => row.id === item.id ? launching : row));
|
|
805
|
+
try {
|
|
806
|
+
const pid = await (deps.launch ?? launch)(launching, queueLaunchArgs(launching), root);
|
|
807
|
+
if (!Number.isSafeInteger(pid) || pid < 1) throw new Error('launcher returned no pid');
|
|
808
|
+
const launched: QueueItem = { ...launching, pid, status: 'launched' };
|
|
809
|
+
save(path, current.map((row) => row.id === item.id ? launched : row));
|
|
810
|
+
try { saveLastLaunchedSeat(path, item.seat); }
|
|
811
|
+
catch (error) {
|
|
812
|
+
try { debug.log('harness.queue', 'round-robin-save-failed', { id: item.id, seat: item.seat, reason: String(error) }, { level: 'warn' }); }
|
|
813
|
+
catch { /* Observation cannot change an already persisted launch. */ }
|
|
814
|
+
}
|
|
815
|
+
observe('launched', { id: item.id, seat: item.seat, pid, kind: item.kind, launchId: launched.launchId }, deps);
|
|
816
|
+
return { outcome: 'launched', item: launched, reason: 'spawned' };
|
|
817
|
+
} catch (error) {
|
|
818
|
+
observe('skipped', { id: item.id, seat: item.seat, reason: `launch uncertain: ${String(error)}`, launchId: launching.launchId }, deps);
|
|
819
|
+
throw error;
|
|
820
|
+
}
|
|
492
821
|
}
|
|
822
|
+
return waiting();
|
|
493
823
|
});
|
|
494
824
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
5
|
+
import type { RunGh, ScannedStoppedPr, StoppedPrCategory } from './helper-scan.js';
|
|
6
|
+
|
|
7
|
+
export interface RepairGoalInput {
|
|
8
|
+
pr: number;
|
|
9
|
+
category: StoppedPrCategory;
|
|
10
|
+
/** The original user's request, not the supervisor's stop report or a paraphrase. */
|
|
11
|
+
originalRequest: string;
|
|
12
|
+
/** Remaining reviewer must-fix items, in their original wording and order. */
|
|
13
|
+
mustFix: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Author a single follow-up goal without changing the original ask or reviewer findings. */
|
|
17
|
+
export function authorRepairGoal({ pr, category, originalRequest, mustFix }: RepairGoalInput): string | null {
|
|
18
|
+
if ((category !== 'review-budget' && category !== 'review-oscillation') || !originalRequest.trim() || !mustFix.length || mustFix.some((item) => !item.trim())) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const guidance = category === 'review-oscillation'
|
|
23
|
+
? '기존 시험 설계를 보존하고 리뷰 진동·설계 역행을 확인한 뒤 남은 must-fix를 수리하라.'
|
|
24
|
+
: '남은 must-fix를 수리하라. 원래 요청의 범위와 기존 시험 설계를 보존하라.';
|
|
25
|
+
return `${originalRequest}${originalRequest.endsWith('\n') ? '' : '\n'}\n## PR #${pr} 수리\n${guidance}\n\n## 남은 must-fix (원문)\n${mustFix.join('\n')}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** PR 본문에서 원 요청(## 요청 절)과 남은 must-fix(## 마지막 리뷰 must-fix · ## Follow-up must-fix 의 «- » 항목)를 원문 그대로 뽑는다. */
|
|
29
|
+
export function repairInputsFromBody(body: string): { originalRequest: string; mustFix: string[] } {
|
|
30
|
+
const section = (title: RegExp) => {
|
|
31
|
+
const lines = body.split('\n');
|
|
32
|
+
const start = lines.findIndex((line) => title.test(line));
|
|
33
|
+
if (start < 0) return [] as string[];
|
|
34
|
+
const end = lines.findIndex((line, index) => index > start && /^## /.test(line));
|
|
35
|
+
return lines.slice(start + 1, end < 0 ? undefined : end);
|
|
36
|
+
};
|
|
37
|
+
const requestLines = section(/^## 요청\s*$/);
|
|
38
|
+
while (requestLines[0] === '') requestLines.shift();
|
|
39
|
+
while (requestLines.length > 1 && requestLines.at(-1) === '' && requestLines.at(-2) === '') requestLines.pop();
|
|
40
|
+
if (requestLines.length === 2 && requestLines[1] === '') requestLines.pop();
|
|
41
|
+
const originalRequest = requestLines.join('\n');
|
|
42
|
+
const mustFix: string[] = [];
|
|
43
|
+
for (const lines of [section(/^## 마지막 리뷰 must-fix/), section(/^## Follow-up must-fix/)]) {
|
|
44
|
+
let item: string[] = [];
|
|
45
|
+
const flush = () => {
|
|
46
|
+
while (item.at(-1) === '') item.pop();
|
|
47
|
+
if (item.length) mustFix.push(item.join('\n'));
|
|
48
|
+
item = [];
|
|
49
|
+
};
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
if (/^- /.test(line)) {
|
|
52
|
+
flush();
|
|
53
|
+
item.push(line);
|
|
54
|
+
} else if (item.length) {
|
|
55
|
+
item.push(line);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
flush();
|
|
59
|
+
}
|
|
60
|
+
return { originalRequest, mustFix };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type RepairShadowRow = { pr: number; category: StoppedPrCategory; goalHash: string; at: string };
|
|
64
|
+
|
|
65
|
+
/** 그림자: 수리 골을 저작해 헬퍼 원장에만 남긴다(발사·라벨 0) · 같은 PR 은 한 번만. */
|
|
66
|
+
export async function recordRepairShadows(rows: readonly ScannedStoppedPr[], deps: { runGh: RunGh; root: string; now?: Date }): Promise<RepairShadowRow[]> {
|
|
67
|
+
const ledger = join(deps.root, 'helper', 'repairs.jsonl');
|
|
68
|
+
const seen = new Set<number>();
|
|
69
|
+
if (existsSync(ledger)) for (const line of readFileSync(ledger, 'utf8').split('\n')) {
|
|
70
|
+
if (!line.trim()) continue;
|
|
71
|
+
try { seen.add((JSON.parse(line) as { pr: number }).pr); } catch { /* malformed line is skipped */ }
|
|
72
|
+
}
|
|
73
|
+
const written: RepairShadowRow[] = [];
|
|
74
|
+
for (const row of rows) {
|
|
75
|
+
if ((row.category !== 'review-budget' && row.category !== 'review-oscillation') || seen.has(row.pr)) continue;
|
|
76
|
+
const view = JSON.parse(await deps.runGh(['pr', 'view', String(row.pr), '--json', 'body'])) as { body: string };
|
|
77
|
+
const goal = authorRepairGoal({ pr: row.pr, category: row.category, ...repairInputsFromBody(view.body) });
|
|
78
|
+
if (!goal) continue;
|
|
79
|
+
const entry: RepairShadowRow = { pr: row.pr, category: row.category, goalHash: createHash('sha256').update(goal).digest('hex').slice(0, 16), at: (deps.now ?? new Date()).toISOString() };
|
|
80
|
+
mkdirSync(dirname(ledger), { recursive: true });
|
|
81
|
+
appendFileSync(ledger, JSON.stringify({ ...entry, mode: 'shadow', goal }) + '\n');
|
|
82
|
+
debug.log('harness.helper', 'repair-authored', { pr: row.pr, category: row.category, mode: 'shadow' });
|
|
83
|
+
seen.add(row.pr);
|
|
84
|
+
written.push(entry);
|
|
85
|
+
}
|
|
86
|
+
return written;
|
|
87
|
+
}
|