elanous 0.2.16 → 0.2.17
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-b46d45b705e8c9ae.js → 1307-aac7332576c4d6fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-3143b07a817d1da9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-17ba086ef82d339b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.js → page-6e5c3a867557298b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/{page-1e962b31d1f11081.js → page-7110f732f45bf944.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-8c2d9526164f5d09.js → page-9a41d65355fc25b9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-c53598cfce6156f2.js → page-4187666132fc98c4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-09328d499162bae4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-29ff3a8f84f03b6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.js → page-80ec7401bc83f511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-76270efaee638f17.js → page-ab93b240f687d69a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-6107ca8ddd5e3bdd.js → page-fc7d86467291ae38.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-dce686e582c23937.js → layout-de43a186e5ba5683.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/{page-d3d53b146416e422.js → page-8b08cafad27759c9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-043a9111e74f6b24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.js → not-found-23a4601ceb7f452e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-ca3952cd78a83ca2.js → page-10d958605a77b3bd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-5b042736aa113756.js → page-166745e51ac0c7b0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-acb749f1e432bcf3.js → page-a04260b8ccc968d4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-12916918988ac97e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-31fa1ce4c4aff9ae.js → page-5a64cb5208c6a8fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-0f5cd57ca928f42b.js → page-1407ec83ea64c1f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-abe8e3b74bea1ffc.js → page-1d824510df025d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-d28573d6792b4275.js → page-7f49fe645d5132f7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-59f03fda76c03d22.js → page-81a4e7f8dbfe74cb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-d751de5f8062ff5e.js → page-27d71f131acd9320.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-424659bacfb8cdd7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-d4aef67abf264874.js → page-fc2df261f8a2c7df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-1ea64acadd704ace.js → page-09461366af82ef13.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-3544f154c87b43ee.js → page-5d4fef9ccd7d406b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-30c2073bfc21c004.js → page-ea90a8ac73ce2749.js} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/package.json +1 -1
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/release-loop/docs-land-node.ts +69 -10
- package/scripts/release-loop/publish-node.ts +2 -2
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/version-node.ts +7 -4
- package/scripts/release-story/draft.ts +42 -14
- package/src/agent-mission/driver.ts +53 -10
- package/src/autopilot/track-agent.ts +5 -3
- package/src/cli/autopilot-cli.ts +66 -0
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/pr-cli.ts +12 -1
- package/src/cli/provider-cli.ts +14 -0
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +200 -1
- package/src/context-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- package/src/harness/harness-cli-command.ts +142 -10
- package/src/harness/harness-pod-dispatch.ts +11 -0
- package/src/harness/harness-queue.ts +86 -7
- package/src/harness/nested-elanous-policy.ts +23 -4
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +8 -0
- package/src/index.ts +57 -8
- package/src/intake-plane/digest.ts +39 -4
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/oauth/codex.ts +82 -2
- package/src/release-loop/landing-freeze.ts +18 -10
- package/src/seat-loop/seat-loop.ts +95 -31
- package/src/self-dev/ask-launch-flow.ts +26 -7
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +7 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +88 -12
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/orchestrator.ts +19 -1
- package/src/task-agent/actions.ts +118 -0
- package/src/task-agent/judge.ts +96 -0
- package/src/task-cards/card-store.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +151 -29
- package/src/telegram-commands.ts +1 -1
- package/src/telegram-loops-command.ts +21 -14
- package/src/user-config.ts +25 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-a0e2a6d1bfb52fc5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-fe1759f94003b32b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-9afdee66b820c76e.js +0 -1
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TASK-AGENT 판단부 — 슈퍼바이저 종료 어휘(+ PR·리뷰 관측) → «다음 한 수».
|
|
3
|
+
*
|
|
4
|
+
* RFC-task-agent-any-task-to-completion-2026-10-06 §A2 표를 순수 함수로 옮긴다.
|
|
5
|
+
* 부작용이 없다(발사·리뷰·착지는 실행부 `actions.ts` 의 몫). 수확 원천 = run-c9ad6d6d `chooseTaskAction`.
|
|
6
|
+
*/
|
|
7
|
+
import type { SupervisorStopReason } from '../self-dev/run-supervisor.js';
|
|
8
|
+
import type { NextActionKind } from './actions.js';
|
|
9
|
+
|
|
10
|
+
export type TaskMove =
|
|
11
|
+
| 'propose-green'
|
|
12
|
+
| 'review'
|
|
13
|
+
| 'propose-land'
|
|
14
|
+
| 'narrow-relaunch'
|
|
15
|
+
| 'wait-retry'
|
|
16
|
+
| 'salvage-relaunch'
|
|
17
|
+
| 'alternative'
|
|
18
|
+
| 'decision-card'
|
|
19
|
+
| 'wait';
|
|
20
|
+
|
|
21
|
+
export interface TaskJudgeInput {
|
|
22
|
+
stopReason?: SupervisorStopReason;
|
|
23
|
+
prState?: 'OPEN' | 'MERGED' | 'CLOSED';
|
|
24
|
+
pr?: string | number;
|
|
25
|
+
/** `self review --json` 판정. 관측 못 했으면 비운다(«통과»로 읽지 않는다). */
|
|
26
|
+
review?: 'pass' | 'fail';
|
|
27
|
+
mustFix?: readonly string[];
|
|
28
|
+
/** 수마다 누적 실패 수. */
|
|
29
|
+
failures?: Partial<Record<TaskMove, number>>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TaskJudgement {
|
|
33
|
+
move: TaskMove;
|
|
34
|
+
stage: 'observing' | 'closing' | 'reviewing' | 'repairing' | 'waiting' | 'escalated';
|
|
35
|
+
reason: string;
|
|
36
|
+
/** 실행부(`executeNextAction`)가 바로 받는 수면 그 종류. 없으면 발사·대기 계열(실행부 밖). */
|
|
37
|
+
executorKind?: NextActionKind;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 같은 수가 이 횟수만큼 실패하면 다른 수로 넘어간다(RFC §A2 «같은 수 2번 실패»). */
|
|
41
|
+
export const SAME_MOVE_FAILURE_LIMIT = 2;
|
|
42
|
+
|
|
43
|
+
const EXECUTOR_KIND: Partial<Record<TaskMove, NextActionKind>> = {
|
|
44
|
+
'propose-green': 'green',
|
|
45
|
+
review: 'review',
|
|
46
|
+
'propose-land': 'land',
|
|
47
|
+
'narrow-relaunch': 'retry',
|
|
48
|
+
'wait-retry': 'retry',
|
|
49
|
+
'salvage-relaunch': 'retry',
|
|
50
|
+
alternative: 'retry',
|
|
51
|
+
'decision-card': 'decision',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function base(input: TaskJudgeInput): Omit<TaskJudgement, 'executorKind'> {
|
|
55
|
+
const stop = input.stopReason;
|
|
56
|
+
const merged = input.prState === 'MERGED' && input.pr !== undefined && input.pr !== '';
|
|
57
|
+
if (merged) {
|
|
58
|
+
return { move: 'propose-green', stage: 'closing', reason: `병합된 PR ${input.pr} 근거로 칸 green 제안` };
|
|
59
|
+
}
|
|
60
|
+
switch (stop) {
|
|
61
|
+
case 'harvestable-awaiting-human':
|
|
62
|
+
case 'needs-human':
|
|
63
|
+
if (input.review === 'pass') return { move: 'propose-land', stage: 'reviewing', reason: 'self review 통과 — 착지 제안' };
|
|
64
|
+
if (input.review === 'fail') {
|
|
65
|
+
return { move: 'narrow-relaunch', stage: 'repairing', reason: `must-fix 로 좁혀 재발사: ${(input.mustFix ?? []).join(' · ') || '(must-fix 비어 있음)'}` };
|
|
66
|
+
}
|
|
67
|
+
return { move: 'review', stage: 'reviewing', reason: '수확 가능 런 — self review 필요' };
|
|
68
|
+
case 'no-progress':
|
|
69
|
+
case 'decomposable-no-progress':
|
|
70
|
+
return { move: 'narrow-relaunch', stage: 'repairing', reason: `${stop} — 범위를 좁혀 한 번 재발사(판정선 유지)` };
|
|
71
|
+
case 'provider-exhausted':
|
|
72
|
+
case 'step-timeout':
|
|
73
|
+
return { move: 'wait-retry', stage: 'waiting', reason: `${stop} — 대기 후 재시도` };
|
|
74
|
+
case 'handed-off-to-salvage':
|
|
75
|
+
return { move: 'salvage-relaunch', stage: 'repairing', reason: '수확 브랜치 diff 를 가져와 이어 발사' };
|
|
76
|
+
case 'converged':
|
|
77
|
+
return { move: 'wait', stage: 'observing', reason: '완주했으나 PR 병합 근거 대기' };
|
|
78
|
+
case undefined:
|
|
79
|
+
return { move: 'wait', stage: 'observing', reason: '종료 관측 대기' };
|
|
80
|
+
default:
|
|
81
|
+
return { move: 'decision-card', stage: 'escalated', reason: `표에 없는 종료 어휘 ${stop} — 근거 붙여 사람 결정` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 종료 어휘·관측에서 다음 한 수를 고른다. 같은 수 2회 실패 → 다른 수 → 그것도 실패 → 결정 카드. */
|
|
86
|
+
export function judgeNextMove(input: TaskJudgeInput): TaskJudgement {
|
|
87
|
+
const failures = input.failures ?? {};
|
|
88
|
+
let judged = base(input);
|
|
89
|
+
if ((failures.alternative ?? 0) >= 1) {
|
|
90
|
+
judged = { move: 'decision-card', stage: 'escalated', reason: `대안도 실패 — 런·PR·must-fix 근거로 사람 결정 (직전: ${judged.move})` };
|
|
91
|
+
} else if (judged.move !== 'wait' && judged.move !== 'decision-card' && (failures[judged.move] ?? 0) >= SAME_MOVE_FAILURE_LIMIT) {
|
|
92
|
+
judged = { move: 'alternative', stage: 'repairing', reason: `같은 수 ${SAME_MOVE_FAILURE_LIMIT}회 실패 — 다른 수 선택 (${judged.move})` };
|
|
93
|
+
}
|
|
94
|
+
const executorKind = EXECUTOR_KIND[judged.move];
|
|
95
|
+
return executorKind ? { ...judged, executorKind } : judged;
|
|
96
|
+
}
|
|
@@ -16,6 +16,7 @@ export interface TaskCard {
|
|
|
16
16
|
goalId: string;
|
|
17
17
|
title: string;
|
|
18
18
|
status: CardStatus;
|
|
19
|
+
closedReason?: string;
|
|
19
20
|
createdAt: string;
|
|
20
21
|
sections: CardSection[];
|
|
21
22
|
}
|
|
@@ -206,7 +207,7 @@ export class CardStore {
|
|
|
206
207
|
if (card.status === 'closed') return card;
|
|
207
208
|
this.append(id, { type: 'closed', createdAt: new Date().toISOString(),
|
|
208
209
|
...(reason === undefined ? {} : { reason: redactSecrets(reason.trim()) }) });
|
|
209
|
-
return { ...card, status: 'closed' as const };
|
|
210
|
+
return { ...card, status: 'closed' as const, ...(reason === undefined ? {} : { closedReason: redactSecrets(reason.trim()) }) };
|
|
210
211
|
}).immediate();
|
|
211
212
|
}
|
|
212
213
|
|
|
@@ -216,8 +217,10 @@ export class CardStore {
|
|
|
216
217
|
} | null;
|
|
217
218
|
if (!row) return null;
|
|
218
219
|
const events = readFileSync(cardEventsPath(id, this.root), 'utf8').trimEnd().split('\n').map((line) => JSON.parse(line) as CardEvent);
|
|
220
|
+
const closed = events.find((event): event is Extract<CardEvent, { type: 'closed' }> => event.type === 'closed');
|
|
219
221
|
return {
|
|
220
222
|
id: row.id, goalId: row.goal_id, title: row.title, status: row.status, createdAt: row.created_at,
|
|
223
|
+
...(closed?.reason === undefined ? {} : { closedReason: closed.reason }),
|
|
221
224
|
sections: events.filter((event): event is Extract<CardEvent, { type: 'section' }> => event.type === 'section')
|
|
222
225
|
.map(({ key, owner, content, createdAt }) => ({ key, owner, content, createdAt })),
|
|
223
226
|
};
|
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
/** Executor kinds of the dev child's `--json` result line (`src/index.ts` writeStdoutJson · DevExecutor.kind). */
|
|
2
|
+
const CHILD_RESULT_KINDS = new Set(['self', 'external']);
|
|
3
|
+
|
|
4
|
+
/** The dev child's terminal line nests the stage: {ok, kind, result:{stage, ok, node, outcome, …}}. Other JSON rows are not results. */
|
|
5
|
+
export function nestedPodResult(row: Record<string, unknown>): { stage: string; ok: boolean; error?: string } | null {
|
|
6
|
+
if (typeof row.kind !== 'string' || !CHILD_RESULT_KINDS.has(row.kind) || typeof row.ok !== 'boolean') return null;
|
|
7
|
+
if (!row.result || typeof row.result !== 'object' || Array.isArray(row.result)) return null;
|
|
8
|
+
const result = row.result as Record<string, unknown>;
|
|
9
|
+
if (typeof result.stage !== 'string' || typeof result.ok !== 'boolean') return null;
|
|
10
|
+
if (result.ok) return { stage: result.stage, ok: true };
|
|
11
|
+
const classification = result.abandonedClassification;
|
|
12
|
+
const detail = typeof result.supervisorVerdict === 'string' ? result.supervisorVerdict
|
|
13
|
+
: classification && typeof classification === 'object' && !Array.isArray(classification)
|
|
14
|
+
? (classification as Record<string, unknown>).classification : undefined;
|
|
15
|
+
const reason = [result.node, result.outcome].filter((part): part is string => typeof part === 'string' && part.length > 0).join(' ');
|
|
16
|
+
const error = [reason || (typeof result.error === 'string' ? result.error : ''), typeof detail === 'string' && detail ? detail : ''].filter(Boolean).join(' · ');
|
|
17
|
+
return { stage: result.stage, ok: false, ...(error ? { error } : {}) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A child terminal row: the legacy flat form {stage, ok, error?} (no kind, or a child kind) or the nested dev result. Any other kind is not a result. */
|
|
21
|
+
export function podTerminalRow(row: Record<string, unknown>): { stage: string; ok: boolean; error?: string } | null {
|
|
22
|
+
if (typeof row.kind === 'string' && !CHILD_RESULT_KINDS.has(row.kind)) return null;
|
|
23
|
+
if (typeof row.stage === 'string' && typeof row.ok === 'boolean') {
|
|
24
|
+
return { stage: row.stage, ok: row.ok, ...(typeof row.error === 'string' ? { error: row.error } : {}) };
|
|
25
|
+
}
|
|
26
|
+
return nestedPodResult(row);
|
|
27
|
+
}
|
|
28
|
+
|
|
1
29
|
/** Extract a human-readable reason from a failed child Pod without treating transfer markers as child errors. */
|
|
2
30
|
export function extractPodFailureReason(input: {
|
|
3
31
|
logs: string;
|
|
@@ -12,7 +40,7 @@ export function extractPodFailureReason(input: {
|
|
|
12
40
|
if (input.containerReason === 'OOMKilled') return 'OOMKilled: 컨테이너 메모리 한도 초과';
|
|
13
41
|
|
|
14
42
|
const isBookkeeping = (line: string): boolean =>
|
|
15
|
-
/^ELANOUS_(?:MEM|RUN_LEDGER|USAGE_ROLLUP|POD_)/u.test(line) || /^(?:at\s+\S|\.\.\.\s+\d+\s+more\b|cleanup\s+(?:complete|done|finished)\b)/iu.test(line);
|
|
43
|
+
/^(?:ELANOUS_(?:MEM|RUN_LEDGER|USAGE_ROLLUP|POD_)|\[graph\] )/u.test(line) || /^(?:at\s+\S|\.\.\.\s+\d+\s+more\b|cleanup\s+(?:complete|done|finished)\b)/iu.test(line);
|
|
16
44
|
const isError = (line: string): boolean =>
|
|
17
45
|
/\berror\b|\bfail(?:ed|ure)?\b|exception|fatal|denied|timed?\s*out|exceeded|overloaded|unavailable|exhausted|오류|실패/iu.test(line);
|
|
18
46
|
const meaningful: Array<{ text: string; terminal: boolean }> = [];
|
|
@@ -24,10 +52,11 @@ export function extractPodFailureReason(input: {
|
|
|
24
52
|
try { parsed = JSON.parse(trimmed); } catch { /* An ordinary line may begin with a brace. */ }
|
|
25
53
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
26
54
|
const row = parsed as Record<string, unknown>;
|
|
27
|
-
|
|
55
|
+
const terminal = podTerminalRow(row);
|
|
56
|
+
if (terminal) {
|
|
28
57
|
meaningful.length = 0;
|
|
29
|
-
if (
|
|
30
|
-
const errorLines =
|
|
58
|
+
if (terminal.ok === false && typeof terminal.error === 'string') {
|
|
59
|
+
const errorLines = terminal.error.split(/\r\n|\n|\r/u).map((part) => part.trim())
|
|
31
60
|
.filter((part) => part && !isBookkeeping(part));
|
|
32
61
|
const errorLine = [...errorLines].reverse().find(isError) ?? errorLines.at(-1);
|
|
33
62
|
if (errorLine) meaningful.push({ text: errorLine, terminal: true });
|
|
@@ -64,6 +64,9 @@ export interface PoolLeaseRecommendation {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/** Kubernetes quantity to bytes (memory) or millicores (CPU). */
|
|
67
|
+
/** Kubernetes memory quantity (`16Gi`, `2Gi`, `512Mi`) → bytes; null when unparseable. */
|
|
68
|
+
export function memoryQuantityBytes(raw: string): number | null { return quantity(raw, 'memory'); }
|
|
69
|
+
|
|
67
70
|
function quantity(raw: string, unit: 'memory' | 'cpu'): number | null {
|
|
68
71
|
const m = /^(\d+(?:\.\d+)?)(n|u|m|k|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$/.exec(raw);
|
|
69
72
|
if (!m) return null;
|
|
@@ -235,7 +238,7 @@ function runPoolDnsProbe(context: string, kubectl: PoolKubectl): PoolDnsProbe {
|
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
/** Each cluster reads resource reservations; DNS requires a bounded disposable Pod. */
|
|
238
|
-
export function measurePoolLease(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe } = {}): PoolLeaseMeasure {
|
|
241
|
+
export function measurePoolLease(members: readonly PodPoolMember[], deps: { kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; /** Read-only snapshots must not create the disposable DNS Pod or update its cache. */ skipDnsProbe?: boolean } = {}): PoolLeaseMeasure {
|
|
239
242
|
const kubectl = deps.kubectl ?? leaseKubectl;
|
|
240
243
|
return { members: members.map((member) => {
|
|
241
244
|
const base = ['--context', member.context, '--request-timeout=10s'];
|
|
@@ -354,7 +357,7 @@ export function measurePoolLease(members: readonly PodPoolMember[], deps: { kube
|
|
|
354
357
|
else if (!reservationsValid && !unknownPhase) reasons.push('cluster reservations: node assignment or memory request/limit missing/invalid');
|
|
355
358
|
}
|
|
356
359
|
result.reason = reasons.length ? reasons.join('; ').slice(0, 240) : null;
|
|
357
|
-
if (nodeItems?.length && jobItems && podItems) {
|
|
360
|
+
if (!deps.skipDnsProbe && nodeItems?.length && jobItems && podItems) {
|
|
358
361
|
let dnsState: PoolDnsProbe = 'unknown';
|
|
359
362
|
try { dnsState = (deps.dns ?? ((context) => probePoolDns(context, kubectl)))(member.context); } catch { /* probe could not be measured */ }
|
|
360
363
|
if (dnsState === 'dns') { result.capacity = 0; result.availableMemoryByNodeBytes = []; result.reason = 'dns'; }
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* 풀을 안 주면 종전처럼 «현재 컨텍스트» 하나다(동작 불변).
|
|
15
15
|
*/
|
|
16
16
|
import { spawn, spawnSync } from 'node:child_process';
|
|
17
|
-
import { existsSync } from 'node:fs';
|
|
17
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
18
19
|
import { join, resolve } from 'node:path';
|
|
19
20
|
import { debug } from '../../debug/log.js';
|
|
20
21
|
import { PodLeaseAdmission, type PodLeasePredecessor, type PodLeaseRelease } from '../../pod-lease/admission.js';
|
|
@@ -186,7 +187,8 @@ export class PodPoolScheduler {
|
|
|
186
187
|
private readonly hostLease: HostPoolLease;
|
|
187
188
|
private readonly pollMs: number;
|
|
188
189
|
private readonly occupancy: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>;
|
|
189
|
-
|
|
190
|
+
private readonly memberMemory: () => Array<{ context: string; allocatableMemoryBytes: number | null }> | Promise<Array<{ context: string; allocatableMemoryBytes: number | null }>>;
|
|
191
|
+
constructor(readonly members: readonly PodPoolMember[], options: { status?: () => PoolLeaseRecommendation | Promise<PoolLeaseRecommendation>; kubectl?: PoolKubectl; dns?: (context: string) => PoolDnsProbe; pollMs?: number; hostLease?: HostPoolLease; memberMemory?: () => Array<{ context: string; allocatableMemoryBytes: number | null }> | Promise<Array<{ context: string; allocatableMemoryBytes: number | null }>>; occupancy?: () => Record<string, MemberOccupancy> | Promise<Record<string, MemberOccupancy>>; dependencyMerged?: (after: PodLeasePredecessor, signal?: AbortSignal) => PredecessorState | boolean | Promise<PredecessorState | boolean> } = {}) {
|
|
190
192
|
this.pollMs = options.pollMs ?? 15_000;
|
|
191
193
|
// One lease directory per pool (contexts, not caps) — every CLI process launching into the same clusters shares it.
|
|
192
194
|
this.hostLease = options.hostLease ?? podPoolHostLease(members);
|
|
@@ -195,6 +197,9 @@ export class PodPoolScheduler {
|
|
|
195
197
|
return recommendConcurrency(measure, { capacity: members.reduce((n, m) => n + m.capacity, 0), accounts: 0, perAccount: 0 });
|
|
196
198
|
});
|
|
197
199
|
this.measure = async () => measureStatus();
|
|
200
|
+
// An injected status (tests, callers with their own measurement) without memberMemory never shells out to kubectl here.
|
|
201
|
+
this.memberMemory = options.memberMemory ?? (options.status ? (() => []) : () => measurePoolLease(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) }).members
|
|
202
|
+
.map((m: PodLeaseMember) => ({ context: m.context, allocatableMemoryBytes: m.allocatableMemoryBytes })));
|
|
198
203
|
this.occupancy = options.occupancy ?? (() => measureMemberOccupancy(members, { ...(options.kubectl ? { kubectl: options.kubectl } : {}), ...(options.dns ? { dns: options.dns } : {}) }));
|
|
199
204
|
this.admission = new PodLeaseAdmission({ status: async () => {
|
|
200
205
|
const measured = await measureStatus();
|
|
@@ -267,6 +272,23 @@ export class PodPoolScheduler {
|
|
|
267
272
|
};
|
|
268
273
|
});
|
|
269
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* LAUNCH-STALL (10-06): members whose total allocatable memory is below one Job's limit can never take it —
|
|
277
|
+
* waiting for them is an endless silent stall (minio 12.5GB vs a 16Gi standard Job). A member whose memory
|
|
278
|
+
* could not be read is not judged unfit (unknown ≠ too small).
|
|
279
|
+
*/
|
|
280
|
+
async unfitMembers(limitBytes: number): Promise<Array<{ context: string; allocatableMemoryBytes: number }>> {
|
|
281
|
+
let rows: Array<{ context: string; allocatableMemoryBytes: number | null }>;
|
|
282
|
+
try { rows = await this.memberMemory(); } catch { return []; }
|
|
283
|
+
return rows.flatMap((r) => r.allocatableMemoryBytes !== null && r.allocatableMemoryBytes < limitBytes
|
|
284
|
+
? [{ context: r.context, allocatableMemoryBytes: r.allocatableMemoryBytes }] : []);
|
|
285
|
+
}
|
|
286
|
+
/** Why admission is still waiting: the pool-wide lease measurement (null before any reading). Slot waits get their reason from tryAcquire's onEmpty. */
|
|
287
|
+
waitReason(): string | null {
|
|
288
|
+
const r = this.lastRaw;
|
|
289
|
+
if (!r) return null;
|
|
290
|
+
return `recommended=${r.recommended ?? 'unknown'} limitedBy=${r.limitedBy ?? '-'} memorySlots=${r.memorySlots ?? '?'}${r.reason ? ` reason=${r.reason}` : ''}`;
|
|
291
|
+
}
|
|
270
292
|
admissionSnapshot(): ReturnType<PodLeaseAdmission['snapshot']> {
|
|
271
293
|
return this.admission.snapshot();
|
|
272
294
|
}
|
|
@@ -277,7 +299,7 @@ export class PodPoolScheduler {
|
|
|
277
299
|
* A member whose occupancy could not be read falls back to inflight-only (never treated as zero occupied).
|
|
278
300
|
* Ties keep the configured member order. Returns null when every readable member is full.
|
|
279
301
|
*/
|
|
280
|
-
async tryAcquire(): Promise<PodPoolMember | null> {
|
|
302
|
+
async tryAcquire(skip?: ReadonlySet<string>, onEmpty?: (reason: string) => void): Promise<PodPoolMember | null> {
|
|
281
303
|
let reading: Record<string, MemberOccupancy>;
|
|
282
304
|
try { reading = await this.occupancy(); }
|
|
283
305
|
catch (error) {
|
|
@@ -293,6 +315,8 @@ export class PodPoolScheduler {
|
|
|
293
315
|
|| (cand.measured !== best.measured ? cand.measured
|
|
294
316
|
: cand.slots / cand.member.capacity > best.slots / best.member.capacity);
|
|
295
317
|
for (const m of this.members) {
|
|
318
|
+
// LAUNCH-STALL: a member this launch cannot fit is skipped for this call only (other launches may fit it).
|
|
319
|
+
if (skip?.has(m.context)) continue;
|
|
296
320
|
const inflight = this.inflight.get(m.context) ?? 0;
|
|
297
321
|
const row = reading[m.context];
|
|
298
322
|
const ok = !!row && row.occupied !== null;
|
|
@@ -304,7 +328,10 @@ export class PodPoolScheduler {
|
|
|
304
328
|
const cand = { member: m, slots, measured: ok };
|
|
305
329
|
if (better(cand)) best = cand;
|
|
306
330
|
}
|
|
307
|
-
if (!best)
|
|
331
|
+
if (!best) {
|
|
332
|
+
onEmpty?.(`no free slot · free=${JSON.stringify(free)} · measured=${JSON.stringify(measured)}${skip?.size ? ` · skipped=${[...skip].join(',')}` : ''}`);
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
308
335
|
this.inflight.set(best.member.context, (this.inflight.get(best.member.context) ?? 0) + 1);
|
|
309
336
|
const share = Object.fromEntries(this.members.map((m) => [m.context, Math.round(Math.max(0, free[m.context] ?? 0) / m.capacity * 100) / 100]));
|
|
310
337
|
debug.log('pod.pool', 'member-selected', { member: best.member.context, free, measured, share });
|
|
@@ -396,13 +423,14 @@ export function podImageBuildScript(cwd: string = process.cwd()): string | null
|
|
|
396
423
|
return null;
|
|
397
424
|
}
|
|
398
425
|
|
|
399
|
-
|
|
426
|
+
/** packedTgz = 풀 동기화가 한 번 만든 패키지 묶음(있으면 build.sh 가 pack 을 다시 하지 않는다). */
|
|
427
|
+
type RemoteBuild = (host: string, cluster: string, registry?: string, packedTgz?: string) => Promise<{ ok: boolean; detail: string }>;
|
|
400
428
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
429
|
+
/** 같은 트리의 패키지 묶음(tgz)을 «한 번» 만든다 — 멤버들이 같이 쓴다. cleanup 은 동기화가 끝나면 부른다. */
|
|
430
|
+
type PoolPack = () => Promise<{ ok: boolean; tgz?: string; detail: string; cleanup?: () => void }>;
|
|
431
|
+
|
|
432
|
+
function runBuildScript(script: string, env: NodeJS.ProcessEnv): Promise<{ ok: boolean; detail: string }> {
|
|
433
|
+
return new Promise((done) => {
|
|
406
434
|
const child = spawn('bash', [script], { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
407
435
|
let out = '';
|
|
408
436
|
child.stdout.on('data', (d) => { out += d; }); child.stderr.on('data', (d) => { out += d; });
|
|
@@ -411,18 +439,53 @@ function defaultRemoteBuild(script: string, image: string): RemoteBuild {
|
|
|
411
439
|
});
|
|
412
440
|
}
|
|
413
441
|
|
|
442
|
+
/**
|
|
443
|
+
* 🩸 2026-10-06: 멤버마다 build.sh 가 같은 트리에서 `bun pm pack` 을 «동시에» 돌렸다 — prepack 이 트리에 쓰는
|
|
444
|
+
* src/version/packed-revision.json 을 다른 쪽 postpack 이 지워 한쪽이 «⛔ pack» 으로 죽었다(node-c 가 매 발사 빠졌다).
|
|
445
|
+
* ⇒ 원격 멤버가 둘 이상이면 pack 은 여기서 한 번만 하고 그 tgz 를 나눠 준다.
|
|
446
|
+
*/
|
|
447
|
+
function defaultPoolPack(script: string): PoolPack {
|
|
448
|
+
return async () => {
|
|
449
|
+
const dir = mkdtempSync(join(tmpdir(), 'elanous-pool-pack-'));
|
|
450
|
+
const tgz = join(dir, 'elanous.tgz');
|
|
451
|
+
const env: NodeJS.ProcessEnv = { ...process.env, ELANOUS_BUILD_PACK_ONLY: tgz };
|
|
452
|
+
delete env.ELANOUS_BUILD_PACKED_TGZ;
|
|
453
|
+
delete env.ELANOUS_BUILD_REMOTE;
|
|
454
|
+
const r = await runBuildScript(script, env);
|
|
455
|
+
const cleanup = () => rmSync(dir, { recursive: true, force: true });
|
|
456
|
+
if (!r.ok || !existsSync(tgz)) { cleanup(); return { ok: false, detail: r.detail }; }
|
|
457
|
+
return { ok: true, tgz, detail: r.detail, cleanup };
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function defaultRemoteBuild(script: string, image: string): RemoteBuild {
|
|
462
|
+
return (host, cluster, registry, packedTgz) => {
|
|
463
|
+
const env: NodeJS.ProcessEnv = { ...process.env, ELANOUS_BUILD_REMOTE: host, ELANOUS_L2_CLUSTER: cluster, ELANOUS_BUILD_IMAGE: image === 'elanous-harness-lite:local' ? 'lite' : 'full' };
|
|
464
|
+
delete env.ELANOUS_BUILD_REGISTRY;
|
|
465
|
+
if (registry) env.ELANOUS_BUILD_REGISTRY = registry;
|
|
466
|
+
delete env.ELANOUS_BUILD_PACK_ONLY;
|
|
467
|
+
delete env.ELANOUS_BUILD_PACKED_TGZ;
|
|
468
|
+
if (packedTgz) env.ELANOUS_BUILD_PACKED_TGZ = packedTgz;
|
|
469
|
+
return runBuildScript(script, env);
|
|
470
|
+
};
|
|
471
|
+
}
|
|
414
472
|
/**
|
|
415
473
|
* 풀의 원격 노드들을 «동시에» 이 트리의 판으로 맞춘다 (09-25 개선).
|
|
416
474
|
* 1순위 = 노드 «쪽에서» 빌드(build.sh ELANOUS_BUILD_REMOTE) — 빌드 재료(수십 MB)만 보내고 무거운 층은 그 노드의 캐시가 재사용한다.
|
|
417
475
|
* 📏 커밋이 바뀐 뒤 두 노드를 올리는 데 벽시계 55초(종전: 다시 굽기 1분 40초 ⊕ 4GB 차례 전송 5분 48초).
|
|
418
476
|
* 실패하면 = 종전의 통째 전송(syncPoolImage)으로 떨어진다.
|
|
419
477
|
*/
|
|
420
|
-
export async function syncPoolImages(members: readonly PodPoolMember[], image: string, localCommit: string | null, deps: { run?: RemoteRun; remoteBuild?: RemoteBuild; buildScript?: string | null; ship?: typeof syncPoolImage; localSkillsDigest?: string | null; waitIntervalMs?: number; waitMaxMs?: number; now?: () => number; sleep?: (ms: number) => Promise<void>; inspect?: LocalImageInspect; transfer?: ImageShip } = {}): Promise<Map<string, PoolImageSync>> {
|
|
478
|
+
export async function syncPoolImages(members: readonly PodPoolMember[], image: string, localCommit: string | null, deps: { run?: RemoteRun; remoteBuild?: RemoteBuild; buildScript?: string | null; ship?: typeof syncPoolImage; localSkillsDigest?: string | null; waitIntervalMs?: number; waitMaxMs?: number; now?: () => number; sleep?: (ms: number) => Promise<void>; inspect?: LocalImageInspect; transfer?: ImageShip; pack?: PoolPack | null } = {}): Promise<Map<string, PoolImageSync>> {
|
|
421
479
|
const run = deps.run ?? defaultRemoteRun;
|
|
422
480
|
const script = deps.buildScript === undefined ? podImageBuildScript() : deps.buildScript;
|
|
423
481
|
const remoteBuild = deps.remoteBuild ?? (script ? defaultRemoteBuild(script, image) : null);
|
|
424
482
|
const ship = deps.ship ?? ((m: PodPoolMember, img: string, commit: string | null, remote: RemoteRun) => syncPoolImage(m, img, commit, { run: remote, inspect: deps.inspect, transfer: deps.transfer }));
|
|
425
483
|
const results = new Map<string, PoolImageSync>();
|
|
484
|
+
// pack 한 번 ⊕ 나눠 쓰기 — 원격 멤버가 둘 이상일 때만(하나뿐이면 종전대로 build.sh 가 스스로 pack 한다).
|
|
485
|
+
// 주입된 remoteBuild(시험)에는 진짜 pack 을 붙이지 않는다 — pack 도 주입해야 쓴다.
|
|
486
|
+
const pack = deps.pack !== undefined ? deps.pack : (!deps.remoteBuild && script ? defaultPoolPack(script) : null);
|
|
487
|
+
const sharePack = pack && members.filter((m) => m.sshHost).length >= 2 ? pack : null;
|
|
488
|
+
let packed: ReturnType<PoolPack> | null = null;
|
|
426
489
|
await Promise.all(members.map(async (m) => {
|
|
427
490
|
const t0 = Date.now();
|
|
428
491
|
if (!m.sshHost) { results.set(m.context, { ok: true, action: 'local', detail: 'this machine', ms: 0 }); return; }
|
|
@@ -437,7 +500,12 @@ export async function syncPoolImages(members: readonly PodPoolMember[], image: s
|
|
|
437
500
|
return;
|
|
438
501
|
}
|
|
439
502
|
if (remoteBuild) {
|
|
440
|
-
|
|
503
|
+
// 같은 판이 아닌 멤버가 처음 여기 닿을 때 pack 한다(전부 fresh 면 pack 도 없다).
|
|
504
|
+
const shared = sharePack ? await (packed ??= sharePack()) : null;
|
|
505
|
+
if (shared && !shared.ok) debug.log('self-implement.pod', 'pool-image-pack-failed', { context: m.context, detail: shared.detail });
|
|
506
|
+
const b = shared && !shared.ok
|
|
507
|
+
? { ok: false, detail: shared.detail }
|
|
508
|
+
: await remoteBuild(m.sshHost, m.k3dCluster, m.registry, shared?.tgz);
|
|
441
509
|
const after = remoteImageCommit(m, image, run);
|
|
442
510
|
const refAfter = registryImageRef(m, localCommit ?? after, run, image);
|
|
443
511
|
// ⭐ 레지스트리 노드면 «이 판 커밋 태그가 레지스트리에 있나»로 판정한다 — Pod 는 그 커밋 태그를 pull 한다.
|
|
@@ -462,6 +530,6 @@ export async function syncPoolImages(members: readonly PodPoolMember[], image: s
|
|
|
462
530
|
return;
|
|
463
531
|
}
|
|
464
532
|
results.set(m.context, { ...ship(m, image, localCommit, run), ms: Date.now() - t0 });
|
|
465
|
-
}));
|
|
533
|
+
})).finally(async () => { if (packed) (await packed).cleanup?.(); });
|
|
466
534
|
return results;
|
|
467
535
|
}
|