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
package/src/cli/usage-cli.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Command } from 'commander';
|
|
|
10
10
|
import { collectUnifiedUsage, formatUnifiedUsage } from '../budget/unified-usage.js';
|
|
11
11
|
import { consumeCodexResetCredits, listCodexResetCredits } from '../budget/codex-reset-credits.js';
|
|
12
12
|
import { rollupRunUsage, type RunUsageInput } from '../budget/run-usage-rollup.js';
|
|
13
|
+
import { formatUsageOutcomes, outcomeTableHasCodexGrokUnknown, queryUsageOutcomes, type UsageOutcomesDeps, type UsageOutcomesReport } from '../budget/usage-outcomes.js';
|
|
13
14
|
import { LogStore, logsDbPath, type LogQuery, type LogStoreRow } from '../mss/logging/log-store.js';
|
|
14
15
|
import { LOGS_SINCE_OPTION, parseSince } from './logs-cli.js';
|
|
15
16
|
import { formatCodexCreditPlan, type CodexCreditPlan } from '../budget/codex-credit-plan.js';
|
|
@@ -28,6 +29,9 @@ export interface UsageCliDeps {
|
|
|
28
29
|
/** 계정 이름 → CODEX_HOME. ⛔ 여기 없는 이름은 «거부»한다(모르는 곳에 쓰지 않는다). */
|
|
29
30
|
readonly homes?: Readonly<Record<string, string>>;
|
|
30
31
|
readonly exit?: (code: number) => never;
|
|
32
|
+
/** 런 원장 결과 표. 시험은 디렉터리를 주입한다. */
|
|
33
|
+
readonly outcomes?: (sinceDays: number) => UsageOutcomesReport;
|
|
34
|
+
readonly outcomeDeps?: UsageOutcomesDeps;
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
/** 계정 이름 → CODEX_HOME 기본 표.
|
|
@@ -134,6 +138,28 @@ export function registerUsageCommand(program: Command, deps: UsageCliDeps = {}):
|
|
|
134
138
|
// 🩸 그래서 ⑴ `--yes` 없이는 «안 쓴다» ⑵ 쓰기 «전»에 남은 수를 보여 준다
|
|
135
139
|
// ⑶ 쓴 «뒤»에도 다시 조회해 「줄었나」를 값으로 낸다.
|
|
136
140
|
// 📏 2026-09-24 현황 예: default 100%·team 100%·third 46% ⇒ third 에 쓰면 54% 가 버려진다.
|
|
141
|
+
const outcomes = deps.outcomes ?? ((sinceDays: number) => queryUsageOutcomes(sinceDays, deps.outcomeDeps));
|
|
142
|
+
usage
|
|
143
|
+
.command('outcomes')
|
|
144
|
+
.description('프로바이더별 무인 착지율 · 라운드 수 · 막힘 사유 분포 (런 원장 childProvider · 미기록은 불명)')
|
|
145
|
+
.option('--days <n>', '최근 N일', '2')
|
|
146
|
+
.option('--json', 'JSON 출력')
|
|
147
|
+
.action((opts: { days?: string; json?: boolean }, command: Command) => {
|
|
148
|
+
const json = opts.json || command.optsWithGlobals<{ json?: boolean }>().json;
|
|
149
|
+
const days = Number(opts.days);
|
|
150
|
+
if (!Number.isFinite(days) || days <= 0) {
|
|
151
|
+
out.log(`⛔ --days 는 양의 수: '${opts.days}'`);
|
|
152
|
+
exit(2);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const report = outcomes(days);
|
|
156
|
+
if (json) {
|
|
157
|
+
out.log(JSON.stringify({ ...report, hasCodexGrokUnknown: outcomeTableHasCodexGrokUnknown(report) }, null, 2));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
out.log(formatUsageOutcomes(report));
|
|
161
|
+
});
|
|
162
|
+
|
|
137
163
|
const list = deps.list ?? listCodexResetCredits;
|
|
138
164
|
const consume = deps.consume ?? consumeCodexResetCredits;
|
|
139
165
|
const homes = deps.homes ?? defaultHomes();
|
|
@@ -66,6 +66,7 @@ import { localOcrText } from './telegram-card-followup.js';
|
|
|
66
66
|
import type { runGraph } from './graph-runner/runner.js';
|
|
67
67
|
import { readSlashContextNow, renderTelegramNow } from './context-bus/context-now-surfaces.js';
|
|
68
68
|
import type { ContextNowDeps } from './context-bus/context-now.js';
|
|
69
|
+
import { botUnavailableSlashReply, deriveTuiSlashAvailability, type TuiSlashAvailability } from './maturity/tui-slash-availability.js';
|
|
69
70
|
|
|
70
71
|
const SLASH_FOCUS_TURNS_DEFAULT = 8;
|
|
71
72
|
|
|
@@ -142,6 +143,8 @@ export interface DiscordSelfMessageDeps {
|
|
|
142
143
|
log?: (msg: string) => void;
|
|
143
144
|
cardFollowupDeps?: { ocrText?: (path: string) => Promise<string | null>; runGraph?: typeof runGraph; rootDir?: () => string };
|
|
144
145
|
nowDeps?: ContextNowDeps;
|
|
146
|
+
/** Override the TUI slash projection for isolated message-path tests. */
|
|
147
|
+
tuiSlashAvailability?: readonly TuiSlashAvailability[];
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
/** Compose the full discord self+interweave onMessage handler. */
|
|
@@ -399,6 +402,12 @@ export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessa
|
|
|
399
402
|
log(`session cmd: ${ctx.text.trim().slice(0, 40)}`);
|
|
400
403
|
return sessionReply;
|
|
401
404
|
}
|
|
405
|
+
const slashName = /^\/([a-z0-9_-]{1,32})(?:\s|$)/i.exec(ctx.text.trim())?.[1]?.toLowerCase();
|
|
406
|
+
if (slashName) {
|
|
407
|
+
const entry = (deps.tuiSlashAvailability ?? deriveTuiSlashAvailability()).find(({ name }) => name === slashName);
|
|
408
|
+
const unavailable = botUnavailableSlashReply(entry, 'discord');
|
|
409
|
+
if (unavailable) return unavailable;
|
|
410
|
+
}
|
|
402
411
|
log(`◀ ${ctx.userName ?? ctx.userId}: ${ctx.text.slice(0, 80)}${ctx.attachments.length ? ` (+첨부 ${ctx.attachments.length})` : ''}`);
|
|
403
412
|
debug.log('discord.self.turn', 'inbound', { channelId: ctx.channelId, chars: ctx.text.length, attachments: ctx.attachments.length });
|
|
404
413
|
// C2+ — 첨부 기억(사진만 먼저 올리는 패턴 지원). 명령 여부와 무관.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TaskCard } from '../task-cards/card-store.js';
|
|
2
|
+
|
|
3
|
+
export interface SplitCell { id: string; title: string; seat?: string; host?: string }
|
|
4
|
+
|
|
5
|
+
/** FLOW1: only explicitly assigned work may enter a seat's queue. The same proposal is used in shadow and live. */
|
|
6
|
+
export function splitCard(card: TaskCard, _opts: { shadow?: boolean } = {}): SplitCell[] {
|
|
7
|
+
const intake = [...card.sections].reverse().find(section => section.key.startsWith('intake:wish:'));
|
|
8
|
+
let text = card.title;
|
|
9
|
+
if (intake) {
|
|
10
|
+
let body: unknown;
|
|
11
|
+
try { body = JSON.parse(intake.content); } catch { body = intake.content; }
|
|
12
|
+
if (typeof body === 'string') text = body;
|
|
13
|
+
else if (body && typeof body === 'object' && typeof (body as { text?: unknown }).text === 'string') text = (body as { text: string }).text;
|
|
14
|
+
}
|
|
15
|
+
const lines = text.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
|
|
16
|
+
const cells: SplitCell[] = lines.map((line, index) => {
|
|
17
|
+
const match = /^(?:[-*]|\d+[.)])?\s*\[(OP|MK|TC|UX)\]\s*(.+)$/u.exec(line);
|
|
18
|
+
if (match) return { id: `${card.id}-${index + 1}`, title: match[2]!.trim(), seat: match[1]! };
|
|
19
|
+
// 담당 없는 행도 칸으로 남긴다 — seat 가 없으니 대기열엔 못 들어가고 tick 이 unplaced-no-owner 로 센다.
|
|
20
|
+
return { id: `${card.id}-${index + 1}`, title: line.replace(/^(?:[-*]|\d+[.)])\s*/u, '') };
|
|
21
|
+
});
|
|
22
|
+
// 의뢰 본문이 있으면 담당이 하나도 없어도 행마다 칸으로 남긴다. 본문 없이 제목뿐일 때만 카드 한 칸.
|
|
23
|
+
return intake || cells.some(cell => cell.seat) ? cells : [{ id: card.id, title: card.title }];
|
|
24
|
+
}
|
|
@@ -23,6 +23,7 @@ import { queryRunningRuns } from '../self-implement/running-runs.js';
|
|
|
23
23
|
import { DevPipelineError } from '../self-dev/dev-pipeline.js';
|
|
24
24
|
import { runDraftSweep, sweepFailureReason, type DraftSweepAdapters, type DraftSweepResult, type SweepDraft, type SweepMergedPr, type SweepReviewGate } from '../self-dev/draft-sweep.js';
|
|
25
25
|
import { debug } from '../debug/log.js';
|
|
26
|
+
import { decideNestedElanousLaunch, readNestedElanousDepth } from './nested-elanous-policy.js';
|
|
26
27
|
import { dispatchTask, type DispatchTaskInput, type DispatchTaskDeps } from '../execution-loop/dispatch-task.js';
|
|
27
28
|
import { launchRequestId, preLaunchGate, type PreLaunchGateDeps } from '../execution-loop/launch-gate.js';
|
|
28
29
|
import { PR_LABELS } from '../github/pr-labels.js';
|
|
@@ -110,6 +111,8 @@ export interface HarnessAskSayOptions {
|
|
|
110
111
|
supervisorSource?: HarnessSupervisorSource;
|
|
111
112
|
goalType?: GoalType;
|
|
112
113
|
correlation?: string;
|
|
114
|
+
/** Depth 0 only. At depth >= 1 this flag is ignored and the launch stays refused. */
|
|
115
|
+
nestedElanous?: 'allow';
|
|
113
116
|
}
|
|
114
117
|
|
|
115
118
|
export interface ResolvedHarnessSupervisor {
|
|
@@ -174,7 +177,8 @@ function registerHarnessCommonOptions(command: Command): Command {
|
|
|
174
177
|
.option('--merge-by-host', 'Pod: merge-ready 까지 실행하고 병합은 발사 호스트가 재게이트')
|
|
175
178
|
.option('--observe-only', 'elanous: child boot부터 SelfImplement 호출을 기록만 한다')
|
|
176
179
|
.addOption(new Option('--no-supervise', 'self: supervisor 재개를 끔').hideHelp())
|
|
177
|
-
.option('--dry-run', '변경 없이 발사 계획만 출력')
|
|
180
|
+
.option('--dry-run', '변경 없이 발사 계획만 출력')
|
|
181
|
+
.addOption(new Option('--nested-elanous <policy>', '깊이 0만: 중첩 elanous 발사를 허용(allow). 깊이 1 이상은 무시되고 거부가 유지된다').choices(['allow']));
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
type HarnessDryRunOpts = { dryRun?: boolean };
|
|
@@ -299,9 +303,33 @@ function normalizeHarnessCommonOptions(opts: HarnessAskSayOptions): HarnessAskSa
|
|
|
299
303
|
...(opts.observeOnly ? { observeOnly: true } : {}),
|
|
300
304
|
...(opts.goalType !== undefined ? { goalType: opts.goalType } : {}),
|
|
301
305
|
...resolveHarnessSupervisor(opts),
|
|
306
|
+
...resolveNestedElanousOption(opts),
|
|
302
307
|
};
|
|
303
308
|
}
|
|
304
309
|
|
|
310
|
+
export class NestedElanousRefused extends Error {
|
|
311
|
+
constructor(readonly depth: number) {
|
|
312
|
+
super(`nested elanous refused (allow-ignored, depth ${depth})`);
|
|
313
|
+
this.name = 'NestedElanousRefused';
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Depth 0 may keep `--nested-elanous allow`.
|
|
319
|
+
* Depth >= 1 cannot flip the refusal: the flag is ignored, logged, and the launch stops.
|
|
320
|
+
* No flag means an ordinary launch — unset depth is 0 and nothing is refused here.
|
|
321
|
+
*/
|
|
322
|
+
export function resolveNestedElanousOption(
|
|
323
|
+
opts: Pick<HarnessAskSayOptions, 'nestedElanous'>,
|
|
324
|
+
env: Record<string, string | undefined> = process.env,
|
|
325
|
+
): { nestedElanous?: 'allow' } {
|
|
326
|
+
if (opts.nestedElanous !== 'allow') return {};
|
|
327
|
+
const decision = decideNestedElanousLaunch({ depth: readNestedElanousDepth(env), allow: true });
|
|
328
|
+
if (decision.reason === 'allow-ignored') throw new NestedElanousRefused(decision.depth);
|
|
329
|
+
if (!decision.allowed) return {};
|
|
330
|
+
return { nestedElanous: 'allow' };
|
|
331
|
+
}
|
|
332
|
+
|
|
305
333
|
function normalizeHarnessAskSayOptions(opts: HarnessAskSayChildLlmOptions): HarnessAskSayChildLlmOptions {
|
|
306
334
|
return {
|
|
307
335
|
...normalizeHarnessCommonOptions(opts),
|
|
@@ -704,7 +732,13 @@ async function dispatchHarnessAskSay(
|
|
|
704
732
|
const podSeat = resolved.substrate === 'pod' && ['OP', 'TC', 'MK', 'UX'].includes(inheritedSeat ?? '')
|
|
705
733
|
? inheritedSeat as QueueSeat : undefined;
|
|
706
734
|
const stampedSeat = resolved.substrate === 'pod' ? opts.seat ?? queueSeat ?? podSeat ?? assigned : assigned;
|
|
707
|
-
|
|
735
|
+
// Depth >= 1 cannot re-allow. Drop the flag here so the launch sees the refusal, not the raw argv.
|
|
736
|
+
const nested = resolveNestedElanousOption(opts);
|
|
737
|
+
const stampedOpts = {
|
|
738
|
+
...opts,
|
|
739
|
+
...(stampedSeat ? { seat: stampedSeat } : {}),
|
|
740
|
+
...(nested.nestedElanous ? { nestedElanous: nested.nestedElanous } : { nestedElanous: undefined }),
|
|
741
|
+
};
|
|
708
742
|
const launch = async () => {
|
|
709
743
|
const previous = process.env.ELANOUS_HARNESS_SEAT;
|
|
710
744
|
if (stampedSeat) process.env.ELANOUS_HARNESS_SEAT = stampedSeat;
|
|
@@ -7,10 +7,14 @@
|
|
|
7
7
|
* 매니페스트가 실은 런 계약(`ELANOUS_RUN_CONTRACT`)으로 자기가 Pod 인 줄 안다(graph-run-contract.ts).
|
|
8
8
|
*/
|
|
9
9
|
import { spawn } from 'node:child_process';
|
|
10
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
11
|
+
import { hostname } from 'node:os';
|
|
10
12
|
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
13
|
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
12
14
|
import { selfDevRunsDir } from '../self-dev/run-store.js';
|
|
13
15
|
import { findGitDir } from '../git-fs/locate.js';
|
|
16
|
+
import { logsDbPath } from '../mss/logging/log-store.js';
|
|
17
|
+
import { POD_REEMIT_LOGS_DB_ENV } from '../task-orchestrator/surfaces/pod-ledger-prod-sink.js';
|
|
14
18
|
import { debug } from '../debug/log.js';
|
|
15
19
|
import { ELANOUS_ENTRY_SCRIPT } from '../self-implement/seams.js';
|
|
16
20
|
import { parsePodSourceSpec } from '../task-orchestrator/surfaces/pod-source-spec.js';
|
|
@@ -18,6 +22,7 @@ import { declaredGoalType, type GoalType } from '../self-implement/goal-author.j
|
|
|
18
22
|
import { appendRunLedgerEntry } from '../self-implement/run-ledger.js';
|
|
19
23
|
import { HARNESS_RUN_ID_ENV } from './harness-space.js';
|
|
20
24
|
import { podMemoryLimitFor, type PodMemoryTier } from '../task-orchestrator/surfaces/self-implement-pod.js';
|
|
25
|
+
import { oldDoorInternalEnv } from '../self-dev/old-door.js';
|
|
21
26
|
import { selectPodMemoryTier } from './pod-memory-policy.js';
|
|
22
27
|
|
|
23
28
|
export interface HarnessPodDispatchInput {
|
|
@@ -46,44 +51,6 @@ export interface HarnessPodDispatchInput {
|
|
|
46
51
|
readonly authorOnPod?: boolean;
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
/** 골 본문에 넣을 문장 — 코드 펜스가 문장을 삼키지 않게 백틱 런을 늘린다. */
|
|
50
|
-
export function fenceVerbatimSentence(sentence: string): { fence: string; body: string } {
|
|
51
|
-
const body = sentence.replace(/\r\n/g, '\n');
|
|
52
|
-
const longest = body.match(/`+/g)?.reduce((n, run) => Math.max(n, run.length), 0) ?? 0;
|
|
53
|
-
return { fence: '`'.repeat(Math.max(3, longest + 1)), body };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Pod 가 문장 그대로 `harness say` 를 돌리게 하는 골. 자격·풀은 기존 Pod 주입을 그대로 쓴다. */
|
|
57
|
-
export function authorOnPodGoal(sentence: string): string {
|
|
58
|
-
const { fence, body } = fenceVerbatimSentence(sentence);
|
|
59
|
-
return [
|
|
60
|
-
'# 골: Pod 안에서 harness say 저작',
|
|
61
|
-
'',
|
|
62
|
-
'- GoalType: implement',
|
|
63
|
-
'- 대상 경로: src/harness/harness-pod-dispatch.ts',
|
|
64
|
-
'',
|
|
65
|
-
'## 할 일',
|
|
66
|
-
'',
|
|
67
|
-
'이 Pod 안에서, 그리고 이 Pod 안에서만, 아래 펜스의 문장을 한 글자도 바꾸지 않고',
|
|
68
|
-
'`elanous harness say --substrate local` 의 인자로 실행한다.',
|
|
69
|
-
'그 한 발이 골 저작·리뷰·게이트·구현·PR 까지 이 Pod 안에서 끝낸다.',
|
|
70
|
-
'호스트에는 발사 영수증과 원장만 남는다. 호스트에서 저작·리뷰·게이트를 지휘하지 않는다.',
|
|
71
|
-
'저작 자격은 이 Job 에 이미 주입된 Pod 자격 규칙을 그대로 쓴다.',
|
|
72
|
-
'',
|
|
73
|
-
'## 문장 (verbatim)',
|
|
74
|
-
'',
|
|
75
|
-
fence,
|
|
76
|
-
body,
|
|
77
|
-
fence,
|
|
78
|
-
'',
|
|
79
|
-
'## 수용 기준',
|
|
80
|
-
'',
|
|
81
|
-
'- 위 펜스 문장만으로 `elanous harness say --substrate local` 한 발이 이 Pod 에서 저작부터 PR 까지 간다.',
|
|
82
|
-
'- 그 명령의 종료 코드가 0 이 아니면 이 골은 실패다.',
|
|
83
|
-
'',
|
|
84
|
-
].join('\n');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
54
|
export function podOrchestrateArgs(input: HarnessPodDispatchInput, goalFile: string): string[] {
|
|
88
55
|
return [
|
|
89
56
|
ELANOUS_ENTRY_SCRIPT, 'self', 'orchestrate', '--goal-file', goalFile,
|
|
@@ -116,7 +83,9 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
|
|
|
116
83
|
return 2;
|
|
117
84
|
}
|
|
118
85
|
}
|
|
119
|
-
const env = { ...process.env };
|
|
86
|
+
const env = { ...process.env, ...oldDoorInternalEnv('self-orchestrate') };
|
|
87
|
+
// POD-OBS(10-06): 자식 orchestrate 는 트리 파생 우주에서 돈다 — Pod 원장 재방출을 «발사한 집»의 logs.db 에도 남기게 경로를 넘긴다.
|
|
88
|
+
env[POD_REEMIT_LOGS_DB_ENV] = logsDbPath();
|
|
120
89
|
let tempDir: string | undefined;
|
|
121
90
|
let goalFile = '';
|
|
122
91
|
let goalText: string;
|
|
@@ -124,21 +93,22 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
|
|
|
124
93
|
goalFile = resolve(cwd, input.input);
|
|
125
94
|
goalText = (deps.readFile ?? ((p) => readFileSync(p, 'utf8')))(goalFile);
|
|
126
95
|
} else if (input.authorOnPod) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
96
|
+
// AUTHOR-POD2 (10-06) — the Pod runs `harness say` on the verbatim sentence itself (podJobManifest
|
|
97
|
+
// authorSentence branch). No wrapper goal and no host-side file under docs/goals: the 10-06 live run
|
|
98
|
+
// showed a wrapper goal is implemented as code by the Pod child instead of being executed.
|
|
99
|
+
goalText = input.input;
|
|
100
|
+
const runsDir = selfDevRunsDir();
|
|
131
101
|
mkdirSync(runsDir, { recursive: true });
|
|
132
|
-
tempDir = mkdtempSync(join(runsDir, '
|
|
133
|
-
goalFile = join(tempDir, '
|
|
134
|
-
writeFileSync(goalFile,
|
|
135
|
-
const runId = env[HARNESS_RUN_ID_ENV]?.trim()
|
|
136
|
-
|
|
102
|
+
tempDir = mkdtempSync(join(runsDir, 'pod-author-'));
|
|
103
|
+
goalFile = join(tempDir, 'sentence.txt');
|
|
104
|
+
writeFileSync(goalFile, input.input, { mode: 0o600 });
|
|
105
|
+
const runId = env[HARNESS_RUN_ID_ENV]?.trim() || `run-${randomUUID()}`;
|
|
106
|
+
env[HARNESS_RUN_ID_ENV] = runId;
|
|
107
|
+
const receipt = { entrance: input.entrance, host: hostname(), runId, authorOnPod: true,
|
|
108
|
+
sentenceChars: input.input.length, sentenceSha256: createHash('sha256').update(input.input).digest('hex') };
|
|
137
109
|
debug.log('harness.substrate', 'author-on-pod-receipt', receipt);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
catch (error) { debug.log('harness.substrate', 'author-on-pod-receipt-unwritten', { reason: error instanceof Error ? error.message : String(error) }); }
|
|
141
|
-
}
|
|
110
|
+
try { appendRunLedgerEntry({ runId, event: 'author-on-pod-receipt', data: receipt }); }
|
|
111
|
+
catch (error) { debug.log('harness.substrate', 'author-on-pod-receipt-unwritten', { reason: error instanceof Error ? error.message : String(error) }); }
|
|
142
112
|
} else {
|
|
143
113
|
const goal = input.input;
|
|
144
114
|
goalText = goal;
|
|
@@ -176,9 +146,9 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
|
|
|
176
146
|
if (input.after) env.ELANOUS_POD_AFTER = input.after;
|
|
177
147
|
if (input.authorOnPod) env.ELANOUS_POD_AUTHOR_ON_POD = '1';
|
|
178
148
|
else delete env.ELANOUS_POD_AUTHOR_ON_POD;
|
|
179
|
-
if (input.entrance === 'cli-harness-ask'
|
|
149
|
+
if (input.entrance === 'cli-harness-ask') {
|
|
180
150
|
const root = findGitDir(cwd)?.root;
|
|
181
|
-
const path =
|
|
151
|
+
const path = resolve(cwd, input.input);
|
|
182
152
|
const relativePath = root ? relative(realpathSync(root), realpathSync(path)) : '';
|
|
183
153
|
if (relativePath && relativePath !== '..' && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath)) {
|
|
184
154
|
env.ELANOUS_POD_GOAL_DOC = relativePath;
|
|
@@ -208,7 +178,7 @@ export function dispatchHarnessOnPod(input: HarnessPodDispatchInput, deps: PodDi
|
|
|
208
178
|
child.once('error', () => { cleanup(); resolveStatus(1); });
|
|
209
179
|
child.once('close', (code, signal) => { cleanup(); resolveStatus(code ?? ((signal ?? forwarded) === 'SIGINT' ? 130 : 143)); });
|
|
210
180
|
}));
|
|
211
|
-
const cleanup = () => { if (tempDir
|
|
181
|
+
const cleanup = () => { if (tempDir) rmSync(tempDir, { recursive: true, force: true }); };
|
|
212
182
|
try {
|
|
213
183
|
const status = run(process.execPath, args, env);
|
|
214
184
|
if (typeof status === 'number' || status === null) {
|
|
@@ -727,10 +727,13 @@ export function buildHarnessSeams(deps: HarnessSeamsDeps): StagedHarnessSeams {
|
|
|
727
727
|
let inspectionError: string | undefined;
|
|
728
728
|
try {
|
|
729
729
|
if (!deps.seams.readPrFiles) throw new Error('readPrFiles seam unavailable');
|
|
730
|
-
const prFiles = await deps.seams.readPrFiles({ number: pr.number, cwd });
|
|
730
|
+
const prFiles = await deps.seams.readPrFiles({ number: pr.number, cwd, ...(deps.base ? { base: deps.base } : {}) });
|
|
731
731
|
if (!Array.isArray(prFiles) || !prFiles.every((path) => typeof path === 'string')) throw new Error('invalid PR file list');
|
|
732
732
|
holdPath = releasePathHold(prFiles);
|
|
733
|
-
} catch (error) {
|
|
733
|
+
} catch (error) {
|
|
734
|
+
const rateLimited = error instanceof Error && (error as { rateLimited?: boolean }).rateLimited === true;
|
|
735
|
+
inspectionError = rateLimited ? `rate-limit: ${String(error)}` : String(error);
|
|
736
|
+
}
|
|
734
737
|
if (holdPath) {
|
|
735
738
|
const protectedPath = holdPath;
|
|
736
739
|
const prCwd = cwd;
|
|
@@ -745,7 +748,14 @@ export function buildHarnessSeams(deps: HarnessSeamsDeps): StagedHarnessSeams {
|
|
|
745
748
|
observe('deployed-pr', { number: pr.number, url: pr.url, autoReview: false, mergeReason: 'release-path-hold' });
|
|
746
749
|
} else if (inspectionError) {
|
|
747
750
|
observe('release-path-inspection-failed', { number: pr.number, error: inspectionError });
|
|
748
|
-
observe('deployed-pr', {
|
|
751
|
+
observe('deployed-pr', {
|
|
752
|
+
number: pr.number,
|
|
753
|
+
url: pr.url,
|
|
754
|
+
autoReview: false,
|
|
755
|
+
mergeReason: inspectionError.startsWith('rate-limit:')
|
|
756
|
+
? 'release-path-inspection-failed: rate-limit'
|
|
757
|
+
: 'release-path-inspection-failed',
|
|
758
|
+
});
|
|
749
759
|
} else {
|
|
750
760
|
let autoReviewLabeled = false;
|
|
751
761
|
if (labels?.includes(AUTO_REVIEW_LABEL)) {
|
|
@@ -2,6 +2,9 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { debug } from '../debug/log.js';
|
|
5
|
+
import { releasePathHold, releasePrFilePaths } from '../self-dev/release-path-guard.js';
|
|
6
|
+
import { getUserConfig, type UserConfig } from '../user-config.js';
|
|
7
|
+
import type { QueueItem, QueueSeat } from './harness-queue.js';
|
|
5
8
|
import type { RunGh, ScannedStoppedPr, StoppedPrCategory } from './helper-scan.js';
|
|
6
9
|
|
|
7
10
|
export interface RepairGoalInput {
|
|
@@ -62,26 +65,154 @@ export function repairInputsFromBody(body: string): { originalRequest: string; m
|
|
|
62
65
|
|
|
63
66
|
export type RepairShadowRow = { pr: number; category: StoppedPrCategory; goalHash: string; at: string };
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
type RepairLedgerResult = 'shadow' | 'launched' | 'cap' | 'needs-human' | 'launch-failed';
|
|
69
|
+
|
|
70
|
+
type RepairLedgerLine = {
|
|
71
|
+
pr: number; category: StoppedPrCategory; goalHash: string; at: string;
|
|
72
|
+
mode: 'shadow' | 'live'; goal: string; result: RepairLedgerResult;
|
|
73
|
+
/** Harness queue item id — the run itself starts on a later queue tick. */ queueId?: string; reason?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const RELEASE_FILE_PAGE = 100;
|
|
77
|
+
|
|
78
|
+
/** Changed files of the original PR. A failed read is a reason to hold, never a reason to launch. */
|
|
79
|
+
async function changedFiles(pr: number, runGh: RunGh): Promise<string[]> {
|
|
80
|
+
const pages: unknown[] = [];
|
|
81
|
+
for (let page = 1; ; page++) {
|
|
82
|
+
const raw = await runGh(['api', '--method', 'GET', '--raw-field', `per_page=${RELEASE_FILE_PAGE}`, '--raw-field', `page=${page}`, `repos/{owner}/{repo}/pulls/${pr}/files`]);
|
|
83
|
+
const parsed: unknown = JSON.parse(raw);
|
|
84
|
+
if (!Array.isArray(parsed)) throw new Error('gh api PR files returned an invalid file list');
|
|
85
|
+
pages.push(parsed);
|
|
86
|
+
if (parsed.length < RELEASE_FILE_PAGE) break;
|
|
87
|
+
}
|
|
88
|
+
return releasePrFilePaths(pages);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const QUEUE_SEATS = ['OP', 'TC', 'MK', 'UX'] as const;
|
|
92
|
+
|
|
93
|
+
/** 원 PR 의 자리 라벨(elanous:seat-<자리> 또는 자리 이름 라벨). 없거나 둘 이상이면 MK. */
|
|
94
|
+
function seatOf(labels: readonly { name?: string }[] | undefined): QueueSeat {
|
|
95
|
+
const found = new Set<QueueSeat>();
|
|
96
|
+
for (const label of labels ?? []) {
|
|
97
|
+
const name = label.name ?? '';
|
|
98
|
+
const prefixed = /^elanous:seat-(OP|TC|MK|UX)$/.exec(name)?.[1];
|
|
99
|
+
const bare = (QUEUE_SEATS as readonly string[]).includes(name) ? name : undefined;
|
|
100
|
+
const seat = prefixed ?? bare;
|
|
101
|
+
if (seat) found.add(seat as QueueSeat);
|
|
102
|
+
}
|
|
103
|
+
return found.size === 1 ? [...found][0]! : 'MK';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function helperSettings(config: UserConfig | undefined): { mode: 'shadow' | 'live'; perDay: number } {
|
|
107
|
+
const helper = config?.harness?.helper;
|
|
108
|
+
return {
|
|
109
|
+
mode: helper?.repair === 'live' ? 'live' : 'shadow',
|
|
110
|
+
perDay: typeof helper?.repairPerDay === 'number' && Number.isSafeInteger(helper.repairPerDay) && helper.repairPerDay >= 0
|
|
111
|
+
? helper.repairPerDay : 3,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 그림자(기본): 수리 골을 원장에만 남긴다. live: 같은 함수로 대기열에 넣고 원 PR 을 superseded 로 표시한다. 같은 PR 은 평생 한 번. */
|
|
116
|
+
export async function recordRepairShadows(rows: readonly ScannedStoppedPr[], deps: {
|
|
117
|
+
runGh: RunGh; root: string; now?: Date;
|
|
118
|
+
config?: UserConfig;
|
|
119
|
+
/** 대기열 추가. 생략하면 addHarnessQueue — 실제 발사는 기존 대기열 틱이 한다. */
|
|
120
|
+
enqueue?: (input: { seat: QueueSeat; say: string; idempotencyKey: string }) => Promise<Pick<QueueItem, 'id'>>;
|
|
121
|
+
}): Promise<RepairShadowRow[]> {
|
|
67
122
|
const ledger = join(deps.root, 'helper', 'repairs.jsonl');
|
|
68
|
-
const
|
|
123
|
+
const prior: RepairLedgerLine[] = [];
|
|
69
124
|
if (existsSync(ledger)) for (const line of readFileSync(ledger, 'utf8').split('\n')) {
|
|
70
125
|
if (!line.trim()) continue;
|
|
71
|
-
try {
|
|
126
|
+
try { prior.push(JSON.parse(line) as RepairLedgerLine); } catch { /* malformed line is skipped */ }
|
|
127
|
+
}
|
|
128
|
+
const { mode, perDay } = helperSettings(deps.config ?? getUserConfig());
|
|
129
|
+
const day = (deps.now ?? new Date()).toISOString().slice(0, 10);
|
|
130
|
+
// Terminal results end a PR for good. A shadow line blocks only while still in shadow (live must still launch it),
|
|
131
|
+
// and a cap or failed launch blocks only for the same UTC day so the PR is retried tomorrow (ACP must-fix).
|
|
132
|
+
// Ledger lines written before results existed have no `result`; they were shadow records.
|
|
133
|
+
for (const line of prior) if (!line.result) line.result = 'shadow';
|
|
134
|
+
const seen = new Set(prior.filter((line) => line.result === 'launched' || line.result === 'needs-human'
|
|
135
|
+
|| (line.result === 'shadow' && mode === 'shadow')
|
|
136
|
+
|| ((line.result === 'cap' || line.result === 'launch-failed') && line.at.slice(0, 10) === day)).map((line) => line.pr));
|
|
137
|
+
// Distinct PRs: a follow-up repair line for an already launched PR is not a second launch.
|
|
138
|
+
let launchedToday = new Set(prior.filter((line) => line.result === 'launched' && line.at.slice(0, 10) === day).map((line) => line.pr)).size;
|
|
139
|
+
// A launched line whose label/comment failed is repaired here — the follow-up only, never a second enqueue.
|
|
140
|
+
if (mode === 'live') {
|
|
141
|
+
const latest = new Map<number, RepairLedgerLine>();
|
|
142
|
+
for (const line of prior) latest.set(line.pr, line);
|
|
143
|
+
for (const line of latest.values()) {
|
|
144
|
+
if (line.result !== 'launched' || !line.reason?.startsWith('후속 실패') || !line.queueId) continue;
|
|
145
|
+
const followups: string[] = [];
|
|
146
|
+
if (line.reason.includes('label ·')) {
|
|
147
|
+
try { await deps.runGh(['pr', 'edit', String(line.pr), '--add-label', 'elanous:superseded']); }
|
|
148
|
+
catch (error) { followups.push(`label · ${error instanceof Error ? error.message : String(error)}`); }
|
|
149
|
+
}
|
|
150
|
+
if (line.reason.includes('comment ·')) {
|
|
151
|
+
try { await deps.runGh(['pr', 'comment', String(line.pr), '--body', `수리 대기열 항목 ${line.queueId} 로 대체(런은 대기열 틱에서 시작)`]); }
|
|
152
|
+
catch (error) { followups.push(`comment · ${error instanceof Error ? error.message : String(error)}`); }
|
|
153
|
+
}
|
|
154
|
+
const repaired: RepairLedgerLine = { ...line, at: (deps.now ?? new Date()).toISOString(), ...(followups.length ? { reason: `후속 실패 · ${followups.join(' · ')}` } : { reason: '후속 복구' }) };
|
|
155
|
+
appendFileSync(ledger, JSON.stringify(repaired) + '\n');
|
|
156
|
+
debug.log('harness.helper', 'repair-followup-retried', { pr: line.pr, queueId: line.queueId, remaining: followups.length });
|
|
157
|
+
}
|
|
72
158
|
}
|
|
159
|
+
const enqueue = deps.enqueue ?? (async (input) => {
|
|
160
|
+
const { addHarnessQueue } = await import('./harness-queue.js');
|
|
161
|
+
return addHarnessQueue(input, { root: deps.root });
|
|
162
|
+
});
|
|
73
163
|
const written: RepairShadowRow[] = [];
|
|
74
164
|
for (const row of rows) {
|
|
75
165
|
if ((row.category !== 'review-budget' && row.category !== 'review-oscillation') || seen.has(row.pr)) continue;
|
|
76
166
|
const view = JSON.parse(await deps.runGh(['pr', 'view', String(row.pr), '--json', 'body'])) as { body: string };
|
|
167
|
+
// 자리 라벨은 본문 조회와 분리한다 — 기존 본문 조회 계약(--json body)을 그대로 둔다. 못 읽으면 MK.
|
|
168
|
+
let labels: { name?: string }[] | undefined;
|
|
169
|
+
if (mode === 'live') {
|
|
170
|
+
try { labels = (JSON.parse(await deps.runGh(['pr', 'view', String(row.pr), '--json', 'labels'])) as { labels?: { name?: string }[] }).labels; }
|
|
171
|
+
catch { labels = undefined; }
|
|
172
|
+
}
|
|
77
173
|
const goal = authorRepairGoal({ pr: row.pr, category: row.category, ...repairInputsFromBody(view.body) });
|
|
78
174
|
if (!goal) continue;
|
|
79
|
-
const
|
|
175
|
+
const at = (deps.now ?? new Date()).toISOString();
|
|
176
|
+
const base = { pr: row.pr, category: row.category, goalHash: createHash('sha256').update(goal).digest('hex').slice(0, 16), at, goal };
|
|
177
|
+
let record: RepairLedgerLine = { ...base, mode: 'shadow', result: 'shadow' };
|
|
178
|
+
if (mode === 'live') {
|
|
179
|
+
let files: string[] | undefined;
|
|
180
|
+
try { files = await changedFiles(row.pr, deps.runGh); }
|
|
181
|
+
catch (error) { files = undefined; debug.log('harness.helper', 'repair-files-unreadable', { pr: row.pr, reason: String(error) }); }
|
|
182
|
+
const release = files && releasePathHold(files);
|
|
183
|
+
// 상한은 «발사한 수»의 상한이다. 사람 승인이 필요한 PR 은 그 수를 쓰지 않으므로 상한보다 먼저 가른다.
|
|
184
|
+
if (files && !release && launchedToday >= perDay) {
|
|
185
|
+
record = { ...base, mode: 'live', result: 'cap', reason: '상한' };
|
|
186
|
+
} else if (!files) {
|
|
187
|
+
// An unreadable file list is transient: hold today, retry on a later day (never a permanent needs-human) (ACP must-fix).
|
|
188
|
+
record = { ...base, mode: 'live', result: 'launch-failed', reason: '변경 파일 조회 실패 · 다음 날 재시도' };
|
|
189
|
+
} else if (release) {
|
|
190
|
+
record = { ...base, mode: 'live', result: 'needs-human', reason: '사람 승인 필요' };
|
|
191
|
+
} else {
|
|
192
|
+
let item: Pick<QueueItem, 'id'> | undefined;
|
|
193
|
+
try {
|
|
194
|
+
item = await enqueue({ seat: seatOf(labels), say: goal, idempotencyKey: `helper-repair-${row.pr}` });
|
|
195
|
+
// The queue item exists from here on: a later label/comment failure is recorded on the launched line,
|
|
196
|
+
// never as launch-failed (which would let the next scan enqueue a second repair) (ACP must-fix).
|
|
197
|
+
const followups: string[] = [];
|
|
198
|
+
try { await deps.runGh(['pr', 'edit', String(row.pr), '--add-label', 'elanous:superseded']); }
|
|
199
|
+
catch (error) { followups.push(`label · ${error instanceof Error ? error.message : String(error)}`); }
|
|
200
|
+
try { await deps.runGh(['pr', 'comment', String(row.pr), '--body', `수리 대기열 항목 ${item.id} 로 대체(런은 대기열 틱에서 시작)`]); }
|
|
201
|
+
catch (error) { followups.push(`comment · ${error instanceof Error ? error.message : String(error)}`); }
|
|
202
|
+
record = { ...base, mode: 'live', result: 'launched', queueId: item.id, ...(followups.length ? { reason: `후속 실패 · ${followups.join(' · ')}` } : {}) };
|
|
203
|
+
launchedToday += 1;
|
|
204
|
+
debug.log('harness.helper', 'repair-launched', { pr: row.pr, queueId: item.id, mode: 'live', followupFailures: followups.length });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
record = { ...base, mode: 'live', result: 'launch-failed', reason: `발사 실패 · ${error instanceof Error ? error.message : String(error)}` };
|
|
207
|
+
debug.log('harness.helper', 'repair-launch-failed', { pr: row.pr, reason: record.reason });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
80
211
|
mkdirSync(dirname(ledger), { recursive: true });
|
|
81
|
-
appendFileSync(ledger, JSON.stringify(
|
|
82
|
-
debug.log('harness.helper', 'repair-authored', { pr: row.pr, category: row.category, mode: 'shadow' });
|
|
212
|
+
appendFileSync(ledger, JSON.stringify(record) + '\n');
|
|
213
|
+
if (record.result === 'shadow') debug.log('harness.helper', 'repair-authored', { pr: row.pr, category: row.category, mode: 'shadow' });
|
|
83
214
|
seen.add(row.pr);
|
|
84
|
-
written.push(
|
|
215
|
+
written.push({ pr: base.pr, category: base.category, goalHash: base.goalHash, at: base.at });
|
|
85
216
|
}
|
|
86
217
|
return written;
|
|
87
218
|
}
|
|
@@ -139,8 +139,19 @@ if (import.meta.main) {
|
|
|
139
139
|
// 명시적 그림자 기록에서만 추가 PR 조회와 헬퍼 원장 쓰기를 수행한다(발사 0).
|
|
140
140
|
const { recordRepairShadows } = await import('./helper-repair.js');
|
|
141
141
|
const { effectiveInstanceRoot } = await import('../instance/resolve.js');
|
|
142
|
-
const
|
|
143
|
-
|
|
142
|
+
const { getUserConfig } = await import('../user-config.js');
|
|
143
|
+
const root = effectiveInstanceRoot();
|
|
144
|
+
const config = getUserConfig();
|
|
145
|
+
const live = config.harness?.helper?.repair === 'live';
|
|
146
|
+
const shadows = await recordRepairShadows(rows, {
|
|
147
|
+
runGh, root, config,
|
|
148
|
+
// live 만 대기열에 넣는다. 실제 발사는 기존 대기열 틱이 한다(새 발사 경로 0).
|
|
149
|
+
...(live ? { enqueue: async (input) => {
|
|
150
|
+
const { addHarnessQueue } = await import('./harness-queue.js');
|
|
151
|
+
return addHarnessQueue(input, { root });
|
|
152
|
+
} } : {}),
|
|
153
|
+
});
|
|
154
|
+
if (shadows.length) console.log(live ? `수리 골 ${shadows.length}건 → helper/repairs.jsonl` : `수리 골 그림자 ${shadows.length}건 → helper/repairs.jsonl`);
|
|
144
155
|
}
|
|
145
156
|
if (json) console.log(JSON.stringify(rows, null, 2));
|
|
146
157
|
else for (const row of rows) {
|