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
|
@@ -3,10 +3,17 @@ import { lstatSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { debug } from '../debug/log.js';
|
|
5
5
|
|
|
6
|
-
export type RevertGuardResult = { reverted: string[]; protected: string[]; warning?: string };
|
|
6
|
+
export type RevertGuardResult = { reverted: string[]; protected: string[]; staleDeleted?: string[]; outsideDeleted?: string[]; warning?: string };
|
|
7
|
+
|
|
8
|
+
function isTargetPath(file: string, targets: readonly string[]): boolean {
|
|
9
|
+
return targets.some((target) => {
|
|
10
|
+
const path = target.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
11
|
+
return path !== '' && (file === path || file.startsWith(`${path}/`));
|
|
12
|
+
});
|
|
13
|
+
}
|
|
7
14
|
|
|
8
15
|
/** Compare the worktree's exact bytes with blobs from recent HEAD ancestry, before add -A. */
|
|
9
|
-
export function dropStaleReverts(cwd: string, targets: readonly string[], runId: string, depth = 50): RevertGuardResult {
|
|
16
|
+
export function dropStaleReverts(cwd: string, targets: readonly string[], runId: string, depth = 50, base?: string): RevertGuardResult {
|
|
10
17
|
const result: RevertGuardResult = { reverted: [], protected: [] };
|
|
11
18
|
const git = (...args: string[]): Buffer => execFileSync('git', args, { cwd, timeout: 20_000, maxBuffer: 16 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
12
19
|
const reason = (error: unknown): string => (error instanceof Error ? error.message : String(error)).replace(/\s+/g, ' ').slice(0, 240);
|
|
@@ -15,6 +22,38 @@ export function dropStaleReverts(cwd: string, targets: readonly string[], runId:
|
|
|
15
22
|
const commits = git('rev-list', `--max-count=${limit + 1}`, 'HEAD').toString('utf8').trim().split('\n');
|
|
16
23
|
const changed = git('diff', '--name-only', '--diff-filter=M', '-z', 'HEAD', '--').toString('utf8').split('\0').filter(Boolean);
|
|
17
24
|
const staged = git('diff', '--cached', '--name-only', '--diff-filter=M', '-z', 'HEAD', '--').toString('utf8').split('\0').filter(Boolean);
|
|
25
|
+
const deleted = new Set([
|
|
26
|
+
...git('diff', '--name-only', '--diff-filter=D', '-z', 'HEAD', '--').toString('utf8').split('\0').filter(Boolean),
|
|
27
|
+
...git('diff', '--cached', '--name-only', '--diff-filter=D', '-z', 'HEAD', '--').toString('utf8').split('\0').filter(Boolean),
|
|
28
|
+
]);
|
|
29
|
+
for (const file of deleted) {
|
|
30
|
+
if (isTargetPath(file, targets)) continue;
|
|
31
|
+
try {
|
|
32
|
+
// add -A stages the worktree: even a dangling symlink replaces the staged deletion.
|
|
33
|
+
try {
|
|
34
|
+
lstatSync(join(cwd, file));
|
|
35
|
+
continue;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
38
|
+
}
|
|
39
|
+
// ls-tree returning an empty entry means absent; a failed lookup must not look like absence.
|
|
40
|
+
const headEntry = git('ls-tree', 'HEAD', '--', file).toString('utf8').trim();
|
|
41
|
+
if (!/^\d+ blob [0-9a-f]+\t/.test(headEntry)) continue;
|
|
42
|
+
const baseEntry = base ? git('ls-tree', base, '--', file).toString('utf8').trim() : null;
|
|
43
|
+
if (baseEntry === '' && base) {
|
|
44
|
+
git('checkout', 'HEAD', '--', file);
|
|
45
|
+
(result.staleDeleted ??= []).push(file);
|
|
46
|
+
result.reverted.push(file);
|
|
47
|
+
debug.log('self-implement.revert-guard', 'reverted-stale-delete', { runId, files: [file] });
|
|
48
|
+
} else {
|
|
49
|
+
(result.outsideDeleted ??= []).push(file);
|
|
50
|
+
debug.log('self-implement.revert-guard', 'outside-delete', { runId, files: [file] }, { level: 'warn' });
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
result.warning = `git inspection/checkout failed for ${file}: ${reason(error)}`;
|
|
54
|
+
debug.log('self-implement.revert-guard', 'fail-open', { runId, reason: result.warning }, { level: 'warn' });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
18
57
|
for (const file of new Set([...changed, ...staged])) {
|
|
19
58
|
try {
|
|
20
59
|
if (!lstatSync(join(cwd, file)).isFile()) continue;
|
|
@@ -37,11 +76,7 @@ export function dropStaleReverts(cwd: string, targets: readonly string[], runId:
|
|
|
37
76
|
}
|
|
38
77
|
}
|
|
39
78
|
if (!matchedCommit) continue;
|
|
40
|
-
|
|
41
|
-
const path = target.replace(/^\.\//, '').replace(/\/+$/, '');
|
|
42
|
-
return path !== '' && (file === path || file.startsWith(`${path}/`));
|
|
43
|
-
});
|
|
44
|
-
if (scoped) {
|
|
79
|
+
if (isTargetPath(file, targets)) {
|
|
45
80
|
result.protected.push(file);
|
|
46
81
|
debug.log('self-implement.revert-guard', 'protected-target', { runId, files: [file], matchedCommit }, { level: 'warn' });
|
|
47
82
|
} else {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
2
2
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { refuseProductionLedgerWriteInTest } from '../harness/ledger-write-guard.js';
|
|
3
4
|
import { tracedPathReferences, verbatimOriginalAsk } from './goal-author.js';
|
|
4
5
|
import { parseAskTargetPathHintsResult } from '../self-dev/launch-preflight.js';
|
|
5
6
|
import { isRunStatus } from './run-status-mapping.js';
|
|
@@ -402,6 +403,7 @@ export function describeFederatedMissingRunLedger(runId: string, options: Federa
|
|
|
402
403
|
|
|
403
404
|
/** Append one observer event. Callers own fail-soft handling. */
|
|
404
405
|
export function appendRunLedgerEntry(entry: RunLedgerEntry, dir = runLedgerDir()): void {
|
|
406
|
+
if (refuseProductionLedgerWriteInTest(dir, 'run-ledger')) return;
|
|
405
407
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
406
408
|
const live = runLedgerPath(entry.runId, dir);
|
|
407
409
|
withRunLedgerLock(dir, entry.runId, () => {
|
|
@@ -16,6 +16,7 @@ export type SelfImplementStage =
|
|
|
16
16
|
| 'worktree-completed'
|
|
17
17
|
| 'gate-failed'
|
|
18
18
|
| 'review-blocked'
|
|
19
|
+
| 'no-changes'
|
|
19
20
|
| 'merge-conflict'
|
|
20
21
|
| 'aborted'
|
|
21
22
|
| 'timed-out'
|
|
@@ -30,6 +31,7 @@ const STAGE_OUTCOMES = {
|
|
|
30
31
|
'worktree-completed': { runStatus: 'completed' },
|
|
31
32
|
'gate-failed': { runStatus: 'failed', failureKind: 'gate' },
|
|
32
33
|
'review-blocked': { runStatus: 'failed', failureKind: 'review' },
|
|
34
|
+
'no-changes': { runStatus: 'failed', failureKind: 'review' },
|
|
33
35
|
'merge-conflict': { runStatus: 'failed', failureKind: 'merge-conflict' },
|
|
34
36
|
aborted: { runStatus: 'failed', failureKind: 'aborted' },
|
|
35
37
|
'timed-out': { runStatus: 'failed', failureKind: 'timed-out' },
|
|
@@ -2513,7 +2513,7 @@ export function defaultSeams(o: DefaultSeamsOptions = {}): SelfImplementSeams {
|
|
|
2513
2513
|
verdict: rr.verdict,
|
|
2514
2514
|
mustFix: rr.mustFix,
|
|
2515
2515
|
shouldFix: rr.shouldFix,
|
|
2516
|
-
summary: renderReview(rr).slice(0, 2000)
|
|
2516
|
+
summary: rr.reviewed ? renderReview(rr).slice(0, 2000) : `리뷰 못 함(사유: ${rr.failureReason ?? '사유 미상'})`,
|
|
2517
2517
|
reviewed: rr.reviewed ?? false,
|
|
2518
2518
|
// ⛔⭐ 이 한 줄이 `orchestrator.ts` 의 「찍는 줄」에 값을 «흘린다». 빼면 그 줄은 살아 있는데 값이 안 온다.
|
|
2519
2519
|
...(rr.failureReason !== undefined ? { failureReason: rr.failureReason } : {}),
|
|
@@ -108,6 +108,7 @@ export function singleRunAsJobResult(feature: string, r: SelfImplementResultWith
|
|
|
108
108
|
stage: r.stage,
|
|
109
109
|
...(!r.ok && r.detail ? { error: { message: r.detail } } : {}),
|
|
110
110
|
...(r.branch ? { branch: r.branch } : {}),
|
|
111
|
+
...(r.harvestable === true && r.branch ? { harvestable: true } : {}),
|
|
111
112
|
...(r.worktreePath ? { worktreePath: r.worktreePath } : {}),
|
|
112
113
|
...(r.prUrl ? { prUrl: r.prUrl } : {}),
|
|
113
114
|
...(r.merged !== undefined ? { merged: r.merged } : {}),
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { prodInstanceRoot } from '../instance/resolve.js';
|
|
6
|
+
import { seatOfTree } from '../loops/orchestrator/traffic.js';
|
|
7
|
+
import { getUserConfig, ORCHESTRATOR_DEFAULTS } from '../user-config.js';
|
|
8
|
+
import { isRunStatus } from './run-status-mapping.js';
|
|
9
|
+
import { loadRunLedger, runLedgerDir, type RunLedgerEntry, type RunLedgerReader } from './run-ledger.js';
|
|
10
|
+
|
|
11
|
+
export type SpeedClaimsOutcome = 'merged' | 'failed' | 'abandoned' | 'completed-without-merge';
|
|
12
|
+
export type SpeedClaimsSeat = 'OP' | 'TC' | 'MK' | 'UX' | 'unknown';
|
|
13
|
+
export interface SpeedClaimsRow {
|
|
14
|
+
seat: SpeedClaimsSeat;
|
|
15
|
+
outcome: SpeedClaimsOutcome;
|
|
16
|
+
count: number;
|
|
17
|
+
underOneMinuteCount: number;
|
|
18
|
+
medianMinutes: number;
|
|
19
|
+
q1Minutes: number;
|
|
20
|
+
q3Minutes: number;
|
|
21
|
+
}
|
|
22
|
+
export interface SpeedClaimsMeasurement {
|
|
23
|
+
scope: 'self-implement-run-ledger';
|
|
24
|
+
ledgerDirectory: string;
|
|
25
|
+
status: 'measured' | 'cannot-measure';
|
|
26
|
+
completedCount: number;
|
|
27
|
+
inProgressCount: number;
|
|
28
|
+
cannotMeasureCount: number;
|
|
29
|
+
unknownSeatCount: number;
|
|
30
|
+
rows: SpeedClaimsRow[];
|
|
31
|
+
note: string;
|
|
32
|
+
parent: ParentSpeedClaimsMeasurement;
|
|
33
|
+
}
|
|
34
|
+
export interface ParentSpeedClaimsEvent {
|
|
35
|
+
ts: string;
|
|
36
|
+
category: string;
|
|
37
|
+
event: string;
|
|
38
|
+
data: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ParentSpeedClaimsRow {
|
|
42
|
+
seat: 'OP' | 'TC' | 'MK' | 'UX' | 'direct' | 'unknown';
|
|
43
|
+
stage: string;
|
|
44
|
+
merged: boolean;
|
|
45
|
+
count: number;
|
|
46
|
+
medianMinutes: number;
|
|
47
|
+
q1Minutes: number;
|
|
48
|
+
q3Minutes: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ParentSpeedClaimsMeasurement {
|
|
52
|
+
scope: 'self-dev.orchestrate-parent-logs';
|
|
53
|
+
status: 'measured' | 'cannot-measure';
|
|
54
|
+
completedCount: number;
|
|
55
|
+
inProgressCount: number;
|
|
56
|
+
cannotMeasureCount: number;
|
|
57
|
+
rows: ParentSpeedClaimsRow[];
|
|
58
|
+
note: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SpeedClaimsOptions {
|
|
62
|
+
dir?: string;
|
|
63
|
+
list?: (path: string) => string[];
|
|
64
|
+
read?: RunLedgerReader;
|
|
65
|
+
readSeat?: (runId: string, ledgerDirectory: string, entries: readonly RunLedgerEntry[]) => SpeedClaimsSeat;
|
|
66
|
+
readParentEvents?: () => readonly ParentSpeedClaimsEvent[];
|
|
67
|
+
readParentQueue?: () => unknown;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const NOTE = 'Child-ledger elapsed minutes from the first child event (or checkpoint creation when unavailable) to the recorded child end, including retries and resumes; this is not the parent launch-to-done interval. Only finished runs enter quartiles (linear interpolation at ranks (n-1)*p). In-progress runs (including parked) and missing, unreadable or invalid timestamps are excluded, not assigned zero. Seats come from the same state root checkpoint, launch queue or recorded launch tree; unresolved seats are counted separately. A completed run without a recorded merge is not a merge.';
|
|
71
|
+
const CANONICAL_RUN_ID = /^run-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
72
|
+
const SEATS: readonly SpeedClaimsSeat[] = ['OP', 'TC', 'MK', 'UX', 'unknown'];
|
|
73
|
+
|
|
74
|
+
function checkpointFor(runId: string, ledgerDirectory: string): Record<string, unknown> | null {
|
|
75
|
+
try {
|
|
76
|
+
const value: unknown = JSON.parse(readFileSync(join(dirname(ledgerDirectory), 'self-dev-runs', `${runId}.json`), 'utf8'));
|
|
77
|
+
return value && typeof value === 'object' && !Array.isArray(value) && 'runId' in value && value.runId === runId
|
|
78
|
+
? value as Record<string, unknown> : null;
|
|
79
|
+
} catch { return null; }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function seatFromLaunch(runId: string, ledgerDirectory: string, entries: readonly RunLedgerEntry[]): SpeedClaimsSeat {
|
|
83
|
+
const checkpoint = checkpointFor(runId, ledgerDirectory);
|
|
84
|
+
if (SEATS.includes(checkpoint?.seat as SpeedClaimsSeat) && checkpoint?.seat !== 'unknown') return checkpoint!.seat as SpeedClaimsSeat;
|
|
85
|
+
// Queue ids and launch ids are not run ids. Only an exact, unique ask-file match can attribute a queued launch.
|
|
86
|
+
const goalFiles = new Set(entries.filter((entry) => entry.event === 'start')
|
|
87
|
+
.map((entry) => entry.data.goalFile).filter((file): file is string => typeof file === 'string' && isAbsolute(file)));
|
|
88
|
+
if (goalFiles.size === 1) {
|
|
89
|
+
try {
|
|
90
|
+
const queue: unknown = JSON.parse(readFileSync(join(dirname(ledgerDirectory), 'harness', 'queue.json'), 'utf8'));
|
|
91
|
+
if (Array.isArray(queue)) {
|
|
92
|
+
const matched = queue.filter((row) => row && typeof row === 'object' && row.kind === 'ask'
|
|
93
|
+
&& (row.status === 'launching' || row.status === 'launched' || row.status === 'finished')
|
|
94
|
+
&& row.input === [...goalFiles][0] && SEATS.includes(row.seat));
|
|
95
|
+
if (matched.length === 1 && matched[0]!.seat !== 'unknown') return matched[0]!.seat as SpeedClaimsSeat;
|
|
96
|
+
}
|
|
97
|
+
} catch { /* An absent or unreadable queue cannot establish attribution. */ }
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const cfg = getUserConfig().loops?.orchestrator ?? ORCHESTRATOR_DEFAULTS;
|
|
101
|
+
const paths = entries.flatMap((entry) => entry.event === 'start' ? [entry.data.targetRoot] : entry.event === 'worktree' ? [entry.data.path] : []);
|
|
102
|
+
const seats = new Set<SpeedClaimsSeat>();
|
|
103
|
+
for (const path of paths) {
|
|
104
|
+
if (typeof path !== 'string' || !isAbsolute(path)) continue;
|
|
105
|
+
try {
|
|
106
|
+
const seat = seatOfTree(path, cfg);
|
|
107
|
+
if (seat) seats.add(seat);
|
|
108
|
+
} catch { /* A removed or unreadable launch tree does not prove a seat. */ }
|
|
109
|
+
}
|
|
110
|
+
if (seats.size === 1) return [...seats][0]!;
|
|
111
|
+
} catch { /* Unavailable seat configuration does not erase a readable run. */ }
|
|
112
|
+
return 'unknown';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseTime(timestamp: unknown): number | null {
|
|
116
|
+
if (typeof timestamp !== 'string') return null;
|
|
117
|
+
const match = /^(\d{4})-(\d\d)-(\d\d)T\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.exec(timestamp);
|
|
118
|
+
if (!match) return null;
|
|
119
|
+
const year = Number(match[1]);
|
|
120
|
+
const month = Number(match[2]);
|
|
121
|
+
const day = Number(match[3]);
|
|
122
|
+
const days = [31, year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
123
|
+
if (month < 1 || month > 12 || day < 1 || day > days[month - 1]!) return null;
|
|
124
|
+
const parsed = Date.parse(timestamp);
|
|
125
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function time(entry: RunLedgerEntry | undefined): number | null {
|
|
129
|
+
return parseTime(entry?.timestamp);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function percentile(sorted: readonly number[], p: number): number {
|
|
133
|
+
const rank = (sorted.length - 1) * p;
|
|
134
|
+
const low = Math.floor(rank);
|
|
135
|
+
return sorted[low]! + (sorted[Math.ceil(rank)]! - sorted[low]!) * (rank - low);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function endOfRun(entries: readonly RunLedgerEntry[]): { entry: RunLedgerEntry; outcome: SpeedClaimsOutcome } | null {
|
|
139
|
+
let end: { entry: RunLedgerEntry; outcome: SpeedClaimsOutcome } | null = null;
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (entry.event === 'start') { end = null; continue; }
|
|
142
|
+
if (entry.event === 'merged' && entry.data.merged === true) end = { entry, outcome: 'merged' };
|
|
143
|
+
if (end?.outcome === 'merged') continue;
|
|
144
|
+
if (entry.event === 'human-stop') end = { entry, outcome: 'abandoned' };
|
|
145
|
+
if (entry.event === 'run-status' && isRunStatus(entry.data.runStatus)) {
|
|
146
|
+
const status = entry.data.runStatus;
|
|
147
|
+
if (status === 'completed') end = { entry, outcome: 'completed-without-merge' };
|
|
148
|
+
else if (status === 'failed') end = { entry, outcome: entry.data.stage === 'aborted' ? 'abandoned' : 'failed' };
|
|
149
|
+
else if (status === 'cancelled') end = { entry, outcome: 'abandoned' };
|
|
150
|
+
else end = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return end;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const PARENT_NOTE = 'Elapsed minutes from the earliest valid parent self-dev.orchestrate start log to the latest valid done log, grouped by that done.data.outcomes[].stage and merged flag. Only runs with both events and valid timestamps enter quartiles. A start without done is in progress. Seats use the same-runId queue seat when available, or direct when a readable queue has no matching entry; an unreadable queue remains unknown. Parent log queries are read-only and independent of child run ledgers.';
|
|
157
|
+
type ParentSeat = ParentSpeedClaimsRow['seat'];
|
|
158
|
+
const PARENT_SEATS: readonly ParentSeat[] = ['OP', 'TC', 'MK', 'UX', 'direct', 'unknown'];
|
|
159
|
+
|
|
160
|
+
function object(value: unknown): Record<string, unknown> | null {
|
|
161
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Read the first-class logs CLI rather than opening logs.db in the measurement process. */
|
|
165
|
+
function readParentEventsFromLogs(): ParentSpeedClaimsEvent[] {
|
|
166
|
+
const command = fileURLToPath(new URL('../../bin/elanous.mjs', import.meta.url));
|
|
167
|
+
const events: ParentSpeedClaimsEvent[] = [];
|
|
168
|
+
let before: string | undefined;
|
|
169
|
+
const seenCursors = new Set<string>();
|
|
170
|
+
for (;;) {
|
|
171
|
+
const result = spawnSync(process.execPath, [command, `--test=${fileURLToPath(new URL('../../', import.meta.url))}`, 'logs', '--instance', 'prod', '--category', 'self-dev.orchestrate', '--json', '--json-data', '--limit', '5000', ...(before ? ['--before', before] : [])], {
|
|
172
|
+
encoding: 'utf8', maxBuffer: 64 * 1024 * 1024,
|
|
173
|
+
});
|
|
174
|
+
if (result.error || result.status !== 0) throw new Error(result.stderr || result.error?.message || 'parent logs query failed');
|
|
175
|
+
const lines = result.stdout.trim().split('\n').filter(Boolean).map((line) => JSON.parse(line) as Record<string, unknown>);
|
|
176
|
+
if (lines.some((line) => object(line._meta)?.type === 'log-query-unreadable-instances')) throw new Error('parent logs query incomplete');
|
|
177
|
+
events.push(...lines.filter((line): line is Record<string, unknown> & ParentSpeedClaimsEvent =>
|
|
178
|
+
line.store === 'prod' && line.category === 'self-dev.orchestrate' && typeof line.ts === 'string' && typeof line.event === 'string'));
|
|
179
|
+
const limit = lines.find((line) => object(line._meta)?.type === 'log-query-limit');
|
|
180
|
+
if (!limit) return events;
|
|
181
|
+
const meta = object(limit._meta);
|
|
182
|
+
const cursor = meta?.nextCursors ?? meta?.nextCursor;
|
|
183
|
+
if (typeof cursor !== 'number' && !object(cursor)) throw new Error('parent logs pagination incomplete');
|
|
184
|
+
const next = typeof cursor === 'number' ? String(cursor) : JSON.stringify(cursor);
|
|
185
|
+
if (seenCursors.has(next)) throw new Error('parent logs pagination repeated a cursor');
|
|
186
|
+
seenCursors.add(next);
|
|
187
|
+
before = next;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readParentQueueFromState(): unknown {
|
|
192
|
+
return JSON.parse(readFileSync(join(prodInstanceRoot(), 'harness', 'queue.json'), 'utf8')) as unknown;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Parent launch-to-done census; no child ledger id is used as a join key. */
|
|
196
|
+
export function measureParentSpeedClaims(
|
|
197
|
+
events: readonly ParentSpeedClaimsEvent[], queue?: unknown,
|
|
198
|
+
): ParentSpeedClaimsMeasurement {
|
|
199
|
+
const result: ParentSpeedClaimsMeasurement = {
|
|
200
|
+
scope: 'self-dev.orchestrate-parent-logs', status: 'measured', completedCount: 0,
|
|
201
|
+
inProgressCount: 0, cannotMeasureCount: 0, rows: [], note: PARENT_NOTE,
|
|
202
|
+
};
|
|
203
|
+
const queueRows = Array.isArray(queue) ? queue : object(queue)?.items;
|
|
204
|
+
const queueByRun = new Map<string, ParentSeat | null>();
|
|
205
|
+
if (Array.isArray(queueRows)) for (const raw of queueRows) {
|
|
206
|
+
const entry = object(raw);
|
|
207
|
+
if (entry && typeof entry.runId === 'string' && PARENT_SEATS.includes(entry.seat as ParentSeat) && entry.seat !== 'direct') {
|
|
208
|
+
const seat = entry.seat as ParentSeat;
|
|
209
|
+
queueByRun.set(entry.runId, queueByRun.has(entry.runId) && queueByRun.get(entry.runId) !== seat ? null : seat);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const runs = new Map<string, { start?: number; end?: number; outcomes?: unknown }>();
|
|
213
|
+
for (const event of events) {
|
|
214
|
+
if (event.category !== 'self-dev.orchestrate' || (event.event !== 'start' && event.event !== 'done')) continue;
|
|
215
|
+
const data = object(event.data);
|
|
216
|
+
const runId = data?.runId;
|
|
217
|
+
if (typeof runId !== 'string' || !CANONICAL_RUN_ID.test(runId)) { result.cannotMeasureCount++; continue; }
|
|
218
|
+
const run = runs.get(runId) ?? {};
|
|
219
|
+
const at = parseTime(event.ts);
|
|
220
|
+
if (event.event === 'start') {
|
|
221
|
+
if (at !== null) run.start = run.start === undefined || !Number.isFinite(run.start) ? at : Math.min(run.start, at);
|
|
222
|
+
else if (run.start === undefined) run.start = NaN;
|
|
223
|
+
} else if (at !== null && (run.end === undefined || !Number.isFinite(run.end) || at > run.end)) {
|
|
224
|
+
run.end = at;
|
|
225
|
+
run.outcomes = data?.outcomes;
|
|
226
|
+
} else if (at === null && run.end === undefined) {
|
|
227
|
+
run.end = NaN;
|
|
228
|
+
}
|
|
229
|
+
runs.set(runId, run);
|
|
230
|
+
}
|
|
231
|
+
const groups = new Map<string, { seat: ParentSeat; stage: string; merged: boolean; values: number[] }>();
|
|
232
|
+
for (const [runId, run] of runs) {
|
|
233
|
+
if (run.start === undefined && run.end !== undefined) { result.cannotMeasureCount++; continue; }
|
|
234
|
+
if (run.end === undefined) { result.inProgressCount++; continue; }
|
|
235
|
+
if (run.start === undefined || !Number.isFinite(run.start) || !Number.isFinite(run.end) || run.end < run.start || !Array.isArray(run.outcomes) || run.outcomes.length === 0) {
|
|
236
|
+
result.cannotMeasureCount++;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const seat = queueByRun.has(runId) ? (queueByRun.get(runId) ?? 'unknown') : Array.isArray(queueRows) ? 'direct' : 'unknown';
|
|
240
|
+
const outcomes = run.outcomes.map(object);
|
|
241
|
+
if (outcomes.some((outcome) => !outcome)) {
|
|
242
|
+
result.cannotMeasureCount++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
for (const outcome of outcomes) {
|
|
246
|
+
const stage = typeof outcome!.stage === 'string' ? outcome!.stage : 'unknown';
|
|
247
|
+
const merged = outcome!.merged === true;
|
|
248
|
+
const key = JSON.stringify([seat, stage, merged]);
|
|
249
|
+
const group = groups.get(key) ?? { seat, stage, merged, values: [] };
|
|
250
|
+
group.values.push((run.end - run.start!) / 60_000);
|
|
251
|
+
groups.set(key, group);
|
|
252
|
+
}
|
|
253
|
+
result.completedCount++;
|
|
254
|
+
}
|
|
255
|
+
result.rows = [...groups.values()].map(({ seat, stage, merged, values }) => {
|
|
256
|
+
values.sort((a, b) => a - b);
|
|
257
|
+
return { seat, stage, merged, count: values.length, medianMinutes: percentile(values, 0.5), q1Minutes: percentile(values, 0.25), q3Minutes: percentile(values, 0.75) };
|
|
258
|
+
}).sort((a, b) => PARENT_SEATS.indexOf(a.seat) - PARENT_SEATS.indexOf(b.seat) || a.stage.localeCompare(b.stage));
|
|
259
|
+
if (result.cannotMeasureCount > 0) result.status = 'cannot-measure';
|
|
260
|
+
return result;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Read-only elapsed-time census of canonical local run ledgers, including unsuccessful runs. */
|
|
264
|
+
export function measureSpeedClaims(options: SpeedClaimsOptions = {}): SpeedClaimsMeasurement {
|
|
265
|
+
const ledgerDirectory = resolve(options.dir ?? runLedgerDir());
|
|
266
|
+
let parent: ParentSpeedClaimsMeasurement;
|
|
267
|
+
try {
|
|
268
|
+
const events = (options.readParentEvents ?? readParentEventsFromLogs)();
|
|
269
|
+
let queue: unknown;
|
|
270
|
+
try { queue = (options.readParentQueue ?? readParentQueueFromState)(); } catch { /* Queue absence does not erase parent observations or establish a direct launch. */ }
|
|
271
|
+
parent = measureParentSpeedClaims(events, queue);
|
|
272
|
+
} catch {
|
|
273
|
+
parent = { ...measureParentSpeedClaims([]), status: 'cannot-measure' };
|
|
274
|
+
}
|
|
275
|
+
const result: SpeedClaimsMeasurement = {
|
|
276
|
+
scope: 'self-implement-run-ledger', ledgerDirectory, status: 'measured',
|
|
277
|
+
completedCount: 0, inProgressCount: 0, cannotMeasureCount: 0, unknownSeatCount: 0, rows: [], note: NOTE, parent,
|
|
278
|
+
};
|
|
279
|
+
let files: string[];
|
|
280
|
+
try { files = (options.list ?? readdirSync)(ledgerDirectory); }
|
|
281
|
+
catch { return { ...result, status: 'cannot-measure' }; }
|
|
282
|
+
|
|
283
|
+
const groups = new Map<string, { seat: SpeedClaimsSeat; outcome: SpeedClaimsOutcome; values: number[] }>();
|
|
284
|
+
for (const file of files.sort()) {
|
|
285
|
+
const runId = file.endsWith('.jsonl') ? file.slice(0, -'.jsonl'.length) : '';
|
|
286
|
+
if (!CANONICAL_RUN_ID.test(runId)) continue;
|
|
287
|
+
try {
|
|
288
|
+
const entries = loadRunLedger(runId, ledgerDirectory, options.read);
|
|
289
|
+
if (!entries || entries.length === 0) { result.cannotMeasureCount++; continue; }
|
|
290
|
+
const end = endOfRun(entries);
|
|
291
|
+
const first = time(entries[0]);
|
|
292
|
+
const createdAt = checkpointFor(runId, ledgerDirectory)?.createdAt;
|
|
293
|
+
const creation = typeof createdAt === 'number' && Number.isFinite(createdAt) && createdAt >= 0 ? createdAt : null;
|
|
294
|
+
const start = first === null ? creation : first;
|
|
295
|
+
const endTime = end ? time(end.entry) : null;
|
|
296
|
+
if (start === null || (end && (endTime === null || endTime < start))) {
|
|
297
|
+
result.cannotMeasureCount++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (!end) { result.inProgressCount++; continue; }
|
|
301
|
+
const seat = (options.readSeat ?? seatFromLaunch)(runId, ledgerDirectory, entries);
|
|
302
|
+
const safeSeat = SEATS.includes(seat) ? seat : 'unknown';
|
|
303
|
+
if (safeSeat === 'unknown') result.unknownSeatCount++;
|
|
304
|
+
const key = `${safeSeat}:${end.outcome}`;
|
|
305
|
+
const group = groups.get(key) ?? { seat: safeSeat, outcome: end.outcome, values: [] };
|
|
306
|
+
group.values.push((endTime! - start) / 60_000);
|
|
307
|
+
groups.set(key, group);
|
|
308
|
+
result.completedCount++;
|
|
309
|
+
} catch { result.cannotMeasureCount++; }
|
|
310
|
+
}
|
|
311
|
+
result.rows = [...groups.values()].map(({ seat, outcome, values }) => {
|
|
312
|
+
values.sort((a, b) => a - b);
|
|
313
|
+
return { seat, outcome, count: values.length, underOneMinuteCount: values.filter((minutes) => minutes < 1).length, medianMinutes: percentile(values, 0.5), q1Minutes: percentile(values, 0.25), q3Minutes: percentile(values, 0.75) };
|
|
314
|
+
}).sort((a, b) => SEATS.indexOf(a.seat) - SEATS.indexOf(b.seat) || a.outcome.localeCompare(b.outcome));
|
|
315
|
+
if (result.cannotMeasureCount > 0) result.status = 'cannot-measure';
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function renderSpeedClaims(result: SpeedClaimsMeasurement): string {
|
|
320
|
+
return [
|
|
321
|
+
'child run-ledger (child start → child end):',
|
|
322
|
+
`ledger directory: ${result.ledgerDirectory}`,
|
|
323
|
+
`status: ${result.status}`,
|
|
324
|
+
`finished (quartile denominator): ${result.completedCount}`,
|
|
325
|
+
`in progress (excluded): ${result.inProgressCount}`,
|
|
326
|
+
`cannot measure (excluded): ${result.cannotMeasureCount}`,
|
|
327
|
+
`unknown seat (finished): ${result.unknownSeatCount}`,
|
|
328
|
+
...result.rows.map((row) => `seat=${row.seat} outcome=${row.outcome} count=${row.count} underOneMinute=${row.underOneMinuteCount} median=${row.medianMinutes}m q1=${row.q1Minutes}m q3=${row.q3Minutes}m`),
|
|
329
|
+
`note: ${result.note}`,
|
|
330
|
+
'parent launch → done (self-dev.orchestrate):',
|
|
331
|
+
`parent status: ${result.parent.status}`,
|
|
332
|
+
`parent finished (quartile denominator): ${result.parent.completedCount}`,
|
|
333
|
+
`parent in progress (excluded): ${result.parent.inProgressCount}`,
|
|
334
|
+
`parent cannot measure (excluded): ${result.parent.cannotMeasureCount}`,
|
|
335
|
+
...result.parent.rows.map((row) => `seat=${row.seat} stage=${row.stage} merged=${row.merged} count=${row.count} median=${row.medianMinutes}m q1=${row.q1Minutes}m q3=${row.q3Minutes}m`),
|
|
336
|
+
`parent note: ${result.parent.note}`,
|
|
337
|
+
].join('\n');
|
|
338
|
+
}
|
package/src/steward/triage.ts
CHANGED
|
@@ -260,9 +260,56 @@ function skipOverlappingRun(root: string, open: FailureStreak['open'] & { pid?:
|
|
|
260
260
|
}
|
|
261
261
|
debug.log('steward.stage', 'skipped-overlap', { openStage: open.stage, pid: open.pid });
|
|
262
262
|
writeFileSync(skippedPath(root), JSON.stringify({ openStage: open.stage, pid: open.pid }));
|
|
263
|
+
logTickDecision(root, 'skipped', `overlapping ${open.stage} (pid ${open.pid ?? 'unknown'})`);
|
|
263
264
|
}
|
|
264
265
|
|
|
265
266
|
function reportPath(root: string): string { return join(root, 'steward', 'observe.json'); }
|
|
267
|
+
|
|
268
|
+
type TickDecision = { verdict: 'launched' | 'failed'; reason: string; goalId?: string };
|
|
269
|
+
|
|
270
|
+
function logTickDecision(root: string | undefined, verdict: 'launched' | 'skipped' | 'reported' | 'failed', reason: string, goalId?: string): void {
|
|
271
|
+
const runId = graphRunId();
|
|
272
|
+
const marker = root && runId && existsSync(join(root, 'steward')) ? join(root, 'steward', `decision-logged-${runId}.json`) : undefined;
|
|
273
|
+
const decision = { verdict, reason: redactSecretText(reason), ...(goalId ? { goalId } : {}), ...(runId ? { runId } : {}) };
|
|
274
|
+
if (marker) try {
|
|
275
|
+
const previous = JSON.parse(readFileSync(marker, 'utf8')) as { verdict?: string; reason?: string };
|
|
276
|
+
// A replay keeps the first success; a failure replaces a success; a success after a failure replaces the failure.
|
|
277
|
+
if (previous.verdict === decision.verdict && previous.reason === decision.reason) return;
|
|
278
|
+
if (previous.verdict !== 'failed' && verdict !== 'failed') return;
|
|
279
|
+
} catch { /* Missing or stale marker: retry the observation. */ }
|
|
280
|
+
try {
|
|
281
|
+
debug.log('loop.steward', 'decision', decision);
|
|
282
|
+
if (marker) writeFileSync(marker, JSON.stringify(decision));
|
|
283
|
+
} catch { /* Observation must not change the tick outcome. */ }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function tickDecisionPath(root: string, runId: string): string {
|
|
287
|
+
return join(root, 'steward', `decision-${runId}.json`);
|
|
288
|
+
}
|
|
289
|
+
function rememberLaunchDecision(root: string, entry: TickDecision): void {
|
|
290
|
+
const runId = graphRunId();
|
|
291
|
+
if (!runId) return;
|
|
292
|
+
const path = tickDecisionPath(root, runId);
|
|
293
|
+
try {
|
|
294
|
+
let current: TickDecision | undefined;
|
|
295
|
+
try { current = JSON.parse(readFileSync(path, 'utf8')) as TickDecision; }
|
|
296
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') return; }
|
|
297
|
+
// Keep the first failure (and its goal/cause), even if a retried schedule later succeeds.
|
|
298
|
+
if (current?.verdict === 'failed' || current?.verdict === entry.verdict) return;
|
|
299
|
+
writeFileSync(path, JSON.stringify(entry));
|
|
300
|
+
} catch { /* An unavailable observation store must not block execution. */ }
|
|
301
|
+
}
|
|
302
|
+
function readLaunchDecision(root: string): TickDecision | undefined {
|
|
303
|
+
const runId = graphRunId();
|
|
304
|
+
if (!runId) return undefined;
|
|
305
|
+
try { return JSON.parse(readFileSync(tickDecisionPath(root, runId), 'utf8')) as TickDecision; }
|
|
306
|
+
catch { return undefined; }
|
|
307
|
+
}
|
|
308
|
+
function logStageFailure(root: string, stage: StewardStage, reason: string): void {
|
|
309
|
+
const runId = graphRunId();
|
|
310
|
+
if (runId) try { rmSync(tickDecisionPath(root, runId), { force: true }); } catch { /* Observation must not change the failure. */ }
|
|
311
|
+
logTickDecision(root, 'failed', `${stage}: ${reason}`);
|
|
312
|
+
}
|
|
266
313
|
function readState(path: string): { issues: Record<string, string>; digestDay?: string } {
|
|
267
314
|
try { return JSON.parse(readFileSync(path, 'utf8')) as ReturnType<typeof readState>; }
|
|
268
315
|
catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { issues: {} }; throw error; }
|
|
@@ -396,10 +443,18 @@ export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'r
|
|
|
396
443
|
const entry = raiseHitl(issue, row, launchDeps);
|
|
397
444
|
recordHitlOnCard(issue, entry, cardDeps);
|
|
398
445
|
}
|
|
446
|
+
let launchDecision: TickDecision | undefined;
|
|
399
447
|
for (const item of plan.launches) {
|
|
400
448
|
const entry = launch(item, launchDeps);
|
|
449
|
+
const decision: TickDecision | undefined = entry.status === 'launched'
|
|
450
|
+
? { verdict: 'launched', reason: 'harness run spawned', goalId: `linear:${item.issue.identifier}` }
|
|
451
|
+
: entry.status === 'failed'
|
|
452
|
+
? { verdict: 'failed', reason: entry.reason ?? 'launch failed', goalId: `linear:${item.issue.identifier}` }
|
|
453
|
+
: undefined;
|
|
454
|
+
if (decision && (!launchDecision || (decision.verdict === 'failed' && launchDecision.verdict !== 'failed'))) launchDecision = decision;
|
|
401
455
|
recordLaunchOnCard(item.issue, entry, cardDeps);
|
|
402
456
|
}
|
|
457
|
+
if (launchDecision) rememberLaunchDecision(root, launchDecision);
|
|
403
458
|
if (settings.mode === 'shadow') {
|
|
404
459
|
const { recordShadowTick } = await import('./readiness.js');
|
|
405
460
|
const tick = recordShadowTick(root, rows, plan, (deps.now ?? (() => new Date()))());
|
|
@@ -459,12 +514,18 @@ export async function runStewardStage(stage: 'sync' | 'triage' | 'schedule' | 'r
|
|
|
459
514
|
}
|
|
460
515
|
}
|
|
461
516
|
const day = at.slice(0, 10);
|
|
517
|
+
let digested = false;
|
|
462
518
|
if (state.digestDay !== day) {
|
|
463
519
|
const send = deps.sendDigest ?? sendStewardDigest;
|
|
464
520
|
await send(`스튜어드 ${day}: ${rows.map(row => `${row.issue} ${row.disposition}`).join(' · ')}`);
|
|
465
521
|
state.digestDay = day;
|
|
466
522
|
writeFileSync(path, JSON.stringify(state));
|
|
523
|
+
digested = true;
|
|
467
524
|
}
|
|
525
|
+
const launched = readLaunchDecision(root);
|
|
526
|
+
if (launched) logTickDecision(root, launched.verdict, launched.reason, launched.goalId);
|
|
527
|
+
else if (outcomes.length || digested) logTickDecision(root, 'reported', outcomes.length ? `${outcomes.length} outcome(s) reported` : 'daily digest sent');
|
|
528
|
+
else logTickDecision(root, 'skipped', 'no new outcome or digest');
|
|
468
529
|
}
|
|
469
530
|
outcome = 'ok';
|
|
470
531
|
} finally {
|
|
@@ -513,10 +574,14 @@ async function withStageSlot<T>(root: string, run: () => Promise<T>, onBusy?: ()
|
|
|
513
574
|
|
|
514
575
|
/** The command-node boundary records both normal exits and an unclosed stage from a killed process. */
|
|
515
576
|
export async function runStewardStageCommand(stage: StewardStage, deps: StewardCommandDeps = {}): Promise<0 | 1> {
|
|
516
|
-
if (resolvedStewardSettings(deps.launchSettings).mode === 'off')
|
|
577
|
+
if (resolvedStewardSettings(deps.launchSettings).mode === 'off') {
|
|
578
|
+
if (stage === 'sync') logTickDecision(deps.root ?? effectiveInstanceRoot(), 'skipped', 'steward mode off');
|
|
579
|
+
return 0;
|
|
580
|
+
}
|
|
517
581
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
518
582
|
if (stage !== 'sync' && existsSync(skippedPath(root))) return 0;
|
|
519
|
-
|
|
583
|
+
try {
|
|
584
|
+
return await withStageSlot(root, async () => {
|
|
520
585
|
const now = deps.now ?? (() => new Date());
|
|
521
586
|
const threshold = stewardSettings().alertAfterFailures ?? 3;
|
|
522
587
|
const alertIfNeeded = async (state: FailureStreak): Promise<void> => {
|
|
@@ -564,10 +629,16 @@ export async function runStewardStageCommand(stage: StewardStage, deps: StewardC
|
|
|
564
629
|
if (!ok) {
|
|
565
630
|
console.error(`steward ${stage}: stage failed — ${reason}`);
|
|
566
631
|
debug.log('steward.stage', 'failed', { stage, reason });
|
|
632
|
+
if (stage === 'sync' || !existsSync(skippedPath(root))) logStageFailure(root, stage, reason);
|
|
567
633
|
return 1;
|
|
568
634
|
}
|
|
569
635
|
return 0;
|
|
570
|
-
|
|
636
|
+
}, stage === 'sync' ? () => { skipOverlappingRun(root, currentOpen(root, stage)); return 0 as const; } : undefined);
|
|
637
|
+
} catch (error) {
|
|
638
|
+
try { if (stage === 'sync' || !existsSync(skippedPath(root))) logStageFailure(root, stage, error instanceof Error ? error.message : String(error)); }
|
|
639
|
+
catch { /* Observation must not change the stage error. */ }
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
571
642
|
}
|
|
572
643
|
|
|
573
644
|
export async function runStewardNodeCommand(
|
|
@@ -53,7 +53,10 @@ export async function recordWorkingBackwardsOnCards(
|
|
|
53
53
|
}
|
|
54
54
|
const card = store.createCard({ goalId: `linear:${issue.identifier}`, title: redactSecrets(issue.title) });
|
|
55
55
|
if (card.sections.some(section => section.key.startsWith('prfaq:')) && card.sections.some(section => section.key.startsWith('manual:'))) continue;
|
|
56
|
-
if (card.sections.some(section => section.key.startsWith('launch:')))
|
|
56
|
+
if (card.sections.some(section => section.key.startsWith('launch:'))) {
|
|
57
|
+
debug.log('steward.working-backwards', 'skipped-launched-before-draft', { issueId: issue.identifier });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
57
60
|
const draft = await draftWorkingBackwards(issue, ask);
|
|
58
61
|
for (const [section, content] of [['prfaq', draft.prfaq], ['manual', draft.manual]] as const) {
|
|
59
62
|
const key = `${section}:${createHash('sha256').update(content).digest('hex')}`;
|