elanous 0.2.12 → 0.2.13
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/{1394-ee3795b112907122.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7758-0b018a9e4fc56200.js → 7758-85c2b53f731da024.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8605-0259994816604c49.js → 8605-9da4678f8a310d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-cb169620945d1952.js → page-e5b4e1b256f41076.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-cefe8169e36cf62a.js → page-e49b9b76c565a0b1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-813c4a52c9d05ca8.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-4731f2c215358937.js → page-c85b816e3b269dbb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a022b257950f39e6.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-a2ffc717e04b8b0a.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b59ee4e70fc18fcf.js → page-a0ea8f5a2341ee6b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c172dabd796eb7a3.js → page-36c6c4183353ac9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-1efd0cb9e0ed9d13.js → page-cb2eb0fd809980da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-d5dc7ff69a37b4cf.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-dfca9b7d2de0b135.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-66c27608f651b866.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-6dd3bfbbde2a5c2c.js → page-6ddd2109b7fc3a80.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-77447356baad75d2.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-6d604227e1664b71.js → page-f672536f14f0969f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-4048543b4b7ad5e0.js → page-e7a6c80f288b1948.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4c3a3126abed551a.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-64b0aecb1ca75e75.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-9547b5cec7cf76bf.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-2cddeb9e2aeedc55.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-baba2e047872fb0f.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-e895da4ec9cac345.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2e9e2fb01c9da1c9.js → page-2b256feed331546f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-d62ceb13411ebba4.js → page-505bb96d69a8622c.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/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/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/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +219 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/release-loop/gate-node.ts +53 -15
- package/scripts/release-loop/ops-upgrade-node.ts +17 -5
- package/scripts/test-deterministic.ts +53 -3
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +12 -7
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +1 -1
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +29 -0
- package/src/cli/pod-cli.ts +10 -3
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +108 -28
- package/src/context-bus/condense.ts +194 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-surfaces.ts +15 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +1 -1
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow.ts +64 -11
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +81 -17
- package/src/harness/harness-cli-command.ts +27 -1
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +4 -2
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/index.ts +199 -7
- package/src/intake-plane/discord-seat-work.ts +13 -6
- package/src/intake-plane/linear-wish.ts +44 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-card.ts +1 -1
- package/src/loops/checker.ts +178 -0
- package/src/loops/loop-cli.ts +37 -3
- package/src/loops/observe.ts +49 -0
- package/src/loops/orchestrator/overlap.ts +141 -0
- package/src/loops/orchestrator/traffic.ts +150 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/verdict.ts +22 -0
- package/src/maturity/feature-maturity.ts +3 -3
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +47 -0
- package/src/nexus/api/personas.ts +2 -1
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +2 -1
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/landed-but-yellow.ts +12 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +90 -16
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +78 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +38 -8
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +10 -4
- package/src/telegram.ts +5 -0
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +94 -10
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { debug } from '../../src/debug/log.js';
|
|
8
|
+
import { streamLLM } from '../../src/llm.js';
|
|
9
|
+
import { unified } from 'unified';
|
|
10
|
+
import remarkParse from 'remark-parse';
|
|
11
|
+
import remarkGfm from 'remark-gfm';
|
|
12
|
+
import remarkRehype from 'remark-rehype';
|
|
13
|
+
import rehypeSanitize from 'rehype-sanitize';
|
|
14
|
+
import rehypeStringify from 'rehype-stringify';
|
|
15
|
+
|
|
16
|
+
export type Stage = 'collect' | 'draft' | 'render' | 'html' | 'pdf';
|
|
17
|
+
export interface Input { name: string; kind: 'pdf' | 'image' | 'markdown'; text: string }
|
|
18
|
+
export interface Counts { inputs: number; diagrams: number; rendered: number; html: 'yes' | 'no'; pdf: 'yes' | 'skipped' }
|
|
19
|
+
export interface StageResult extends Counts { outcome: 'ok' | 'skipped' }
|
|
20
|
+
export interface StageOptions {
|
|
21
|
+
llm?: (prompt: string) => Promise<string>;
|
|
22
|
+
toolsPath?: string;
|
|
23
|
+
referenceRoot?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const ownRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../skills');
|
|
27
|
+
const kinds: Record<string, Input['kind']> = {
|
|
28
|
+
'.pdf': 'pdf', '.md': 'markdown', '.png': 'image', '.jpg': 'image', '.jpeg': 'image', '.webp': 'image',
|
|
29
|
+
};
|
|
30
|
+
const mermaid = /^```mermaid\s*\n([\s\S]*?)^```\s*$/gm;
|
|
31
|
+
const escapeHtml = (text: string) => text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
32
|
+
|
|
33
|
+
function pathStatus(path: string): ReturnType<typeof lstatSync> | undefined {
|
|
34
|
+
try { return lstatSync(path); }
|
|
35
|
+
catch (error) {
|
|
36
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function outputDir(folder: string): string {
|
|
42
|
+
if (!lstatSync(folder).isDirectory()) throw new Error(`강의 폴더가 아니다: ${folder}`);
|
|
43
|
+
const dir = join(resolve(folder), 'lecture-note');
|
|
44
|
+
const status = pathStatus(dir);
|
|
45
|
+
if (status && (status.isSymbolicLink() || !status.isDirectory())) throw new Error('lecture-note 출력 폴더가 안전하지 않다');
|
|
46
|
+
mkdirSync(dir, { recursive: true });
|
|
47
|
+
return dir;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function artifact(dir: string, name: string): string {
|
|
51
|
+
const path = join(dir, name);
|
|
52
|
+
const status = pathStatus(path);
|
|
53
|
+
if (status && (status.isSymbolicLink() || !status.isFile())) throw new Error(`출력 파일이 안전하지 않다: ${path}`);
|
|
54
|
+
return path;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function binary(name: string, path = process.env.PATH ?? ''): string | undefined {
|
|
58
|
+
for (const part of path.split(':').filter(Boolean)) {
|
|
59
|
+
const at = join(part, name);
|
|
60
|
+
if (existsSync(at) && statSync(at).isFile()) return at;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function run(bin: string, args: string[], dir: string, timeout: number): ReturnType<typeof spawnSync> {
|
|
66
|
+
return spawnSync(bin, args, { cwd: dir, encoding: 'utf8', timeout, maxBuffer: 20 * 1024 * 1024,
|
|
67
|
+
env: { ...process.env, HOME: dir, XDG_CACHE_HOME: dir, XDG_CONFIG_HOME: dir, UV_CACHE_DIR: dir, PYTHONDONTWRITEBYTECODE: '1' } });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readInputs(dir: string): Input[] {
|
|
71
|
+
const value: unknown = JSON.parse(readFileSync(join(dir, 'inputs.json'), 'utf8'));
|
|
72
|
+
if (!Array.isArray(value) || !value.every((item) => item && typeof item.name === 'string' && typeof item.kind === 'string' && typeof item.text === 'string')) {
|
|
73
|
+
throw new Error('inputs.json 형식 오류');
|
|
74
|
+
}
|
|
75
|
+
return value as Input[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function counts(dir: string): Counts {
|
|
79
|
+
const inputs = existsSync(join(dir, 'inputs.json')) ? readInputs(dir).length : 0;
|
|
80
|
+
const notes = existsSync(join(dir, 'notes.md')) ? readFileSync(join(dir, 'notes.md'), 'utf8') : '';
|
|
81
|
+
const diagrams = [...notes.matchAll(mermaid)].length;
|
|
82
|
+
const rendered = [...Array(diagrams).keys()].filter(i => existsSync(join(dir, `diagram-${i + 1}.png`))).length;
|
|
83
|
+
return { inputs, diagrams, rendered, html: existsSync(join(dir, 'notes.html')) ? 'yes' : 'no', pdf: existsSync(join(dir, 'notes.pdf')) ? 'yes' : 'skipped' };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function collect(folder: string, options: StageOptions = {}): StageResult {
|
|
87
|
+
const dir = outputDir(folder);
|
|
88
|
+
const files: Input[] = [];
|
|
89
|
+
const pdfText = binary('pdftotext', options.toolsPath);
|
|
90
|
+
const sources = resolve(folder, 'sources/common');
|
|
91
|
+
const scan = [{ base: folder, prefix: '' }, ...(pathStatus(sources)?.isDirectory() && !pathStatus(sources)?.isSymbolicLink()
|
|
92
|
+
? [{ base: sources, prefix: 'sources/common/' }] : [])];
|
|
93
|
+
for (const { base, prefix } of scan) for (const entry of readdirSync(base, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
94
|
+
const kind = kinds[extname(entry.name).toLowerCase()];
|
|
95
|
+
if (!entry.isFile() || !kind) continue;
|
|
96
|
+
const at = join(base, entry.name);
|
|
97
|
+
const text = kind === 'markdown' ? readFileSync(at, 'utf8') : kind === 'image' ? '이미지 — 시각 자료 (텍스트 추출 없음)' :
|
|
98
|
+
pdfText ? (() => {
|
|
99
|
+
const result = run(pdfText, ['-layout', at, '-'], dir, 60000);
|
|
100
|
+
return result.status === 0 ? String(result.stdout) : 'PDF 텍스트 없음 — pdftotext 추출 실패';
|
|
101
|
+
})() : 'PDF 텍스트 없음 — pdftotext 없음';
|
|
102
|
+
files.push({ name: prefix + entry.name, kind, text });
|
|
103
|
+
}
|
|
104
|
+
writeFileSync(artifact(dir, 'inputs.json'), JSON.stringify(files, null, 2) + '\n');
|
|
105
|
+
return { ...counts(dir), outcome: 'ok' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const instructions = `강의 자료로 종합 디지털 강의 노트 Markdown만 작성하라.
|
|
109
|
+
강의 진행 순서를 유지하고 손필기/슬라이드의 시각 정보와 요약의 텍스트를 통합하라. 읽지 못한 이미지는 추측하지 말고 출처를 표시하라.
|
|
110
|
+
다이어그램은 Mermaid 우선으로 해당 설명 위치에 \`\`\`mermaid 코드블록으로 내장하라 (곡선·자유형은 코드로 억지 변환하지 않는다).
|
|
111
|
+
비교·분류는 표로, 실제 사례는 별도 사례 박스로, 핵심 원칙은 인용문으로 표시하라.
|
|
112
|
+
수식은 KaTeX 인라인 \\( ... \\) 형식, 강조 수식은 > \\( ... \\) 형식으로 쓰고 $$ 블록 수식이나 볼드/코드 수식은 쓰지 마라.
|
|
113
|
+
강의 개요 바로 아래에 해당 주차 신규 용어집 표(| 약어 | 영문 | 한국어 | 정의 |)를 배치하라.
|
|
114
|
+
목차와 각 헤딩 위의 영문 앵커를 만들고, 맨 끝에 다이어그램 목록 부록을 둬라. 근거 없는 내용은 만들어내지 마라.`;
|
|
115
|
+
|
|
116
|
+
export async function draft(folder: string, options: StageOptions = {}): Promise<StageResult> {
|
|
117
|
+
const dir = outputDir(folder);
|
|
118
|
+
const inputs = readInputs(dir);
|
|
119
|
+
if (!inputs.length) throw new Error('강의 자료가 없다');
|
|
120
|
+
const prompt = `${instructions}\n\n자료:\n${inputs.map(i => `### ${i.name} (${i.kind})\n${i.text}`).join('\n\n')}`;
|
|
121
|
+
const ask = options.llm ?? ((request: string) => streamLLM([{ role: 'user', content: request }], () => {}));
|
|
122
|
+
const notes = (await ask(prompt)).trim();
|
|
123
|
+
if (!notes) throw new Error('LLM 초안이 비었다');
|
|
124
|
+
writeFileSync(artifact(dir, 'notes.md'), notes + '\n');
|
|
125
|
+
return { ...counts(dir), outcome: 'ok' };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function render(folder: string, options: StageOptions = {}): StageResult {
|
|
129
|
+
const dir = outputDir(folder);
|
|
130
|
+
const notes = readFileSync(join(dir, 'notes.md'), 'utf8');
|
|
131
|
+
const renderer = join(options.referenceRoot ?? ownRoot, 'diagram-master/references/engines/mermaid/render_mermaid.py');
|
|
132
|
+
const uv = binary('uv', options.toolsPath);
|
|
133
|
+
let unavailable = 0;
|
|
134
|
+
for (const [i, match] of [...notes.matchAll(mermaid)].entries()) {
|
|
135
|
+
const png = artifact(dir, `diagram-${i + 1}.png`);
|
|
136
|
+
if (existsSync(png)) rmSync(png);
|
|
137
|
+
if (!uv || !existsSync(renderer)) { unavailable++; continue; }
|
|
138
|
+
const mmd = artifact(dir, `diagram-${i + 1}.mmd`);
|
|
139
|
+
writeFileSync(mmd, match[1]!);
|
|
140
|
+
const result = run(uv, ['run', 'python', renderer, mmd, '--output', png], dir, 120000);
|
|
141
|
+
if (result.status !== 0 || !existsSync(png)) {
|
|
142
|
+
if (existsSync(png)) rmSync(png);
|
|
143
|
+
throw new Error(`Mermaid 렌더 실패 (diagram-${i + 1}, exit=${result.status}): ${result.error?.message ?? result.stderr ?? result.stdout ?? 'PNG 없음'}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (unavailable) console.log(`렌더러 없음: ${unavailable}`);
|
|
147
|
+
return { ...counts(dir), outcome: 'ok' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function markdownHtml(text: string): string {
|
|
151
|
+
let suffix = 0;
|
|
152
|
+
while (text.includes(`LECTUREMATHOPEN${suffix}`) || text.includes(`LECTUREMATHCLOSE${suffix}`)) suffix++;
|
|
153
|
+
const open = `LECTUREMATHOPEN${suffix}`;
|
|
154
|
+
const close = `LECTUREMATHCLOSE${suffix}`;
|
|
155
|
+
const protectedText = text.replace(/\\\(/g, open).replace(/\\\)/g, close);
|
|
156
|
+
return String(unified().use(remarkParse).use(remarkGfm)
|
|
157
|
+
.use(remarkRehype).use(rehypeSanitize).use(rehypeStringify).processSync(protectedText))
|
|
158
|
+
.replaceAll(open, '\\(').replaceAll(close, '\\)');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function html(folder: string, options: StageOptions = {}): StageResult {
|
|
162
|
+
const dir = outputDir(folder);
|
|
163
|
+
const notes = readFileSync(join(dir, 'notes.md'), 'utf8');
|
|
164
|
+
const template = readFileSync(join(options.referenceRoot ?? ownRoot, 'lecture-note-digitizer/references/html-template.md'), 'utf8');
|
|
165
|
+
const css = template.match(/```css\n([\s\S]*?)\n```/)?.[1];
|
|
166
|
+
if (!css) throw new Error('HTML 템플릿 CSS 없음');
|
|
167
|
+
let n = 0;
|
|
168
|
+
const withoutAppendix = notes.replace(/\n(?:---\n\n)?(?:<a id="appendix"><\/a>\n)?#+ 부록: 다이어그램 목록[\s\S]*$/u, '');
|
|
169
|
+
const anchored = withoutAppendix.replace(/^<a id="([a-zA-Z0-9-]+)"><\/a>$/gm, (_match, id: string) => `\n\nLECTURE_ANCHOR_${id}_END\n\n`);
|
|
170
|
+
const body = markdownHtml(anchored.replace(mermaid, (_block, source: string) => {
|
|
171
|
+
const png = `diagram-${++n}.png`;
|
|
172
|
+
return existsSync(join(dir, png)) ? `\n\n\n\n` : `\n\n\`\`\`mermaid\n${source}\`\`\`\n\n`;
|
|
173
|
+
})).replace(/<p><img src="(diagram-\d+\.png)" alt="(diagram-\d+)"><\/p>/g,
|
|
174
|
+
'<div class="diagram-container"><img src="$1" alt="$2"></div>')
|
|
175
|
+
.replace(/<p>LECTURE_ANCHOR_([a-zA-Z0-9-]+)_END<\/p>/g, '<a id="$1"></a>');
|
|
176
|
+
const title = notes.match(/^# (.+)$/m)?.[1] ?? '강의 노트';
|
|
177
|
+
const sections: string[] = [];
|
|
178
|
+
let heading = 0;
|
|
179
|
+
const numbered = body.replace(/<(h[12])>(.*?)<\/\1>/g, (_m, tag: string, label: string) => {
|
|
180
|
+
const id = `sec-${++heading}`;
|
|
181
|
+
sections.push(`<li><a href="#${id}">${label}</a></li>`);
|
|
182
|
+
return `<${tag} id="${id}">${label}</${tag}>`;
|
|
183
|
+
});
|
|
184
|
+
const page = `<!DOCTYPE html>\n<html lang="ko"><head><meta charset="UTF-8"><title>${escapeHtml(title)}</title>` +
|
|
185
|
+
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">' +
|
|
186
|
+
'<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>' +
|
|
187
|
+
'<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body,{delimiters:[{left:\'\\\\(\',right:\'\\\\)\',display:false}]});"></script>' +
|
|
188
|
+
`<style>${css}</style></head><body><div class="cover"><h1>${escapeHtml(title)}</h1></div>` +
|
|
189
|
+
`<div class="toc"><h2>목차</h2><ol>${sections.join('\n')}</ol></div><div class="section">${numbered}</div></body></html>\n`;
|
|
190
|
+
writeFileSync(artifact(dir, 'notes.html'), page);
|
|
191
|
+
return { ...counts(dir), outcome: 'ok' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function pdf(folder: string, options: StageOptions = {}): StageResult {
|
|
195
|
+
const dir = outputDir(folder);
|
|
196
|
+
const source = artifact(dir, 'notes.html');
|
|
197
|
+
if (!existsSync(source)) throw new Error('notes.html 없음');
|
|
198
|
+
const converter = join(options.referenceRoot ?? ownRoot, 'lecture-note-digitizer/references/convert_to_pdf.py');
|
|
199
|
+
const python = binary('python3', options.toolsPath);
|
|
200
|
+
const target = artifact(dir, 'notes.pdf');
|
|
201
|
+
if (existsSync(target)) throw new Error('기존 notes.pdf 를 덮어쓰지 않는다');
|
|
202
|
+
const result = python && existsSync(converter) ? run(python, [converter, source], dir, 120000) : undefined;
|
|
203
|
+
const engineMissing = !result || (result.status !== 0 && !result.error &&
|
|
204
|
+
(/^ModuleNotFoundError: No module named ['"]playwright['"]$/m.test(String(result.stderr)) ||
|
|
205
|
+
/BrowserType\.launch: Executable doesn't exist at/.test(String(result.stderr))));
|
|
206
|
+
if (engineMissing) {
|
|
207
|
+
if (existsSync(target)) rmSync(target);
|
|
208
|
+
console.log('PDF 엔진 없음 — HTML 이 산출물');
|
|
209
|
+
return { ...counts(dir), pdf: 'skipped', outcome: 'skipped' };
|
|
210
|
+
}
|
|
211
|
+
if (result && (result.status !== 0 || !existsSync(target))) {
|
|
212
|
+
if (existsSync(target)) rmSync(target);
|
|
213
|
+
throw new Error(`PDF 변환 실패 (exit=${result.status}): ${result.error?.message ?? result.stderr ?? result.stdout ?? 'PDF 없음'}`);
|
|
214
|
+
}
|
|
215
|
+
return { ...counts(dir), outcome: 'ok' };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function runStage(stage: Stage, folder: string, options: StageOptions = {}): Promise<StageResult> {
|
|
219
|
+
const result = stage === 'collect' ? collect(folder, options) : stage === 'draft' ? await draft(folder, options) :
|
|
220
|
+
stage === 'render' ? render(folder, options) : stage === 'html' ? html(folder, options) : pdf(folder, options);
|
|
221
|
+
debug.log('lecture.note', stage, { inputs: result.inputs, diagrams: result.diagrams, rendered: result.rendered, pdf: result.pdf });
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function folderFromContext(): string {
|
|
226
|
+
const arg = process.argv.indexOf('--folder');
|
|
227
|
+
if (arg >= 0) {
|
|
228
|
+
const folder = process.argv[arg + 1];
|
|
229
|
+
if (!folder) throw new Error('--folder 값 없음');
|
|
230
|
+
return resolve(folder.replace(/^~(?=\/)/, homedir()));
|
|
231
|
+
}
|
|
232
|
+
const context = process.env.ELANOUS_GRAPH_CONTEXT;
|
|
233
|
+
if (!context) throw new Error('input.folder 필요');
|
|
234
|
+
const data: unknown = JSON.parse(context.trim().startsWith('{') ? context : readFileSync(context, 'utf8'));
|
|
235
|
+
const folder = (data as { input?: { folder?: unknown } })?.input?.folder;
|
|
236
|
+
if (typeof folder !== 'string' || !folder.trim()) throw new Error('input.folder 필요');
|
|
237
|
+
const expanded = folder.replace(/^~(?=\/)/, homedir());
|
|
238
|
+
if (!isAbsolute(expanded)) throw new Error('input.folder 절대 경로 필요');
|
|
239
|
+
return resolve(expanded);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (import.meta.main) {
|
|
243
|
+
const stage = process.argv[2];
|
|
244
|
+
if (!['collect', 'draft', 'render', 'html', 'pdf'].includes(stage ?? '')) throw new Error('stage: collect|draft|render|html|pdf');
|
|
245
|
+
let folder = '';
|
|
246
|
+
try {
|
|
247
|
+
folder = folderFromContext();
|
|
248
|
+
const result = await runStage(stage as Stage, folder);
|
|
249
|
+
console.log(`lecture-note ${folder} inputs=${result.inputs} diagrams=${result.diagrams} rendered=${result.rendered} html=${result.html} pdf=${result.pdf}`);
|
|
250
|
+
console.log(JSON.stringify({ outcome: result.outcome }));
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error(error);
|
|
253
|
+
console.log(JSON.stringify({ outcome: 'fail', reason: error instanceof Error ? error.message : String(error) }));
|
|
254
|
+
process.exitCode = 1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -18,7 +18,7 @@ import { POD_BUN_CACHE_HOST_PATH, parseInstallSeconds, podBunCacheVolume } from
|
|
|
18
18
|
import { PodPoolScheduler, parsePodPool, checkPodPool } from '../../src/task-orchestrator/surfaces/pod-pool.js';
|
|
19
19
|
import { defaultKubectl } from '../../src/task-orchestrator/surfaces/self-implement-pod.js';
|
|
20
20
|
|
|
21
|
-
interface CommandResult { rc: number; output: string; passedIds?: string[] }
|
|
21
|
+
interface CommandResult { rc: number; output: string; passedIds?: string[]; stalledEnv?: Array<{ file: string; reason: 'no-output' }> }
|
|
22
22
|
export interface GateRunner {
|
|
23
23
|
command(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
24
24
|
localCommand(cmd: string, args: string[], cwd: string): Promise<CommandResult>;
|
|
@@ -50,6 +50,7 @@ export interface GateResult {
|
|
|
50
50
|
fixed: number;
|
|
51
51
|
knownEnv: number;
|
|
52
52
|
knownEnvCleared: string[];
|
|
53
|
+
stalledEnv: Array<{ file: string; reason: 'no-output'; local: 'passed' | 'failed' }>;
|
|
53
54
|
baselineSource?: 'ledger' | 'instance' | 'cut-logs' | 'swept';
|
|
54
55
|
durationMs: number;
|
|
55
56
|
error?: string;
|
|
@@ -322,13 +323,15 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
322
323
|
const ran = clean && lastMatch(clean, /Ran (\d+) tests? across (\d+) files?/);
|
|
323
324
|
const passes = clean === undefined ? NaN : summaryCount(clean, 'pass');
|
|
324
325
|
const fails = clean === undefined ? NaN : summaryCount(clean, 'fail');
|
|
326
|
+
const errors = clean === undefined ? NaN : summaryCount(clean, 'error');
|
|
325
327
|
let reason: ShardReason | undefined;
|
|
326
328
|
let namedFailures: number | undefined;
|
|
327
329
|
let unattributedDetail: string | undefined;
|
|
328
330
|
if (jobFailed || (jobExitCode !== undefined && jobExitCode !== 0) || (rc !== undefined && rc !== 0 && rc !== 1)) reason = 'job-failed';
|
|
329
331
|
else if (!clean || rc === undefined) reason = 'no-output';
|
|
330
332
|
else if (!ran || !Number.isFinite(passes) || !Number.isFinite(fails)
|
|
331
|
-
|| Number(ran[2]) === 0 || Number(ran[2]) > paths.length
|
|
333
|
+
|| Number(ran[2]) === 0 || Number(ran[2]) > paths.length
|
|
334
|
+
|| (Number(ran[1]) === 0 && !(errors > 0))) reason = 'incomplete';
|
|
332
335
|
else {
|
|
333
336
|
try {
|
|
334
337
|
const attributed = failuresOf({ rc, output: clean }, `pod-${shard} shard`);
|
|
@@ -400,7 +403,7 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
400
403
|
return { runs: children.flatMap((child) => child.runs), stalled: children.flatMap((child) => child.stalled) };
|
|
401
404
|
}
|
|
402
405
|
return { runs: [{ output: clean!, rc: rc!, pass: passes, fail: fails,
|
|
403
|
-
errors: Number.isNaN(
|
|
406
|
+
errors: Number.isNaN(errors) ? 0 : errors,
|
|
404
407
|
ran: Number(ran![1]), files: Number(ran![2]), passedIds: junit ? junitPassedIds(junit) : [] }], stalled: [] };
|
|
405
408
|
};
|
|
406
409
|
// 모든 조각이 끝난 뒤 판정한다 — 한 조각의 예외로 먼저 돌아가면 다른 Pod 가 도는 채로 정리가 시작된다(#22002 리뷰 R3).
|
|
@@ -413,11 +416,12 @@ export function createGateRunner(repo: string, remote?: string, commandOverride?
|
|
|
413
416
|
const total = measured.reduce((sum, run) => ({ pass: sum.pass + run.pass, fail: sum.fail + run.fail,
|
|
414
417
|
errors: sum.errors + run.errors, ran: sum.ran + run.ran, files: sum.files + run.files }),
|
|
415
418
|
{ pass: 0, fail: 0, errors: 0, ran: 0, files: 0 });
|
|
416
|
-
if (stalledShards.length) throw new StalledPodShards(stalledShards, total);
|
|
417
|
-
if (total.ran === 0 && total.errors === 0) throw new Error('sweep incomplete: no tests ran');
|
|
419
|
+
if (stalledShards.some((item) => item.reason !== 'no-output' || item.files.length !== 1)) throw new StalledPodShards(stalledShards, total);
|
|
420
|
+
if (total.ran === 0 && total.errors === 0 && !stalledShards.length) throw new Error('sweep incomplete: no tests ran');
|
|
418
421
|
const outputs = measured.map((run) => run.output.replace(/(?:^|\n)\s*\d+ (?:pass|fail|errors?)\s*(?=\n|$)/g, '\n').replace(/Ran \d+ tests? across \d+ files?\.?/g, ''));
|
|
419
422
|
return { rc: total.fail + total.errors ? 1 : 0, output: outputs.join('\n') + `\n${total.pass} pass\n${total.fail} fail\n${total.errors} errors\nRan ${total.ran} tests across ${total.files} files.\n`,
|
|
420
|
-
passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort()
|
|
423
|
+
passedIds: [...new Set(measured.flatMap((run) => run.passedIds))].sort(),
|
|
424
|
+
stalledEnv: stalledShards.flatMap((item) => item.files.map((file) => ({ file, reason: 'no-output' as const }))) };
|
|
421
425
|
};
|
|
422
426
|
return {
|
|
423
427
|
command,
|
|
@@ -550,7 +554,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
550
554
|
return baselineCommit(root, version);
|
|
551
555
|
};
|
|
552
556
|
const repo = resolve(opts.repo ?? process.cwd());
|
|
553
|
-
const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: 0 };
|
|
557
|
+
const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], durationMs: 0 };
|
|
554
558
|
let work: string | undefined;
|
|
555
559
|
let baseSnapshot = false;
|
|
556
560
|
let cutFailures: SweepFailures | undefined;
|
|
@@ -601,7 +605,38 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
601
605
|
for (const dir of [cutTree, join(cutTree, 'apps/pwa')]) check(await runner.command('bun', ['install'], dir), `bun install ${dir}`);
|
|
602
606
|
const cutRun = await runner.sweep(cutTree, join(root, 'release', opts.version, 'gate-logs', 'cut'),
|
|
603
607
|
opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
|
|
604
|
-
const
|
|
608
|
+
const localStalled = async (run: CommandResult, tree: string, commit: string, label: string) => {
|
|
609
|
+
const stalled = run.stalledEnv ?? [];
|
|
610
|
+
const noCompletedShard = stalled.length && run.rc === 0
|
|
611
|
+
&& /(?:^|\n)0 pass\n0 fail\n0 errors\nRan 0 tests across 0 files\./.test(run.output);
|
|
612
|
+
const measured: SweepFailures = noCompletedShard ? { failures: [], errors: [] } : failuresOf(run, `${label} sweep`);
|
|
613
|
+
if (!stalled.length) return measured;
|
|
614
|
+
let hostTree = tree;
|
|
615
|
+
const local: GateResult['stalledEnv'] = [];
|
|
616
|
+
try {
|
|
617
|
+
if (opts.remote) {
|
|
618
|
+
hostTree = join(mkdtempSync(join(tmpdir(), 'release-gate-local-')), 'cut');
|
|
619
|
+
check(await runner.localCommand('git', ['clone', '--quiet', '--shared', '--no-checkout', repo, hostTree], repo), 'host cut clone');
|
|
620
|
+
check(await runner.localCommand('git', ['checkout', '--quiet', '--detach', commit], hostTree), 'host cut checkout');
|
|
621
|
+
for (const dir of [hostTree, join(hostTree, 'apps/pwa')]) check(await runner.localCommand('bun', ['install'], dir), `host bun install ${dir}`);
|
|
622
|
+
}
|
|
623
|
+
for (const { file, reason } of stalled) {
|
|
624
|
+
if (!/^(?:[\w.-]+\/)+[\w.-]+\.test\.tsx?$/.test(file) || file.split('/').includes('..')) throw new Error(`unsafe test path: ${file}`);
|
|
625
|
+
const isolated = await runner.localCommand('bun', ['run', 'test:deterministic', asPath(file)], hostTree);
|
|
626
|
+
const parsed = failuresOf(isolated, `${label} host isolated ${file}`);
|
|
627
|
+
if ([...parsed.failures, ...parsed.errors].some((id) => fileOf(id) !== file)) throw new Error(`host isolated run attributed to another file: ${file}`);
|
|
628
|
+
measured.failures.push(...parsed.failures);
|
|
629
|
+
measured.errors.push(...parsed.errors);
|
|
630
|
+
local.push({ file, reason, local: parsed.failures.length || parsed.errors.length ? 'failed' : 'passed' });
|
|
631
|
+
}
|
|
632
|
+
} finally {
|
|
633
|
+
if (opts.remote && hostTree !== tree) rmSync(dirname(hostTree), { recursive: true, force: true });
|
|
634
|
+
}
|
|
635
|
+
result.stalledEnv.push(...local);
|
|
636
|
+
debug.log('release-loop.gate', 'pod-shard-stalled-env', { files: stalled.map(({ file, reason }) => ({ file, reason })), local });
|
|
637
|
+
return measured;
|
|
638
|
+
};
|
|
639
|
+
const cut = await localStalled(cutRun, cutTree, opts.commit, 'cut');
|
|
605
640
|
cutFailures = cut;
|
|
606
641
|
const baseSha = opts.baselineCommit ?? baselineReleaseCommit(opts.baselineVersion);
|
|
607
642
|
const cached = cachedBaseline(opts.baselineVersion, baseSha);
|
|
@@ -630,22 +665,24 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
630
665
|
// hour of local baseline after a 23-minute Pod cut).
|
|
631
666
|
const baseRun = await runner.sweep(await getBaseTree(), join(root, 'release', opts.version, 'gate-logs', 'baseline'),
|
|
632
667
|
opts.pod ? { ...opts.pod, durationSource: join(ledger, 'release', opts.baselineVersion, 'gate-logs', 'cut') } : undefined);
|
|
633
|
-
baseline =
|
|
668
|
+
baseline = await localStalled(baseRun, baseTree!, baseSha, 'baseline');
|
|
634
669
|
}
|
|
635
670
|
for (const id of [...baseline.failures, ...baseline.errors, ...cut.failures, ...cut.errors]) fileOf(id);
|
|
636
671
|
const known = opts.pod ? loadEnvKnownFailures(repo) : new Set<string>();
|
|
637
672
|
const cutIds = [...cut.failures, ...cut.errors];
|
|
673
|
+
const cutHostFailed = new Set(cutRun.stalledEnv?.map(({ file }) => file) ?? []);
|
|
638
674
|
const baselineIds = [...baseline.failures, ...baseline.errors];
|
|
639
675
|
const countedCut = splitKnownEnv(cutIds, known);
|
|
640
676
|
const countedBaseline = splitKnownEnv(baselineIds, known);
|
|
641
677
|
result.knownEnv = countedCut.knownEnv.length;
|
|
642
|
-
result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id)).sort();
|
|
678
|
+
result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id) && !cutRun.stalledEnv?.some(({ file }) => fileOf(id) === file)).sort();
|
|
643
679
|
const diff = diffFailures(countedCut.counted, countedBaseline.counted);
|
|
644
680
|
result.fixed = diff.fixed.length;
|
|
645
681
|
result.preexisting = diff.common.length;
|
|
646
682
|
for (const file of new Set(diff.newFailures.map(fileOf))) {
|
|
647
|
-
const
|
|
648
|
-
|
|
683
|
+
const isolatedCut = cutHostFailed.has(file)
|
|
684
|
+
? { failures: cut.failures.filter((id) => fileOf(id) === file), errors: cut.errors.filter((id) => fileOf(id) === file) }
|
|
685
|
+
: failuresOf(await runner.command('bun', ['run', 'test:deterministic', asPath(file)], cutTree), `cut isolated ${file}`);
|
|
649
686
|
const reproduced = new Set([...isolatedCut.failures, ...isolatedCut.errors]);
|
|
650
687
|
const candidates = diff.newFailures.filter((id) => fileOf(id) === file && reproduced.has(id));
|
|
651
688
|
if (candidates.length === 0) continue;
|
|
@@ -690,7 +727,7 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
690
727
|
if (opts.remote) runner.remoteMirror = undefined;
|
|
691
728
|
result.durationMs = Date.now() - start;
|
|
692
729
|
}
|
|
693
|
-
if (cutFailures && !result.stalledShards?.length) {
|
|
730
|
+
if (cutFailures && !result.stalledShards?.length && !result.stalledEnv.length) {
|
|
694
731
|
try {
|
|
695
732
|
for (const location of roots) {
|
|
696
733
|
const path = failureFile(location, opts.version);
|
|
@@ -714,7 +751,8 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
714
751
|
export function graphGateResult(result: GateResult, graph: boolean) {
|
|
715
752
|
return { ...result, ...(graph && result.outcome === 'regression' ? { outcome: 'fail' as const } : {}),
|
|
716
753
|
verdict: result.outcome === 'ok' ? 'pass' as const : 'fail' as const,
|
|
717
|
-
summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}`
|
|
754
|
+
summary: (result.outcome === 'ok' ? `새 회귀 ${result.introduced.length} · 기존 ${result.preexisting} · 고침 ${result.fixed}` : `게이트 ${result.outcome}: ${result.error ?? result.introduced.length + ' new regressions'}`) + ` · 환경 알려진 실패 ${result.knownEnv}`
|
|
755
|
+
+ (result.stalledEnv.length ? ` · 환경 멈춤 ${result.stalledEnv.length} (Pod 밖 통과 ${result.stalledEnv.filter((item) => item.local === 'passed').length})` : '') };
|
|
718
756
|
}
|
|
719
757
|
|
|
720
758
|
export function parseOptions(args: string[], env: NodeJS.ProcessEnv): GateOptions | 'help' {
|
|
@@ -772,7 +810,7 @@ if (import.meta.main) {
|
|
|
772
810
|
version = opts.version;
|
|
773
811
|
result = await judgeGate(opts);
|
|
774
812
|
} catch (error) {
|
|
775
|
-
result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], durationMs: Date.now() - start, error: String(error) };
|
|
813
|
+
result = { outcome: 'error', commit: '', introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], durationMs: Date.now() - start, error: String(error) };
|
|
776
814
|
}
|
|
777
815
|
if (result.error) console.error(result.error);
|
|
778
816
|
debug.log('release-loop.gate', 'result', { version, outcome: result.outcome });
|
|
@@ -7,7 +7,7 @@ import { errorResult, finishNode, lastResult, nodeOutput, readGraphContext, runC
|
|
|
7
7
|
|
|
8
8
|
interface HostResult { host: string; ok: boolean; before: string; after: string; error?: string; skipped?: string; hooks?: string; restart?: 'no-service' }
|
|
9
9
|
/** Where the internal feed is packed from: a checkout of the release tag, never the run's working tree. */
|
|
10
|
-
export interface FeedSource { checkout: string; commit: string; cleanup: () => void }
|
|
10
|
+
export interface FeedSource { checkout: string; commit: string; deps: 'linked' | 'installed'; cleanup: () => void }
|
|
11
11
|
interface OpsUpgradeDeps { exists?: (path: string) => boolean; installSource?: () => string | null; feedSource?: (version: string) => FeedSource }
|
|
12
12
|
|
|
13
13
|
/** Detached worktree at refs/tags/v<version> with the PWA built — the feed ships what was released (10-04 0.2.11:
|
|
@@ -26,16 +26,28 @@ export function tagFeedSource(run: CommandRunner, repo: string = process.cwd()):
|
|
|
26
26
|
try {
|
|
27
27
|
const added = run('git', ['worktree', 'add', '--detach', tree, commit], repo);
|
|
28
28
|
if (added.status !== 0) throw new Error(`태그 체크아웃 실패: ${reason(added)}`);
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const sameDeps = ['bun.lock', 'apps/pwa/bun.lock', 'package.json', 'apps/pwa/package.json'].every((file) => {
|
|
30
|
+
try { return readFileSync(join(repo, file), 'utf8') === readFileSync(join(tree, file), 'utf8'); }
|
|
31
|
+
catch { return false; }
|
|
32
|
+
});
|
|
33
|
+
const deps: FeedSource['deps'] = sameDeps ? 'linked' : 'installed';
|
|
34
|
+
if (sameDeps) {
|
|
35
|
+
symlinkSync(join(repo, 'node_modules'), join(tree, 'node_modules'));
|
|
36
|
+
if (existsSync(join(repo, 'apps/pwa/node_modules'))) symlinkSync(join(repo, 'apps/pwa/node_modules'), join(tree, 'apps/pwa/node_modules'));
|
|
37
|
+
} else {
|
|
38
|
+
for (const cwd of [tree, join(tree, 'apps/pwa')]) {
|
|
39
|
+
const installed = run('bun', ['install', '--frozen-lockfile'], cwd, 900_000);
|
|
40
|
+
if (installed.status !== 0) throw new Error(`태그 의존성 설치 실패: ${reason(installed)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
31
43
|
// The feed carries the PWA build (apps/pwa/out is ignored, so a fresh tag checkout has none until built).
|
|
32
44
|
const built = run('bun', ['bin/elanous.mjs', 'nexus', 'build'], tree, 900_000);
|
|
33
45
|
if (built.status !== 0) throw new Error(`태그 PWA 빌드 실패: ${reason(built)}`);
|
|
46
|
+
return { checkout: tree, commit, deps, cleanup };
|
|
34
47
|
} catch (error) {
|
|
35
48
|
cleanup();
|
|
36
49
|
throw error;
|
|
37
50
|
}
|
|
38
|
-
return { checkout: tree, commit, cleanup };
|
|
39
51
|
};
|
|
40
52
|
}
|
|
41
53
|
|
|
@@ -96,7 +108,7 @@ export function runOpsUpgrade(run: CommandRunner = runCommand, deps: OpsUpgradeD
|
|
|
96
108
|
const data = lastResult(feed);
|
|
97
109
|
if (feed.status !== 0 || data?.ok !== true || data.version !== version) feedError = `내부 피드 발행 실패: ${feed.status === 0 && data?.ok === true ? `판 불일치 (${String(data.version)})` : reason(feed)}`;
|
|
98
110
|
else if (data.commit !== source.commit) feedError = `내부 피드 발행 실패: 묶은 커밋 ${String(data.commit).slice(0, 12)} ≠ 태그 ${source.commit.slice(0, 12)}`;
|
|
99
|
-
debug.log('release-loop.ops-upgrade', 'feed-source', { version, commit: source.commit, source: `refs/tags/v${version}`, packed: data?.commit, pwaBuild: data?.pwaBuild });
|
|
111
|
+
debug.log('release-loop.ops-upgrade', 'feed-source', { version, commit: source.commit, source: `refs/tags/v${version}`, deps: source.deps, packed: data?.commit, pwaBuild: data?.pwaBuild });
|
|
100
112
|
}
|
|
101
113
|
} catch (error) { feedError = `내부 피드 발행 실패: ${firstLine(String(error))}`; }
|
|
102
114
|
finally { source?.cleanup(); }
|
|
@@ -31,8 +31,9 @@
|
|
|
31
31
|
// reports the failure.
|
|
32
32
|
|
|
33
33
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
34
|
+
import { spawnSync } from 'node:child_process';
|
|
34
35
|
import { tmpdir } from 'node:os';
|
|
35
|
-
import { join } from 'node:path';
|
|
36
|
+
import { join, resolve } from 'node:path';
|
|
36
37
|
// The predicate lives in its own side-effect-free module so it can be tested:
|
|
37
38
|
// importing THIS file used to spawn the suite at load time. The runner is now
|
|
38
39
|
// behind `import.meta.main` / `runDeterministicTests()`, but the filter stays
|
|
@@ -56,6 +57,8 @@ const HARNESS_TEST_ENV_KEYS = [
|
|
|
56
57
|
// Discovery: rg -n "process\.env\.ELANOUS_(HOST|RUN|ORIGIN|SUPERVISOR|PARENT)" src
|
|
57
58
|
// These are ambient execution identities, not inputs to a deterministic test.
|
|
58
59
|
// debug.log also attributes SUBSTRATE and ARM_ID to every plain-object record.
|
|
60
|
+
const GIT_LOCATION_ENV_KEYS = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_COMMON_DIR'] as const;
|
|
61
|
+
|
|
59
62
|
const EXECUTION_ORIGIN_ENV_KEYS = [
|
|
60
63
|
'ELANOUS_HOST_ID',
|
|
61
64
|
'ELANOUS_RUN_ID',
|
|
@@ -120,6 +123,7 @@ export type DeterministicRunnerDeps = {
|
|
|
120
123
|
childGraceMs?: number;
|
|
121
124
|
killProcessGroup?: KillProcessGroup;
|
|
122
125
|
getProcessGroupId?: GetProcessGroupId;
|
|
126
|
+
gitConfig?: (cwd: string, args: string[]) => string;
|
|
123
127
|
};
|
|
124
128
|
|
|
125
129
|
export function isLivePid(pid: number): boolean {
|
|
@@ -269,11 +273,22 @@ export async function terminateDirectChild(
|
|
|
269
273
|
reportVisible(`failed to terminate child pid=${pid}`, report);
|
|
270
274
|
}
|
|
271
275
|
|
|
276
|
+
function gitConfig(cwd: string, args: string[]): string {
|
|
277
|
+
const env = { ...process.env };
|
|
278
|
+
for (const key of GIT_LOCATION_ENV_KEYS) delete env[key];
|
|
279
|
+
const result = spawnSync('git', args, { cwd, env, encoding: 'utf8' });
|
|
280
|
+
if (result.error || (result.status !== 0 && !(result.status === 1 && args.includes('--get-all') && !result.stderr))) {
|
|
281
|
+
throw new Error(`git ${args.join(' ')} failed: ${result.error?.message ?? result.stderr}`);
|
|
282
|
+
}
|
|
283
|
+
return result.stdout.trim();
|
|
284
|
+
}
|
|
285
|
+
|
|
272
286
|
export function prepareIsolatedTestEnv(sourceEnv: NodeJS.ProcessEnv, testRoot: string): NodeJS.ProcessEnv {
|
|
273
287
|
const env = Object.fromEntries(
|
|
274
288
|
Object.entries(sourceEnv).filter(([key]) => !isCredentialKey(key)
|
|
275
289
|
&& !HARNESS_TEST_ENV_KEYS.includes(key as typeof HARNESS_TEST_ENV_KEYS[number])
|
|
276
|
-
&& !EXECUTION_ORIGIN_ENV_KEYS.includes(key as typeof EXECUTION_ORIGIN_ENV_KEYS[number])
|
|
290
|
+
&& !EXECUTION_ORIGIN_ENV_KEYS.includes(key as typeof EXECUTION_ORIGIN_ENV_KEYS[number])
|
|
291
|
+
&& !GIT_LOCATION_ENV_KEYS.includes(key as typeof GIT_LOCATION_ENV_KEYS[number])),
|
|
277
292
|
);
|
|
278
293
|
env.HOME = testRoot;
|
|
279
294
|
// userConfigPath() warns whenever XDG_CONFIG_HOME is set, including when
|
|
@@ -392,6 +407,38 @@ export async function runDeterministicTests(deps: DeterministicRunnerDeps = {}):
|
|
|
392
407
|
const report = deps.report ?? console.error;
|
|
393
408
|
const spawn = deps.spawn ?? defaultSpawn;
|
|
394
409
|
const argv = deps.argv ?? process.argv.slice(2);
|
|
410
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
411
|
+
const config = deps.gitConfig ?? gitConfig;
|
|
412
|
+
const commonDir = config(cwd, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
|
|
413
|
+
const configFile = join(resolve(cwd, commonDir), 'config');
|
|
414
|
+
const bareArgs = ['config', '--file', configFile];
|
|
415
|
+
const readBare = () => config(cwd, [...bareArgs, '--get-all', 'core.bare']);
|
|
416
|
+
const bareBefore = readBare();
|
|
417
|
+
let bareChanged = false;
|
|
418
|
+
let bareChecked = false;
|
|
419
|
+
const checkBare = (): void => {
|
|
420
|
+
bareChecked = true;
|
|
421
|
+
let bareAfter = '<unreadable>';
|
|
422
|
+
const visible = (values: string) => values ? values.replaceAll('\n', ',') : '<unset>';
|
|
423
|
+
try {
|
|
424
|
+
bareAfter = readBare();
|
|
425
|
+
if (bareAfter === bareBefore) return;
|
|
426
|
+
bareChanged = true;
|
|
427
|
+
const originalValues = bareBefore ? bareBefore.split('\n') : [];
|
|
428
|
+
if (originalValues.length > 0) {
|
|
429
|
+
config(cwd, [...bareArgs, '--replace-all', 'core.bare', originalValues[0]!]);
|
|
430
|
+
for (const value of originalValues.slice(1)) config(cwd, [...bareArgs, '--add', 'core.bare', value]);
|
|
431
|
+
} else if (bareAfter) {
|
|
432
|
+
config(cwd, [...bareArgs, '--unset-all', 'core.bare']);
|
|
433
|
+
}
|
|
434
|
+
const restored = readBare();
|
|
435
|
+
if (restored !== bareBefore) throw new Error(`expected ${visible(bareBefore)}, got ${visible(restored)}`);
|
|
436
|
+
report(`[test-deterministic] core.bare changed ${visible(bareBefore)} -> ${visible(bareAfter)}; restored ${visible(bareBefore)}; failing run`);
|
|
437
|
+
} catch (error) {
|
|
438
|
+
bareChanged = true;
|
|
439
|
+
report(`[test-deterministic] core.bare changed ${visible(bareBefore)} -> ${visible(bareAfter)}; restore failed: ${formatError(error)}`);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
395
442
|
// ⛔ 사람이 «경로»를 직접 준 창에서는 아무것도 빼지 않는다 — 그 창은 「이것만 돌려라」다.
|
|
396
443
|
const explicitPaths = argv.some((arg) => !arg.startsWith('-'));
|
|
397
444
|
const cdpPatterns = explicitPaths ? [] : (deps.deriveCdpPatterns ?? deriveCdpTestPatterns)();
|
|
@@ -451,6 +498,7 @@ export async function runDeterministicTests(deps: DeterministicRunnerDeps = {}):
|
|
|
451
498
|
});
|
|
452
499
|
cleanupTemporaryRoot(testRoot, { rmSync: deps.rmSync, report });
|
|
453
500
|
rootCleanupAttempted = true;
|
|
501
|
+
checkBare();
|
|
454
502
|
gate?.dispose();
|
|
455
503
|
gate = undefined;
|
|
456
504
|
if (deps.killSelf) {
|
|
@@ -465,7 +513,8 @@ export async function runDeterministicTests(deps: DeterministicRunnerDeps = {}):
|
|
|
465
513
|
}
|
|
466
514
|
|
|
467
515
|
cleanupRoot();
|
|
468
|
-
|
|
516
|
+
checkBare();
|
|
517
|
+
return bareChanged ? 1 : (outcome.code ?? 1);
|
|
469
518
|
} catch (error) {
|
|
470
519
|
if (child) {
|
|
471
520
|
try {
|
|
@@ -484,6 +533,7 @@ export async function runDeterministicTests(deps: DeterministicRunnerDeps = {}):
|
|
|
484
533
|
} finally {
|
|
485
534
|
gate?.dispose();
|
|
486
535
|
cleanupRoot();
|
|
536
|
+
if (!bareChecked) checkBare();
|
|
487
537
|
}
|
|
488
538
|
}
|
|
489
539
|
|