elanous 0.2.7 → 0.2.8
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/4836-3084c58c1725a40b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7032-f4ef698518fba61c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8067-d976b441a104ab74.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8519-5632377cce994f86.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8894-7317998f6cb97d8c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9740-8f5ff96c522b5f55.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-9aa47b0bc6d570ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f4d2ecbbae6088d6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-2053e49930c82748.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-624f7a2f89b60c8f.js → page-d4b6df905f185055.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-5a5fb385a4df0ffe.js → page-a70ba27c33003dd0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-186fc6952f027acc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-7457f52683562161.js → page-18166f885d831d3e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1c9d6cfe68b121c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-cbbe03c0da7b081a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c108ff1fdb665c97.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-b7dcf5d48a854b76.js → page-147a328753d983e2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-51480757e7d68b92.js → page-799395a9df7d95aa.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-6a8158b71d1fe9d7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-9567560270857531.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-30533117beb53625.js → page-e7ed3d010153d818.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-c0c0d19d51342d3e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-07267f1391b96aba.js → page-2e002ad8faa43291.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-55d50e9b0006a91d.js → page-b672e02e4bec8deb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-1ba4a5d857276334.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-9db2226f38c542fb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-a7e37af7d0399cb8.js → page-70ee65f5f7a151dc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0bb8e43203efb23b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-092e1bcf27d751cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-15e5a15db84c56aa.js → page-eb52580c4b75b905.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-a2e572e4511f2a16.js → page-7464c1987dd76e6d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-dd107bf1c7d52a90.js → page-e573437b0a1c728f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-294300625e49e4d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-085847b088675978.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-9fe9d38c36999eb4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d8ab4d1489223459.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-6141f67f531d8b6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-295a02b1026b8255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1eb9a0e1e054209d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-56adbe1a1f1379bd.js → page-82efa177a30f3741.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d0be51cec4f62606.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-2e367075ffdbc062.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d422f38c052967e.js → page-647ed2b66c1d49f1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ba058c56e38f2792.js +1 -0
- package/apps/pwa/out/_next/static/css/5ef72a618a8994f3.css +1 -0
- 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/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/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/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/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/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/test-diet/recipes.yaml +9 -0
- package/graphs/test-diet/test-diet.yaml +22 -0
- package/package.json +2 -1
- package/persona-presets/_index.yaml +10 -0
- package/persona-presets/_schema.json +146 -0
- package/persona-presets/dana.yaml +26 -0
- package/persona-presets/haru.yaml +27 -0
- package/persona-presets/kai.yaml +27 -0
- package/persona-presets/leo.yaml +27 -0
- package/persona-presets/mira.yaml +30 -0
- package/persona-presets/nari.yaml +27 -0
- package/persona-presets/sora.yaml +27 -0
- package/persona-presets/yuna.yaml +27 -0
- package/plugins/geo-check/graphs/run-step.ts +34 -10
- package/scripts/field-feed/deliver-node.ts +8 -3
- package/scripts/field-feed/lib.ts +2 -0
- package/scripts/install.sh +20 -11
- package/scripts/persona-presets/lint.ts +79 -0
- package/scripts/release-loop/checklist-gate-node.ts +2 -2
- package/scripts/release-loop/gate-node.ts +27 -3
- package/scripts/release-loop/npm-publish-node.ts +10 -4
- package/scripts/release-loop/ops-upgrade-node.ts +11 -3
- package/scripts/release-loop/publish-node.ts +23 -1
- package/scripts/release-loop/shard-plan.ts +17 -0
- package/scripts/storyboard/storyboard.ts +167 -0
- package/scripts/test-diet/lib.ts +88 -0
- package/scripts/test-diet/node.ts +102 -0
- package/src/acp/auth-methods.ts +30 -0
- package/src/acp/server.ts +5 -5
- package/src/channel-import/detect.ts +191 -0
- package/src/channel-import/run.ts +111 -0
- package/src/cli/config-test-sync.ts +4 -0
- package/src/cli/decisions-cli.ts +13 -3
- package/src/cli/doctor-bundle.ts +128 -0
- package/src/cli/doctor-cli.ts +36 -1
- package/src/cli/doctor-local-llm.ts +95 -0
- package/src/cli/import-cli.ts +60 -0
- package/src/cli/mcp-login.ts +13 -4
- package/src/cli/persona-cli.ts +116 -0
- package/src/cli/release-cli.ts +8 -7
- package/src/cli/skills-cli.ts +173 -0
- package/src/cli/start-cli.ts +13 -4
- package/src/codex/setup.ts +15 -13
- package/src/dashboard/index.ts +17 -1
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -2
- package/src/dashboard/slash-runtime/help-from-registry.ts +4 -2
- package/src/decisions/decision-cards.ts +291 -0
- package/src/decisions/decision-ledger.ts +5 -2
- package/src/decisions/discord-decision-cards.ts +133 -0
- package/src/decisions/telegram-decision-cards.ts +100 -0
- package/src/directives/version-at.ts +77 -0
- package/src/discord.ts +17 -0
- package/src/domains/outbound-alert.ts +44 -1
- package/src/domains/repeated-failure.ts +1 -1
- package/src/error-report/report.ts +200 -0
- package/src/exec-requests/planner.ts +8 -2
- package/src/exec-requests/runner.ts +64 -24
- package/src/exec-requests/store.ts +2 -0
- package/src/field/field-feed-auto.ts +48 -0
- package/src/field/field-reel.ts +8 -1
- package/src/hooks/error-report.ts +210 -0
- package/src/hooks/receiver.ts +41 -1
- package/src/import/apply.ts +128 -0
- package/src/import/plan.ts +197 -0
- package/src/index.ts +82 -7
- package/src/input-core/text-entry.ts +13 -5
- package/src/intake-plane/seat-answer.ts +96 -0
- package/src/intake-plane/seat-doc-route.ts +17 -3
- package/src/nexus/api/consult-requests.ts +19 -8
- package/src/nexus/api/error-reports.ts +108 -0
- package/src/nexus/api/http-server.ts +21 -1
- package/src/nexus/api/outbound-report.ts +7 -0
- package/src/nexus/api/running-runs-background.ts +68 -0
- package/src/nexus/api/running-runs-child.ts +6 -0
- package/src/nexus/api/share-captures.ts +91 -0
- package/src/nexus/api/skill-problems.ts +56 -0
- package/src/nexus/api/terminals.ts +24 -8
- package/src/nexus/index.ts +25 -1
- package/src/nexus/setup-status.ts +7 -2
- package/src/oauth/browser-availability.ts +14 -0
- package/src/oauth/codex-quota-policy.ts +16 -0
- package/src/onboarding/continue-here.ts +13 -8
- package/src/onboarding/web-first.ts +8 -1
- package/src/onboarding.ts +66 -12
- package/src/persona/presets.ts +105 -0
- package/src/release-loop/checklist.ts +28 -5
- package/src/self-dev/orchestrate.ts +18 -11
- package/src/shell-env-bootstrap.ts +44 -7
- package/src/skills/connect-claude-plugin.ts +142 -0
- package/src/skills/connect.ts +70 -0
- package/src/skills/index.ts +29 -1
- package/src/skills/repair.ts +124 -0
- package/src/skills/runner.ts +11 -2
- package/src/skills/trigger-extract.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-command-job.ts +16 -8
- package/src/telegram-commands.ts +13 -0
- package/src/telegram.ts +34 -0
- package/src/user-config.ts +111 -13
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/4836-2eba6079f31b88d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8067-f74bf14d67ad98f1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8519-f6b5d8a5833a7603.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8894-e92491a164192cc1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9740-67051141115ce775.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9804-64c02c3c127d5014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d5ebde04844689e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-10ed24f413ed66e9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-211810b6c93ab66a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-80497e93a878ddc5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-cee86394de407aa6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-4ff8edab44196b8c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-92e4f44428d5e290.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-2a05dfe80c8bdf16.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-d9979633aa82f8e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-f7857826c2f97ad7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-9699b1786be6171d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-43dd772aee8af75f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-973ab7af59c718b7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-a9f8d3865b91e436.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-d42b99cdfb9f27c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-2764ff40dad5a995.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-c5f1152db116ff96.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-f933bca81ea38a6f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-c1c95739c0bff3b8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-a7aaffcf771662d8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-71d18eec33b9c667.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-2c4e59079ee357ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a81f113f3ef89d72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-fc27b00e20733146.js +0 -1
- package/apps/pwa/out/_next/static/css/f537bdb19c69306a.css +0 -1
- /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → lywc8m_HDzH9MKSDQhyEj}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{keajW7DDyghEs1y8oJe6T → lywc8m_HDzH9MKSDQhyEj}/_ssgManifest.js +0 -0
|
@@ -18,6 +18,8 @@ export type FeedDraft = {
|
|
|
18
18
|
hashtags: string[];
|
|
19
19
|
location: string | null;
|
|
20
20
|
reel: string | null;
|
|
21
|
+
/** deliver 가 남긴다 — 알림 경로(daemon·direct·failed·off)와 시각. 도착의 근거는 아니고 «어디로 보냈나»다. */
|
|
22
|
+
delivered?: { at: string; path: 'daemon' | 'direct' | 'failed' | 'off'; revision: number };
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
export const ENGINE = resolve(import.meta.dir, '../../skills/explainer-video/engine');
|
package/scripts/install.sh
CHANGED
|
@@ -177,7 +177,7 @@ for command in "${REQUIRED_COMMANDS[@]}"; do
|
|
|
177
177
|
fi
|
|
178
178
|
done
|
|
179
179
|
for command in "${HARNESS_COMMANDS[@]}"; do
|
|
180
|
-
command -v "$command" >/dev/null 2>&1 || echo "⚠️ harness command missing: $command" >&2
|
|
180
|
+
command -v "$command" >/dev/null 2>&1 || echo "$(t "⚠️ harness command missing: $command (see Next below)" "⚠️ 하니스가 쓰는 명령이 없습니다: $command (아래 «다음» 참고)")" >&2
|
|
181
181
|
done
|
|
182
182
|
|
|
183
183
|
# bun 의 «실제 실행 파일» — PATH 의 이름이 아니라 bun 이 스스로 말하는 경로(링크를 끝까지 푼 것).
|
|
@@ -444,7 +444,7 @@ STEP=1
|
|
|
444
444
|
# INST1 order: new shell → elanous (first-time setup, which also signs in) → missing tools → harness say.
|
|
445
445
|
case ":$PATH:" in
|
|
446
446
|
*":$PREFIX/bin:"*) ;;
|
|
447
|
-
*) if [ "$MODIFY_PATH" -eq 1 ]; then echo " $STEP) $(t "open a new shell (or: source $STARTUP)" "새 셸을 여세요(또는: source $STARTUP)")"; else echo " $STEP) $(t "add $PREFIX/bin to PATH" "$PREFIX/bin 을 PATH 에 넣으세요")";
|
|
447
|
+
*) if [ "$START_SETUP" -eq 1 ]; then :; elif [ "$MODIFY_PATH" -eq 1 ]; then echo " $STEP) $(t "open a new shell (or: source $STARTUP)" "새 셸을 여세요(또는: source $STARTUP)")"; STEP=$((STEP + 1)); else echo " $STEP) $(t "add $PREFIX/bin to PATH" "$PREFIX/bin 을 PATH 에 넣으세요")"; STEP=$((STEP + 1)); fi ;;
|
|
448
448
|
esac
|
|
449
449
|
if [ "$START_SETUP" -eq 0 ]; then
|
|
450
450
|
echo " $STEP) $(t "Run: $PREFIX/bin/elanous # first-time setup (in a new shell: elanous)" "실행: $PREFIX/bin/elanous # 첫 설정(새 셸에서는: elanous)")"; STEP=$((STEP + 1))
|
|
@@ -473,7 +473,7 @@ if ! command -v make >/dev/null 2>&1 || ! command -v c++ >/dev/null 2>&1; then
|
|
|
473
473
|
if [ -n "$BUILD_HINT" ]; then
|
|
474
474
|
echo " $STEP) $BUILD_HINT"; STEP=$((STEP + 1))
|
|
475
475
|
fi
|
|
476
|
-
echo " $STEP) elanous doctor --fix --yes # rebuild node-pty after installing build tools"; STEP=$((STEP + 1))
|
|
476
|
+
echo " $STEP) elanous doctor --fix --yes # $(t 'rebuild node-pty after installing build tools' '빌드 도구를 깐 뒤 node-pty 다시 빌드')"; STEP=$((STEP + 1))
|
|
477
477
|
fi
|
|
478
478
|
if ! command -v rg >/dev/null 2>&1; then
|
|
479
479
|
if [ -z "${OS_ID:-}" ] && [ -r "${ELANOUS_INSTALL_OS_RELEASE_FILE:-/etc/os-release}" ]; then
|
|
@@ -487,22 +487,22 @@ if ! command -v rg >/dev/null 2>&1; then
|
|
|
487
487
|
Linux) case " ${OS_ID:-} ${OS_LIKE:-} " in
|
|
488
488
|
*' debian '*|*' ubuntu '*) RG_HINT='sudo apt-get install -y ripgrep' ;;
|
|
489
489
|
*' fedora '*) RG_HINT='sudo dnf install -y ripgrep' ;;
|
|
490
|
-
*) RG_HINT='install ripgrep (rg) with your package manager' ;;
|
|
490
|
+
*) RG_HINT="$(t 'install ripgrep (rg) with your package manager' '패키지 관리자로 ripgrep(rg)을 설치하세요')" ;;
|
|
491
491
|
esac ;;
|
|
492
|
-
*) RG_HINT='install ripgrep (rg) with your package manager' ;;
|
|
492
|
+
*) RG_HINT="$(t 'install ripgrep (rg) with your package manager' '패키지 관리자로 ripgrep(rg)을 설치하세요')" ;;
|
|
493
493
|
esac
|
|
494
494
|
echo " $STEP) $RG_HINT"; STEP=$((STEP + 1))
|
|
495
495
|
fi
|
|
496
496
|
if ! command -v codex >/dev/null 2>&1; then
|
|
497
497
|
if ! command -v node >/dev/null 2>&1; then
|
|
498
|
-
echo " $STEP) install Node.js 20+ (the Codex CLI runs on node) # e.g. your package manager or https://nodejs.org"; STEP=$((STEP + 1))
|
|
498
|
+
echo " $STEP) $(t 'install Node.js 20+ (the Codex CLI runs on node) # e.g. your package manager or https://nodejs.org' 'Node.js 20 이상을 설치하세요(Codex CLI 가 node 로 돕니다) # 예: 패키지 관리자 또는 https://nodejs.org')"; STEP=$((STEP + 1))
|
|
499
499
|
fi
|
|
500
|
-
echo " $STEP) npm install -g @openai/codex # the harness drives the Codex CLI
|
|
500
|
+
echo " $STEP) npm install -g @openai/codex # $(t 'the harness drives the Codex CLI' '하니스가 Codex CLI 를 씁니다')"; STEP=$((STEP + 1))
|
|
501
501
|
fi
|
|
502
502
|
if ! command -v gh >/dev/null 2>&1; then
|
|
503
|
-
echo " $STEP) install gh, then: gh auth login # the harness opens pull requests with it"; STEP=$((STEP + 1))
|
|
503
|
+
echo " $STEP) $(t 'install gh, then: gh auth login # the harness opens pull requests with it' 'gh 를 설치한 뒤: gh auth login # 하니스가 PR 을 열 때 씁니다')"; STEP=$((STEP + 1))
|
|
504
504
|
elif ! gh auth status >/dev/null 2>&1; then
|
|
505
|
-
echo " $STEP) gh auth login # the harness opens pull requests with it"; STEP=$((STEP + 1))
|
|
505
|
+
echo " $STEP) gh auth login # $(t 'the harness opens pull requests with it' '하니스가 PR 을 열 때 씁니다')"; STEP=$((STEP + 1))
|
|
506
506
|
fi
|
|
507
507
|
if [ "$ELN_AVAILABLE" -eq 1 ]; then
|
|
508
508
|
echo " $STEP) $(t 'eln harness say "<one line of what you want>" (or: elanous harness say)' 'eln harness say "<하고 싶은 일 한 줄>" (또는: elanous harness say)')"
|
|
@@ -519,8 +519,17 @@ if [ "$START_SETUP" -eq 1 ]; then
|
|
|
519
519
|
*":$PREFIX/bin:"*) ;;
|
|
520
520
|
*) echo " $(t "(new shells will find elanous on PATH; this window uses $PREFIX/bin/elanous)" "(새 셸에서는 PATH 로 elanous 를 찾습니다 · 이 창은 $PREFIX/bin/elanous 를 씁니다)")" ;;
|
|
521
521
|
esac
|
|
522
|
+
# INST3 (10-01 · node-c tmux): a wizard whose stdin is a fresh open of /dev/tty never saw a key on macOS — its event
|
|
523
|
+
# loop cannot watch that fd (the same binary started from a shell, which hands over the real tty, worked). So give it
|
|
524
|
+
# the real terminal device; fall back to /dev/tty only when there is none.
|
|
525
|
+
SETUP_TTY=/dev/tty
|
|
526
|
+
REAL_TTY="$(ps -o tty= -p $$ 2>/dev/null | tr -d ' ')"
|
|
527
|
+
case "$REAL_TTY" in
|
|
528
|
+
''|'?'|'??') ;;
|
|
529
|
+
*) if [ -r "/dev/$REAL_TTY" ] && [ -w "/dev/$REAL_TTY" ]; then SETUP_TTY="/dev/$REAL_TTY"; fi ;;
|
|
530
|
+
esac
|
|
522
531
|
if [ -n "${ELANOUS_INSTALL_SETUP_EXEC:-}" ]; then
|
|
523
|
-
exec "$ELANOUS_INSTALL_SETUP_EXEC" "$PREFIX/bin/elanous"
|
|
532
|
+
ELANOUS_INSTALL_SETUP_TTY="$SETUP_TTY" exec "$ELANOUS_INSTALL_SETUP_EXEC" "$PREFIX/bin/elanous"
|
|
524
533
|
fi
|
|
525
|
-
exec "$PREFIX/bin/elanous"
|
|
534
|
+
exec "$PREFIX/bin/elanous" <"$SETUP_TTY"
|
|
526
535
|
fi
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// 페르소나 프리셋 문면 검사(PS1 · 2026-10-02) — persona-presets/<id>.yaml
|
|
3
|
+
// bun scripts/persona-presets/lint.ts 오류가 있으면 rc 1 · 경고만이면 rc 0
|
|
4
|
+
// 지키는 것: 도구는 «지금 설치할 수 있는 것»만(공개 묶음 표 · 저장소 플러그인 · core) · 용어(«캐릭터» 금지) ·
|
|
5
|
+
// 자리(OP·MK·TC·UX)와 이름이 겹치지 않음 · 직함은 선택 속성 · 과장 금지어.
|
|
6
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { join, resolve } from 'node:path';
|
|
8
|
+
import { parse } from 'yaml';
|
|
9
|
+
|
|
10
|
+
const ROOT = resolve(import.meta.dir, '../..');
|
|
11
|
+
const DIR = join(ROOT, 'persona-presets');
|
|
12
|
+
|
|
13
|
+
export type Preset = {
|
|
14
|
+
personaId: string; displayName: string; names: string[]; role: string; title?: string | null; oneLine: string;
|
|
15
|
+
voice: { rule: string; examples: string[] }; tasks: Array<{ when: string; what: string }>;
|
|
16
|
+
tools: Array<{ name: string; from: string; optional?: boolean }>; firstQuestions: string[]; askBefore?: string[]; doesNot: string[]; forWhom: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const CORE = new Set(['harness']);
|
|
20
|
+
const TITLES = new Set(['COO', 'CMO', 'CTO', 'CXO']);
|
|
21
|
+
const SEATS = /^(OP|MK|TC|UX)$/i;
|
|
22
|
+
const BANNED = ['캐릭터', '완전 자율', '무엇이든', '유일', '알아서 다'];
|
|
23
|
+
|
|
24
|
+
/** Official packs and the skills each one lists, read from the public plugin table (the same page users read). */
|
|
25
|
+
export function officialPacks(root = ROOT): Map<string, string> {
|
|
26
|
+
const table = readFileSync(join(root, 'release/public/docs/plugins.md'), 'utf8');
|
|
27
|
+
const packs = new Map<string, string>();
|
|
28
|
+
for (const row of table.split('\n')) {
|
|
29
|
+
const m = row.match(/^\| `([a-z0-9-]+)` \| (.*) \| (.*) \|$/);
|
|
30
|
+
if (m && !/not published/i.test(m[3]!)) packs.set(m[1]!, m[2]!);
|
|
31
|
+
}
|
|
32
|
+
return packs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function lintPreset(p: Preset, file: string, packs: Map<string, string>, root = ROOT): { errors: string[]; warnings: string[] } {
|
|
36
|
+
const errors: string[] = []; const warnings: string[] = [];
|
|
37
|
+
const need = (ok: unknown, msg: string) => { if (!ok) errors.push(msg); };
|
|
38
|
+
need(/^[a-z][a-z0-9-]{1,30}$/.test(p.personaId ?? '') && file.endsWith(`/${p.personaId}.yaml`), 'personaId = 파일 이름(소문자)');
|
|
39
|
+
need(p.displayName && p.role && p.oneLine && p.forWhom, 'displayName · role · oneLine · forWhom 필요');
|
|
40
|
+
need(Array.isArray(p.names) && p.names.length >= 2, 'names 후보 둘 이상');
|
|
41
|
+
need(p.voice?.rule && p.voice.examples?.length === 2, 'voice = rule ⊕ 예시 두 줄');
|
|
42
|
+
need(p.tasks?.length === 3 && p.tasks.every((t) => t.when && t.what), 'tasks = 할 일 셋(when · what)');
|
|
43
|
+
need(p.firstQuestions?.length >= 2 && p.firstQuestions.length <= 3, 'firstQuestions = 처음 묻는 칸 둘~셋');
|
|
44
|
+
need(p.doesNot?.length >= 2, 'doesNot = 하지 않는 것 둘 이상');
|
|
45
|
+
if (p.title != null && !TITLES.has(p.title)) errors.push(`title 은 없음 또는 ${[...TITLES].join('·')}`);
|
|
46
|
+
for (const name of p.names ?? []) if (SEATS.test(name)) errors.push(`이름 «${name}» 이 자리(OP·MK·TC·UX)와 겹친다`);
|
|
47
|
+
const text = JSON.stringify(p);
|
|
48
|
+
for (const word of BANNED) if (text.includes(word)) errors.push(`금지어 «${word}»`);
|
|
49
|
+
for (const tool of p.tools ?? []) {
|
|
50
|
+
if (tool.from === 'core') { if (!CORE.has(tool.name)) errors.push(`core 도구 «${tool.name}» 는 없다`); continue; }
|
|
51
|
+
const row = packs.get(tool.from);
|
|
52
|
+
if (row !== undefined) {
|
|
53
|
+
if (tool.from !== tool.name && !row.includes(`\`${tool.name}\``)) errors.push(`«${tool.name}» 이 묶음 «${tool.from}» 의 목록에 없다`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// 공식 묶음에 없는 도구 = 저장소 플러그인일 때만 · 반드시 optional(공개 판 사용자는 따로 설치해야 쓴다)
|
|
57
|
+
if (tool.from !== tool.name || !existsSync(join(root, 'plugins', tool.name, 'plugin.json'))) errors.push(`«${tool.name}» 은 공식 묶음에도 plugins/${tool.name}/plugin.json 에도 없다`);
|
|
58
|
+
else if (tool.optional !== true) errors.push(`«${tool.name}» 은 공식 묶음에 없다 — optional: true 로 둔다`);
|
|
59
|
+
else warnings.push(`«${tool.name}» 은 선택 도구(공식 묶음 게재 전)`);
|
|
60
|
+
}
|
|
61
|
+
return { errors, warnings };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (import.meta.main) {
|
|
65
|
+
const packs = officialPacks();
|
|
66
|
+
const index = parse(readFileSync(join(DIR, '_index.yaml'), 'utf8')) as { presets: string[] };
|
|
67
|
+
const files = readdirSync(DIR).filter((f) => f.endsWith('.yaml') && !f.startsWith('_'));
|
|
68
|
+
let bad = 0; const names = new Map<string, string>();
|
|
69
|
+
for (const f of files) {
|
|
70
|
+
const p = parse(readFileSync(join(DIR, f), 'utf8')) as Preset;
|
|
71
|
+
const { errors, warnings } = lintPreset(p, `persona-presets/${f}`, packs);
|
|
72
|
+
if (!index.presets.includes(p.personaId)) errors.push('_index.yaml presets 에 없다');
|
|
73
|
+
for (const n of p.names ?? []) { if (names.has(n)) errors.push(`이름 «${n}» 이 ${names.get(n)} 와 겹친다`); names.set(n, p.personaId); }
|
|
74
|
+
for (const e of errors) console.log(`✗ ${f}: ${e}`);
|
|
75
|
+
for (const w of warnings) console.log(`⚠ ${f}: ${w}`);
|
|
76
|
+
if (errors.length) bad++; else console.log(`✓ ${f} · ${p.displayName} · 도구 ${p.tools.length}`);
|
|
77
|
+
}
|
|
78
|
+
process.exit(bad ? 1 : 0);
|
|
79
|
+
}
|
|
@@ -7,14 +7,14 @@ export function runChecklistGate(context: GraphContext = readGraphContext()) {
|
|
|
7
7
|
const data = listChecklist(version);
|
|
8
8
|
const gate = checklistGate(version);
|
|
9
9
|
const counts = summarizeChecklist(data);
|
|
10
|
-
const summary = `확인표 🟢${counts.green} 🟡${counts.yellow}(이동 ${gate.moved.length} · 알려진 문제 ${gate.knownIssues.length} · 막음 ${gate.blocked.length} · 판정 없음 ${gate.undecided.length}) 🔴${counts.red}${gate.red.length ? ` (${gate.red.join(', ')})` : ''}`;
|
|
10
|
+
const summary = `확인표 🟢${counts.green} 🟡${counts.yellow}(이동 ${gate.moved.length} · 알려진 문제 ${gate.knownIssues.length} · 막음 ${gate.blocked.length} · 판정 없음 ${gate.undecided.length}) 🔴${counts.red}${gate.red.length ? ` (${gate.red.join(', ')})` : ''}${gate.parity?.length ? ` · ⚠ 짝 경고 ${gate.parity.length}(${gate.parity.map((p) => p.id).join(', ')})` : ''}`;
|
|
11
11
|
if (gate.ok && gate.moved.length) {
|
|
12
12
|
const [major, minor, patch] = version.split('.').map(Number);
|
|
13
13
|
const next = `${major}.${minor}.${patch! + 1}`;
|
|
14
14
|
for (const id of gate.moved) {
|
|
15
15
|
const item = data.items.find((candidate) => candidate.id === id)!;
|
|
16
16
|
if (listChecklist(next).items.some((candidate) => candidate.id === id)) continue;
|
|
17
|
-
try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}) }); }
|
|
17
|
+
try { addItem(next, { id, title: item.title, ...(item.owner !== undefined ? { owner: item.owner } : {}), ...(item.kind !== undefined ? { kind: item.kind } : {}) }); }
|
|
18
18
|
catch (error) {
|
|
19
19
|
if (!listChecklist(next).items.some((candidate) => candidate.id === id)) throw error;
|
|
20
20
|
}
|
|
@@ -10,7 +10,7 @@ import { getElanousConfigDirOverride } from '../../src/elanous-config-dir.js';
|
|
|
10
10
|
import { diffFailures, junitFailures, parseFailures } from './gate-diff';
|
|
11
11
|
import { loadEnvKnownFailures, splitKnownEnv } from './env-known-failures';
|
|
12
12
|
import { baselineFromCutLogs } from './baseline-from-logs';
|
|
13
|
-
import { planShards, readFileDurations } from './shard-plan';
|
|
13
|
+
import { planShards, readFileDurations, readFileMemory } from './shard-plan';
|
|
14
14
|
import { deriveCdpTestPatterns } from '../test-deterministic';
|
|
15
15
|
import { emitNodeResult, readGraphContext } from './node-verdict.js';
|
|
16
16
|
import { runPodCommand, type RunPodCommandOptions, type PodCommandResult } from '../../src/task-orchestrator/surfaces/pod-command-job.js';
|
|
@@ -112,10 +112,18 @@ const summaryCount = (output: string, label: string) => Number(lastMatch(output,
|
|
|
112
112
|
/** bun reads a bare positional as a substring filter (`test` matches every *.test.ts); `./` makes it a path. */
|
|
113
113
|
const asPath = (path: string) => path.startsWith('./') || path.startsWith('/') ? path : `./${path}`;
|
|
114
114
|
export const POD_ISOLATION_MEMORY_LIMIT = '32Gi';
|
|
115
|
+
/** GT1b — a file whose measured peak is at least this runs alone at the isolation limit from the start: one such file
|
|
116
|
+
* sank a whole 203-file root shard at 16Gi (0.2.6 shard 12 · `unwired-exports` 9.9 GB) and cost two halving rounds. */
|
|
117
|
+
export const POD_HEAVY_FILE_MB = 4096;
|
|
118
|
+
/** Per-file peak memory, read from the cut tree (absent → no heavy files; the halving path still catches OOM). */
|
|
119
|
+
export const POD_MEMORY_SOURCE = 'docs/measurements/td1-whole-gate-mechanical-2026-10-01.tsv';
|
|
115
120
|
/** Integration tests that the Pod sweep does not run — each is judged by a real run elsewhere at the cut.
|
|
116
121
|
* 0.2.7 run 5 (10-01): `scripts/install.test.ts` (real `bun pm pack` → `bun add` installs, 301 s alone on node-b)
|
|
117
122
|
* sat in a shard of its own past the 1200 s deadline with no output and stopped the whole gate. Judged on the
|
|
118
123
|
* mbp at the cut instead (TD1 moves it to the integration line). Keep this list short and explained. */
|
|
124
|
+
/** GT1 — files per retry chunk when a root shard fails (0.2.7: bundles of 54+ files OOMed at 16Gi, smaller passed). */
|
|
125
|
+
export const POD_SHARD_FILE_CAP = 27;
|
|
126
|
+
|
|
119
127
|
export const POD_SWEEP_INTEGRATION_ONLY: readonly string[] = ['scripts/install.test.ts'];
|
|
120
128
|
const tail40 = (output: string) => output.trimEnd().split(/\r?\n/).slice(-40).join('\n');
|
|
121
129
|
const lastStartedTestFile = (output: string, paths: string[]): string | undefined => {
|
|
@@ -208,7 +216,14 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
208
216
|
if (integrationOnly.length) debug.log('release-loop.gate', 'pod-sweep-integration-only', { files: integrationOnly });
|
|
209
217
|
if (!assignable.length) throw new Error('sweep incomplete: no tests ran');
|
|
210
218
|
const durations = pod.durationSource ? readFileDurations(pod.durationSource) : new Map<string, number>();
|
|
211
|
-
const
|
|
219
|
+
const memory = readFileMemory(join(tree, POD_MEMORY_SOURCE));
|
|
220
|
+
const heavy = new Set(assignable.filter((file) => (memory.get(file) ?? 0) >= POD_HEAVY_FILE_MB));
|
|
221
|
+
const light = assignable.filter((file) => !heavy.has(file));
|
|
222
|
+
const shards = [
|
|
223
|
+
...(light.length ? planShards(light, durations, shardCount) : []),
|
|
224
|
+
...[...heavy].map((file) => ({ files: [file], plannedSeconds: durations.get(file) ?? 0 })),
|
|
225
|
+
];
|
|
226
|
+
if (heavy.size) debug.log('release-loop.gate', 'pod-heavy-alone', { files: [...heavy], thresholdMb: POD_HEAVY_FILE_MB, memoryLimit: POD_ISOLATION_MEMORY_LIMIT });
|
|
212
227
|
const known = assignable.filter((file) => durations.has(file)).length;
|
|
213
228
|
debug.log('release-loop.gate', 'pod-shard-plan', {
|
|
214
229
|
shards: shards.length, known, unknown: assignable.length - known,
|
|
@@ -259,7 +274,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
259
274
|
pool: pod.pool, poolScheduler, clone: true, source: { kind: 'commit', sha: commit }, deadlineSeconds,
|
|
260
275
|
...(bunCache ? { bunCache } : {}),
|
|
261
276
|
// 실패 뒤 다시 도는 파일 하나짜리 Job 은 메모리 한도를 올린다 — 16Gi 에선 무거운 한 파일이 혼자서도 OOM 이었다(09-30 `unwired-exports`).
|
|
262
|
-
...(paths.length === 1 && depth >= 1 ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
|
|
277
|
+
...(paths.length === 1 && (depth >= 1 || heavy.has(paths[0]!)) ? { memoryLimit: POD_ISOLATION_MEMORY_LIMIT } : {}),
|
|
263
278
|
name: `gate-${randomUUID()}`,
|
|
264
279
|
command: ['bash', '-lc', `mkdir -p "$HOME/outbox"; ${cachePrefix}(cd .. && cd repo && bun install && (cd apps/pwa && bun install) && ${args}) 2>&1 | tee "$HOME/outbox/shard.log"; echo \${PIPESTATUS[0]} > "$HOME/outbox/shard.rc"`],
|
|
265
280
|
});
|
|
@@ -350,6 +365,15 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
350
365
|
...(lastFile ? { lastFile } : {}),
|
|
351
366
|
...(reason === 'unattributed' ? { summaryFailures: fails, namedFailures, ...(unattributedDetail ? { detail: unattributedDetail } : {}) } : {}),
|
|
352
367
|
}] };
|
|
368
|
+
// GT1 — a big shard that failed at the root goes straight to cap-sized chunks instead of halving twice: memory
|
|
369
|
+
// piles up when one bun process runs many files (0.2.7: only bundles of 54+ OOMed), and each halving round costs
|
|
370
|
+
// a Pod start, an install and a rerun. A failing chunk then isolates its files (depth 2), as before.
|
|
371
|
+
if (depth === 0 && paths.length > POD_SHARD_FILE_CAP) {
|
|
372
|
+
const chunks = Array.from({ length: Math.ceil(paths.length / POD_SHARD_FILE_CAP) }, (_, i) => paths.slice(i * POD_SHARD_FILE_CAP, (i + 1) * POD_SHARD_FILE_CAP));
|
|
373
|
+
debug.log('release-loop.gate', 'pod-shard-cap-split', { shard, files: paths.length, chunks: chunks.length, cap: POD_SHARD_FILE_CAP, reason });
|
|
374
|
+
const children = await Promise.all(chunks.map((chunk, i) => runShard(chunk, shard, 2, `${branch}-c${i}`)));
|
|
375
|
+
return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
|
|
376
|
+
}
|
|
353
377
|
debug.log('release-loop.gate', 'pod-shard-split', { shard, depth, files: paths, reason });
|
|
354
378
|
const middle = Math.ceil(paths.length / 2);
|
|
355
379
|
const children = await Promise.all([
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { debug } from '../../src/debug/log.js';
|
|
7
7
|
import { sendOutbound } from '../../src/domains/outbound-alert.js';
|
|
8
|
-
import { effectiveInstanceRoot } from '../../src/instance/resolve.js';
|
|
8
|
+
import { effectiveInstanceRoot, prodInstanceRoot } from '../../src/instance/resolve.js';
|
|
9
9
|
import { emitNodeResult, readGraphContext, type GraphContext } from './node-verdict.js';
|
|
10
10
|
|
|
11
11
|
type Result = { outcome: 'ok' | 'fail'; verdict: 'pass' | 'fail'; summary: string; npm?: 'published' | 'already-published' | 'staged-not-visible'; humanAction?: 'npm-support' };
|
|
@@ -16,6 +16,8 @@ export interface NpmPublishDeps {
|
|
|
16
16
|
registry?: Registry;
|
|
17
17
|
sleep?: (ms: number) => Promise<void>;
|
|
18
18
|
stateDir?: string;
|
|
19
|
+
/** Test seam — the production root whose secrets/npm-token is the last fallback. */
|
|
20
|
+
productionRoot?: string;
|
|
19
21
|
alert?: (text: string, kind: string) => boolean;
|
|
20
22
|
log?: (version: string, waitedMinutes: number) => void;
|
|
21
23
|
}
|
|
@@ -48,7 +50,10 @@ export async function runNpmPublish(context: GraphContext = readGraphContext(),
|
|
|
48
50
|
try {
|
|
49
51
|
if (context.outputs.publish?.outcome !== 'ok') throw new Error('publish must succeed before npm publish');
|
|
50
52
|
const stateDir = deps.stateDir ?? effectiveInstanceRoot();
|
|
51
|
-
|
|
53
|
+
// REL3 — the archive prepare actually built (its output) wins over this process's universe: a resumed run can
|
|
54
|
+
// resolve another universe (10-01 0.2.7: prepared in the pilot test universe, resumed in production).
|
|
55
|
+
const prepared = context.outputs.prepare?.candidate;
|
|
56
|
+
const candidate = typeof prepared === 'string' && prepared.endsWith('.tgz') ? prepared : join(stateDir, 'release', version, 'prepared', 'dist', 'elanous.tgz');
|
|
52
57
|
const run = deps.exec ?? exec;
|
|
53
58
|
const readRegistry = deps.registry ?? registry;
|
|
54
59
|
const wait = deps.sleep ?? sleep;
|
|
@@ -71,7 +76,8 @@ export async function runNpmPublish(context: GraphContext = readGraphContext(),
|
|
|
71
76
|
const before = await inspect();
|
|
72
77
|
if (before.tagged) return { outcome: 'ok', verdict: 'pass', npm: 'already-published', summary: `npm ${version} already published with latest tag` };
|
|
73
78
|
if (!before.visible) {
|
|
74
|
-
|
|
79
|
+
// The npm token is a machine secret (like the GitHub App key): input → this universe → the production root.
|
|
80
|
+
const tokenFile = context.input.npmTokenFile ?? [join(stateDir, 'secrets', 'npm-token'), join(deps.productionRoot ?? prodInstanceRoot(), 'secrets', 'npm-token')].find((p) => existsSync(p)) ?? join(stateDir, 'secrets', 'npm-token');
|
|
75
81
|
if (typeof tokenFile !== 'string' || !tokenFile.trim()) throw new Error('input.npmTokenFile must be a path');
|
|
76
82
|
token = readFileSync(tokenFile, 'utf8').trim();
|
|
77
83
|
if (!token) throw new Error('npm token file empty');
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runCommand, type CommandResult, type CommandRunner } from './node-verdict.js';
|
|
4
4
|
|
|
5
|
-
interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string }
|
|
5
|
+
interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string; hooks?: string }
|
|
6
6
|
|
|
7
7
|
function firstLine(text: string): string { return text.trim().split(/\r?\n/)[0]?.trim() ?? ''; }
|
|
8
8
|
|
|
@@ -44,7 +44,8 @@ export function runOpsUpgrade(run: CommandRunner = runCommand) {
|
|
|
44
44
|
}
|
|
45
45
|
const command = (args: string[]): CommandResult => host === 'local'
|
|
46
46
|
? run('elanous', args)
|
|
47
|
-
|
|
47
|
+
// The installer's own bin dir comes first — a --no-modify-path install (the bot VM) has elanous only there.
|
|
48
|
+
: run('ssh', [host, `PATH="$HOME/.local/share/elanous/bin:$HOME/.local/bin:$HOME/.bun/bin:$PATH"; export PATH; elanous ${args.map(quote).join(' ')}`]);
|
|
48
49
|
let before = '';
|
|
49
50
|
let after = '';
|
|
50
51
|
try {
|
|
@@ -68,7 +69,14 @@ export function runOpsUpgrade(run: CommandRunner = runCommand) {
|
|
|
68
69
|
results.push({ host, ok: false, before, after, error: `올렸는데 판이 그대로 (기대 ${version}, 실제 ${after || '없음'})` });
|
|
69
70
|
continue;
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
+
// The webhook receiver (`elanous hooks serve`, a user systemd unit on the bot VM) keeps the old code until it
|
|
73
|
+
// restarts. try-restart touches it only where it runs; elsewhere the unit is absent and nothing happens.
|
|
74
|
+
let hooks: string | undefined;
|
|
75
|
+
if (host !== 'local' && context.input.opsRestart === true) {
|
|
76
|
+
const restarted = run('ssh', [host, 'systemctl --user try-restart elanous-hooks.service 2>/dev/null && systemctl --user is-active elanous-hooks.service 2>/dev/null || echo absent']);
|
|
77
|
+
hooks = firstLine(restarted.stdout) || 'absent';
|
|
78
|
+
}
|
|
79
|
+
results.push({ host, ok: true, before, after, ...(hooks ? { hooks } : {}) });
|
|
72
80
|
} catch (error) {
|
|
73
81
|
results.push({ host, ok: false, before, after, error: firstLine(String(error)) });
|
|
74
82
|
}
|
|
@@ -17,6 +17,22 @@ export function publicNotes(markdown: string, pages: { pages: Array<{ id: string
|
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Polls each release asset until it answers 200 (following redirects). Returns the names still missing. */
|
|
21
|
+
export function waitForAssets(run: CommandRunner, repo: string, tag: string, names: readonly string[], waitSeconds: unknown = 600,
|
|
22
|
+
sleep: (ms: number) => void = (ms) => Bun.sleepSync(ms)): string[] {
|
|
23
|
+
if (!repo || names.length === 0) return [];
|
|
24
|
+
const limit = typeof waitSeconds === 'number' && Number.isFinite(waitSeconds) && waitSeconds >= 0 ? Math.min(waitSeconds, 1800) : 600;
|
|
25
|
+
let pending = [...names];
|
|
26
|
+
for (let waited = 0; ; waited += 15) {
|
|
27
|
+
pending = pending.filter((name) => {
|
|
28
|
+
const probe = run('curl', ['-sIL', '-o', '/dev/null', '-w', '%{http_code}', '-m', '20', `https://github.com/${repo}/releases/download/${tag}/${name}`]);
|
|
29
|
+
return probe.stdout.trim() !== '200';
|
|
30
|
+
});
|
|
31
|
+
if (pending.length === 0 || waited >= limit) return pending;
|
|
32
|
+
sleep(15_000);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
export function runPublish(run: CommandRunner = runCommand) {
|
|
21
37
|
const context = readGraphContext();
|
|
22
38
|
const version = context.input.version;
|
|
@@ -50,7 +66,13 @@ export function runPublish(run: CommandRunner = runCommand) {
|
|
|
50
66
|
const result = lastResult(published);
|
|
51
67
|
if (published.status === 1) return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `release publish failed: ${result?.error ?? published.stderr.trim()}` };
|
|
52
68
|
if (published.status !== 0 || result?.ok !== true || result.published !== true || result.tag !== `v${version}`) throw new Error(`release publish incomplete or tag mismatch: ${result?.error ?? published.stderr.trim()}`);
|
|
53
|
-
|
|
69
|
+
// REL3 — «published» only once every asset downloads. 10-01: the run went on to verify before GitHub served the
|
|
70
|
+
// assets (release created 13:16Z, assets 13:20Z, run ended 13:15Z) and verify failed on a missing installer.
|
|
71
|
+
const repo = typeof result.publicRepo === 'string' ? result.publicRepo : '';
|
|
72
|
+
const names = Array.isArray(result.assets) ? (result.assets as unknown[]).filter((a): a is string => typeof a === 'string').map((a) => a.split('/').at(-1)!) : [];
|
|
73
|
+
const pending = waitForAssets(run, repo, `v${version}`, names, context.input.assetWaitSeconds);
|
|
74
|
+
if (pending.length) return { outcome: 'fail' as const, verdict: 'fail' as const, summary: `published v${version} but assets not downloadable yet: ${pending.join(', ')}`, tag: `v${version}` };
|
|
75
|
+
return { outcome: 'ok' as const, verdict: 'pass' as const, summary: `published v${version} · ${names.length} assets downloadable`, tag: `v${version}` };
|
|
54
76
|
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
55
77
|
}
|
|
56
78
|
|
|
@@ -26,6 +26,23 @@ export function readFileDurations(dir: string): Map<string, number> {
|
|
|
26
26
|
return durations;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** Per-file peak memory (MB) from a TSV with `file` and `rss_mb` columns (TD1 whole-gate measurement); missing → empty. */
|
|
30
|
+
export function readFileMemory(path: string): Map<string, number> {
|
|
31
|
+
const memory = new Map<string, number>();
|
|
32
|
+
let lines: string[];
|
|
33
|
+
try { lines = readFileSync(path, 'utf8').split(/\r?\n/); } catch { return memory; }
|
|
34
|
+
const header = lines[0]?.split('\t') ?? [];
|
|
35
|
+
const fileAt = header.indexOf('file');
|
|
36
|
+
const rssAt = header.indexOf('rss_mb');
|
|
37
|
+
if (fileAt < 0 || rssAt < 0) return memory;
|
|
38
|
+
for (const line of lines.slice(1)) {
|
|
39
|
+
const cells = line.split('\t');
|
|
40
|
+
const mb = Number(cells[rssAt]);
|
|
41
|
+
if (cells[fileAt] && cells[rssAt] !== '' && Number.isFinite(mb) && mb >= 0) memory.set(cells[fileAt]!, mb);
|
|
42
|
+
}
|
|
43
|
+
return memory;
|
|
44
|
+
}
|
|
45
|
+
|
|
29
46
|
/** Longest processing time first; tie-breaks use lexical paths, then the least populated lightest shard. */
|
|
30
47
|
export function planShards(files: string[], durations: Map<string, number>, count: number): Array<{ files: string[]; plannedSeconds: number }> {
|
|
31
48
|
if (!Number.isSafeInteger(count) || count < 1) throw new Error('invalid shard count');
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// 스토리보드 정본(storyboards/<kind>/<id>.yaml) — 검사 ⊕ MD 뽑기 (대표 10-01 23:0x · OP 형식 결정 = YAML 정본 ⊕ 스키마 ⊕ 생성 MD).
|
|
3
|
+
// bun scripts/storyboard/storyboard.ts lint [파일…] 스키마 필수 칸 · 시간 겹침 · 화면 문구 출처 · 승인본 규칙
|
|
4
|
+
// bun scripts/storyboard/storyboard.ts render [파일…] 내부 문서 `STORYBOARD-<id>-v<version>` 를 다시 쓴다(손으로 안 고친다)
|
|
5
|
+
// 인자가 없으면 storyboards/ 아래 전부. lint 는 오류가 있으면 rc 1, 경고만이면 rc 0.
|
|
6
|
+
import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { join, relative, resolve } from 'node:path';
|
|
8
|
+
import { parse } from 'yaml';
|
|
9
|
+
|
|
10
|
+
const ROOT = resolve(import.meta.dir, '../..');
|
|
11
|
+
const DIR = join(ROOT, 'storyboards');
|
|
12
|
+
|
|
13
|
+
type Text = { text: string; source: string; ref?: string };
|
|
14
|
+
type Shot = {
|
|
15
|
+
id: string; t: [number, number]; stage: string; layer?: string; persona?: string; purpose: string; subject?: string; action?: string;
|
|
16
|
+
camera?: { move?: string; lens_mm?: number; angle?: string; framing?: string }; lighting?: string; color?: string;
|
|
17
|
+
transition_in?: { type: string; intent: string }; on_screen_text?: Text[]; audio?: { vo?: string; sfx?: string; bgm?: string };
|
|
18
|
+
generation?: { tool: string; model?: string; prompt?: string; asset?: string }; real_or_staged: string; state: string; notes?: string;
|
|
19
|
+
};
|
|
20
|
+
/** kind: site — a site's page structure, menu and copy draft, planned like a storyboard (SITE2 · 10-02). */
|
|
21
|
+
type Section = { id: string; title: string; purpose: string; change: string; copy?: Text[]; notes?: string };
|
|
22
|
+
type Site = { domain: string; role: string; preview?: string; branch?: string; nav: Array<{ label: string; href: string }>; pages: Array<{ path: string; sections: Section[] }> };
|
|
23
|
+
export type Storyboard = {
|
|
24
|
+
id: string; kind: string; title: string; version: number; status: string; approved?: { by: string; at: string; note?: string } | null;
|
|
25
|
+
supersedes?: string; owner: string; directive?: string; format: { aspects: string[]; duration_s: number; fps?: number; loop?: boolean; surface?: string };
|
|
26
|
+
scqa?: { s?: string; c?: string; q?: string; a?: string }; principles?: string[]; layers?: Record<string, string>; shots: Shot[]; sites?: Site[];
|
|
27
|
+
references?: string[]; open_questions?: string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const KINDS = ['teaser', 'site-hero', 'event-demo', 'persona-scene', 'explainer', 'ad', 'site'];
|
|
31
|
+
const CHANGES = ['keep', 'move', 'new', 'replace', 'remove'];
|
|
32
|
+
const STAGES = ['hook', 'buildup', 'climax', 'transition', 'end', 'loop'];
|
|
33
|
+
const SOURCES = ['real-ui', 'real-cli', 'real-output', 'copy', 'example'];
|
|
34
|
+
|
|
35
|
+
export function lint(sb: Storyboard, file = ''): { errors: string[]; warnings: string[] } {
|
|
36
|
+
const errors: string[] = [], warnings: string[] = [];
|
|
37
|
+
const need = (cond: unknown, msg: string) => { if (!cond) errors.push(msg); };
|
|
38
|
+
need(/^[a-z0-9][a-z0-9-]{2,80}$/.test(sb.id ?? ''), 'id: 소문자·숫자·하이픈 3~80자');
|
|
39
|
+
need(KINDS.includes(sb.kind), `kind: ${KINDS.join('|')}`);
|
|
40
|
+
need(Number.isInteger(sb.version) && sb.version >= 1, 'version: 1 이상 정수');
|
|
41
|
+
need(['draft', 'approved', 'superseded'].includes(sb.status), 'status: draft|approved|superseded');
|
|
42
|
+
need(sb.owner, 'owner 필요');
|
|
43
|
+
if (sb.status === 'approved') need(sb.approved?.by && sb.approved?.at, '승인본은 approved.by·at 필요');
|
|
44
|
+
if (file && sb.kind && !file.includes(`/${sb.kind}/`)) errors.push(`파일 위치가 kind(${sb.kind}) 폴더가 아니다`);
|
|
45
|
+
if (sb.kind === 'site') return lintSite(sb, errors, warnings);
|
|
46
|
+
need(sb.format?.aspects?.length && sb.format.duration_s > 0, 'format.aspects ⊕ duration_s 필요');
|
|
47
|
+
if (!Array.isArray(sb.shots) || !sb.shots.length) { errors.push('shots 가 비었다'); return { errors, warnings }; }
|
|
48
|
+
const ids = new Set<string>();
|
|
49
|
+
sb.shots.forEach((s, i) => {
|
|
50
|
+
const at = `shots[${i}](${s.id ?? '?'})`;
|
|
51
|
+
need(s.id && !ids.has(s.id), `${at}: id 없음·중복`); ids.add(s.id);
|
|
52
|
+
need(Array.isArray(s.t) && s.t.length === 2 && s.t[0] < s.t[1], `${at}: t = [시작, 끝] 이고 시작 < 끝`);
|
|
53
|
+
need(STAGES.includes(s.stage), `${at}: stage ${STAGES.join('|')}`);
|
|
54
|
+
need(s.purpose, `${at}: purpose(이 비트의 한 가지) 필요`);
|
|
55
|
+
need(['real', 'staged', 'composite'].includes(s.real_or_staged), `${at}: real_or_staged 필요`);
|
|
56
|
+
need(['planned', 'shot', 'approved'].includes(s.state), `${at}: state planned|shot|approved`);
|
|
57
|
+
if (s.transition_in && !s.transition_in.intent) errors.push(`${at}: 전환에는 의도 한 줄(«그냥 컷»은 설계가 아니다)`);
|
|
58
|
+
for (const txt of s.on_screen_text ?? []) {
|
|
59
|
+
need(SOURCES.includes(txt.source), `${at}: 화면 문구 «${txt.text}» 의 source 가 ${SOURCES.join('|')} 가 아니다`);
|
|
60
|
+
if (txt.source?.startsWith('real-') && !txt.ref) warnings.push(`${at}: 실제 문면 «${txt.text}» 에 ref(출처) 가 없다`);
|
|
61
|
+
if (/\d/.test(txt.text) && txt.source === 'copy') warnings.push(`${at}: 마케팅 문구에 숫자 «${txt.text}» — 실측 근거나 «example» 로`);
|
|
62
|
+
}
|
|
63
|
+
if (s.generation?.tool === 'higgsfield' && !s.generation.prompt) warnings.push(`${at}: 힉스필드 샷에 prompt 가 없다`);
|
|
64
|
+
if (s.real_or_staged !== 'real' && !/연출/.test(JSON.stringify(sb))) warnings.push(`${at}: 연출 샷이 있는데 문서 어디에도 «연출 장면» 표기가 없다`);
|
|
65
|
+
});
|
|
66
|
+
const end = Math.max(...sb.shots.map((s) => s.t?.[1] ?? 0));
|
|
67
|
+
if (sb.format?.duration_s && Math.abs(end - sb.format.duration_s) > 0.5) warnings.push(`마지막 샷 끝(${end}s) ≠ format.duration_s(${sb.format.duration_s}s)`);
|
|
68
|
+
return { errors, warnings };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A site plan has pages instead of shots: every section says why it is there and what changes; copy names its source. */
|
|
72
|
+
function lintSite(sb: Storyboard, errors: string[], warnings: string[]): { errors: string[]; warnings: string[] } {
|
|
73
|
+
if (!Array.isArray(sb.sites) || !sb.sites.length) { errors.push('site 종류는 sites 가 필요하다'); return { errors, warnings }; }
|
|
74
|
+
for (const site of sb.sites) {
|
|
75
|
+
const at = `sites(${site.domain ?? '?'})`;
|
|
76
|
+
if (!site.domain || !site.role) errors.push(`${at}: domain ⊕ role(이 사이트가 맡는 것) 필요`);
|
|
77
|
+
if (!site.nav?.length) errors.push(`${at}: nav(메뉴) 필요`);
|
|
78
|
+
if (!site.pages?.length) errors.push(`${at}: pages 필요`);
|
|
79
|
+
if (!site.preview) warnings.push(`${at}: preview(미리보기 주소) 없음`);
|
|
80
|
+
const ids = new Set<string>();
|
|
81
|
+
for (const page of site.pages ?? []) for (const sec of page.sections ?? []) {
|
|
82
|
+
const where = `${at}${page.path} #${sec.id ?? '?'}`;
|
|
83
|
+
if (!sec.id || ids.has(`${page.path}#${sec.id}`)) errors.push(`${where}: id 없음·중복`);
|
|
84
|
+
ids.add(`${page.path}#${sec.id}`);
|
|
85
|
+
if (!sec.purpose) errors.push(`${where}: purpose(이 칸이 하는 한 가지) 필요`);
|
|
86
|
+
if (!CHANGES.includes(sec.change)) errors.push(`${where}: change ${CHANGES.join('|')}`);
|
|
87
|
+
for (const txt of sec.copy ?? []) {
|
|
88
|
+
if (!SOURCES.includes(txt.source)) errors.push(`${where}: 문구 «${txt.text}» 의 source 가 ${SOURCES.join('|')} 가 아니다`);
|
|
89
|
+
if (/\d/.test(txt.text) && txt.source === 'copy' && !txt.ref) warnings.push(`${where}: 마케팅 문구에 숫자 «${txt.text}» — ref(실측 근거) 를 단다`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { errors, warnings };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function renderSites(sb: Storyboard): string[] {
|
|
97
|
+
return (sb.sites ?? []).flatMap((site) => [
|
|
98
|
+
`## ${site.domain} — ${site.role}`, '',
|
|
99
|
+
...(site.preview ? [`- 미리보기: ${site.preview}${site.branch ? ` (가지 \`${site.branch}\`)` : ''}`] : []),
|
|
100
|
+
`- 메뉴: ${site.nav.map((n) => `${n.label}(${n.href})`).join(' · ')}`, '',
|
|
101
|
+
...site.pages.flatMap((page) => [
|
|
102
|
+
`### ${page.path}`, '', '| # | 칸 | 제목 | 목적 | 바뀜 | 문구(출처) | 메모 |', '|---|---|---|---|---|---|---|',
|
|
103
|
+
...page.sections.map((sec, i) => `| ${i + 1} | ${md(sec.id)} | ${md(sec.title)} | ${md(sec.purpose)} | ${sec.change} | ${md((sec.copy ?? []).map((x) => `«${x.text}»(${x.source}${x.ref ? ` · ${x.ref}` : ''})`).join(' / '))} | ${md(sec.notes)} |`),
|
|
104
|
+
'',
|
|
105
|
+
]),
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const md = (v: unknown) => String(v ?? '').replace(/\|/g, '\\|').replace(/\n+/g, ' ');
|
|
110
|
+
export function render(sb: Storyboard, file: string): string {
|
|
111
|
+
const head = [
|
|
112
|
+
`# STORYBOARD — ${sb.title} (v${sb.version} · ${sb.status}${sb.approved ? ` · 승인 ${sb.approved.by} ${sb.approved.at}` : ''})`,
|
|
113
|
+
'',
|
|
114
|
+
`> ⛔ 자동 생성 — 손으로 고치지 않는다. 정본 = \`${file}\` · 다시 뽑기 = \`bun scripts/storyboard/storyboard.ts render ${file}\`.`,
|
|
115
|
+
`> 종류 ${sb.kind} · 소유 ${sb.owner}${sb.format ? ` · ${sb.format.aspects.join(' · ')} · ${sb.format.duration_s}초${sb.format.loop ? ' · 반복' : ''}${sb.format.surface ? ` · ${sb.format.surface}` : ''}` : ''}${sb.supersedes ? ` · 앞 판 ${sb.supersedes}` : ''}`,
|
|
116
|
+
...(sb.directive ? [`> 지시: ${sb.directive}`] : []),
|
|
117
|
+
'',
|
|
118
|
+
];
|
|
119
|
+
const scqa = sb.scqa ? ['## SCQA', ...(['s', 'c', 'q', 'a'] as const).filter((k) => sb.scqa?.[k]).map((k) => `- **${k.toUpperCase()}** — ${sb.scqa![k]}`), ''] : [];
|
|
120
|
+
const pr = sb.principles?.length ? ['## 원칙', ...sb.principles.map((p, i) => `${i + 1}. ${p}`), ''] : [];
|
|
121
|
+
const layers = sb.layers ? ['## 층', ...Object.entries(sb.layers).map(([k, v]) => `- **${k}** — ${v}`), ''] : [];
|
|
122
|
+
const table = sb.kind === 'site' ? renderSites(sb) : [
|
|
123
|
+
'## 샷 구조표',
|
|
124
|
+
'| # | 초 | 단계 | 층·인물 | 목적 | 화면(피사체·행동) | 카메라 | 빛·색 | 전환(의도) | 화면 문구(출처) | 소리 | 만들기 | 실물/연출 · 상태 |',
|
|
125
|
+
'|---|---|---|---|---|---|---|---|---|---|---|---|---|',
|
|
126
|
+
...sb.shots.map((s) => `| ${md(s.id)} | ${s.t[0]}–${s.t[1]} | ${s.stage} | ${md([s.layer, s.persona].filter(Boolean).join(' · '))} | ${md(s.purpose)} | ${md([s.subject, s.action].filter(Boolean).join(' — '))} | ${md(s.camera ? [s.camera.move, s.camera.lens_mm ? `${s.camera.lens_mm}mm` : '', s.camera.angle, s.camera.framing].filter(Boolean).join(' · ') : '')} | ${md([s.lighting, s.color].filter(Boolean).join(' · '))} | ${md(s.transition_in ? `${s.transition_in.type} — ${s.transition_in.intent}` : '')} | ${md((s.on_screen_text ?? []).map((x) => `«${x.text}»(${x.source}${x.ref ? ` · ${x.ref}` : ''})`).join(' / '))} | ${md(s.audio ? [s.audio.vo && `VO ${s.audio.vo}`, s.audio.sfx && `SFX ${s.audio.sfx}`, s.audio.bgm && `BGM ${s.audio.bgm}`].filter(Boolean).join(' · ') : '')} | ${md(s.generation ? [s.generation.tool, s.generation.model].filter(Boolean).join(' · ') : '')} | ${s.real_or_staged} · ${s.state} |`),
|
|
127
|
+
'',
|
|
128
|
+
];
|
|
129
|
+
const shots = sb.shots ?? [];
|
|
130
|
+
const prompts = shots.filter((s) => s.generation?.prompt);
|
|
131
|
+
const pblock = prompts.length ? ['## 생성 프롬프트', ...prompts.flatMap((s) => [`### ${s.id} · ${s.generation!.tool}${s.generation!.model ? ` · ${s.generation!.model}` : ''}`, '```', s.generation!.prompt!.trim(), '```', ...(s.generation!.asset ? [`결과: \`${s.generation!.asset}\``] : []), ''])] : [];
|
|
132
|
+
const notes = shots.filter((s) => s.notes).map((s) => `- ${s.id}: ${s.notes}`);
|
|
133
|
+
const tail = [
|
|
134
|
+
...(notes.length ? ['## 샷 메모', ...notes, ''] : []),
|
|
135
|
+
...(sb.open_questions?.length ? ['## 열린 물음', ...sb.open_questions.map((q) => `- ${q}`), ''] : []),
|
|
136
|
+
...(sb.references?.length ? ['## 참고', ...sb.references.map((r) => `- ${r}`), ''] : []),
|
|
137
|
+
];
|
|
138
|
+
return [...head, ...scqa, ...pr, ...layers, ...table, ...pblock, ...tail].join('\n');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function files(args: string[]): string[] {
|
|
142
|
+
if (args.length) return args.map((a) => resolve(a));
|
|
143
|
+
const out: string[] = [];
|
|
144
|
+
const walk = (d: string) => { for (const f of readdirSync(d)) { const p = join(d, f); if (statSync(p).isDirectory()) walk(p); else if (/\.ya?ml$/.test(f) && !f.startsWith('_')) out.push(p); } };
|
|
145
|
+
walk(DIR);
|
|
146
|
+
return out.sort();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (import.meta.main) {
|
|
150
|
+
const [cmd, ...rest] = process.argv.slice(2);
|
|
151
|
+
if (cmd !== 'lint' && cmd !== 'render') { console.error('usage: storyboard.ts lint|render [files…]'); process.exit(2); }
|
|
152
|
+
let bad = 0;
|
|
153
|
+
for (const f of files(rest)) {
|
|
154
|
+
const rel = relative(ROOT, f);
|
|
155
|
+
const sb = parse(readFileSync(f, 'utf8')) as Storyboard;
|
|
156
|
+
const { errors, warnings } = lint(sb, rel);
|
|
157
|
+
for (const e of errors) console.log(`✗ ${rel}: ${e}`);
|
|
158
|
+
for (const w of warnings) console.log(`⚠ ${rel}: ${w}`);
|
|
159
|
+
if (errors.length) { bad++; continue; }
|
|
160
|
+
if (cmd === 'render') {
|
|
161
|
+
const out = join(ROOT, 'docs', 'marketing', `STORYBOARD-${sb.id}-v${sb.version}.md`);
|
|
162
|
+
writeFileSync(out, render(sb, rel) + '\n');
|
|
163
|
+
console.log(`✓ ${rel} → ${relative(ROOT, out)}`);
|
|
164
|
+
} else console.log(`✓ ${rel} · v${sb.version} · ${sb.status} · ${sb.kind === 'site' ? `사이트 ${sb.sites?.length ?? 0}` : `샷 ${sb.shots.length}`}`);
|
|
165
|
+
}
|
|
166
|
+
process.exit(bad ? 1 : 0);
|
|
167
|
+
}
|