elanous 0.2.27 → 0.2.28
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/{1454-a9c338f5bb556f42.js → 1454-29d1d773d5996a49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-07110da72c45e68f.js → page-24453f6a302816b4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d2d76d4a7eae91bd.js → page-6c131d30bb1cf871.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-fa46d4588e8442c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-e5a9302c105725a3.js → page-dc0772b11861af51.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-6a16a48d1aa6e91f.js → page-256ffd365d32e100.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e0de78ae7b99f01c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-39f82acf619cca23.js → page-4c741eb760f2f77c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-7c2c8f81b185f9c0.js → page-e9ba98f5f1440a21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-ae7aff07e32676bb.js → page-dafc901bb1b72b5f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-482c60c1bc540d0a.js → page-2f6f303fa114ec24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-7cb996c1aa08b6f5.js → layout-1634081c5272006c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/{page-623e08fe4477d606.js → page-ced0bcfc6542b3fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-db460cd8aecb8490.js → page-bb1f05cd51c38944.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-ff27902d0b11ef4f.js → page-646b50f63b6fe732.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-1917ea87ea71ab6b.js → page-f736324afa2117c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-25a302273b054fd7.js → page-6f131b51193a36f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-670510eda8b9443f.js → not-found-435f5e2faf02bc01.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-497e0eedf20e7890.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a8d28fdf995e16ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4bb7556e2e97a750.js → page-8966452228753c5e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-4eb96fc7241c2456.js → page-66a67eb19ebd976b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-0d64ff212dc55d03.js → page-c71267e7681da982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-7f096248993fc2d4.js → page-a21d1e1797172692.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-e47fc72049149a38.js → page-cb249e81ed339318.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-f7d808e31e005895.js → page-781cfc5114e097da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-5c2b24b5496dad73.js → page-478157214db6a023.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-76cdb531668b5191.js → page-86141f12cf090b7f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-7256c73bb1c36751.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-dab163b338e3b966.js → page-979d318a534e92da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-0260ffc7eda50db1.js → page-21050928a697ed7a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-99c4e6c7345a9156.js → page-601fb77340e63c9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-11bf9e5725a72c22.js +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/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/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/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +13 -13
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/implement-loop.yaml +38 -1
- package/graphs/steward/recipes.yaml +6 -0
- package/graphs/steward/release-day.yaml +23 -0
- package/graphs/steward/steward.yaml +9 -1
- package/graphs/work/code-pr.yaml +23 -0
- package/package.json +1 -1
- package/scripts/browser-observe-cycle.ts +76 -2
- package/scripts/ci-typecheck-changed.ts +50 -17
- package/scripts/detect-test-interference.ts +31 -1
- package/scripts/release-loop/docs-node.ts +51 -22
- package/scripts/release-loop/gate-node.ts +37 -4
- package/scripts/release-loop/node-verdict.ts +3 -1
- package/scripts/release-loop/publish-node.ts +3 -0
- package/scripts/release-loop/tui-node-checks.ts +5 -2
- package/scripts/skills/public-check.ts +5 -2
- package/scripts/test-deterministic.ts +520 -37
- package/scripts/ux-sim.ts +7 -1
- package/scripts/verify-kpack6-live.ts +230 -0
- package/src/acp/codex-plugins.ts +17 -0
- package/src/agent/task-notification.ts +2 -1
- package/src/agent-mission/in-agent-plugin.ts +8 -3
- package/src/agent-substrate/self-review-cli.ts +34 -10
- package/src/briefing/brief-items.ts +151 -35
- package/src/budget/llm-cost.ts +25 -2
- package/src/budget/usage-outcomes.ts +32 -0
- package/src/claims/claims-ledger.ts +23 -0
- package/src/claims/claims-render.ts +22 -2
- package/src/cli/brief-cli.ts +131 -10
- package/src/cli/claims-cli.ts +24 -4
- package/src/cli/msg-cli.ts +165 -9
- package/src/cli/plugin-cli.ts +11 -3
- package/src/cli/pr-cli.ts +80 -18
- package/src/cli/pr-land-refusal.ts +20 -0
- package/src/cli/release-cli.ts +2 -0
- package/src/cli/rfc-cli.ts +24 -0
- package/src/cli/tailscale-serve.ts +5 -8
- package/src/cli/tasks-cli.ts +393 -26
- package/src/cli/usage-cli.ts +9 -4
- package/src/dashboard/first-screen-band.ts +63 -13
- package/src/dashboard/index.ts +8 -3
- package/src/dashboard/skill-detection-runtime.ts +24 -4
- package/src/domains/morning-report.ts +3 -1
- package/src/github/pr-labels.ts +10 -4
- package/src/graph-runner/runner.ts +101 -14
- package/src/harness/harness-clean.ts +21 -3
- package/src/harness/harness-cli-command.ts +22 -6
- package/src/harness/harness-pod-dispatch.ts +11 -4
- package/src/hq/seats.ts +17 -3
- package/src/index.ts +34 -5
- package/src/intelligence-map/model-bench-scores.ts +102 -0
- package/src/loops/orchestrator/inbox.ts +73 -0
- package/src/loops/orchestrator/resync-shadow.ts +100 -12
- package/src/loops/orchestrator/tick.ts +73 -2
- package/src/maturity/feature-maturity.ts +1 -1
- package/src/msg/msg-sse-server.ts +141 -0
- package/src/msg/msg-store.ts +84 -14
- package/src/patents/ledger.ts +128 -0
- package/src/plugins/install/market-fetch.ts +32 -1
- package/src/pod-lease/admission.ts +116 -41
- package/src/release-loop/checklist.ts +11 -0
- package/src/release-loop/merged-pr-checklist.ts +24 -5
- package/src/release-loop/rubric.ts +70 -0
- package/src/rfc-registry/registry.ts +88 -0
- package/src/seat-dispatch/seat-request-ledger.ts +4 -0
- package/src/seat-loop/knowhow-lint.ts +233 -0
- package/src/seat-loop/seat-pack.ts +16 -4
- package/src/seat-loop/traffic-signal.ts +7 -0
- package/src/self-dev/ask-launch-flow.ts +48 -8
- package/src/self-dev/ask-path-candidates.ts +69 -14
- package/src/self-dev/decompose-proposal.ts +43 -1
- package/src/self-dev/graph-journey-nodes.ts +2 -0
- package/src/self-dev/launch-preflight-text.ts +5 -1
- package/src/self-dev/launch-preflight.ts +143 -10
- package/src/self-implement/gate-cli.ts +6 -2
- package/src/self-implement/goal-author.ts +2 -1
- package/src/self-implement/goal-new-paths.ts +4 -5
- package/src/self-implement/graph-authority.ts +17 -3
- package/src/self-implement/graph-yaml.ts +15 -7
- package/src/self-implement/headless-elanous-driver.ts +16 -1
- package/src/self-implement/host-regate.ts +44 -9
- package/src/self-implement/orchestrator.ts +3 -10
- package/src/self-implement/rework-policy.ts +80 -2
- package/src/self-implement/speed-claims.ts +145 -6
- package/src/skills/tools/persistent-grounding.ts +59 -4
- package/src/skills/tools/shell.ts +79 -1
- package/src/steward/rejudge-tick.ts +12 -1
- package/src/steward/release-day.ts +158 -0
- package/src/steward/resync-tick.ts +123 -0
- package/src/task-agent/actions.ts +1 -0
- package/src/task-agent/adopt-pr.ts +140 -0
- package/src/task-agent/board.ts +11 -1
- package/src/task-agent/card-evidence.ts +37 -5
- package/src/task-agent/cover.ts +84 -17
- package/src/task-agent/failure-notify.ts +61 -0
- package/src/task-agent/green-sweep.ts +170 -0
- package/src/task-agent/land-worktree.ts +52 -5
- package/src/task-agent/live-move-history.ts +1 -0
- package/src/task-agent/live-moves.ts +43 -12
- package/src/task-agent/parent-host.ts +1 -1
- package/src/task-agent/postland-card.ts +139 -0
- package/src/task-agent/rejudge-on-head.ts +106 -7
- package/src/task-agent/remote-rejudge-bind.ts +62 -0
- package/src/task-agent/repair-branch-cas.ts +182 -0
- package/src/task-agent/repair-cycle.ts +275 -0
- package/src/task-agent/repair-next-move.ts +31 -0
- package/src/task-agent/repair-review.ts +24 -0
- package/src/task-agent/repair-worker.ts +87 -0
- package/src/task-agent/review-result-capture.ts +2 -1
- package/src/task-agent/shadow.ts +13 -7
- package/src/task-agent/task-hand.ts +115 -6
- package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +70 -12
- package/src/telegram.ts +30 -1
- package/src/user-config.ts +8 -1
- package/src/ux-sim/fault-sqlite-busy.ts +50 -0
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-a1ce306364f08da9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-20662900d6b4b455.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f707a509515ac50c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-8397a5c6ca569bf4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-15c0a4095baddfba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d376e0c83edc041a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-08019efef111ed2e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-980ce6fe36ad95eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-0ab42e0f05823013.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +0 -1
- /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
|
@@ -4,7 +4,15 @@
|
|
|
4
4
|
// 기본 MCP 전송은 기존 McpClient 의 initialize + Mcp-Session-Id 수명주기를 탄다.
|
|
5
5
|
// 스케줄러 진입점 배선은 다음 조각의 몫이다. 이 파일은 호출 가능한 함수만 내보낸다.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import {
|
|
10
|
+
McpClient,
|
|
11
|
+
McpConnectionError,
|
|
12
|
+
McpServerError,
|
|
13
|
+
type McpHttpFetch,
|
|
14
|
+
} from '../src/mcp/client.js';
|
|
15
|
+
import { getElanousConfigDir } from '../src/elanous-config-dir.js';
|
|
8
16
|
import { debug } from '../src/debug/log.js';
|
|
9
17
|
|
|
10
18
|
export const ELANOUS_PWA_URL = 'http://127.0.0.1:31415/app';
|
|
@@ -21,6 +29,10 @@ export const BROWSER_OBSERVE_OK = 'ok';
|
|
|
21
29
|
export const BROWSER_OBSERVE_WRONG_SCREEN = 'wrong-screen';
|
|
22
30
|
/** 관측하지 못했다 */
|
|
23
31
|
export const BROWSER_OBSERVE_UNOBSERVED = 'unobserved';
|
|
32
|
+
/** `unobserved` 사유 — 루프백 owner 토큰을 못 읽어 Authorization 헤더를 못 붙였다 */
|
|
33
|
+
export const BROWSER_OBSERVE_REASON_AUTH_MISSING = 'auth-missing';
|
|
34
|
+
/** `unobserved` 사유 — 루프백 owner 토큰을 붙였는데 서버가 401 로 거부했다 */
|
|
35
|
+
export const BROWSER_OBSERVE_REASON_AUTH_REJECTED = 'auth-rejected';
|
|
24
36
|
|
|
25
37
|
export type BrowserObserveStatus =
|
|
26
38
|
| typeof BROWSER_OBSERVE_OK
|
|
@@ -50,6 +62,39 @@ export interface BrowserObserveResult extends BrowserObserveClassification {
|
|
|
50
62
|
request: BrowserObserveMcpRequest;
|
|
51
63
|
}
|
|
52
64
|
|
|
65
|
+
/** 루프백 owner 토큰을 어디서 읽는지 — `src/cli/mcp-reload.ts` 의 기존 꼴과 같은 자리다. */
|
|
66
|
+
export function browserObserveTokenPath(): string {
|
|
67
|
+
return join(getElanousConfigDir(), 'acp-token');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 루프백 ACP 토큰을 **읽기 전용**으로 얻는다(실패는 null).
|
|
72
|
+
* ⛔ `ensureAuthToken`(src/auth/acp-token.ts) 을 쓰지 않는다 — 그것은 «없으면 만든다».
|
|
73
|
+
* 이 조각은 새 토큰 생성 0: 파일이 없거나 비면 그대로 auth-missing 이다.
|
|
74
|
+
*/
|
|
75
|
+
export function readBrowserObserveToken(): string | null {
|
|
76
|
+
const tokenPath = browserObserveTokenPath();
|
|
77
|
+
try {
|
|
78
|
+
if (!existsSync(tokenPath)) return null;
|
|
79
|
+
const token = readFileSync(tokenPath, 'utf8').trim();
|
|
80
|
+
return token.length > 0 ? token : null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** MCP POST 전부에 `Authorization: Bearer <token>` 을 얹는 전송 래퍼. */
|
|
87
|
+
export function withBrowserObserveAuthorization(
|
|
88
|
+
fetchImpl: BrowserObserveHttpFetch,
|
|
89
|
+
token: string,
|
|
90
|
+
): BrowserObserveHttpFetch {
|
|
91
|
+
return (url, init) =>
|
|
92
|
+
fetchImpl(url, {
|
|
93
|
+
...init,
|
|
94
|
+
headers: { ...init.headers, authorization: `Bearer ${token}` },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
53
98
|
export type BrowserObserveMcpCall = (request: BrowserObserveMcpRequest) => Promise<unknown>;
|
|
54
99
|
export type BrowserObserveLog = (category: string, event: string, data?: unknown) => void;
|
|
55
100
|
export type BrowserObserveHttpFetch = McpHttpFetch;
|
|
@@ -58,6 +103,8 @@ export interface BrowserObserveDeps {
|
|
|
58
103
|
mcpCall?: BrowserObserveMcpCall;
|
|
59
104
|
fetch?: BrowserObserveHttpFetch;
|
|
60
105
|
log?: BrowserObserveLog;
|
|
106
|
+
/** 토큰 획득 주입 자리 — 시험이 파일 시스템 없이 결정성을 얻는다. 생략하면 `readBrowserObserveToken` */
|
|
107
|
+
readToken?: () => string | null;
|
|
61
108
|
}
|
|
62
109
|
|
|
63
110
|
/**
|
|
@@ -328,6 +375,15 @@ function emit(
|
|
|
328
375
|
}
|
|
329
376
|
|
|
330
377
|
function unobservedFromCaught(caught: unknown): BrowserObserveClassification {
|
|
378
|
+
// 루프백 토큰을 붙였는데 서버가 401 로 거부한 경우 — McpClient 가
|
|
379
|
+
// McpConnectionError('auth-required') 로 던진다. 긴 오류 문자열 대신 이유 한 칸으로 가른다.
|
|
380
|
+
if (caught instanceof McpConnectionError && caught.reason === 'auth-required') {
|
|
381
|
+
return {
|
|
382
|
+
status: BROWSER_OBSERVE_UNOBSERVED,
|
|
383
|
+
observedUrl: null,
|
|
384
|
+
reason: BROWSER_OBSERVE_REASON_AUTH_REJECTED,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
331
387
|
if (caught instanceof McpServerError) {
|
|
332
388
|
return {
|
|
333
389
|
status: BROWSER_OBSERVE_UNOBSERVED,
|
|
@@ -342,14 +398,32 @@ function unobservedFromCaught(caught: unknown): BrowserObserveClassification {
|
|
|
342
398
|
};
|
|
343
399
|
}
|
|
344
400
|
|
|
401
|
+
/** 기본 전송(McpClient) 에 루프백 토큰을 얹는다. 토큰이 없으면 null — 401 이 보장된 호출은 아예 안 한다. */
|
|
402
|
+
function defaultAuthorizedMcpCall(deps: BrowserObserveDeps): BrowserObserveMcpCall | null {
|
|
403
|
+
const token = deps.readToken ? deps.readToken() : readBrowserObserveToken();
|
|
404
|
+
if (token === null) return null;
|
|
405
|
+
const base: BrowserObserveHttpFetch = deps.fetch ?? ((url, init) => fetch(url, init));
|
|
406
|
+
const authorized = withBrowserObserveAuthorization(base, token);
|
|
407
|
+
return (request) => callAsideReplViaMcpClient(request, authorized);
|
|
408
|
+
}
|
|
409
|
+
|
|
345
410
|
/** MCP 로 브라우저에서 값을 가져와 공개 분류 함수에 넘긴다. 의존성 주입으로 실 데몬 없이 검증한다. */
|
|
346
411
|
export async function observeBrowserCycle(
|
|
347
412
|
deps: BrowserObserveDeps = {},
|
|
348
413
|
): Promise<BrowserObserveResult> {
|
|
349
414
|
if (!deps.log) await ensureBrowserObserveLogSink();
|
|
350
415
|
const log = deps.log ?? defaultLog;
|
|
351
|
-
const mcpCall = deps.mcpCall ?? ((request) => callAsideReplViaMcpClient(request, deps.fetch));
|
|
352
416
|
const request = buildBrowserObserveMcpRequest();
|
|
417
|
+
const mcpCall = deps.mcpCall ?? defaultAuthorizedMcpCall(deps);
|
|
418
|
+
if (mcpCall === null) {
|
|
419
|
+
const classified: BrowserObserveClassification = {
|
|
420
|
+
status: BROWSER_OBSERVE_UNOBSERVED,
|
|
421
|
+
observedUrl: null,
|
|
422
|
+
reason: BROWSER_OBSERVE_REASON_AUTH_MISSING,
|
|
423
|
+
};
|
|
424
|
+
emit(log, classified);
|
|
425
|
+
return { ...classified, request };
|
|
426
|
+
}
|
|
353
427
|
try {
|
|
354
428
|
const response = await mcpCall(request);
|
|
355
429
|
const rpcError = jsonRpcErrorMessage(response);
|
|
@@ -759,7 +759,7 @@ export function exportedLiteralUnionMembersInChanges(
|
|
|
759
759
|
pathsResult: ChangedPathResult = changedPathPairs(base),
|
|
760
760
|
readBase: GitFileReader = readGitFile,
|
|
761
761
|
readHead = (file: string) => readFileSync(file, 'utf8'),
|
|
762
|
-
sources?: RepositorySources,
|
|
762
|
+
sources?: RepositorySources | (() => RepositorySources),
|
|
763
763
|
): LiteralUnionMemberCheck {
|
|
764
764
|
if (pathsResult.failure) return { additions: [], failure: pathsResult.failure };
|
|
765
765
|
const additions: LiteralUnionAddition[] = [];
|
|
@@ -780,7 +780,7 @@ export function exportedLiteralUnionMembersInChanges(
|
|
|
780
780
|
}
|
|
781
781
|
if (removals.length === 0) return { additions };
|
|
782
782
|
try {
|
|
783
|
-
return { additions, removals: literalUnionReferencesInSource(removals, changed, sources ?? repositoryTypeScriptSources()) };
|
|
783
|
+
return { additions, removals: literalUnionReferencesInSource(removals, changed, typeof sources === 'function' ? sources() : sources ?? repositoryTypeScriptSources()) };
|
|
784
784
|
} catch (error: unknown) {
|
|
785
785
|
return { additions, failure: `저장소 리터럴 참조 탐색 실패: ${error instanceof Error ? error.message : String(error)}` };
|
|
786
786
|
}
|
|
@@ -873,6 +873,7 @@ export type GateIo = {
|
|
|
873
873
|
literalUnionMembers?: (changed: ReadonlySet<string>, base: string) => LiteralUnionMember[] | LiteralUnionMemberCheck;
|
|
874
874
|
readBaselineDiagnostics?: (base: string, command: string) => BaselineDiagnostics;
|
|
875
875
|
runTscCmd: (cmd: string) => TscRun;
|
|
876
|
+
resolveTscScript?: (cwd: string) => string | undefined;
|
|
876
877
|
/** Landing tree. Git, existsSync, tsc, and the test baseline are measured here — not in process.cwd(). */
|
|
877
878
|
cwd?: string;
|
|
878
879
|
/** Explicit base revision. Same rule as TSC_BASE_REF, without reading the process environment. */
|
|
@@ -982,16 +983,30 @@ export function planRootCheck(input: {
|
|
|
982
983
|
if (roots.length > TSC_GATE_ROOT_LIMIT) return { mode: 'full', fallbackReason: `루트 ${roots.length}개 > 상한 ${TSC_GATE_ROOT_LIMIT}`, roots: roots.length };
|
|
983
984
|
return { mode: 'affected', roots, changedCount: gateChanged.size };
|
|
984
985
|
}
|
|
985
|
-
/**
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
986
|
+
/** Target checkout first, then the installed CLI's top-level and nested dependencies. */
|
|
987
|
+
export function resolveTscScript(cwd: string, scriptsDir = import.meta.dir, present: (path: string) => boolean = existsSync): string | undefined {
|
|
988
|
+
return [
|
|
989
|
+
join(cwd, 'node_modules/typescript/bin/tsc'),
|
|
990
|
+
join(scriptsDir, '../../typescript/bin/tsc'),
|
|
991
|
+
join(scriptsDir, '../node_modules/typescript/bin/tsc'),
|
|
992
|
+
].find(present);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/** The installed compiler cannot resolve declarations from a checkout without its own dependencies. */
|
|
996
|
+
export function missingCheckoutTypeDependencies(cwd: string, needsPwa = false, present: (path: string) => boolean = existsSync): string[] {
|
|
997
|
+
const missing: string[] = [];
|
|
998
|
+
if (!present(join(cwd, 'node_modules'))) missing.push('node_modules');
|
|
999
|
+
if (!present(join(cwd, 'node_modules/bun-types'))) missing.push('bun-types');
|
|
1000
|
+
if (needsPwa && !present(join(cwd, 'apps/pwa/node_modules'))) missing.push('apps/pwa/node_modules');
|
|
1001
|
+
return missing;
|
|
1002
|
+
}
|
|
992
1003
|
|
|
993
1004
|
export function runGate(io: Partial<GateIo> = {}): void {
|
|
994
1005
|
const cwd = io.cwd ?? process.cwd();
|
|
1006
|
+
const targetGit: GitRunner = (command, args, options) => execFileSync(command, args, { ...options, cwd, env: gitEnv() });
|
|
1007
|
+
const targetPaths = (base: string) => changedPathPairs(base, targetGit);
|
|
1008
|
+
const targetFile = (ref: string, path: string) => readGitFile(ref, path, targetGit);
|
|
1009
|
+
const targetSource = (file: string) => readFileSync(join(cwd, file), 'utf8');
|
|
995
1010
|
const defaultChanged = () => changedTsFiles((cmd) => sh(cmd, cwd), process.env, cwd, io.baseRef);
|
|
996
1011
|
const defaultRunTsc = (cmd: string) => runTsc(cmd, ((command, options) => execSync(command, { ...options, cwd, env: tscExecEnv(options?.env) })) as typeof execSync);
|
|
997
1012
|
const { changedTsFiles: getChanged = defaultChanged, runTscCmd = defaultRunTsc, log = (message: string) => console.log(message), warn = (message: string) => console.warn(message), error = (message: string) => console.error(message), exit = ((code: number) => process.exit(code)) as GateIo['exit'] } = io;
|
|
@@ -999,10 +1014,10 @@ export function runGate(io: Partial<GateIo> = {}): void {
|
|
|
999
1014
|
// Set 주입은 기존 테스트 심이며 기준 revision이 없다. 실제 ChangedTsFiles 경로만 Git 관측을 요구한다.
|
|
1000
1015
|
const requiredExportFields = io.requiredExportFields ?? ((changed: ReadonlySet<string>, base: string) => base === 'injected test seam'
|
|
1001
1016
|
? { fields: [], removedSymbols: [], parameterIncreases: [] }
|
|
1002
|
-
: requiredExportFieldsInChanges(changed, base));
|
|
1017
|
+
: requiredExportFieldsInChanges(changed, base, targetPaths(base), targetFile, targetSource));
|
|
1003
1018
|
const literalUnionMembers = io.literalUnionMembers ?? ((changed: ReadonlySet<string>, base: string) => base === 'injected test seam'
|
|
1004
1019
|
? { additions: [] }
|
|
1005
|
-
: exportedLiteralUnionMembersInChanges(changed, base));
|
|
1020
|
+
: exportedLiteralUnionMembersInChanges(changed, base, targetPaths(base), targetFile, targetSource, () => repositoryTypeScriptSources(cwd)));
|
|
1006
1021
|
const changedResult = getChanged();
|
|
1007
1022
|
const { files: changed, base, baseSource, head, diffFiles, warnings = [], failures = [] } = changedResult instanceof Set
|
|
1008
1023
|
? { files: changedResult, base: 'injected test seam', baseSource: 'injected test seam', head: 'injected test seam', diffFiles: new Set<string>(), warnings: [], failures: [] }
|
|
@@ -1099,15 +1114,33 @@ export function runGate(io: Partial<GateIo> = {}): void {
|
|
|
1099
1114
|
}, { level: 'info' });
|
|
1100
1115
|
log(`[tsc-gate] root 설정 범위 ${rootScope.size}개 관측 (apps/pwa/** 제외).`);
|
|
1101
1116
|
if (runsPwa) log(`[tsc-gate] PWA 설정 범위 ${pwaScope.size}개 관측.`);
|
|
1102
|
-
//
|
|
1103
|
-
//
|
|
1104
|
-
//
|
|
1105
|
-
|
|
1106
|
-
|
|
1117
|
+
// K-OP-039: PWA checks without app dependencies produce fake type errors.
|
|
1118
|
+
// The target checkout's declarations are required even when a compiler is
|
|
1119
|
+
// supplied by the installed CLI or injected by another gate caller.
|
|
1120
|
+
const missing = missingCheckoutTypeDependencies(cwd, runsPwa || wholeRepository);
|
|
1121
|
+
if (missing.length > 0) {
|
|
1122
|
+
const quotedCwd = `'${cwd.replaceAll("'", "'\\''")}'`;
|
|
1123
|
+
const install = `cd ${quotedCwd} && bun install --frozen-lockfile${missing.includes('apps/pwa/node_modules') ? ' && (cd apps/pwa && bun install --frozen-lockfile)' : ''}`;
|
|
1124
|
+
error(`[tsc-gate] ⛔ 대상 트리 타입 의존성 없음: ${missing.join(', ')} — ${install}`);
|
|
1125
|
+
error('[tsc-gate] FAIL — 대상 의존성을 확인하지 못했다(설치본 tsc 존재 ≠ 대상 타입 검사 가능).');
|
|
1126
|
+
exit(1);
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
// The .bin shim uses `#!/usr/bin/env node`. A LaunchAgent can have Bun but no
|
|
1130
|
+
// node on PATH, so invoke the installed compiler with this process's absolute
|
|
1131
|
+
// runtime instead. A missing dependency must fail the gate, never fetch via npx.
|
|
1132
|
+
// Preserve the target checkout's compiler version when available. The
|
|
1133
|
+
// installed CLI may resolve TypeScript from a sibling top-level node_modules
|
|
1134
|
+
// or its own nested node_modules; verify each path before starting a child.
|
|
1135
|
+
const tscScript = (io.resolveTscScript ?? resolveTscScript)(cwd);
|
|
1136
|
+
if (!tscScript) {
|
|
1137
|
+
error('[tsc-gate] ⛔ TypeScript 실행 파일 없음 — 대상 트리 또는 설치본에 bun install --frozen-lockfile 뒤 재실행하십시오.');
|
|
1138
|
+
error('[tsc-gate] FAIL — tsc 를 실행하지 못했다(의존성 없음 ≠ 진단 0건).');
|
|
1107
1139
|
exit(1);
|
|
1108
1140
|
return;
|
|
1109
1141
|
}
|
|
1110
|
-
const
|
|
1142
|
+
const shellArg = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
1143
|
+
const tscBin = `${shellArg(process.execPath)} ${shellArg(tscScript)}`;
|
|
1111
1144
|
const baseline = readTestTypecheckBaseline(cwd);
|
|
1112
1145
|
const rootCommand = `${tscBin} --noEmit -p ${TYPECHECK_GATE_CONFIG}`;
|
|
1113
1146
|
const pwaCommand = `${tscBin} --noEmit -p apps/pwa/tsconfig.json`;
|
|
@@ -26,6 +26,9 @@ export type MeasuredRun = {
|
|
|
26
26
|
fail: number | null;
|
|
27
27
|
status: 'measured' | 'unmeasurable';
|
|
28
28
|
reason?: string;
|
|
29
|
+
/** Names observed in Bun's failed-test output. Absent names must not be inferred from the selected files. */
|
|
30
|
+
failedFiles?: string[];
|
|
31
|
+
failedTests?: string[];
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
export type InterferenceReport = {
|
|
@@ -61,6 +64,32 @@ export function parseFailureCount(output: string): number | null {
|
|
|
61
64
|
return Number(failures[0]![1]);
|
|
62
65
|
}
|
|
63
66
|
|
|
67
|
+
/** Bun prints a file heading before its `(fail) name` lines. Only retain names actually printed by that run. */
|
|
68
|
+
export function parseFailedTestNames(output: string): { files: string[]; tests: string[] } {
|
|
69
|
+
const files = new Set<string>();
|
|
70
|
+
const tests = new Set<string>();
|
|
71
|
+
let currentFile: string | undefined;
|
|
72
|
+
for (const raw of output.replace(/\x1b\[[0-9;]*m/g, '').split(/\r?\n/)) {
|
|
73
|
+
const line = raw.trim();
|
|
74
|
+
if (/^(?:\d+\s+(?:pass|fail|skip)|Ran\s+\d+\s+tests?|Bun test v)/i.test(line)) { currentFile = undefined; continue; }
|
|
75
|
+
const failure = /^\(fail\)\s+(.+?)(?:\s+\[[^\]]+\])?$/.exec(line);
|
|
76
|
+
if (failure) {
|
|
77
|
+
tests.add(failure[1]!.trim());
|
|
78
|
+
if (currentFile) files.add(currentFile);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const heading = /^(.+):$/.exec(line);
|
|
82
|
+
if (heading) currentFile = /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(heading[1]!) ? heading[1] : undefined;
|
|
83
|
+
}
|
|
84
|
+
return { files: [...files], tests: [...tests] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function failedNamesFromStreams(result: RunnerResult): { files: string[]; tests: string[] } {
|
|
88
|
+
const stdout = parseFailedTestNames(result.stdout);
|
|
89
|
+
const stderr = parseFailedTestNames(result.stderr);
|
|
90
|
+
return { files: [...new Set([...stdout.files, ...stderr.files])], tests: [...new Set([...stdout.tests, ...stderr.tests])] };
|
|
91
|
+
}
|
|
92
|
+
|
|
64
93
|
async function measure(files: readonly string[], runner: TestRunner): Promise<MeasuredRun> {
|
|
65
94
|
try {
|
|
66
95
|
const result = await runner(files);
|
|
@@ -68,9 +97,10 @@ async function measure(files: readonly string[], runner: TestRunner): Promise<Me
|
|
|
68
97
|
return { files: [...files], fail: null, status: 'unmeasurable', reason: result.signal ? `runner terminated by ${result.signal}` : 'runner exit code unavailable' };
|
|
69
98
|
}
|
|
70
99
|
const fail = parseFailureCount(`${result.stdout}${result.stderr}`);
|
|
100
|
+
const names = fail !== null && fail > 0 ? failedNamesFromStreams(result) : undefined;
|
|
71
101
|
return fail === null
|
|
72
102
|
? { files: [...files], fail: null, status: 'unmeasurable', reason: 'test summary unavailable, ambiguous, or ran zero files' }
|
|
73
|
-
: { files: [...files], fail, status: 'measured' };
|
|
103
|
+
: { files: [...files], fail, status: 'measured', ...(names?.files.length ? { failedFiles: names.files } : {}), ...(names?.tests.length ? { failedTests: names.tests } : {}) };
|
|
74
104
|
} catch (error) {
|
|
75
105
|
return { files: [...files], fail: null, status: 'unmeasurable', reason: error instanceof Error ? error.message : String(error) };
|
|
76
106
|
}
|
|
@@ -130,7 +130,11 @@ export function runDocs(
|
|
|
130
130
|
const SHA = /^[0-9a-f]{40}$/i;
|
|
131
131
|
const PREFETCH_DEADLINE_MS = 15 * 60_000;
|
|
132
132
|
type Cut = { version: string; commit: string; manifestHash: string };
|
|
133
|
-
|
|
133
|
+
// Two separately published cache files: a finished TUI must survive a docs failure or delay,
|
|
134
|
+
// so it never lives inside the same file as the docs result (should-fix ②).
|
|
135
|
+
type TuiCache = Cut & { tui: Record<string, unknown> };
|
|
136
|
+
type DocsCache = Cut & { main: string; tree: string; docs: DocsResult };
|
|
137
|
+
type PrefetchCache = DocsCache & { tui: Record<string, unknown> | null };
|
|
134
138
|
type Runner = (command: string, args: string[], cwd?: string, timeout?: number, env?: NodeJS.ProcessEnv) => string;
|
|
135
139
|
export const commandRunner: Runner = (command, args, cwd, timeout, env) => {
|
|
136
140
|
const output = spawnSync(command, args, { cwd, env: env ?? process.env, timeout: timeout ?? 15 * 60_000, encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 });
|
|
@@ -162,11 +166,27 @@ function claimPrefetch(dir: string, owner: 'worker' | 'docs'): boolean {
|
|
|
162
166
|
function cacheFile(dir: string, cut: Cut): string {
|
|
163
167
|
return join(dir, `${cut.commit}-${cut.manifestHash}.json`);
|
|
164
168
|
}
|
|
165
|
-
function
|
|
169
|
+
function tuiCacheFile(dir: string, cut: Cut): string {
|
|
170
|
+
return join(dir, `${cut.commit}-${cut.manifestHash}.tui.json`);
|
|
171
|
+
}
|
|
172
|
+
function loadTuiCache(dir: string, cut: Cut): TuiCache | null {
|
|
173
|
+
try {
|
|
174
|
+
const value = JSON.parse(readFileSync(tuiCacheFile(dir, cut), 'utf8')) as TuiCache;
|
|
175
|
+
if (value.version !== cut.version || value.commit !== cut.commit || value.manifestHash !== cut.manifestHash) return null;
|
|
176
|
+
return value;
|
|
177
|
+
} catch { return null; }
|
|
178
|
+
}
|
|
179
|
+
/** Publish `data` to `path` atomically (temp + rename), so a reader never sees a partial file. */
|
|
180
|
+
function publish(path: string, data: unknown): void {
|
|
181
|
+
const staged = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
182
|
+
writeFileSync(staged, JSON.stringify(data) + '\n');
|
|
183
|
+
try { renameSync(staged, path); } catch (error) { rmSync(staged, { force: true }); throw error; }
|
|
184
|
+
}
|
|
185
|
+
function loadCache(dir: string, cut: Cut): DocsCache | null {
|
|
166
186
|
try {
|
|
167
|
-
const value = JSON.parse(readFileSync(cacheFile(dir, cut), 'utf8')) as
|
|
187
|
+
const value = JSON.parse(readFileSync(cacheFile(dir, cut), 'utf8')) as DocsCache;
|
|
168
188
|
if (value.version !== cut.version || value.commit !== cut.commit || value.manifestHash !== cut.manifestHash
|
|
169
|
-
|| !SHA.test(value.main) || value.tree !==
|
|
189
|
+
|| !SHA.test(value.main) || dirname(value.tree) !== dir || !/^tree-[0-9a-f-]{36}$/.test(basename(value.tree)) || !existsSync(value.tree)
|
|
170
190
|
|| value.docs?.outcome !== 'ok' || value.docs.version !== cut.version
|
|
171
191
|
|| value.docs.notes !== join(value.tree, 'release/public/docs/releases', `${cut.version}.md`)
|
|
172
192
|
|| value.docs.pages !== join(value.tree, 'website/pages.json')
|
|
@@ -191,21 +211,34 @@ export function stagePrefetch(context: GraphContext, env: NodeJS.ProcessEnv = pr
|
|
|
191
211
|
const cut = cutFor(context, stateRoot);
|
|
192
212
|
mkdirSync(dir, { recursive: true });
|
|
193
213
|
const already = loadCache(dir, cut);
|
|
194
|
-
if (already) return already;
|
|
195
|
-
|
|
214
|
+
if (already) return { ...already, tui: loadTuiCache(dir, cut)?.tui ?? null };
|
|
215
|
+
// An attempt gets its own path. A failed add can only remove a tree belonging to this attempt,
|
|
216
|
+
// even when an earlier cut or a concurrent worker has a checkout in the same run directory.
|
|
217
|
+
const tree = join(dir, `tree-${randomUUID()}`);
|
|
196
218
|
let added = false;
|
|
219
|
+
// `git worktree add` can fail AFTER creating the tree directory (e.g. a failed checkout): clean up
|
|
220
|
+
// whenever the add succeeded OR the tree is on disk, so a retry of this run is never blocked (should-fix ①).
|
|
221
|
+
const removeTree = () => {
|
|
222
|
+
if (!(added || existsSync(tree))) return;
|
|
223
|
+
try { runner('git', ['worktree', 'remove', '--force', tree], repo, 60_000); } catch { /* preserve original failure */ }
|
|
224
|
+
// A remove that failed (or could not unregister the tree) must still leave no blocking directory behind.
|
|
225
|
+
if (existsSync(tree)) rmSync(tree, { recursive: true, force: true });
|
|
226
|
+
};
|
|
197
227
|
try {
|
|
198
228
|
const main = run('git', ['rev-parse', '--verify', 'origin/main^{commit}'], repo);
|
|
199
229
|
if (!SHA.test(main)) throw new Error('invalid origin/main commit');
|
|
200
|
-
|
|
230
|
+
const cachedTui = loadTuiCache(dir, cut);
|
|
231
|
+
// TUI runs against the cut, on its own isolated session, before touching the docs checkout.
|
|
201
232
|
// tui-sim-node.ts prints the RAW result (no `outcome`); judge it the way node-verdict.ts tui would before reuse.
|
|
202
|
-
|
|
233
|
+
// Publish the reusable TUI in its OWN file the moment it is confirmed — a later docs failure or
|
|
234
|
+
// delay must not throw away a finished TUI (should-fix ②).
|
|
235
|
+
const tui = cachedTui ? cachedTui.tui : reusableTui(withTuiOutcome(0, deps.tui ? deps.tui(cut.commit) : (() => {
|
|
203
236
|
const tuiRun = spawnSync('bun', ['scripts/release-loop/tui-sim-node.ts', '--commit', cut.commit, '--json'], {
|
|
204
237
|
cwd: repo, timeout: Math.min(6 * 60_000, left()), encoding: 'utf8', maxBuffer: 16 * 1024 * 1024,
|
|
205
238
|
});
|
|
206
239
|
return tuiRun.status === 0 ? lastResult({ status: tuiRun.status, stdout: tuiRun.stdout ?? '', stderr: tuiRun.stderr ?? '' }) ?? null : null;
|
|
207
|
-
})();
|
|
208
|
-
|
|
240
|
+
})()), cut.commit);
|
|
241
|
+
if (tui && !cachedTui) publish(tuiCacheFile(dir, cut), { ...cut, tui });
|
|
209
242
|
run('git', ['worktree', 'add', '--detach', tree, main], repo);
|
|
210
243
|
added = true;
|
|
211
244
|
run('bun', ['install', '--frozen-lockfile'], tree);
|
|
@@ -220,19 +253,14 @@ export function stagePrefetch(context: GraphContext, env: NodeJS.ProcessEnv = pr
|
|
|
220
253
|
const finalCut = cutFor(context, stateRoot);
|
|
221
254
|
if (finalCut.manifestHash !== cut.manifestHash) throw new Error('manifest changed during prefetch');
|
|
222
255
|
const result: PrefetchCache = { ...cut, main, tree, docs, tui };
|
|
223
|
-
const
|
|
224
|
-
const staged = `${final}.${process.pid}.${randomUUID()}.tmp`;
|
|
225
|
-
writeFileSync(staged, JSON.stringify(result) + '\n');
|
|
256
|
+
const docsCache: DocsCache = { ...cut, main, tree, docs };
|
|
226
257
|
// Publication handshake: whoever creates `claim` first (O_EXCL) wins. The docs node claims before it reads,
|
|
227
258
|
// so a worker that loses can never publish a cache that a docs attempt would consume.
|
|
228
|
-
if (!claimPrefetch(dir, 'worker'))
|
|
229
|
-
|
|
230
|
-
throw new Error('docs node already ran; late prefetch is discarded');
|
|
231
|
-
}
|
|
232
|
-
renameSync(staged, final);
|
|
259
|
+
if (!claimPrefetch(dir, 'worker')) throw new Error('docs node already ran; late prefetch is discarded');
|
|
260
|
+
publish(cacheFile(dir, cut), docsCache);
|
|
233
261
|
return result;
|
|
234
262
|
} catch (error) {
|
|
235
|
-
|
|
263
|
+
removeTree();
|
|
236
264
|
throw error;
|
|
237
265
|
}
|
|
238
266
|
}
|
|
@@ -247,9 +275,10 @@ export function joinTui(context: GraphContext, env: NodeJS.ProcessEnv, repo: str
|
|
|
247
275
|
}, stateRoot = effectiveInstanceRoot()): Record<string, unknown> {
|
|
248
276
|
if (context.outputs.upgrade?.outcome !== 'ok') throw new Error('upgrade did not pass');
|
|
249
277
|
// Any failure to key the cache (no manifest, cutoff mismatch, no run dir) is a cache miss → original TUI path.
|
|
278
|
+
// The TUI cache file is independent of docs (should-fix ②): a docs failure or delay never blocks this read.
|
|
250
279
|
let cut: Cut | null = null;
|
|
251
|
-
let cached:
|
|
252
|
-
try { cut = cutFor(context, stateRoot); cached =
|
|
280
|
+
let cached: TuiCache | null = null;
|
|
281
|
+
try { cut = cutFor(context, stateRoot); cached = loadTuiCache(runCacheDir(env), cut); } catch { /* cache miss */ }
|
|
253
282
|
const reused = cut && cached?.tui && reusableTui(cached.tui, cut.commit);
|
|
254
283
|
if (reused) return { ...reused, outcome: 'ok', summary: `tui ${reused.verdict}` };
|
|
255
284
|
// Preserve node-verdict.ts's exit convention and last-JSON output on a cache miss.
|
|
@@ -287,7 +316,7 @@ export function graphDocs(context: GraphContext, env: NodeJS.ProcessEnv, repo: s
|
|
|
287
316
|
try {
|
|
288
317
|
// No fetch (nor any write to remote) until every gate is verified.
|
|
289
318
|
for (const node of ['gate', 'prepare', 'upgrade', 'tui']) if (context.outputs[node]?.outcome !== 'ok') throw new Error(`${node} did not pass`);
|
|
290
|
-
let cached:
|
|
319
|
+
let cached: DocsCache | null = null;
|
|
291
320
|
// A cache key that cannot be computed is a miss — the original fresh-worktree path runs.
|
|
292
321
|
if (mayConsume && cacheDir) { try { const cut = cutFor(context, stateRoot); cached = loadCache(cacheDir, cut); } catch { cached = null; } }
|
|
293
322
|
const branch = `release-docs/${version}`;
|
|
@@ -117,6 +117,8 @@ export interface GateResult {
|
|
|
117
117
|
const sha = /^[0-9a-f]{7,40}$/i;
|
|
118
118
|
const versionPattern = /^\d+\.\d+\.\d+(?:-(?:rc|alpha|beta)\.\d+)?$/;
|
|
119
119
|
const failureFile = (root: string, version: string) => join(root, 'release', version, 'gate-failures.json');
|
|
120
|
+
/** Judged identities live beside the machine release ledger; gate-failures.json keeps its raw baseline schema. */
|
|
121
|
+
const verdictFile = (root: string, version: string) => join(root, 'release', version, 'gate-verdict.json');
|
|
120
122
|
const quote = (s: string) => `'${s.replaceAll("'", "'\\''")}'`;
|
|
121
123
|
const defaultRemoteMirror = '~/mirror/elanous-agent.git';
|
|
122
124
|
|
|
@@ -1431,6 +1433,8 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
1431
1433
|
};
|
|
1432
1434
|
const repo = resolve(opts.repo ?? process.cwd());
|
|
1433
1435
|
const result: GateResult = { outcome: 'error', commit: opts.commit, introduced: [], preexisting: 0, fixed: 0, knownEnv: 0, knownEnvCleared: [], stalledEnv: [], rcNonzeroNoFailures: [], durationMs: 0 };
|
|
1436
|
+
const preexistingIds: string[] = [];
|
|
1437
|
+
let knownEnvIds: string[] = [];
|
|
1434
1438
|
let work: string | undefined;
|
|
1435
1439
|
let baseSnapshot = false;
|
|
1436
1440
|
let cutFailures: SweepFailures | undefined;
|
|
@@ -1598,11 +1602,13 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
1598
1602
|
const baselineIds = [...baseline.failures, ...baseline.errors];
|
|
1599
1603
|
const countedCut = splitKnownEnv(cutIds, known);
|
|
1600
1604
|
const countedBaseline = splitKnownEnv(baselineIds, known);
|
|
1601
|
-
|
|
1605
|
+
knownEnvIds = countedCut.knownEnv;
|
|
1606
|
+
result.knownEnv = knownEnvIds.length;
|
|
1602
1607
|
result.knownEnvCleared = [...new Set(cutRun.passedIds ?? [])].filter((id) => known.has(id) && !cutIds.includes(id) && !cutRun.stalledEnv?.some(({ file }) => fileOf(id) === file)).sort();
|
|
1603
1608
|
const diff = diffFailures(countedCut.counted, countedBaseline.counted);
|
|
1604
1609
|
result.fixed = result.baselineDeferred ? null : diff.fixed.length;
|
|
1605
|
-
|
|
1610
|
+
preexistingIds.push(...diff.common);
|
|
1611
|
+
result.preexisting = preexistingIds.length;
|
|
1606
1612
|
// GATE-BASELINE-CACHE — only the files whose new failures reproduce in isolation reach the baseline, and each
|
|
1607
1613
|
// (commit, file, host) is run at most once across gates: this gate's cut results also seed the next gate's baseline.
|
|
1608
1614
|
const cacheHost = opts.remote ?? 'local';
|
|
@@ -1703,10 +1709,11 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
1703
1709
|
debug.log('release-loop.gate', 'isolated-rc-nonzero-no-failures', { file, label: 'cut isolated', rc: rcOnly.rc, pass: rcOnly.pass, baseline });
|
|
1704
1710
|
}
|
|
1705
1711
|
for (const id of candidates) {
|
|
1706
|
-
if (oldFailures.has(id))
|
|
1712
|
+
if (oldFailures.has(id)) preexistingIds.push(id);
|
|
1707
1713
|
else result.introduced.push(id);
|
|
1708
1714
|
}
|
|
1709
1715
|
}
|
|
1716
|
+
result.preexisting = preexistingIds.length;
|
|
1710
1717
|
result.baselineCache = cacheStats;
|
|
1711
1718
|
debug.log('release.gate', 'baseline-cache', { hits: cacheStats.hits, misses: cacheStats.misses, commit: baseSha });
|
|
1712
1719
|
result.outcome = result.introduced.length ? 'regression' : 'ok';
|
|
@@ -1751,7 +1758,33 @@ export async function judgeGate(opts: GateOptions, runner: GateRunner = createGa
|
|
|
1751
1758
|
result.error = `baseline persistence: ${String(error)}`;
|
|
1752
1759
|
}
|
|
1753
1760
|
}
|
|
1754
|
-
if (result.outcome !== 'error')
|
|
1761
|
+
if (result.outcome !== 'error') {
|
|
1762
|
+
const ids = {
|
|
1763
|
+
introduced: [...result.introduced].sort(),
|
|
1764
|
+
knownEnv: [...knownEnvIds].sort(),
|
|
1765
|
+
preexisting: [...preexistingIds].sort(),
|
|
1766
|
+
loadFlaky: [...(result.loadFlaky ?? [])].sort(),
|
|
1767
|
+
};
|
|
1768
|
+
try {
|
|
1769
|
+
const path = verdictFile(ledger, opts.version);
|
|
1770
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1771
|
+
const temp = join(dirname(path), `.gate-verdict-${process.pid}-${randomUUID()}.tmp`);
|
|
1772
|
+
try {
|
|
1773
|
+
writeFileSync(temp, JSON.stringify({ version: opts.version, commit: opts.commit, outcome: result.outcome, ...ids }, null, 2) + '\n', { mode: 0o600 });
|
|
1774
|
+
chmodSync(temp, 0o600);
|
|
1775
|
+
renameSync(temp, path);
|
|
1776
|
+
} finally { if (existsSync(temp)) rmSync(temp); }
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
result.outcome = 'error';
|
|
1779
|
+
result.error = `verdict persistence: ${String(error)}`;
|
|
1780
|
+
}
|
|
1781
|
+
if (result.outcome !== 'error') debug.log('release-loop.gate', 'judged', {
|
|
1782
|
+
version: opts.version, commit: opts.commit,
|
|
1783
|
+
introduced: result.introduced.length, preexisting: result.preexisting,
|
|
1784
|
+
knownEnv: result.knownEnv, loadFlaky: ids.loadFlaky.length,
|
|
1785
|
+
introducedIds: ids.introduced, knownEnvIds: ids.knownEnv, preexistingIds: ids.preexisting, loadFlakyIds: ids.loadFlaky,
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1755
1788
|
// GATE-LIVE-OBS-RC-WORDING ⓒ — the shards board says «판정 전» until this lands; then «새 실패 N» only (UX · gate-shards.ts).
|
|
1756
1789
|
// Same ledger as the board's own writes (sweep's shardsFile). A write failure is logged and never changes the verdict.
|
|
1757
1790
|
if (result.outcome !== 'error' && opts.pod) {
|
|
@@ -7,6 +7,7 @@ import { debug } from '../../src/debug/log.js';
|
|
|
7
7
|
export type NodeOutcome = 'ok' | 'fail' | 'error';
|
|
8
8
|
export type NodeVerdict = 'pass' | 'flaky' | 'fail';
|
|
9
9
|
export interface GraphContext {
|
|
10
|
+
nodeId?: string;
|
|
10
11
|
input: { version: string; previousVersion: string; [key: string]: unknown };
|
|
11
12
|
outputs: Record<string, Record<string, unknown>>;
|
|
12
13
|
}
|
|
@@ -20,7 +21,8 @@ export function readGraphContext(env: NodeJS.ProcessEnv = process.env): GraphCon
|
|
|
20
21
|
const input = context.input as Record<string, unknown> | undefined;
|
|
21
22
|
if (!input || typeof input.version !== 'string' || !isReleaseVersion(input.version)
|
|
22
23
|
|| typeof input.previousVersion !== 'string' || !/^\d+\.\d+\.\d+$/.test(input.previousVersion)) throw new Error('version and previousVersion required');
|
|
23
|
-
return {
|
|
24
|
+
return { ...(typeof context.nodeId === 'string' ? { nodeId: context.nodeId } : {}),
|
|
25
|
+
input: input as GraphContext['input'], outputs: context.outputs && typeof context.outputs === 'object' ? context.outputs as GraphContext['outputs'] : {} };
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export function nodeOutput(context: GraphContext, node: string, key: string): string {
|
|
@@ -48,6 +48,9 @@ export function waitForAssets(run: CommandRunner, repo: string, tag: string, nam
|
|
|
48
48
|
export function runPublish(run: CommandRunner = runCommand, prodFreezeRoot?: string,
|
|
49
49
|
clock: { now: () => number; sleep: (ms: number) => void } = { now: Date.now, sleep: Bun.sleepSync }) {
|
|
50
50
|
const context = readGraphContext();
|
|
51
|
+
// A resumed graph once invoked this irreversible node at npm-publish, docs-land, and failed.
|
|
52
|
+
// The runner's context is the authority: only the publish visit may publish.
|
|
53
|
+
if (context.nodeId !== 'publish') throw new Error(`publish node context mismatch: expected publish, got ${context.nodeId ?? 'missing'}`);
|
|
51
54
|
const version = context.input.version;
|
|
52
55
|
// A measured automatic decision or the existing human approval must authorize publication.
|
|
53
56
|
const auto = context.outputs['auto-approve'];
|
|
@@ -11,6 +11,9 @@ export interface TuiScreens {
|
|
|
11
11
|
export interface TuiCheck { id: string; pass: boolean; detail: string }
|
|
12
12
|
|
|
13
13
|
const ERROR_TEXT = /\b(TypeError|ReferenceError|SyntaxError|Unhandled|panic|Segmentation fault)\b|Error: /;
|
|
14
|
+
// The startup band can say "Commands — /help lists every command" (#26954).
|
|
15
|
+
// Only the modal's title proves that the help overlay itself remains on screen.
|
|
16
|
+
const HELP_MODAL_TITLE = 'Dashboard help';
|
|
14
17
|
|
|
15
18
|
export function tuiChecks(s: TuiScreens, readyLimitMs = 60_000): TuiCheck[] {
|
|
16
19
|
const all = [s.first, s.help, s.afterEsc].join('\n');
|
|
@@ -18,8 +21,8 @@ export function tuiChecks(s: TuiScreens, readyLimitMs = 60_000): TuiCheck[] {
|
|
|
18
21
|
{ id: 'boot', pass: s.readyMs !== null && s.readyMs <= readyLimitMs, detail: s.readyMs === null ? 'never ready' : `ready in ${s.readyMs}ms` },
|
|
19
22
|
{ id: 'prompt', pass: s.first.includes('❯'), detail: 'input line (❯) on the first screen' },
|
|
20
23
|
{ id: 'status-bar', pass: /│.*ctx \d+/.test(s.first), detail: 'status bar with model and ctx' },
|
|
21
|
-
{ id: 'help-opens', pass: s.help.includes(
|
|
22
|
-
{ id: 'help-closes', pass: !s.afterEsc.includes(
|
|
24
|
+
{ id: 'help-opens', pass: s.help.includes(HELP_MODAL_TITLE) && s.help.includes('Commands') && s.help.includes('/help'), detail: '/help shows the help overlay' },
|
|
25
|
+
{ id: 'help-closes', pass: !s.afterEsc.includes(HELP_MODAL_TITLE) && s.afterEsc.includes('❯'), detail: 'Esc closes the overlay' },
|
|
23
26
|
{ id: 'no-error-text', pass: !ERROR_TEXT.test(all), detail: 'no error text on any screen' },
|
|
24
27
|
];
|
|
25
28
|
}
|
|
@@ -17,7 +17,9 @@ export interface PublicCheckReport {
|
|
|
17
17
|
readonly ok: boolean;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// 개인 경로(`/Users/`·`~/source`)는 대소문자를 가리고, 내부 표지는 기존처럼 `iu`.
|
|
21
|
+
const PERSONAL_PATH = /\/Users\/|~\/source(?:\/|\b)/u;
|
|
22
|
+
const INTERNAL_MARKER = /\b(?:INTERNAL_ONLY|ELANOUS_INTERNAL|PRIVATE_INTERNAL|internal-only)\b|@internal\b|내부\s*전용/iu;
|
|
21
23
|
const EXECUTABLE = /\.(?:ts|tsx|js|mjs|cjs|py|sh|bash)$/iu;
|
|
22
24
|
const ENTRY_DIR = /^(?:scripts|src|bin)\//u;
|
|
23
25
|
const NON_ENTRY = /(?:\.d\.ts|\.(?:test|spec)\.[^.]+)$/iu;
|
|
@@ -165,7 +167,8 @@ export function checkPublicSkills(root: string, boundary: SkillBoundaryReport =
|
|
|
165
167
|
const bytes = readFileSync(file);
|
|
166
168
|
if (bytes.includes(0)) continue;
|
|
167
169
|
try {
|
|
168
|
-
|
|
170
|
+
const decoded = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
171
|
+
if (PERSONAL_PATH.test(decoded) || INTERNAL_MARKER.test(decoded)) matches.push(relative(root, file));
|
|
169
172
|
} catch {
|
|
170
173
|
// Binary assets are not text; extensionless and dotfiles are checked when decodable.
|
|
171
174
|
}
|