elanous 0.2.11 → 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-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
- 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-85c2b53f731da024.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.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-e5b4e1b256f41076.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.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-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.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-36c6c4183353ac9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.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-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.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-e7a6c80f288b1948.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
- 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-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/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/context/nightly-condense.yaml +21 -0
- package/graphs/context/recipes.yaml +12 -0
- package/graphs/demo/inside-seed.yaml +20 -0
- package/graphs/demo/recipes.yaml +12 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/plugins/card-followup/examples/sample-card.png +0 -0
- 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/lib/telegram-inject-args.ts +31 -0
- package/scripts/measure-pod-memory-by-goal.ts +130 -0
- package/scripts/release-loop/auto-approve-node.ts +1 -1
- package/scripts/release-loop/checklist-gate-node.ts +2 -1
- package/scripts/release-loop/gate-node.ts +61 -17
- package/scripts/release-loop/ops-upgrade-node.ts +59 -6
- package/scripts/release-loop/publish-node.ts +4 -1
- package/scripts/release-loop/version-node.ts +13 -3
- package/scripts/telegram-inject.ts +14 -6
- package/scripts/test-deterministic.ts +53 -3
- package/scripts/test-diet/remeasure-list.ts +112 -0
- package/scripts/ux/demo-rehearsal.ts +17 -13
- package/scripts/ux/demo-seed.ts +108 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
- 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 +150 -23
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +2 -2
- package/src/autopilot/pty-control-loop.ts +4 -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/chat/index.ts +1 -0
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +45 -13
- package/src/cli/pod-cli.ts +27 -3
- package/src/cli/pod-memory-advice.ts +35 -0
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +135 -15
- 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 +32 -2
- package/src/context-bus/context-now.ts +2 -2
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +153 -0
- package/src/context-bus/session-events.ts +35 -0
- package/src/dashboard/dashboard-status-lines.ts +2 -0
- package/src/dashboard/index.ts +8 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/domains/outbound-alert.ts +99 -31
- package/src/domains/outbound-log-probe.ts +8 -0
- package/src/execution-loop/launch-gate.ts +17 -8
- package/src/field-note/telegram.ts +142 -0
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow-llm.ts +23 -13
- package/src/graph-runner/graph-grow.ts +69 -13
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +92 -24
- package/src/harness/harness-cli-command.ts +165 -17
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/consult-intake.ts +153 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +82 -3
- 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 +232 -16
- package/src/intake-plane/discord-seat-work.ts +36 -3
- 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 +33 -0
- package/src/llm/difficulty.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 +5 -4
- 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 +78 -8
- package/src/nexus/api/outbound-report.ts +40 -7
- package/src/nexus/api/personas.ts +97 -54
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-history.ts +88 -0
- package/src/nexus/api/workflows.ts +52 -0
- package/src/nexus/outbound/delivery-ledger.ts +17 -8
- package/src/nexus/outbound/router.ts +8 -3
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +10 -2
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/plugins/maker/market-bundle.ts +2 -1
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/checklist.ts +15 -3
- package/src/release-loop/feature-store.ts +24 -10
- package/src/release-loop/landed-but-yellow.ts +60 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +253 -18
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/draft-sweep.ts +44 -10
- package/src/self-dev/draft-triage-rules.ts +54 -2
- package/src/self-implement/budget-gate.ts +29 -12
- package/src/self-implement/gate-candidate-ranking.ts +60 -0
- package/src/self-implement/gate-scope.ts +9 -8
- package/src/self-implement/headless-elanous-driver.ts +7 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +15 -0
- package/src/self-implement/test-seams.ts +1 -0
- 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-command-job.ts +14 -10
- 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 +46 -8
- package/src/telegram-commands.ts +21 -0
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +84 -4
- package/src/telegram.ts +37 -5
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +101 -13
- 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/2310-7a59f043f6db207e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
- package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
- package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -2,12 +2,32 @@
|
|
|
2
2
|
// in a per-user directory; a launch is admitted only while live leases stay under the pool's measured total.
|
|
3
3
|
// Without this, independent CLI processes each measured an empty pool and all launched (review round 2 of #23212).
|
|
4
4
|
import { spawnSync } from 'node:child_process';
|
|
5
|
-
import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
6
6
|
import { tmpdir, userInfo } from 'node:os';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { debug } from '../debug/log.js';
|
|
9
9
|
|
|
10
|
-
export interface HostLeaseRecord { pid: number; startedAt: string; at: string }
|
|
10
|
+
export interface HostLeaseRecord { pid: number; startedAt: string; at: string; stage?: 'authoring' | 'job'; claimed?: boolean; job?: string; context?: string; namespace?: string }
|
|
11
|
+
|
|
12
|
+
export interface PendingJobIdentity { context: string; namespace: string; job: string }
|
|
13
|
+
|
|
14
|
+
/** Only the same cluster, namespace and Job can account for an existing host lease. */
|
|
15
|
+
export function leaseHasPendingPod(lease: HostLeaseRecord, pending: readonly PendingJobIdentity[]): boolean {
|
|
16
|
+
return lease.stage === 'job' && !!lease.context && !!lease.namespace && !!lease.job &&
|
|
17
|
+
pending.some((pod) => pod.context === lease.context && pod.namespace === lease.namespace && pod.job === lease.job);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface HostLeaseHandle {
|
|
21
|
+
(): void;
|
|
22
|
+
readonly id: string;
|
|
23
|
+
/** Job creation is not release: retain the lease until its Pod is observed Running or terminal. */
|
|
24
|
+
applied: (job: string, context: string, namespace: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A Job without a Running/terminal Pod still occupies a host reservation. */
|
|
28
|
+
export function hostLeaseCounts(records: readonly HostLeaseRecord[]): { reserved: number; waitingJobs: number } {
|
|
29
|
+
return { reserved: records.filter((r) => r.stage !== 'job').length, waitingJobs: records.filter((r) => r.stage === 'job').length };
|
|
30
|
+
}
|
|
11
31
|
|
|
12
32
|
export interface HostPoolLeaseOptions {
|
|
13
33
|
/** Base directory — default `$TMPDIR/elanous-pod-leases-<uid>` (0700). */
|
|
@@ -53,8 +73,42 @@ export class HostPoolLease {
|
|
|
53
73
|
this.staleLockMs = options.staleLockMs ?? 10_000;
|
|
54
74
|
}
|
|
55
75
|
|
|
76
|
+
reservationIsOther(id: string): boolean {
|
|
77
|
+
if (!/^[A-Za-z0-9._-]+\.json$/.test(id)) return false;
|
|
78
|
+
try {
|
|
79
|
+
const record = JSON.parse(readFileSync(join(this.dir, id), 'utf8')) as HostLeaseRecord;
|
|
80
|
+
return record.stage === 'authoring' && Number.isSafeInteger(record.pid) && record.pid > 0 &&
|
|
81
|
+
typeof record.startedAt === 'string' && this.processStart(record.pid) === record.startedAt &&
|
|
82
|
+
(record.pid !== this.pid || record.startedAt !== this.selfStart());
|
|
83
|
+
} catch { return false; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Transfer a reservation across the authoring → orchestrator process boundary. */
|
|
87
|
+
claim(id: string): HostLeaseHandle | null {
|
|
88
|
+
if (!/^[A-Za-z0-9._-]+\.json$/.test(id)) return null;
|
|
89
|
+
const file = join(this.dir, id);
|
|
90
|
+
const deadline = Date.now() + this.staleLockMs;
|
|
91
|
+
while (!this.lock()) {
|
|
92
|
+
if (Date.now() >= deadline) return null;
|
|
93
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
if (this.ownStart === undefined) this.ownStart = this.processStart(this.pid);
|
|
97
|
+
if (!this.ownStart) return null;
|
|
98
|
+
const original = JSON.parse(readFileSync(file, 'utf8')) as HostLeaseRecord;
|
|
99
|
+
if (original.stage !== 'authoring' || original.claimed || !Number.isSafeInteger(original.pid) || original.pid <= 0 ||
|
|
100
|
+
typeof original.startedAt !== 'string' || this.processStart(original.pid) !== original.startedAt) return null;
|
|
101
|
+
const record: HostLeaseRecord = { ...original, pid: this.pid, startedAt: this.ownStart, claimed: true };
|
|
102
|
+
this.replaceRecord(file, record);
|
|
103
|
+
return this.handle(file, record, id);
|
|
104
|
+
} catch { return null; }
|
|
105
|
+
finally { this.unlock(); }
|
|
106
|
+
}
|
|
107
|
+
|
|
56
108
|
/** Live leases after removing stale ones (dead pid or a recycled pid whose start time differs). */
|
|
57
|
-
live(): HostLeaseRecord[] {
|
|
109
|
+
live(): HostLeaseRecord[] { return this.readLive(false); }
|
|
110
|
+
|
|
111
|
+
private readLive(lockHeld: boolean): HostLeaseRecord[] {
|
|
58
112
|
const out: HostLeaseRecord[] = [];
|
|
59
113
|
let names: string[] = [];
|
|
60
114
|
try { names = readdirSync(this.dir).filter((name) => name.endsWith('.json')); } catch { return out; }
|
|
@@ -62,50 +116,106 @@ export class HostPoolLease {
|
|
|
62
116
|
const file = join(this.dir, name);
|
|
63
117
|
let record: HostLeaseRecord | null = null;
|
|
64
118
|
try { record = JSON.parse(readFileSync(file, 'utf8')) as HostLeaseRecord; } catch { record = null; }
|
|
65
|
-
const alive = record &&
|
|
119
|
+
const alive = record && Number.isSafeInteger(record.pid) && record.pid > 0 && typeof record.startedAt === 'string'
|
|
120
|
+
&& this.processStart(record.pid) === record.startedAt;
|
|
66
121
|
if (record && alive) { out.push(record); continue; }
|
|
67
|
-
|
|
68
|
-
|
|
122
|
+
// A concurrent claim can replace the record while we check the original PID.
|
|
123
|
+
if (!lockHeld && !this.lock()) { if (record) out.push(record); continue; }
|
|
124
|
+
try {
|
|
125
|
+
let current: HostLeaseRecord | null;
|
|
126
|
+
try { current = JSON.parse(readFileSync(file, 'utf8')) as HostLeaseRecord; } catch { current = null; }
|
|
127
|
+
if (current && Number.isSafeInteger(current.pid) && current.pid > 0 && typeof current.startedAt === 'string' &&
|
|
128
|
+
this.processStart(current.pid) === current.startedAt) { out.push(current); continue; }
|
|
129
|
+
try { unlinkSync(file); } catch { /* already cleaned */ }
|
|
130
|
+
debug.log('pod.lease', 'host-lease-stale-cleared', { file: name, pid: current?.pid ?? null });
|
|
131
|
+
} finally { if (!lockHeld) this.unlock(); }
|
|
69
132
|
}
|
|
70
133
|
return out;
|
|
71
134
|
}
|
|
72
135
|
|
|
73
|
-
|
|
74
|
-
othersPending(): number {
|
|
136
|
+
selfStart(): string | null {
|
|
75
137
|
if (this.ownStart === undefined) this.ownStart = this.processStart(this.pid);
|
|
76
|
-
return this.
|
|
138
|
+
return this.ownStart;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Reservations held by other processes while the cluster cannot yet observe them. */
|
|
142
|
+
othersPending(): number {
|
|
143
|
+
return this.live().filter((r) => !(r.pid === this.pid && r.startedAt === this.selfStart())).length;
|
|
77
144
|
}
|
|
78
145
|
|
|
79
146
|
/**
|
|
80
|
-
* Under the host lock, take a lease only if `allowed(
|
|
81
|
-
*
|
|
147
|
+
* Under the host lock, take a lease only if `allowed(pending)` holds — the check and the write are one step.
|
|
148
|
+
* By default pending excludes this process's permits (the local admission gate owns them); includeOwn
|
|
149
|
+
* counts every reservation for independent authoring launches from the same process.
|
|
150
|
+
* Null = refused or lock busy (retry later).
|
|
82
151
|
*/
|
|
83
|
-
tryReserve(allowed: (othersPending: number) => boolean
|
|
152
|
+
tryReserve(allowed: (othersPending: number) => boolean, includeOwn = false): HostLeaseHandle | null {
|
|
84
153
|
try { mkdirSync(this.dir, { recursive: true, mode: 0o700 }); } catch { return null; }
|
|
85
154
|
if (!this.lock()) return null;
|
|
86
155
|
try {
|
|
87
156
|
if (this.ownStart === undefined) this.ownStart = this.processStart(this.pid);
|
|
88
157
|
if (!this.ownStart) return null;
|
|
89
|
-
const others = this.
|
|
158
|
+
const others = this.readLive(true).filter((r) => includeOwn || !(r.pid === this.pid && r.startedAt === this.ownStart)).length;
|
|
90
159
|
if (!allowed(others)) {
|
|
91
160
|
debug.log('pod.lease', 'host-lease-refused', { othersPending: others });
|
|
92
161
|
return null;
|
|
93
162
|
}
|
|
94
163
|
const file = join(this.dir, `${this.pid}-${this.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
|
|
95
|
-
const record: HostLeaseRecord = { pid: this.pid, startedAt: this.ownStart, at: new Date(this.now()).toISOString() };
|
|
164
|
+
const record: HostLeaseRecord = { pid: this.pid, startedAt: this.ownStart, at: new Date(this.now()).toISOString(), stage: 'authoring' };
|
|
96
165
|
writeFileSync(file, JSON.stringify(record), { flag: 'wx', mode: 0o600 });
|
|
97
166
|
debug.log('pod.lease', 'host-lease-acquired', { othersPending: others });
|
|
98
|
-
|
|
99
|
-
return () => {
|
|
100
|
-
if (released) return;
|
|
101
|
-
released = true;
|
|
102
|
-
try { unlinkSync(file); } catch { /* already cleaned */ }
|
|
103
|
-
};
|
|
167
|
+
return this.handle(file, record, file.slice(this.dir.length + 1));
|
|
104
168
|
} finally {
|
|
105
169
|
this.unlock();
|
|
106
170
|
}
|
|
107
171
|
}
|
|
108
172
|
|
|
173
|
+
private replaceRecord(file: string, record: HostLeaseRecord): void {
|
|
174
|
+
const temp = `${file}.${this.pid}-${Math.random().toString(36).slice(2)}.tmp`;
|
|
175
|
+
try {
|
|
176
|
+
writeFileSync(temp, JSON.stringify(record), { flag: 'wx', mode: 0o600 });
|
|
177
|
+
renameSync(temp, file);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
try { unlinkSync(temp); } catch { /* already gone */ }
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private handle(file: string, record: HostLeaseRecord, id: string): HostLeaseHandle {
|
|
185
|
+
let released = false;
|
|
186
|
+
const release = (() => {
|
|
187
|
+
if (released) return;
|
|
188
|
+
// Claim replaces the same file under this lock. An old owner must not unlink a
|
|
189
|
+
// newly transferred reservation between reading and removing its record.
|
|
190
|
+
const deadline = Date.now() + this.staleLockMs;
|
|
191
|
+
while (!this.lock()) {
|
|
192
|
+
if (Date.now() >= deadline) return;
|
|
193
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const current = JSON.parse(readFileSync(file, 'utf8')) as HostLeaseRecord;
|
|
197
|
+
if (current.pid === record.pid && current.startedAt === record.startedAt && current.claimed === record.claimed) unlinkSync(file);
|
|
198
|
+
} catch { /* transferred or already cleaned */ }
|
|
199
|
+
finally { released = true; this.unlock(); }
|
|
200
|
+
}) as HostLeaseHandle;
|
|
201
|
+
Object.defineProperty(release, 'id', { value: id });
|
|
202
|
+
release.applied = (job, context, namespace) => {
|
|
203
|
+
if (released) return;
|
|
204
|
+
const deadline = Date.now() + this.staleLockMs;
|
|
205
|
+
while (!this.lock()) {
|
|
206
|
+
if (Date.now() >= deadline) throw new Error('pod lease: cannot update Job reservation');
|
|
207
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const current = JSON.parse(readFileSync(file, 'utf8')) as HostLeaseRecord;
|
|
211
|
+
if (current.pid !== record.pid || current.startedAt !== record.startedAt || current.claimed !== record.claimed) throw new Error('pod lease: reservation owner changed');
|
|
212
|
+
if (!job || !context || !namespace) throw new Error('pod lease: Job identity requires context, namespace and name');
|
|
213
|
+
this.replaceRecord(file, { ...record, stage: 'job', job, context, namespace });
|
|
214
|
+
} finally { this.unlock(); }
|
|
215
|
+
};
|
|
216
|
+
return release;
|
|
217
|
+
}
|
|
218
|
+
|
|
109
219
|
private lockDir(): string { return join(this.dir, '.lock'); }
|
|
110
220
|
|
|
111
221
|
private lock(): boolean {
|
|
@@ -29,6 +29,7 @@ export interface ChecklistHistory {
|
|
|
29
29
|
released: string;
|
|
30
30
|
dev: string;
|
|
31
31
|
force?: true;
|
|
32
|
+
reason?: string;
|
|
32
33
|
}
|
|
33
34
|
export interface Checklist {
|
|
34
35
|
version: string;
|
|
@@ -72,13 +73,18 @@ export function listChecklist(v: string, root?: string): Checklist {
|
|
|
72
73
|
return decodeClaimHistory(root === undefined ? store.list(v) : store.list(v, undefined, undefined, root));
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/** Events for this id across every release, ordered by event time (then ledger sequence). */
|
|
77
|
+
export function checklistHistory(id: string): Array<ChecklistHistory & { version: string; seq: number }> {
|
|
78
|
+
return store.history(id).map(decodeClaimHistoryEntry);
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
/** Status events in the requested window; other ledger fields never become status changes. */
|
|
76
82
|
export function statusChangesSince(history: readonly ChecklistHistory[], sinceIso: string): ChecklistHistory[] {
|
|
77
83
|
const since = Date.parse(sinceIso);
|
|
78
84
|
return history.filter((entry) => entry.field === 'status' && Date.parse(entry.at) >= since);
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
function mutate(v: string, apply: (data: Checklist) => boolean): Checklist {
|
|
87
|
+
function mutate(v: string, apply: (data: Checklist, otherItems: (id: string) => store.ChecklistCollision[]) => boolean): Checklist {
|
|
82
88
|
return decodeClaimHistory(store.mutate(v, store.releasedVersion(), devVersion(), apply));
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -87,13 +93,19 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
|
|
|
87
93
|
debug.log('release-loop.checklist', 'change', { version: data.version, id, field, from: from ?? null, to: to ?? null, by });
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind }): Checklist {
|
|
96
|
+
export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind }, options: { allowDuplicateId?: boolean } = {}): Checklist {
|
|
91
97
|
if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
|
|
92
98
|
if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
|
|
93
99
|
if (input.kind !== undefined && input.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen');
|
|
94
100
|
if (input.owner !== undefined) parseOwner(input.owner);
|
|
95
|
-
return mutate(v, (data) => {
|
|
101
|
+
return mutate(v, (data, otherItems) => {
|
|
96
102
|
if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
|
|
103
|
+
const collisions = otherItems(input.id);
|
|
104
|
+
if (collisions.length) {
|
|
105
|
+
const displayedId = JSON.stringify(input.id).slice(1, -1);
|
|
106
|
+
console.error(`⚠ 중복 id: ${displayedId} — ${collisions.map((item) => `${item.version} · 담당 ${JSON.stringify(item.owner ?? '-').slice(1, -1)} · ${item.title.replace(/\s+/g, ' ').slice(0, 40)}`).join(' / ')}`);
|
|
107
|
+
if (!options.allowDuplicateId) throw new CliUserError(`다른 판에 이미 있는 칸: ${displayedId}`, '--allow-duplicate-id 로 허용');
|
|
108
|
+
}
|
|
97
109
|
const by = process.env.ELANOUS_TRACK || 'cli';
|
|
98
110
|
const at = new Date().toISOString();
|
|
99
111
|
const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), updatedAt: at, updatedBy: by };
|
|
@@ -50,12 +50,13 @@ function open(root = releaseLedgerRoot()): Database {
|
|
|
50
50
|
db.exec('PRAGMA foreign_keys = ON');
|
|
51
51
|
if (!db.query("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'imported_versions'").get()) db.exec(`CREATE TABLE IF NOT EXISTS features (id TEXT PRIMARY KEY, title TEXT NOT NULL, owner TEXT, kind TEXT, created_at TEXT NOT NULL);
|
|
52
52
|
CREATE TABLE IF NOT EXISTS assignments (feature_id TEXT NOT NULL REFERENCES features(id), version TEXT NOT NULL, status TEXT NOT NULL, disposition TEXT, evidence TEXT, title_override TEXT, owner TEXT, kind TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL, PRIMARY KEY(feature_id, version));
|
|
53
|
-
CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY AUTOINCREMENT, at TEXT NOT NULL, by TEXT NOT NULL, feature_id TEXT NOT NULL, version TEXT NOT NULL, field TEXT NOT NULL, "from" TEXT, "to" TEXT, released TEXT NOT NULL, dev TEXT NOT NULL);
|
|
53
|
+
CREATE TABLE IF NOT EXISTS events (seq INTEGER PRIMARY KEY AUTOINCREMENT, at TEXT NOT NULL, by TEXT NOT NULL, feature_id TEXT NOT NULL, version TEXT NOT NULL, field TEXT NOT NULL, "from" TEXT, "to" TEXT, released TEXT NOT NULL, dev TEXT NOT NULL, reason TEXT);
|
|
54
54
|
CREATE TABLE IF NOT EXISTS evidence (feature_id TEXT NOT NULL, version TEXT NOT NULL, ref TEXT NOT NULL, at TEXT NOT NULL, by TEXT NOT NULL);
|
|
55
55
|
CREATE TABLE IF NOT EXISTS imported_versions (version TEXT PRIMARY KEY, json_hash TEXT, imported_at TEXT);`);
|
|
56
56
|
const columns = db.query('PRAGMA table_info(imported_versions)').all() as Array<{ name: string }>;
|
|
57
57
|
if (!columns.some((column) => column.name === 'json_hash')) db.exec('ALTER TABLE imported_versions ADD COLUMN json_hash TEXT');
|
|
58
58
|
if (!columns.some((column) => column.name === 'imported_at')) db.exec('ALTER TABLE imported_versions ADD COLUMN imported_at TEXT');
|
|
59
|
+
if (!(db.query('PRAGMA table_info(events)').all() as Array<{ name: string }>).some((column) => column.name === 'reason')) db.exec('ALTER TABLE events ADD COLUMN reason TEXT');
|
|
59
60
|
db.exec('CREATE TABLE IF NOT EXISTS release_schedules (version TEXT PRIMARY KEY, cut_at TEXT NOT NULL, land_by TEXT, updated_at TEXT NOT NULL, updated_by TEXT NOT NULL)');
|
|
60
61
|
return db;
|
|
61
62
|
} catch (error) { db.close(); throw error; }
|
|
@@ -119,14 +120,15 @@ function readTransaction<T>(db: Database, work: () => T): T {
|
|
|
119
120
|
catch (error) { db.exec('ROLLBACK'); throw error; }
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
type EventRow = { seq: number; at: string; by: string; feature_id: string; version: string; field: string; from: string; to: string; released: string; dev: string };
|
|
123
|
+
type EventRow = { seq: number; at: string; by: string; feature_id: string; version: string; field: string; from: string; to: string; released: string; dev: string; reason: string | null };
|
|
123
124
|
function decode(value: string | null): unknown { return value === null ? null : JSON.parse(value); }
|
|
124
125
|
function historyRow(row: EventRow): ChecklistHistory {
|
|
125
|
-
return { at: row.at, by: row.by, id: row.feature_id, field: row.field, from: decode(row.from), to: decode(row.to), released: row.released, dev: row.dev
|
|
126
|
+
return { at: row.at, by: row.by, id: row.feature_id, field: row.field, from: decode(row.from), to: decode(row.to), released: row.released, dev: row.dev,
|
|
127
|
+
...(row.reason !== null ? { reason: row.reason } : {}) };
|
|
126
128
|
}
|
|
127
129
|
function record(db: Database, version: string, entry: ChecklistHistory): void {
|
|
128
|
-
db.query('INSERT INTO events (at, by, feature_id, version, field, "from", "to", released, dev) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
129
|
-
.run(entry.at, entry.by, entry.id, version, entry.field, JSON.stringify(entry.from ?? null), JSON.stringify(entry.to ?? null), entry.released, entry.dev);
|
|
130
|
+
db.query('INSERT INTO events (at, by, feature_id, version, field, "from", "to", released, dev, reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
131
|
+
.run(entry.at, entry.by, entry.id, version, entry.field, JSON.stringify(entry.from ?? null), JSON.stringify(entry.to ?? null), entry.released, entry.dev, entry.reason ?? null);
|
|
130
132
|
}
|
|
131
133
|
function evidenceRefs(db: Database, version: string, id: string): string[] {
|
|
132
134
|
return (db.query('SELECT ref FROM evidence WHERE feature_id = ? AND version = ? ORDER BY rowid').all(id, version) as Array<{ ref: string }>).map((row) => row.ref);
|
|
@@ -288,7 +290,9 @@ export function list(version: string, released?: string, dev = devVersion(), roo
|
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
/** The checklist API supplies its existing validation, field ordering and legacy history shape. */
|
|
291
|
-
export
|
|
293
|
+
export interface ChecklistCollision { version: string; owner?: string; title: string }
|
|
294
|
+
|
|
295
|
+
export function mutate(version: string, released: string, dev: string, apply: (data: Checklist, otherItems: (id: string) => ChecklistCollision[]) => boolean): Checklist {
|
|
292
296
|
validateVersion(version);
|
|
293
297
|
const db = open();
|
|
294
298
|
try {
|
|
@@ -298,7 +302,16 @@ export function mutate(version: string, released: string, dev: string, apply: (d
|
|
|
298
302
|
const data = snapshot(db, version, released, dev);
|
|
299
303
|
const before = new Map(data.items.map((item) => [item.id, JSON.stringify(item)]));
|
|
300
304
|
const count = data.history.length;
|
|
301
|
-
if (apply(data)
|
|
305
|
+
if (apply(data, (id) => {
|
|
306
|
+
for (const other of legacyVersions()) if (other !== version) importOnDb(db, other);
|
|
307
|
+
return db.query(`SELECT a.version, a.owner, COALESCE(a.title_override, f.title) AS title
|
|
308
|
+
FROM assignments a JOIN features f ON f.id = a.feature_id
|
|
309
|
+
WHERE a.feature_id = ? AND a.version <> ? ORDER BY a.version`).all(id, version)
|
|
310
|
+
.map((row) => {
|
|
311
|
+
const item = row as { version: string; owner: string | null; title: string };
|
|
312
|
+
return { version: item.version, ...(item.owner !== null ? { owner: item.owner } : {}), title: item.title };
|
|
313
|
+
});
|
|
314
|
+
})) {
|
|
302
315
|
for (const item of data.items) if (before.get(item.id) !== JSON.stringify(item)) {
|
|
303
316
|
const previous = before.get(item.id);
|
|
304
317
|
const evidenceChanged = previous && (JSON.parse(previous) as ChecklistItem).evidence !== item.evidence;
|
|
@@ -356,7 +369,7 @@ export function remove(version: string, id: string, by: string, released = relea
|
|
|
356
369
|
return true;
|
|
357
370
|
});
|
|
358
371
|
}
|
|
359
|
-
export function move(id: string, from: string, to: string, by: string, released = releasedVersion(), dev = devVersion()): Checklist {
|
|
372
|
+
export function move(id: string, from: string, to: string, by: string, released = releasedVersion(), dev = devVersion(), reason?: string): Checklist {
|
|
360
373
|
validateVersion(from); validateVersion(to);
|
|
361
374
|
if (from === to) throw new CliUserError('같은 판으로 옮길 수 없다');
|
|
362
375
|
const db = open();
|
|
@@ -366,11 +379,12 @@ export function move(id: string, from: string, to: string, by: string, released
|
|
|
366
379
|
importedFrom = importOnDb(db, from); importedTo = importOnDb(db, to);
|
|
367
380
|
const item = items(db, from).find((i) => i.id === id);
|
|
368
381
|
if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
|
|
369
|
-
|
|
382
|
+
const collision = items(db, to).find((i) => i.id === id);
|
|
383
|
+
if (collision) throw new CliUserError(`이미 있는 칸: ${JSON.stringify(id).slice(1, -1)} — ${to} · 담당 ${JSON.stringify(collision.owner ?? '-').slice(1, -1)} · ${collision.title.replace(/\s+/g, ' ').slice(0, 40)}`);
|
|
370
384
|
const at = new Date().toISOString();
|
|
371
385
|
db.query('UPDATE assignments SET version = ?, updated_at = ?, updated_by = ? WHERE feature_id = ? AND version = ?').run(to, at, by, id, from);
|
|
372
386
|
db.query('UPDATE evidence SET version = ? WHERE feature_id = ? AND version = ?').run(to, id, from);
|
|
373
|
-
record(db, to, { at, by, id, field: 'move', from, to, released, dev });
|
|
387
|
+
record(db, to, { at, by, id, field: 'move', from, to, released, dev, ...(reason !== undefined ? { reason } : {}) });
|
|
374
388
|
return snapshot(db, to, released, dev);
|
|
375
389
|
});
|
|
376
390
|
if (importedFrom) debug.log('release.features', 'imported', { version: from });
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ownerMatches, type ChecklistItem } from './checklist.js';
|
|
2
|
+
|
|
3
|
+
export interface MergedChecklistPr { number: number; title: string; body: string | null; mergedAt: string }
|
|
4
|
+
type PrMatchBasis = 'cell-line' | 'title-strong' | 'mention';
|
|
5
|
+
export interface LandedButYellowRow {
|
|
6
|
+
id: string;
|
|
7
|
+
owner: string | undefined;
|
|
8
|
+
status: 'yellow' | 'red';
|
|
9
|
+
prs: Array<Pick<MergedChecklistPr, 'number' | 'title' | 'mergedAt'> & { basis: PrMatchBasis }>;
|
|
10
|
+
alreadyInEvidence: boolean[];
|
|
11
|
+
newer: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function evidencePlan(rows: readonly LandedButYellowRow[]): Array<{ id: string; ref: string }> {
|
|
15
|
+
const seen = new Set<string>();
|
|
16
|
+
return rows.flatMap((row) => row.prs.flatMap((pr, index) => {
|
|
17
|
+
if (pr.basis === 'mention' || row.alreadyInEvidence[index]) return [];
|
|
18
|
+
const ref = `#${pr.number}`;
|
|
19
|
+
const key = JSON.stringify([row.id, ref]);
|
|
20
|
+
if (seen.has(key)) return [];
|
|
21
|
+
seen.add(key);
|
|
22
|
+
return [{ id: row.id, ref }];
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Prefer a dedicated body cell line over title evidence; incidental mentions only qualify for long ids. */
|
|
27
|
+
export function landedButYellow(items: readonly ChecklistItem[], prs: readonly MergedChecklistPr[], { owner }: { owner?: string } = {}): LandedButYellowRow[] {
|
|
28
|
+
const rows: LandedButYellowRow[] = [];
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
if (item.status !== 'yellow' && item.status !== 'red') continue;
|
|
31
|
+
if (owner !== undefined && !ownerMatches(item.owner, owner)) continue;
|
|
32
|
+
const id = item.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
33
|
+
const word = new RegExp(`(?<![A-Za-z0-9])${id}(?![A-Za-z0-9])`);
|
|
34
|
+
const shortId = /^[A-Z]{1,2}\d{1,2}[a-z]?$/.test(item.id);
|
|
35
|
+
const titleStart = shortId
|
|
36
|
+
? new RegExp(`^${id}(?:$|\\s+(?:수확|(?:첫|둘째|셋째)\\s*조각|fixed|new|old)(?=\\s|:|$))`)
|
|
37
|
+
: new RegExp(`^${id}(?:\\s|$)`);
|
|
38
|
+
const explicitTitle = new RegExp(`(?<![A-Za-z0-9])${id}\\s+(?:수확|(?:첫|둘째|셋째)\\s*조각)(?![A-Za-z0-9가-힣])|\\(${id}\\)`);
|
|
39
|
+
const rank: Record<PrMatchBasis, number> = { 'cell-line': 0, 'title-strong': 1, mention: 2 };
|
|
40
|
+
const matched = prs.flatMap((pr) => {
|
|
41
|
+
const cellLine = (pr.body ?? '').split('\n').some((line) => {
|
|
42
|
+
const match = /^[ \t]*칸(?:[ \t]*:[ \t]*|[ \t]+)([A-Za-z0-9_-]+(?:[ \t]*,[ \t]*[A-Za-z0-9_-]+)*)[ \t]*$/.exec(line.endsWith('\r') ? line.slice(0, -1) : line);
|
|
43
|
+
return match?.[1]?.split(',').some((candidate) => candidate.trim() === item.id) ?? false;
|
|
44
|
+
});
|
|
45
|
+
const basis: PrMatchBasis | undefined = cellLine ? 'cell-line'
|
|
46
|
+
: titleStart.test(pr.title) || explicitTitle.test(pr.title) ? 'title-strong'
|
|
47
|
+
: !shortId && (word.test(pr.title) || word.test(pr.body ?? '')) ? 'mention' : undefined;
|
|
48
|
+
return basis ? [{ number: pr.number, title: pr.title, mergedAt: pr.mergedAt, basis }] : [];
|
|
49
|
+
}).sort((a, b) => rank[a.basis] - rank[b.basis] || b.mergedAt.localeCompare(a.mergedAt) || b.number - a.number);
|
|
50
|
+
if (!matched.length) continue;
|
|
51
|
+
const shown = matched.slice(0, 5);
|
|
52
|
+
rows.push({
|
|
53
|
+
id: item.id, owner: item.owner, status: item.status,
|
|
54
|
+
prs: shown,
|
|
55
|
+
alreadyInEvidence: shown.map(({ number }) => new RegExp(`(?<![A-Za-z0-9_#])#${number}(?![A-Za-z0-9_])`).test(item.evidence ?? '')),
|
|
56
|
+
newer: matched.some((pr) => pr.mergedAt > item.updatedAt),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return rows.sort((a, b) => Number(b.newer) - Number(a.newer));
|
|
60
|
+
}
|
|
@@ -1,31 +1,58 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
2
3
|
import { debug } from '../debug/log.js';
|
|
3
4
|
import { openMsgStore, type MsgStore } from '../msg/msg-store.js';
|
|
4
5
|
import { dispatchCeoTask, type CeoCommandDeps } from './ceo-commands.js';
|
|
6
|
+
import type { SeatId } from './seat-questions.js';
|
|
5
7
|
|
|
6
8
|
export type AskOrigin =
|
|
7
9
|
| { channel: 'telegram'; chatId: number | string; messageId: number; threadId?: number; botId?: string }
|
|
8
10
|
| { channel: 'discord'; channelId: string; messageId: string; threadId?: string }
|
|
9
11
|
| { channel: 'pwa'; clientId: string };
|
|
10
12
|
|
|
11
|
-
export const DEFAULT_ASK_TIMEOUT_MS =
|
|
13
|
+
export const DEFAULT_ASK_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
12
14
|
export const parseSeatAsk = (text: string): string | null => /^\s*CTO\s*에게\s*물어봐\s*[::]\s*(\S[\s\S]*)$/i.exec(text)?.[1]?.trim() ?? null;
|
|
13
15
|
|
|
14
|
-
interface AskRow { id: string; seat: string; origin: string; deadline: number; timeout_minutes: number }
|
|
16
|
+
interface AskRow { id: string; seat: string; origin: string; deadline: number; timeout_minutes: number; answer: string | null; answered_at: number | null }
|
|
15
17
|
|
|
16
18
|
function createTable(store: MsgStore): void {
|
|
17
19
|
store.db.exec(`CREATE TABLE IF NOT EXISTS seat_asks (
|
|
18
20
|
id TEXT PRIMARY KEY, seat TEXT NOT NULL, origin TEXT NOT NULL,
|
|
19
|
-
deadline INTEGER NOT NULL, timeout_minutes INTEGER NOT NULL DEFAULT
|
|
21
|
+
deadline INTEGER NOT NULL, timeout_minutes INTEGER NOT NULL DEFAULT 120, status TEXT NOT NULL DEFAULT 'pending',
|
|
22
|
+
answer TEXT, answered_at INTEGER
|
|
20
23
|
); CREATE TABLE IF NOT EXISTS seat_ask_outbox (
|
|
21
24
|
id TEXT PRIMARY KEY, origin TEXT NOT NULL, text TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
|
|
22
25
|
claim_until INTEGER NOT NULL DEFAULT 0, claim_token TEXT
|
|
23
26
|
)`);
|
|
27
|
+
const askColumns = store.db.query('PRAGMA table_info(seat_asks)').all() as Array<{ name: string }>;
|
|
28
|
+
if (!askColumns.some(({ name }) => name === 'answer')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answer TEXT');
|
|
29
|
+
if (!askColumns.some(({ name }) => name === 'answered_at')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answered_at INTEGER');
|
|
24
30
|
const columns = store.db.query('PRAGMA table_info(seat_ask_outbox)').all() as Array<{ name: string }>;
|
|
25
31
|
if (!columns.some(({ name }) => name === 'claim_until')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_until INTEGER NOT NULL DEFAULT 0');
|
|
26
32
|
if (!columns.some(({ name }) => name === 'claim_token')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_token TEXT');
|
|
27
33
|
}
|
|
28
34
|
|
|
35
|
+
export interface SeatAskShadow {
|
|
36
|
+
from: SeatId;
|
|
37
|
+
to: SeatId;
|
|
38
|
+
itemId: string;
|
|
39
|
+
version: string;
|
|
40
|
+
question: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Keep an internal seat-to-seat question in the existing ask ledger, without dispatching or queuing an outbox message. */
|
|
44
|
+
export function recordSeatAskShadow(root: string, ask: SeatAskShadow, now: number): boolean {
|
|
45
|
+
if (ask.from === ask.to || !ask.itemId || !ask.question.trim() || !Number.isFinite(now)) throw new Error('invalid shadow seat ask');
|
|
46
|
+
const id = `seat-loop:${createHash('sha256').update(JSON.stringify([ask.from, ask.to, ask.version, ask.itemId])).digest('hex')}`;
|
|
47
|
+
const store = openMsgStore(join(root, 'msg', 'messages.db'));
|
|
48
|
+
try {
|
|
49
|
+
createTable(store);
|
|
50
|
+
const origin = JSON.stringify({ channel: 'seat-loop', from: ask.from, itemId: ask.itemId, version: ask.version, question: ask.question.trim() });
|
|
51
|
+
return store.db.query("INSERT OR IGNORE INTO seat_asks (id, seat, origin, deadline, timeout_minutes, status) VALUES (?, ?, ?, ?, ?, 'shadow')")
|
|
52
|
+
.run(id, ask.to, origin, now + DEFAULT_ASK_TIMEOUT_MS, Math.ceil(DEFAULT_ASK_TIMEOUT_MS / 60000)).changes === 1;
|
|
53
|
+
} finally { store.close(); }
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
export interface SeatAskDeps {
|
|
30
57
|
open?: () => MsgStore;
|
|
31
58
|
now?: () => number;
|
|
@@ -52,7 +79,7 @@ export async function askSeat(text: string, origin: AskOrigin, command: CeoComma
|
|
|
52
79
|
.run(id, 'TC', JSON.stringify(origin), (deps.now ?? Date.now)() + timeout, minutes, 'pending');
|
|
53
80
|
} finally { store.close(); }
|
|
54
81
|
try {
|
|
55
|
-
const result = await (deps.dispatch ?? dispatchCeoTask)('TC', `질문: ${body}\n답장 요청: ${id}\n회신: elanous
|
|
82
|
+
const result = await (deps.dispatch ?? dispatchCeoTask)('TC', `질문: ${body}\n답장 요청: ${id}\n회신: elanous seat answer ${id} "<답>"`, command,
|
|
56
83
|
{ via: origin.channel, ref: `ask:${id}` });
|
|
57
84
|
debug.log('seat.ask', 'sent', { seat: 'TC', id, via: origin.channel, channel: result.channel });
|
|
58
85
|
return `CTO에게 물었습니다. 답을 기다립니다 (최대 ${minutes}분). 요청: ${id}${result.channel === 'failed' ? ` · 조율 채널 전송 실패: ${result.channelError}` : ''}`;
|
|
@@ -64,22 +91,40 @@ export async function askSeat(text: string, origin: AskOrigin, command: CeoComma
|
|
|
64
91
|
}
|
|
65
92
|
}
|
|
66
93
|
|
|
67
|
-
/**
|
|
94
|
+
/** Record a seat reply in its original request row; delivery stays with the originating surface's worker. */
|
|
95
|
+
export function answerSeatAsk(id: string, text: string, deps: Pick<SeatAskDeps, 'open' | 'now'> = {}): void {
|
|
96
|
+
const answer = text.trim();
|
|
97
|
+
if (!answer) throw new Error('seat answer must not be empty');
|
|
98
|
+
const store = (deps.open ?? openMsgStore)();
|
|
99
|
+
try {
|
|
100
|
+
createTable(store);
|
|
101
|
+
const now = (deps.now ?? Date.now)();
|
|
102
|
+
const updated = store.db.query(`UPDATE seat_asks SET answer = ?, answered_at = ?
|
|
103
|
+
WHERE id = ? AND seat = 'TC' AND status = 'pending' AND answer IS NULL AND deadline > ?`)
|
|
104
|
+
.run(answer, now, id, now);
|
|
105
|
+
if (updated.changes !== 1) throw new Error(`seat ask not pending or deadline passed: ${id}`);
|
|
106
|
+
debug.log('seat.ask', 'reply-recorded', { seat: 'TC', id });
|
|
107
|
+
} finally { store.close(); }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Deliver recorded seat answers or expiration notices via the original surface. */
|
|
68
111
|
export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
69
112
|
const store = (deps.open ?? openMsgStore)();
|
|
70
113
|
try {
|
|
71
114
|
createTable(store);
|
|
72
|
-
const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes FROM seat_asks WHERE status = 'pending'").all() as AskRow[];
|
|
115
|
+
const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE status = 'pending'").all() as AskRow[];
|
|
73
116
|
const decide = store.db.transaction((id: string, now: number): 'answered' | 'expired' | null => {
|
|
74
|
-
const ask = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes FROM seat_asks WHERE id = ? AND status = 'pending'").get(id) as AskRow | null;
|
|
117
|
+
const ask = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE id = ? AND status = 'pending'").get(id) as AskRow | null;
|
|
75
118
|
if (!ask) return null;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
119
|
+
// Retain compatibility with replies already posted to the existing seat mailbox.
|
|
120
|
+
const reply = ask.answer === null ? store.db.query("SELECT id, body, created_at FROM msg_messages WHERE recipient = 'CEO' AND kind = 'seat-ask-reply' AND substr(body, 1, ?) = ? AND sender = ? ORDER BY id LIMIT 1")
|
|
121
|
+
.get(id.length + 2, `${id}: `, ask.seat) as { id: number; body: string; created_at: string } | null : null;
|
|
122
|
+
const answer = ask.answer !== null && ask.answered_at !== null && ask.answered_at < ask.deadline ? ask.answer
|
|
123
|
+
: reply && Date.parse(reply.created_at) < ask.deadline ? reply.body.slice(id.length + 2).trim() : '';
|
|
79
124
|
const status = answer ? 'answered' : now >= ask.deadline ? 'expired' : null;
|
|
80
125
|
if (!status) return null;
|
|
81
126
|
const text = status === 'answered' ? `CTO 답변 (${id}): ${answer}`
|
|
82
|
-
: `CTO 미답 (${id}): ${ask.timeout_minutes}분
|
|
127
|
+
: `CTO 미답 (${id}): 아직 답 없음 (${ask.timeout_minutes}분 경과).`;
|
|
83
128
|
const updated = store.db.query("UPDATE seat_asks SET status = ? WHERE id = ? AND status = 'pending'").run(status, id);
|
|
84
129
|
if (updated.changes !== 1) return null;
|
|
85
130
|
store.db.query('INSERT INTO seat_ask_outbox (id, origin, text) VALUES (?, ?, ?)').run(id, ask.origin, text);
|