elanous 0.2.12 → 0.2.13
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/{1394-ee3795b112907122.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{7758-0b018a9e4fc56200.js → 7758-85c2b53f731da024.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8605-0259994816604c49.js → 8605-9da4678f8a310d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-cb169620945d1952.js → page-e5b4e1b256f41076.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-cefe8169e36cf62a.js → page-e49b9b76c565a0b1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-813c4a52c9d05ca8.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-4731f2c215358937.js → page-c85b816e3b269dbb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a022b257950f39e6.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-a2ffc717e04b8b0a.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b59ee4e70fc18fcf.js → page-a0ea8f5a2341ee6b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c172dabd796eb7a3.js → page-36c6c4183353ac9e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-1efd0cb9e0ed9d13.js → page-cb2eb0fd809980da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-d5dc7ff69a37b4cf.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-dfca9b7d2de0b135.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-793fa536b3a6ef37.js → page-66c27608f651b866.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-6dd3bfbbde2a5c2c.js → page-6ddd2109b7fc3a80.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-77447356baad75d2.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-6d604227e1664b71.js → page-f672536f14f0969f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-4048543b4b7ad5e0.js → page-e7a6c80f288b1948.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-4c3a3126abed551a.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-64b0aecb1ca75e75.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-9547b5cec7cf76bf.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-2cddeb9e2aeedc55.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-e297866051595d81.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-baba2e047872fb0f.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-e895da4ec9cac345.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2e9e2fb01c9da1c9.js → page-2b256feed331546f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-d62ceb13411ebba4.js → page-505bb96d69a8622c.js} +1 -1
- 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/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/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/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/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +219 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/release-loop/gate-node.ts +53 -15
- package/scripts/release-loop/ops-upgrade-node.ts +17 -5
- package/scripts/test-deterministic.ts +53 -3
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +12 -7
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +1 -1
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +29 -0
- package/src/cli/pod-cli.ts +10 -3
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +108 -28
- package/src/context-bus/condense.ts +194 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-surfaces.ts +15 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +1 -1
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow.ts +64 -11
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +81 -17
- package/src/harness/harness-cli-command.ts +27 -1
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +4 -2
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/index.ts +199 -7
- package/src/intake-plane/discord-seat-work.ts +13 -6
- package/src/intake-plane/linear-wish.ts +44 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-card.ts +1 -1
- package/src/loops/checker.ts +178 -0
- package/src/loops/loop-cli.ts +37 -3
- package/src/loops/observe.ts +49 -0
- package/src/loops/orchestrator/overlap.ts +141 -0
- package/src/loops/orchestrator/traffic.ts +150 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/verdict.ts +22 -0
- package/src/maturity/feature-maturity.ts +3 -3
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +47 -0
- package/src/nexus/api/personas.ts +2 -1
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +2 -1
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/landed-but-yellow.ts +12 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +90 -16
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +78 -0
- package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +38 -8
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +10 -4
- package/src/telegram.ts +5 -0
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +94 -10
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/chunks/1147-466ec15afacc1a65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7277-0b2fc7c16ba2d64b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9920-5cfdc5e8fd209825.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d2e35228bc497119.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5c05b0da4bc1392c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e0ccdb576812b523.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-a67e11ba962337b5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-e71c84189e8c683e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-1d6a533ce5a6efcb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-490653e8d4cc8852.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-29e961191f6057df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-c822f55b884d9104.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fa4ccfc432424e52.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-9c7e8f744a765daf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-4292d0149020e8a7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-db14d0be7f1c460c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-e5d8072cd7cfef73.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e6dcb83265530636.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-45248f08d6aecae5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-4344faf20216233b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-698bb29f11864b89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-bd6a6dc1ddc273f2.js +0 -1
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{-6XrAoD3eU7NyMHAE4xPS → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { Database } from 'bun:sqlite';
|
|
5
5
|
import { dirname, join, resolve } from 'node:path';
|
|
@@ -8,13 +8,15 @@ import { TRACK_AGENT_FORBIDDEN_ACTION_REGEX, universeLaunch } from '../autopilot
|
|
|
8
8
|
import { debug } from '../debug/log.js';
|
|
9
9
|
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
10
10
|
import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
|
|
11
|
+
import { loopEvent } from '../loops/observe.js';
|
|
11
12
|
import { getUserConfig, type PersonaLoopConfig, type SeatLoopConfig, type UserConfig } from '../user-config.js';
|
|
12
13
|
import { checkSeatBudget, type RunningSeatPod } from '../org/seat-budget.js';
|
|
13
14
|
import { DEFAULT_STALL_PARENTS, findStalls } from '../org/stall-escalation.js';
|
|
14
15
|
import { openMsgStore, type MessageEnvelope } from '../msg/msg-store.js';
|
|
15
16
|
import { loadLayeredPersonaDirs, resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
|
|
16
|
-
import {
|
|
17
|
+
import { orderedPersonaTodos, personaTodoPath, readPersonaTodos, type PersonaTodo } from '../persona/persona-todo.js';
|
|
17
18
|
import { PersonaRegistry } from '../persona/registry.js';
|
|
19
|
+
import { recordSeatAskShadow } from '../seat-dispatch/seat-ask.js';
|
|
18
20
|
import { answerSeat, askSeat, deliveredSeatQuestion, hasSeatAnswer, isSeatId, seatQuestions, type SeatId } from '../seat-dispatch/seat-questions.js';
|
|
19
21
|
import { queryRunningRuns } from '../self-implement/running-runs.js';
|
|
20
22
|
import { checklistHistory, listChecklist, type Checklist } from '../release-loop/checklist.js';
|
|
@@ -33,11 +35,12 @@ export type OpCandidate =
|
|
|
33
35
|
| { kind: 'decision-delegation'; id: string | null; title?: string; category?: DecisionEntry['category']; dueAt?: string; verdict: 'review-delegation' | 'none' };
|
|
34
36
|
export type TcCandidate =
|
|
35
37
|
| { kind: 'merged-pr-yellow-cell'; version: string; id: string; title: string; pr: number; mergedAt: string }
|
|
36
|
-
| { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
|
|
38
|
+
| { kind: 'publication-pr-approval-wait'; pr: number; title: string; readyAt: string; approvalWaitAt: string; paths: string[] }
|
|
39
|
+
| { kind: 'other-seat-cell-defect'; version: string; id: string; title: string; to: SeatId };
|
|
37
40
|
export type TcPullRequest = { number: number; title: string; body?: string; state: 'OPEN' | 'MERGED' | 'CLOSED'; isDraft: boolean; createdAt: string; updatedAt?: string; mergedAt?: string | null; readyAt?: string; approvalWaitAt?: string; paths?: string[]; reviewDecision?: string };
|
|
38
41
|
export type SeatEntry = { seat: string; ts?: string; at: string; status: 'shadow' | 'attempting' | 'outcome-unknown' | 'launched' | 'hitl' | 'wait' | 'asked' | 'answered' | 'awaiting-answer' | 'skipped-budget' | 'skipped-empty'; item?: SeatItem; candidate?: OpCandidate | TcCandidate; action?: SeatAction; reason?: string; runId?: string; inquiry?: SeatInquiry; answer?: string; escalated?: boolean };
|
|
39
42
|
export type SeatLoopResult = SeatEntry | { seat: string; status: 'skipped-off' };
|
|
40
|
-
export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null };
|
|
43
|
+
export type PersonaShadowEntry = { personaId: string; ts: string; status: 'shadow' | 'skipped-empty'; todo: PersonaTodo | null; action?: 'decision' | 'harness' | 'wait'; what?: string };
|
|
41
44
|
export type SeatDeps = {
|
|
42
45
|
personaConfig?: PersonaLoopConfig;
|
|
43
46
|
personaRegistry?: PersonaRegistry;
|
|
@@ -134,8 +137,17 @@ export function personaShadowLedgerPath(personaId: string, root: string, now: Da
|
|
|
134
137
|
return join(root, 'persona-loop', personaId, `${seatDay(now)}.jsonl`);
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
export async function runPersonaLoopOnce(name: string, deps: SeatDeps = {}): Promise<PersonaShadowEntry> {
|
|
141
|
+
const entries = await pickPersonaShadows(deps, name);
|
|
142
|
+
return entries[0]!;
|
|
143
|
+
}
|
|
144
|
+
|
|
137
145
|
export async function runPersonaShadowOnce(deps: SeatDeps = {}): Promise<PersonaShadowEntry[]> {
|
|
138
146
|
if ((deps.personaConfig ?? getUserConfig().loops?.persona)?.enabled !== true) return [];
|
|
147
|
+
return pickPersonaShadows(deps);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function pickPersonaShadows(deps: SeatDeps, name?: string): Promise<PersonaShadowEntry[]> {
|
|
139
151
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
140
152
|
const dir = deps.personaDir ?? (deps.root ? join(root, 'personas') : resolveStatePersonaDir());
|
|
141
153
|
const registry = deps.personaRegistry ?? new PersonaRegistry();
|
|
@@ -145,19 +157,31 @@ export async function runPersonaShadowOnce(deps: SeatDeps = {}): Promise<Persona
|
|
|
145
157
|
}
|
|
146
158
|
const now = (deps.now ?? (() => new Date()))();
|
|
147
159
|
const entries: PersonaShadowEntry[] = [];
|
|
148
|
-
|
|
160
|
+
const profiles = registry.list().sort((a, b) => a.personaId.localeCompare(b.personaId));
|
|
161
|
+
const selected = name === undefined ? profiles : profiles.filter((profile) => profile.personaId.toLowerCase() === name.trim().toLowerCase()
|
|
162
|
+
|| profile.displayName.toLowerCase() === name.trim().toLowerCase());
|
|
163
|
+
if (name !== undefined && selected.length !== 1) throw new Error(selected.length ? `ambiguous persona name: ${name}` : `persona not found: ${name}`);
|
|
164
|
+
for (const profile of selected) {
|
|
149
165
|
const personaId = profile.personaId;
|
|
150
|
-
const
|
|
166
|
+
const todoFile = profile.todo ?? `${personaId}.todo.jsonl`;
|
|
167
|
+
const { todos, errors } = readPersonaTodos(dir, todoFile.slice(0, -'.todo.jsonl'.length));
|
|
151
168
|
for (const error of errors) observe('persona-todo-error', { personaId, error });
|
|
152
|
-
const
|
|
153
|
-
const
|
|
169
|
+
const candidates = orderedPersonaTodos(todos);
|
|
170
|
+
const picked = pickNext({ requests: candidates.map((candidate) => ({
|
|
171
|
+
source: 'request', id: candidate.id, title: candidate.title, text: candidate.title, createdAt: candidate.createdAt,
|
|
172
|
+
})), checklist: [], role: '' }, [], { shadow: true });
|
|
173
|
+
const todo = picked ? candidates.find((candidate) => candidate.id === picked.id) ?? null : null;
|
|
174
|
+
const planned = name !== undefined && todo && picked ? planAction(picked, profile.seat) : undefined;
|
|
175
|
+
const entry: PersonaShadowEntry = { personaId, ts: now.toISOString(), status: todo ? 'shadow' : 'skipped-empty', todo,
|
|
176
|
+
...(planned ? { action: planned.kind, what: planned.text } : {}) };
|
|
154
177
|
const path = personaShadowLedgerPath(personaId, root, now);
|
|
155
178
|
if (deps.appendPersona) deps.appendPersona(path, entry);
|
|
156
179
|
else {
|
|
157
180
|
mkdirSync(dirname(path), { recursive: true });
|
|
158
181
|
appendFileSync(path, `${JSON.stringify(entry)}\n`, { mode: 0o600 });
|
|
159
182
|
}
|
|
160
|
-
|
|
183
|
+
try { debug.log('persona.loop', 'picked', { personaId, status: entry.status, todoId: todo?.id ?? null, title: todo?.title ?? null, action: entry.action ?? null }); }
|
|
184
|
+
catch { /* observation cannot alter the shadow ledger */ }
|
|
161
185
|
entries.push(entry);
|
|
162
186
|
}
|
|
163
187
|
return entries;
|
|
@@ -167,6 +191,25 @@ export async function runSeatLoopTurn(seat: string, deps: SeatDeps = {}): Promis
|
|
|
167
191
|
const result = await runSeatLoopOnce(seat, deps);
|
|
168
192
|
try { await runPersonaShadowOnce(deps); }
|
|
169
193
|
catch (error) { observe('persona-shadow-error', { error: String(error).slice(0, 200) }); }
|
|
194
|
+
try {
|
|
195
|
+
const config = deps.config ?? getUserConfig().loops?.seat ?? { mode: 'off' };
|
|
196
|
+
const profile = result.status === 'skipped-off' ? 'off' : config.mode;
|
|
197
|
+
const context = { runId: 'runId' in result && result.runId ? result.runId : `seat-turn-${randomUUID()}`,
|
|
198
|
+
profile, sourceRef: `docs/roles/${seat}.md` };
|
|
199
|
+
const loopId = `${seat.toLowerCase()}-seat`;
|
|
200
|
+
loopEvent(loopId, 'tick', { ...context, outcome: result.status, reason: result.status });
|
|
201
|
+
if ('inquiry' in result && result.inquiry && (result.status === 'asked' || result.status === 'hitl')) {
|
|
202
|
+
loopEvent(loopId, 'exchange', { ...context, outcome: result.status, reason: 'delegation',
|
|
203
|
+
from: seat, to: result.inquiry.to, itemId: result.item?.id });
|
|
204
|
+
}
|
|
205
|
+
if ('escalated' in result && result.escalated && result.status === 'skipped-budget') {
|
|
206
|
+
loopEvent(loopId, 'exchange', { ...context, outcome: result.status, reason: 'delegation',
|
|
207
|
+
from: seat, to: (config.stall?.parents ?? {})[seat] ?? DEFAULT_STALL_PARENTS[seat], itemId: result.item?.id });
|
|
208
|
+
} else if (result.status === 'skipped-budget') {
|
|
209
|
+
loopEvent(loopId, 'exchange', { ...context, outcome: result.status, reason: 'requeue',
|
|
210
|
+
itemId: result.item?.id });
|
|
211
|
+
}
|
|
212
|
+
} catch { /* observation cannot change a completed seat turn */ }
|
|
170
213
|
return result;
|
|
171
214
|
}
|
|
172
215
|
|
|
@@ -198,6 +241,8 @@ function decisionReceipt(root: string, seat: string, item: SeatItem): DecisionEn
|
|
|
198
241
|
function handledKeys(ledger: readonly SeatEntry[], shadow: boolean, root?: string): Set<string> {
|
|
199
242
|
const inquiryRow = (entry: SeatEntry) => entry.action === 'seat-question' || entry.action === 'seat-answer' || !!entry.inquiry;
|
|
200
243
|
return new Set(ledger.filter((entry, index) => entry.status === 'asked' || entry.status === 'answered'
|
|
244
|
+
// RM3a: only TC's other-seat defect ask-shadows are deduplicated here — an ordinary inquiry recorded in shadow must still be delivered once questions turn on.
|
|
245
|
+
|| (entry.status === 'shadow' && entry.candidate?.kind === 'other-seat-cell-defect')
|
|
201
246
|
|| (entry.status === 'hitl' && (!entry.inquiry || (!!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))))
|
|
202
247
|
|| (entry.inquiry?.to === 'CEO' && !!root && !!entry.item && !!decisionReceipt(root, entry.seat, entry.item))
|
|
203
248
|
|| (!inquiryRow(entry) && entry.status === 'outcome-unknown')
|
|
@@ -303,9 +348,10 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
|
|
|
303
348
|
debug.log('seat.loop', 'schedule-unreadable', { seat, error: String(error).slice(0, 200) });
|
|
304
349
|
}
|
|
305
350
|
const checklist: SeatItem[] = [];
|
|
351
|
+
const closed: string[] = [];
|
|
306
352
|
for (const version of open === null ? [] : versions().sort(versionOrder)) {
|
|
307
353
|
if (!open!.has(version)) {
|
|
308
|
-
|
|
354
|
+
closed.push(version);
|
|
309
355
|
continue;
|
|
310
356
|
}
|
|
311
357
|
let items: ReturnType<typeof itemsOf>;
|
|
@@ -325,6 +371,9 @@ export async function gatherSeatInputs(seat: string, deps: SeatDeps = {}, ledger
|
|
|
325
371
|
}
|
|
326
372
|
}
|
|
327
373
|
}
|
|
374
|
+
if (closed.length > 0) {
|
|
375
|
+
debug.log('seat.loop', 'skip-closed-versions', { seat, count: closed.length, newest: closed.at(-1)!, oldest: closed[0]! });
|
|
376
|
+
}
|
|
328
377
|
checklist.sort((a, b) => versionOrder(a.version!, b.version!) || a.id.localeCompare(b.id));
|
|
329
378
|
return { requests: pending, checklist, role: read(join(deps.repo ?? repoRoot, 'docs', 'roles', `${seat}.md`)).slice(0, 4_000) };
|
|
330
379
|
}
|
|
@@ -375,7 +424,7 @@ function readTcChecklist(root: string): Array<{ version: string; id: string; tit
|
|
|
375
424
|
const db = new Database(path, { readonly: true, strict: true });
|
|
376
425
|
try {
|
|
377
426
|
return db.query(`SELECT a.version, f.id, COALESCE(a.title_override, f.title) AS title, a.status, a.owner
|
|
378
|
-
FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.status
|
|
427
|
+
FROM assignments a JOIN features f ON f.id = a.feature_id WHERE a.status IN ('yellow', 'red') AND a.owner IN ('OP', 'TC', 'MK', 'UX')
|
|
379
428
|
ORDER BY a.version, f.id`).all() as Array<{ version: string; id: string; title: string; status: string; owner: string | null }>;
|
|
380
429
|
} finally { db.close(); }
|
|
381
430
|
}
|
|
@@ -461,16 +510,27 @@ async function tcCandidates(deps: SeatDeps, now: Date): Promise<TcCandidate[]> {
|
|
|
461
510
|
const candidates: TcCandidate[] = [];
|
|
462
511
|
const releaseRoot = deps.root ?? releaseLedgerRoot();
|
|
463
512
|
try {
|
|
464
|
-
const cells =
|
|
513
|
+
const cells = deps.checklistItems && deps.schedules
|
|
465
514
|
? deps.schedules().flatMap(({ version }) => deps.checklistItems!(version).map((cell) => ({ ...cell, version })))
|
|
466
515
|
: readTcChecklist(releaseRoot);
|
|
516
|
+
let shipped: string | undefined;
|
|
517
|
+
try { shipped = releasedVersion(releaseRoot); }
|
|
518
|
+
catch (error) { observe('tc-release-unreadable', { seat: 'TC', error: String(error).slice(0, 200) }); }
|
|
519
|
+
const openVersions = deps.schedules ? new Set(deps.schedules().filter(({ cutAt }) => Number.isFinite(Date.parse(cutAt))
|
|
520
|
+
&& Date.parse(cutAt) > now.getTime()).map(({ version }) => version)) : null;
|
|
467
521
|
for (const cell of cells) {
|
|
468
|
-
if (cell.owner !== 'TC'
|
|
522
|
+
if (cell.owner !== 'TC' && ((shipped && versionOrder(cell.version, shipped) <= 0) || (openVersions && !openVersions.has(cell.version)))) continue;
|
|
523
|
+
if (cell.status !== 'yellow' && cell.status !== 'red') continue;
|
|
524
|
+
if (!cell.owner || !isSeatId(cell.owner)) continue;
|
|
525
|
+
if (cell.owner !== 'TC') {
|
|
526
|
+
if (cell.status === 'red') candidates.push({ kind: 'other-seat-cell-defect', version: cell.version, id: cell.id, title: cell.title, to: cell.owner });
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
469
529
|
for (const pr of merged) {
|
|
470
530
|
const id = cell.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
471
531
|
const reference = new RegExp(`(?<![A-Za-z0-9])${id}(?![A-Za-z0-9])`);
|
|
472
532
|
if (reference.test(pr.title) || reference.test(pr.body ?? '')) {
|
|
473
|
-
candidates.push({ kind: 'merged-pr-yellow-cell', version: cell.version, id: cell.id, title: cell.title, pr: pr.number, mergedAt: pr.mergedAt! });
|
|
533
|
+
if (cell.owner === 'TC' && cell.status === 'yellow') candidates.push({ kind: 'merged-pr-yellow-cell', version: cell.version, id: cell.id, title: cell.title, pr: pr.number, mergedAt: pr.mergedAt! });
|
|
474
534
|
}
|
|
475
535
|
}
|
|
476
536
|
}
|
|
@@ -624,6 +684,20 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
624
684
|
if (seat === 'TC') {
|
|
625
685
|
const candidates = await tcCandidates(deps, now);
|
|
626
686
|
for (const candidate of candidates) {
|
|
687
|
+
if (candidate.kind === 'other-seat-cell-defect') {
|
|
688
|
+
const question = `${candidate.version} 체크리스트 ${candidate.id} · ${candidate.title}: ${candidate.to} 칸 결함을 확인해 주세요.`;
|
|
689
|
+
const recorded = recordSeatAskShadow(stateRoot, { from: 'TC', to: candidate.to, itemId: candidate.id, version: candidate.version, question }, now.getTime());
|
|
690
|
+
const inLedger = ledger.some((row) => row.status === 'shadow' && row.candidate?.kind === 'other-seat-cell-defect'
|
|
691
|
+
&& row.candidate.version === candidate.version && row.candidate.id === candidate.id && row.candidate.to === candidate.to);
|
|
692
|
+
if (!inLedger) {
|
|
693
|
+
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate,
|
|
694
|
+
item: { source: 'checklist', kind: 'cell', id: candidate.id, title: candidate.title, text: candidate.title, version: candidate.version, seat: candidate.to },
|
|
695
|
+
action: 'seat-question', inquiry: { to: candidate.to, question } };
|
|
696
|
+
(deps.append ?? defaultAppend)(path, entry);
|
|
697
|
+
}
|
|
698
|
+
if (!inLedger || recorded) observe('ask-shadow', { from: seat, to: candidate.to, itemId: candidate.id });
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
627
701
|
if (ledger.some((row) => row.status === 'shadow' && JSON.stringify(row.candidate) === JSON.stringify(candidate))) continue;
|
|
628
702
|
const entry: SeatEntry = { seat, ts: now.toISOString(), at: now.toISOString(), status: 'shadow', candidate };
|
|
629
703
|
(deps.append ?? defaultAppend)(path, entry);
|
|
@@ -761,7 +835,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
761
835
|
await run(['decisions', 'raise', '--title', `${seat}: ${ask.question}`, '--category', 'other',
|
|
762
836
|
'--s', `${seat} 질문 · ${item.title}`, '--c', '판단 근거 부족 — 사람의 결정이 필요하다',
|
|
763
837
|
'--option', 'a=승인:사람 판단으로 진행', '--option', 'b=보류:진행하지 않음',
|
|
764
|
-
'--skip-recommend', '근거가 부족하다', '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
|
|
838
|
+
'--skip-recommend', '근거가 부족하다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②', '--agent', 'seat-loop', '--ref', decisionRef(seat, item), '--json']);
|
|
765
839
|
}
|
|
766
840
|
if (!decisionReceipt(root, seat, item)) throw new Error('decisions raise did not deliver a matching card');
|
|
767
841
|
entry.status = 'hitl';
|
|
@@ -866,7 +940,7 @@ export async function runSeatLoopOnce(seat: string, deps: SeatDeps = {}): Promis
|
|
|
866
940
|
const raised = await run(['decisions', 'raise', '--title', title, '--category', 'other',
|
|
867
941
|
'--s', `${seat} 배정 항목: ${action.text.replace(/[\r\n]+/g, ' ')}\n역할: ${inputs.role}`, '--c', '자동 실행 금지 문면에 해당하여 사람 결정이 필요하다',
|
|
868
942
|
'--option', 'a=사람 승인:승인 후 별도로 집행', '--option', 'b=보류:집행하지 않음',
|
|
869
|
-
'--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--agent', 'seat-loop', '--json']);
|
|
943
|
+
'--skip-recommend', '금지 문면은 자동으로 권고하지 않는다', '--no-xcheck', '자리 루프 · 이웃 교환은 DEC-XCHECK ②', '--agent', 'seat-loop', '--json']);
|
|
870
944
|
const decision: unknown = JSON.parse(raised.trim());
|
|
871
945
|
if (!decision || typeof decision !== 'object' || typeof (decision as { id?: unknown }).id !== 'string') {
|
|
872
946
|
throw new Error('decisions raise returned no decision id');
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { userInfo } from 'node:os';
|
|
5
|
+
import { DEFAULT_CONCURRENT_PODS, DEFAULT_DAILY_GOALS } from '../org/seat-budget.js';
|
|
6
|
+
|
|
7
|
+
export interface SeatPackManifest {
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
files: Array<{ path: string; sha256: string }>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class SeatPackLeakError extends Error {
|
|
14
|
+
constructor(file: string, line: number, marker: string) {
|
|
15
|
+
super(`${file}:${line}: public leak (${marker})`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function regularFile(path: string): boolean {
|
|
20
|
+
return lstatSync(path).isFile();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function walk(directory: string, root: string): string[] {
|
|
24
|
+
if (!existsSync(directory)) return [];
|
|
25
|
+
if (!lstatSync(directory).isDirectory()) throw new Error(`not a directory: ${relative(root, directory)}`);
|
|
26
|
+
return readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))
|
|
27
|
+
.flatMap((entry) => {
|
|
28
|
+
const path = join(directory, entry.name);
|
|
29
|
+
if (entry.isSymbolicLink()) throw new Error(`symlink in seat pack: ${relative(root, path)}`);
|
|
30
|
+
return entry.isDirectory() ? walk(path, root) : entry.isFile() ? [relative(root, path)] : [];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function scan(file: string, body: string): void {
|
|
35
|
+
const usernames = new Set([userInfo().username, process.env.ELANOUS_LEAK_HOME_USER].filter((name): name is string => Boolean(name)));
|
|
36
|
+
const accountPatterns = [...usernames].map((name) => new RegExp(`(?<![A-Za-z0-9._-])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9._-])`, 'iu'));
|
|
37
|
+
const patterns: Array<[string, RegExp]> = [
|
|
38
|
+
['royal marker', /\u{1F451}/u],
|
|
39
|
+
['track marker', /(?:🅞|🅣|🅢|🅕|\*\*\[(?:OP|TC|MK|UX|S|T|O|F)\]\*\*|\[(?:OP|TC|MK|UX|S|T|O|F)\])/u],
|
|
40
|
+
['absolute user path', /\/Users\/[A-Za-z0-9._-]+/u],
|
|
41
|
+
...accountPatterns.map((pattern): [string, RegExp] => ['account name', pattern]),
|
|
42
|
+
];
|
|
43
|
+
for (const [index, line] of body.split(/\r?\n/u).entries()) {
|
|
44
|
+
for (const [marker, pattern] of patterns) {
|
|
45
|
+
if (pattern.test(line)) throw new SeatPackLeakError(file, index + 1, marker);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Build from charters, explicitly referenced seat graphs and the seat's own `graphs/<seat>/` folder; never copy an entire shared graph directory by default. */
|
|
51
|
+
export function exportSeatPack(seat: string, out: string, root = resolve(import.meta.dir, '../..')): SeatPackManifest {
|
|
52
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/u.test(seat)) throw new Error(`invalid seat: ${seat}`);
|
|
53
|
+
const repo = resolve(root);
|
|
54
|
+
const output = resolve(out);
|
|
55
|
+
const roles = join(repo, 'docs', 'roles');
|
|
56
|
+
const charter = join(roles, `${seat}.md`);
|
|
57
|
+
if (!existsSync(charter) || !regularFile(charter)) throw new Error(`seat charter not found: docs/roles/${seat}.md`);
|
|
58
|
+
const roleFiles = [`docs/roles/${seat}.md`, ...walk(join(roles, seat), repo).filter((file) => file.endsWith('.md'))];
|
|
59
|
+
const graphFiles = new Set<string>();
|
|
60
|
+
const contents = new Map<string, Buffer>();
|
|
61
|
+
for (const file of roleFiles) {
|
|
62
|
+
const body = readFileSync(join(repo, file));
|
|
63
|
+
contents.set(file, body);
|
|
64
|
+
for (const match of body.toString('utf8').matchAll(/graphs\/([A-Za-z0-9_./-]+(?:\.ya?ml|\/\*\*))/gu)) {
|
|
65
|
+
const ref = `graphs/${match[1]!}`;
|
|
66
|
+
if (ref.split('/').includes('..')) throw new Error(`invalid seat graph: ${ref}`);
|
|
67
|
+
if (ref.endsWith('/**')) {
|
|
68
|
+
if (!existsSync(join(repo, ref.slice(0, -3)))) throw new Error(`seat graph not found: ${ref}`);
|
|
69
|
+
for (const graph of walk(join(repo, ref.slice(0, -3)), repo)) {
|
|
70
|
+
if (/\.ya?ml$/u.test(graph)) graphFiles.add(graph);
|
|
71
|
+
}
|
|
72
|
+
} else graphFiles.add(ref);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// A seat's own graph folder (`graphs/<seat in lower case>/`) belongs to the seat even when the
|
|
76
|
+
// charter no longer mentions it — inclusion must not hinge on a text reference that can be edited away.
|
|
77
|
+
const ownGraphs = join(repo, 'graphs', seat.toLowerCase());
|
|
78
|
+
if (existsSync(ownGraphs)) {
|
|
79
|
+
for (const graph of walk(ownGraphs, repo)) if (/\.ya?ml$/u.test(graph)) graphFiles.add(graph);
|
|
80
|
+
}
|
|
81
|
+
for (const file of graphFiles) {
|
|
82
|
+
const path = join(repo, file);
|
|
83
|
+
if (!existsSync(path) || !regularFile(path)) throw new Error(`seat graph not found: ${file}`);
|
|
84
|
+
contents.set(file, readFileSync(path));
|
|
85
|
+
}
|
|
86
|
+
const verdicts = roleFiles.flatMap((file) => contents.get(file)!.toString('utf8').split(/\r?\n/u)
|
|
87
|
+
.filter((line) => /\|\s*판정선\s*\|/u.test(line)).map((line) => ({ source: file, text: line.split('|').slice(2, -1).join('|').trim() })));
|
|
88
|
+
for (const file of roleFiles) {
|
|
89
|
+
const charter = contents.get(file)!.toString('utf8');
|
|
90
|
+
for (const match of charter.matchAll(/(?:^|\n)\s*[-*]\s*\*\*(?:관문|판정선)\*\*\s*[—:-]\s*([^\n]+)/gu)) {
|
|
91
|
+
verdicts.push({ source: file, text: match[1]!.trim() });
|
|
92
|
+
}
|
|
93
|
+
let inGateSection = false;
|
|
94
|
+
for (const line of charter.split(/\r?\n/u)) {
|
|
95
|
+
if (/^##\s+\d+\.\s+관문(?:\s|\(|$)/u.test(line)) { inGateSection = true; continue; }
|
|
96
|
+
if (/^##\s/u.test(line)) inGateSection = false;
|
|
97
|
+
if (!inGateSection) continue;
|
|
98
|
+
const bullet = line.match(/^\s*[-*]\s+(.+)$/u);
|
|
99
|
+
if (bullet) verdicts.push({ source: file, text: bullet[1]!.trim() });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (verdicts.length === 0) throw new Error(`seat verdict not found for ${seat}`);
|
|
103
|
+
contents.set('verdicts.json', Buffer.from(`${JSON.stringify(verdicts, null, 2)}\n`));
|
|
104
|
+
contents.set('budget.json', Buffer.from(`${JSON.stringify({ dailyGoals: DEFAULT_DAILY_GOALS, concurrentPods: DEFAULT_CONCURRENT_PODS }, null, 2)}\n`));
|
|
105
|
+
const version = (JSON.parse(readFileSync(join(repo, 'package.json'), 'utf8')) as { version: string }).version;
|
|
106
|
+
const files = [...contents].sort(([a], [b]) => a.localeCompare(b)).map(([path, body]) => ({
|
|
107
|
+
path, sha256: createHash('sha256').update(body).digest('hex'),
|
|
108
|
+
}));
|
|
109
|
+
const manifest: SeatPackManifest = { name: seat, version, files };
|
|
110
|
+
const manifestBody = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
111
|
+
for (const [file, body] of [...contents, ['manifest.json', manifestBody] as [string, Buffer]]) scan(file, body.toString('utf8'));
|
|
112
|
+
if (existsSync(output)) {
|
|
113
|
+
if (!lstatSync(output).isDirectory() || readdirSync(output).length > 0) throw new Error(`output must be an empty directory: ${output}`);
|
|
114
|
+
}
|
|
115
|
+
const sources = [roles, join(repo, 'graphs')];
|
|
116
|
+
if (output === repo || sources.some((source) => output === source || source.startsWith(`${output}${sep}`) || output.startsWith(`${source}${sep}`))) {
|
|
117
|
+
throw new Error('output overlaps seat pack sources');
|
|
118
|
+
}
|
|
119
|
+
mkdirSync(output, { recursive: true });
|
|
120
|
+
for (const [file, body] of contents) {
|
|
121
|
+
const dest = join(output, file);
|
|
122
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
123
|
+
writeFileSync(dest, body);
|
|
124
|
+
}
|
|
125
|
+
writeFileSync(join(output, 'manifest.json'), manifestBody);
|
|
126
|
+
return manifest;
|
|
127
|
+
}
|
|
@@ -158,6 +158,8 @@ export interface AskLaunchFlowDeps {
|
|
|
158
158
|
/** ⛔ 못 얻으면 `null` — 같은 이유. */
|
|
159
159
|
recentAuthoringSamples(): RecentAuthoringSample[] | null;
|
|
160
160
|
authorGoal(args: readonly string[], options: Record<string, unknown>): Promise<AskAuthoredGoal>;
|
|
161
|
+
/** Pod launches reserve a host slot before goal authoring begins. */
|
|
162
|
+
beforeAuthoring?(): Promise<void>;
|
|
161
163
|
/** 발사 전 권고용 분해 seam. 생략하면 전용 관측명을 준 기존 분해기를 쓴다. */
|
|
162
164
|
decomposeGoal?(goal: string): Promise<SelfDevDecomposition>;
|
|
163
165
|
/**
|
|
@@ -825,6 +827,7 @@ export async function runAskLaunchFlow(
|
|
|
825
827
|
const launchPreflight = await runPrePreflight(input, deps);
|
|
826
828
|
if (launchPreflight === false) return { kind: 'stopped-before-authoring' };
|
|
827
829
|
|
|
830
|
+
await deps.beforeAuthoring?.();
|
|
828
831
|
const authorAndPreflight = async (): Promise<{
|
|
829
832
|
readonly goalFile: string;
|
|
830
833
|
readonly decision: ReturnType<typeof decideAskPreflight>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { NEXT_MD_PATH, resolveNextMdConflict } from '../release-loop/next-md-merge.js';
|
|
6
|
+
import { getDefaultLogStore } from '../mss/logging/log-store.js';
|
|
6
7
|
|
|
7
8
|
type Command = (bin: string, args: readonly string[], cwd: string) => { status: number | null; stdout: string; stderr: string };
|
|
8
9
|
export type L8MergeQueueInput = { number: number; cwd: string; matchHeadCommit: string };
|
|
@@ -13,6 +14,161 @@ export type L8MergeQueueDeps = {
|
|
|
13
14
|
acquire?: (cwd: string) => Promise<() => void>;
|
|
14
15
|
};
|
|
15
16
|
|
|
17
|
+
export type L8ShadowVerdict = { number: number; head: string; base?: string; verdict: 'pass' | 'conflict' | 'fail'; detail?: string; tests: string[]; at: string };
|
|
18
|
+
export type L8ShadowQueueState = { pending: { number: number; head: string }[]; verdicts: L8ShadowVerdict[] };
|
|
19
|
+
export type L8ShadowQueueDeps = L8MergeQueueDeps & { observe?: (verdict: L8ShadowVerdict) => void | Promise<void> };
|
|
20
|
+
|
|
21
|
+
function shadowCommand(execute: Command, bin: string, args: string[], cwd: string): string {
|
|
22
|
+
const result = execute(bin, args, cwd);
|
|
23
|
+
if (result.status !== 0) throw new Error(`${bin} ${args.join(' ')}: ${(result.stderr || result.stdout || `exit ${result.status}`).trim().slice(0, 500)}`);
|
|
24
|
+
return args.includes('-z') ? result.stdout : result.stdout.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function shadowQueuePath(cwd: string, execute: Command): string {
|
|
28
|
+
const dir = shadowCommand(execute, 'git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], cwd);
|
|
29
|
+
if (!dir) throw new Error('merge queue common git directory unavailable');
|
|
30
|
+
return join(dir, 'elanous-l8-shadow-queue.json');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readShadowQueue(path: string): L8ShadowQueueState {
|
|
34
|
+
if (!existsSync(path)) return { pending: [], verdicts: [] };
|
|
35
|
+
const value: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
36
|
+
if (!value || typeof value !== 'object' || !('pending' in value) || !Array.isArray(value.pending)
|
|
37
|
+
|| !value.pending.every((p: { number?: number; head?: string }) => Number.isSafeInteger(p?.number) && p.number! > 0 && typeof p.head === 'string' && /^[0-9a-f]{40}$/i.test(p.head))
|
|
38
|
+
|| !('verdicts' in value) || !Array.isArray(value.verdicts)) throw new Error('invalid shadow queue ledger');
|
|
39
|
+
return value as L8ShadowQueueState;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeShadowQueue(path: string, state: L8ShadowQueueState): void {
|
|
43
|
+
const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(temp, JSON.stringify(state, null, 2) + '\n', { flag: 'wx' });
|
|
46
|
+
renameSync(temp, path);
|
|
47
|
+
} finally { rmSync(temp, { force: true }); }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function withShadowQueue<T>(cwd: string, deps: L8ShadowQueueDeps, fn: (state: L8ShadowQueueState, path: string) => Promise<T>): Promise<T> {
|
|
51
|
+
const release = await (deps.acquire ?? acquireL8MergeQueue)(cwd);
|
|
52
|
+
try {
|
|
53
|
+
const path = shadowQueuePath(cwd, deps.command ?? command);
|
|
54
|
+
return await fn(readShadowQueue(path), path);
|
|
55
|
+
} finally { release(); }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function shadowPrNumber(raw: string): number {
|
|
59
|
+
const text = raw.replace(/^#/, '');
|
|
60
|
+
const number = Number(text);
|
|
61
|
+
if (!/^[1-9]\d*$/.test(text) || !Number.isSafeInteger(number)) throw new Error('expected a positive PR number');
|
|
62
|
+
return number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function statusL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowQueueState> {
|
|
66
|
+
return withShadowQueue(cwd, deps, async (state) => state);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function enqueueL8ShadowQueue(cwd: string, number: number, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowQueueState> {
|
|
70
|
+
if (!Number.isSafeInteger(number) || number <= 0) throw new Error('expected a positive PR number');
|
|
71
|
+
return withShadowQueue(cwd, deps, async (state, path) => {
|
|
72
|
+
const execute = deps.command ?? command;
|
|
73
|
+
const view = JSON.parse(shadowCommand(execute, 'gh', ['pr', 'view', String(number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'], cwd)) as {
|
|
74
|
+
headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
|
|
75
|
+
};
|
|
76
|
+
if (!view.headRefOid || !/^[0-9a-f]{40}$/i.test(view.headRefOid) || view.baseRefName !== 'main'
|
|
77
|
+
|| view.state !== 'OPEN' || view.isDraft !== false || view.isCrossRepository !== false) throw new Error('PR is not an open, ready, same-repository main PR');
|
|
78
|
+
if (state.pending.some((p) => p.number === number)) throw new Error(`PR #${number} already queued`);
|
|
79
|
+
state.pending.push({ number, head: view.headRefOid });
|
|
80
|
+
writeShadowQueue(path, state);
|
|
81
|
+
return state;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Evaluate one pinned queue head against freshly fetched main. Never push or call a merge API. */
|
|
86
|
+
export async function runL8ShadowQueue(cwd: string, deps: L8ShadowQueueDeps = {}): Promise<L8ShadowVerdict | null> {
|
|
87
|
+
return withShadowQueue(cwd, deps, async (state, path) => {
|
|
88
|
+
const item = state.pending[0];
|
|
89
|
+
if (!item) return null;
|
|
90
|
+
const execute = deps.command ?? command;
|
|
91
|
+
const run = (bin: string, args: string[], where = cwd) => shadowCommand(execute, bin, args, where);
|
|
92
|
+
const verdict: L8ShadowVerdict = { number: item.number, head: item.head, verdict: 'fail', tests: [], at: new Date().toISOString() };
|
|
93
|
+
const view = JSON.parse(run('gh', ['pr', 'view', String(item.number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'])) as {
|
|
94
|
+
headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
|
|
95
|
+
};
|
|
96
|
+
if (view.headRefOid !== item.head || view.baseRefName !== 'main' || view.state !== 'OPEN'
|
|
97
|
+
|| view.isDraft !== false || view.isCrossRepository !== false) throw new Error('queued PR changed or is not ready');
|
|
98
|
+
run('git', ['fetch', 'origin', 'refs/heads/main']);
|
|
99
|
+
const base = run('git', ['rev-parse', 'FETCH_HEAD']);
|
|
100
|
+
if (!/^[0-9a-f]{40}$/i.test(base)) throw new Error('main commit unavailable');
|
|
101
|
+
verdict.base = base;
|
|
102
|
+
run('git', ['fetch', 'origin', `refs/pull/${item.number}/head`]);
|
|
103
|
+
if (run('git', ['rev-parse', 'FETCH_HEAD']) !== item.head) throw new Error('fetched PR head changed');
|
|
104
|
+
let temp: string | undefined;
|
|
105
|
+
let attached = false;
|
|
106
|
+
try {
|
|
107
|
+
temp = mkdtempSync(join(tmpdir(), 'elanous-l8-shadow-'));
|
|
108
|
+
run('git', ['worktree', 'add', '--detach', temp, base]);
|
|
109
|
+
attached = true;
|
|
110
|
+
const fork = run('git', ['merge-base', base, item.head], temp);
|
|
111
|
+
const changed = run('git', ['diff', '--name-only', '-z', fork, item.head], temp).split('\0').filter(Boolean);
|
|
112
|
+
if (!changed.length) throw new Error('PR changed-file paths unavailable');
|
|
113
|
+
const diff = run('git', ['diff', '--name-only', '-z', '--diff-filter=ACMRT', fork, item.head], temp);
|
|
114
|
+
const files = diff.split('\0').filter(Boolean);
|
|
115
|
+
if (changed.some((file) => file.startsWith('-') || file.startsWith('/') || file.split('/').includes('..') || file.includes('\\'))) throw new Error('unsafe changed-file path');
|
|
116
|
+
verdict.tests = files.filter((file) => /(?:^|\/)[^/]+\.(?:test|spec)\.(?:[cm]?[jt]sx?)$/.test(file));
|
|
117
|
+
const merge = execute('git', ['-c', 'user.name=elanous shadow', '-c', 'user.email=shadow@localhost', 'merge', '--no-ff', '--no-edit', item.head], temp);
|
|
118
|
+
if (merge.status !== 0) {
|
|
119
|
+
const unresolved = run('git', ['diff', '--name-only', '--diff-filter=U'], temp);
|
|
120
|
+
if (!unresolved) throw new Error(`git merge failed: ${merge.stderr.trim() || `exit ${merge.status}`}`);
|
|
121
|
+
verdict.verdict = 'conflict';
|
|
122
|
+
verdict.detail = unresolved;
|
|
123
|
+
} else {
|
|
124
|
+
if (verdict.tests.length) {
|
|
125
|
+
if (verdict.tests.some((file) => !existsSync(join(temp!, file)))) throw new Error('changed test path unavailable');
|
|
126
|
+
run('bun', ['install', '--frozen-lockfile'], temp);
|
|
127
|
+
const result = execute('bun', ['run', 'test:deterministic', ...verdict.tests], temp);
|
|
128
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
129
|
+
const counts = /\b(\d+) pass\b[\s\S]*?\b(\d+) fail\b[\s\S]*?Ran ([1-9]\d*) tests? across ([1-9]\d*) files?/.exec(output);
|
|
130
|
+
if (!counts || Number(counts[1]) + Number(counts[2]) !== Number(counts[3])
|
|
131
|
+
|| Number(counts[4]) !== verdict.tests.length || (result.status !== 0 && Number(counts[2]) === 0)) {
|
|
132
|
+
throw new Error(`changed tests unmeasured: ${output.slice(-400)}`);
|
|
133
|
+
}
|
|
134
|
+
if (Number(counts[2]) > 0 || Number(counts[1]) === 0) {
|
|
135
|
+
verdict.detail = `changed tests failed: ${output.slice(-400)}`;
|
|
136
|
+
} else {
|
|
137
|
+
verdict.verdict = 'pass';
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
verdict.verdict = 'pass';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
try {
|
|
145
|
+
if (attached && temp) run('git', ['worktree', 'remove', '--force', temp]);
|
|
146
|
+
} finally {
|
|
147
|
+
if (temp) rmSync(temp, { recursive: true, force: true });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (verdict.verdict === 'pass') {
|
|
151
|
+
run('git', ['fetch', 'origin', 'refs/heads/main']);
|
|
152
|
+
if (run('git', ['rev-parse', 'FETCH_HEAD']) !== verdict.base) throw new Error('main advanced during shadow verification');
|
|
153
|
+
const current = JSON.parse(run('gh', ['pr', 'view', String(item.number), '--json', 'headRefOid,baseRefName,state,isDraft,isCrossRepository'])) as {
|
|
154
|
+
headRefOid?: string; baseRefName?: string; state?: string; isDraft?: boolean; isCrossRepository?: boolean;
|
|
155
|
+
};
|
|
156
|
+
if (current.headRefOid !== item.head || current.baseRefName !== 'main' || current.state !== 'OPEN'
|
|
157
|
+
|| current.isDraft !== false || current.isCrossRepository !== false) throw new Error('PR changed during shadow verification');
|
|
158
|
+
}
|
|
159
|
+
// A rejected insert leaves the pinned item queued for a later evaluation.
|
|
160
|
+
await (deps.observe ?? ((entry) => {
|
|
161
|
+
const store = getDefaultLogStore();
|
|
162
|
+
if (!store) throw new Error('merge-queue observation store unavailable');
|
|
163
|
+
store.insertBatch([{ rec: { ts: new Date().toISOString(), category: 'merge-queue', event: 'shadow-verdict', data: entry }, surface: 'merge-queue' }]);
|
|
164
|
+
}))(verdict);
|
|
165
|
+
state.pending.shift();
|
|
166
|
+
state.verdicts.push(verdict);
|
|
167
|
+
writeShadowQueue(path, state);
|
|
168
|
+
return verdict;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
16
172
|
const command: Command = (bin, args, cwd) => {
|
|
17
173
|
const r = spawnSync(bin, [...args], { cwd, encoding: 'utf8', timeout: 600_000, maxBuffer: 32 * 1024 * 1024 });
|
|
18
174
|
return { status: r.status, stdout: r.stdout ?? '', stderr: (r.stderr ?? '') + (r.error?.message ?? '') };
|
package/src/steward/launch.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { redactSecrets } from '../task-cards/card-store.js';
|
|
|
7
7
|
import { preLaunchGate, type PreLaunchGateDecision } from '../execution-loop/launch-gate.js';
|
|
8
8
|
import type { BudgetDecision } from '../self-implement/budget-gate.js';
|
|
9
9
|
import type { ScheduledDecision, StewardSettings, TriageIssue } from './triage.js';
|
|
10
|
+
import { launchModeOf, rescueAllows } from './rescue.js';
|
|
10
11
|
|
|
11
12
|
/** `completed` = the run finished without a merge (a non-code artifact, or nothing to deliver) — never reported as a failure. */
|
|
12
13
|
export type LaunchStatus = 'shadow' | 'launched' | 'skipped-budget' | 'blocked-duplicate' | 'blocked-location' | 'failed' | 'running' | 'merged' | 'completed';
|
|
@@ -50,7 +51,7 @@ const RUN_ID = /\brun-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{1
|
|
|
50
51
|
const DEFAULT_POOL = 'pool-node-b@node-b:8';
|
|
51
52
|
// `harness budget --json` outcomes: proceed | next-provider | wait-reset | stop. next-provider still runs (the harness switches provider itself).
|
|
52
53
|
const BUDGET_PROCEED: readonly string[] = ['proceed', 'next-provider'];
|
|
53
|
-
const launchMode = (settings: StewardSettings): 'off' | 'shadow' | 'live' => settings.mode ?? settings.launch ?? 'shadow';
|
|
54
|
+
const launchMode = (settings: StewardSettings): 'off' | 'shadow' | 'live' => launchModeOf(settings.mode ?? settings.launch ?? 'shadow');
|
|
54
55
|
const cli = (args: string[]) => ['bun', 'bin/elanous.mjs', ...(process.env.NODE_ENV === 'test' ? ['--test'] : []), ...args];
|
|
55
56
|
const sourceOf = (issue: TriageIssue): string => /^출처: (telegram|pwa|tui|cli)$/m.exec(issue.body)?.[1] ?? `linear:${issue.identifier}`;
|
|
56
57
|
const promptOf = (issue: TriageIssue): string => `${issue.title}\n${issue.body.slice(0, 1500)}`;
|
|
@@ -125,6 +126,7 @@ export function launch(item: LaunchItem, deps: LaunchDeps): LaunchEntry {
|
|
|
125
126
|
const { ledger, settings, root } = deps;
|
|
126
127
|
if (item.row.rung !== 4 || item.row.hitlReason || item.row.disposition !== 'now' || (ledger.launches[item.issue.identifier] && !['skipped-budget', 'blocked-duplicate', 'blocked-location'].includes(ledger.launches[item.issue.identifier].status) && !(launchMode(settings) === 'live' && ledger.launches[item.issue.identifier].status === 'shadow'))) throw new Error('steward launch is not eligible');
|
|
127
128
|
if (launchMode(settings) === 'off') throw new Error('steward launch is off');
|
|
129
|
+
if (!rescueAllows(settings.mode, 'launch', { issue: item.issue.identifier })) throw new Error('steward rescue refused launch');
|
|
128
130
|
const key = item.issue.identifier;
|
|
129
131
|
const entry: LaunchEntry = { issue: key, title: redactSecrets(item.issue.title), source: item.source, command: redactSecrets(item.command), status: 'shadow' };
|
|
130
132
|
if (launchMode(settings) !== 'live') {
|
|
@@ -187,6 +189,7 @@ export function launch(item: LaunchItem, deps: LaunchDeps): LaunchEntry {
|
|
|
187
189
|
export function raiseHitl(issue: TriageIssue, row: ScheduledDecision, deps: LaunchDeps): HitlEntry {
|
|
188
190
|
const old = deps.ledger.hitl[issue.identifier];
|
|
189
191
|
if (old && (old.attempted || launchMode(deps.settings) !== 'live')) return old;
|
|
192
|
+
if (!rescueAllows(deps.settings.mode, 'decision-card', { issue: issue.identifier })) return old ?? { issue: issue.identifier, reason: redactSecrets(row.hitlReason ?? row.why), raised: false };
|
|
190
193
|
const entry: HitlEntry = old ?? { issue: issue.identifier, reason: redactSecrets(row.hitlReason ?? row.why), raised: false };
|
|
191
194
|
deps.ledger.hitl[issue.identifier] = entry;
|
|
192
195
|
if (launchMode(deps.settings) !== 'live') {
|
|
@@ -215,6 +218,7 @@ function alive(pid: number | undefined): boolean {
|
|
|
215
218
|
|
|
216
219
|
export function collectOutcomes(ledger: LaunchLedger, deps: LaunchDeps): LaunchEntry[] {
|
|
217
220
|
if (launchMode(deps.settings) !== 'live') return [];
|
|
221
|
+
if (!rescueAllows(deps.settings.mode, 'harvest')) return [];
|
|
218
222
|
const done: LaunchEntry[] = [];
|
|
219
223
|
for (const entry of Object.values(ledger.launches)) {
|
|
220
224
|
if (entry.reported || entry.status === 'shadow' || entry.status === 'skipped-budget' || entry.status === 'blocked-duplicate' || entry.status === 'blocked-location') continue;
|