elanous 0.2.15 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1307-33ffcc49ab45123b.js → 1307-b46d45b705e8c9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-3cb52e5ea8fc2fa5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-5491c115b1f9a923.js → page-c1e20d7d338713f1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-482c5dbde699947c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-8c2d9526164f5d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-c53598cfce6156f2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-13a63953f53c6a23.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-5d3c8429d04f3831.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-3193bdc0bbab047a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-6107ca8ddd5e3bdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-dce686e582c23937.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-955ec2da3be4c560.js → page-51f811fa119308b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-9f88fbc15d9058d1.js → page-dacff46049e1557e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-4167348049166852.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-de52e8fdf9095f7a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-c9fc5706b8abaa11.js → page-36a7d994f4ca0299.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-ca3952cd78a83ca2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-acb749f1e432bcf3.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-1ebfe049c640e171.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-85f1a1fc0a82738c.js → page-57924393e3e4e539.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-31fa1ce4c4aff9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-0f5cd57ca928f42b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-d28573d6792b4275.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d751de5f8062ff5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-f5597e894c02cd33.js → page-fe1759f94003b32b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-0337fdf7fb5e37b6.js → page-d64c984e855c8c68.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-8fcec4c9ea2e7f70.js → page-9afdee66b820c76e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-d4aef67abf264874.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-3544f154c87b43ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +15 -15
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +34 -15
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +131 -9
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/gate-node.ts +3 -1
- package/scripts/release-loop/vault-note-node.ts +19 -2
- package/scripts/rhythm/monthly.ts +97 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-mission/driver.ts +51 -1
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/briefing/brief-items.ts +191 -36
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pr-cli.ts +80 -12
- package/src/cli/release-cli.ts +11 -8
- package/src/cli/self-update.ts +200 -1
- package/src/cli/usage-cli.ts +26 -0
- package/src/discord-self-message.ts +9 -0
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +36 -2
- package/src/harness/harness-pod-dispatch.ts +25 -55
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +139 -8
- package/src/harness/helper-scan.ts +13 -2
- package/src/harness/nested-elanous-policy.ts +114 -0
- package/src/hq/hq.ts +8 -2
- package/src/hq/lease.ts +5 -3
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +112 -6
- package/src/intake-plane/digest.ts +48 -26
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/tick.ts +96 -25
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/maturity/tui-slash-availability.ts +9 -0
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/release-loop/checklist.ts +10 -5
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/placement.ts +17 -8
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-loop/seat-loop.ts +90 -3
- package/src/self-dev/ask-launch-flow.ts +22 -1
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +6 -5
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/goal-author.ts +19 -13
- package/src/self-implement/heal-triage.ts +44 -0
- package/src/self-implement/orchestrator.ts +138 -7
- package/src/self-implement/run-ledger.ts +4 -0
- package/src/self-implement/seams.ts +44 -15
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/working-backwards.ts +16 -1
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +5 -3
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +14 -5
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +19 -10
- package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
- package/src/telegram-commands.ts +5 -0
- package/src/telegram.ts +47 -0
- package/src/user-config.ts +36 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-73735b64bfe1ae67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
- package/src/launch-head/seat-goal-queue.ts +0 -198
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
|
@@ -8,9 +8,11 @@ import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
|
8
8
|
import { getUserConfig } from '../../user-config.js';
|
|
9
9
|
import { debug } from '../../debug/log.js';
|
|
10
10
|
import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../self-dev/run-store.js';
|
|
11
|
+
import { planAction, runSeatLoopOnce, type SeatItem } from '../../seat-loop/seat-loop.js';
|
|
12
|
+
import { harnessQueueOutcome } from '../../harness/harness-queue.js';
|
|
11
13
|
|
|
12
14
|
export type Window = '08' | '12' | '18';
|
|
13
|
-
export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'reconcile' | 'report';
|
|
15
|
+
export type Node = 'intake' | 'split' | 'place' | 'delegate' | 'launch' | 'reconcile' | 'report';
|
|
14
16
|
export type Mode = 'off' | 'shadow' | 'live';
|
|
15
17
|
const SEAT_LOOP_IDS: Readonly<Record<string, string>> = { OP: 'op-seat', MK: 'cmo-seat', TC: 'tc-seat', UX: 'ux-seat' };
|
|
16
18
|
export interface Cell { id: string; title: string; seat?: string; host?: string; version?: string; cardId: string; origin: 'candidate' | 'flow1a'; priority?: 'P0' | 'P1' | 'P2'; predecessors?: string[] }
|
|
@@ -19,6 +21,9 @@ export interface TickState {
|
|
|
19
21
|
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
22
|
reconciled: { reached: number; progressing: number; blocked: number; unknown: number };
|
|
21
23
|
runSummaries?: string[];
|
|
24
|
+
launched?: Array<{ seat: string; status: string; key?: string; queueId?: string; runId?: string }>;
|
|
25
|
+
queueOutcomes?: Array<{ queueId: string; key?: string; outcome: string }>;
|
|
26
|
+
steps: Array<{ node: Node; reason: string; count: number; targetLoopId?: string }>;
|
|
22
27
|
nodes: Partial<Record<Node, 'ok' | 'skipped'>>;
|
|
23
28
|
missing: string[];
|
|
24
29
|
}
|
|
@@ -26,11 +31,13 @@ export interface TickState {
|
|
|
26
31
|
export type SplitAdapter = (card: TaskCard, opts?: { shadow?: boolean }) => Promise<Array<{ id: string; title: string; seat?: string; host?: string }>> | Array<{ id: string; title: string; seat?: string; host?: string }>;
|
|
27
32
|
/** RELPLAN1 `placeCell(input: PlacementCell)` — owner·priority·predecessors are required; the result carries no seat. */
|
|
28
33
|
export type PlacementInput = { id: string; title: string; owner: string; priority: 'P0' | 'P1' | 'P2'; predecessors: string[] };
|
|
29
|
-
export type PlaceAdapter = (cell: PlacementInput) => Promise<{ version?: string } | null> | { version?: string } | null;
|
|
34
|
+
export type PlaceAdapter = (cell: PlacementInput, deps?: { dryRun?: boolean; now?: Date }) => Promise<{ version?: string } | null> | { version?: string } | null;
|
|
30
35
|
export type RebalanceAdapter = (version: string) => Promise<{ decisions: unknown[]; blocked: Array<{ id: string; reason: string }> }> | { decisions: unknown[]; blocked: Array<{ id: string; reason: string }> };
|
|
31
36
|
export interface TickDeps {
|
|
32
37
|
root?: string; now?: Date; mode?: Mode; runId?: string; window?: Window;
|
|
33
38
|
cards?: () => TaskCard[]; split?: SplitAdapter; placeCell?: PlaceAdapter;
|
|
39
|
+
seatTurn?: (seat: string, root: string) => Promise<{ status: string; queueId?: string; runId?: string; item?: { source: string; id: string } }>;
|
|
40
|
+
queueOutcome?: (queueId: string, root: string) => 'pending' | 'unknown' | 'retryable' | 'succeeded';
|
|
34
41
|
loadAdapter?: (path: string, name: string) => Promise<unknown>;
|
|
35
42
|
checklist?: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[];
|
|
36
43
|
observe?: (event: string, data: { loopId: string; runId: string; window: Window; node: Node; reason: string; count: number; targetLoopId?: string }) => void;
|
|
@@ -54,7 +61,10 @@ export function wishCards(cards: readonly TaskCard[]): TaskCard[] {
|
|
|
54
61
|
return cards.filter(card => card.status === 'open' && card.goalId.startsWith('wish:') &&
|
|
55
62
|
!card.sections.some(section => section.key === 'orch:split' || section.key.startsWith('orch:split:')));
|
|
56
63
|
}
|
|
57
|
-
export function reconciliation(cells: readonly Cell[], requests: readonly { key: string; status: string }[], checklist: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[]
|
|
64
|
+
export function reconciliation(cells: readonly Cell[], requests: readonly { key: string; status: string }[], checklist: (version: string) => Pick<ChecklistItem, 'id' | 'status'>[],
|
|
65
|
+
queue: readonly { key: string; outcome: string }[] = []): TickState['reconciled'] {
|
|
66
|
+
// 체크리스트가 정본이고, 칸이 아직 체크리스트에 없을 때만 «그 칸의 의뢰 키»로 발사된 대기열 결과를 본다.
|
|
67
|
+
const queueByKey = new Map(queue.map(row => [row.key, row.outcome]));
|
|
58
68
|
const result = { reached: 0, progressing: 0, blocked: 0, unknown: 0 };
|
|
59
69
|
const byVersion = new Map<string, Pick<ChecklistItem, 'id' | 'status'>[]>();
|
|
60
70
|
const keys = new Set(requests.filter(row => row.status === 'queued').map(row => row.key));
|
|
@@ -65,7 +75,12 @@ export function reconciliation(cells: readonly Cell[], requests: readonly { key:
|
|
|
65
75
|
if (status === 'green' || status === 'done') result.reached++;
|
|
66
76
|
else if (status === 'yellow') result.progressing++;
|
|
67
77
|
else if (status === 'red') result.blocked++;
|
|
68
|
-
else
|
|
78
|
+
else {
|
|
79
|
+
const outcome = queueByKey.get(`orch:${cell.cardId}:${cell.id}`);
|
|
80
|
+
if (outcome === 'pending' || outcome === 'succeeded') result.progressing++;
|
|
81
|
+
else if (outcome === 'retryable') result.blocked++;
|
|
82
|
+
else result.unknown++;
|
|
83
|
+
}
|
|
69
84
|
}
|
|
70
85
|
return result;
|
|
71
86
|
}
|
|
@@ -116,7 +131,7 @@ function stateFile(root: string, runId: string): string {
|
|
|
116
131
|
if (!/^[a-zA-Z0-9._-]+$/.test(runId) || runId === '.' || runId === '..') throw new Error('Invalid orchestrator runId');
|
|
117
132
|
return join(root, 'loop', 'orchestrator', `${runId}.json`);
|
|
118
133
|
}
|
|
119
|
-
function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState, 'cards' | 'cells'> | null {
|
|
134
|
+
function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState, 'cards' | 'cells' | 'launched'> | null {
|
|
120
135
|
const dir = join(root, 'loop', 'orchestrator');
|
|
121
136
|
if (!existsSync(dir)) return null;
|
|
122
137
|
const states = readdirSync(dir).filter(name => /^[a-zA-Z0-9._-]+\.json$/.test(name))
|
|
@@ -126,15 +141,17 @@ function restorePrevious(root: string, day: string, mode: Mode): Pick<TickState,
|
|
|
126
141
|
states.sort((a, b) => a.name.localeCompare(b.name));
|
|
127
142
|
const cards = new Map<string, TaskCard>();
|
|
128
143
|
const cells = new Map<string, Cell>();
|
|
144
|
+
const launched = new Map<string, NonNullable<TickState['launched']>[number]>();
|
|
129
145
|
for (const { state } of states) {
|
|
130
146
|
for (const card of state.cards) cards.set(card.id, card);
|
|
131
147
|
for (const cell of state.cells) cells.set(`${cell.cardId}:${cell.id}`, cell);
|
|
148
|
+
for (const turn of state.launched ?? []) if (turn.queueId) launched.set(turn.queueId, turn);
|
|
132
149
|
}
|
|
133
|
-
return { cards: [...cards.values()], cells: [...cells.values()] };
|
|
150
|
+
return { cards: [...cards.values()], cells: [...cells.values()], launched: [...launched.values()] };
|
|
134
151
|
}
|
|
135
152
|
function initial(runId: string, window: Window, mode: Mode, day: string): TickState {
|
|
136
153
|
return { runId, window, mode, day, cards: [], skippedCards: [], cells: [], placed: 0, delegated: 0, wouldDelegate: 0,
|
|
137
|
-
reconciled: { reached: 0, progressing: 0, blocked: 0, unknown: 0 }, nodes: {}, missing: [] };
|
|
154
|
+
reconciled: { reached: 0, progressing: 0, blocked: 0, unknown: 0 }, steps: [], nodes: {}, missing: [] };
|
|
138
155
|
}
|
|
139
156
|
function graphRunId(): string | undefined {
|
|
140
157
|
const context = process.env.ELANOUS_GRAPH_CONTEXT;
|
|
@@ -163,8 +180,9 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
163
180
|
const configured = deps.mode ?? getUserConfig().loops?.orchestrator?.mode;
|
|
164
181
|
const mode: Mode = configured === 'off' || configured === 'live' ? configured : 'shadow';
|
|
165
182
|
const state: TickState = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) as TickState : initial(runId, window, mode, kstDay(now));
|
|
183
|
+
state.steps ??= [];
|
|
166
184
|
if (state.window !== window || state.mode !== mode) throw new Error('orchestrator run window/mode changed');
|
|
167
|
-
const predecessor: Partial<Record<Node, Node>> = { split: 'intake', place: 'split', delegate: 'place',
|
|
185
|
+
const predecessor: Partial<Record<Node, Node>> = { split: 'intake', place: 'split', delegate: 'place', launch: 'delegate', reconcile: 'launch', report: 'reconcile' };
|
|
168
186
|
const preceding = predecessor[node];
|
|
169
187
|
if (preceding && !state.nodes[preceding]) throw new Error(`orchestrator ${node}: missing ${preceding} result`);
|
|
170
188
|
const loadAdapter = deps.loadAdapter ?? optionalAdapter;
|
|
@@ -174,6 +192,7 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
174
192
|
};
|
|
175
193
|
const observe = (event: string, reason = '', count = 0, targetLoopId?: string): void => {
|
|
176
194
|
const data = { loopId: 'orchestrator', runId, window, node, reason, count, ...(targetLoopId ? { targetLoopId } : {}) };
|
|
195
|
+
if (event !== 'tick') state.steps.push({ node, reason, count, ...(targetLoopId ? { targetLoopId } : {}) });
|
|
177
196
|
try { (deps.observe ?? ((name, payload) => debug.log('loop.orchestrator', name, payload)))(event, data); }
|
|
178
197
|
catch { /* observation must not change the run */ }
|
|
179
198
|
};
|
|
@@ -198,6 +217,7 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
198
217
|
if (!cell.id?.trim() || !cell.title?.trim()) throw new Error('invalid split cell');
|
|
199
218
|
state.cells.push({ cardId: card.id, id: cell.id, title: cell.title, origin: split ? 'flow1a' : 'candidate', ...(cell.seat ? { seat: cell.seat } : {}), ...(cell.host !== undefined ? { host: cell.host } : {}) });
|
|
200
219
|
}
|
|
220
|
+
observe('exchange', `split:${card.id}`, cells.length);
|
|
201
221
|
if (split && cells.length && mode === 'live') {
|
|
202
222
|
const store = new CardStore(root);
|
|
203
223
|
try {
|
|
@@ -209,15 +229,24 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
209
229
|
const place = deps.placeCell ?? await adapter<PlaceAdapter>('../../release-loop/placement.js', 'placeCell');
|
|
210
230
|
const available = typeof place === 'function';
|
|
211
231
|
if (!available) { state.missing.push('relplan1-absent'); observe('node-missing', 'relplan1-absent', state.cells.length); }
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
232
|
+
if (mode === 'shadow' && deps.placeCell) observe('exchange', 'relplan1-shadow-not-invoked', state.cells.length);
|
|
233
|
+
// 담당 없는 칸은 배치를 부르든 안 부르든 센다 — 배치 호출 여부와 집계를 묶지 않는다.
|
|
234
|
+
for (const cell of state.cells) {
|
|
235
|
+
if (cell.origin === 'flow1a' && (!cell.seat || !SEAT_LOOP_IDS[cell.seat])) { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-owner', 1); }
|
|
236
|
+
}
|
|
237
|
+
if (available && (mode === 'live' || !deps.placeCell)) for (const cell of state.cells) {
|
|
238
|
+
if (cell.origin !== 'flow1a' || !cell.seat || !SEAT_LOOP_IDS[cell.seat]) continue;
|
|
239
|
+
// RELPLAN's dryRun calculates the same release without writing a checklist in shadow.
|
|
240
|
+
try {
|
|
241
|
+
const placed = await place({ id: cell.id, title: cell.title, owner: cell.seat, priority: cell.priority ?? 'P2', predecessors: cell.predecessors ?? [] }, { dryRun: mode === 'shadow', now });
|
|
242
|
+
if (validVersion(placed?.version)) {
|
|
243
|
+
cell.version = placed.version;
|
|
244
|
+
state.placed++;
|
|
245
|
+
observe('exchange', mode === 'shadow' ? `would-place:${cell.id}:${cell.version}` : `placed:${cell.id}:${cell.version}`, 1);
|
|
246
|
+
} else { state.unplaced = (state.unplaced ?? 0) + 1; observe('exchange', 'unplaced-no-release', 1); }
|
|
247
|
+
} catch (error) {
|
|
248
|
+
state.unplaced = (state.unplaced ?? 0) + 1;
|
|
249
|
+
observe('exchange', `unplaced:${cell.id}:${String(error).slice(0, 160)}`, 1);
|
|
221
250
|
}
|
|
222
251
|
}
|
|
223
252
|
} else if (node === 'delegate') {
|
|
@@ -229,9 +258,11 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
229
258
|
if (!targetLoopId) continue;
|
|
230
259
|
const key = `orch:${cell.cardId}:${cell.id}`;
|
|
231
260
|
if (keys.has(key)) continue;
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
261
|
+
if (!validVersion(cell.version)) {
|
|
262
|
+
if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', `${key} (unplaced)`, 1, targetLoopId); }
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (mode === 'shadow') { keys.add(key); state.wouldDelegate++; observe('would-delegate', key, 1, targetLoopId); continue; }
|
|
235
266
|
keys.add(key);
|
|
236
267
|
mkdirSync(dirname(path), { recursive: true });
|
|
237
268
|
// TC review must-fix ①: the seat-requests journal contract needs receiptId, and appends are serialized under the
|
|
@@ -249,13 +280,53 @@ export async function runOrchestratorNode(node: Node, deps: TickDeps = {}): Prom
|
|
|
249
280
|
state.delegated++;
|
|
250
281
|
observe('exchange', 'queued', 1, targetLoopId);
|
|
251
282
|
}
|
|
283
|
+
} else if (node === 'launch') {
|
|
284
|
+
const eligible = state.cells.filter(cell => cell.origin === 'flow1a' && cell.seat && SEAT_LOOP_IDS[cell.seat]
|
|
285
|
+
&& validVersion(cell.version) && (mode === 'shadow' || readRequests(root).some(row => row.key === `orch:${cell.cardId}:${cell.id}` && row.status === 'queued')));
|
|
286
|
+
const seats = [...new Set(eligible.map(cell => cell.seat!))];
|
|
287
|
+
state.launched = [];
|
|
288
|
+
for (const seat of seats) {
|
|
289
|
+
if (mode === 'shadow') {
|
|
290
|
+
const cell = eligible.find(row => row.seat === seat)!;
|
|
291
|
+
const item: SeatItem = { source: 'request', id: `orch:${cell.cardId}:${cell.id}`, title: cell.title, text: cell.title };
|
|
292
|
+
const action = planAction(item, seat);
|
|
293
|
+
state.launched.push({ seat, key: item.id, status: action.kind === 'harness' ? 'would-launch' : `would-${action.kind}` });
|
|
294
|
+
observe('exchange', `would-${action.kind}:${seat}`, 1, SEAT_LOOP_IDS[seat]);
|
|
295
|
+
} else {
|
|
296
|
+
const turn = await (deps.seatTurn ?? ((assigned: string, stateRoot: string) => runSeatLoopOnce(assigned, {
|
|
297
|
+
root: stateRoot, config: { ...getUserConfig().loops?.seat, mode: 'live-safe', seats: [assigned] },
|
|
298
|
+
})))(seat, root);
|
|
299
|
+
// 자리 턴이 «이 틱이 넣은 의뢰»를 처리했을 때만 칸에 잇는다 — 대기열의 다른 의뢰는 이 카드의 발사가 아니다.
|
|
300
|
+
const targets = new Set(eligible.filter(row => row.seat === seat).map(row => `orch:${row.cardId}:${row.id}`));
|
|
301
|
+
const handled = 'item' in turn && turn.item?.source === 'request' ? turn.item.id : undefined;
|
|
302
|
+
const key = handled && targets.has(handled) ? handled : undefined;
|
|
303
|
+
if (handled && !key) observe('exchange', `seat-turn-other-request:${seat}`, 1, SEAT_LOOP_IDS[seat]);
|
|
304
|
+
state.launched.push({ seat, status: turn.status, ...(key ? { key } : {}),
|
|
305
|
+
...('queueId' in turn && turn.queueId ? { queueId: turn.queueId } : {}),
|
|
306
|
+
...('runId' in turn && turn.runId ? { runId: turn.runId } : {}) });
|
|
307
|
+
observe('exchange', `seat-turn:${seat}:${turn.status}`, 1, SEAT_LOOP_IDS[seat]);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
252
310
|
} else if (node === 'reconcile') {
|
|
253
|
-
if (window !== '08'
|
|
311
|
+
if (window !== '08') {
|
|
312
|
+
// 새 카드가 있어도 앞 틱의 의뢰·발사를 같이 대조한다 — 지금 틱 것이 같은 키를 이긴다.
|
|
254
313
|
const previous = restorePrevious(root, state.day, mode);
|
|
255
|
-
if (previous) {
|
|
314
|
+
if (previous) {
|
|
315
|
+
const merge = <T>(older: readonly T[], newer: readonly T[], key: (row: T) => string) =>
|
|
316
|
+
[...new Map([...older, ...newer].map(row => [key(row), row])).values()];
|
|
317
|
+
state.cards = merge(previous.cards, state.cards, card => card.id);
|
|
318
|
+
state.cells = merge(previous.cells, state.cells, cell => `${cell.cardId}:${cell.id}`);
|
|
319
|
+
state.launched = merge(previous.launched ?? [], state.launched ?? [], turn => turn.queueId ?? `${turn.seat}:${turn.key ?? ''}`);
|
|
320
|
+
}
|
|
256
321
|
}
|
|
322
|
+
state.queueOutcomes = (state.launched ?? []).filter(turn => turn.queueId).map(turn => {
|
|
323
|
+
const outcome = (deps.queueOutcome ?? ((id: string, stateRoot: string) => harnessQueueOutcome(id, { root: stateRoot })))(turn.queueId!, root);
|
|
324
|
+
observe('exchange', `queue-outcome:${turn.queueId}:${outcome}`, 1, SEAT_LOOP_IDS[turn.seat]);
|
|
325
|
+
return { queueId: turn.queueId!, ...(turn.key ? { key: turn.key } : {}), outcome };
|
|
326
|
+
});
|
|
257
327
|
const requests = readRequests(root);
|
|
258
|
-
state.reconciled = reconciliation(state.cells, requests, mode === 'shadow' ? version => readChecklist(root, version) : (deps.checklist ?? (version => readChecklist(root, version)))
|
|
328
|
+
state.reconciled = reconciliation(state.cells, requests, mode === 'shadow' ? version => readChecklist(root, version) : (deps.checklist ?? (version => readChecklist(root, version))),
|
|
329
|
+
state.queueOutcomes.flatMap(row => row.key ? [{ key: row.key, outcome: row.outcome }] : []));
|
|
259
330
|
const from = new Date(`${state.day}T00:00:00+09:00`).getTime();
|
|
260
331
|
state.runSummaries = listSelfDevRuns(selfDevRunsDir(root))
|
|
261
332
|
.filter(run => run.updatedAt >= from && run.updatedAt < from + 86_400_000)
|
|
@@ -295,9 +366,9 @@ if (import.meta.main) {
|
|
|
295
366
|
const node = process.argv[2];
|
|
296
367
|
const index = process.argv.indexOf('--window');
|
|
297
368
|
const window = index < 0 ? undefined : process.argv[index + 1];
|
|
298
|
-
if (!['intake', 'split', 'place', 'delegate', 'reconcile', 'report'].includes(node ?? '') ||
|
|
369
|
+
if (!['intake', 'split', 'place', 'delegate', 'launch', 'reconcile', 'report'].includes(node ?? '') ||
|
|
299
370
|
(window !== undefined && !['08', '12', '18'].includes(window))) {
|
|
300
|
-
console.error('orchestrator: expected <intake|split|place|delegate|reconcile|report> [--window 08|12|18]');
|
|
371
|
+
console.error('orchestrator: expected <intake|split|place|delegate|launch|reconcile|report> [--window 08|12|18]');
|
|
301
372
|
process.exitCode = 2;
|
|
302
373
|
} else {
|
|
303
374
|
void (async () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SlashCommand } from '../chat/index.js';
|
|
2
2
|
import type { Maturity } from './feature-maturity.js';
|
|
3
3
|
import type { BotSurface } from './bot-command-maturity.js';
|
|
4
|
+
import { botUnavailableSlashReply, deriveTuiSlashAvailability, formatBotUnavailableSlashReply } from './tui-slash-availability.js';
|
|
4
5
|
|
|
5
6
|
export interface BotSlashCatalogEntry {
|
|
6
7
|
name: string;
|
|
@@ -56,11 +57,19 @@ export function buildBotSlashCatalog(options: {
|
|
|
56
57
|
commands.push({ name, description: `${description.slice(0, 100 - suffix.length)}${suffix}`, supported: false });
|
|
57
58
|
seen.add(name);
|
|
58
59
|
}
|
|
59
|
-
const
|
|
60
|
+
const handled = new Set(handledCommands.map(({ name }) => name));
|
|
61
|
+
const unsupported = new Set(commands.filter(({ supported }) => !supported).map(({ name }) => name));
|
|
62
|
+
const availability = new Map(deriveTuiSlashAvailability(
|
|
63
|
+
coreCommands.filter(({ name }) => Object.hasOwn(maturity.tuiSlash, name)), maturity,
|
|
64
|
+
).map((entry) => [entry.name, entry]));
|
|
60
65
|
return {
|
|
61
66
|
commands,
|
|
62
67
|
unsupportedReply(name) {
|
|
63
|
-
|
|
68
|
+
if (handled.has(name)) return null;
|
|
69
|
+
const entry = availability.get(name);
|
|
70
|
+
if (entry?.maturity === 'system') return null;
|
|
71
|
+
if (unsupported.has(name)) return formatBotUnavailableSlashReply(name, surface);
|
|
72
|
+
return botUnavailableSlashReply(entry, surface);
|
|
64
73
|
},
|
|
65
74
|
};
|
|
66
75
|
}
|
|
@@ -59,7 +59,7 @@ export const FEATURE_MATURITY = {
|
|
|
59
59
|
pty: 'beta', pr: 'beta', repo: 'beta', role: 'beta', machine: 'beta', 'agent-env': 'beta',
|
|
60
60
|
where: 'stable', shadow: 'beta', llm: 'beta', storage: 'beta', leader: 'beta',
|
|
61
61
|
pod: 'beta', browser: 'beta', questions: 'beta', usage: 'stable', live: 'beta',
|
|
62
|
-
research: 'beta', release: 'beta', freeze: 'beta', flow: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
|
|
62
|
+
research: 'beta', release: 'beta', know: 'beta', freeze: 'beta', flow: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
|
|
63
63
|
control: 'beta', resources: 'beta', hooks: 'beta', setup: 'stable', start: 'stable',
|
|
64
64
|
grounding: 'beta', graph: 'beta', loop: 'stable', card: 'stable', 'launch-head': 'beta',
|
|
65
65
|
a2a: 'beta', plugin: 'stable', make: 'beta', skills: 'beta', connect: 'beta', import: 'beta',
|
|
@@ -75,7 +75,7 @@ export const FEATURE_MATURITY = {
|
|
|
75
75
|
schedule: 'beta', intake: 'beta', logs: 'stable', ad: 'beta', docs: 'beta',
|
|
76
76
|
decisions: 'beta', directives: 'beta', claims: 'beta', lesson: 'beta', fleet: 'beta', ops: 'stable', outputs: 'beta', publish: 'beta',
|
|
77
77
|
env: 'beta', 'agent-mission': 'beta', dev: 'beta', keys: 'beta', finance: 'beta',
|
|
78
|
-
config: 'stable', tasks: 'stable', labels: 'beta', msg: 'beta', ask: 'stable',
|
|
78
|
+
config: 'stable', tasks: 'stable', brief: 'beta', labels: 'beta', msg: 'beta', ask: 'stable',
|
|
79
79
|
onboarding: 'beta', login: 'stable', session: 'beta', chat: 'stable', repl: 'stable',
|
|
80
80
|
agent: 'stable', registry: 'beta', tier: 'stable', local: 'beta', scheduler: 'beta',
|
|
81
81
|
attach: 'beta', wf: 'beta', nexus: 'stable', phone: 'beta', token: 'beta',
|
|
@@ -8,6 +8,15 @@ export interface TuiSlashAvailability {
|
|
|
8
8
|
discord: boolean;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function formatBotUnavailableSlashReply(name: string, surface: 'telegram' | 'discord'): string {
|
|
12
|
+
return `/${name}은(는) ${surface === 'telegram' ? '텔레그램' : '디스코드'}에서 아직 지원되지 않습니다. TUI에서 /${name}을(를) 사용하세요.`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function botUnavailableSlashReply(entry: TuiSlashAvailability | undefined, surface: 'telegram' | 'discord'): string | null {
|
|
16
|
+
if (!entry || entry[surface] || entry.maturity === 'system') return null;
|
|
17
|
+
return formatBotUnavailableSlashReply(entry.name, surface);
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
/** Project canonical TUI commands onto the maturity map and bot-surface grade maps.
|
|
12
21
|
* An absent bot grade means unavailable; a missing TUI grade is a registry error.
|
|
13
22
|
*/
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Launch-time application of the overload gate. Kept out of dev-cli.ts so a
|
|
2
|
+
// unit test can import it without the dev-cli module graph (yaml, typescript).
|
|
3
|
+
|
|
4
|
+
import type { ChildLlmSelection } from '../agent/run-context.js';
|
|
5
|
+
import { debug } from '../debug/log.js';
|
|
6
|
+
import { resolveGrokCredential } from '../grok/credential.js';
|
|
7
|
+
import {
|
|
8
|
+
applyOverloadFailoverToChild,
|
|
9
|
+
readLlmCallOutcomes,
|
|
10
|
+
type OverloadFailoverLaunchDecision,
|
|
11
|
+
} from './overload-failover.js';
|
|
12
|
+
|
|
13
|
+
const CODEX_NAMES = new Set(['codex', 'openai-codex']);
|
|
14
|
+
|
|
15
|
+
export function codexProviderNamed(provider: string | undefined): boolean {
|
|
16
|
+
if (!provider?.trim()) return false;
|
|
17
|
+
return CODEX_NAMES.has(provider.trim().toLowerCase());
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let launchOverloadOutcomesForTesting: (() => ReturnType<typeof readLlmCallOutcomes>) | undefined;
|
|
21
|
+
|
|
22
|
+
/** Test seam. `undefined` restores the real log store. */
|
|
23
|
+
export function setLaunchOverloadOutcomesForTesting(
|
|
24
|
+
reader: (() => ReturnType<typeof readLlmCallOutcomes>) | undefined,
|
|
25
|
+
): void {
|
|
26
|
+
launchOverloadOutcomesForTesting = reader;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface LaunchOverloadFlags {
|
|
30
|
+
provider?: string;
|
|
31
|
+
model?: string;
|
|
32
|
+
/** Pinned childLlm config naming codex. A flag is handled via `provider`. */
|
|
33
|
+
codexPinned?: boolean;
|
|
34
|
+
/** Test seam. Default: a grok credential resolves on this host. */
|
|
35
|
+
grokAvailable?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Explicit codex stays. Otherwise k `llm.call`/`outcome` overload rows move
|
|
39
|
+
* the unpinned child to grok before spawn. */
|
|
40
|
+
export function applyLaunchOverloadFailover(
|
|
41
|
+
selection: ChildLlmSelection | undefined,
|
|
42
|
+
flags: LaunchOverloadFlags = {},
|
|
43
|
+
readOutcomes?: () => ReturnType<typeof readLlmCallOutcomes>,
|
|
44
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
45
|
+
): ChildLlmSelection | undefined {
|
|
46
|
+
// Test processes never read this machine's operational logs.db — results must not depend on the host's history.
|
|
47
|
+
const inTest = env.NODE_ENV === 'test' || Boolean(env.ELANOUS_TEST_HOME);
|
|
48
|
+
const reader = launchOverloadOutcomesForTesting ?? readOutcomes ?? (inTest ? () => [] : () => readLlmCallOutcomes());
|
|
49
|
+
let outcomes: ReturnType<typeof readLlmCallOutcomes> = [];
|
|
50
|
+
try { outcomes = reader(); } catch { outcomes = []; }
|
|
51
|
+
const explicit = codexProviderNamed(flags.provider) || flags.codexPinned === true;
|
|
52
|
+
let grokAvailable = flags.grokAvailable;
|
|
53
|
+
if (grokAvailable === undefined && outcomes.length > 0 && !inTest) {
|
|
54
|
+
// Only resolve when a switch is possible — a host without grok credentials must stay on codex. Tests inject it.
|
|
55
|
+
try { grokAvailable = resolveGrokCredential() !== null; } catch { grokAvailable = false; }
|
|
56
|
+
}
|
|
57
|
+
const { selection: next, decision } = applyOverloadFailoverToChild(selection, {
|
|
58
|
+
codexExplicit: explicit,
|
|
59
|
+
outcomes,
|
|
60
|
+
...(grokAvailable !== undefined ? { grokAvailable } : {}),
|
|
61
|
+
defaultProvider: selection?.provider ?? 'openai-codex',
|
|
62
|
+
});
|
|
63
|
+
if (decision.switched && next?.provider === 'grok' && next.model) {
|
|
64
|
+
debug.log('self-dev', 'overload-failover', {
|
|
65
|
+
from: selection?.provider ?? 'openai-codex',
|
|
66
|
+
to: next.provider,
|
|
67
|
+
model: next.model,
|
|
68
|
+
why: decision.why,
|
|
69
|
+
}, { level: 'warn' });
|
|
70
|
+
return { provider: next.provider, model: next.model, source: 'config' };
|
|
71
|
+
}
|
|
72
|
+
return selection;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type { OverloadFailoverLaunchDecision };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Launch-time codex → grok switch driven only by `llm.call` / `outcome`.
|
|
2
|
+
//
|
|
3
|
+
// #24256 read `llm.response` / `llm.stream`. Those rows do not carry
|
|
4
|
+
// provider + status + kind, so a launch that consulted them never saw
|
|
5
|
+
// overload. This module refuses every other category.
|
|
6
|
+
//
|
|
7
|
+
// Boundaries:
|
|
8
|
+
// • an explicit codex child (flag or pinned config) never switches (#24183)
|
|
9
|
+
// • k consecutive codex overload/5xx/rate-limit outcomes switch the next
|
|
10
|
+
// child to grok before that child is spawned
|
|
11
|
+
// • k later successful outcomes restore codex
|
|
12
|
+
// • a 400 (kind `other`) never counts
|
|
13
|
+
|
|
14
|
+
import { logsDbPath, LogStore, type LogStoreRow } from '../mss/logging/log-store.js';
|
|
15
|
+
import {
|
|
16
|
+
codexOverloadStreakReached,
|
|
17
|
+
overloadRecovered,
|
|
18
|
+
OVERLOAD_FAILOVER_STREAK,
|
|
19
|
+
type LlmCallOutcome,
|
|
20
|
+
type LlmCallOutcomeKind,
|
|
21
|
+
} from '../session-runtime/retry-policy.js';
|
|
22
|
+
import { lookupLlmTierSpec } from '../model-tier/llm-tier-map.js';
|
|
23
|
+
|
|
24
|
+
export const LLM_CALL_CATEGORY = 'llm.call';
|
|
25
|
+
export const LLM_CALL_EVENT = 'outcome';
|
|
26
|
+
|
|
27
|
+
const KINDS = new Set<LlmCallOutcomeKind>(['ok', 'overloaded', '5xx', 'rate-limit', 'other']);
|
|
28
|
+
|
|
29
|
+
export interface OverloadFailoverLaunchInput {
|
|
30
|
+
/** True when the launch named codex (flag or pinned childLlm). */
|
|
31
|
+
readonly codexExplicit: boolean;
|
|
32
|
+
/** Provider the launch would have used before this gate. */
|
|
33
|
+
readonly provider: string;
|
|
34
|
+
readonly model?: string;
|
|
35
|
+
readonly outcomes: readonly LlmCallOutcome[];
|
|
36
|
+
readonly streak?: number;
|
|
37
|
+
/** grok may be selected. Unknown is not a reason to stay on a hot codex. */
|
|
38
|
+
readonly grokAvailable?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface OverloadFailoverLaunchDecision {
|
|
42
|
+
readonly provider: string;
|
|
43
|
+
readonly model?: string;
|
|
44
|
+
readonly switched: boolean;
|
|
45
|
+
readonly why: 'explicit-codex' | 'not-codex' | 'streak' | 'recovered' | 'below-streak' | 'grok-unavailable';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function parseLlmCallOutcome(row: Pick<LogStoreRow, 'category' | 'event' | 'data'>): LlmCallOutcome | undefined {
|
|
49
|
+
if (row.category !== LLM_CALL_CATEGORY || row.event !== LLM_CALL_EVENT || !row.data) return undefined;
|
|
50
|
+
let parsed: unknown;
|
|
51
|
+
try { parsed = JSON.parse(row.data); } catch { return undefined; }
|
|
52
|
+
if (!parsed || typeof parsed !== 'object') return undefined;
|
|
53
|
+
const rec = parsed as { provider?: unknown; status?: unknown; kind?: unknown };
|
|
54
|
+
if (typeof rec.provider !== 'string' || rec.provider.length === 0) return undefined;
|
|
55
|
+
if (typeof rec.status !== 'number' || !Number.isFinite(rec.status)) return undefined;
|
|
56
|
+
if (typeof rec.kind !== 'string' || !KINDS.has(rec.kind as LlmCallOutcomeKind)) return undefined;
|
|
57
|
+
return { provider: rec.provider, status: rec.status, kind: rec.kind as LlmCallOutcomeKind };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Newest-last. Rows that are not `llm.call`/`outcome` are dropped, never guessed. */
|
|
61
|
+
export function outcomesFromCallRows(rows: readonly (Pick<LogStoreRow, 'category' | 'event' | 'data' | 'ts_ms'> & { id?: number })[]): LlmCallOutcome[] {
|
|
62
|
+
// The store returns newest first; calls in the same millisecond keep that order unless ties break on row id.
|
|
63
|
+
return [...rows]
|
|
64
|
+
.filter((row) => row.category === LLM_CALL_CATEGORY && row.event === LLM_CALL_EVENT)
|
|
65
|
+
.sort((a, b) => a.ts_ms - b.ts_ms || (a.id ?? 0) - (b.id ?? 0))
|
|
66
|
+
.flatMap((row) => {
|
|
67
|
+
const parsed = parseLlmCallOutcome(row);
|
|
68
|
+
return parsed ? [parsed] : [];
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function readLlmCallOutcomes(opts: {
|
|
73
|
+
dbPath?: string;
|
|
74
|
+
sinceMs?: number;
|
|
75
|
+
limit?: number;
|
|
76
|
+
open?: (path: string) => Pick<LogStore, 'query' | 'close'>;
|
|
77
|
+
} = {}): LlmCallOutcome[] {
|
|
78
|
+
const path = opts.dbPath ?? logsDbPath();
|
|
79
|
+
const open = opts.open ?? ((dbPath: string) => LogStore.openReadOnly(dbPath));
|
|
80
|
+
let store: Pick<LogStore, 'query' | 'close'> | undefined;
|
|
81
|
+
try {
|
|
82
|
+
store = open(path);
|
|
83
|
+
const rows = store.query({
|
|
84
|
+
exactCategories: [LLM_CALL_CATEGORY],
|
|
85
|
+
events: [LLM_CALL_EVENT],
|
|
86
|
+
...(opts.sinceMs !== undefined ? { sinceMs: opts.sinceMs } : {}),
|
|
87
|
+
limit: opts.limit ?? 50,
|
|
88
|
+
});
|
|
89
|
+
return outcomesFromCallRows(rows);
|
|
90
|
+
} catch {
|
|
91
|
+
return [];
|
|
92
|
+
} finally {
|
|
93
|
+
try { store?.close(); } catch { /* a missing store is "no sample", not a launch failure */ }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function decideOverloadFailoverLaunch(input: OverloadFailoverLaunchInput): OverloadFailoverLaunchDecision {
|
|
98
|
+
const streak = input.streak ?? OVERLOAD_FAILOVER_STREAK;
|
|
99
|
+
const base = { provider: input.provider, ...(input.model !== undefined ? { model: input.model } : {}) };
|
|
100
|
+
if (input.codexExplicit) return { ...base, switched: false, why: 'explicit-codex' };
|
|
101
|
+
const onCodex = input.provider === 'openai-codex' || input.provider === 'codex';
|
|
102
|
+
if (!onCodex) return { ...base, switched: false, why: 'not-codex' };
|
|
103
|
+
if (overloadRecovered(input.outcomes, streak)) return { ...base, switched: false, why: 'recovered' };
|
|
104
|
+
if (!codexOverloadStreakReached(input.outcomes, streak)) return { ...base, switched: false, why: 'below-streak' };
|
|
105
|
+
if (input.grokAvailable === false) return { ...base, switched: false, why: 'grok-unavailable' };
|
|
106
|
+
return {
|
|
107
|
+
provider: 'grok',
|
|
108
|
+
model: lookupLlmTierSpec('grok', 'better').model,
|
|
109
|
+
switched: true,
|
|
110
|
+
why: 'streak',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Apply the gate to a not-yet-pinned child selection. Explicit codex is left untouched. */
|
|
115
|
+
export function applyOverloadFailoverToChild<T extends { provider: string; model: string; source: 'flag' | 'config' }>(
|
|
116
|
+
selection: T | undefined,
|
|
117
|
+
opts: {
|
|
118
|
+
codexExplicit: boolean;
|
|
119
|
+
outcomes: readonly LlmCallOutcome[];
|
|
120
|
+
grokAvailable?: boolean;
|
|
121
|
+
defaultProvider?: string;
|
|
122
|
+
},
|
|
123
|
+
): { selection: T | { provider: string; model: string; source: 'config' } | undefined; decision: OverloadFailoverLaunchDecision } {
|
|
124
|
+
const provider = selection?.provider ?? opts.defaultProvider ?? 'openai-codex';
|
|
125
|
+
const decision = decideOverloadFailoverLaunch({
|
|
126
|
+
codexExplicit: opts.codexExplicit,
|
|
127
|
+
provider,
|
|
128
|
+
...(selection?.model !== undefined ? { model: selection.model } : {}),
|
|
129
|
+
outcomes: opts.outcomes,
|
|
130
|
+
grokAvailable: opts.grokAvailable,
|
|
131
|
+
});
|
|
132
|
+
if (!decision.switched || !decision.model) return { selection, decision };
|
|
133
|
+
return {
|
|
134
|
+
selection: { provider: decision.provider, model: decision.model, source: 'config' },
|
|
135
|
+
decision,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -18,6 +18,8 @@ export interface ChecklistItem {
|
|
|
18
18
|
deadlineVersion?: string;
|
|
19
19
|
ceoMinutes?: number;
|
|
20
20
|
ceoDate?: string;
|
|
21
|
+
/** 다른 칸의 처리량을 올리는 칸(자율성·효율성·동시성). 없으면 가속 등급이 아니다. */
|
|
22
|
+
accelerator?: true;
|
|
21
23
|
evidence?: string;
|
|
22
24
|
disposition?: ChecklistDisposition;
|
|
23
25
|
kind?: ChecklistKind;
|
|
@@ -103,7 +105,7 @@ export function validateCeoLoad(input: { ceoMinutes?: number; ceoDate?: string }
|
|
|
103
105
|
if (input.ceoDate !== undefined && (!/^\d{4}-\d{2}-\d{2}$/.test(input.ceoDate) || !Number.isFinite(Date.parse(`${input.ceoDate}T00:00:00Z`)) || new Date(`${input.ceoDate}T00:00:00Z`).toISOString().slice(0, 10) !== input.ceoDate)) throw new CliUserError('대표 손 날짜는 YYYY-MM-DD 이어야 한다');
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }, options: { allowDuplicateId?: boolean } = {}): Checklist {
|
|
108
|
+
export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }, options: { allowDuplicateId?: boolean } = {}): Checklist {
|
|
107
109
|
if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
|
|
108
110
|
if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
|
|
109
111
|
if (input.kind !== undefined && input.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen');
|
|
@@ -111,6 +113,7 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
|
|
|
111
113
|
if (input.priority !== undefined && !['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
|
|
112
114
|
if (input.predecessors !== undefined && (!Array.isArray(input.predecessors) || input.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === input.id))) throw new CliUserError('잘못된 선행 칸');
|
|
113
115
|
if (input.deadlineVersion !== undefined) store.validateVersion(input.deadlineVersion);
|
|
116
|
+
if (input.accelerator !== undefined && input.accelerator !== true) throw new CliUserError('가속 등급은 true 이거나 생략한다');
|
|
114
117
|
validateCeoLoad(input);
|
|
115
118
|
return mutate(v, (data, otherItems) => {
|
|
116
119
|
if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
|
|
@@ -122,14 +125,14 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
|
|
|
122
125
|
}
|
|
123
126
|
const by = process.env.ELANOUS_TRACK || 'cli';
|
|
124
127
|
const at = new Date().toISOString();
|
|
125
|
-
const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), updatedAt: at, updatedBy: by };
|
|
128
|
+
const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator === true ? { accelerator: true as const } : {}), updatedAt: at, updatedBy: by };
|
|
126
129
|
data.items.push(item);
|
|
127
130
|
change(data, item.id, 'add', null, item, by, at);
|
|
128
131
|
return true;
|
|
129
132
|
});
|
|
130
133
|
}
|
|
131
134
|
|
|
132
|
-
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }, by: string): Checklist {
|
|
135
|
+
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean | null }, by: string): Checklist {
|
|
133
136
|
return mutate(v, (data) => {
|
|
134
137
|
const item = data.items.find((i) => i.id === id);
|
|
135
138
|
if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
|
|
@@ -139,6 +142,7 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
|
|
|
139
142
|
if (patch.priority !== undefined && !['P0', 'P1', 'P2'].includes(patch.priority)) throw new CliUserError(`잘못된 우선순위: ${patch.priority}`);
|
|
140
143
|
if (patch.predecessors !== undefined && (!Array.isArray(patch.predecessors) || patch.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === id))) throw new CliUserError('잘못된 선행 칸');
|
|
141
144
|
if (patch.deadlineVersion !== undefined) store.validateVersion(patch.deadlineVersion);
|
|
145
|
+
if (patch.accelerator !== undefined && patch.accelerator !== true && patch.accelerator !== null) throw new CliUserError('가속 등급은 true(부여) 또는 null(해제)이다');
|
|
142
146
|
validateCeoLoad(patch);
|
|
143
147
|
if (patch.owner !== undefined) {
|
|
144
148
|
parseOwner(patch.owner);
|
|
@@ -146,13 +150,14 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
|
|
|
146
150
|
throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
|
-
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field]));
|
|
153
|
+
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate', 'accelerator'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field] ?? null));
|
|
150
154
|
if (fields.length === 0) return false;
|
|
151
155
|
const at = new Date().toISOString();
|
|
152
156
|
for (const field of fields) {
|
|
153
157
|
const from = item[field];
|
|
154
158
|
const to = patch[field]!;
|
|
155
|
-
(
|
|
159
|
+
if (field === 'accelerator' && to === null) delete item.accelerator;
|
|
160
|
+
else (item as unknown as Record<string, unknown>)[field] = to;
|
|
156
161
|
item.updatedAt = at;
|
|
157
162
|
item.updatedBy = by;
|
|
158
163
|
change(data, id, field, from, to, by, at);
|