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
|
@@ -38,6 +38,21 @@ const PROTECTED_LABELS: ReadonlySet<string> = new Set<string>([...PR_LABELS
|
|
|
38
38
|
const goalId = (branch: string): string | undefined => /(?:^|[-/])goalid-([a-f0-9]+)(?=-|\/|$)/i.exec(branch)?.[1]?.toLowerCase();
|
|
39
39
|
export const branchStem = (branch: string): string | undefined => /^self-impl\/(.+)-[a-f0-9]{8}-r[a-f0-9]+$/i.exec(branch)?.[1];
|
|
40
40
|
const slot = (body?: string): string | undefined => /^칸:\s*(.+?)\s*$/m.exec(body ?? '')?.[1];
|
|
41
|
+
const goalText = (body?: string): string | undefined => /^골:\s*(\S.*?)\s*$/m.exec(body ?? '')?.[1];
|
|
42
|
+
// Auto titles name only files («src/harness: harness-queue.ts») — different goals share them, so they never identify a goal.
|
|
43
|
+
export const isAutoTitle = (title: string): boolean =>
|
|
44
|
+
/^[\w./-]+:\s*[^\s,]+\.[a-z0-9]+(?:\s*,\s*[^\s,]+\.[a-z0-9]+)*$/i.test(title.trim());
|
|
45
|
+
|
|
46
|
+
/** Same goal (TC 10-05): ① both carry a goal identifier → compare it ② otherwise exact title ⊕ both harness drafts ⊕ same branch stem, never an auto title ③ else false. */
|
|
47
|
+
export function sameGoalPr(a: DraftTriagePr, b: DraftTriagePr): boolean {
|
|
48
|
+
const idA = goalId(a.branch), idB = goalId(b.branch);
|
|
49
|
+
if (idA && idB) return idA === idB;
|
|
50
|
+
const textA = goalText(a.body), textB = goalText(b.body);
|
|
51
|
+
if (textA && textB) return textA === textB;
|
|
52
|
+
if (!a.title || a.title !== b.title || isAutoTitle(a.title)) return false;
|
|
53
|
+
const stemA = branchStem(a.branch);
|
|
54
|
+
return Boolean(stemA && a.branch.startsWith('self-impl/') && b.branch.startsWith('self-impl/') && stemA === branchStem(b.branch));
|
|
55
|
+
}
|
|
41
56
|
const referencesDraft = (text: string, number: number): boolean =>
|
|
42
57
|
new RegExp(`(?:\\(수확\\s*#${number}\\s*\\)|\\bsuperseded\\s*#${number}(?!\\d))`, 'i').test(text);
|
|
43
58
|
const landingCommentReferencesDraft = (text: string, number: number): boolean =>
|
|
@@ -55,28 +70,30 @@ export function decideDraft({ draft, runStatus, mergedTwins, openDrafts, ageHour
|
|
|
55
70
|
return { action: 'keep', reason: 'live' };
|
|
56
71
|
}
|
|
57
72
|
const lineage = goalId(draft.branch);
|
|
58
|
-
const
|
|
59
|
-
(draft.title && pr.title === draft.title) || (lineage && goalId(pr.branch) === lineage)
|
|
60
|
-
));
|
|
61
|
-
if (twin) return { action: 'close', reason: `superseded-by #${twin.number}` };
|
|
73
|
+
const draftSlot = slot(draft.body);
|
|
62
74
|
const stem = branchStem(draft.branch);
|
|
75
|
+
const compatible = (pr: DraftTriagePr): boolean =>
|
|
76
|
+
!(lineage && goalId(pr.branch) && goalId(pr.branch) !== lineage)
|
|
77
|
+
&& !(draftSlot && slot(pr.body) && slot(pr.body) !== draftSlot);
|
|
78
|
+
const twin = mergedTwins.find((pr) => pr.number !== draft.number && compatible(pr) && sameGoalPr(draft, pr));
|
|
79
|
+
if (twin) return { action: 'close', reason: `superseded-by #${twin.number}` };
|
|
63
80
|
const createdAt = Date.parse(draft.createdAt ?? '');
|
|
64
81
|
const newer = stem && draft.title && Number.isFinite(createdAt) ? openDrafts
|
|
65
|
-
?.filter((pr) => pr.number !== draft.number && pr.title === draft.title && branchStem(pr.branch) === stem
|
|
82
|
+
?.filter((pr) => pr.number !== draft.number && pr.title === draft.title && branchStem(pr.branch) === stem && sameGoalPr(draft, pr)
|
|
66
83
|
&& Number.isFinite(Date.parse(pr.createdAt ?? '')) && Date.parse(pr.createdAt!) > createdAt)
|
|
67
84
|
.sort((a, b) => Date.parse(b.createdAt!) - Date.parse(a.createdAt!) || b.number - a.number)[0] : undefined;
|
|
68
85
|
if (newer) return { action: 'close', reason: `duplicate-of-open #${newer.number}` };
|
|
69
|
-
const harvest = mergedTwins.find((pr) => pr.number !== draft.number && (
|
|
86
|
+
const harvest = mergedTwins.find((pr) => pr.number !== draft.number && compatible(pr) && (
|
|
70
87
|
[pr.mergeCommitMessage, pr.body].some((text) => text && referencesDraft(text, draft.number))
|
|
71
88
|
|| pr.landingVerifiedComments?.some((text) => landingCommentReferencesDraft(text, draft.number))
|
|
72
89
|
));
|
|
73
90
|
if (harvest) return { action: 'close', reason: `superseded-by #${harvest.number} (harvest #${draft.number})` };
|
|
74
91
|
const created = Date.parse(draft.createdAt ?? '');
|
|
75
92
|
const files = draft.changedFiles;
|
|
76
|
-
const draftSlot = slot(draft.body);
|
|
77
93
|
const later = mergedTwins.filter((pr) => pr.number !== draft.number && Number.isFinite(created)
|
|
78
94
|
&& Number.isFinite(Date.parse(pr.mergedAt ?? '')) && Date.parse(pr.mergedAt!) > created
|
|
79
|
-
&& ((lineage && goalId(pr.branch) === lineage)
|
|
95
|
+
&& compatible(pr) && ((lineage && goalId(pr.branch) === lineage)
|
|
96
|
+
|| (draftSlot && slot(pr.body) === draftSlot) || (stem && branchStem(pr.branch) === stem)));
|
|
80
97
|
if (files?.length && files.every((file) => {
|
|
81
98
|
const changed = Date.parse(draft.latestFileChanges?.[file] ?? '');
|
|
82
99
|
return Number.isFinite(changed) && changed >= created && later.some((pr) =>
|
|
@@ -260,12 +260,23 @@ export interface SelfDevJobResult {
|
|
|
260
260
|
* (merged / pr-opened / gate-failed / review-blocked / pr-declined). */
|
|
261
261
|
stage?: string;
|
|
262
262
|
branch?: string;
|
|
263
|
+
/** Child confirmed the finished implementation was pushed for manual harvest. */
|
|
264
|
+
harvestable?: true;
|
|
263
265
|
worktreePath?: string;
|
|
264
266
|
prUrl?: string;
|
|
265
267
|
prNumber?: number;
|
|
266
268
|
merged?: boolean;
|
|
267
269
|
/** Child commit SHA, when observed by the Pod/host. */
|
|
268
270
|
checkedHeadCommit?: string;
|
|
271
|
+
/**
|
|
272
|
+
* This run's own `--base` (not main). A non-main completion mode
|
|
273
|
+
* (worktree-only, draft, `--base` other than main) lands commits here.
|
|
274
|
+
*/
|
|
275
|
+
base?: string;
|
|
276
|
+
/** Child finished with GOAL-COMPLETE (`ok=true`). Distinct from a main merge. */
|
|
277
|
+
ok?: boolean;
|
|
278
|
+
/** Commits this child added against its own `--base`. 0 is not completion progress. */
|
|
279
|
+
commitsAheadOfBase?: number;
|
|
269
280
|
/** A dependency is done but its result is not on main; no child was launched. */
|
|
270
281
|
blockReason?: 'parent-unlanded';
|
|
271
282
|
parentPrNumber?: number;
|
|
@@ -405,6 +416,7 @@ export type ResumeDisposition = 'skip' | 'rerun' | 'rerun-duplicate-risk';
|
|
|
405
416
|
*/
|
|
406
417
|
export function classifyResumeDisposition(result: SelfDevJobResult): ResumeDisposition {
|
|
407
418
|
if (hasDelivered(result)) return 'skip';
|
|
419
|
+
if (result.harvestable === true && result.branch) return 'skip';
|
|
408
420
|
if (result.stage === 'pr-opened') return 'rerun-duplicate-risk';
|
|
409
421
|
if (result.merged === false) return 'rerun';
|
|
410
422
|
if (result.status !== 'done') return 'rerun';
|
|
@@ -621,6 +633,7 @@ export function classifyFailure(
|
|
|
621
633
|
) {
|
|
622
634
|
return 'awaiting-human';
|
|
623
635
|
}
|
|
636
|
+
if (result.harvestable === true && result.branch) return 'main-sync-blocked';
|
|
624
637
|
if (result.status === 'done' && !hasDelivered(result)) return convergedFailureKind();
|
|
625
638
|
// ⭐ 상류가 죽어 «취소»된 조각 — 종전엔 status 가 failed 도 done 도 아니라 null 로 사라졌다.
|
|
626
639
|
// ⛔ 이것을 못 보면 「무엇이 상류를 기다리는가」를 슈퍼바이저가 영영 모른다.
|
|
@@ -1479,6 +1492,7 @@ export function orchestrateSelfDev(opts: OrchestrateSelfDevOptions): Promise<Sel
|
|
|
1479
1492
|
...(resumeDisposition ? { resumeDisposition } : {}),
|
|
1480
1493
|
...(disp?.stage ? { stage: disp.stage } : {}),
|
|
1481
1494
|
...(disp?.branch ? { branch: disp.branch } : {}),
|
|
1495
|
+
...(disp?.harvestable === true && disp.branch ? { harvestable: true } : {}),
|
|
1482
1496
|
...(disp?.worktreePath ? { worktreePath: disp.worktreePath } : {}),
|
|
1483
1497
|
...(disp?.prUrl ? { prUrl: disp.prUrl } : {}),
|
|
1484
1498
|
...(disp?.prNumber !== undefined ? { prNumber: disp.prNumber } : {}),
|
|
@@ -1526,7 +1540,7 @@ export function orchestrateSelfDev(opts: OrchestrateSelfDevOptions): Promise<Sel
|
|
|
1526
1540
|
const rm = opts.removeWorktree ?? defaultRemoveWorktree;
|
|
1527
1541
|
let removed = 0;
|
|
1528
1542
|
for (const r of out) {
|
|
1529
|
-
if (!r.worktreePath || r.prUrl) continue;
|
|
1543
|
+
if (!r.worktreePath || r.prUrl || r.harvestable) continue;
|
|
1530
1544
|
try { rm(r.worktreePath); removed++; } catch { /* fail-soft */ }
|
|
1531
1545
|
}
|
|
1532
1546
|
if (removed > 0) debug.log('self-dev.orchestrate', 'teardown', { removed });
|
|
@@ -73,13 +73,18 @@ export function buildReviewProviderAttempts(
|
|
|
73
73
|
const out: ReviewProviderAttempt[] = [primary];
|
|
74
74
|
const seen = new Set([primary.model]);
|
|
75
75
|
for (const f of fallbacks) {
|
|
76
|
-
if (!f.model.trim() || seen.has(f.model)) continue;
|
|
76
|
+
if (!f.model.trim() || seen.has(f.model) || (primary.provider && f.provider?.name === primary.provider.name)) continue;
|
|
77
77
|
seen.add(f.model);
|
|
78
78
|
out.push(f);
|
|
79
79
|
}
|
|
80
80
|
return out;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/** A distinct provider is available even when the optional config list is empty. */
|
|
84
|
+
export function defaultReviewFallbackModel(primaryProvider?: string): string {
|
|
85
|
+
return resolveModelAlias(primaryProvider === 'grok' ? 'claude' : 'grok')!;
|
|
86
|
+
}
|
|
87
|
+
|
|
83
88
|
export interface ReviewFallbackObservation {
|
|
84
89
|
readonly attempt: number;
|
|
85
90
|
readonly total: number;
|
|
@@ -10,6 +10,14 @@ import { sweepFrozenMerges } from '../self-implement/frozen-merges.js';
|
|
|
10
10
|
export interface SupervisorRound {
|
|
11
11
|
round: number;
|
|
12
12
|
landed: number;
|
|
13
|
+
/**
|
|
14
|
+
* Pieces that finished (`ok=true`) with at least one new commit against
|
|
15
|
+
* this run's own `--base` — not a main merge. Omitted on rounds that
|
|
16
|
+
* never measured it, so older history stays comparable.
|
|
17
|
+
*/
|
|
18
|
+
baseCompleted?: number;
|
|
19
|
+
/** The run's own `--base` those completions were counted against. */
|
|
20
|
+
base?: string;
|
|
13
21
|
actionable: number;
|
|
14
22
|
reviewMustFixTrend?: SupervisorDecision['reviewMustFixTrend'];
|
|
15
23
|
}
|
|
@@ -22,6 +30,7 @@ export type SupervisorStopReason =
|
|
|
22
30
|
| 'parent-signals-red'
|
|
23
31
|
| 'needs-human'
|
|
24
32
|
| 'no-actionable-work'
|
|
33
|
+
| 'harvestable-awaiting-human'
|
|
25
34
|
| 'handed-off-to-salvage'
|
|
26
35
|
| 'max-rounds'
|
|
27
36
|
| 'no-progress'
|
|
@@ -39,6 +48,7 @@ export const SUPERVISOR_STOP_REASONS = [
|
|
|
39
48
|
'parent-signals-red',
|
|
40
49
|
'needs-human',
|
|
41
50
|
'no-actionable-work',
|
|
51
|
+
'harvestable-awaiting-human',
|
|
42
52
|
'handed-off-to-salvage',
|
|
43
53
|
'max-rounds',
|
|
44
54
|
'no-progress',
|
|
@@ -105,9 +115,25 @@ export function countLanded(results: readonly SelfDevJobResult[]): number {
|
|
|
105
115
|
return results.filter(hasDelivered).length;
|
|
106
116
|
}
|
|
107
117
|
|
|
118
|
+
/** One base label for the round. Mixed or missing bases stay unnamed. */
|
|
119
|
+
export function roundBase(results: readonly SelfDevJobResult[]): string | undefined {
|
|
120
|
+
const bases = [...new Set(results.flatMap((result) => result.base === undefined ? [] : [result.base]))];
|
|
121
|
+
return bases.length === 1 ? bases[0] : undefined;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Finished pieces (`ok=true`) that added at least one commit against this
|
|
126
|
+
* run's own `--base`. A main merge is not required, and a commit without
|
|
127
|
+
* `ok=true` is not completion.
|
|
128
|
+
*/
|
|
129
|
+
export function countBaseCompleted(results: readonly SelfDevJobResult[]): number {
|
|
130
|
+
return results.filter((result) => result.ok === true && (result.commitsAheadOfBase ?? 0) >= 1).length;
|
|
131
|
+
}
|
|
132
|
+
|
|
108
133
|
export function madeProgress(prev: SupervisorRound | undefined, next: SupervisorRound): boolean {
|
|
109
134
|
if (!prev) return true;
|
|
110
135
|
if (next.landed > 0) return true;
|
|
136
|
+
if ((next.baseCompleted ?? 0) > (prev.baseCompleted ?? 0)) return true;
|
|
111
137
|
if (next.actionable < prev.actionable) return true;
|
|
112
138
|
return next.reviewMustFixTrend === 'improving';
|
|
113
139
|
}
|
|
@@ -191,8 +217,16 @@ export function decideNextRun(input: {
|
|
|
191
217
|
const t = triageRun(activeResults, deployFindings, observation === 'failed');
|
|
192
218
|
const round = history.length;
|
|
193
219
|
const landed = countLanded(results);
|
|
220
|
+
const baseCompleted = countBaseCompleted(results);
|
|
221
|
+
const completionBase = roundBase(results);
|
|
194
222
|
const actionable = countActionable(t);
|
|
195
|
-
const thisRound: SupervisorRound = {
|
|
223
|
+
const thisRound: SupervisorRound = {
|
|
224
|
+
round,
|
|
225
|
+
landed,
|
|
226
|
+
...(baseCompleted > 0 || completionBase !== undefined ? { baseCompleted, ...(completionBase === undefined ? {} : { base: completionBase }) } : {}),
|
|
227
|
+
actionable,
|
|
228
|
+
reviewMustFixTrend,
|
|
229
|
+
};
|
|
196
230
|
const goalPlanRevisionObservation = formatGoalPlanRevisionObservation(results);
|
|
197
231
|
const withGoalPlanRevisionObservation = (why: string): string => goalPlanRevisionObservation
|
|
198
232
|
&& goalPlanRevisionObservation !== GOAL_PLAN_REVISION_OBSERVATION_UNAVAILABLE
|
|
@@ -226,6 +260,16 @@ export function decideNextRun(input: {
|
|
|
226
260
|
};
|
|
227
261
|
}
|
|
228
262
|
|
|
263
|
+
const harvestable = results.filter((result) => result.harvestable === true && !!result.branch);
|
|
264
|
+
if (harvestable.length > 0) {
|
|
265
|
+
return {
|
|
266
|
+
...base,
|
|
267
|
+
action: 'stop',
|
|
268
|
+
stopReason: 'harvestable-awaiting-human',
|
|
269
|
+
why: withGoalPlanRevisionObservation(`구현 완료 · 수확 대기 — ${harvestable.map((result) => result.branch).join(' · ')}`),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
229
273
|
if (deliverableMergeCompleteness === 'complete') {
|
|
230
274
|
return {
|
|
231
275
|
...base,
|
|
@@ -364,7 +408,7 @@ export function decideNextRun(input: {
|
|
|
364
408
|
...base,
|
|
365
409
|
action: 'stop',
|
|
366
410
|
stopReason: 'no-progress',
|
|
367
|
-
why: withNoProgressGoalPlanRevisionObservation(`${stallRounds}라운드 연속 제자리 —
|
|
411
|
+
why: withNoProgressGoalPlanRevisionObservation(`${stallRounds}라운드 연속 제자리 — base=${completionBase ?? 'main'} 기준 완료 ${baseCompleted} · 착지 ${landed} · 남은 조각도 안 줄었다(${actionable}). 골·불변식을 의심할 자리`),
|
|
368
412
|
};
|
|
369
413
|
}
|
|
370
414
|
}
|
|
@@ -390,9 +434,12 @@ export function appendRound(
|
|
|
390
434
|
reviewMustFixTrend?: SupervisorDecision['reviewMustFixTrend'],
|
|
391
435
|
): SupervisorRound[] {
|
|
392
436
|
const t = triageRun(results.filter((result) => result.salvage !== 'launched'));
|
|
437
|
+
const baseCompleted = countBaseCompleted(results);
|
|
438
|
+
const base = roundBase(results);
|
|
393
439
|
return [...history, {
|
|
394
440
|
round: history.length,
|
|
395
441
|
landed: countLanded(results),
|
|
442
|
+
...(baseCompleted > 0 || base !== undefined ? { baseCompleted, ...(base === undefined ? {} : { base }) } : {}),
|
|
396
443
|
actionable: countActionable(t),
|
|
397
444
|
...(reviewMustFixTrend === undefined ? {} : { reviewMustFixTrend }),
|
|
398
445
|
}];
|
|
@@ -7,7 +7,7 @@ import { debug } from '../debug/log.js';
|
|
|
7
7
|
import { beginLandingMerge, readLandingFreeze, type LandingFreeze } from '../release-loop/landing-freeze.js';
|
|
8
8
|
|
|
9
9
|
/** `manual`: the owning repository could not be determined — a sweep never resumes it; a person does. */
|
|
10
|
-
export interface FrozenMerge { prNumber: number; headCommit: string; repoRoot: string; manual?: true }
|
|
10
|
+
export interface FrozenMerge { prNumber: number; headCommit: string; repoRoot: string; manual?: true; /** Host goal document, so a resumed merge can still apply its «이 칸 완료» declaration (CL-AUTO). */ goalFile?: string }
|
|
11
11
|
/** What a resume sweep hands its merge function: the held entry, marked as already claimed by this sweep. */
|
|
12
12
|
export type ResumedMerge = FrozenMerge & { resumed: true };
|
|
13
13
|
|
|
@@ -65,7 +65,8 @@ function readQueue(root: string): FrozenMerge[] {
|
|
|
65
65
|
Number.isSafeInteger(item.prNumber) && item.prNumber > 0 &&
|
|
66
66
|
typeof item.headCommit === 'string' && /^[a-f0-9]{40}$/i.test(item.headCommit) &&
|
|
67
67
|
typeof item.repoRoot === 'string' && item.repoRoot.length > 0 &&
|
|
68
|
-
(item.manual === undefined || item.manual === true)
|
|
68
|
+
(item.manual === undefined || item.manual === true) &&
|
|
69
|
+
(item.goalFile === undefined || (typeof item.goalFile === 'string' && item.goalFile.length > 0)))) throw new Error(`invalid frozen merge queue: ${path}`);
|
|
69
70
|
return data as FrozenMerge[];
|
|
70
71
|
}
|
|
71
72
|
|
|
@@ -6,6 +6,7 @@ import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from 'no
|
|
|
6
6
|
import { groundMissionInCodebase, isRepositoryImplementationCandidate, type CodebaseGrounding } from '../autopilot/mission-codebase-gate.js';
|
|
7
7
|
import { extractSlugSource, generateMissionSlug, slugify } from '../autopilot/mission-registry.js';
|
|
8
8
|
import { enhancePrompt, type EnhanceOpts, type EnhanceResult } from '../prompt-enhance/enhance.js';
|
|
9
|
+
import type { LLMUsage } from '../prompt-cache/types.js';
|
|
9
10
|
import { debug } from '../debug/log.js';
|
|
10
11
|
|
|
11
12
|
function observeGoalAuthor(
|
|
@@ -324,9 +325,12 @@ function startGoalAuthorPhase(phase: GoalAuthorTimedPhase, authorRunId: string,
|
|
|
324
325
|
return Date.now();
|
|
325
326
|
}
|
|
326
327
|
|
|
327
|
-
|
|
328
|
+
type GoalAuthorTokens = { inputTokens: number | null; outputTokens: number | null };
|
|
329
|
+
const UNKNOWN_GOAL_AUTHOR_TOKENS: GoalAuthorTokens = { inputTokens: null, outputTokens: null };
|
|
330
|
+
|
|
331
|
+
function endGoalAuthorPhase(phase: GoalAuthorTimedPhase, startedAt: number, authorRunId: string, onProgress?: GoalAuthorDeps['onProgress'], tokens: GoalAuthorTokens = UNKNOWN_GOAL_AUTHOR_TOKENS): void {
|
|
328
332
|
const elapsedMs = Date.now() - startedAt;
|
|
329
|
-
observeGoalAuthor('goal-author', 'phase-end', { phase, authorRunId, elapsedMs });
|
|
333
|
+
observeGoalAuthor('goal-author', 'phase-end', { phase, authorRunId, elapsedMs, ...tokens });
|
|
330
334
|
if (GOAL_AUTHOR_PHASES.includes(phase as GoalAuthorPhase)) {
|
|
331
335
|
if (onProgress) onProgress(phase as GoalAuthorPhase, 'end');
|
|
332
336
|
else process.stderr.write(`[goal-author] ${phase} ended in ${elapsedMs}ms\n`);
|
|
@@ -1156,6 +1160,21 @@ function observeAuthoredGroundingPathSections(document: string, paths: readonly
|
|
|
1156
1160
|
}
|
|
1157
1161
|
}
|
|
1158
1162
|
|
|
1163
|
+
function authoredGoalSectionChars(document: string): Record<string, number> {
|
|
1164
|
+
const headings = linesOutsideFencedCode(document)
|
|
1165
|
+
.filter(({ text }) => /^## (?:\S)/.test(text))
|
|
1166
|
+
.map(({ text, start }) => ({ heading: text.slice(3).trim(), start }));
|
|
1167
|
+
const sections: Record<string, number> = { preamble: [...document.slice(0, headings[0]?.start ?? document.length)].length };
|
|
1168
|
+
for (let index = 0; index < headings.length; index += 1) {
|
|
1169
|
+
const { heading, start } = headings[index];
|
|
1170
|
+
sections[heading] = (sections[heading] ?? 0) + [...document.slice(start, headings[index + 1]?.start ?? document.length)].length;
|
|
1171
|
+
}
|
|
1172
|
+
for (const heading of ['PROBLEM', 'WHAT TO BUILD', 'ACCEPTANCE CRITERIA', 'TRACED PATHS', 'SCOPE BOUNDARY', 'STEPS', '답하지 못하는 것']) {
|
|
1173
|
+
sections[heading] ??= 0;
|
|
1174
|
+
}
|
|
1175
|
+
return sections;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1159
1178
|
function canonicalBlocks(document: string): string[] {
|
|
1160
1179
|
return linesOutsideFencedCode(document)
|
|
1161
1180
|
.filter((line) => /^##(?:\s|$)/.test(line.text))
|
|
@@ -4906,6 +4925,13 @@ async function authorGoalWithSupersededRootIntent(
|
|
|
4906
4925
|
}
|
|
4907
4926
|
// ⭐ 이 저작 «한 번»의 id. goalId 보다 «먼저» 있어야 ground·enhance 의 소요를 그 골에 붙일 수 있다.
|
|
4908
4927
|
const authorRunId = generateGoalId();
|
|
4928
|
+
const authorStartedAt = Date.now();
|
|
4929
|
+
const enhanceTokens: GoalAuthorTokens = { inputTokens: null, outputTokens: null };
|
|
4930
|
+
const onEnhanceUsage = (usage: LLMUsage): void => {
|
|
4931
|
+
if (usage.inputTokens !== undefined) enhanceTokens.inputTokens = (enhanceTokens.inputTokens ?? 0) + usage.inputTokens;
|
|
4932
|
+
if (usage.outputTokens !== undefined) enhanceTokens.outputTokens = (enhanceTokens.outputTokens ?? 0) + usage.outputTokens;
|
|
4933
|
+
try { deps.enhanceOpts?.onUsage?.(usage); } catch { /* usage observation is fail-soft */ }
|
|
4934
|
+
};
|
|
4909
4935
|
let facts: CodebaseGrounding | null = null;
|
|
4910
4936
|
let groundingError = false;
|
|
4911
4937
|
const persistentGrounding = resolveGoalAuthorPersistentGrounding('persistentGrounding' in deps ? deps.persistentGrounding : undefined);
|
|
@@ -4937,8 +4963,8 @@ async function authorGoalWithSupersededRootIntent(
|
|
|
4937
4963
|
? await deps.enhance(
|
|
4938
4964
|
enhanceAsk,
|
|
4939
4965
|
persistentFacts.length > 0
|
|
4940
|
-
? { ...deps.enhanceOpts, groundedFacts: persistentFacts }
|
|
4941
|
-
: deps.enhanceOpts,
|
|
4966
|
+
? { ...deps.enhanceOpts, groundedFacts: persistentFacts, onUsage: onEnhanceUsage }
|
|
4967
|
+
: { ...deps.enhanceOpts, onUsage: onEnhanceUsage },
|
|
4942
4968
|
)
|
|
4943
4969
|
: { original: ask, checklist: [], verbatimPreserved: true };
|
|
4944
4970
|
// ⭐ goal-context 규범이 «어디에도 안 실린다»는 것을 관측으로 남긴다.
|
|
@@ -4956,7 +4982,7 @@ async function authorGoalWithSupersededRootIntent(
|
|
|
4956
4982
|
placement: 'omitted',
|
|
4957
4983
|
});
|
|
4958
4984
|
} finally {
|
|
4959
|
-
endGoalAuthorPhase('enhance', enhanceStartedAt, authorRunId, deps.onProgress);
|
|
4985
|
+
endGoalAuthorPhase('enhance', enhanceStartedAt, authorRunId, deps.onProgress, enhanceTokens);
|
|
4960
4986
|
}
|
|
4961
4987
|
if (!enhancement.verbatimPreserved || (enhanceAsk && enhancement.original !== enhanceAsk)) {
|
|
4962
4988
|
throw new Error('goal author refused an enhancement that does not preserve its input verbatim');
|
|
@@ -5431,6 +5457,18 @@ async function authorGoalWithSupersededRootIntent(
|
|
|
5431
5457
|
} finally {
|
|
5432
5458
|
endGoalAuthorPhase('lint', lintStartedAt, authorRunId, deps.onProgress);
|
|
5433
5459
|
}
|
|
5460
|
+
try {
|
|
5461
|
+
observeGoalAuthor('goal-author', 'summary', {
|
|
5462
|
+
authorRunId,
|
|
5463
|
+
elapsedMs: Date.now() - authorStartedAt,
|
|
5464
|
+
inputTokens: enhanceTokens.inputTokens,
|
|
5465
|
+
outputTokens: enhanceTokens.outputTokens,
|
|
5466
|
+
totalTokens: enhanceTokens.inputTokens === null || enhanceTokens.outputTokens === null
|
|
5467
|
+
? null : enhanceTokens.inputTokens + enhanceTokens.outputTokens,
|
|
5468
|
+
goalChars: [...document].length,
|
|
5469
|
+
sectionChars: authoredGoalSectionChars(document),
|
|
5470
|
+
});
|
|
5471
|
+
} catch { /* observation is fail-soft */ }
|
|
5434
5472
|
return { document, facts, grounded: !groundingError && hasEvidence, authorRunId };
|
|
5435
5473
|
} finally {
|
|
5436
5474
|
endAssembleSubphase();
|
|
@@ -35,6 +35,8 @@ type GoalClarificationAnsweredBy = 'human' | 'agent' | 'none' | 'mixed';
|
|
|
35
35
|
|
|
36
36
|
export interface GoalClarificationEscalationInput {
|
|
37
37
|
goalFile: string;
|
|
38
|
+
/** Explicit provenance for isolated test fixtures; never inferred from goalFile. */
|
|
39
|
+
testOrigin?: true;
|
|
38
40
|
delivery?: GoalClarificationDelivery;
|
|
39
41
|
resolvedDelivery?: GoalClarificationObservedDelivery;
|
|
40
42
|
dispatch?: (
|
|
@@ -577,6 +579,9 @@ export async function escalateGoalDocumentClarifications(
|
|
|
577
579
|
}],
|
|
578
580
|
},
|
|
579
581
|
input.dispatchContext?.sessionId,
|
|
582
|
+
{},
|
|
583
|
+
undefined,
|
|
584
|
+
{ testOrigin: input.testOrigin },
|
|
580
585
|
);
|
|
581
586
|
pendingQuestionPersistence.write(pending);
|
|
582
587
|
pendingWritten += 1;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
4
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
4
5
|
import { debug } from '../debug/log.js';
|
|
6
|
+
import { getUserConfig } from '../user-config.js';
|
|
7
|
+
import { psProcessStartMs, START_TOLERANCE_MS } from '../harness/harness-stop.js';
|
|
5
8
|
|
|
6
9
|
export type FailureSource = 'release-run' | 'harness-run' | 'loop-tick' | 'cron';
|
|
7
10
|
|
|
@@ -14,6 +17,114 @@ export interface FailureEvent {
|
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
const FOLD_WINDOW_MS = 60 * 60 * 1_000;
|
|
20
|
+
const HEAL_GRAPH = join(import.meta.dir, '../../graphs/heal/heal-loop.yaml');
|
|
21
|
+
|
|
22
|
+
type HealLoopOptions = { runId: string; input: { failureEvent: FailureEvent }; deps: { root: string } };
|
|
23
|
+
type StartHealLoop = (file: string, options: HealLoopOptions) => Promise<unknown>;
|
|
24
|
+
|
|
25
|
+
function healRunId(event: FailureEvent): string {
|
|
26
|
+
return `heal-${createHash('sha256').update(JSON.stringify([event.source, event.ref, event.at])).digest('hex')}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function maxConcurrentLoops(): number {
|
|
30
|
+
const raw = getUserConfig().raw.heal;
|
|
31
|
+
const cap = raw && typeof raw === 'object' && !Array.isArray(raw)
|
|
32
|
+
? (raw as Record<string, unknown>).maxConcurrentLoops : undefined;
|
|
33
|
+
return typeof cap === 'number' && Number.isSafeInteger(cap) && cap > 0 ? cap : 2;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ownerAlive(pid: unknown, pidStartedAt: unknown): boolean {
|
|
37
|
+
if (!Number.isSafeInteger(pid) || (pid as number) <= 0 || typeof pidStartedAt !== 'string') return false;
|
|
38
|
+
const started = Date.parse(pidStartedAt);
|
|
39
|
+
if (!Number.isFinite(started)) return false;
|
|
40
|
+
try {
|
|
41
|
+
const actual = psProcessStartMs(pid as number);
|
|
42
|
+
return actual !== null && Number.isFinite(actual) && Math.abs(actual - started) <= START_TOLERANCE_MS;
|
|
43
|
+
} catch { return false; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function liveHealRuns(root: string): Set<string> {
|
|
47
|
+
const live = new Set<string>();
|
|
48
|
+
const dir = join(root, 'graph-runs', 'heal-loop');
|
|
49
|
+
let files: string[];
|
|
50
|
+
try { files = readdirSync(dir); }
|
|
51
|
+
catch (error) {
|
|
52
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return live;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
if (!file.endsWith('.json')) continue;
|
|
57
|
+
try {
|
|
58
|
+
const state = JSON.parse(readFileSync(join(dir, file), 'utf8')) as Record<string, unknown>;
|
|
59
|
+
if (state.graphId === 'heal-loop' && state.runId === file.slice(0, -5) &&
|
|
60
|
+
state.status === 'running' && ownerAlive(state.pid, state.pidStartedAt)) live.add(state.runId as string);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT' && !(error instanceof SyntaxError)) throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return live;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A test process must never start the real heal loop — it spawns external grounding (10-05 load 150 incident). */
|
|
69
|
+
export async function startHealLoop(file: string, options: HealLoopOptions, env: NodeJS.ProcessEnv = process.env,
|
|
70
|
+
run: StartHealLoop = async (graph, args) => (await import('../graph-runner/runner.js')).runGraph(graph, args)): Promise<unknown> {
|
|
71
|
+
if (env.NODE_ENV === 'test' || env.ELANOUS_TEST_HOME) {
|
|
72
|
+
debug.log('heal.intake', 'loop-start-skipped-test', { runId: options.runId });
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const root = options.deps.root;
|
|
76
|
+
const claimDir = join(root, 'heal', 'starts');
|
|
77
|
+
const selected = withInboxLock(inboxPath(root), () => {
|
|
78
|
+
mkdirSync(claimDir, { recursive: true });
|
|
79
|
+
const live = liveHealRuns(root);
|
|
80
|
+
const claimed = new Set<string>();
|
|
81
|
+
for (const name of readdirSync(claimDir)) {
|
|
82
|
+
if (!/^heal-[a-f0-9]{64}\.json$/.test(name)) continue;
|
|
83
|
+
const runId = name.slice(0, -5);
|
|
84
|
+
const path = join(claimDir, name);
|
|
85
|
+
const claim = JSON.parse(readFileSync(path, 'utf8')) as { active?: boolean; started?: boolean; pid?: number; pidStartedAt?: string };
|
|
86
|
+
const owner = claim.active && ownerAlive(claim.pid, claim.pidStartedAt);
|
|
87
|
+
if (!owner && !claim.started && !existsSync(join(root, 'graph-runs', 'heal-loop', `${runId}.json`))) {
|
|
88
|
+
unlinkSync(path);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
claimed.add(runId);
|
|
92
|
+
if (owner && !live.has(runId)) live.add(runId);
|
|
93
|
+
}
|
|
94
|
+
const cap = maxConcurrentLoops();
|
|
95
|
+
if (live.size >= cap) {
|
|
96
|
+
debug.log('heal.intake', 'loop-start-deferred', { running: live.size, cap, runId: options.runId });
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const next = readInbox(inboxPath(root)).find(event => !claimed.has(healRunId(event)) &&
|
|
100
|
+
!existsSync(join(root, 'graph-runs', 'heal-loop', `${healRunId(event)}.json`)));
|
|
101
|
+
if (!next) return undefined;
|
|
102
|
+
const runId = healRunId(next);
|
|
103
|
+
writeFileSync(join(claimDir, `${runId}.json`), JSON.stringify({ active: true, pid: process.pid,
|
|
104
|
+
pidStartedAt: new Date(Date.now() - process.uptime() * 1_000).toISOString() }), { flag: 'wx' });
|
|
105
|
+
return { event: next, runId };
|
|
106
|
+
});
|
|
107
|
+
if (!selected) return undefined;
|
|
108
|
+
let started = false;
|
|
109
|
+
try {
|
|
110
|
+
const result = await run(file, { runId: selected.runId, input: { failureEvent: selected.event }, deps: { root } });
|
|
111
|
+
started = true;
|
|
112
|
+
return result;
|
|
113
|
+
} finally {
|
|
114
|
+
withInboxLock(inboxPath(root), () => {
|
|
115
|
+
const claim = join(claimDir, `${selected.runId}.json`);
|
|
116
|
+
if (started || existsSync(join(root, 'graph-runs', 'heal-loop', `${selected.runId}.json`))) {
|
|
117
|
+
writeFileSync(claim, JSON.stringify({ active: false, started: true }));
|
|
118
|
+
} else {
|
|
119
|
+
unlinkSync(claim);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
// A completed run opens a slot; a pre-ledger failure waits for the next intake rather than retrying in a tight loop.
|
|
123
|
+
if (started) void Promise.resolve().then(() => startHealLoop(file, options, env, run)).catch(error => {
|
|
124
|
+
debug.log('heal.intake', 'loop-start-failed', { runId: selected.runId, error: String(error) }, { level: 'error' });
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
17
128
|
|
|
18
129
|
function inboxPath(root: string): string {
|
|
19
130
|
return join(root, 'heal', 'inbox.jsonl');
|
|
@@ -55,7 +166,7 @@ function withInboxLock<T>(file: string, work: () => T): T {
|
|
|
55
166
|
}
|
|
56
167
|
|
|
57
168
|
/** One durable JSON line per failure; repeat source+ref within an hour is folded. */
|
|
58
|
-
export function recordFailureEvent(event: FailureEvent, root = effectiveInstanceRoot()): { folded: boolean } {
|
|
169
|
+
export function recordFailureEvent(event: FailureEvent, root = effectiveInstanceRoot(), startLoop: StartHealLoop = startHealLoop): { folded: boolean } {
|
|
59
170
|
if (!['release-run', 'harness-run', 'loop-tick', 'cron'].includes(event.source) ||
|
|
60
171
|
!event.kind || !event.ref || !event.summary || !Number.isFinite(Date.parse(event.at))) {
|
|
61
172
|
throw new Error('invalid heal failure event');
|
|
@@ -69,6 +180,13 @@ export function recordFailureEvent(event: FailureEvent, root = effectiveInstance
|
|
|
69
180
|
return false;
|
|
70
181
|
});
|
|
71
182
|
debug.log('heal.intake', 'recorded', { source: event.source, kind: event.kind, ref: event.ref, folded });
|
|
183
|
+
if (!folded) {
|
|
184
|
+
const runId = healRunId(event);
|
|
185
|
+
void Promise.resolve().then(() => startLoop(HEAL_GRAPH, { runId, input: { failureEvent: event }, deps: { root } }))
|
|
186
|
+
.catch(error => {
|
|
187
|
+
debug.log('heal.intake', 'loop-start-failed', { source: event.source, kind: event.kind, ref: event.ref, runId, error: String(error) }, { level: 'error' });
|
|
188
|
+
});
|
|
189
|
+
}
|
|
72
190
|
return { folded };
|
|
73
191
|
}
|
|
74
192
|
|
|
@@ -20,7 +20,7 @@ export function needsPwaBuild(files: readonly string[]): boolean {
|
|
|
20
20
|
return files.some((file) => file.startsWith('apps/pwa/') || (file.startsWith('src/') && !/\.test\.tsx?$/.test(file)));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export interface HostRegateInput { prNumber: number; headCommit: string; repoRoot: string; verifyOnly?: boolean; /** Called by a resume sweep that already holds this PR's claim. */ resumed?: true }
|
|
23
|
+
export interface HostRegateInput { prNumber: number; headCommit: string; repoRoot: string; goalFile?: string; verifyOnly?: boolean; /** Called by a resume sweep that already holds this PR's claim. */ resumed?: true }
|
|
24
24
|
export interface HostRegateResult { passed: boolean; failures: Array<{ step: string; detail: string }>; os: string; status?: 'passed' | 'failed' | 'unmeasured' | 'frozen'; /** verifyOnly: 실제로 얹어 잰 base 끝 */ baseCommit?: string }
|
|
25
25
|
export type HostRegateDeps = {
|
|
26
26
|
command?: (bin: string, args: readonly string[], cwd: string, env?: NodeJS.ProcessEnv) => { status: number | null; stdout: string; stderr: string };
|
|
@@ -31,6 +31,7 @@ export type HostRegateDeps = {
|
|
|
31
31
|
log?: (event: 'passed' | 'failed' | 'unmeasured' | 'base-raced', data: Record<string, unknown>) => void;
|
|
32
32
|
freezeRoot?: string;
|
|
33
33
|
runExposeGate?: typeof runExposeGate;
|
|
34
|
+
syncMergedChecklist?: typeof import('../release-loop/merged-pr-checklist.js').syncMergedPrChecklist;
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
const SPAWN_MAX_BUFFER = 256 * 1024 * 1024;
|
|
@@ -240,7 +241,7 @@ export async function runHostRegate(input: HostRegateInput, deps: HostRegateDeps
|
|
|
240
241
|
// --match-head-commit pins the head. gh has no base pin, so the base was re-read just
|
|
241
242
|
// above; the seconds between that read and the merge are checked after the fact below.
|
|
242
243
|
if (input.verifyOnly) return result('passed');
|
|
243
|
-
const landing = admitLandingMerge({ prNumber: input.prNumber, headCommit: input.headCommit, repoRoot: input.repoRoot }, deps.freezeRoot, {}, input.resumed ? undefined : { prNumber: input.prNumber, repoRoot: input.repoRoot, headCommit: input.headCommit });
|
|
244
|
+
const landing = admitLandingMerge({ prNumber: input.prNumber, headCommit: input.headCommit, repoRoot: input.repoRoot, ...(input.goalFile ? { goalFile: input.goalFile } : {}) }, deps.freezeRoot, {}, input.resumed ? undefined : { prNumber: input.prNumber, repoRoot: input.repoRoot, headCommit: input.headCommit });
|
|
244
245
|
if (landing.kind !== 'merge') {
|
|
245
246
|
debug.log('harness.merge', 'frozen', { pr: input.prNumber, ...(landing.kind === 'held' ? { reason: landing.freeze.reason, until: landing.freeze.until } : { resumedElsewhere: true }) });
|
|
246
247
|
return { passed: true, failures: [], os: process.platform, status: 'frozen' };
|
|
@@ -255,6 +256,12 @@ export async function runHostRegate(input: HostRegateInput, deps: HostRegateDeps
|
|
|
255
256
|
catch (e) { return result('unmeasured', 'merge-confirm', String(e)); }
|
|
256
257
|
if (merged.state !== 'MERGED') return result('unmeasured', 'merge-confirm', `PR state after merge is ${merged.state ?? 'unknown'}`);
|
|
257
258
|
landed = true;
|
|
259
|
+
try {
|
|
260
|
+
const sync = deps.syncMergedChecklist ?? (await import('../release-loop/merged-pr-checklist.js')).syncMergedPrChecklist;
|
|
261
|
+
sync(input.prNumber, input.repoRoot, input.goalFile);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
debug.log('release.checklist', 'merged-pr-sync-failed', { pr: input.prNumber, error: String(error) });
|
|
264
|
+
}
|
|
258
265
|
const mergeCommit = merged.mergeCommit?.oid ?? '';
|
|
259
266
|
let mergedOnto = '';
|
|
260
267
|
try {
|