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
package/src/cli/decisions-cli.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Option, type Command } from 'commander';
|
|
2
|
+
import { hqCliWriteAllowed, type HqDeps } from '../hq/hq.js';
|
|
3
|
+
import { fileLeaseStore } from '../hq/lease.js';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
6
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
7
|
+
import { getUserConfig } from '../user-config.js';
|
|
2
8
|
import { projectDecisionsToLinear } from '../decisions/decision-linear-projection.js';
|
|
3
9
|
import { DecisionLedger, importDecisionMarkdown, type DecisionCategory, type DecisionLedgerOptions, type DecisionOption, type DecisionTrack, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
4
10
|
|
|
@@ -46,13 +52,19 @@ export function formatDecisionDetail(e: DecisionEntry): string {
|
|
|
46
52
|
...(e.refs?.length ? [`참조: ${e.refs.join(' · ')}`] : [])].join('\n');
|
|
47
53
|
}
|
|
48
54
|
|
|
49
|
-
export function registerDecisionsCommands(program: Command, config: DecisionLedgerOptions = {}, out: Pick<Console, 'log'> = console): void {
|
|
50
|
-
const
|
|
55
|
+
export function registerDecisionsCommands(program: Command, config: DecisionLedgerOptions = {}, out: Pick<Console, 'log'> = console, hqDeps: HqDeps = {}): void {
|
|
56
|
+
const noXcheck = new Option('--no-xcheck <이유>', '교차 확인 생략 이유');
|
|
57
|
+
// This flag takes a reason, not Commander's negated boolean default.
|
|
58
|
+
noXcheck.negate = false;
|
|
59
|
+
const root = program.command('decisions').description('대표 결정 원장 · 로컬 전용').option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
|
|
60
|
+
const mayWrite = (command: string, override?: boolean) => hqCliWriteAllowed(`decisions ${command}`, Boolean(override || root.opts().hqOverride),
|
|
61
|
+
(getElanousConfigDirOverride() || config.stateDir) && !hqDeps.store && !getUserConfig().hq?.arbiter ? { ...hqDeps, store: fileLeaseStore(join(config.stateDir ?? effectiveInstanceRoot(), 'hq', 'lease.json')), seenPath: join(config.stateDir ?? effectiveInstanceRoot(), 'hq', 'seen-generation'), localPath: join(config.stateDir ?? effectiveInstanceRoot(), 'hq', 'local.json') } : hqDeps);
|
|
51
62
|
const emit = (value: unknown, json?: boolean, text?: string) => out.log(json ? JSON.stringify(value) : text ?? JSON.stringify(value));
|
|
52
63
|
const fail = (action: () => void) => { try { action(); } catch (e) { throw new Error(`decisions: ${e instanceof Error ? e.message : String(e)}`); } };
|
|
53
64
|
root.command('linear-sync').description('열린 결정을 COO Linear 프로젝트에 투영하고 닫힌 결정을 동기화한다')
|
|
54
|
-
.option('--dry-run').option('--json')
|
|
55
|
-
.action(async (o: { dryRun?: boolean; json?: boolean }) => {
|
|
65
|
+
.option('--dry-run').option('--json').option('--hq-override')
|
|
66
|
+
.action(async (o: { dryRun?: boolean; json?: boolean; hqOverride?: boolean }) => {
|
|
67
|
+
if (!o.dryRun && !mayWrite('linear-sync', o.hqOverride)) return;
|
|
56
68
|
const result = await projectDecisionsToLinear({ stateDir: config.stateDir, dryRun: o.dryRun });
|
|
57
69
|
emit(result, o.json, result.reason ?? `생성 ${result.created} · 닫음 ${result.closed} · 건너뜀 ${result.skipped} · 실패 ${result.failed}${result.plan?.length ? `\n${result.plan.map(p => `${p.action} ${p.decisionId}${p.issue ? ` ${p.issue}` : ''}`).join('\n')}` : ''}`);
|
|
58
70
|
});
|
|
@@ -61,21 +73,36 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
|
|
|
61
73
|
.requiredOption('--s <text>').requiredOption('--c <text>').option('--q <text>').option('--a <text>')
|
|
62
74
|
.requiredOption('--option <key=label:consequence>', '선택지 (두 번 이상)', repeat, [] as string[])
|
|
63
75
|
.option('--recommend <key>').option('--why <text>').option('--skip-recommend <reason>')
|
|
76
|
+
.option('--xcheck <SEAT:메모>', '교차 확인 (반복)', repeat, [] as string[])
|
|
77
|
+
.addOption(noXcheck)
|
|
78
|
+
.option('--alternative <key>').option('--dissent <text>')
|
|
64
79
|
.option('--track <track>').option('--agent <agent>').option('--session <id>')
|
|
65
80
|
.option('--resume-question <qid>').option('--run <runId>')
|
|
66
81
|
.option('--ref <url>', '참조 (반복)', repeat, [] as string[])
|
|
67
82
|
.option('--due <when>', '기한 — UTC ISO(2026-10-04T09:00:00Z) 또는 +Nh(지금부터 N시간) · 기한 2시간 전 텔레그램·디스코드로 다시 알린다')
|
|
68
|
-
.option('--json')
|
|
69
|
-
.action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean }) => fail(() => {
|
|
83
|
+
.option('--json').option('--hq-override')
|
|
84
|
+
.action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; xcheck: string[]; noXcheck?: string; alternative?: string; dissent?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean; hqOverride?: boolean }) => fail(() => {
|
|
70
85
|
if (o.run !== undefined && o.resumeQuestion === undefined) throw new Error('--run requires --resume-question');
|
|
71
86
|
if (o.skipRecommend !== undefined && (o.recommend !== undefined || o.why !== undefined)) throw new Error('choose recommendation or skip, not both');
|
|
72
87
|
if (o.skipRecommend === undefined && (!o.recommend || !o.why)) throw new Error('--recommend and --why required, or --skip-recommend <reason>');
|
|
73
88
|
const options: DecisionOption[] = o.option.map(parseOption);
|
|
89
|
+
if (o.xcheck.length && o.noXcheck !== undefined) throw new Error('choose --xcheck or --no-xcheck, not both');
|
|
90
|
+
const crossCheck = o.xcheck.map(raw => {
|
|
91
|
+
const match = /^([^:\s]+):(.+)$/.exec(raw);
|
|
92
|
+
if (!match || !match[2]!.trim()) throw new Error(`invalid --xcheck: ${raw} (expected SEAT:메모)`);
|
|
93
|
+
return { seat: match[1]!, at: new Date().toISOString(), note: match[2]!.trim() };
|
|
94
|
+
});
|
|
95
|
+
if (!crossCheck.length && o.noXcheck === undefined && getUserConfig().decisions?.requireCrossCheck) throw new Error('교차 확인 없음 — --xcheck SEAT:메모 또는 --no-xcheck 이유');
|
|
96
|
+
const who = agent(o.agent);
|
|
97
|
+
if (!mayWrite('raise', o.hqOverride)) return;
|
|
74
98
|
const entry = new DecisionLedger(config).raise({ title: o.title, category: o.category, scqa: { s: o.s, c: o.c, ...(o.q ? { q: o.q } : {}), ...(o.a ? { a: o.a } : {}) }, options,
|
|
99
|
+
...(crossCheck.length ? { crossCheck } : { crossCheckSkipped: o.noXcheck ?? 'missing' }),
|
|
100
|
+
...(o.alternative !== undefined ? { alternative: o.alternative } : {}), ...(o.dissent !== undefined ? { dissent: o.dissent } : {}),
|
|
75
101
|
recommendation: o.skipRecommend !== undefined ? { skipped: true, reason: o.skipRecommend } : { option: o.recommend!, why: o.why! },
|
|
76
|
-
raisedBy: { agent:
|
|
102
|
+
raisedBy: { agent: who, ...(o.track ? { track: o.track } : {}), ...(o.session ? { session: o.session } : {}) }, ...(o.ref.length ? { refs: o.ref } : {}),
|
|
77
103
|
...(o.resumeQuestion !== undefined ? { resume: { questionId: o.resumeQuestion, ...(o.run !== undefined ? { runId: o.run } : {}) } } : {}),
|
|
78
104
|
...(o.due ? { dueAt: dueAt(o.due) } : {}) });
|
|
105
|
+
if (entry.crossCheckSkipped === 'missing') console.error('교차 확인 없음 — --xcheck SEAT:메모 또는 --no-xcheck 이유');
|
|
79
106
|
emit(entry, o.json, `올림: ${formatDecisionRow(entry)}`);
|
|
80
107
|
}));
|
|
81
108
|
root.command('list').description('결정 목록 (기본 열린 것)')
|
|
@@ -88,11 +115,12 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
|
|
|
88
115
|
root.command('show <id>').description('SCQA · 선택지 · 권고 · 이력 · 판').option('--json')
|
|
89
116
|
.action((id: string, o: { json?: boolean }) => fail(() => { const entry = new DecisionLedger(config).show(id); emit(entry, o.json, formatDecisionDetail(entry)); }));
|
|
90
117
|
root.command('decide <id> <option>').description('사람 또는 AUTO 위임 결정 기록')
|
|
91
|
-
.option('--note <text>').option('--auto').option('--delegation <reason>').option('--track <track>').option('--agent <agent>').option('--json')
|
|
92
|
-
.action((id: string, choice: string, o: { note?: string; auto?: boolean; delegation?: string; track?: DecisionTrack; agent?: string; json?: boolean }) => fail(() => {
|
|
118
|
+
.option('--note <text>').option('--auto').option('--delegation <reason>').option('--track <track>').option('--agent <agent>').option('--json').option('--hq-override')
|
|
119
|
+
.action((id: string, choice: string, o: { note?: string; auto?: boolean; delegation?: string; track?: DecisionTrack; agent?: string; json?: boolean; hqOverride?: boolean }) => fail(() => {
|
|
93
120
|
if (o.auto && !o.delegation?.trim()) throw new Error('--auto requires --delegation');
|
|
94
121
|
if (!o.auto && (o.delegation || o.track || o.agent)) throw new Error('delegation/track/agent require --auto');
|
|
95
122
|
const by = o.auto ? { kind: 'auto' as const, agent: agent(o.agent), delegation: o.delegation!, ...(o.track ? { track: o.track } : {}) } : { kind: 'human' as const };
|
|
123
|
+
if (!mayWrite('decide', o.hqOverride)) return;
|
|
96
124
|
const { entry, delivery } = new DecisionLedger(config).decideWithDelivery(id, choice, by, o.note);
|
|
97
125
|
if (delivery && !delivery.ok) {
|
|
98
126
|
// The decision is recorded; only the answer to the waiting run failed — say so and how to retry, never «success».
|
|
@@ -103,22 +131,26 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
|
|
|
103
131
|
}
|
|
104
132
|
emit(delivery ? { ...entry, delivery } : entry, o.json, `결정: ${formatDecisionDetail(entry)}`);
|
|
105
133
|
}));
|
|
106
|
-
root.command('retry-answer <id>').description('기록된 결정의 대기 질문 답 전달만 재시도한다').option('--json')
|
|
107
|
-
.action((id: string, o: { json?: boolean }) => fail(() => {
|
|
134
|
+
root.command('retry-answer <id>').description('기록된 결정의 대기 질문 답 전달만 재시도한다').option('--json').option('--hq-override')
|
|
135
|
+
.action((id: string, o: { json?: boolean; hqOverride?: boolean }) => fail(() => {
|
|
136
|
+
if (!mayWrite('retry-answer', o.hqOverride)) return;
|
|
108
137
|
const entry = new DecisionLedger(config).retryAnswer(id);
|
|
109
138
|
emit(entry, o.json, `답 전달: ${entry.id} → ${entry.resume?.questionId}`);
|
|
110
139
|
}));
|
|
111
140
|
root.command('add-options <id>').description('선택지가 미기재된 과거 항목에 확인된 선택지를 추가')
|
|
112
141
|
.requiredOption('--option <key=label:consequence>', '선택지 (두 번 이상)', repeat, [] as string[])
|
|
113
|
-
.option('--agent <agent>').option('--json')
|
|
114
|
-
.action((id: string, o: { option: string[]; agent?: string; json?: boolean }) => fail(() => {
|
|
115
|
-
const
|
|
142
|
+
.option('--agent <agent>').option('--json').option('--hq-override')
|
|
143
|
+
.action((id: string, o: { option: string[]; agent?: string; json?: boolean; hqOverride?: boolean }) => fail(() => {
|
|
144
|
+
const who = agent(o.agent);
|
|
145
|
+
if (!mayWrite('add-options', o.hqOverride)) return;
|
|
146
|
+
const entry = new DecisionLedger(config).addOptions(id, o.option.map(parseOption), who);
|
|
116
147
|
emit(entry, o.json, formatDecisionDetail(entry));
|
|
117
148
|
}));
|
|
118
|
-
root.command('withdraw <id>').description('결정 철회도 원장에 남긴다').requiredOption('--reason <reason>').option('--json')
|
|
119
|
-
.action((id: string, o: { reason: string; json?: boolean }) => fail(() => { const entry = new DecisionLedger(config).withdraw(id, o.reason); emit(entry, o.json, `철회: ${formatDecisionDetail(entry)}`); }));
|
|
120
|
-
root.command('import-markdown <file>').description('기존 대표 결정 문서 씨앗을 가져온다 (재실행 안전)').option('--json')
|
|
121
|
-
.action((file: string, o: { json?: boolean }) => fail(() => {
|
|
149
|
+
root.command('withdraw <id>').description('결정 철회도 원장에 남긴다').requiredOption('--reason <reason>').option('--json').option('--hq-override')
|
|
150
|
+
.action((id: string, o: { reason: string; json?: boolean; hqOverride?: boolean }) => fail(() => { if (!mayWrite('withdraw', o.hqOverride)) return; const entry = new DecisionLedger(config).withdraw(id, o.reason); emit(entry, o.json, `철회: ${formatDecisionDetail(entry)}`); }));
|
|
151
|
+
root.command('import-markdown <file>').description('기존 대표 결정 문서 씨앗을 가져온다 (재실행 안전)').option('--json').option('--hq-override')
|
|
152
|
+
.action((file: string, o: { json?: boolean; hqOverride?: boolean }) => fail(() => {
|
|
153
|
+
if (!mayWrite('import-markdown', o.hqOverride)) return;
|
|
122
154
|
const result = importDecisionMarkdown(new DecisionLedger(config), file);
|
|
123
155
|
emit(result, o.json, `가져옴 ${result.imported.length} · 기존 ${result.existing.length} · 불완전 ${result.incomplete.length} · 못 읽음 ${result.unread.length}${result.incomplete.length ? `\n${result.incomplete.join('\n')}` : ''}${result.unread.length ? `\n${result.unread.join('\n')}` : ''}`);
|
|
124
156
|
if (result.unread.length) process.exitCode = 1;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { accessSync, constants, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import type { Command } from 'commander';
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
5
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
6
|
+
import type { OutputEntry } from '../outputs/ledger.js';
|
|
7
|
+
import { writeStdoutJson } from './stdout-json.js';
|
|
8
|
+
|
|
9
|
+
type ListOptions = { since?: string; source?: string; limit?: string; json?: boolean };
|
|
10
|
+
|
|
11
|
+
function sinceIso(value: string): string {
|
|
12
|
+
if (value === '24h' || value === '7d') {
|
|
13
|
+
return new Date(Date.now() - (value === '24h' ? 24 : 7 * 24) * 60 * 60 * 1000).toISOString();
|
|
14
|
+
}
|
|
15
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
|
|
16
|
+
|| !Number.isFinite(Date.parse(value))
|
|
17
|
+
|| new Date(`${value.slice(0, 10)}T00:00:00Z`).toISOString().slice(0, 10) !== value.slice(0, 10)) {
|
|
18
|
+
throw new Error(`invalid --since: ${value} (ISO|24h|7d)`);
|
|
19
|
+
}
|
|
20
|
+
return new Date(value).toISOString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// listOutputs intentionally tolerates unreadable months. Detect inaccessible ledger files here so the CLI
|
|
24
|
+
// does not report an incomplete read as a genuine zero (including when run as root under chmod 000).
|
|
25
|
+
function assertReadableLedger(root: string): void {
|
|
26
|
+
const dir = join(root, 'outputs');
|
|
27
|
+
let names: string[];
|
|
28
|
+
try {
|
|
29
|
+
const info = statSync(dir);
|
|
30
|
+
if (!info.isDirectory() || (info.mode & 0o444) === 0 || (info.mode & 0o111) === 0) {
|
|
31
|
+
throw new Error(`outputs ledger directory unreadable: ${dir}`);
|
|
32
|
+
}
|
|
33
|
+
accessSync(dir, constants.R_OK | constants.X_OK);
|
|
34
|
+
names = readdirSync(dir);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
for (const name of names.filter(name => /^outputs-\d{4}-(0[1-9]|1[0-2])\.jsonl$/.test(name) || name === 'outputs.jsonl')) {
|
|
40
|
+
const file = join(dir, name);
|
|
41
|
+
const info = statSync(file);
|
|
42
|
+
if (!info.isFile() || (info.mode & 0o444) === 0) throw new Error(`outputs ledger file unreadable: ${file}`);
|
|
43
|
+
accessSync(file, constants.R_OK);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function registerOutputsCommands(program: Command): void {
|
|
48
|
+
program.command('outputs').description('산출물 원장 조회 (읽기 전용)')
|
|
49
|
+
.command('list').description('산출물 최신순 목록과 종류별 수')
|
|
50
|
+
.option('--since <ISO|24h|7d>', '이 시각 이후')
|
|
51
|
+
.option('--source <source>', 'exec|field-reel|field-feed')
|
|
52
|
+
.option('--limit <N>', '최대 건수 (기본 50)', '50')
|
|
53
|
+
.option('--json', 'JSON 출력')
|
|
54
|
+
.action(async (options: ListOptions) => {
|
|
55
|
+
try {
|
|
56
|
+
if (options.source !== undefined && !['exec', 'field-reel', 'field-feed'].includes(options.source)) {
|
|
57
|
+
throw new Error(`invalid --source: ${options.source} (exec|field-reel|field-feed)`);
|
|
58
|
+
}
|
|
59
|
+
if (!options.limit || !/^[1-9]\d*$/.test(options.limit) || !Number.isSafeInteger(Number(options.limit))) {
|
|
60
|
+
throw new Error(`invalid --limit: ${options.limit}`);
|
|
61
|
+
}
|
|
62
|
+
const scope = {
|
|
63
|
+
since: options.since === undefined ? undefined : sinceIso(options.since),
|
|
64
|
+
source: options.source as OutputEntry['source'] | undefined,
|
|
65
|
+
limit: Number(options.limit),
|
|
66
|
+
};
|
|
67
|
+
const displayedScope = { since: scope.since ?? null, source: scope.source ?? null, limit: scope.limit };
|
|
68
|
+
const root = effectiveInstanceRoot();
|
|
69
|
+
assertReadableLedger(root);
|
|
70
|
+
const { listOutputs } = await import('../outputs/ledger.js');
|
|
71
|
+
const items = listOutputs(scope, root);
|
|
72
|
+
const byKind: Record<string, number> = {};
|
|
73
|
+
for (const item of items) byKind[item.kind] = (byKind[item.kind] ?? 0) + 1;
|
|
74
|
+
debug.log('outputs.cli', 'list', { count: items.length, source: scope.source, since: scope.since });
|
|
75
|
+
if (options.json) {
|
|
76
|
+
await writeStdoutJson(`${JSON.stringify({ items, count: items.length, byKind, scope: displayedScope })}\n`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
for (const item of items) {
|
|
80
|
+
const at = Number.isFinite(Date.parse(item.at))
|
|
81
|
+
? new Intl.DateTimeFormat('sv-SE', { timeZone: 'Asia/Seoul', dateStyle: 'short', timeStyle: 'medium', hourCycle: 'h23' }).format(new Date(item.at))
|
|
82
|
+
: item.at;
|
|
83
|
+
console.log(`${at} KST · ${item.kind} · ${item.title} · ${item.source}/${item.sourceId} · ${item.seat ?? '—'} · ${item.path ?? item.url ?? '—'}`);
|
|
84
|
+
}
|
|
85
|
+
if (!items.length) console.log(`scope: ${JSON.stringify(displayedScope)}`);
|
|
86
|
+
console.log(`${items.length}개 · 종류별 수 ${JSON.stringify(byKind)}`);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(`outputs list: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
package/src/cli/persona-cli.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { parsePersonaYaml } from '../persona/loader.js';
|
|
|
9
9
|
import { resolveRepositoryPersonaDir, resolveStatePersonaDir } from '../persona/global-registry.js';
|
|
10
10
|
import { UnknownPersonaSchemaKeyError, choosePersonaId, clonePreset, describePreset, editPersona, findPreset, loadPresetIndex, loadPresets, presetToProfileYaml } from '../persona/presets.js';
|
|
11
11
|
import { addTeamSet, listTeamSets, removeTeamSet } from '../persona/team-set.js';
|
|
12
|
+
import { validateTeamFile } from '../persona/team.js';
|
|
13
|
+
import { runPersonaLoopOnce } from '../seat-loop/seat-loop.js';
|
|
12
14
|
|
|
13
15
|
interface StoredPersona { personaId: string; displayName: string; description?: string; preset?: string; title?: string; path: string }
|
|
14
16
|
|
|
@@ -68,6 +70,18 @@ function editOptions(fromFile: string | undefined, assignments: readonly string[
|
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export function registerPersonaCommands(program: Command): void {
|
|
73
|
+
program.command('team').description('Validate a YAML team definition')
|
|
74
|
+
.command('validate <file>').description('Check team schema, persona references and handoff order')
|
|
75
|
+
.action((file: string) => {
|
|
76
|
+
try {
|
|
77
|
+
const definition = validateTeamFile(file);
|
|
78
|
+
console.log(`Valid team ${definition.name}`);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(error instanceof Error ? error.message.replace(/\s+/g, ' ').trim() : String(error));
|
|
81
|
+
process.exitCode = 2;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
71
85
|
const team = program.command('team-set').description('Install, remove and list persona team sets');
|
|
72
86
|
team.command('add <file>').description('Install a YAML team set').action((file: string) => {
|
|
73
87
|
try { const result = addTeamSet(file); console.log(`Installed team-set ${result['team-set']}`); }
|
|
@@ -178,6 +192,21 @@ export function registerPersonaCommands(program: Command): void {
|
|
|
178
192
|
}
|
|
179
193
|
});
|
|
180
194
|
|
|
195
|
+
persona.command('loop <name>')
|
|
196
|
+
.description('Shadow-pick one todo for a saved persona without executing it')
|
|
197
|
+
.requiredOption('--once', 'Pick exactly one todo')
|
|
198
|
+
.option('--json', 'JSON output')
|
|
199
|
+
.action(async (name: string, opts: { json?: boolean }) => {
|
|
200
|
+
try {
|
|
201
|
+
const entry = await runPersonaLoopOnce(name);
|
|
202
|
+
if (opts.json) console.log(JSON.stringify(entry));
|
|
203
|
+
else console.log(entry.todo ? `그림자 선택 — ${entry.todo.title} (${entry.todo.id})` : '할 일 없음');
|
|
204
|
+
} catch (error) {
|
|
205
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
206
|
+
process.exitCode = 1;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
181
210
|
persona.command('show <name>')
|
|
182
211
|
.description('Show one of your personas (id or name), or a preset')
|
|
183
212
|
.option('--json', 'JSON output')
|
package/src/cli/pod-cli.ts
CHANGED
|
@@ -6,9 +6,10 @@ import { debug } from '../debug/log.js';
|
|
|
6
6
|
import { readPodMemoryAdvice } from './pod-memory-advice.js';
|
|
7
7
|
import { getUserConfig } from '../user-config.js';
|
|
8
8
|
import { listCodexAccountsInStore } from '../oauth/codex-account-store.js';
|
|
9
|
-
import { parsePodPool, resolvePodPoolSpec, type PoolKubectl } from '../task-orchestrator/surfaces/pod-pool.js';
|
|
9
|
+
import { parsePodPool, resolvePodPoolSpec, podPoolHostLease, type PoolKubectl } from '../task-orchestrator/surfaces/pod-pool.js';
|
|
10
10
|
import { leaseKubectl, measurePoolLease, probePoolDns, recommendConcurrency, type PoolLeaseMeasure, type PoolDnsProbe } from '../task-orchestrator/surfaces/pod-lease.js';
|
|
11
11
|
import { POD_COMMAND_DEADLINE_SECONDS, runPodCommand, type PodCommandResult, type RunPodCommandOptions } from '../task-orchestrator/surfaces/pod-command-job.js';
|
|
12
|
+
import { hostLeaseCounts, leaseHasPendingPod } from '../pod-lease/host-lease.js';
|
|
12
13
|
|
|
13
14
|
export interface PodCliIo {
|
|
14
15
|
log: (line: string) => void;
|
|
@@ -78,8 +79,13 @@ export function registerPodCommands(program: Command, deps: PodCliDeps = {}): vo
|
|
|
78
79
|
const perAccount = (deps.perAccount ?? (() => getUserConfig().pod?.lease?.perAccount ?? 4))();
|
|
79
80
|
const decision = recommendConcurrency(measure, { capacity: members.reduce((sum, m) => sum + m.capacity, 0), accounts: accounts ?? -1, perAccount });
|
|
80
81
|
if (!context) { decision.recommended = null; decision.limitedBy = null; decision.reason = '측정 불가: cluster (current context)'; }
|
|
81
|
-
const
|
|
82
|
-
|
|
82
|
+
const records = members.length ? podPoolHostLease(members).live() : [];
|
|
83
|
+
const host = hostLeaseCounts(records);
|
|
84
|
+
// Pending Pods and Job-only reservations are disjoint; a lease for an already Pending Job is the same Job.
|
|
85
|
+
const waitingJobs = decision.pending === null ? null : decision.pending +
|
|
86
|
+
records.filter((r) => r.stage === 'job' && !leaseHasPendingPod(r, decision.pendingJobs ?? [])).length;
|
|
87
|
+
const result = { pool: spec, ...measure, accounts, perAccount, ...decision, reserved: host.reserved, waitingJobs };
|
|
88
|
+
debug.log('pod.lease', 'status', { pool: spec ? members.map((m) => ({ capacity: m.capacity })) : null, running: decision.running, pending: decision.pending, reserved: host.reserved, waitingJobs, unleasedRunning: decision.unleasedRunning, recommended: decision.recommended, limitedBy: decision.limitedBy });
|
|
83
89
|
if (opts.json) io.log(JSON.stringify(result));
|
|
84
90
|
else {
|
|
85
91
|
io.log(`풀: ${(spec ?? context) || '?'}`);
|
|
@@ -89,6 +95,7 @@ export function registerPodCommands(program: Command, deps: PodCliDeps = {}): vo
|
|
|
89
95
|
io.log(`${m.context} | ${m.capacity} | ${m.running ?? '?'} | ${m.pending ?? '?'} | ${mem(m.memoryLimitBytes)}/${mem(m.allocatableMemoryBytes)} | ${m.allocatableCpuMillicores === null ? '?' : m.allocatableCpuMillicores / 1000}${m.reason ? ` (${m.reason})` : ''}`);
|
|
90
96
|
}
|
|
91
97
|
io.log(`권장 지금 ${decision.recommended ?? '?'} 개 더 (limitedBy=${decision.limitedBy ?? 'unknown'})${decision.reason ? ` · ${decision.reason}` : ''}`);
|
|
98
|
+
io.log(`예약(저작 중) ${host.reserved} · 대기 Job ${waitingJobs ?? '?'} · 실행 ${decision.running ?? '?'}`);
|
|
92
99
|
io.log(`임대 없는 실행 ${decision.unleasedRunning ?? '?'}`);
|
|
93
100
|
io.log(`capacity: ${decision.capacitySlots ?? '?'} 칸 (상한 ${members.reduce((sum, m) => sum + m.capacity, 0)} − Running ${decision.running ?? '?'} − Pending ${decision.pending ?? '?'}; 권장 수에서 건강한 멤버의 임대 없는 실행 차감)`);
|
|
94
101
|
io.log(`memory: ${decision.memorySlots ?? '?'} 칸 (할당 가능 − Running 하니스 상한 합; 골당 16Gi · 모든 네임스페이스의 노드별 기존 예약을 반영해 배치 가능 ${decision.placeableSlots ?? '?'} 칸)`);
|
package/src/cli/pr-cli.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
} from './pr-granularity.js';
|
|
54
54
|
import { branchLineageSlug, findSiblingPrs } from './pr-lineage.js';
|
|
55
55
|
import { parseReleaseNoteSection } from '../release-loop/release-note.js';
|
|
56
|
+
import { enqueueL8ShadowQueue, runL8ShadowQueue, shadowPrNumber, statusL8ShadowQueue, type L8ShadowQueueDeps } from '../self-implement/l8-merge-queue.js';
|
|
56
57
|
|
|
57
58
|
export { branchLineageSlug, findSiblingPrs };
|
|
58
59
|
|
|
@@ -1635,8 +1636,21 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1635
1636
|
return 0;
|
|
1636
1637
|
}
|
|
1637
1638
|
|
|
1638
|
-
export function registerPrCommands(program: Command, deps: PrLandDeps = {}): void {
|
|
1639
|
+
export function registerPrCommands(program: Command, deps: PrLandDeps = {}, queueDeps: L8ShadowQueueDeps = {}): void {
|
|
1639
1640
|
const pr = program.command('pr').description('PR landing commands');
|
|
1641
|
+
const queue = pr.command('queue').description('Shadow merge queue (never lands PRs)');
|
|
1642
|
+
const queueAction = async (action: () => Promise<unknown>) => {
|
|
1643
|
+
try {
|
|
1644
|
+
(deps.out ?? liveOut).log(JSON.stringify(await action()));
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
(deps.out ?? liveOut).error(`✗ merge-queue: ${String(error)}`);
|
|
1647
|
+
(deps.setExitCode ?? ((code: number) => { process.exitCode = code; }))(1);
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
queue.command('status').description('Show pending PRs and shadow verdicts').action(() => queueAction(() => statusL8ShadowQueue(process.cwd(), queueDeps)));
|
|
1651
|
+
queue.command('enqueue <pr>').description('Pin a ready PR at the tail of the shadow queue').action((prNumber: string) => queueAction(() => enqueueL8ShadowQueue(process.cwd(), shadowPrNumber(prNumber), queueDeps)));
|
|
1652
|
+
queue.command('run').description('Evaluate the first PR without merging').requiredOption('--shadow', 'Only evaluate; never land').action(() => queueAction(() => runL8ShadowQueue(process.cwd(), queueDeps)));
|
|
1653
|
+
|
|
1640
1654
|
pr.command('land')
|
|
1641
1655
|
.description('현재 브랜치 PR을 생성 또는 갱신해 ready 상태로 만들고 squash merge')
|
|
1642
1656
|
.option('--base <branch>', 'PR base branch')
|
package/src/cli/release-cli.ts
CHANGED
|
@@ -23,13 +23,15 @@ import { debug } from '../debug/log.js';
|
|
|
23
23
|
import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
|
|
24
24
|
import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
25
25
|
import { envLiteral } from '../platform/env-literal.js';
|
|
26
|
-
import { userConfigPath } from '../user-config.js';
|
|
26
|
+
import { getUserConfig, userConfigPath } from '../user-config.js';
|
|
27
27
|
import { addItem, claimItem, decodeClaimHistoryEntry, devVersion, listChecklist, ownerMatches, parseOwner, parityGap, removeItem, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
|
|
28
28
|
import * as features from '../release-loop/feature-store.js';
|
|
29
|
-
import { landedButYellow, type MergedChecklistPr } from '../release-loop/landed-but-yellow.js';
|
|
29
|
+
import { evidencePlan, landedButYellow, type MergedChecklistPr } from '../release-loop/landed-but-yellow.js';
|
|
30
30
|
import { formatSchedule, getSchedule, listSchedules, setSchedule } from '../release-loop/release-schedule.js';
|
|
31
31
|
import { writeStdoutJson } from './stdout-json.js';
|
|
32
32
|
import { CliUserError } from './cli-user-error.js';
|
|
33
|
+
import { hqCliWriteAllowed, type HqDeps } from '../hq/hq.js';
|
|
34
|
+
import { fileLeaseStore } from '../hq/lease.js';
|
|
33
35
|
import { runUnattendedRelease, type UnattendedReleaseDeps } from '../../scripts/release-loop/unattended-release.js';
|
|
34
36
|
import { cutReleaseBranch } from '../../scripts/release-loop/cut-branch.js';
|
|
35
37
|
import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
|
|
@@ -504,12 +506,44 @@ export interface LandedButYellowDeps {
|
|
|
504
506
|
run?: Runner;
|
|
505
507
|
checklist?: typeof listChecklist;
|
|
506
508
|
now?: () => Date;
|
|
509
|
+
evidenceAdd?: typeof features.evidenceAdd;
|
|
510
|
+
schedules?: typeof features.readSchedules;
|
|
511
|
+
released?: typeof features.releasedVersion;
|
|
507
512
|
}
|
|
508
513
|
|
|
509
|
-
|
|
514
|
+
function printLandedRows(rows: ReturnType<typeof landedButYellow>, items: ReturnType<typeof listChecklist>['items']): void {
|
|
515
|
+
const updatedAtById = new Map(items.map((item) => [item.id, item.updatedAt]));
|
|
516
|
+
const date = (iso: string) => {
|
|
517
|
+
const parts = new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Seoul', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(new Date(iso));
|
|
518
|
+
const part = (name: string) => parts.find((entry) => entry.type === name)?.value;
|
|
519
|
+
return `${part('month')}-${part('day')} ${part('hour')}:${part('minute')}`;
|
|
520
|
+
};
|
|
521
|
+
for (const row of rows) {
|
|
522
|
+
const icon = row.status === 'yellow' ? '🟡' : '🔴';
|
|
523
|
+
console.log(`${row.id} ${row.owner ?? '-'} ${icon} ← ${row.prs.map((pr, i) => `#${pr.number} «${pr.title}» (${date(pr.mergedAt)} · 근거에 ${row.alreadyInEvidence[i] ? '있음' : '없음'}${pr.mergedAt > (updatedAtById.get(row.id) ?? '') ? ' · 칸보다 새것' : ''})`).join(' · ')}`);
|
|
524
|
+
}
|
|
525
|
+
console.log(`${rows.length}칸 · 그중 근거에 없는 PR 이 있는 칸 ${rows.filter((row) => row.alreadyInEvidence.includes(false)).length}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function compareReleaseVersions(a: string, b: string): number {
|
|
529
|
+
const parse = (version: string) => {
|
|
530
|
+
const [core, pre] = version.split('-');
|
|
531
|
+
const [major, minor, patch] = core!.split('.').map(Number);
|
|
532
|
+
const [stage, sequence] = pre?.split('.') ?? [];
|
|
533
|
+
return [major!, minor!, patch!, stage === undefined ? 3 : ({ alpha: 0, beta: 1, rc: 2 } as Record<string, number>)[stage]!, Number(sequence ?? 0)];
|
|
534
|
+
};
|
|
535
|
+
const left = parse(a), right = parse(b);
|
|
536
|
+
for (let i = 0; i < left.length; i++) if (left[i] !== right[i]) return left[i]! - right[i]!;
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function registerReleaseCommands(program: Command, releaseRunDeps: UnattendedReleaseDeps = {}, landedDeps: LandedButYellowDeps = {}, hqDeps: HqDeps = {}): void {
|
|
510
541
|
const release = program.command('release').description('공개 배포 한 판 — prepare(로컬) → publish(--yes) → verify (docs/manual/MANUAL-versioning-and-release-2026-09-25.md)');
|
|
511
542
|
const schedule = release.command('schedule').description('판별 컷·착지 마감 조회·갱신');
|
|
512
543
|
const scheduleLedgerRoot = () => releaseRunDeps.ledgerRoot ?? releaseLedgerRoot();
|
|
544
|
+
const ledgerFenceDeps = (): HqDeps => getElanousConfigDirOverride() && !hqDeps.store && !getUserConfig().hq?.arbiter
|
|
545
|
+
? { ...hqDeps, store: fileLeaseStore(join(effectiveInstanceRoot(), 'hq', 'lease.json')), seenPath: join(effectiveInstanceRoot(), 'hq', 'seen-generation'), localPath: join(effectiveInstanceRoot(), 'hq', 'local.json') }
|
|
546
|
+
: hqDeps;
|
|
513
547
|
schedule.command('list').option('--json', '결과 JSON').action((o: { json?: boolean }) => {
|
|
514
548
|
const rows = listSchedules(scheduleLedgerRoot());
|
|
515
549
|
if (o.json) console.log(JSON.stringify(rows));
|
|
@@ -522,21 +556,25 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
522
556
|
if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
|
|
523
557
|
});
|
|
524
558
|
schedule.command('set').requiredOption('--version <v>', '갱신할 판')
|
|
525
|
-
.option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감').option('--json', '결과 JSON')
|
|
526
|
-
.action((o: { version: string; cutAt?: string; landBy?: string; json?: boolean }) => {
|
|
559
|
+
.option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감').option('--json', '결과 JSON').option('--hq-override')
|
|
560
|
+
.action((o: { version: string; cutAt?: string; landBy?: string; json?: boolean; hqOverride?: boolean }) => {
|
|
527
561
|
if (o.cutAt === undefined && o.landBy === undefined) throw new CliUserError('갱신할 시각을 지정하라', '--cut-at <iso> 또는 --land-by <iso>');
|
|
562
|
+
if (!hqCliWriteAllowed('release schedule set', Boolean(o.hqOverride), ledgerFenceDeps())) return;
|
|
528
563
|
const row = setSchedule(o.version, { cutAt: o.cutAt, landBy: o.landBy }, process.env.ELANOUS_TRACK || 'cli', scheduleLedgerRoot());
|
|
529
564
|
if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
|
|
530
565
|
});
|
|
531
566
|
const checklist = release.command('checklist').description('판별 확인표 조회·갱신')
|
|
532
567
|
.option('--version <v>', '판 또는 별칭(기본: package.json 의 개발판에서 -dev.N 제거)')
|
|
533
|
-
.option('--json', '결과 JSON')
|
|
568
|
+
.option('--json', '결과 JSON')
|
|
569
|
+
.option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
|
|
570
|
+
const mayWrite = (cmd: Command) => hqCliWriteAllowed(`release checklist ${cmd.name()}`, Boolean(cmd.optsWithGlobals().hqOverride), ledgerFenceDeps());
|
|
534
571
|
const context = (cmd: Command) => {
|
|
535
572
|
const opts = { ...(cmd.parent?.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.opts() as { version?: string; json?: boolean }) };
|
|
536
573
|
const codenames = checklistCodenames();
|
|
537
574
|
return { version: checklistVersion(opts.version, codenames), codenames, json: opts.json };
|
|
538
575
|
};
|
|
539
|
-
const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON');
|
|
576
|
+
const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON').option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
|
|
577
|
+
const actor = () => process.env.ELANOUS_TRACK || 'cli';
|
|
540
578
|
withContext(checklist.command('list').description('모든 칸')).option('--owner <owner>', '자리 또는 하위 자리로 거르기').action(async (opts: { owner?: string }, cmd: Command) => {
|
|
541
579
|
const { version, codenames, json } = context(cmd);
|
|
542
580
|
await checklistOutput(version, codenames, 'list', json, opts.owner);
|
|
@@ -545,32 +583,66 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
545
583
|
const { version, codenames, json } = context(cmd);
|
|
546
584
|
await checklistOutput(version, codenames, 'status', json, opts.owner);
|
|
547
585
|
});
|
|
548
|
-
withContext(checklist.command('landed-but-yellow').description('병합 PR 이 있지만 노랑·빨강인 칸(읽기 전용)'))
|
|
586
|
+
withContext(checklist.command('landed-but-yellow').description('병합 PR 이 있지만 노랑·빨강인 칸(기본 읽기 전용)'))
|
|
549
587
|
.option('--owner <owner>', '자리 또는 하위 자리로 거르기')
|
|
550
|
-
.
|
|
551
|
-
|
|
588
|
+
.option('--open', '미발행 일정 판 중 칸이 있는 판 전부')
|
|
589
|
+
.option('--apply-evidence', '확실한 PR 짝의 근거를 붙인다(상태는 그대로)')
|
|
590
|
+
.option('--dry-run', '근거 추가 없이 계획만 출력')
|
|
591
|
+
.action(async (opts: { owner?: string; open?: boolean; applyEvidence?: boolean; dryRun?: boolean }, cmd: Command) => {
|
|
552
592
|
try {
|
|
593
|
+
const inherited = cmd.parent?.opts() as { version?: string; json?: boolean } | undefined;
|
|
594
|
+
const local = cmd.opts() as { version?: string; json?: boolean };
|
|
595
|
+
if (opts.open && (local.version !== undefined || inherited?.version !== undefined)) throw new CliUserError('--open 과 --version 을 함께 쓸 수 없다');
|
|
596
|
+
const json = local.json ?? inherited?.json;
|
|
553
597
|
if (opts.owner !== undefined) parseOwner(opts.owner);
|
|
598
|
+
const released = opts.open ? (landedDeps.released ?? features.releasedVersion)() : '';
|
|
599
|
+
const openSnapshots = opts.open
|
|
600
|
+
? (landedDeps.schedules ?? features.readSchedules)()
|
|
601
|
+
.filter(({ version }) => isReleaseVersion(version) && (!released || compareReleaseVersions(version, released) > 0))
|
|
602
|
+
.sort((a, b) => compareReleaseVersions(a.version, b.version))
|
|
603
|
+
.map(({ version }) => (landedDeps.checklist ?? listChecklist)(version)).filter((snapshot) => snapshot.items.length > 0)
|
|
604
|
+
: [];
|
|
605
|
+
if (opts.open && !openSnapshots.length) {
|
|
606
|
+
if (json) await writeStdoutJson('[]\n'); else console.log('열린 판 없음');
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (opts.applyEvidence && !opts.dryRun && !mayWrite(cmd)) return;
|
|
554
610
|
const since = new Date((landedDeps.now ?? (() => new Date()))().getTime() - 7 * 86_400_000).toISOString().slice(0, 10);
|
|
555
611
|
const raw = must((landedDeps.run ?? defaultRunner)('gh', ['pr', 'list', '--state', 'merged', '--search', `merged:>=${since}`, '--limit', '400', '--json', 'number,title,body,mergedAt'], process.cwd()), '병합 PR 조회(gh)');
|
|
556
612
|
const prs = JSON.parse(raw) as MergedChecklistPr[];
|
|
557
613
|
if (!Array.isArray(prs)) throw new Error('병합 PR 조회(gh): 배열이 아니다');
|
|
558
|
-
const
|
|
559
|
-
const rows
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
};
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
614
|
+
const snapshots = opts.open ? openSnapshots : [(landedDeps.checklist ?? listChecklist)(context(cmd).version)];
|
|
615
|
+
const results: Array<{ version: string; rows?: ReturnType<typeof landedButYellow>; plan?: ReturnType<typeof evidencePlan> }> = [];
|
|
616
|
+
let totalAdded = 0, totalSkipped = 0;
|
|
617
|
+
for (const snapshot of snapshots) {
|
|
618
|
+
const { version, items } = snapshot;
|
|
619
|
+
const rows = landedButYellow(items, prs, { owner: opts.owner });
|
|
620
|
+
const skippedMention = new Set(rows.flatMap((row) => row.prs.flatMap((pr) =>
|
|
621
|
+
pr.basis === 'mention' ? [JSON.stringify([row.id, pr.number])] : []))).size;
|
|
622
|
+
if (opts.open) totalSkipped += skippedMention;
|
|
623
|
+
if (opts.open && !json) console.log(`— ${version} —`);
|
|
624
|
+
if (opts.applyEvidence || opts.dryRun) {
|
|
625
|
+
const plan = evidencePlan(rows);
|
|
626
|
+
const log = json ? console.error : console.log;
|
|
627
|
+
for (const { id, ref } of plan) {
|
|
628
|
+
if (!opts.dryRun) (landedDeps.evidenceAdd ?? features.evidenceAdd)(id, version, ref, actor(), snapshot.released, snapshot.dev);
|
|
629
|
+
log(`${opts.dryRun ? '·' : '✅'} ${id} 근거 ${ref}`);
|
|
630
|
+
}
|
|
631
|
+
const added = opts.dryRun ? 0 : plan.length;
|
|
632
|
+
totalAdded += added;
|
|
633
|
+
log(`붙임 ${added} · 건너뜀(언급만) ${skippedMention}`);
|
|
634
|
+
if (!opts.open && !opts.dryRun) debug.log('release.checklist', 'evidence-synced', { added, skippedMention });
|
|
635
|
+
results.push({ version, plan });
|
|
636
|
+
} else {
|
|
637
|
+
if (!json) printLandedRows(rows, items);
|
|
638
|
+
results.push({ version, rows });
|
|
571
639
|
}
|
|
572
|
-
console.log(`${rows.length}칸 · 그중 근거에 없는 PR 이 있는 칸 ${rows.filter((row) => row.alreadyInEvidence.includes(false)).length}`);
|
|
573
640
|
}
|
|
641
|
+
if (opts.open) {
|
|
642
|
+
if (opts.applyEvidence && !opts.dryRun) debug.log('release.checklist', 'evidence-synced', { versions: snapshots.map(({ version }) => version), added: totalAdded, skippedMention: totalSkipped });
|
|
643
|
+
if (!json) console.log(`판 ${snapshots.length} · 붙임 합 ${totalAdded} · 건너뜀(언급만) 합 ${totalSkipped}`);
|
|
644
|
+
else await writeStdoutJson(`${JSON.stringify(results)}\n`);
|
|
645
|
+
} else if (json) await writeStdoutJson(`${JSON.stringify(opts.applyEvidence || opts.dryRun ? results[0]!.plan : results[0]!.rows)}\n`);
|
|
574
646
|
if (prs.length >= 400) console.error('⚠ 병합 PR 400개 상한: 목록이 잘렸을 수 있다');
|
|
575
647
|
} catch (error) {
|
|
576
648
|
console.error(`⛔ ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -581,6 +653,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
581
653
|
.option('--allow-duplicate-id', '다른 판에 같은 id 가 있어도 경고 후 추가')
|
|
582
654
|
.action((id: string, title: string, opts: { owner?: string; kind?: string; allowDuplicateId?: boolean }, cmd: Command) => {
|
|
583
655
|
const { version, json } = context(cmd);
|
|
656
|
+
if (!mayWrite(cmd)) return;
|
|
584
657
|
const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
|
|
585
658
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 추가`);
|
|
586
659
|
});
|
|
@@ -592,6 +665,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
592
665
|
if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new CliUserError(`잘못된 상태: ${opts.status}`, 'green|yellow|red|done');
|
|
593
666
|
if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new CliUserError(`잘못된 처분: ${opts.disposition}`, 'move|known-issue|block');
|
|
594
667
|
if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind 중 하나');
|
|
668
|
+
if (!mayWrite(cmd)) return;
|
|
595
669
|
const data = setItem(version, id, { ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, process.env.ELANOUS_TRACK || 'cli');
|
|
596
670
|
const item = data.items.find((entry) => entry.id === id);
|
|
597
671
|
const why = item?.kind === 'screen' && item.status === 'green' ? parityGap(item.evidence) : null;
|
|
@@ -605,40 +679,45 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
605
679
|
.requiredOption('--by <owner>', '잡을 자리 또는 하위 자리').option('--force', '기존 주인에서 강제로 바꾼다')
|
|
606
680
|
.action((id: string, opts: { by: string; force?: boolean }, cmd: Command) => {
|
|
607
681
|
const { version, json } = context(cmd);
|
|
682
|
+
if (!mayWrite(cmd)) return;
|
|
608
683
|
const data = claimItem(version, id, opts.by, { force: opts.force });
|
|
609
684
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 주인 ${opts.by}`);
|
|
610
685
|
});
|
|
611
686
|
withContext(checklist.command('rm <id>').description('칸 삭제')).action((id: string, _opts: unknown, cmd: Command) => {
|
|
612
687
|
const { version, json } = context(cmd);
|
|
688
|
+
if (!mayWrite(cmd)) return;
|
|
613
689
|
const data = removeItem(version, id, process.env.ELANOUS_TRACK || 'cli');
|
|
614
690
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 삭제`);
|
|
615
691
|
});
|
|
616
692
|
withContext(checklist.command('seed').description('마크다운 로드맵에서 빈 칸 들이기')).requiredOption('--from <file>', '로드맵 파일')
|
|
617
693
|
.action((opts: { from: string }, cmd: Command) => {
|
|
618
694
|
const { version, json } = context(cmd);
|
|
695
|
+
if (!mayWrite(cmd)) return;
|
|
619
696
|
const data = seedFromRoadmap(version, readFileSync(opts.from, 'utf8'));
|
|
620
697
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${version} ${data.items.length}칸`);
|
|
621
698
|
});
|
|
622
|
-
const actor = () => process.env.ELANOUS_TRACK || 'cli';
|
|
623
699
|
withContext(checklist.command('move <id>').description('칸을 한 트랜잭션으로 다른 판에 옮긴다'))
|
|
624
700
|
.requiredOption('--from <v>', '현재 판').requiredOption('--to <v>', '새 판').option('--reason <reason>', '이동 사유')
|
|
625
701
|
.action((id: string, opts: { from: string; to: string; reason?: string }, cmd: Command) => {
|
|
626
702
|
const { codenames, json } = context(cmd);
|
|
627
703
|
const from = checklistVersion(opts.from, codenames), to = checklistVersion(opts.to, codenames);
|
|
704
|
+
if (!mayWrite(cmd)) return;
|
|
628
705
|
features.move(id, from, to, actor(), undefined, undefined, opts.reason);
|
|
629
706
|
if (json) console.log(JSON.stringify(listChecklist(to))); else console.log(`✅ ${id} ${from} → ${to}`);
|
|
630
707
|
});
|
|
631
708
|
withContext(checklist.command('retitle <id> <title>').description('칸 제목 수정')).action((id: string, title: string, _opts: unknown, cmd: Command) => {
|
|
632
709
|
const { version, json } = context(cmd);
|
|
710
|
+
if (!mayWrite(cmd)) return;
|
|
633
711
|
const snapshot = listChecklist(version);
|
|
634
712
|
features.retitle(id, title, actor(), snapshot.released, snapshot.dev);
|
|
635
713
|
if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${id} 제목 수정`);
|
|
636
714
|
});
|
|
637
715
|
const evidence = checklist.command('evidence').description('칸의 PR·커밋 근거');
|
|
638
716
|
withContext(evidence.command('add <id> <ref>').description('근거 참조 추가')).action((id: string, ref: string, _opts: unknown, cmd: Command) => {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
717
|
+
const { version, json } = context(cmd);
|
|
718
|
+
if (!mayWrite(cmd)) return;
|
|
719
|
+
const snapshot = listChecklist(version);
|
|
720
|
+
features.evidenceAdd(id, version, ref, actor(), snapshot.released, snapshot.dev);
|
|
642
721
|
if (json) console.log(JSON.stringify(features.history(id).map(decodeClaimHistoryEntry))); else console.log(`✅ ${id} 근거 ${ref}`);
|
|
643
722
|
});
|
|
644
723
|
withContext(checklist.command('history <id>').description('칸의 판 이동·변경 이력')).action((id: string, _opts: unknown, cmd: Command) => {
|
|
@@ -654,6 +733,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
654
733
|
});
|
|
655
734
|
withContext(checklist.command('export').description('SQLite 에서 checklist.json 스냅샷 생성')).action((_opts: unknown, cmd: Command) => {
|
|
656
735
|
const { version, json } = context(cmd);
|
|
736
|
+
if (!mayWrite(cmd)) return;
|
|
657
737
|
const snapshot = listChecklist(version);
|
|
658
738
|
features.exportJson(version, snapshot.released, snapshot.dev);
|
|
659
739
|
if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${version} checklist.json 내보냄`);
|