elanous 0.2.17 → 0.2.18
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/P6YTVgzuZz5oTZhY4Din_/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-aac7332576c4d6fc.js → 1307-30dff22c9e1f3073.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/195-fb1d7690bf30bab4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1986-33e13d648a9df765.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2537-6c289376caa88ca1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-325cc923ba7b8631.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3857-4f936cdb30322aa6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-5761612531d0146b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4565-3d7e3fdeb43686a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-a11f37254196b659.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{6158.9c0449a9a8d0064b.js → 6158.af2e93b7adff709b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6877-74399f69c7da1433.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7797-df5aa0381fdc078f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8401-8615a660c84cdb03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8695-846034ce44852c70.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-3143b07a817d1da9.js → page-d92f7ee3994e84c7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-17ba086ef82d339b.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-7eb661ff23af7da2.js → page-d22fd849cc22f104.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-bb742f995c6769e7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-6e5c3a867557298b.js → page-02b073a2ac793b83.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-60c3da708f9a0ccc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8a9b3594bc23289b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-5e77cd1802567c89.js → page-a0eb02638b1e6f31.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-308111abadf35670.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-6eaf7bac37830c21.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-4187666132fc98c4.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-09328d499162bae4.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-29ff3a8f84f03b6a.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-80ec7401bc83f511.js → page-9b72a07d424701c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-bd5d586429167fea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-fc7d86467291ae38.js → page-a572f58cd3ca5864.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-59b7834d497c2255.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/page-be5ea965a07fbc41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-0e2d6985fb42cd2f.js → page-c130b63862437814.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-13e4108999f58d13.js → page-fa244bd146a00ae8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-e8296ee284aa1293.js → page-44d56d6391965a6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-043a9111e74f6b24.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-23a4601ceb7f452e.js → not-found-7b3170f374f77b9d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-5f23d0c66a3b4793.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-10d958605a77b3bd.js → page-a3c58f5c72a5f3f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-a2c4625596f8d69c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-0e9099531d239f11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-7a1d85e121065cc8.js → page-e8e1ecaea7cd71d2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-12916918988ac97e.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d8a3347a9d0c3f4a.js → page-2da6f9b8a38c6978.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-102310930791197a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-f331e38fefc00393.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/page-6ed0b73bae8351d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-1d824510df025d18.js → page-44d5f767491692bf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-43336055451f6e37.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-7f49fe645d5132f7.js → page-3cc47dca0922ab9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-a77ee355a2304ff0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-27d71f131acd9320.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-61553851aa606146.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-a1f585e154e61c59.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-424659bacfb8cdd7.js → page-7bf54c9bb9274af8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-5150ef17b5383a57.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b9d8ce70792c1672.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-a5a3de231f2cc7d5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-5d4fef9ccd7d406b.js → page-134da468538baf48.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-31ac4afa2dcd915c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{webpack-539cde99554be20f.js → webpack-c2782b330447ffb6.js} +1 -1
- package/apps/pwa/out/_next/static/css/7fa358fec7da7c47.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/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/decisions/index.html +1 -0
- package/apps/pwa/out/decisions/index.txt +30 -0
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/package.json +2 -1
- package/samples/workflows/templates/approval-gated-deploy.yaml +30 -0
- package/samples/workflows/templates/chat-driven-research.yaml +35 -0
- package/samples/workflows/templates/discord-listen-and-respond.yaml +29 -0
- package/samples/workflows/templates/http-poll-then-alert.yaml +30 -0
- package/samples/workflows/templates/manual-quickstart.yaml +21 -0
- package/samples/workflows/templates/scheduled-daily-digest.yaml +30 -0
- package/samples/workflows/templates/webhook-deploy-hook.yaml +27 -0
- package/scripts/ci-public-export-import-gate.ts +7 -2
- package/scripts/ci-typecheck-changed.ts +21 -6
- package/scripts/discord-seat-probe.ts +3 -2
- package/scripts/docs/generate-cli-anatomy.ts +162 -0
- package/scripts/goals/authoring-metrics.ts +73 -0
- package/scripts/landing-heal/must-fix-match.ts +7 -7
- package/scripts/landing-heal/run-mission.ts +3 -2
- package/scripts/public-export-test-run.ts +37 -2
- package/scripts/release-loop/checklist-gate-node.ts +13 -11
- package/scripts/release-loop/cut-branch.ts +2 -1
- package/scripts/release-loop/cutoff.ts +4 -2
- package/scripts/release-loop/docs-land-node.ts +8 -1
- package/scripts/release-loop/docs-node.ts +1 -1
- package/scripts/release-loop/gate-node.ts +99 -14
- package/scripts/release-loop/node-verdict.ts +2 -1
- package/scripts/release-loop/npm-publish-node.ts +11 -7
- package/scripts/release-loop/ops-upgrade-node.ts +8 -1
- package/scripts/release-loop/preflight.ts +246 -0
- package/scripts/release-loop/publish-node.ts +8 -1
- package/scripts/release-loop/release-readiness.ts +11 -6
- package/scripts/release-loop/release-version.ts +48 -0
- package/scripts/release-loop/resume-release.ts +115 -0
- package/scripts/release-loop/unattended-release.ts +43 -7
- package/scripts/release-loop/version-node.ts +145 -40
- package/scripts/rhythm/daily.ts +25 -4
- package/scripts/skills/public-check.ts +62 -12
- package/scripts/test-deterministic.ts +51 -33
- package/scripts/test-diet/effectiveness.ts +62 -1
- package/scripts/test-diet/lib.ts +1 -1
- package/scripts/test-diet/node.ts +8 -1
- package/src/agent-mission/review-loop.ts +4 -3
- package/src/agent-substrate/pr-comment-meta.ts +20 -0
- package/src/autopilot/build/llm-conflict-merge.ts +28 -8
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +30 -11
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +73 -1
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/provider-cli.ts +12 -2
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/tasks-cli.ts +309 -3
- package/src/context-bus/condense.ts +19 -3
- package/src/context-bus/context-day.ts +77 -24
- package/src/context-bus/context-first.ts +33 -0
- package/src/discord-self-message.ts +20 -2
- package/src/discord-slash-wire.ts +1 -0
- package/src/domains/outbound-alert.ts +200 -27
- package/src/domains/outbound-log-probe.ts +16 -1
- package/src/domains/release-tool.ts +2 -2
- package/src/execution-loop/gates.ts +11 -2
- package/src/flow-loop/split.ts +8 -4
- package/src/git-fs/worktree.ts +23 -3
- package/src/harness/harness-cli-command.ts +10 -2
- package/src/harness/harness-queue.ts +388 -33
- package/src/harness/helper-scan.ts +10 -3
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +133 -15
- package/src/intake-plane/adopt-gate.ts +49 -0
- package/src/intake-plane/check.ts +3 -0
- package/src/intake-plane/collect-github.ts +57 -25
- package/src/intake-plane/discord-seat-work.ts +8 -2
- package/src/intake-plane/requirement-funnel.ts +316 -0
- package/src/knowledge/know-find.ts +10 -1
- package/src/loops/budget.ts +36 -0
- package/src/loops/orchestrator/finish-rate.ts +73 -1
- package/src/loops/orchestrator/grid-place.ts +11 -0
- package/src/loops/orchestrator/tick.ts +295 -20
- package/src/maturity/feature-maturity.ts +3 -2
- package/src/nexus/api/decisions-route.ts +11 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +38 -0
- package/src/onboarding/first-chat-gate.ts +29 -0
- package/src/release-loop/checklist.ts +104 -6
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +6 -4
- package/src/release-loop/placement.ts +13 -3
- package/src/release-loop/rubric.ts +80 -0
- package/src/seat-dispatch/seat-ask.ts +79 -16
- package/src/seat-loop/seat-loop.ts +12 -6
- package/src/self-dev/ask-launch-flow.ts +24 -3
- package/src/self-dev/ask-path-candidates.ts +68 -0
- package/src/self-dev/dev-pipeline.ts +4 -0
- package/src/self-dev/draft-triage-rules.ts +1 -1
- package/src/self-dev/orchestrate-cli.ts +22 -0
- package/src/self-dev/round-pr-supersede.ts +244 -0
- package/src/self-dev/run-supervisor.ts +14 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author.ts +8 -3
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +59 -12
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +39 -6
- package/src/task-agent/judge.ts +31 -5
- package/src/task-agent/mission.ts +423 -0
- package/src/task-agent/shadow.ts +90 -0
- package/src/task-agent/task-hand.ts +235 -0
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +10 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +86 -18
- package/src/telegram-loops-command.ts +1 -1
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +63 -3
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4614-21403aac730d4586.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6877-fe94e76a5841ee9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9a41d65355fc25b9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-41afec0b255ed2bb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-de43a186e5ba5683.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-a04260b8ccc968d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-5a64cb5208c6a8fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-1407ec83ea64c1f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-fc2df261f8a2c7df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/yRA5ur6DcPl0YHSRheH2O/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{yRA5ur6DcPl0YHSRheH2O → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync } from 'node:fs';
|
|
1
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync } from 'node:fs';
|
|
2
2
|
import { dlopen, FFIType } from 'bun:ffi';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { listMemories, memoryRoot, saveMemory, type MemoryEntry } from '../memory.js';
|
|
5
5
|
import { DecisionLedger } from '../decisions/decision-ledger.js';
|
|
6
|
+
import { debug } from '../debug/log.js';
|
|
6
7
|
import { condenseContextWindowWithReport, readCondenseEvents, type CondenseFunnel, type CondenseSkipped, type MemoryDeps, type MemoryItem, type MemorySource, type MemorySummary } from './long-term-memory.js';
|
|
7
8
|
|
|
8
9
|
const summarize = async (source: MemorySource): Promise<MemorySummary> =>
|
|
@@ -51,6 +52,7 @@ export function renderCondenseMarkdown(report: Pick<CondenseReport, 'since' | 'u
|
|
|
51
52
|
|
|
52
53
|
type CondenseStore = {
|
|
53
54
|
list(): MemoryEntry[];
|
|
55
|
+
hasMemoryDirectory?(): boolean;
|
|
54
56
|
save(input: Parameters<typeof saveMemory>[0]): MemoryEntry;
|
|
55
57
|
raise(input: Parameters<DecisionLedger['raiseOnce']>[0], ref: string): string;
|
|
56
58
|
withLock<T>(action: () => T): T;
|
|
@@ -96,6 +98,7 @@ function defaultStore(root?: string): CondenseStore {
|
|
|
96
98
|
const ledger = new DecisionLedger(root ? { stateDir: root } : {});
|
|
97
99
|
return {
|
|
98
100
|
list: () => listMemories({}, memoryDir),
|
|
101
|
+
hasMemoryDirectory: () => existsSync(memoryDir),
|
|
99
102
|
save: input => saveMemory(input, memoryDir),
|
|
100
103
|
raise: (input, ref) => ledger.raiseOnce(input, ref).id,
|
|
101
104
|
withLock: action => withMemoryLock(memoryDir, action),
|
|
@@ -118,10 +121,22 @@ function newestActive(entries: readonly MemoryEntry[]): MemoryEntry | undefined
|
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
/** Apply candidate cards through the existing curated memory and decision ledger paths. */
|
|
121
|
-
export function applyCondensedCards(cards: readonly MemoryItem[], store: CondenseStore): NonNullable<CondenseReport['applied']> {
|
|
124
|
+
export function applyCondensedCards(cards: readonly MemoryItem[], store: CondenseStore, now: Date = new Date()): NonNullable<CondenseReport['applied']> {
|
|
122
125
|
const decisions: string[] = [];
|
|
123
126
|
let written = 0;
|
|
124
127
|
let retired = 0;
|
|
128
|
+
const cutoff = now.getTime() - 30 * 24 * 3_600_000;
|
|
129
|
+
const retireAged = () => {
|
|
130
|
+
let agedRetired = 0;
|
|
131
|
+
for (const old of store.list()) {
|
|
132
|
+
if (old.contextCard?.status !== 'active' || !(Date.parse(old.contextCard.updatedAt) < cutoff)) continue;
|
|
133
|
+
store.save({ type: old.type, name: old.name, description: old.description, body: old.body, id: old.id,
|
|
134
|
+
pinned: false, priority: old.priority, contextCard: { ...old.contextCard, status: 'retired' } });
|
|
135
|
+
agedRetired++;
|
|
136
|
+
}
|
|
137
|
+
retired += agedRetired;
|
|
138
|
+
debug.log('context.condense', 'aged-out', { retired: agedRetired });
|
|
139
|
+
};
|
|
125
140
|
for (const card of cards) {
|
|
126
141
|
if (card.conflict) {
|
|
127
142
|
const ref = `context-condense:${JSON.stringify([card.project, card.seat, card.topic, card.source, card.updatedAt])}`;
|
|
@@ -162,6 +177,7 @@ export function applyCondensedCards(cards: readonly MemoryItem[], store: Condens
|
|
|
162
177
|
};
|
|
163
178
|
store.withLock(applyTopic);
|
|
164
179
|
}
|
|
180
|
+
if (store.hasMemoryDirectory?.() !== false) store.withLock(retireAged);
|
|
165
181
|
return { written, retired, decisions };
|
|
166
182
|
}
|
|
167
183
|
|
|
@@ -184,6 +200,6 @@ export async function runContextCondense(options: {
|
|
|
184
200
|
const conflicts = cards.filter(card => card.conflict !== undefined).length;
|
|
185
201
|
const report: CondenseReport = { since, until, cards, conflicts, skipped, folded, droppedHarnessChild, funnel,
|
|
186
202
|
markdown: renderCondenseMarkdown({ since, until, cards, conflicts, skipped, droppedHarnessChild, funnel }) };
|
|
187
|
-
if (options.apply === true) report.applied = applyCondensedCards(cards, store ?? defaultStore(options.root));
|
|
203
|
+
if (options.apply === true) report.applied = applyCondensedCards(cards, store ?? defaultStore(options.root), now);
|
|
188
204
|
return report;
|
|
189
205
|
}
|
|
@@ -12,6 +12,16 @@ export interface ContextDayRow {
|
|
|
12
12
|
status: '들어옴' | '안 들어옴';
|
|
13
13
|
}
|
|
14
14
|
export interface ContextDayReport { since: string; until: string; rows: ContextDayRow[] }
|
|
15
|
+
export interface ContextDayTimelineEvent {
|
|
16
|
+
at: string;
|
|
17
|
+
source: ContextDaySource;
|
|
18
|
+
kind: ContextDayKind | 'other';
|
|
19
|
+
summary: string;
|
|
20
|
+
link: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface ContextDayTimelineReport { since: string; until: string; events: ContextDayTimelineEvent[] }
|
|
23
|
+
interface JournalEvent { surface: string; kind: string; ts: string; refs: string | null; summary: string | null }
|
|
24
|
+
type DayDeps = { now?: () => Date; dbPath?: string; seat?: ContextDaySource };
|
|
15
25
|
|
|
16
26
|
/** Reject malformed windows instead of silently changing the time range being judged. */
|
|
17
27
|
export function contextDayHours(since: string): number {
|
|
@@ -34,43 +44,86 @@ function kindFor(surface: string, kind: string): ContextDayKind | null {
|
|
|
34
44
|
return null;
|
|
35
45
|
}
|
|
36
46
|
|
|
47
|
+
function sourceFor(event: JournalEvent): { source: ContextDaySource; refs: { url?: unknown; source?: unknown } } | null {
|
|
48
|
+
let refs: { seat?: unknown; origin?: unknown; url?: unknown; source?: unknown };
|
|
49
|
+
try { refs = JSON.parse(event.refs ?? '{}') as typeof refs; } catch { return null; }
|
|
50
|
+
if (!refs || typeof refs !== 'object') return null;
|
|
51
|
+
const source = event.surface === 'context:external' ? refs.origin : refs.seat;
|
|
52
|
+
if (typeof source !== 'string' || !CONTEXT_DAY_SOURCES.some(item => item === source)) return null;
|
|
53
|
+
const isExternal = CONTEXT_DAY_SOURCES.indexOf(source as ContextDaySource) >= 4;
|
|
54
|
+
// Do not attribute an external hook to a seat (or a seat event to an external origin).
|
|
55
|
+
if ((event.surface === 'context:external') !== isExternal) return null;
|
|
56
|
+
return { source: source as ContextDaySource, refs };
|
|
57
|
+
}
|
|
58
|
+
|
|
37
59
|
/** Read only the context journal, not recallEvents (which mutates recall counts) or the writable DB opener. */
|
|
38
|
-
|
|
60
|
+
function readDay(since: string, deps: DayDeps): { since: string; until: string; events: JournalEvent[] } {
|
|
39
61
|
const hours = contextDayHours(since);
|
|
40
62
|
const now = (deps.now ?? (() => new Date()))();
|
|
41
63
|
const until = now.toISOString();
|
|
42
64
|
const from = new Date(now.getTime() - hours * 3_600_000).toISOString();
|
|
43
|
-
const rows: ContextDayRow[] = CONTEXT_DAY_SOURCES.map(source => ({
|
|
44
|
-
source, counts: { 'task-claimed': 0, done: 0, asked: 0, 'guidance-changed': 0 }, lastAt: null, status: '안 들어옴',
|
|
45
|
-
}));
|
|
46
|
-
const bySource = new Map(rows.map(row => [row.source, row]));
|
|
47
65
|
const path = deps.dbPath ?? surfaceEventsDbPath();
|
|
48
|
-
if (!existsSync(path)) return { since: from, until,
|
|
66
|
+
if (!existsSync(path)) return { since: from, until, events: [] };
|
|
49
67
|
const db = new Database(path, { readonly: true });
|
|
50
68
|
try {
|
|
51
|
-
const events = db.prepare(`SELECT surface, kind, ts, refs FROM events
|
|
69
|
+
const events = db.prepare(`SELECT surface, kind, ts, refs, summary FROM events
|
|
52
70
|
WHERE surface IN ('coord:channel', 'context:session', 'context:external')
|
|
53
|
-
AND direction='outbound' AND ts>=? AND ts<=? ORDER BY ts ASC`).all(from, until) as
|
|
54
|
-
|
|
55
|
-
}>;
|
|
56
|
-
for (const event of events) {
|
|
57
|
-
let refs: { seat?: unknown; origin?: unknown };
|
|
58
|
-
try { refs = JSON.parse(event.refs ?? '{}') as typeof refs; } catch { continue; }
|
|
59
|
-
const source = event.surface === 'context:external' ? refs.origin : refs.seat;
|
|
60
|
-
if (typeof source !== 'string') continue;
|
|
61
|
-
const row = bySource.get(source as ContextDaySource);
|
|
62
|
-
if (!row) continue;
|
|
63
|
-
// Do not attribute an external hook to a seat (or a seat event to an external origin).
|
|
64
|
-
if ((event.surface === 'context:external') !== (rows.indexOf(row) >= 4)) continue;
|
|
65
|
-
row.lastAt = event.ts;
|
|
66
|
-
row.status = '들어옴';
|
|
67
|
-
const kind = kindFor(event.surface, event.kind);
|
|
68
|
-
if (kind) row.counts[kind]++;
|
|
69
|
-
}
|
|
71
|
+
AND direction='outbound' AND ts>=? AND ts<=? ORDER BY ts ASC`).all(from, until) as JournalEvent[];
|
|
72
|
+
return { since: from, until, events };
|
|
70
73
|
} finally { db.close(); }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function contextDay(since = '24h', deps: { now?: () => Date; dbPath?: string } = {}): ContextDayReport {
|
|
77
|
+
const { since: from, until, events } = readDay(since, deps);
|
|
78
|
+
const rows: ContextDayRow[] = CONTEXT_DAY_SOURCES.map(source => ({
|
|
79
|
+
source, counts: { 'task-claimed': 0, done: 0, asked: 0, 'guidance-changed': 0 }, lastAt: null, status: '안 들어옴',
|
|
80
|
+
}));
|
|
81
|
+
const bySource = new Map(rows.map(row => [row.source, row]));
|
|
82
|
+
for (const event of events) {
|
|
83
|
+
const attribution = sourceFor(event);
|
|
84
|
+
if (!attribution) continue;
|
|
85
|
+
const row = bySource.get(attribution.source)!;
|
|
86
|
+
row.lastAt = event.ts;
|
|
87
|
+
row.status = '들어옴';
|
|
88
|
+
const kind = kindFor(event.surface, event.kind);
|
|
89
|
+
if (kind) row.counts[kind]++;
|
|
90
|
+
}
|
|
71
91
|
return { since: from, until, rows };
|
|
72
92
|
}
|
|
73
93
|
|
|
94
|
+
export function contextDayTimeline(since = '24h', deps: DayDeps = {}): ContextDayTimelineReport {
|
|
95
|
+
if (deps.seat !== undefined && !CONTEXT_DAY_SOURCES.some(source => source === deps.seat)) {
|
|
96
|
+
throw new Error(`unknown --seat: ${deps.seat}`);
|
|
97
|
+
}
|
|
98
|
+
const { since: from, until, events } = readDay(since, deps);
|
|
99
|
+
const timeline: ContextDayTimelineEvent[] = [];
|
|
100
|
+
for (const event of events) {
|
|
101
|
+
const attribution = sourceFor(event);
|
|
102
|
+
if (!attribution || (deps.seat && attribution.source !== deps.seat)) continue;
|
|
103
|
+
const { url, source } = attribution.refs;
|
|
104
|
+
const link = [url, source].find(value => typeof value === 'string' && /^(?:https?:\/\/|elanous:\/\/)[^\s]+$/.test(value));
|
|
105
|
+
timeline.push({ at: event.ts, source: attribution.source, kind: kindFor(event.surface, event.kind) ?? 'other',
|
|
106
|
+
summary: event.summary ?? '', link: typeof link === 'string' ? link : null });
|
|
107
|
+
}
|
|
108
|
+
return { since: from, until, events: timeline };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const kstFormatter = new Intl.DateTimeFormat('en-CA', {
|
|
112
|
+
timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
|
|
113
|
+
});
|
|
114
|
+
function kst(at: string): string {
|
|
115
|
+
const parts = Object.fromEntries(kstFormatter.formatToParts(new Date(at)).map(part => [part.type, part.value]));
|
|
116
|
+
return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute} KST`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function renderContextDayTimeline(report: ContextDayTimelineReport, dbPath = surfaceEventsDbPath()): string {
|
|
120
|
+
const lines = [`맥락 사건 ${kst(report.since)} ~ ${kst(report.until)}`];
|
|
121
|
+
if (!report.events.length) lines.push(`이 창에 사건 0건 — 원장 경로: ${dbPath}`);
|
|
122
|
+
else lines.push(...report.events.map(event => `${kst(event.at).slice(11)} · ${event.source} · ${event.kind} · ${event.summary.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, ' ')} · ${event.link ?? '—'}`));
|
|
123
|
+
lines.push(`${report.events.length}건 · 출처 ${new Set(report.events.map(event => event.source)).size}곳`);
|
|
124
|
+
return lines.join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
74
127
|
export function renderContextDay(report: ContextDayReport): string {
|
|
75
128
|
return [
|
|
76
129
|
`맥락 이벤트 ${report.since} ~ ${report.until} (UTC)`,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ContextNowAnswer, ContextNowDeps } from './context-now.js';
|
|
2
|
+
|
|
3
|
+
const CONTEXT_FIRST_GAP_MS = 6 * 60 * 60 * 1000;
|
|
4
|
+
const CONTEXT_UNREADABLE = '맥락 못 읽음';
|
|
5
|
+
|
|
6
|
+
/** Track every allowed utterance, consuming a pending summary only on a normal turn. */
|
|
7
|
+
export function createContextFirstGate({ now, gapMs = CONTEXT_FIRST_GAP_MS }: { now: () => number; gapMs?: number }) {
|
|
8
|
+
const lastUtteranceAt = new Map<string, number>();
|
|
9
|
+
const pending = new Set<string>();
|
|
10
|
+
return {
|
|
11
|
+
note(key: string): void {
|
|
12
|
+
const at = now();
|
|
13
|
+
const previous = lastUtteranceAt.get(key);
|
|
14
|
+
lastUtteranceAt.set(key, at);
|
|
15
|
+
if (previous === undefined || at - previous >= gapMs) pending.add(key);
|
|
16
|
+
},
|
|
17
|
+
take(key: string): boolean {
|
|
18
|
+
return pending.delete(key);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderContextFirst(
|
|
24
|
+
read: (options: {}, deps?: ContextNowDeps) => ContextNowAnswer,
|
|
25
|
+
deps: ContextNowDeps | undefined,
|
|
26
|
+
render: (answer: ContextNowAnswer) => string,
|
|
27
|
+
): string {
|
|
28
|
+
try {
|
|
29
|
+
return render(read({}, deps));
|
|
30
|
+
} catch {
|
|
31
|
+
return CONTEXT_UNREADABLE;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -65,7 +65,9 @@ import { detectCard, runCardFollowup, NotACardError } from './card-followup/core
|
|
|
65
65
|
import { localOcrText } from './telegram-card-followup.js';
|
|
66
66
|
import type { runGraph } from './graph-runner/runner.js';
|
|
67
67
|
import { readSlashContextNow, renderTelegramNow } from './context-bus/context-now-surfaces.js';
|
|
68
|
-
import type
|
|
68
|
+
import { contextNow, type ContextNowDeps } from './context-bus/context-now.js';
|
|
69
|
+
import { createContextFirstGate, renderContextFirst } from './context-bus/context-first.js';
|
|
70
|
+
import { telegramLoopsStatus, type LoopsSources } from './telegram-loops-command.js';
|
|
69
71
|
import { botUnavailableSlashReply, deriveTuiSlashAvailability, type TuiSlashAvailability } from './maturity/tui-slash-availability.js';
|
|
70
72
|
|
|
71
73
|
const SLASH_FOCUS_TURNS_DEFAULT = 8;
|
|
@@ -143,6 +145,9 @@ export interface DiscordSelfMessageDeps {
|
|
|
143
145
|
log?: (msg: string) => void;
|
|
144
146
|
cardFollowupDeps?: { ocrText?: (path: string) => Promise<string | null>; runGraph?: typeof runGraph; rootDir?: () => string };
|
|
145
147
|
nowDeps?: ContextNowDeps;
|
|
148
|
+
/** Override the clock for the context-first gate in isolated message-path tests. */
|
|
149
|
+
contextFirstNow?: () => number;
|
|
150
|
+
loopsSources?: LoopsSources;
|
|
146
151
|
/** Override the TUI slash projection for isolated message-path tests. */
|
|
147
152
|
tuiSlashAvailability?: readonly TuiSlashAvailability[];
|
|
148
153
|
}
|
|
@@ -151,6 +156,15 @@ export interface DiscordSelfMessageDeps {
|
|
|
151
156
|
export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessageHandler {
|
|
152
157
|
const log = deps.log ?? ((): void => {});
|
|
153
158
|
const cfg = deps.userConfig;
|
|
159
|
+
const contextFirstGate = createContextFirstGate({ now: deps.contextFirstNow ?? Date.now });
|
|
160
|
+
const maybeSendContextFirst = async (channelId: string): Promise<void> => {
|
|
161
|
+
if (!contextFirstGate.take(channelId) || cfg.discord?.contextFirst === false) return;
|
|
162
|
+
try {
|
|
163
|
+
await deps.getBot()?.sendMessage(channelId, renderContextFirst(contextNow, deps.nowDeps, renderTelegramNow));
|
|
164
|
+
} catch (err) {
|
|
165
|
+
log(`discord context-first failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
154
168
|
// C2+ (2026-07-12 dogfood): 디스코드 첨부는 메시지 단위라 "사진 먼저,
|
|
155
169
|
// !cdx 명령은 다음 메시지" 패턴에서 위임 턴이 빈손이 된다. 채널별로
|
|
156
170
|
// 마지막 첨부를 기억해 두고, 첨부 없는 위임 명령이 2분 내 같은
|
|
@@ -359,6 +373,7 @@ export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessa
|
|
|
359
373
|
|
|
360
374
|
return async (ctx, streamer) => {
|
|
361
375
|
if (deps.channelScope && ctx.channelId !== deps.channelScope) return undefined;
|
|
376
|
+
contextFirstGate.note(ctx.channelId);
|
|
362
377
|
// Voice text commands pass the guild gate for the voice adapter's
|
|
363
378
|
// sake — never route them into the LLM turn (legacy behavior).
|
|
364
379
|
if (/^\/voice-(join|leave|status)\b/.test(ctx.text.trim())) return undefined;
|
|
@@ -367,6 +382,8 @@ export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessa
|
|
|
367
382
|
const args = nowCommand[1]?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
368
383
|
return renderTelegramNow(readSlashContextNow(args, deps.nowDeps));
|
|
369
384
|
}
|
|
385
|
+
if (/^[/!]loops$/.test(ctx.text.trim())) return telegramLoopsStatus([], deps.loopsSources);
|
|
386
|
+
if (/^[/!]loops\s+\S/.test(ctx.text.trim())) return telegramLoopsStatus(['extra'], deps.loopsSources);
|
|
370
387
|
if (!ctx.text.trim() && ctx.attachments.length === 1 && ctx.attachments[0]!.contentType?.startsWith('image/')) {
|
|
371
388
|
const photo = ctx.attachments[0]!;
|
|
372
389
|
if (detectCard({ width: photo.width, height: photo.height, ocrText: null }).decision !== 'skip') {
|
|
@@ -408,6 +425,8 @@ export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessa
|
|
|
408
425
|
const unavailable = botUnavailableSlashReply(entry, 'discord');
|
|
409
426
|
if (unavailable) return unavailable;
|
|
410
427
|
}
|
|
428
|
+
const cmd = parseDiscordAcpCommand(ctx.text);
|
|
429
|
+
if (!cmd && !/^[!/]/.test(ctx.text.trim())) await maybeSendContextFirst(ctx.channelId);
|
|
411
430
|
log(`◀ ${ctx.userName ?? ctx.userId}: ${ctx.text.slice(0, 80)}${ctx.attachments.length ? ` (+첨부 ${ctx.attachments.length})` : ''}`);
|
|
412
431
|
debug.log('discord.self.turn', 'inbound', { channelId: ctx.channelId, chars: ctx.text.length, attachments: ctx.attachments.length });
|
|
413
432
|
// C2+ — 첨부 기억(사진만 먼저 올리는 패턴 지원). 명령 여부와 무관.
|
|
@@ -416,7 +435,6 @@ export function buildDiscordSelfOnMessage(deps: DiscordSelfMessageDeps): DcMessa
|
|
|
416
435
|
const delegationKey = delegationChatKey('dc', ctx.channelId);
|
|
417
436
|
|
|
418
437
|
// 1) Explicit text commands — /cc·/cdx·/gem delegate, /brain exits.
|
|
419
|
-
const cmd = parseDiscordAcpCommand(ctx.text);
|
|
420
438
|
if (cmd?.kind === 'brain') {
|
|
421
439
|
clearActiveDelegation(delegationKey);
|
|
422
440
|
return '🧠 브레인 모드로 복귀했습니다 (active delegation 해제).';
|
|
@@ -48,6 +48,7 @@ export const ELANOUS_SLASH_COMMANDS: readonly SlashCommandSchema[] = [
|
|
|
48
48
|
{ name: 'now', description: '지금 판·칸·결정·자리·최근 맥락 보기', options: [
|
|
49
49
|
{ name: 'topic', description: '좁혀 볼 주제 (선택)', type: 3, required: false },
|
|
50
50
|
] },
|
|
51
|
+
{ name: 'loops', description: '루프·크론 현황 (늦음·실패 먼저)' },
|
|
51
52
|
{ name: 'sessions', description: '바인딩된 세션 목록' },
|
|
52
53
|
{ name: 'new', description: '새 세션으로 시작 (이전 대화 보존)' },
|
|
53
54
|
{ name: 'attach', description: '이 채널을 다른 세션에 연결', options: [
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// send.py 와 동일 동작을 TS 로 포팅(완전 elanous 소유). sync(curl) 계약.
|
|
6
6
|
|
|
7
7
|
import * as childProcess from 'node:child_process';
|
|
8
|
-
import { existsSync, readFileSync, appendFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
9
|
-
import { dirname, join, relative } from 'node:path';
|
|
8
|
+
import { existsSync, readFileSync, appendFileSync, mkdirSync, unlinkSync, readdirSync, renameSync, openSync, closeSync, statSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { basename, dirname, join, relative } from 'node:path';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
12
12
|
import { conatusEnv } from './conatus-env.js';
|
|
@@ -138,11 +138,11 @@ export function userRecentlyActive(now: Date = new Date()): boolean {
|
|
|
138
138
|
|
|
139
139
|
/** 야간 보류 적재 (jsonl append — 크론 동시 실행에 안전). origin 있으면 함께 적재 —
|
|
140
140
|
* 아침 flush 가 그 origin(발신 채널)으로 되돌려 발송(없으면 report 묶음). */
|
|
141
|
-
function deferOutbound(text: string, kind: string, origin?: MissionOrigin | null, path = DEFERRED_PATH): boolean {
|
|
141
|
+
export function deferOutbound(text: string, kind: string, origin?: MissionOrigin | null, path = DEFERRED_PATH): boolean {
|
|
142
142
|
try {
|
|
143
143
|
mkdirSync(dirname(path), { recursive: true });
|
|
144
144
|
const rec = { ts: new Date().toISOString(), kind, text, ...(origin ? { origin } : {}) };
|
|
145
|
-
appendFileSync(path, JSON.stringify(rec) + '\n');
|
|
145
|
+
withQueueLock(path, () => appendFileSync(path, JSON.stringify(rec) + '\n'));
|
|
146
146
|
console.error(`[outbound] 야간 무음(00:00~06:30 KST) — 보류 적재 (${kind})`);
|
|
147
147
|
return true;
|
|
148
148
|
} catch (e) {
|
|
@@ -198,32 +198,195 @@ function deliverToOrigin(origin: MissionOrigin, text: string, onBot?: (bot: stri
|
|
|
198
198
|
try { return sendTelegramRaw(token, origin.chatId, text, origin.threadId); } catch { return false; }
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
/**
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
/** 보류 큐 재시도 상한 env — config `outbound.deferredMaxAttempts` 가 없을 때만 읽는다. */
|
|
202
|
+
export const DEFERRED_MAX_ATTEMPTS_ENV = 'ELANOUS_OUTBOUND_DEFERRED_MAX_ATTEMPTS';
|
|
203
|
+
|
|
204
|
+
/** 보류 항목이 격리되기 전까지 허용하는 실패 flush 수 — config > env > 5. */
|
|
205
|
+
export function deferredMaxAttempts(config?: Pick<UserConfig, 'outbound'>): number {
|
|
206
|
+
try {
|
|
207
|
+
const v = (config ?? getUserConfig()).outbound?.deferredMaxAttempts;
|
|
208
|
+
if (typeof v === 'number' && Number.isSafeInteger(v) && v > 0) return v;
|
|
209
|
+
} catch { /* unreadable config: env, then default */ }
|
|
210
|
+
const raw = Number(process.env[DEFERRED_MAX_ATTEMPTS_ENV]?.trim() || NaN);
|
|
211
|
+
if (Number.isSafeInteger(raw) && raw > 0) return raw;
|
|
212
|
+
return 5;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** 격리 파일 — 보류 큐 옆 `<name>.failed.jsonl`. */
|
|
216
|
+
export function deferredQuarantinePath(path: string): string {
|
|
217
|
+
return path.endsWith('.jsonl') ? `${path.slice(0, -'.jsonl'.length)}.failed.jsonl` : `${path}.failed`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function pidAlive(pid: number): boolean {
|
|
221
|
+
try { process.kill(pid, 0); return true; } catch (e) { return (e as NodeJS.ErrnoException).code === 'EPERM'; }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** 큐 적재·claim 이 서로를 가로지르지 않게 하는 짧은 배타 잠금(`<queue>.lock`, O_EXCL).
|
|
225
|
+
* 적재는 open→write→close 를 잠금 안에서 하므로 claim 의 rename 이 «열린 채 쓰기 전»인 적재를 가져가지 않는다.
|
|
226
|
+
* 잠금을 못 얻으면(2초 · 30초 넘은 잔여 잠금은 걷는다) 그대로 진행한다 — 알림을 잃는 것보다 낫다. */
|
|
227
|
+
function withQueueLock<T>(path: string, fn: () => T): T {
|
|
228
|
+
const lock = `${path}.lock`;
|
|
229
|
+
const deadline = Date.now() + 2_000;
|
|
230
|
+
let fd: number | null = null;
|
|
231
|
+
while (fd === null && Date.now() <= deadline) {
|
|
232
|
+
try { fd = openSync(lock, 'wx'); break; } catch (e) {
|
|
233
|
+
const code = (e as NodeJS.ErrnoException).code;
|
|
234
|
+
if (code === 'ENOENT') { try { mkdirSync(dirname(path), { recursive: true }); } catch { break; } continue; }
|
|
235
|
+
if (code !== 'EEXIST') break;
|
|
236
|
+
try { if (Date.now() - statSync(lock).mtimeMs > 30_000) { unlinkSync(lock); continue; } } catch { continue; }
|
|
237
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (fd === null) { try { debug.log('outbound.send', 'deferred-lock-unavailable', { lock: basename(lock) }); } catch { /* */ } }
|
|
241
|
+
try { return fn(); } finally {
|
|
242
|
+
if (fd !== null) { try { closeSync(fd); } catch { /* */ } try { unlinkSync(lock); } catch { /* */ } }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 이보다 오래된 claim 은 소유 pid 가 살아 있어도 회수한다(pid 재사용 대비 · 정상 flush 는 이보다 훨씬 짧다). */
|
|
247
|
+
const STALE_CLAIM_MS = 15 * 60_000;
|
|
248
|
+
|
|
249
|
+
let claimSeq = 0;
|
|
250
|
+
/** 큐를 «가져간다» — 원자적 rename 으로 이 flush 만의 파일로 옮긴다. flush 도중 적재되는 항목은 새 큐 파일에
|
|
251
|
+
* 쌓이므로 잃지 않는다. 죽은 flusher(또는 이 프로세스의 앞 호출)가 남긴 claim 도 같이 회수한다. */
|
|
252
|
+
function claimDeferred(path: string): string[] {
|
|
253
|
+
const claims: string[] = [];
|
|
254
|
+
const next = (): string => `${path}.flushing-${process.pid}-${Date.now()}-${claimSeq++}`;
|
|
255
|
+
const dir = dirname(path);
|
|
256
|
+
const prefix = `${basename(path)}.flushing-`;
|
|
257
|
+
try {
|
|
258
|
+
for (const name of readdirSync(dir)) {
|
|
259
|
+
if (!name.startsWith(prefix)) continue;
|
|
260
|
+
const m = /^(\d+)-(\d+)-/.exec(name.slice(prefix.length));
|
|
261
|
+
const pid = Number(m?.[1]);
|
|
262
|
+
const at = Number(m?.[2]);
|
|
263
|
+
if (!Number.isSafeInteger(pid)) continue;
|
|
264
|
+
// flushDeferred 는 동기라 같은 pid 의 claim 은 «지난 호출의 잔여»다. 남의 것은 죽었거나 오래됐을 때만.
|
|
265
|
+
const stale = Number.isFinite(at) && Date.now() - at > STALE_CLAIM_MS;
|
|
266
|
+
if (pid !== process.pid && !stale && pidAlive(pid)) continue;
|
|
267
|
+
const own = next();
|
|
268
|
+
try { renameSync(join(dir, name), own); claims.push(own); } catch { /* another flusher took it */ }
|
|
269
|
+
}
|
|
270
|
+
} catch { /* no directory yet */ }
|
|
271
|
+
withQueueLock(path, () => {
|
|
272
|
+
if (!existsSync(path)) return;
|
|
273
|
+
const own = next();
|
|
274
|
+
try { renameSync(path, own); claims.push(own); } catch { /* another flusher took it */ }
|
|
275
|
+
});
|
|
276
|
+
return claims;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** flushDeferred 주입점 — 시험은 실제 발송 대신 sendBatch 를 준다. */
|
|
280
|
+
export type FlushDeferredDeps = {
|
|
281
|
+
sendBatch?: (text: string, kind: string, origin?: MissionOrigin) => boolean;
|
|
282
|
+
maxAttempts?: number;
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
/** 보류분 일괄 발송 — 묶음 단위. 배달 건수 반환.
|
|
286
|
+
* 무음 창 밖 첫 sendOutbound 가 자동 호출 + 06:31 플러시 크론이 보장.
|
|
287
|
+
* ⭐ 실패한 묶음의 항목만 큐에 남긴다(성공 묶음은 다시 보내지 않는다 · 10-07 야간 11회 재발송 사고).
|
|
288
|
+
* 실패가 `maxAttempts` 번 쌓인 항목은 `<name>.failed.jsonl` 로 격리한다. */
|
|
289
|
+
export function flushDeferred(path = DEFERRED_PATH, deps: FlushDeferredDeps = {}): number {
|
|
204
290
|
ensureOutboundLogSink();
|
|
205
|
-
const observeFlush = (
|
|
291
|
+
const observeFlush = (
|
|
292
|
+
count: number, lagMin: number, kinds: string[],
|
|
293
|
+
outcome: { sent: number; kept: number; quarantined: number },
|
|
294
|
+
): void => {
|
|
206
295
|
const lagWarnMin = flushLagWarnMin();
|
|
207
296
|
const over = count > 0 && lagMin > lagWarnMin;
|
|
208
297
|
logSend(
|
|
209
298
|
'flush',
|
|
210
299
|
'deferred-batch',
|
|
211
|
-
{ count, lagMin, kinds: kinds.map(safeObservationLabel), path, lagWarnMin },
|
|
300
|
+
{ count, lagMin, kinds: kinds.map(safeObservationLabel), path, lagWarnMin, ...outcome },
|
|
212
301
|
over ? { level: 'warn' } : undefined,
|
|
213
302
|
);
|
|
214
303
|
};
|
|
215
|
-
|
|
216
|
-
|
|
304
|
+
const none = { sent: 0, kept: 0, quarantined: 0 };
|
|
305
|
+
const claims = claimDeferred(path);
|
|
306
|
+
if (claims.length === 0) {
|
|
307
|
+
observeFlush(0, 0, [], none);
|
|
217
308
|
return 0;
|
|
218
309
|
}
|
|
219
|
-
type Item = { ts: string; kind: string; text: string; origin?: MissionOrigin };
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
310
|
+
type Item = { ts: string; kind: string; text: string; origin?: MissionOrigin; attempts?: number };
|
|
311
|
+
const items: Item[] = [];
|
|
312
|
+
const corrupt: string[] = [];
|
|
313
|
+
const readClaims: string[] = [];
|
|
314
|
+
for (const claim of claims) {
|
|
315
|
+
let body = '';
|
|
316
|
+
try { body = readFileSync(claim, 'utf-8'); } catch (e) {
|
|
317
|
+
// 못 읽은 claim 은 지우지 않는다 — 다음 flush 가 다시 회수한다.
|
|
318
|
+
try { debug.log('outbound.send', 'deferred-claim-unreadable', { error: e instanceof Error ? e.message : String(e) }, { level: 'warn' }); } catch { /* */ }
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
readClaims.push(claim);
|
|
322
|
+
for (const line of body.split('\n')) {
|
|
323
|
+
if (!line.trim()) continue;
|
|
324
|
+
try {
|
|
325
|
+
const rec = JSON.parse(line) as Item;
|
|
326
|
+
if (rec && typeof rec === 'object' && typeof rec.text === 'string') items.push(rec);
|
|
327
|
+
else corrupt.push(line);
|
|
328
|
+
} catch { corrupt.push(line); }
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const qPath = deferredQuarantinePath(path);
|
|
332
|
+
const settleFailed = (stage: string, e: unknown): void => {
|
|
333
|
+
try { debug.log('outbound.send', 'deferred-settle-failed', { stage, error: e instanceof Error ? e.message : String(e) }, { level: 'warn' }); } catch { /* */ }
|
|
334
|
+
};
|
|
335
|
+
/** 읽은 claim 을 정산한다. 격리 기록이 실패하면 그 줄은 큐로 돌린다(다음에 다시 격리).
|
|
336
|
+
* 큐 기록이 실패하면 claim 하나를 «남길 줄만»으로 바꿔 두어 다음 flush 가 배달분을 다시 보내지 않게 한다. */
|
|
337
|
+
/** 정산된 claim 을 걷는다. 삭제가 실패하면 비워 둔다 — 남은 claim 이 다음 flush 에 배달분을 다시 내지 않게. */
|
|
338
|
+
const retireClaim = (claim: string): void => {
|
|
339
|
+
try { unlinkSync(claim); return; } catch (e) { settleFailed('claim-unlink', e); }
|
|
340
|
+
try { writeFileSync(claim, ''); } catch (e) { settleFailed('claim-truncate', e); }
|
|
341
|
+
};
|
|
342
|
+
type Quarantine = { line: string; kind?: string; attempts?: number };
|
|
343
|
+
/** 반환 = 실제 정산 결과(관측은 이것으로 센다). */
|
|
344
|
+
const settle = (keep: string[], quarantine: Quarantine[]): { kept: number; quarantined: number } => {
|
|
345
|
+
let back = keep;
|
|
346
|
+
let quarantined = 0;
|
|
347
|
+
if (quarantine.length) {
|
|
348
|
+
try {
|
|
349
|
+
appendFileSync(qPath, quarantine.map(q => q.line).join('\n') + '\n');
|
|
350
|
+
quarantined = quarantine.length;
|
|
351
|
+
for (const q of quarantine) {
|
|
352
|
+
if (q.kind === undefined) continue;
|
|
353
|
+
try { debug.log('outbound.send', 'deferred-quarantined', { kind: safeObservationLabel(q.kind), attempts: q.attempts }); } catch { /* */ }
|
|
354
|
+
}
|
|
355
|
+
} catch (e) { settleFailed('quarantine', e); back = [...keep, ...quarantine.map(q => q.line)]; }
|
|
356
|
+
}
|
|
357
|
+
const result = { kept: back.length, quarantined };
|
|
358
|
+
if (readClaims.length === 0) return result;
|
|
359
|
+
let wrote = back.length === 0;
|
|
360
|
+
if (!wrote) {
|
|
361
|
+
try { withQueueLock(path, () => appendFileSync(path, back.join('\n') + '\n')); wrote = true; } catch (e) { settleFailed('queue', e); }
|
|
362
|
+
}
|
|
363
|
+
if (!wrote) {
|
|
364
|
+
const first = readClaims[0]!;
|
|
365
|
+
const tmp = join(dirname(first), `.${basename(first)}.tmp`);
|
|
366
|
+
try {
|
|
367
|
+
writeFileSync(tmp, back.join('\n') + '\n');
|
|
368
|
+
renameSync(tmp, first);
|
|
369
|
+
for (const claim of readClaims.slice(1)) retireClaim(claim);
|
|
370
|
+
} catch (e) { settleFailed('claim-rewrite', e); }
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
for (const claim of readClaims) retireClaim(claim);
|
|
374
|
+
return result;
|
|
375
|
+
};
|
|
376
|
+
const maxAttempts = deps.maxAttempts ?? deferredMaxAttempts();
|
|
377
|
+
const priorAttempts = (it: Item): number =>
|
|
378
|
+
typeof it.attempts === 'number' && Number.isSafeInteger(it.attempts) && it.attempts > 0 ? it.attempts : 0;
|
|
379
|
+
const quarantine: Quarantine[] = corrupt.map(line => ({ line }));
|
|
380
|
+
// 이미 상한에 닿은 줄(앞 flush 의 격리 기록이 실패해 큐로 돌아온 것)은 보내지 않고 다시 격리만 시도한다.
|
|
381
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
382
|
+
const it = items[i]!;
|
|
383
|
+
if (priorAttempts(it) < maxAttempts) continue;
|
|
384
|
+
quarantine.push({ line: JSON.stringify(it), kind: it.kind, attempts: priorAttempts(it) });
|
|
385
|
+
items.splice(i, 1);
|
|
386
|
+
}
|
|
224
387
|
if (items.length === 0) {
|
|
225
|
-
|
|
226
|
-
observeFlush(0, 0, []);
|
|
388
|
+
const settled = settle([], quarantine);
|
|
389
|
+
observeFlush(0, 0, [], { ...none, ...settled });
|
|
227
390
|
return 0;
|
|
228
391
|
}
|
|
229
392
|
const kst = (iso: string) => {
|
|
@@ -236,8 +399,7 @@ export function flushDeferred(path = DEFERRED_PATH): number {
|
|
|
236
399
|
`🌙 야간 보류 알림 ${list.length}건 (00:00~06:30 KST 무음 · ⏳ 최대 ${lagMin}분 지연 — 일괄 전달)`,
|
|
237
400
|
...list.map(i => `\n── [${kst(i.ts)} · ${i.kind}] ──\n${i.text}`),
|
|
238
401
|
].join('\n');
|
|
239
|
-
|
|
240
|
-
const sendBatch = (text: string, kind: string, origin?: MissionOrigin): boolean => {
|
|
402
|
+
const sendBatch = deps.sendBatch ?? ((text: string, kind: string, origin?: MissionOrigin): boolean => {
|
|
241
403
|
let bot = 'unknown';
|
|
242
404
|
const path = origin
|
|
243
405
|
? (deliverToOrigin(origin, text, label => { bot = label; }) ? 'origin' : false)
|
|
@@ -250,15 +412,17 @@ export function flushDeferred(path = DEFERRED_PATH): number {
|
|
|
250
412
|
});
|
|
251
413
|
} catch { /* observation must not change delivery */ }
|
|
252
414
|
return path !== false;
|
|
415
|
+
});
|
|
416
|
+
const failed: Item[] = [];
|
|
417
|
+
const send = (batch: Item[], kind: string, origin?: MissionOrigin): void => {
|
|
418
|
+
if (!sendBatch(fmt(batch), kind, origin)) failed.push(...batch);
|
|
253
419
|
};
|
|
254
|
-
let delivered = 0;
|
|
255
420
|
// origin 없는 매매 알림만 report 묶음. 그 외는 kind 별로 운영 봇 경계를 유지한다.
|
|
256
421
|
const noOrigin = items.filter(i => !i.origin);
|
|
257
422
|
const tradingBatch = noOrigin.filter(i => !isOperationalKind(i.kind));
|
|
258
|
-
if (tradingBatch.length
|
|
423
|
+
if (tradingBatch.length) send(tradingBatch, 'report');
|
|
259
424
|
for (const kind of new Set(noOrigin.filter(i => isOperationalKind(i.kind)).map(i => i.kind))) {
|
|
260
|
-
|
|
261
|
-
if (sendBatch(fmt(batch), kind)) delivered += batch.length;
|
|
425
|
+
send(noOrigin.filter(i => i.kind === kind), kind);
|
|
262
426
|
}
|
|
263
427
|
// origin 있는 것 = 발신 채널·스레드별로 묶어 서로 다른 수신자에게 섞이지 않게 발송.
|
|
264
428
|
const groups = new Map<string, { origin: MissionOrigin; list: Item[] }>();
|
|
@@ -272,10 +436,19 @@ export function flushDeferred(path = DEFERRED_PATH): number {
|
|
|
272
436
|
}
|
|
273
437
|
for (const g of groups.values()) {
|
|
274
438
|
const kinds = new Set(g.list.map(i => i.kind));
|
|
275
|
-
|
|
439
|
+
send(g.list, kinds.size === 1 ? g.list[0]!.kind : 'mixed', g.origin);
|
|
440
|
+
}
|
|
441
|
+
// 실패 묶음의 항목만 되돌린다(시도 수 +1). 상한에 닿은 항목은 격리 — 영원히 재시도하지 않는다.
|
|
442
|
+
const keep: string[] = [];
|
|
443
|
+
for (const it of failed) {
|
|
444
|
+
const attempts = priorAttempts(it) + 1;
|
|
445
|
+
const line = JSON.stringify({ ...it, attempts });
|
|
446
|
+
if (attempts >= maxAttempts) quarantine.push({ line, kind: it.kind, attempts });
|
|
447
|
+
else keep.push(line);
|
|
276
448
|
}
|
|
277
|
-
|
|
278
|
-
|
|
449
|
+
const settled = settle(keep, quarantine);
|
|
450
|
+
const delivered = items.length - failed.length;
|
|
451
|
+
observeFlush(items.length, lagMin, [...new Set(items.map(i => i.kind))], { sent: delivered, ...settled });
|
|
279
452
|
return delivered;
|
|
280
453
|
}
|
|
281
454
|
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import { sendOutbound, setInProcessOutbound } from './outbound-alert.js';
|
|
1
|
+
import { inQuietHours, kstMinutes, sendOutbound, setInProcessOutbound } from './outbound-alert.js';
|
|
2
2
|
import { setUserConfigOverlay } from '../user-config.js';
|
|
3
3
|
|
|
4
4
|
// Focused subprocess fixture: isolated state/config roots and a fake curl on PATH.
|
|
5
5
|
setUserConfigOverlay(cfg => ({ ...cfg, telegram: { ...cfg.telegram, botToken: '123456:private-secret', homeChannel: 98765 } }));
|
|
6
6
|
process.env.SEND_VIA_ELANOUS = '0';
|
|
7
|
+
// Quiet hours (00:00~06:30 KST) defer instead of sending, so a gate run at night saw no `sent` row.
|
|
8
|
+
// Shift this fixture's clock back to 23:59 KST of the previous day; the shift stays inside this process.
|
|
9
|
+
const quietShiftMs = inQuietHours() ? (kstMinutes() + 1) * 60_000 : 0;
|
|
10
|
+
if (quietShiftMs > 0) {
|
|
11
|
+
const RealDate = Date;
|
|
12
|
+
const shifted = () => RealDate.now() - quietShiftMs;
|
|
13
|
+
class ShiftedDate extends RealDate {
|
|
14
|
+
constructor(...args: unknown[]) {
|
|
15
|
+
if (args.length === 0) super(shifted());
|
|
16
|
+
else super(...(args as [string | number | Date]));
|
|
17
|
+
}
|
|
18
|
+
static override now(): number { return shifted(); }
|
|
19
|
+
}
|
|
20
|
+
globalThis.Date = ShiftedDate as DateConstructor;
|
|
21
|
+
}
|
|
7
22
|
setInProcessOutbound(null);
|
|
8
23
|
if (!sendOutbound('SECRET-BODY-DO-NOT-LOG', 'ops-alert', { channel: 'cli', surface: process.env.OUTBOUND_PROBE_SOURCE ?? 'outbound-log-probe' })) process.exitCode = 1;
|
|
@@ -2,7 +2,7 @@ import type { LLMToolSpec } from '../llm.js';
|
|
|
2
2
|
import type { ToolRuntimeContext } from '../tool-runtime/types.js';
|
|
3
3
|
import { debug } from '../debug/log.js';
|
|
4
4
|
import { requestConfirmation, type ConfirmOpts, type ConfirmResult } from '../hitl/confirm.js';
|
|
5
|
-
import { devVersion, listChecklist, summarizeChecklist, type Checklist } from '../release-loop/checklist.js';
|
|
5
|
+
import { devVersion, checklistDevVersion, listChecklist, summarizeChecklist, type Checklist } from '../release-loop/checklist.js';
|
|
6
6
|
import { add, move, validateVersion } from '../release-loop/feature-store.js';
|
|
7
7
|
import { formatKst, getSchedule, setSchedule, type ReleaseSchedule } from '../release-loop/release-schedule.js';
|
|
8
8
|
import { latestGraphRun, type GraphRunState } from '../graph-runner/runner.js';
|
|
@@ -64,7 +64,7 @@ function dDay(days: number): string {
|
|
|
64
64
|
export function dispatchReleaseStatus(args: Record<string, unknown>, deps: ReleaseToolDeps = {}): { text: string; structured: Record<string, unknown> } {
|
|
65
65
|
const requested = args.version;
|
|
66
66
|
if (requested !== undefined && (typeof requested !== 'string' || !requested.trim())) throw new Error('판 이름이 필요합니다');
|
|
67
|
-
const version = requested ? null : (deps.devVersion?.() ?? devVersion());
|
|
67
|
+
const version = requested ? null : checklistDevVersion(deps.devVersion?.() ?? devVersion());
|
|
68
68
|
const versions = requested ? [requested as string] : [version!, nextVersion(version!)];
|
|
69
69
|
const now = deps.now ?? new Date();
|
|
70
70
|
const run = (deps.latestRun ?? latestGraphRun)('release-loop');
|