elanous 0.2.27 → 0.2.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1454-a9c338f5bb556f42.js → 1454-29d1d773d5996a49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-07110da72c45e68f.js → page-24453f6a302816b4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d2d76d4a7eae91bd.js → page-6c131d30bb1cf871.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-fa46d4588e8442c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-e5a9302c105725a3.js → page-dc0772b11861af51.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-6a16a48d1aa6e91f.js → page-256ffd365d32e100.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e0de78ae7b99f01c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-39f82acf619cca23.js → page-4c741eb760f2f77c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-7c2c8f81b185f9c0.js → page-e9ba98f5f1440a21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-ae7aff07e32676bb.js → page-dafc901bb1b72b5f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-482c60c1bc540d0a.js → page-2f6f303fa114ec24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-7cb996c1aa08b6f5.js → layout-1634081c5272006c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/{page-623e08fe4477d606.js → page-ced0bcfc6542b3fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-db460cd8aecb8490.js → page-bb1f05cd51c38944.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-ff27902d0b11ef4f.js → page-646b50f63b6fe732.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-1917ea87ea71ab6b.js → page-f736324afa2117c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-25a302273b054fd7.js → page-6f131b51193a36f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-670510eda8b9443f.js → not-found-435f5e2faf02bc01.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-497e0eedf20e7890.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a8d28fdf995e16ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4bb7556e2e97a750.js → page-8966452228753c5e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-4eb96fc7241c2456.js → page-66a67eb19ebd976b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-0d64ff212dc55d03.js → page-c71267e7681da982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-7f096248993fc2d4.js → page-a21d1e1797172692.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-e47fc72049149a38.js → page-cb249e81ed339318.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-f7d808e31e005895.js → page-781cfc5114e097da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-5c2b24b5496dad73.js → page-478157214db6a023.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-76cdb531668b5191.js → page-86141f12cf090b7f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-7256c73bb1c36751.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-dab163b338e3b966.js → page-979d318a534e92da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-0260ffc7eda50db1.js → page-21050928a697ed7a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-99c4e6c7345a9156.js → page-601fb77340e63c9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-11bf9e5725a72c22.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +13 -13
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/implement-loop.yaml +38 -1
- package/graphs/steward/recipes.yaml +6 -0
- package/graphs/steward/release-day.yaml +23 -0
- package/graphs/steward/steward.yaml +9 -1
- package/graphs/work/code-pr.yaml +23 -0
- package/package.json +1 -1
- package/scripts/browser-observe-cycle.ts +76 -2
- package/scripts/ci-typecheck-changed.ts +50 -17
- package/scripts/detect-test-interference.ts +31 -1
- package/scripts/release-loop/docs-node.ts +51 -22
- package/scripts/release-loop/gate-node.ts +37 -4
- package/scripts/release-loop/node-verdict.ts +3 -1
- package/scripts/release-loop/publish-node.ts +3 -0
- package/scripts/release-loop/tui-node-checks.ts +5 -2
- package/scripts/skills/public-check.ts +5 -2
- package/scripts/test-deterministic.ts +520 -37
- package/scripts/ux-sim.ts +7 -1
- package/scripts/verify-kpack6-live.ts +230 -0
- package/src/acp/codex-plugins.ts +17 -0
- package/src/agent/task-notification.ts +2 -1
- package/src/agent-mission/in-agent-plugin.ts +8 -3
- package/src/agent-substrate/self-review-cli.ts +34 -10
- package/src/briefing/brief-items.ts +151 -35
- package/src/budget/llm-cost.ts +25 -2
- package/src/budget/usage-outcomes.ts +32 -0
- package/src/claims/claims-ledger.ts +23 -0
- package/src/claims/claims-render.ts +22 -2
- package/src/cli/brief-cli.ts +131 -10
- package/src/cli/claims-cli.ts +24 -4
- package/src/cli/msg-cli.ts +165 -9
- package/src/cli/plugin-cli.ts +11 -3
- package/src/cli/pr-cli.ts +80 -18
- package/src/cli/pr-land-refusal.ts +20 -0
- package/src/cli/release-cli.ts +2 -0
- package/src/cli/rfc-cli.ts +24 -0
- package/src/cli/tailscale-serve.ts +5 -8
- package/src/cli/tasks-cli.ts +393 -26
- package/src/cli/usage-cli.ts +9 -4
- package/src/dashboard/first-screen-band.ts +63 -13
- package/src/dashboard/index.ts +8 -3
- package/src/dashboard/skill-detection-runtime.ts +24 -4
- package/src/domains/morning-report.ts +3 -1
- package/src/github/pr-labels.ts +10 -4
- package/src/graph-runner/runner.ts +101 -14
- package/src/harness/harness-clean.ts +21 -3
- package/src/harness/harness-cli-command.ts +22 -6
- package/src/harness/harness-pod-dispatch.ts +11 -4
- package/src/hq/seats.ts +17 -3
- package/src/index.ts +34 -5
- package/src/intelligence-map/model-bench-scores.ts +102 -0
- package/src/loops/orchestrator/inbox.ts +73 -0
- package/src/loops/orchestrator/resync-shadow.ts +100 -12
- package/src/loops/orchestrator/tick.ts +73 -2
- package/src/maturity/feature-maturity.ts +1 -1
- package/src/msg/msg-sse-server.ts +141 -0
- package/src/msg/msg-store.ts +84 -14
- package/src/patents/ledger.ts +128 -0
- package/src/plugins/install/market-fetch.ts +32 -1
- package/src/pod-lease/admission.ts +116 -41
- package/src/release-loop/checklist.ts +11 -0
- package/src/release-loop/merged-pr-checklist.ts +24 -5
- package/src/release-loop/rubric.ts +70 -0
- package/src/rfc-registry/registry.ts +88 -0
- package/src/seat-dispatch/seat-request-ledger.ts +4 -0
- package/src/seat-loop/knowhow-lint.ts +233 -0
- package/src/seat-loop/seat-pack.ts +16 -4
- package/src/seat-loop/traffic-signal.ts +7 -0
- package/src/self-dev/ask-launch-flow.ts +48 -8
- package/src/self-dev/ask-path-candidates.ts +69 -14
- package/src/self-dev/decompose-proposal.ts +43 -1
- package/src/self-dev/graph-journey-nodes.ts +2 -0
- package/src/self-dev/launch-preflight-text.ts +5 -1
- package/src/self-dev/launch-preflight.ts +143 -10
- package/src/self-implement/gate-cli.ts +6 -2
- package/src/self-implement/goal-author.ts +2 -1
- package/src/self-implement/goal-new-paths.ts +4 -5
- package/src/self-implement/graph-authority.ts +17 -3
- package/src/self-implement/graph-yaml.ts +15 -7
- package/src/self-implement/headless-elanous-driver.ts +16 -1
- package/src/self-implement/host-regate.ts +44 -9
- package/src/self-implement/orchestrator.ts +3 -10
- package/src/self-implement/rework-policy.ts +80 -2
- package/src/self-implement/speed-claims.ts +145 -6
- package/src/skills/tools/persistent-grounding.ts +59 -4
- package/src/skills/tools/shell.ts +79 -1
- package/src/steward/rejudge-tick.ts +12 -1
- package/src/steward/release-day.ts +158 -0
- package/src/steward/resync-tick.ts +123 -0
- package/src/task-agent/actions.ts +1 -0
- package/src/task-agent/adopt-pr.ts +140 -0
- package/src/task-agent/board.ts +11 -1
- package/src/task-agent/card-evidence.ts +37 -5
- package/src/task-agent/cover.ts +84 -17
- package/src/task-agent/failure-notify.ts +61 -0
- package/src/task-agent/green-sweep.ts +170 -0
- package/src/task-agent/land-worktree.ts +52 -5
- package/src/task-agent/live-move-history.ts +1 -0
- package/src/task-agent/live-moves.ts +43 -12
- package/src/task-agent/parent-host.ts +1 -1
- package/src/task-agent/postland-card.ts +139 -0
- package/src/task-agent/rejudge-on-head.ts +106 -7
- package/src/task-agent/remote-rejudge-bind.ts +62 -0
- package/src/task-agent/repair-branch-cas.ts +182 -0
- package/src/task-agent/repair-cycle.ts +275 -0
- package/src/task-agent/repair-next-move.ts +31 -0
- package/src/task-agent/repair-review.ts +24 -0
- package/src/task-agent/repair-worker.ts +87 -0
- package/src/task-agent/review-result-capture.ts +2 -1
- package/src/task-agent/shadow.ts +13 -7
- package/src/task-agent/task-hand.ts +115 -6
- package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +70 -12
- package/src/telegram.ts +30 -1
- package/src/user-config.ts +8 -1
- package/src/ux-sim/fault-sqlite-busy.ts +50 -0
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-a1ce306364f08da9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-20662900d6b4b455.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f707a509515ac50c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-8397a5c6ca569bf4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-15c0a4095baddfba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d376e0c83edc041a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-08019efef111ed2e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-980ce6fe36ad95eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-0ab42e0f05823013.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +0 -1
- /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/** Release-day shadow: observe the scheduled phase without performing its action. */
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { Database } from 'bun:sqlite';
|
|
5
|
+
import { debug } from '../debug/log.js';
|
|
6
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
7
|
+
import { listSchedules, type ReleaseSchedule } from '../release-loop/release-schedule.js';
|
|
8
|
+
|
|
9
|
+
export type ReleaseDayPhase = 'gate-mode' | 'carry-p0' | 'cut-run' | 'green-back' | 'none' | 'unmeasured';
|
|
10
|
+
export interface ReleaseDayDecision {
|
|
11
|
+
version: string;
|
|
12
|
+
phase: ReleaseDayPhase;
|
|
13
|
+
dueAt: string | null;
|
|
14
|
+
scheduleUpdatedAt: string;
|
|
15
|
+
reason?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The release record, rather than publishAt (a planned time), opens green-back. */
|
|
19
|
+
export function decideReleaseDayPhase(schedule: ReleaseSchedule, now: Date, publishedAt: string | null): ReleaseDayDecision {
|
|
20
|
+
const cut = Date.parse(schedule.cutAt);
|
|
21
|
+
const land = schedule.landBy ? Date.parse(schedule.landBy) : cut;
|
|
22
|
+
const gate = cut - 30 * 60_000;
|
|
23
|
+
const time = now.getTime();
|
|
24
|
+
let phase: ReleaseDayPhase = 'none';
|
|
25
|
+
let dueAt: string | null = null;
|
|
26
|
+
if (publishedAt && time >= Date.parse(publishedAt)) {
|
|
27
|
+
phase = 'green-back'; dueAt = publishedAt;
|
|
28
|
+
} else if (time >= cut) {
|
|
29
|
+
phase = 'cut-run'; dueAt = schedule.cutAt;
|
|
30
|
+
} else if (time >= land) {
|
|
31
|
+
phase = 'carry-p0'; dueAt = schedule.landBy;
|
|
32
|
+
} else if (time >= gate) {
|
|
33
|
+
phase = 'gate-mode'; dueAt = new Date(gate).toISOString();
|
|
34
|
+
}
|
|
35
|
+
return { version: schedule.version, phase, dueAt, scheduleUpdatedAt: schedule.updatedAt };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type PublicationProbe = { kind: 'published'; at: string } | { kind: 'unpublished' } | { kind: 'unmeasured'; reason: string };
|
|
39
|
+
|
|
40
|
+
function actualPublicationProbe(version: string, root: string): PublicationProbe {
|
|
41
|
+
const path = join(root, 'release', version, 'release.json');
|
|
42
|
+
let raw: string;
|
|
43
|
+
try { raw = readFileSync(path, 'utf8'); }
|
|
44
|
+
catch (error) {
|
|
45
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return { kind: 'unpublished' };
|
|
46
|
+
return { kind: 'unmeasured', reason: 'release-record-read-failed' };
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const record = JSON.parse(raw) as { version?: unknown; publishedAt?: unknown };
|
|
50
|
+
if (record.version !== version) return { kind: 'unmeasured', reason: 'release-record-version-mismatch' };
|
|
51
|
+
if (record.publishedAt == null) return { kind: 'unmeasured', reason: 'release-record-published-at-missing' };
|
|
52
|
+
if (typeof record.publishedAt !== 'string' || !Number.isFinite(Date.parse(record.publishedAt)))
|
|
53
|
+
return { kind: 'unmeasured', reason: 'release-record-published-at-invalid' };
|
|
54
|
+
return { kind: 'published', at: record.publishedAt };
|
|
55
|
+
} catch { return { kind: 'unmeasured', reason: 'release-record-json-failed' }; }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ReleaseDayScheduleState { publication: PublicationProbe; greenObserved: boolean }
|
|
59
|
+
|
|
60
|
+
/** A late release and the next gate can both be due; return both instead of starving either. */
|
|
61
|
+
export function selectReleaseDaySchedules(
|
|
62
|
+
schedules: ReleaseSchedule[], now: Date, state: (version: string) => ReleaseDayScheduleState,
|
|
63
|
+
): ReleaseSchedule[] {
|
|
64
|
+
const ordered = schedules.filter(s => Number.isFinite(Date.parse(s.cutAt))).sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt));
|
|
65
|
+
const time = now.getTime();
|
|
66
|
+
const dueFuture = ordered.filter(s => {
|
|
67
|
+
const cut = Date.parse(s.cutAt);
|
|
68
|
+
const firstDue = Math.min(cut - 30 * 60_000, s.landBy ? Date.parse(s.landBy) : cut);
|
|
69
|
+
return time < cut && time >= firstDue;
|
|
70
|
+
});
|
|
71
|
+
const past = ordered.filter(s => Date.parse(s.cutAt) <= time);
|
|
72
|
+
const unpublished = past.filter(s => state(s.version).publication.kind === 'unpublished');
|
|
73
|
+
const unmeasured = past.filter(s => state(s.version).publication.kind === 'unmeasured');
|
|
74
|
+
const pendingGreen = past.filter(s => {
|
|
75
|
+
const status = state(s.version);
|
|
76
|
+
return status.publication.kind === 'published' && !status.greenObserved;
|
|
77
|
+
});
|
|
78
|
+
const due = [...dueFuture, ...unpublished, ...unmeasured, ...pendingGreen];
|
|
79
|
+
if (due.length) return [...new Map(due.map(s => [s.version, s])).values()];
|
|
80
|
+
const upcoming = ordered.find(s => Date.parse(s.cutAt) > time);
|
|
81
|
+
return upcoming && Date.parse(upcoming.cutAt) - time <= 24 * 60 * 60_000 ? [upcoming] : [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ReleaseDayShadowDeps {
|
|
85
|
+
now?: () => Date;
|
|
86
|
+
releaseRoot?: string;
|
|
87
|
+
stateRoot?: string;
|
|
88
|
+
schedules?: () => ReleaseSchedule[];
|
|
89
|
+
publishedAt?: (version: string, root: string) => string | null;
|
|
90
|
+
log?: (event: string, data: Record<string, unknown>) => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface ReleaseDayShadowResult extends ReleaseDayDecision { recorded: boolean }
|
|
94
|
+
|
|
95
|
+
export function runReleaseDayShadow(deps: ReleaseDayShadowDeps = {}): ReleaseDayShadowResult[] {
|
|
96
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
97
|
+
// The graph child's stamped universe also owns its release schedule under --test.
|
|
98
|
+
const releaseRoot = deps.releaseRoot ?? effectiveInstanceRoot();
|
|
99
|
+
const schedules = (deps.schedules ?? (() => listSchedules(releaseRoot)))();
|
|
100
|
+
const log = deps.log ?? ((event: string, data: Record<string, unknown>) => debug.log('steward.release-day', event, data));
|
|
101
|
+
if (!schedules.length) { log('decision', { phase: 'none', reason: 'no-release-schedule', at: now.toISOString(), recorded: false, mode: 'shadow' }); return []; }
|
|
102
|
+
const dir = join(deps.stateRoot ?? effectiveInstanceRoot(), 'steward');
|
|
103
|
+
mkdirSync(dir, { recursive: true });
|
|
104
|
+
const path = join(dir, 'release-day-shadow.sqlite');
|
|
105
|
+
const db = new Database(path, { create: true, strict: true });
|
|
106
|
+
try {
|
|
107
|
+
chmodSync(path, 0o600);
|
|
108
|
+
db.exec('PRAGMA busy_timeout = 10000');
|
|
109
|
+
db.exec('CREATE TABLE IF NOT EXISTS phase_observations (version TEXT NOT NULL, phase TEXT NOT NULL, due_at TEXT NOT NULL, observed_at TEXT NOT NULL, schedule_updated_at TEXT NOT NULL, PRIMARY KEY (version, phase))');
|
|
110
|
+
const states = new Map<string, ReleaseDayScheduleState>();
|
|
111
|
+
for (const schedule of schedules) {
|
|
112
|
+
const observed = db.query('SELECT phase FROM phase_observations WHERE version = ?').all(schedule.version) as Array<{ phase: string }>;
|
|
113
|
+
const injected = deps.publishedAt?.(schedule.version, releaseRoot);
|
|
114
|
+
states.set(schedule.version, {
|
|
115
|
+
publication: deps.publishedAt
|
|
116
|
+
? injected == null ? { kind: 'unpublished' }
|
|
117
|
+
: Number.isFinite(Date.parse(injected)) ? { kind: 'published', at: injected }
|
|
118
|
+
: { kind: 'unmeasured', reason: 'injected-published-at-invalid' }
|
|
119
|
+
: actualPublicationProbe(schedule.version, releaseRoot),
|
|
120
|
+
greenObserved: observed.some(row => row.phase === 'green-back'),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const selected = selectReleaseDaySchedules(schedules, now, version => states.get(version)!);
|
|
124
|
+
if (!selected.length) { log('decision', { phase: 'none', reason: 'outside-release-window', at: now.toISOString(), recorded: false, mode: 'shadow' }); return []; }
|
|
125
|
+
const results: ReleaseDayShadowResult[] = [];
|
|
126
|
+
for (const schedule of selected) {
|
|
127
|
+
const publication = states.get(schedule.version)!.publication;
|
|
128
|
+
if (publication.kind === 'unmeasured') {
|
|
129
|
+
const result: ReleaseDayShadowResult = { version: schedule.version, phase: 'unmeasured', dueAt: null,
|
|
130
|
+
scheduleUpdatedAt: schedule.updatedAt, reason: publication.reason, recorded: false };
|
|
131
|
+
log('unmeasured', { ...result, at: now.toISOString(), mode: 'shadow' });
|
|
132
|
+
results.push(result);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const publishedAt = publication.kind === 'published' ? publication.at : null;
|
|
136
|
+
const decision = decideReleaseDayPhase(schedule, now, publishedAt);
|
|
137
|
+
let recorded = false;
|
|
138
|
+
if (decision.phase !== 'none') {
|
|
139
|
+
const row = db.query('INSERT OR IGNORE INTO phase_observations (version, phase, due_at, observed_at, schedule_updated_at) VALUES (?, ?, ?, ?, ?)')
|
|
140
|
+
.run(decision.version, decision.phase, decision.dueAt!, now.toISOString(), decision.scheduleUpdatedAt);
|
|
141
|
+
recorded = row.changes === 1;
|
|
142
|
+
}
|
|
143
|
+
log('decision', { ...decision, at: now.toISOString(), recorded, mode: 'shadow', publishedAt });
|
|
144
|
+
results.push({ ...decision, recorded });
|
|
145
|
+
}
|
|
146
|
+
return results;
|
|
147
|
+
} finally { db.close(); }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (import.meta.main) {
|
|
151
|
+
try {
|
|
152
|
+
const sinkReady = await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('steward');
|
|
153
|
+
if (!sinkReady) throw new Error('durable log sink unavailable');
|
|
154
|
+
const results = runReleaseDayShadow();
|
|
155
|
+
console.log(`steward release-day shadow: ${results.map(result => `${result.version}:${result.phase}${result.recorded ? ' · recorded' : ''}`).join(', ') || 'none'}`);
|
|
156
|
+
}
|
|
157
|
+
catch (error) { console.error(`steward release-day shadow failed: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
|
|
158
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Resync labeled PRs before the steward's TA rejudge node sees their heads. */
|
|
2
|
+
import { debug } from '../debug/log.js';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
7
|
+
import { resolveHqLaunchSeat } from '../hq/seats.js';
|
|
8
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
9
|
+
import { runResyncShadow, type ResyncShadowSummary } from '../loops/orchestrator/resync-shadow.js';
|
|
10
|
+
import { listSchedules, type ReleaseSchedule } from '../release-loop/release-schedule.js';
|
|
11
|
+
|
|
12
|
+
type ReleaseRunState = 'active' | 'idle' | 'unknown';
|
|
13
|
+
type ReleaseWindowState = 'inside' | 'outside' | 'unknown';
|
|
14
|
+
type ResyncTickResult = { decision: 'ran' | 'failed' | 'skipped-release' | 'skipped-release-window'; summary?: ResyncShadowSummary; reason?: string };
|
|
15
|
+
|
|
16
|
+
export function resyncPendingPath(): string { return join(effectiveInstanceRoot(), 'steward', 'resync-pending.json'); }
|
|
17
|
+
|
|
18
|
+
/** The same registered, detached OP checkout used by steward launch; the installed package is not a git repository. */
|
|
19
|
+
export function stewardResyncRepoRoot(
|
|
20
|
+
resolveSeat: typeof resolveHqLaunchSeat = resolveHqLaunchSeat,
|
|
21
|
+
configDir: () => string = getElanousConfigDir,
|
|
22
|
+
): string { return resolveSeat('OP', configDir()); }
|
|
23
|
+
|
|
24
|
+
/** A release may detach the OP checkout. Unknown process state also skips remote branch writes. */
|
|
25
|
+
export function probeReleaseRun(
|
|
26
|
+
list: () => { status: number | null; stdout?: string; error?: Error } = () => spawnSync('pgrep', ['-f', 'release[[:space:]]+(run|resume)'], { encoding: 'utf8', timeout: 5_000 }),
|
|
27
|
+
command: (pid: string) => string | null = pid => {
|
|
28
|
+
const result = spawnSync('ps', ['-p', pid, '-o', 'command='], { encoding: 'utf8', timeout: 5_000 });
|
|
29
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
30
|
+
},
|
|
31
|
+
): ReleaseRunState {
|
|
32
|
+
try {
|
|
33
|
+
const result = list();
|
|
34
|
+
if (result.error || result.status === null) return 'unknown';
|
|
35
|
+
if (result.status === 1) return 'idle';
|
|
36
|
+
if (result.status !== 0) return 'unknown';
|
|
37
|
+
const pids = (result.stdout ?? '').trim().split(/\s+/).filter(pid => /^\d+$/.test(pid));
|
|
38
|
+
if (!pids.length) return 'unknown';
|
|
39
|
+
for (const pid of pids) {
|
|
40
|
+
const argv = command(pid);
|
|
41
|
+
if (argv === null) continue;
|
|
42
|
+
const executable = argv.trim().split(/\s+/)[0]?.split('/').at(-1);
|
|
43
|
+
if (['bun', 'node', 'eln', 'elanous'].includes(executable ?? '') && /(?:^|\s)release\s+(?:run|resume)(?:\s|$)/.test(argv)) return 'active';
|
|
44
|
+
// pgrep saw a release-shaped command, but a shell/wrapper cannot prove whether it will launch the CLI.
|
|
45
|
+
}
|
|
46
|
+
return 'unknown';
|
|
47
|
+
} catch { return 'unknown'; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A release cut may start after the process probe; the scheduled cut/publication window is a fixed safety boundary. */
|
|
51
|
+
export function releaseWindowState(
|
|
52
|
+
nowMs = Date.now(),
|
|
53
|
+
read: () => ReleaseSchedule[] = listSchedules,
|
|
54
|
+
): ReleaseWindowState {
|
|
55
|
+
try {
|
|
56
|
+
const schedules = read();
|
|
57
|
+
if (!schedules.length) return 'unknown';
|
|
58
|
+
const rows = schedules.map(row => ({ cut: Date.parse(row.cutAt), publish: Date.parse(row.publishAt ?? '') }));
|
|
59
|
+
if (rows.some(row => !Number.isFinite(row.cut))) return 'unknown';
|
|
60
|
+
const latestPast = rows.filter(row => row.cut <= nowMs).sort((a, b) => b.cut - a.cut)[0];
|
|
61
|
+
if (latestPast && !Number.isFinite(latestPast.publish)) return 'unknown';
|
|
62
|
+
if (latestPast && nowMs <= latestPast.publish + 60 * 60_000) return 'inside';
|
|
63
|
+
const next = rows.filter(row => row.cut > nowMs).sort((a, b) => a.cut - b.cut)[0];
|
|
64
|
+
if (!next || !Number.isFinite(next.publish) || next.publish < next.cut) return 'unknown';
|
|
65
|
+
return nowMs >= next.cut - 40 * 60_000 ? 'inside' : 'outside';
|
|
66
|
+
} catch { return 'unknown'; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function defaultResyncRun(
|
|
70
|
+
repoRoot = stewardResyncRepoRoot(),
|
|
71
|
+
execute: typeof runResyncShadow = runResyncShadow,
|
|
72
|
+
): Promise<ResyncShadowSummary> { return execute(3, { repoRoot }, 'live'); }
|
|
73
|
+
|
|
74
|
+
export async function resyncTick(
|
|
75
|
+
run: () => Promise<ResyncShadowSummary> = () => defaultResyncRun(),
|
|
76
|
+
log: (event: string, data: Record<string, unknown>) => void = (event, data) => debug.log('loop.steward', event, data),
|
|
77
|
+
pendingPath: string = resyncPendingPath(),
|
|
78
|
+
): Promise<ResyncTickResult> { return resyncTickWithReleaseProbe(run, log, pendingPath, probeReleaseRun); }
|
|
79
|
+
|
|
80
|
+
export async function resyncTickWithReleaseProbe(
|
|
81
|
+
run: () => Promise<ResyncShadowSummary>,
|
|
82
|
+
log: (event: string, data: Record<string, unknown>) => void,
|
|
83
|
+
pendingPath: string,
|
|
84
|
+
releaseProbe: () => ReleaseRunState,
|
|
85
|
+
windowProbe: () => ReleaseWindowState = releaseWindowState,
|
|
86
|
+
): Promise<ResyncTickResult> {
|
|
87
|
+
let result: ResyncTickResult;
|
|
88
|
+
try {
|
|
89
|
+
const release = releaseProbe();
|
|
90
|
+
if (release !== 'idle') {
|
|
91
|
+
result = { decision: 'skipped-release', reason: release === 'active' ? 'release run active' : 'release process probe unavailable' };
|
|
92
|
+
} else {
|
|
93
|
+
const window = windowProbe();
|
|
94
|
+
if (window !== 'outside') {
|
|
95
|
+
result = { decision: 'skipped-release-window', reason: window === 'inside' ? 'scheduled release window' : 'release schedule unavailable' };
|
|
96
|
+
} else {
|
|
97
|
+
const summary = await run();
|
|
98
|
+
if (summary.saved > 0) {
|
|
99
|
+
mkdirSync(dirname(pendingPath), { recursive: true });
|
|
100
|
+
writeFileSync(pendingPath, JSON.stringify({ savedAt: new Date().toISOString() }));
|
|
101
|
+
}
|
|
102
|
+
result = { decision: 'ran', summary };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) { result = { decision: 'failed', reason: (error instanceof Error ? error.message : String(error)).slice(-300) }; }
|
|
107
|
+
try { log('resync-tick', { ...result }); }
|
|
108
|
+
catch (error) {
|
|
109
|
+
result = { decision: 'failed', reason: `log failed: ${error instanceof Error ? error.message : String(error)}`.slice(-300) };
|
|
110
|
+
console.error(`steward resync: ${result.reason}`);
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (import.meta.main) {
|
|
116
|
+
let sinkReady = false;
|
|
117
|
+
try { sinkReady = await (await import('../domains/standalone-log-sink.js')).registerStandaloneLogSink('steward'); }
|
|
118
|
+
catch (error) { console.error(`steward resync: log sink registration failed — ${error instanceof Error ? error.message : String(error)}`); }
|
|
119
|
+
// A missing durable sink must not precede a remote branch update.
|
|
120
|
+
const result = sinkReady ? await resyncTick() : { decision: 'failed' as const, reason: 'log sink unavailable' };
|
|
121
|
+
console.log(`steward resync: ${result.decision}${result.reason ? ` — ${result.reason}` : ''}`);
|
|
122
|
+
process.exitCode = 0;
|
|
123
|
+
}
|
|
@@ -166,6 +166,7 @@ export async function executeNextAction(action: NextAction, deps: ActionDeps): P
|
|
|
166
166
|
catch { return card('PR labels unavailable'); }
|
|
167
167
|
if (!Array.isArray(labels) || !labels.every(label => label && typeof label === 'object' && typeof label.name === 'string')) return card('PR labels unavailable');
|
|
168
168
|
if (labels.some(label => label.name === 'elanous:release-path')) return card('release-path');
|
|
169
|
+
if (labels.some(label => label.name === 'elanous:keep')) return card('elanous:keep');
|
|
169
170
|
const day = new Date().toLocaleDateString('en-CA', { timeZone: 'Asia/Seoul' });
|
|
170
171
|
if (deps.landingDay && deps.landingDay !== day) deps.landingsToday = 0;
|
|
171
172
|
deps.landingDay = day;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/** Attach an existing open PR to a TA card without starting another implementation run. */
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { loadRunLedger, runLedgerDir, type RunLedgerEntry } from '../self-implement/run-ledger.js';
|
|
4
|
+
import { remoteLedgerMirrorDir } from './parent-host.js';
|
|
5
|
+
import { prodInstanceRoot } from '../instance/resolve.js';
|
|
6
|
+
import { cardPrNumber, updateTaskCard, type TaskCard } from './task-hand.js';
|
|
7
|
+
|
|
8
|
+
export interface AdoptPrView {
|
|
9
|
+
number: number; url: string; state: string; baseRefName: string; headRefOid: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AdoptPrInput {
|
|
12
|
+
pr: AdoptPrView;
|
|
13
|
+
repo: string;
|
|
14
|
+
statePath: string;
|
|
15
|
+
cardId?: string;
|
|
16
|
+
version?: string;
|
|
17
|
+
checklist?: { version: string; id: string; title: string; status: string };
|
|
18
|
+
now?: Date;
|
|
19
|
+
terminalRunEvidence?: boolean;
|
|
20
|
+
unlinkedTerminal?: boolean;
|
|
21
|
+
observedRun?: { runId?: string; runChildId?: string; parentHost?: string; launchId?: string };
|
|
22
|
+
observedChecklist?: { id: string; version: string; status: string; cardVersion?: string };
|
|
23
|
+
checklistAtWrite?: () => { id: string; status: string } | undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A bound implementation may still push to its PR. Require the child to have stopped on this exact PR first. */
|
|
27
|
+
export function adoptedRunTerminal(card: TaskCard, pr: AdoptPrView,
|
|
28
|
+
load: (runId: string, dir: string) => RunLedgerEntry[] | null = loadRunLedger, allowUnlinked = false): boolean {
|
|
29
|
+
if (!card.runId) return false;
|
|
30
|
+
const dir = card.parentHost ? remoteLedgerMirrorDir(prodInstanceRoot(), card.parentHost.host) : runLedgerDir();
|
|
31
|
+
try {
|
|
32
|
+
const parent = load(card.runId, dir);
|
|
33
|
+
if (!parent) return false;
|
|
34
|
+
const childIds = parent.filter((entry) => entry.event === 'pod-child-run' && typeof entry.data?.childRunId === 'string')
|
|
35
|
+
.map((entry) => String(entry.data.childRunId));
|
|
36
|
+
const candidates = childIds.length ? childIds : [card.runId];
|
|
37
|
+
let allStopped = candidates.length > 0;
|
|
38
|
+
let noPrEvents = true;
|
|
39
|
+
let exactMatches = 0;
|
|
40
|
+
for (const childId of candidates) {
|
|
41
|
+
const child = childId === card.runId ? parent : load(childId, dir);
|
|
42
|
+
if (!child) { allStopped = false; continue; }
|
|
43
|
+
let prAt = -1;
|
|
44
|
+
let statusAt = -1;
|
|
45
|
+
for (let at = child.length - 1; at >= 0 && (prAt < 0 || statusAt < 0); at--) {
|
|
46
|
+
if (prAt < 0 && child[at]?.event === 'pr-opened') prAt = at;
|
|
47
|
+
if (statusAt < 0 && child[at]?.event === 'run-status') statusAt = at;
|
|
48
|
+
}
|
|
49
|
+
const opened = child[prAt];
|
|
50
|
+
const status = child[statusAt];
|
|
51
|
+
if (prAt >= 0) noPrEvents = false;
|
|
52
|
+
if (!status || !['completed', 'failed', 'cancelled'].includes(String(status.data?.runStatus))) allStopped = false;
|
|
53
|
+
if ((!card.runChildId || card.runChildId === childId) && opened?.data?.number === pr.number && opened.data?.url === pr.url && statusAt > prAt) exactMatches++;
|
|
54
|
+
}
|
|
55
|
+
if (!allStopped) return false;
|
|
56
|
+
if (exactMatches === 1) return true;
|
|
57
|
+
if (allowUnlinked && exactMatches === 0 && noPrEvents && (!card.runChildId || candidates.includes(card.runChildId))) return true;
|
|
58
|
+
} catch { /* unreadable is not terminal evidence */ }
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The PR is read from the selected repository before this call; rejudge checks it again before every live move. */
|
|
63
|
+
export function adoptExistingPr(input: AdoptPrInput): { card: TaskCard; created: boolean; changed: boolean } {
|
|
64
|
+
const { pr } = input;
|
|
65
|
+
const slug = pr.url.match(/^https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/\d+\/?$/)?.[1];
|
|
66
|
+
if (!Number.isSafeInteger(pr.number) || pr.number < 1 || pr.state !== 'OPEN' || pr.baseRefName !== 'main'
|
|
67
|
+
|| !slug || slug.toLowerCase() !== input.repo.toLowerCase()
|
|
68
|
+
|| Number(pr.url.match(/\/pull\/(\d+)/)?.[1]) !== pr.number
|
|
69
|
+
|| !/^[a-f0-9]{40}$/i.test(pr.headRefOid)) throw new Error('입양할 PR은 현재 저장소의 OPEN/main PR 번호·URL·head SHA40이어야 한다');
|
|
70
|
+
if ((input.cardId === undefined) === (input.checklist === undefined)) throw new Error('--card 또는 --checklist/--version 중 하나만 준다');
|
|
71
|
+
if (input.cardId && !input.version) throw new Error('기존 카드는 체크리스트 판을 확정해야 한다');
|
|
72
|
+
if (input.checklist && (!input.checklist.id || !input.checklist.title || !input.checklist.version
|
|
73
|
+
|| ['green', 'done'].includes(input.checklist.status))) throw new Error('입양할 체크리스트 칸은 미완 판·id·제목이어야 한다');
|
|
74
|
+
const id = input.cardId ?? `ta-${(input.now ?? new Date()).toISOString().slice(0, 10).replaceAll('-', '')}-${randomBytes(3).toString('hex')}`;
|
|
75
|
+
const at = (input.now ?? new Date()).toISOString();
|
|
76
|
+
let created = false;
|
|
77
|
+
let changed = false;
|
|
78
|
+
const result = updateTaskCard(input.statePath, id, (current, tasks) => {
|
|
79
|
+
const duplicate = Object.values(tasks).find((card) => card.id !== id && cardPrNumber(card) === pr.number);
|
|
80
|
+
if (duplicate) throw new Error(`PR #${pr.number}는 이미 카드 ${duplicate.id}에 묶였다`);
|
|
81
|
+
const checklistId = current?.checklistId ?? input.checklist?.id;
|
|
82
|
+
const checklistVersion = input.version ?? input.checklist?.version;
|
|
83
|
+
const sameCell = Object.values(tasks).find((card) => card.id !== id && card.status === 'launched' && card.taLand === true
|
|
84
|
+
&& cardPrNumber(card) !== undefined && card.checklistId === checklistId
|
|
85
|
+
&& (!card.checklistVersion || !checklistVersion || card.checklistVersion === checklistVersion));
|
|
86
|
+
if (sameCell) throw new Error(`칸 ${checklistId}에 이미 TA 착지 후보 카드 ${sameCell.id}가 있다`);
|
|
87
|
+
if (!input.cardId) {
|
|
88
|
+
const owner = Object.values(tasks).find((card) => card.status === 'launched' && card.checklistId === checklistId
|
|
89
|
+
&& (!card.checklistVersion || !checklistVersion || card.checklistVersion === checklistVersion));
|
|
90
|
+
if (owner) throw new Error(`칸 ${checklistId}에 기존 카드 ${owner.id}가 있다 — --card로 그 카드를 선택한다`);
|
|
91
|
+
}
|
|
92
|
+
if (input.cardId && (!input.observedRun || current?.runId !== input.observedRun.runId
|
|
93
|
+
|| current?.runChildId !== input.observedRun.runChildId
|
|
94
|
+
|| current?.parentHost?.host !== input.observedRun.parentHost
|
|
95
|
+
|| current?.launchId !== input.observedRun.launchId))
|
|
96
|
+
throw new Error(`입양 중 카드 런 식별자가 바뀌었다: ${id}`);
|
|
97
|
+
if (input.cardId && (!input.observedChecklist || current?.checklistId !== input.observedChecklist.id
|
|
98
|
+
|| current?.checklistVersion !== input.observedChecklist.cardVersion
|
|
99
|
+
|| input.version !== input.observedChecklist.version
|
|
100
|
+
|| ['green', 'done'].includes(input.observedChecklist.status)))
|
|
101
|
+
throw new Error(`입양 중 카드 칸·판이 바뀌었다: ${id}`);
|
|
102
|
+
const liveCell = input.checklistAtWrite?.();
|
|
103
|
+
if (input.checklistAtWrite && (!liveCell || liveCell.id !== checklistId || ['green', 'done'].includes(liveCell.status)))
|
|
104
|
+
throw new Error(`입양 중 체크리스트 칸이 바뀌거나 완료됐다: ${checklistId}`);
|
|
105
|
+
if (current?.adoptedPr?.pr === pr.number && cardPrNumber(current) === pr.number && current.taLand === true
|
|
106
|
+
&& typeof current.pr === 'object' && current.pr.url === pr.url) {
|
|
107
|
+
if (current.status !== 'launched') throw new Error(`이미 닫힌 입양 카드다: ${id} (${current.status})`);
|
|
108
|
+
if (current.checklistVersion !== (input.version ?? input.checklist?.version)) throw new Error(`이미 입양한 카드의 판이 다르다: ${current.checklistVersion ?? '미기록'} ≠ ${input.version ?? input.checklist?.version}`);
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
if (input.cardId) {
|
|
112
|
+
const boundPr = current && cardPrNumber(current);
|
|
113
|
+
const sameBoundPr = boundPr === pr.number && typeof current?.pr === 'object' && current.pr.url === pr.url;
|
|
114
|
+
if (!current || current.status !== 'launched' || !current.checklistId
|
|
115
|
+
|| (boundPr !== undefined && !sameBoundPr)
|
|
116
|
+
|| (input.checklist && current.checklistId !== input.checklist.id)) throw new Error(`입양할 launched·미바인딩/동일 PR 바인딩 카드가 아니다: ${id}`);
|
|
117
|
+
if (boundPr !== undefined && input.unlinkedTerminal)
|
|
118
|
+
throw new Error(`이미 PR이 바인딩된 카드는 --unlinked-terminal로 입양할 수 없다: ${id}`);
|
|
119
|
+
if (input.terminalRunEvidence !== true) throw new Error(`구현 런 종결·PR 근거가 없다: ${current.runId ?? current.launchId ?? 'runId 없음'} — runId 없는 카드는 tasks close 후 --checklist 새 카드로 입양`);
|
|
120
|
+
if (input.version && current.checklistVersion && current.checklistVersion !== input.version)
|
|
121
|
+
throw new Error(`카드 판이 다르다: ${current.checklistVersion} ≠ ${input.version}`);
|
|
122
|
+
} else {
|
|
123
|
+
if (current) throw new Error(`새 카드 id 충돌: ${id}`);
|
|
124
|
+
}
|
|
125
|
+
const base: TaskCard = current ?? {
|
|
126
|
+
id, text: input.checklist!.title, checklistId: input.checklist!.id,
|
|
127
|
+
createdAt: at, status: 'launched', history: [],
|
|
128
|
+
};
|
|
129
|
+
created = !current;
|
|
130
|
+
changed = true;
|
|
131
|
+
return { ...base, ...(input.version || input.checklist ? { checklistVersion: input.version ?? input.checklist!.version } : {}),
|
|
132
|
+
taLand: true, pr: { number: pr.number, url: pr.url },
|
|
133
|
+
adoptedPr: { pr: pr.number, url: pr.url, head: pr.headRefOid, at },
|
|
134
|
+
history: [...base.history, { at, event: 'pr-adopted', pr: pr.number, head: pr.headRefOid,
|
|
135
|
+
detail: `기존 OPEN PR 입양 · ${pr.url} · 새 구현 발사 0${input.unlinkedTerminal ? ' · 명시적 비연결 종결 원장' : ''}` }],
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
if (!result) throw new Error(`카드 쓰기 실패: ${id}`);
|
|
139
|
+
return { card: result, created, changed };
|
|
140
|
+
}
|
package/src/task-agent/board.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { releaseLedgerRoot } from '../instance/resolve.js';
|
|
|
12
12
|
import { listChecklist, type ChecklistStatus } from '../release-loop/checklist.js';
|
|
13
13
|
import { buildRunProgressRows, queryRunningRuns, type RunningRunsResult, type RunProgressRow } from '../self-implement/running-runs.js';
|
|
14
14
|
import {
|
|
15
|
-
cardPrNumber, CLOSED_BY_HAND_EVENT, HAND_MOVE_HISTORY_EVENT, LIVE_MOVE_HISTORY_EVENT, LIVE_MOVE_RESULT_EVENT, readTaskAgentState,
|
|
15
|
+
boundPrTerminal, cardPrNumber, CLOSED_BY_HAND_EVENT, HAND_MOVE_HISTORY_EVENT, LIVE_MOVE_HISTORY_EVENT, LIVE_MOVE_RESULT_EVENT, readTaskAgentState,
|
|
16
16
|
type CompletionKind, type TaskCard,
|
|
17
17
|
} from './task-hand.js';
|
|
18
18
|
|
|
@@ -29,6 +29,10 @@ export interface BoardTaskLedger {
|
|
|
29
29
|
lastEvent: { at: string; event: string; detail: string | null } | null;
|
|
30
30
|
/** 카드가 failed · launch-failed 일 때 마지막 줄의 사유(예: `failed/needs-owner — exit 1 · pod-failure`). 그 밖에는 null. */
|
|
31
31
|
exitReason: string | null;
|
|
32
|
+
/** Terminal PR observation, only when it still matches this card's binding. */
|
|
33
|
+
prTerminal?: NonNullable<TaskCard['prTerminal']>;
|
|
34
|
+
/** Checklist judgement after merge; partial means this slice ended while the cell remains open. */
|
|
35
|
+
postLand?: NonNullable<TaskCard['postLand']>;
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/** `GET /api/task-agent/board` 응답 — progress 는 진전 관측(`queryRunningRuns`)을 싣는다; 관측을 못 얻으면 null(0을 «없다»로 쓰지 않는다). */
|
|
@@ -87,11 +91,14 @@ function aggregate(node: BoardNode): void {
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
function cardStatus(card: TaskCard): BoardStatus {
|
|
94
|
+
const terminal = boundPrTerminal(card);
|
|
95
|
+
if (terminal) return terminal.state === 'MERGED' ? 'done' : 'blocked';
|
|
90
96
|
if (card.status === 'launch-failed' || card.status === 'failed') return 'blocked';
|
|
91
97
|
return card.greenProposal ? 'done' : 'open';
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
function cardLedger(card: TaskCard): BoardTaskLedger {
|
|
101
|
+
const terminal = boundPrTerminal(card);
|
|
95
102
|
const last = (card.history ?? [])
|
|
96
103
|
.filter((item) => item.event !== LIVE_MOVE_HISTORY_EVENT && item.event !== LIVE_MOVE_RESULT_EVENT && item.event !== HAND_MOVE_HISTORY_EVENT)
|
|
97
104
|
.at(-1);
|
|
@@ -105,6 +112,9 @@ function cardLedger(card: TaskCard): BoardTaskLedger {
|
|
|
105
112
|
lastStatus: card.status,
|
|
106
113
|
lastEvent: last ? { at: last.at, event: last.event, detail: last.detail ?? null } : null,
|
|
107
114
|
exitReason: ended ? reason ?? null : null,
|
|
115
|
+
...(terminal ? { prTerminal: terminal } : {}),
|
|
116
|
+
...(terminal && terminal.state === 'MERGED' && card.postLand?.pr === terminal.pr && card.postLand.head === terminal.head
|
|
117
|
+
? { postLand: card.postLand } : {}),
|
|
108
118
|
};
|
|
109
119
|
}
|
|
110
120
|
|
|
@@ -17,6 +17,7 @@ import { appendRunLedgerEntry, type RunLedgerEntry } from '../self-implement/run
|
|
|
17
17
|
import { cardPrNumber, taskAgentStatePath, updateTaskCard, readTaskCard, type TaskCard } from './task-hand.js';
|
|
18
18
|
import { judgeNextMove, type TaskJudgeInput } from './judge.js';
|
|
19
19
|
import { remoteParentAlive, type SshRunner } from './parent-host.js';
|
|
20
|
+
import { notifyRemoteFailure, remoteFailureReason } from './failure-notify.js';
|
|
20
21
|
|
|
21
22
|
const GOAL_ID = /^[a-f0-9]{16}$/;
|
|
22
23
|
const PR_URL = /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/(\d+)$/;
|
|
@@ -134,6 +135,8 @@ export interface CardRunEvidenceDeps {
|
|
|
134
135
|
dispatchPod?: (runId: string, sinceIso: string) => boolean;
|
|
135
136
|
runExit?: (runId: string) => RunExit | undefined;
|
|
136
137
|
ssh?: SshRunner;
|
|
138
|
+
/** Isolated tests can put incident messages in their own mailbox; production uses the resolved default store. */
|
|
139
|
+
incidentStorePath?: string;
|
|
137
140
|
}
|
|
138
141
|
|
|
139
142
|
async function defaultLoadLedger(): Promise<(runId: string) => RunLedgerEntry[] | null> {
|
|
@@ -145,8 +148,7 @@ async function defaultLoadLedger(): Promise<(runId: string) => RunLedgerEntry[]
|
|
|
145
148
|
// HARNESS-PARENT-ON-MSB1: 원격 부모 런의 원장 거울(`tasks show` 가 ssh 로 당긴다) — 이 우주 ⊕ 연합 «뒤»에만 붙는다.
|
|
146
149
|
try {
|
|
147
150
|
const { remoteLedgerMirrorDirs } = await import('./parent-host.js');
|
|
148
|
-
|
|
149
|
-
dirs = [...new Set([...dirs, ...remoteLedgerMirrorDirs(prodInstanceRoot())])];
|
|
151
|
+
dirs = [...new Set([...dirs, ...remoteLedgerMirrorDirs(effectiveInstanceRoot())])];
|
|
150
152
|
} catch { /* 거울이 없으면 종전 그대로 */ }
|
|
151
153
|
return ledgerLoader(dirs, (runId, dir) => loadRunLedger(runId, dir), federationFailure);
|
|
152
154
|
}
|
|
@@ -216,16 +218,46 @@ export async function readCardRunSnapshot(card: Pick<TaskCard, 'runId' | 'create
|
|
|
216
218
|
*/
|
|
217
219
|
export async function refreshCardRunBinding(cardId: string, statePath = taskAgentStatePath(), deps: CardRunEvidenceDeps = {}): Promise<{ card: TaskCard | undefined; evidence?: CardRunEvidence }> {
|
|
218
220
|
let card = readTaskCard(cardId, statePath);
|
|
219
|
-
// 수동 근거(refSource manual)는
|
|
220
|
-
//
|
|
221
|
+
// 수동 근거(refSource manual)는 아래 바인딩에서 덮지 않는다. 원격 부모의 확정 실패는
|
|
222
|
+
// 수동 근거가 있어도 읽어야 하므로 여기서 반환하지 않는다.
|
|
223
|
+
// 둘 다 차 있으면 다시 읽지 않는다(한 번만) — 단 «10분 발사 근거 없음»으로 failed 된 카드는
|
|
224
|
+
// PR·골이 먼저 묶였어도 되살림 판정을 계속 받는다.
|
|
221
225
|
const awaitingRevival = card?.status === 'failed' && [...card.history].reverse().find((item) => item.event === 'failed')?.detail === LATE_LAUNCH_FAILURE;
|
|
222
|
-
if (!card?.runId ||
|
|
226
|
+
if (!card?.runId || (card.pr !== undefined && card.goalId !== undefined && !awaitingRevival)) return { card };
|
|
223
227
|
const runId = card.runId;
|
|
228
|
+
// A stale HQ mirror cannot prove a terminal remote failure. Refresh it here as well as in `tasks show`,
|
|
229
|
+
// since mission advance and direct callers also reach this function.
|
|
230
|
+
if (card.parentHost && !deps.loadLedger) {
|
|
231
|
+
try {
|
|
232
|
+
const { mirrorRemoteRunLedgers } = await import('./parent-host.js');
|
|
233
|
+
mirrorRemoteRunLedgers(card.parentHost, runId, effectiveInstanceRoot(), deps.ssh);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
try { debug.log('task-agent', 'card-bind-failed', { card: cardId, runId, reason: `remote ledger refresh failed: ${error instanceof Error ? error.message : String(error)}` }); } catch { /* fail-soft */ }
|
|
236
|
+
return { card };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
224
239
|
const snapshot = await readCardRunSnapshot(card, deps);
|
|
225
240
|
if (snapshot.readErrors?.length) {
|
|
226
241
|
try { debug.log('task-agent', 'card-bind-failed', { card: cardId, runId: card.runId, errors: snapshot.readErrors }); } catch { /* fail-soft */ }
|
|
227
242
|
}
|
|
228
243
|
const evidence = bindCardEvidence(card, snapshot);
|
|
244
|
+
const remoteReason = remoteFailureReason(card, snapshot, evidence, deps.ssh);
|
|
245
|
+
if (remoteReason && (card.status === 'launched' || card.status === 'failed')) {
|
|
246
|
+
if (card.status === 'launched') {
|
|
247
|
+
const at = (deps.now ?? (() => new Date()))().toISOString();
|
|
248
|
+
card = updateTaskCard(statePath, cardId, (current) => current?.status === 'launched' && current.runId === runId && !current.pr
|
|
249
|
+
? { ...current, status: 'failed', history: [...current.history, { at, event: 'failed', detail: `failed/needs-owner — ${remoteReason}`, runId }] }
|
|
250
|
+
: undefined) ?? card;
|
|
251
|
+
}
|
|
252
|
+
// An older generic Pod/no-launch failure may have closed this same run before its supervisor tail arrived.
|
|
253
|
+
// The terminal evidence still warrants an incident; message provenance keeps retries at one row.
|
|
254
|
+
const lastFailure = [...card.history].reverse().find((row) => row.event === 'failed');
|
|
255
|
+
if (card.status === 'failed' && lastFailure?.runId === runId && !card.history.some((row) => row.event === 'closed-by-hand')) {
|
|
256
|
+
try { notifyRemoteFailure(card, remoteReason, deps.incidentStorePath); }
|
|
257
|
+
catch (error) { try { debug.log('task-agent', 'failure-notify-failed', { card: cardId, runId, reason: error instanceof Error ? error.message : String(error) }); } catch { /* fail-soft */ } }
|
|
258
|
+
return { card, evidence };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
229
261
|
let revived = false;
|
|
230
262
|
const lastFailure = [...card.history].reverse().find((item) => item.event === 'failed');
|
|
231
263
|
if (card.status === 'failed' && lastFailure?.event === 'failed' && lastFailure.detail === LATE_LAUNCH_FAILURE
|