elanous 0.2.14 → 0.2.15
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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -0
- package/apps/pwa/out/ceo/index.txt +33 -0
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -2
- package/package.json +1 -1
- package/scripts/brand/check.ts +7 -5
- package/scripts/ci-typecheck-changed.ts +50 -18
- package/scripts/docs/generate-graph-anatomy.ts +315 -0
- package/scripts/expose-rubric-suggest.ts +18 -12
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +30 -3
- package/scripts/marketing/generate-ax-brief.ts +140 -0
- package/scripts/marketing/generate-feature-map.ts +118 -0
- package/scripts/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/gate-node.ts +80 -14
- package/scripts/release-loop/vault-note-node.ts +240 -8
- package/scripts/release-story/draft.ts +57 -9
- package/scripts/rhythm/daily.ts +263 -76
- package/scripts/rhythm/weekly.ts +30 -9
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/src/agent-env/profile.ts +161 -0
- package/src/agent-substrate/pr-reviewer.ts +2 -0
- package/src/ask-user-question/pending-questions.ts +5 -0
- package/src/auth/github-app-token.ts +112 -13
- package/src/autopilot/build/llm-conflict-merge.ts +32 -6
- package/src/briefing/brief-items.ts +121 -0
- package/src/claims/claims-render.ts +14 -5
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/decisions-cli.ts +9 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/intake-cli.ts +46 -17
- package/src/cli/pod-cli.ts +2 -0
- package/src/cli/release-cli.ts +61 -27
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/context-bus/context-now-public.ts +16 -6
- package/src/context-bus/context-now.ts +3 -1
- package/src/context-bus/coord-events.ts +6 -4
- package/src/decisions/decision-cards.ts +33 -7
- package/src/decisions/proact-meter.ts +286 -0
- package/src/decisions/proact1-lite.ts +86 -0
- package/src/decisions/web-push-decision-cards.ts +1 -1
- package/src/domains/alert-audit.ts +271 -0
- package/src/domains/core-tools.ts +4 -3
- package/src/domains/ops-facts-tool.ts +31 -1
- package/src/harness/harness-cli-command.ts +111 -15
- package/src/harness/harness-incidents.ts +2 -0
- package/src/harness/harness-pod-dispatch.ts +100 -11
- package/src/harness/harness-queue.ts +380 -50
- package/src/harness/helper-repair.ts +87 -0
- package/src/harness/helper-scan.ts +16 -4
- package/src/harness/launch-stamp.ts +236 -0
- package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
- package/src/harness/ledger-write-guard.ts +35 -0
- package/src/harness/pod-memory-policy.ts +15 -0
- package/src/harness/repository-name.ts +55 -0
- package/src/hq/fence-audit.ts +90 -0
- package/src/hq/hq.ts +62 -4
- package/src/hq/lease.ts +4 -4
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +31 -0
- package/src/index.ts +77 -7
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +37 -15
- package/src/intake-plane/front-classifier.ts +6 -0
- package/src/intake-plane/items.ts +3 -2
- package/src/intake-plane/lens.ts +177 -0
- package/src/intake-plane/route.ts +145 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
- package/src/launch-head/seat-goal-queue.ts +198 -0
- package/src/loops/budget.ts +36 -5
- package/src/loops/checker.ts +2 -2
- package/src/loops/orchestrator/author-depth.ts +65 -9
- package/src/loops/orchestrator/author-input-check.ts +8 -3
- package/src/loops/orchestrator/author-ledger.ts +3 -1
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +11 -4
- package/src/loops/orchestrator/traffic.ts +117 -11
- package/src/loops/registry.ts +142 -61
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/maturity/tui-slash-availability.ts +31 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/http-server.ts +28 -14
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/schedules-read.ts +13 -0
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +6 -3
- package/src/release-loop/checklist.ts +13 -4
- package/src/release-loop/feature-store.ts +58 -31
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +86 -26
- package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
- package/src/seat-dispatch/seat-request-ledger.ts +133 -0
- package/src/seat-loop/seat-loop.ts +126 -10
- package/src/self-dev/dev-pipeline.ts +8 -4
- package/src/self-dev/draft-sweep.ts +90 -10
- package/src/self-dev/draft-triage-rules.ts +25 -8
- package/src/self-dev/orchestrate.ts +15 -1
- package/src/self-dev/review-provider-fallback.ts +6 -1
- package/src/self-dev/run-supervisor.ts +49 -2
- package/src/self-implement/frozen-merges.ts +3 -2
- package/src/self-implement/goal-author.ts +43 -5
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/heal-intake.ts +120 -2
- package/src/self-implement/host-regate.ts +9 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +187 -40
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +42 -7
- package/src/self-implement/run-ledger.ts +2 -0
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +1 -1
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
- package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
- package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
- package/src/telegram.ts +25 -4
- package/src/user-config.ts +29 -5
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-e6b84ab07d3567d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-160089123a40dd91.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e7e10cd3386222e5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-58043db78ba519ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.js +0 -1
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { appendFileSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { homedir, tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { debug } from '../../src/debug/log.js';
|
|
7
|
+
import {
|
|
8
|
+
defaultGitMergeSeam, defaultLlmResolve, mergeMainWithLlmResolve,
|
|
9
|
+
type MergeGitSeam,
|
|
10
|
+
} from '../../src/autopilot/build/llm-conflict-merge.js';
|
|
11
|
+
import { NEXT_MD_PATH } from '../../src/release-loop/next-md-merge.js';
|
|
12
|
+
|
|
13
|
+
type Command = (cwd: string, program: 'git' | 'gh', args: readonly string[]) => { status: number; stdout: string; stderr: string };
|
|
14
|
+
export type ShadowOutcome = 'clean' | 'resolved-deterministic' | 'resolved-llm' | 'unresolved' | 'unmeasured';
|
|
15
|
+
export interface ShadowRow { pr: number; outcome: ShadowOutcome; files: string[]; durationMs: number }
|
|
16
|
+
interface Draft { number: number; headRefName: string; isDraft: boolean; mergeable: string | boolean | null }
|
|
17
|
+
|
|
18
|
+
export interface ShadowOptions {
|
|
19
|
+
repo: string;
|
|
20
|
+
stateRoot: string;
|
|
21
|
+
limit?: number;
|
|
22
|
+
run?: Command;
|
|
23
|
+
git?: MergeGitSeam;
|
|
24
|
+
resolve?: (file: string, conflicted: string) => Promise<string>;
|
|
25
|
+
log?: (line: string) => void;
|
|
26
|
+
now?: () => number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const runCommand: Command = (cwd, program, args) => {
|
|
30
|
+
const result = spawnSync(program, [...args], { cwd, encoding: 'utf8' });
|
|
31
|
+
return { status: result.status ?? 1, stdout: result.stdout ?? '', stderr: result.stderr ?? String(result.error ?? '') };
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function mustRun(run: Command, cwd: string, program: 'git' | 'gh', args: readonly string[]): string {
|
|
35
|
+
const result = run(cwd, program, args);
|
|
36
|
+
if (result.status !== 0) throw new Error(`${program} ${args.slice(0, 3).join(' ')}: ${result.stderr || 'failed'}`);
|
|
37
|
+
return result.stdout;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Use the same harness ownership boundary as the draft sweep; never touch a non-harness PR. */
|
|
41
|
+
export function conflictingHarnessDrafts(json: string): Draft[] {
|
|
42
|
+
const rows: unknown = JSON.parse(json);
|
|
43
|
+
if (!Array.isArray(rows)) throw new Error('gh PR inventory is not an array');
|
|
44
|
+
if (rows.some((row) => !row || typeof row !== 'object' || !Number.isSafeInteger(row.number)
|
|
45
|
+
|| row.number <= 0 || typeof row.headRefName !== 'string' || typeof row.isDraft !== 'boolean'
|
|
46
|
+
|| !('mergeable' in row) || !['string', 'boolean'].includes(typeof row.mergeable) && row.mergeable !== null)) {
|
|
47
|
+
throw new Error('gh PR inventory is incomplete');
|
|
48
|
+
}
|
|
49
|
+
return (rows as Draft[]).filter((row) => row.isDraft && row.headRefName.startsWith('self-impl/')
|
|
50
|
+
&& (row.mergeable === 'CONFLICTING' || row.mergeable === false));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** GitHub 는 병합 가능 여부를 늦게 계산한다 — 첫 조회에 UNKNOWN 이면 «충돌 0»이 아니라 «아직 못 잼»이다(10-05 첫 운영 실행이 조용히 0). */
|
|
54
|
+
export function unknownMergeableDrafts(json: string): number {
|
|
55
|
+
const rows = JSON.parse(json) as Draft[];
|
|
56
|
+
return rows.filter((row) => row.isDraft && row.headRefName.startsWith('self-impl/') && (row.mergeable === 'UNKNOWN' || row.mergeable === null)).length;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function shadowSummary(rows: readonly ShadowRow[]): string {
|
|
60
|
+
const measured = rows.filter((row) => row.outcome !== 'unmeasured');
|
|
61
|
+
const recoverable = measured.filter((row) => row.outcome !== 'unresolved').length;
|
|
62
|
+
return `충돌 draft ${measured.length} 중 살릴 수 있음 ${recoverable} = ${measured.length ? Math.round(recoverable / measured.length * 100) : 0}%`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Shadow only: fetch a PR snapshot, detach it outside the repo, simulate merging fresh origin/main, then discard it. */
|
|
66
|
+
export async function runResyncShadow(options: ShadowOptions): Promise<ShadowRow[]> {
|
|
67
|
+
const { repo, stateRoot, limit = 10, run = runCommand, log = console.log, now = Date.now } = options;
|
|
68
|
+
if (!Number.isSafeInteger(limit) || limit < 1) throw new Error('--limit must be a positive integer');
|
|
69
|
+
const rows: ShadowRow[] = [];
|
|
70
|
+
const output = join(stateRoot, 'smart-merge', 'resync-shadow.jsonl');
|
|
71
|
+
const record = (row: ShadowRow): void => {
|
|
72
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
73
|
+
appendFileSync(output, `${JSON.stringify(row)}\n`);
|
|
74
|
+
rows.push(row);
|
|
75
|
+
debug.log('smart-merge.resync', 'shadow-result', { pr: row.pr, outcome: row.outcome, files: row.files });
|
|
76
|
+
};
|
|
77
|
+
let drafts: Draft[];
|
|
78
|
+
try {
|
|
79
|
+
// gh's mergeable GraphQL field is CONFLICTING/MERGEABLE/UNKNOWN (some adapters return false/true).
|
|
80
|
+
const inventory = mustRun(run, repo, 'gh', [
|
|
81
|
+
'pr', 'list', '--state', 'open', '--limit', '1000', '--json', 'number,isDraft,headRefName,mergeable',
|
|
82
|
+
]);
|
|
83
|
+
drafts = conflictingHarnessDrafts(inventory).slice(0, limit);
|
|
84
|
+
const unknown = unknownMergeableDrafts(inventory);
|
|
85
|
+
if (unknown) log(`병합 가능 여부 미계산 draft ${unknown} — GitHub 계산 뒤 다시 돌려라(이번 수에 안 들어감)`);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
log(`못 잼: GitHub draft 조회 실패 (${String(error)})`);
|
|
88
|
+
log(shadowSummary(rows));
|
|
89
|
+
return rows;
|
|
90
|
+
}
|
|
91
|
+
for (const draft of drafts) {
|
|
92
|
+
const start = now();
|
|
93
|
+
let temp: string | undefined;
|
|
94
|
+
let attached = false;
|
|
95
|
+
let outcome: ShadowOutcome = 'unmeasured';
|
|
96
|
+
let files: string[] = [];
|
|
97
|
+
try {
|
|
98
|
+
// refs/pull/N/head supports fork heads too; FETCH_HEAD is consumed before the next fetch.
|
|
99
|
+
mustRun(run, repo, 'git', ['fetch', 'origin', `refs/pull/${draft.number}/head`]);
|
|
100
|
+
temp = mkdtempSync(join(tmpdir(), 'elanous-resync-shadow-'));
|
|
101
|
+
mustRun(run, repo, 'git', ['worktree', 'add', '--detach', temp, 'FETCH_HEAD']);
|
|
102
|
+
attached = true;
|
|
103
|
+
const seam = options.git ?? defaultGitMergeSeam();
|
|
104
|
+
const observedGit: MergeGitSeam = {
|
|
105
|
+
...seam,
|
|
106
|
+
conflictedFiles: (wt) => { files = seam.conflictedFiles(wt); return files; },
|
|
107
|
+
};
|
|
108
|
+
const result = await mergeMainWithLlmResolve(temp, 'origin/main',
|
|
109
|
+
options.resolve ?? ((file, conflicted) => defaultLlmResolve(file, conflicted, 'origin/main', { worktreePath: temp })), observedGit);
|
|
110
|
+
if (result.status === 'merged' || result.status === 'up-to-date') outcome = 'clean';
|
|
111
|
+
else if (result.status === 'llm-resolved') {
|
|
112
|
+
files = result.resolvedFiles ?? files;
|
|
113
|
+
outcome = files.length > 0 && files.every((file) => file === NEXT_MD_PATH) ? 'resolved-deterministic' : 'resolved-llm';
|
|
114
|
+
} else if (result.status === 'conflict-unresolved') outcome = 'unresolved';
|
|
115
|
+
// A git error is a measurement failure, not evidence of an unresolvable conflict.
|
|
116
|
+
} catch {
|
|
117
|
+
outcome = 'unmeasured';
|
|
118
|
+
} finally {
|
|
119
|
+
if (attached && temp) {
|
|
120
|
+
try { mustRun(run, repo, 'git', ['worktree', 'remove', '--force', temp]); }
|
|
121
|
+
catch { outcome = 'unmeasured'; }
|
|
122
|
+
}
|
|
123
|
+
if (temp) {
|
|
124
|
+
try { rmSync(temp, { recursive: true, force: true }); }
|
|
125
|
+
catch { outcome = 'unmeasured'; }
|
|
126
|
+
}
|
|
127
|
+
record({ pr: draft.number, outcome, files, durationMs: Math.max(0, now() - start) });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
log(shadowSummary(rows));
|
|
131
|
+
return rows;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (import.meta.main) {
|
|
135
|
+
const args = process.argv.slice(2);
|
|
136
|
+
const valid = args.length === 0 || args.length === 2 && args[0] === '--limit' && /^[1-9]\d*$/.test(args[1]!);
|
|
137
|
+
if (!valid) {
|
|
138
|
+
console.error('usage: bun scripts/smart-merge/resync-shadow.ts [--limit N]');
|
|
139
|
+
process.exitCode = 2;
|
|
140
|
+
} else {
|
|
141
|
+
await runResyncShadow({
|
|
142
|
+
repo: mustRun(runCommand, process.cwd(), 'git', ['rev-parse', '--show-toplevel']).trim(),
|
|
143
|
+
stateRoot: process.env.ELANOUS_STATE_DIR || join(homedir(), '.elanous'),
|
|
144
|
+
...(args.length ? { limit: Number(args[1]) } : {}),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
type Agent = 'claude' | 'codex' | 'grok';
|
|
6
|
+
type Presence = 'present' | '«없음»';
|
|
7
|
+
|
|
8
|
+
function present(path: string): Presence {
|
|
9
|
+
return existsSync(path) ? 'present' : '«없음»';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function record(value: unknown): Record<string, unknown> {
|
|
13
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
14
|
+
? value as Record<string, unknown> : {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function json(path: string): Record<string, unknown> | null {
|
|
18
|
+
if (!existsSync(path)) return null;
|
|
19
|
+
return record(JSON.parse(readFileSync(path, 'utf8')));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Command strings can contain tokens even without a shell; export only known executable names.
|
|
23
|
+
function safeCommand(value: unknown): string | undefined {
|
|
24
|
+
if (typeof value !== 'string') return undefined;
|
|
25
|
+
return /^(?:npx|npm|node|bun|uv|uvx|python|python3|docker|deno|pnpm|yarn|bash|sh|claude|codex|grok|cct)$/.test(value)
|
|
26
|
+
? value : '<redacted>';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Key-shaped values (sk-…, ghp_…, AKIA…) pass the character check, so shape alone cannot prove a value is not a secret (10-05 post-review).
|
|
30
|
+
const SECRET_PREFIX = /^(?:sk-|sk_|pk_|rk_|xai-|ghp_|gho_|ghu_|ghs_|ghr_|github_pat_|glpat-|AKIA|ASIA|AIza|ya29\.|xox[abprs]-|eyJ)/;
|
|
31
|
+
|
|
32
|
+
export function safeIdentifier(value: unknown): string | null {
|
|
33
|
+
if (typeof value !== 'string') return null;
|
|
34
|
+
const digits = (value.match(/[0-9]/g) ?? []).length;
|
|
35
|
+
const keyShaped = SECRET_PREFIX.test(value) || value.length > 40 || (value.length >= 20 && digits >= 8);
|
|
36
|
+
return /^[a-zA-Z0-9_.:-]{1,90}$/.test(value) && !keyShaped && !/(?:token|secret|password|api.?key|credential)/i.test(value)
|
|
37
|
+
? value : '<redacted>';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type McpSource = 'settings' | 'user' | 'project';
|
|
41
|
+
|
|
42
|
+
function mcpEntry(server: unknown, source: McpSource): Record<string, unknown> {
|
|
43
|
+
const body = record(server);
|
|
44
|
+
return {
|
|
45
|
+
source,
|
|
46
|
+
command: safeCommand(body.command) ?? null,
|
|
47
|
+
env: Object.fromEntries(Object.keys(record(body.env)).map(key => [key, '<redacted>'])),
|
|
48
|
+
headers: Object.fromEntries(Object.keys(record(body.headers)).map(key => [key, '<redacted>'])),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function collectMcp(
|
|
53
|
+
into: Record<string, unknown>,
|
|
54
|
+
servers: unknown,
|
|
55
|
+
source: McpSource,
|
|
56
|
+
): void {
|
|
57
|
+
for (const [name, server] of Object.entries(record(servers))) into[name] = mcpEntry(server, source);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hookGroups(hooks: Record<string, unknown>): Record<string, unknown[]> {
|
|
61
|
+
return Object.fromEntries(Object.entries(hooks).map(([event, groups]) => [event,
|
|
62
|
+
Array.isArray(groups) ? groups.map(group => {
|
|
63
|
+
const body = record(group);
|
|
64
|
+
const entries = body.hooks;
|
|
65
|
+
return {
|
|
66
|
+
matcher: safeIdentifier(body.matcher),
|
|
67
|
+
hooks: Array.isArray(entries)
|
|
68
|
+
? entries.map(entry => ({ command: safeCommand(record(entry).command) ?? null }))
|
|
69
|
+
: [],
|
|
70
|
+
};
|
|
71
|
+
}) : [],
|
|
72
|
+
]));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function claude(home: string): Record<string, unknown> {
|
|
76
|
+
const dir = join(home, '.claude');
|
|
77
|
+
const settingsPath = join(dir, 'settings.json');
|
|
78
|
+
const userPath = join(home, '.claude.json');
|
|
79
|
+
const settings = json(settingsPath);
|
|
80
|
+
const user = json(userPath);
|
|
81
|
+
const data = record(settings);
|
|
82
|
+
const userData = record(user);
|
|
83
|
+
const plugins = record(data.enabledPlugins);
|
|
84
|
+
const hooks = record(data.hooks);
|
|
85
|
+
const statusLine = record(data.statusLine);
|
|
86
|
+
const skillsDir = join(dir, 'skills');
|
|
87
|
+
const team = join(home, '.teamclaude');
|
|
88
|
+
const teamManifest = json(join(team, 'package.json'));
|
|
89
|
+
const teamVersion = typeof teamManifest?.version === 'string' && /^\d+\.\d+\.\d+(?:[-+.][a-zA-Z0-9.-]+)?$/.test(teamManifest.version)
|
|
90
|
+
? teamManifest.version : undefined;
|
|
91
|
+
const mcp: Record<string, unknown> = {};
|
|
92
|
+
if (settings !== null) collectMcp(mcp, data.mcpServers, 'settings');
|
|
93
|
+
if (user !== null) {
|
|
94
|
+
collectMcp(mcp, userData.mcpServers, 'user');
|
|
95
|
+
for (const project of Object.values(record(userData.projects))) {
|
|
96
|
+
collectMcp(mcp, record(project).mcpServers, 'project');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const mcpFilesMissing = settings === null && user === null;
|
|
100
|
+
return {
|
|
101
|
+
settings: settings === null ? '«없음»' : 'present',
|
|
102
|
+
userConfig: user === null ? '«없음»' : 'present',
|
|
103
|
+
permissionMode: safeIdentifier(record(data.permissions).defaultMode),
|
|
104
|
+
statusLine: settings === null ? '«없음»' : data.statusLine === undefined ? '«없음»' : {
|
|
105
|
+
type: safeIdentifier(statusLine.type),
|
|
106
|
+
command: safeCommand(statusLine.command) ?? null,
|
|
107
|
+
},
|
|
108
|
+
hooks: settings === null ? '«없음»' : hookGroups(hooks),
|
|
109
|
+
enabledPlugins: settings === null ? '«없음»' : Object.fromEntries(Object.entries(plugins).map(([name, enabled]) => [name, enabled === true])),
|
|
110
|
+
mcpServers: mcpFilesMissing ? '«없음»' : mcp,
|
|
111
|
+
skills: !existsSync(skillsDir) ? '«없음»' : readdirSync(skillsDir, { withFileTypes: true })
|
|
112
|
+
.filter(entry => entry.isDirectory()).map(entry => entry.name).sort(),
|
|
113
|
+
teamclaude: { installed: present(team), version: teamVersion ?? null },
|
|
114
|
+
auth: present(join(dir, 'auth.json')),
|
|
115
|
+
credentials: present(join(dir, '.credentials.json')),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function codex(home: string): Record<string, unknown> {
|
|
120
|
+
const dir = join(home, '.codex');
|
|
121
|
+
const file = join(dir, 'config.toml');
|
|
122
|
+
if (!existsSync(file)) return { config: '«없음»', model: null, profile: null, mcpServers: '«없음»', auth: present(join(dir, 'auth.json')) };
|
|
123
|
+
const data = record(Bun.TOML.parse(readFileSync(file, 'utf8')));
|
|
124
|
+
const mcp = record(data.mcp_servers);
|
|
125
|
+
return {
|
|
126
|
+
config: 'present',
|
|
127
|
+
model: safeIdentifier(data.model),
|
|
128
|
+
profile: safeIdentifier(data.profile),
|
|
129
|
+
mcpServers: Object.keys(mcp).sort(),
|
|
130
|
+
auth: present(join(dir, 'auth.json')),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function grok(home: string): Record<string, unknown> {
|
|
135
|
+
const dir = join(home, '.grok');
|
|
136
|
+
const jsonPath = join(dir, 'config.json');
|
|
137
|
+
const tomlPath = join(dir, 'config.toml');
|
|
138
|
+
const file = existsSync(jsonPath) ? jsonPath : tomlPath;
|
|
139
|
+
const data = !existsSync(file) ? null : file === jsonPath ? json(file) : record(Bun.TOML.parse(readFileSync(file, 'utf8')));
|
|
140
|
+
return {
|
|
141
|
+
config: data === null ? '«없음»' : 'present',
|
|
142
|
+
keys: data === null ? '«없음»' : Object.keys(data).sort(),
|
|
143
|
+
auth: present(join(dir, 'auth.json')),
|
|
144
|
+
credentials: present(join(dir, 'credentials.json')),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function captureAgentEnv(options: { agent?: Agent; home?: string; now?: Date } = {}): {
|
|
149
|
+
version: number; capturedAt: string; agents: Record<string, Record<string, unknown>>;
|
|
150
|
+
} {
|
|
151
|
+
const home = options.home ?? homedir();
|
|
152
|
+
const selected = options.agent ? [options.agent] : ['claude', 'codex', 'grok'] as const;
|
|
153
|
+
const agents: Record<string, Record<string, unknown>> = {};
|
|
154
|
+
for (const agent of selected) {
|
|
155
|
+
if (agent === 'claude') agents.claude = claude(home);
|
|
156
|
+
else if (agent === 'codex') agents.codex = codex(home);
|
|
157
|
+
else if (agent === 'grok') agents.grok = grok(home);
|
|
158
|
+
else throw new Error('Unknown agent');
|
|
159
|
+
}
|
|
160
|
+
return { version: 1, capturedAt: (options.now ?? new Date()).toISOString(), agents };
|
|
161
|
+
}
|
|
@@ -825,6 +825,8 @@ export async function reviewPullRequest(
|
|
|
825
825
|
|
|
826
826
|
/** 리뷰 결과를 PR 코멘트/알림/CLI 용 markdown 으로 렌더. */
|
|
827
827
|
export function renderReview(r: ReviewResult): string {
|
|
828
|
+
// A review that never ran must not read as PASS anywhere it is rendered (PR body, comments, CLI) — 10-05 #23942·#24151.
|
|
829
|
+
if (r.reviewed === false) return `⚠️ 자율 PR 리뷰: 리뷰 안 돎 — 사유 ${r.failureReason?.trim() || '미상'} (통과 판정 아님)`;
|
|
828
830
|
const icon = r.verdict === 'pass' ? '✅' : r.verdict === 'warn' ? '⚠️' : '⛔';
|
|
829
831
|
const head = `${icon} 자율 PR 리뷰: ${r.verdict.toUpperCase()}`;
|
|
830
832
|
const parts = [head];
|
|
@@ -9,6 +9,8 @@ export interface PendingQuestion {
|
|
|
9
9
|
id: string;
|
|
10
10
|
sessionId?: string;
|
|
11
11
|
runId?: string;
|
|
12
|
+
/** Explicit provenance for isolated test questions; a temporary goal path alone is not proof. */
|
|
13
|
+
testOrigin?: true;
|
|
12
14
|
questions: Array<Pick<Question, 'id' | 'question' | 'options' | 'impact' | 'recommendedIndex'>>;
|
|
13
15
|
startedAt: string;
|
|
14
16
|
expiresAt?: string;
|
|
@@ -84,6 +86,7 @@ function isPendingQuestion(value: unknown): value is PendingQuestion {
|
|
|
84
86
|
return typeof pending.id === 'string'
|
|
85
87
|
&& (pending.sessionId === undefined || typeof pending.sessionId === 'string')
|
|
86
88
|
&& (pending.runId === undefined || typeof pending.runId === 'string')
|
|
89
|
+
&& (pending.testOrigin === undefined || pending.testOrigin === true)
|
|
87
90
|
&& Array.isArray(pending.questions)
|
|
88
91
|
&& pending.questions.every((question) => {
|
|
89
92
|
if (question === null || typeof question !== 'object' || Array.isArray(question)) return false;
|
|
@@ -175,6 +178,7 @@ export function createPendingQuestion(
|
|
|
175
178
|
sessionId?: string,
|
|
176
179
|
deps: Pick<PendingQuestionStoreDeps, 'now'> = {},
|
|
177
180
|
presentation: PendingQuestionPresentation = { surface: 'tui', delivery: request.delivery ?? 'modal' },
|
|
181
|
+
provenance: { testOrigin?: true } = {},
|
|
178
182
|
): PendingQuestion {
|
|
179
183
|
// File-surface waits must carry a concrete deadline so the CLI answer path can
|
|
180
184
|
// reject an expired record (see isPendingQuestion). Fail loudly at creation
|
|
@@ -186,6 +190,7 @@ export function createPendingQuestion(
|
|
|
186
190
|
id,
|
|
187
191
|
...(sessionId === undefined ? {} : { sessionId }),
|
|
188
192
|
...(request.runId === undefined ? {} : { runId: request.runId }),
|
|
193
|
+
...(provenance.testOrigin ? { testOrigin: true as const } : {}),
|
|
189
194
|
questions: request.questions.map(({ id, question, options, impact, recommendedIndex }) => ({
|
|
190
195
|
id, question, options,
|
|
191
196
|
...(impact === undefined ? {} : { impact }),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, createSign } from 'node:crypto';
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, isAbsolute, join } from 'node:path';
|
|
5
5
|
import { getElanousConfigDir, getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
6
6
|
import { prodInstanceRoot } from '../instance/resolve.js';
|
|
@@ -45,21 +45,23 @@ export function githubAutomationToken(deps: GithubAutomationTokenDeps = {}): str
|
|
|
45
45
|
return githubInstallationCredential(deps)?.token ?? null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** The App credential is machine-level (like `gh auth` in ~/.config/gh), not per universe: a tree-derived test
|
|
49
|
+
* universe resolves its config dir to `<tree>/.elanous-test`, where the file never is — 09-29 #21879 was inert there.
|
|
50
|
+
* Order: explicit path → this universe's config dir → the prod root `~/.elanous`.
|
|
51
|
+
* An explicit config-dir override (tests · `--config-dir`) is a deliberate boundary — never reach past it.
|
|
52
|
+
* ELANOUS_GITHUB_APP_CONFIG_PATH pins the machine-level fallback (the bun test preload points it at nothing,
|
|
53
|
+
* so tests never mint); an explicit config-dir override still wins. */
|
|
54
|
+
function appConfigPath(explicit?: string): string {
|
|
55
|
+
const rel = join('secrets', 'github-app', 'app.json');
|
|
56
|
+
const own = join(getElanousConfigDir(), rel);
|
|
57
|
+
const pinned = process.env.ELANOUS_GITHUB_APP_CONFIG_PATH?.trim();
|
|
58
|
+
return explicit ?? (getElanousConfigDirOverride() ? own : pinned || (existsSync(own) ? own : join(prodInstanceRoot(), rel)));
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
/** Scoped requests always mint anew; the returned expiry comes from GitHub's response. */
|
|
49
62
|
export function githubInstallationCredential(deps: GithubAutomationTokenDeps = {}): { token: string; expires_at: string } | null {
|
|
50
63
|
let path: string;
|
|
51
|
-
|
|
52
|
-
// universe resolves its config dir to `<tree>/.elanous-test`, where the file never is — 09-29 #21879 was inert there.
|
|
53
|
-
// Order: explicit path → this universe's config dir → the prod root `~/.elanous`.
|
|
54
|
-
try {
|
|
55
|
-
const rel = join('secrets', 'github-app', 'app.json');
|
|
56
|
-
const own = join(getElanousConfigDir(), rel);
|
|
57
|
-
// An explicit config-dir override (tests · `--config-dir`) is a deliberate boundary — never reach past it.
|
|
58
|
-
// ELANOUS_GITHUB_APP_CONFIG_PATH pins the machine-level fallback (the bun test preload points it at nothing,
|
|
59
|
-
// so tests never mint); an explicit config-dir override still wins.
|
|
60
|
-
const pinned = process.env.ELANOUS_GITHUB_APP_CONFIG_PATH?.trim();
|
|
61
|
-
path = deps.configPath ?? (getElanousConfigDirOverride() ? own : pinned || (existsSync(own) ? own : join(prodInstanceRoot(), rel)));
|
|
62
|
-
}
|
|
64
|
+
try { path = appConfigPath(deps.configPath); }
|
|
63
65
|
catch {
|
|
64
66
|
try { debug.log('auth.github-app', 'token-failed', { reason: 'config-path-failed' }); } catch { /* fail open */ }
|
|
65
67
|
return null;
|
|
@@ -113,3 +115,100 @@ export function githubInstallationCredential(deps: GithubAutomationTokenDeps = {
|
|
|
113
115
|
return null;
|
|
114
116
|
}
|
|
115
117
|
}
|
|
118
|
+
|
|
119
|
+
/** PODCRED1 (10-05): simultaneous launches each minted a scoped token in the same second and the Pods' logins died
|
|
120
|
+
* together (12:49Z · 5 in 2 s). Concurrent issuers for one repository — across processes — share one mint: the first
|
|
121
|
+
* takes a lock and mints, the others wait and reuse that result while it is recent and long-lived. */
|
|
122
|
+
export interface CoalescedInstallationOptions {
|
|
123
|
+
cacheDir?: string;
|
|
124
|
+
/** Reuse a token minted at most this long ago. */
|
|
125
|
+
windowMs?: number;
|
|
126
|
+
/** …and only while it still has this much life left. */
|
|
127
|
+
minRemainingMs?: number;
|
|
128
|
+
/** Skip reuse (still mint under the lock and publish the result) — for a retry after the shared token failed. */
|
|
129
|
+
fresh?: boolean;
|
|
130
|
+
lockWaitMs?: number;
|
|
131
|
+
staleLockMs?: number;
|
|
132
|
+
now?: () => number;
|
|
133
|
+
sleepSync?: (ms: number) => void;
|
|
134
|
+
mint?: (scope: GithubInstallationTokenScope) => { token: string; expires_at: string } | null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const COALESCE_WINDOW_MS = 120_000;
|
|
138
|
+
const COALESCE_MIN_REMAINING_MS = 50 * 60_000;
|
|
139
|
+
|
|
140
|
+
function blockingSleep(ms: number): void {
|
|
141
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function coalescedInstallationCredential(scope: GithubInstallationTokenScope, opts: CoalescedInstallationOptions = {}): { token: string; expires_at: string } | null {
|
|
145
|
+
const now = opts.now ?? Date.now;
|
|
146
|
+
const mint = opts.mint ?? ((s: GithubInstallationTokenScope) => githubInstallationCredential({ scope: s }));
|
|
147
|
+
const windowMs = opts.windowMs ?? COALESCE_WINDOW_MS;
|
|
148
|
+
const minRemainingMs = opts.minRemainingMs ?? COALESCE_MIN_REMAINING_MS;
|
|
149
|
+
const sleepSync = opts.sleepSync ?? blockingSleep;
|
|
150
|
+
// Same boundary as the App config: a test process (or config-dir override) never writes the operational root.
|
|
151
|
+
if (!opts.cacheDir && (process.env.NODE_ENV === 'test' || process.env.ELANOUS_TEST_HOME)) return mint(scope);
|
|
152
|
+
let dir: string;
|
|
153
|
+
let identity: string;
|
|
154
|
+
try {
|
|
155
|
+
const configPath = appConfigPath();
|
|
156
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8')) as Partial<AppConfig>;
|
|
157
|
+
identity = JSON.stringify([configPath, config.id, config.installation_id]);
|
|
158
|
+
dir = opts.cacheDir ?? join(getElanousConfigDirOverride() ? getElanousConfigDir() : prodInstanceRoot(), 'cache', 'github-app-scoped');
|
|
159
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
160
|
+
} catch {
|
|
161
|
+
if (!opts.cacheDir) return mint(scope);
|
|
162
|
+
dir = opts.cacheDir;
|
|
163
|
+
identity = 'injected';
|
|
164
|
+
}
|
|
165
|
+
// Keyed by App installation and repository, so another App (another universe or owner) never reuses this token.
|
|
166
|
+
const key = createHash('sha256').update(`${identity}\0${scope.repository}`).digest('hex').slice(0, 32);
|
|
167
|
+
const file = join(dir, `${key}.json`);
|
|
168
|
+
const lock = join(dir, `${key}.lock`);
|
|
169
|
+
let waitStart: number | undefined;
|
|
170
|
+
const reuse = (): { token: string; expires_at: string } | null => {
|
|
171
|
+
try {
|
|
172
|
+
const entry = JSON.parse(readFileSync(file, 'utf8')) as { token?: unknown; expiresAt?: unknown; mintedAt?: unknown };
|
|
173
|
+
const t = now();
|
|
174
|
+
if (typeof entry.token === 'string' && entry.token && typeof entry.expiresAt === 'number' && typeof entry.mintedAt === 'number'
|
|
175
|
+
&& t - entry.mintedAt <= windowMs && entry.expiresAt - t >= minRemainingMs) {
|
|
176
|
+
try { debug.log('auth.github-app', 'token-coalesced', { ageMs: t - entry.mintedAt, expiresAt: new Date(entry.expiresAt).toISOString(), ...(waitStart === undefined ? {} : { waitedMs: t - waitStart }) }); } catch { /* fail open */ }
|
|
177
|
+
return { token: entry.token, expires_at: new Date(entry.expiresAt).toISOString() };
|
|
178
|
+
}
|
|
179
|
+
} catch { /* no usable entry */ }
|
|
180
|
+
return null;
|
|
181
|
+
};
|
|
182
|
+
const hit = opts.fresh ? null : reuse();
|
|
183
|
+
if (hit) return hit;
|
|
184
|
+
waitStart = now();
|
|
185
|
+
const deadline = waitStart + (opts.lockWaitMs ?? 15_000);
|
|
186
|
+
let locked = false;
|
|
187
|
+
for (;;) {
|
|
188
|
+
try { mkdirSync(lock); locked = true; break; }
|
|
189
|
+
catch {
|
|
190
|
+
try { if (now() - statSync(lock).mtimeMs > (opts.staleLockMs ?? 30_000)) { rmSync(lock, { recursive: true, force: true }); continue; } } catch { continue; }
|
|
191
|
+
if (now() >= deadline) break;
|
|
192
|
+
sleepSync(200);
|
|
193
|
+
const waited = opts.fresh ? null : reuse();
|
|
194
|
+
if (waited) return waited;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!locked) { try { debug.log('auth.github-app', 'token-coalesce-lock-timeout', { waitedMs: now() - (waitStart ?? now()) }); } catch { /* fail open */ } }
|
|
198
|
+
try {
|
|
199
|
+
const again = opts.fresh ? null : reuse();
|
|
200
|
+
if (again) return again;
|
|
201
|
+
const minted = mint(scope);
|
|
202
|
+
const expiresAt = minted ? Date.parse(minted.expires_at) : NaN;
|
|
203
|
+
if (minted && Number.isFinite(expiresAt)) {
|
|
204
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
205
|
+
try {
|
|
206
|
+
writeFileSync(temp, JSON.stringify({ token: minted.token, expiresAt, mintedAt: now() }), { mode: 0o600 });
|
|
207
|
+
renameSync(temp, file);
|
|
208
|
+
} catch { try { rmSync(temp, { force: true }); } catch { /* best effort */ } }
|
|
209
|
+
}
|
|
210
|
+
return minted;
|
|
211
|
+
} finally {
|
|
212
|
+
if (locked) { try { rmSync(lock, { recursive: true, force: true }); } catch { /* stale-lock rule recovers */ } }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -67,6 +67,9 @@ export interface LlmMergeOutcome {
|
|
|
67
67
|
testDeclarationUnmeasured?: string[];
|
|
68
68
|
/** 해결기 응답이 LLM 공급자 오류 문구였다 — 파일 내용이 아니다(🅢 2026-09-27 #21086: 세 파일이 통째로 그 문구가 됐다). */
|
|
69
69
|
providerFailure?: string[];
|
|
70
|
+
/** LLM 입력 상한 초과: 사람 수확으로 넘긴다. 원래 브랜치는 abort 후 보존된다. */
|
|
71
|
+
reason?: 'conflict-input-too-large' | 'conflict-resolver-interrupted';
|
|
72
|
+
inputChars?: number;
|
|
70
73
|
/** 병합 결과가 양쪽 판 중 작은 쪽의 절반 아래로 줄었다 — 통째로 날린 것으로 보고 해결 실패로 친다. */
|
|
71
74
|
sizeCollapse?: Array<{ file: string; ours: number; theirs: number; merged: number }>;
|
|
72
75
|
/** `status: 'error'` 일 때 어느 단계인지. 선택 — 옛 호출자는 이 칸 없이 돌아도 된다. */
|
|
@@ -79,6 +82,13 @@ export interface LlmMergeOutcome {
|
|
|
79
82
|
export const PROVIDER_FAILURE_TEXT = /^\s*\[LLM PROVIDER (?:BLOCKED|STOPPED)\]/;
|
|
80
83
|
/** 이 줄 수 미만 파일은 크기 붕괴 판정에서 뺀다(작은 파일은 정당하게 크게 줄 수 있다). */
|
|
81
84
|
export const SIZE_COLLAPSE_MIN_LINES = 50;
|
|
85
|
+
export const DEFAULT_MERGE_CONFLICT_INPUT_MAX_CHARS = 60_000;
|
|
86
|
+
|
|
87
|
+
export class MergeConflictInputTooLarge extends Error {
|
|
88
|
+
constructor(readonly inputChars: number) {
|
|
89
|
+
super(`충돌 큼 — 사람 수확 (LLM 입력 ${inputChars}자)`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
82
92
|
|
|
83
93
|
function lineCount(s: string): number {
|
|
84
94
|
if (s.length === 0) return 0;
|
|
@@ -207,12 +217,21 @@ export async function mergeMainWithLlmResolve(
|
|
|
207
217
|
let merged: string;
|
|
208
218
|
try {
|
|
209
219
|
conflicted = git.readFile(abs);
|
|
220
|
+
} catch {
|
|
221
|
+
git.abort(worktreePath);
|
|
222
|
+
return measuredOutcome({ status: 'conflict-unresolved', resolvedFiles });
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
210
225
|
// release/next.md: concurrent landings each append a line — keep both without asking the LLM (REL7c).
|
|
211
226
|
const deterministic = f === NEXT_MD_PATH ? deterministicNextMd(git, worktreePath, f) : null;
|
|
212
227
|
merged = deterministic ?? await resolve(f, conflicted);
|
|
213
|
-
} catch {
|
|
228
|
+
} catch (error) {
|
|
214
229
|
git.abort(worktreePath); // LLM 예외 → base 유지
|
|
215
|
-
return measuredOutcome({ status: 'conflict-unresolved', resolvedFiles
|
|
230
|
+
return measuredOutcome({ status: 'conflict-unresolved', resolvedFiles,
|
|
231
|
+
...(error instanceof MergeConflictInputTooLarge
|
|
232
|
+
? { reason: 'conflict-input-too-large' as const, inputChars: error.inputChars }
|
|
233
|
+
: { reason: 'conflict-resolver-interrupted' as const }),
|
|
234
|
+
});
|
|
216
235
|
}
|
|
217
236
|
if (PROVIDER_FAILURE_TEXT.test(merged)) {
|
|
218
237
|
git.abort(worktreePath); // 해결기가 공급자 오류 문구를 «내용»으로 돌려줬다 → base 유지
|
|
@@ -335,15 +354,22 @@ export async function defaultLlmResolve(
|
|
|
335
354
|
filePath: string, conflicted: string, mergeTarget: string,
|
|
336
355
|
options: { worktreePath?: string; mode?: 'off' | 'shadow' | 'on'; git?: IntentGit; stream?: typeof import('../../llm.js')['streamLLM'] } = {},
|
|
337
356
|
): Promise<string> {
|
|
357
|
+
let rawConfig: { mergeConflictInputMaxChars?: unknown; mergeIntent?: unknown } | undefined;
|
|
358
|
+
try { rawConfig = getUserConfig().raw.selfImplement as typeof rawConfig; }
|
|
359
|
+
catch { /* unreadable config keeps the old resolver and default input limit */ }
|
|
360
|
+
const configuredMax = rawConfig?.mergeConflictInputMaxChars;
|
|
361
|
+
const maxChars = typeof configuredMax === 'number' && Number.isSafeInteger(configuredMax) && configuredMax > 0
|
|
362
|
+
? configuredMax : DEFAULT_MERGE_CONFLICT_INPUT_MAX_CHARS;
|
|
338
363
|
let rawMode: unknown = options.mode;
|
|
339
364
|
if (rawMode === undefined) {
|
|
340
|
-
|
|
341
|
-
catch { /* unreadable config keeps the old resolver */ }
|
|
365
|
+
rawMode = rawConfig?.mergeIntent;
|
|
342
366
|
}
|
|
343
367
|
const mode = rawMode === 'shadow' || rawMode === 'on' ? rawMode : 'off';
|
|
344
|
-
const streamLLM = options.stream ?? (await import('../../llm.js')).streamLLM;
|
|
345
368
|
const resolve = async (intent?: { ours: string | null; theirs: string[] }): Promise<string> => {
|
|
346
|
-
const
|
|
369
|
+
const prompt = conflictResolvePrompt(filePath, conflicted, mergeTarget, intent);
|
|
370
|
+
if (prompt.length > maxChars) throw new MergeConflictInputTooLarge(prompt.length);
|
|
371
|
+
const streamLLM = options.stream ?? (await import('../../llm.js')).streamLLM;
|
|
372
|
+
const out = await streamLLM([{ role: 'user', content: prompt }], () => {}, { model: process.env.ELANOUS_CONFLICT_MODEL || tierModel('better'), reasoningEffort: 'medium' });
|
|
347
373
|
// ⛔ 라우터는 공급자가 전부 막히면 던지지 않고 오류 문구를 돌려준다 — 그것은 파일 내용이 아니다.
|
|
348
374
|
if (PROVIDER_FAILURE_TEXT.test(out)) throw new Error(`conflict resolve: LLM provider failure for ${filePath}`);
|
|
349
375
|
return `${out.replace(/^```[\w.-]*\n?/, '').replace(/\n?```\s*$/, '').trimEnd()}\n`;
|