elanous 0.2.16 → 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-b46d45b705e8c9ae.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/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-d92f7ee3994e84c7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3cb52e5ea8fc2fa5.js → page-4733c967b56a4277.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-d22fd849cc22f104.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-c1e20d7d338713f1.js → page-bb742f995c6769e7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-482c5dbde699947c.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-a0eb02638b1e6f31.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a0e2a6d1bfb52fc5.js → page-308111abadf35670.js} +1 -1
- 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-c53598cfce6156f2.js → page-5d97b2901e0d1ea6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-13a63953f53c6a23.js → page-c39554c321576982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-5d3c8429d04f3831.js → page-8c3d8e63ae2e1c98.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-3193bdc0bbab047a.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-6107ca8ddd5e3bdd.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-c130b63862437814.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-fa244bd146a00ae8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-44d56d6391965a6a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-4167348049166852.js → page-83a700d4c754d802.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-de52e8fdf9095f7a.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-ca3952cd78a83ca2.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-e8e1ecaea7cd71d2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-a1b7ffed710a93e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-1ebfe049c640e171.js → page-1467047104141297.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-2da6f9b8a38c6978.js +1 -0
- 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-abe8e3b74bea1ffc.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-d28573d6792b4275.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-d751de5f8062ff5e.js → page-b17cd46cf0e9b8cf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-fe1759f94003b32b.js → page-61553851aa606146.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-d64c984e855c8c68.js → page-a1f585e154e61c59.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-7bf54c9bb9274af8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-9afdee66b820c76e.js → page-5150ef17b5383a57.js} +1 -1
- 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-3544f154c87b43ee.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/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- 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/__fixtures__/intake-news.xml +7 -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/intake-news.ts +169 -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 +77 -11
- 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 +10 -3
- package/scripts/release-loop/release-docs.ts +134 -0
- 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 +149 -41
- package/scripts/release-story/draft.ts +42 -14
- 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/driver.ts +53 -10
- 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/autopilot/track-agent.ts +5 -3
- package/src/briefing/brief-items.ts +86 -17
- package/src/cli/autopilot-cli.ts +86 -1
- package/src/cli/brief-cli.ts +36 -2
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/freeze-cli.ts +3 -2
- package/src/cli/intake-cli.ts +74 -2
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/notify-cli.ts +75 -0
- package/src/cli/pr-cli.ts +12 -1
- package/src/cli/provider-cli.ts +24 -0
- package/src/cli/release-cli.ts +101 -17
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +200 -1
- 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/context-card/index.ts +45 -0
- package/src/context-card/task.ts +76 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +2 -0
- package/src/decisions/decision-ledger.ts +10 -1
- 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 +151 -11
- package/src/harness/harness-pod-dispatch.ts +11 -0
- package/src/harness/harness-queue.ts +466 -32
- package/src/harness/helper-scan.ts +10 -3
- package/src/harness/nested-elanous-policy.ts +23 -4
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +8 -0
- package/src/hq/seats.ts +369 -0
- package/src/index.ts +190 -23
- 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/digest.ts +39 -4
- 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/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/loop-edges.ts +69 -13
- package/src/nexus/api/ops-seats-sources.ts +2 -2
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-templates.ts +3 -0
- package/src/oauth/codex.ts +120 -2
- 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 +24 -14
- 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 +107 -37
- package/src/self-dev/ask-launch-flow.ts +50 -10
- 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-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +21 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/follow-up-goals.ts +2 -2
- package/src/self-implement/frozen-merges.ts +25 -20
- package/src/self-implement/gate-remote.ts +427 -0
- package/src/self-implement/goal-author-clarification.ts +81 -0
- package/src/self-implement/goal-author-cli.ts +33 -5
- package/src/self-implement/goal-author.ts +93 -12
- package/src/self-implement/goal-clarification-escalation.ts +37 -1
- package/src/self-implement/goal-document-write.ts +28 -0
- package/src/self-implement/heal-intake.ts +4 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/l8-merge-queue.ts +29 -5
- package/src/self-implement/orchestrator.ts +78 -13
- package/src/self-implement/prior-draft-findings.ts +4 -3
- package/src/skills/skill-feedback.ts +76 -0
- package/src/task-agent/actions.ts +151 -0
- package/src/task-agent/judge.ts +122 -0
- 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-cards/card-store.ts +4 -1
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +43 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-pool.ts +81 -13
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +225 -35
- package/src/telegram-commands.ts +1 -1
- package/src/telegram-loops-command.ts +22 -15
- package/src/telegram.ts +5 -20
- package/src/user-config.ts +86 -4
- 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-7c6208705c284124.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/8827-e2f5b09a8d37e83b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8949-ecb4128a0858b911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9247-29d889b809288e65.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-8c2d9526164f5d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-dce686e582c23937.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1a61c7ca1ceb3ae7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-51f811fa119308b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-5b042736aa113756.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-57924393e3e4e539.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-0f5cd57ca928f42b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-59f03fda76c03d22.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-1ea64acadd704ace.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +0 -1
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +0 -1
- package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{rv9cierV0jSOvDBm1g0XB → P6YTVgzuZz5oTZhY4Din_}/_ssgManifest.js +0 -0
|
@@ -97,3 +97,23 @@ export function parse(body: string): PrCommentMeta | null {
|
|
|
97
97
|
}
|
|
98
98
|
return parsed;
|
|
99
99
|
}
|
|
100
|
+
|
|
101
|
+
/** 상태 댓글의 details 에 쌓인 라운드 기록을 개별 댓글 본문으로 펼친다. */
|
|
102
|
+
export function expandRecords(body: string): string[] {
|
|
103
|
+
if (!/^<!-- elanous:run-status -->(?:\r?\n|$)/.test(body)) return [body];
|
|
104
|
+
|
|
105
|
+
const records: string[] = [];
|
|
106
|
+
for (const details of body.matchAll(/<details(?:\s[^>]*)?>([\s\S]*?)<\/details>/g)) {
|
|
107
|
+
const content = details[1];
|
|
108
|
+
const headers: Array<{ index: number; round?: number }> = [];
|
|
109
|
+
for (const header of content.matchAll(/^<!-- elanous-pr-comment v1 [^\r\n]* -->$/gm)) {
|
|
110
|
+
const meta = parse(header[0]);
|
|
111
|
+
if (meta) headers.push({ index: header.index, round: meta.round });
|
|
112
|
+
}
|
|
113
|
+
for (let i = 0; i < headers.length; i++) {
|
|
114
|
+
if (headers[i].round === undefined) continue;
|
|
115
|
+
records.push(content.slice(headers[i].index, headers[i + 1]?.index ?? content.length).replace(/(?:\r?\n)+$/, ''));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return records.length ? records : [body];
|
|
119
|
+
}
|
|
@@ -67,8 +67,10 @@ export interface LlmMergeOutcome {
|
|
|
67
67
|
testDeclarationUnmeasured?: string[];
|
|
68
68
|
/** 해결기 응답이 LLM 공급자 오류 문구였다 — 파일 내용이 아니다(🅢 2026-09-27 #21086: 세 파일이 통째로 그 문구가 됐다). */
|
|
69
69
|
providerFailure?: string[];
|
|
70
|
-
/**
|
|
71
|
-
reason?: 'conflict-input-too-large' | 'conflict-resolver-interrupted';
|
|
70
|
+
/** 충돌 미해결의 원인. 기존 두 원인에 마커 잔존·파일 읽기 실패를 추가한다. */
|
|
71
|
+
reason?: 'conflict-input-too-large' | 'conflict-resolver-interrupted' | 'conflict-markers-remain' | 'conflict-file-unreadable';
|
|
72
|
+
/** 해결이 멎은 충돌 파일(worktree 상대 경로). */
|
|
73
|
+
failedFile?: string;
|
|
72
74
|
inputChars?: number;
|
|
73
75
|
/** 병합 결과가 양쪽 판 중 작은 쪽의 절반 아래로 줄었다 — 통째로 날린 것으로 보고 해결 실패로 친다. */
|
|
74
76
|
sizeCollapse?: Array<{ file: string; ours: number; theirs: number; merged: number }>;
|
|
@@ -107,6 +109,20 @@ function addSizeChange(size: LlmMergeSizeChange, file: string, before: string, a
|
|
|
107
109
|
|
|
108
110
|
export function formatLlmMergeOutcome(outcome: LlmMergeOutcome): string {
|
|
109
111
|
const files = outcome.resolvedFiles?.length ? ` (LLM 종합 ${outcome.resolvedFiles.length}파일: ${outcome.resolvedFiles.join(', ')})` : '';
|
|
112
|
+
if (outcome.status === 'conflict-unresolved') {
|
|
113
|
+
// ⛔ reason·failedFile 은 «선택» 칸이다 — 공급자 오류·시험 선언 유실·크기 붕괴 출구는 계약상 reason 이 없다.
|
|
114
|
+
// 있는 칸만 싣고, 그 출구들은 자기 실제 결과(어느 판정이 멈췄나)를 싣는다 — 「사유 undefined」 금지.
|
|
115
|
+
const parts = [
|
|
116
|
+
...(outcome.reason ? [`사유 ${outcome.reason}`] : []),
|
|
117
|
+
...(outcome.failedFile ? [`파일 ${outcome.failedFile}`] : []),
|
|
118
|
+
...(outcome.providerFailure?.length ? [`공급자 오류 문구 ${outcome.providerFailure.join(', ')}`] : []),
|
|
119
|
+
...(outcome.testDeclarationLoss?.length
|
|
120
|
+
? [`시험 선언 유실 ${outcome.testDeclarationLoss.map((l) => `${l.file} ${l.ours}/${l.theirs}→${l.merged}`).join(', ')}`] : []),
|
|
121
|
+
...(outcome.sizeCollapse?.length
|
|
122
|
+
? [`크기 붕괴 ${outcome.sizeCollapse.map((c) => `${c.file} ${c.ours}/${c.theirs}→${c.merged}줄`).join(', ')}`] : []),
|
|
123
|
+
];
|
|
124
|
+
return `${outcome.status}${files}${parts.length ? `; ${parts.join(' · ')}` : ''}`;
|
|
125
|
+
}
|
|
110
126
|
if (outcome.status !== 'llm-resolved') return `${outcome.status}${files}`;
|
|
111
127
|
const size = outcome.sizeChange;
|
|
112
128
|
if (size === undefined) return `${outcome.status}${files}; 규모 변화: 못 쟀다`;
|
|
@@ -211,6 +227,10 @@ export async function mergeMainWithLlmResolve(
|
|
|
211
227
|
...outcome,
|
|
212
228
|
...(testDeclarationUnmeasured.length ? { testDeclarationUnmeasured } : {}),
|
|
213
229
|
});
|
|
230
|
+
const unresolved = (outcome: LlmMergeOutcome & { status: 'conflict-unresolved'; failedFile: string }): LlmMergeOutcome => {
|
|
231
|
+
debug.log('self-dev.merge', 'conflict-unresolved', { reason: outcome.reason, failedFile: outcome.failedFile, resolvedCount: resolvedFiles.length });
|
|
232
|
+
return measuredOutcome(outcome);
|
|
233
|
+
};
|
|
214
234
|
for (const f of files) {
|
|
215
235
|
const abs = join(worktreePath, f);
|
|
216
236
|
let conflicted: string;
|
|
@@ -219,7 +239,7 @@ export async function mergeMainWithLlmResolve(
|
|
|
219
239
|
conflicted = git.readFile(abs);
|
|
220
240
|
} catch {
|
|
221
241
|
git.abort(worktreePath);
|
|
222
|
-
return
|
|
242
|
+
return unresolved({ status: 'conflict-unresolved', reason: 'conflict-file-unreadable', failedFile: f, resolvedFiles });
|
|
223
243
|
}
|
|
224
244
|
try {
|
|
225
245
|
// release/next.md: concurrent landings each append a line — keep both without asking the LLM (REL7c).
|
|
@@ -227,7 +247,7 @@ export async function mergeMainWithLlmResolve(
|
|
|
227
247
|
merged = deterministic ?? await resolve(f, conflicted);
|
|
228
248
|
} catch (error) {
|
|
229
249
|
git.abort(worktreePath); // LLM 예외 → base 유지
|
|
230
|
-
return
|
|
250
|
+
return unresolved({ status: 'conflict-unresolved', resolvedFiles, failedFile: f,
|
|
231
251
|
...(error instanceof MergeConflictInputTooLarge
|
|
232
252
|
? { reason: 'conflict-input-too-large' as const, inputChars: error.inputChars }
|
|
233
253
|
: { reason: 'conflict-resolver-interrupted' as const }),
|
|
@@ -235,11 +255,11 @@ export async function mergeMainWithLlmResolve(
|
|
|
235
255
|
}
|
|
236
256
|
if (PROVIDER_FAILURE_TEXT.test(merged)) {
|
|
237
257
|
git.abort(worktreePath); // 해결기가 공급자 오류 문구를 «내용»으로 돌려줬다 → base 유지
|
|
238
|
-
return
|
|
258
|
+
return unresolved({ status: 'conflict-unresolved', resolvedFiles, failedFile: f, providerFailure: [f] });
|
|
239
259
|
}
|
|
240
260
|
if (hasConflictMarkers(merged)) {
|
|
241
261
|
git.abort(worktreePath); // LLM 이 종합 못 함(마커 잔존) → base 유지
|
|
242
|
-
return
|
|
262
|
+
return unresolved({ status: 'conflict-unresolved', reason: 'conflict-markers-remain', failedFile: f, resolvedFiles });
|
|
243
263
|
}
|
|
244
264
|
try {
|
|
245
265
|
const ours = countTestDeclarations(git.readIndexStage(worktreePath, 2, f));
|
|
@@ -248,7 +268,7 @@ export async function mergeMainWithLlmResolve(
|
|
|
248
268
|
if (mergedDeclarations < Math.min(ours, theirs)) {
|
|
249
269
|
const testDeclarationLoss = [{ file: f, ours, theirs, merged: mergedDeclarations }];
|
|
250
270
|
git.abort(worktreePath);
|
|
251
|
-
return
|
|
271
|
+
return unresolved({ status: 'conflict-unresolved', resolvedFiles, failedFile: f, testDeclarationLoss });
|
|
252
272
|
}
|
|
253
273
|
} catch {
|
|
254
274
|
testDeclarationUnmeasured.push(f);
|
|
@@ -260,7 +280,7 @@ export async function mergeMainWithLlmResolve(
|
|
|
260
280
|
const smaller = Math.min(ours, theirs);
|
|
261
281
|
if (smaller >= SIZE_COLLAPSE_MIN_LINES && mergedLines * 2 < smaller) {
|
|
262
282
|
git.abort(worktreePath); // 양쪽 판 모두보다 절반 넘게 작다 → 통째로 날렸다 → base 유지
|
|
263
|
-
return
|
|
283
|
+
return unresolved({ status: 'conflict-unresolved', resolvedFiles, failedFile: f, sizeCollapse: [{ file: f, ours, theirs, merged: mergedLines }] });
|
|
264
284
|
}
|
|
265
285
|
} catch {
|
|
266
286
|
/* 한쪽 판이 없는 충돌(삭제·추가) — 크기 비교 대상이 아니다 */
|
|
@@ -47,7 +47,9 @@ const runCli = async (args: string[]): Promise<{ stdout: string }> => {
|
|
|
47
47
|
const { argv, env } = universeLaunch(args);
|
|
48
48
|
return exec('bun', argv, { cwd: repoDir(), env, timeout: 120_000, maxBuffer: 1024 * 1024, encoding: 'utf8' });
|
|
49
49
|
};
|
|
50
|
-
|
|
50
|
+
// A work topic containing a verb is not necessarily an instruction; the guard matches executable requests.
|
|
51
|
+
// Security words (secret · credential · force · 자격 · 비밀 · 강제) stay blocked anywhere — topic or request (UX harvest).
|
|
52
|
+
const forbiddenActionRegex = /\b(?:delete|remove|deploy|publish|release|restart|reboot|merge|pay|purchase|sudo)\b(?=\s+(?:the\s+(?:branch|site|app|release|file|record)\b|(?:this\s+)?branch\b|(?:to|into)\s+main\b|main\b|now\b|(?:site|app)\b))|\bcut\s+a\s+release\b|\brm\s+-rf\b|\bconfig\s+set\b|\b(?:secret|credential|force|sudo)\b|자격|비밀|강제|(?:^|\s)--prod\b|(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제|강제)(?:해라|하세요|해줘|하라|해\s*주세요|하시오)(?!\S)|(?:^\s*|후\s*)(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제)(?=\s*$|\s*후\s*)|(?:^\s*운영\s*(?:배포|재시작)(?=\s*$))|(?:삭제|배포|게시|릴리스|재시작|재부팅|병합|결제)(?=\s*(?:해|실행|진행)(?:\s|$))/iu;
|
|
51
53
|
export const TRACK_AGENT_FORBIDDEN_ACTION_REGEX = new RegExp(forbiddenActionRegex.source, forbiddenActionRegex.flags);
|
|
52
54
|
let running = 0;
|
|
53
55
|
|
|
@@ -101,7 +103,7 @@ async function decideTrackActionOnce(input: TrackAgentInput, deps: RunnerDeps):
|
|
|
101
103
|
const values = decision as Record<string, unknown>;
|
|
102
104
|
if (!['say', 'hold'].includes(String(values.action)) || (values.reason !== undefined && typeof values.reason !== 'string')
|
|
103
105
|
|| Object.keys(values).some((key) => !['action', 'reason'].includes(key))) throw new Error('invalid decision');
|
|
104
|
-
if (typeof values.reason === 'string' && forbiddenActionRegex.test(values.reason)) throw new Error('forbidden action');
|
|
106
|
+
if (values.action === 'say' && typeof values.reason === 'string' && forbiddenActionRegex.test(values.reason)) throw new Error('forbidden action');
|
|
105
107
|
return decision as TrackAgentDecision;
|
|
106
108
|
} catch (error) {
|
|
107
109
|
log('decision-failed', { track: input.track, taskId: input.taskId, error: redactSecretText(String(error)) });
|
|
@@ -111,7 +113,7 @@ async function decideTrackActionOnce(input: TrackAgentInput, deps: RunnerDeps):
|
|
|
111
113
|
|
|
112
114
|
export async function executeTrackAction(decision: TrackAgentDecision, input: TrackAgentInput, deps: RunnerDeps = {}): Promise<TrackAgentResult> {
|
|
113
115
|
if (decision.action !== 'say') return { status: 'held', detail: decision.reason ?? '사람 확인' };
|
|
114
|
-
if (forbiddenActionRegex.test(
|
|
116
|
+
if (forbiddenActionRegex.test(input.prompt)) return { status: 'held', detail: '금지 작업 — 사람 확인' };
|
|
115
117
|
if (!findTrack(input.track)) return { status: 'held', detail: '트랙 미정 — 사람 확인' };
|
|
116
118
|
try {
|
|
117
119
|
const checkBudget = deps.checkBudget ?? (async () => {
|
|
@@ -4,6 +4,7 @@ import { dirname, join } from 'node:path';
|
|
|
4
4
|
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
5
5
|
import { debug } from '../debug/log.js';
|
|
6
6
|
import { dateKey } from '../time/format.js';
|
|
7
|
+
import { readRequirementFunnel, type RequirementFunnelResult } from '../intake-plane/requirement-funnel.js';
|
|
7
8
|
|
|
8
9
|
export const BRIEF_DOMAINS = ['운영', '판', '흡수', '시장', '행정', '코나투스'] as const;
|
|
9
10
|
export const BRIEF_PRIORITIES = ['P0', 'P1', 'P2'] as const;
|
|
@@ -41,6 +42,7 @@ export interface BriefItemsOptions {
|
|
|
41
42
|
stateDir?: string;
|
|
42
43
|
now?: () => Date;
|
|
43
44
|
log?: (category: string, event: string, data?: unknown) => void;
|
|
45
|
+
requirementFunnel?: (now: Date) => RequirementFunnelResult;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
export class BriefItemsInputError extends Error {
|
|
@@ -89,8 +91,15 @@ function needsDecision(item: BriefItem, today: string): boolean {
|
|
|
89
91
|
* duplicate claims dropped (same normalized sentence, most urgent kept).
|
|
90
92
|
*/
|
|
91
93
|
export function composeBriefMarkdown(items: readonly BriefItem[], slot: BriefSlot, now: Date): string {
|
|
94
|
+
return composeBriefPage(items, slot, now).markdown;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The ids are exactly the rows represented by the composed page, after deduplication. */
|
|
98
|
+
export function composeBriefPage(items: readonly BriefItem[], slot: BriefSlot, now: Date): { markdown: string; ids: number[] } {
|
|
92
99
|
const today = dateKey(now, { timeZone: KST });
|
|
93
|
-
|
|
100
|
+
// A duplicate left out of a delivered page stays unsent in the ledger, but must not reappear on a later page.
|
|
101
|
+
const deliveredClaims = new Set(items.filter(item => item.sent_at !== null).map(item => normalizeClaim(item.text)));
|
|
102
|
+
const eligible = items.filter(item => item.sent_at === null && beforeSlot(item.created_at, slot, now) && !deliveredClaims.has(normalizeClaim(item.text)));
|
|
94
103
|
// Duplicate claims keep the copy that needs a decision today (P0 or due today), then the higher priority, then the earliest (ACP must-fix).
|
|
95
104
|
const rank = (item: BriefItem) => [needsDecision(item, today) ? 0 : 1, BRIEF_PRIORITIES.indexOf(item.priority), item.id];
|
|
96
105
|
const better = (a: BriefItem, b: BriefItem) => { const x = rank(a), y = rank(b); return x[0]! - y[0]! || x[1]! - y[1]! || x[2]! - y[2]!; };
|
|
@@ -117,7 +126,7 @@ export function composeBriefMarkdown(items: readonly BriefItem[], slot: BriefSlo
|
|
|
117
126
|
for (const item of group.sort((a, b) => a.id - b.id)) lines.push(itemLine(item));
|
|
118
127
|
lines.push('');
|
|
119
128
|
}
|
|
120
|
-
return `${lines.join('\n').trimEnd()}\n
|
|
129
|
+
return { markdown: `${lines.join('\n').trimEnd()}\n`, ids: unique.map(item => item.id) };
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
function itemLine(item: BriefItem): string {
|
|
@@ -166,11 +175,13 @@ export class BriefItemsLedger {
|
|
|
166
175
|
readonly path: string;
|
|
167
176
|
private readonly now: () => Date;
|
|
168
177
|
private readonly log: (category: string, event: string, data?: unknown) => void;
|
|
178
|
+
private readonly requirementFunnel: (now: Date) => RequirementFunnelResult;
|
|
169
179
|
|
|
170
180
|
constructor(options: BriefItemsOptions = {}) {
|
|
171
181
|
this.path = join(options.stateDir ?? elanousStateRoot(), 'briefing', 'items.sqlite');
|
|
172
182
|
this.now = options.now ?? (() => new Date());
|
|
173
183
|
this.log = options.log ?? ((category, event, data) => debug.log(category, event, data));
|
|
184
|
+
this.requirementFunnel = options.requirementFunnel ?? ((now) => readRequirementFunnel({ stateDir: options.stateDir, now }));
|
|
174
185
|
}
|
|
175
186
|
|
|
176
187
|
private using<T>(work: (db: Database) => T): T {
|
|
@@ -246,15 +257,81 @@ export class BriefItemsLedger {
|
|
|
246
257
|
|
|
247
258
|
/**
|
|
248
259
|
* Markdown for one slot. Reads only — never marks, rewrites, or sends an item.
|
|
249
|
-
* Items created at or after the slot instant are left out.
|
|
260
|
+
* Items created at or after the slot instant are left out. At 08:30 the
|
|
261
|
+
* requirement funnel contributes ephemeral lines without adding ledger rows.
|
|
250
262
|
*/
|
|
251
263
|
compose(slot: string): string {
|
|
264
|
+
return this.composeWithIds(slot).markdown;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private itemsFromDb(db: Database): BriefItem[] {
|
|
268
|
+
return db.query('SELECT id, text, domain, priority, deadline, evidence, source, created_at, (SELECT MIN(sent_at) FROM sends WHERE sends.item_id = items.id) AS sent_at FROM items ORDER BY id').all() as BriefItem[];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private pageFromDb(db: Database, slot: BriefSlot): { markdown: string; ids: number[] } {
|
|
272
|
+
const items = this.itemsFromDb(db);
|
|
273
|
+
const now = this.now();
|
|
274
|
+
const morningCutoff = Date.parse(`${dateKey(now, { timeZone: KST })}T08:30:00+09:00`);
|
|
275
|
+
let funnelItems: BriefItem[] = [];
|
|
276
|
+
if (slot === '08:30' && now.getTime() >= morningCutoff) {
|
|
277
|
+
// A funnel failure must not take the morning brief down with it.
|
|
278
|
+
try {
|
|
279
|
+
const extra = this.requirementFunnel(new Date(morningCutoff - 1)).briefItems;
|
|
280
|
+
// Negative ids never collide with ledger rows and keep the funnel's own order (summary first).
|
|
281
|
+
funnelItems = extra.map((item, index): BriefItem => ({
|
|
282
|
+
id: index - extra.length, text: item.text, domain: item.domain, priority: item.priority, deadline: null,
|
|
283
|
+
evidence: item.evidence ?? null, source: item.source,
|
|
284
|
+
created_at: new Date(morningCutoff - 1).toISOString(), sent_at: null,
|
|
285
|
+
}));
|
|
286
|
+
} catch (error) {
|
|
287
|
+
this.log('briefing.items', 'requirement-funnel-failed', { slot, error: error instanceof Error ? error.message : String(error) });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const composed = composeBriefPage([...items, ...funnelItems], slot, now);
|
|
291
|
+
// Ephemeral funnel lines appear on the page but have no ledger row to mark sent.
|
|
292
|
+
const page = { markdown: composed.markdown, ids: composed.ids.filter(id => id > 0) };
|
|
293
|
+
const candidates = items.filter(item => item.sent_at === null && beforeSlot(item.created_at, slot, now)).length;
|
|
294
|
+
this.log('briefing.items', 'composed', { slot, candidates, bytes: Buffer.byteLength(page.markdown) });
|
|
295
|
+
return page;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Read-only page and the exact ledger item ids printed on it (after deduplication). */
|
|
299
|
+
composeWithIds(slot: string): { markdown: string; ids: number[] } {
|
|
252
300
|
if (!(BRIEF_SLOTS as readonly string[]).includes(slot)) throw new BriefItemsInputError('invalid slot');
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
301
|
+
return this.using(db => this.pageFromDb(db, slot as BriefSlot));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Hold the ledger's SQLite writer lock from selecting unsent rows through delivery and marking.
|
|
305
|
+
* A failed delivery rolls back and releases the lock; the next sender can retry.
|
|
306
|
+
*/
|
|
307
|
+
withSendLock<T>(slot: string, work: (page: { markdown: string; ids: number[] }, markSent: (ids: readonly number[], slot: BriefSlot) => number) => T): T {
|
|
308
|
+
if (!(BRIEF_SLOTS as readonly string[]).includes(slot)) throw new BriefItemsInputError('invalid slot');
|
|
309
|
+
return this.using(db => {
|
|
310
|
+
db.exec('BEGIN IMMEDIATE');
|
|
311
|
+
try {
|
|
312
|
+
const page = this.pageFromDb(db, slot as BriefSlot);
|
|
313
|
+
const result = work(page, (ids, sentSlot) => {
|
|
314
|
+
if (sentSlot !== slot || ids.length !== page.ids.length || ids.some((id, index) => id !== page.ids[index])) {
|
|
315
|
+
throw new BriefItemsInputError('marked ids must match composed page');
|
|
316
|
+
}
|
|
317
|
+
return this.markSentInDb(db, ids, sentSlot);
|
|
318
|
+
});
|
|
319
|
+
db.exec('COMMIT');
|
|
320
|
+
return result;
|
|
321
|
+
} catch (cause) {
|
|
322
|
+
db.exec('ROLLBACK');
|
|
323
|
+
throw cause;
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private markSentInDb(db: Database, ids: readonly number[], slot: BriefSlot): number {
|
|
329
|
+
const at = this.now().toISOString();
|
|
330
|
+
const insert = db.query('INSERT INTO sends (item_id, slot, sent_at) VALUES (?, ?, ?)');
|
|
331
|
+
let count = 0;
|
|
332
|
+
for (const id of ids) { insert.run(id, slot, at); count++; }
|
|
333
|
+
this.log('briefing.items', 'marked-sent', { slot, count });
|
|
334
|
+
return count;
|
|
258
335
|
}
|
|
259
336
|
|
|
260
337
|
/**
|
|
@@ -263,14 +340,6 @@ export class BriefItemsLedger {
|
|
|
263
340
|
*/
|
|
264
341
|
markSent(ids: readonly number[], slot: BriefSlot): number {
|
|
265
342
|
if (!(BRIEF_SLOTS as readonly string[]).includes(slot)) throw new BriefItemsInputError('invalid slot');
|
|
266
|
-
|
|
267
|
-
const count = this.using(db => {
|
|
268
|
-
const insert = db.query('INSERT INTO sends (item_id, slot, sent_at) VALUES (?, ?, ?)');
|
|
269
|
-
let n = 0;
|
|
270
|
-
db.transaction(() => { for (const id of ids) { insert.run(id, slot, at); n++; } })();
|
|
271
|
-
return n;
|
|
272
|
-
});
|
|
273
|
-
this.log('briefing.items', 'marked-sent', { slot, count });
|
|
274
|
-
return count;
|
|
343
|
+
return this.using(db => db.transaction(() => this.markSentInDb(db, ids, slot))());
|
|
275
344
|
}
|
|
276
345
|
}
|
package/src/cli/autopilot-cli.ts
CHANGED
|
@@ -6,10 +6,95 @@ import { writeStdoutJson } from './stdout-json.js';
|
|
|
6
6
|
// runner. Spawns an ACP backend, opens a single session, runs the
|
|
7
7
|
// AutopilotLoopDriver against the mission, prints agent deltas on
|
|
8
8
|
// stdout + envelope/termination summary on stderr.
|
|
9
|
-
export
|
|
9
|
+
export interface AutopilotCliDeps {
|
|
10
|
+
/** logs.db 싱크 등록(시험 주입). 기본 = `registerStandaloneLogSink`. `task-agent-action` 이 부른다. */
|
|
11
|
+
registerSink?: (surface: string) => Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function registerAutopilotCommands(program: Command, cliDeps: AutopilotCliDeps = {}): void {
|
|
15
|
+
// Explicit runtime entry for a selected action; the task chooser can invoke this entry.
|
|
10
16
|
const autopilotCmd = program
|
|
11
17
|
.command('autopilot')
|
|
12
18
|
.description('Mission-driven autopilot — runs an ACP agent against a single mission with safety + budget guards');
|
|
19
|
+
autopilotCmd.command('task-agent-action <actionJson>')
|
|
20
|
+
.description('Execute a selected task-agent action; defaults to shadow')
|
|
21
|
+
.action(async (actionJson: string) => {
|
|
22
|
+
// 독립 프로세스 — 싱크를 붙이지 않으면 `task-agent`/`action` 관측이 logs.db 에 안 닿는다(fail-open).
|
|
23
|
+
const { registerTaskAgentSink } = await import('./tasks-cli.js');
|
|
24
|
+
await registerTaskAgentSink(cliDeps.registerSink);
|
|
25
|
+
const { executeNextAction } = await import('../task-agent/actions.js');
|
|
26
|
+
const { getUserConfig } = await import('../user-config.js');
|
|
27
|
+
const { spawnSync } = await import('node:child_process');
|
|
28
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
29
|
+
// 상태 파일은 «없음»(ENOENT)만 빈 상태다 — 손상·해석 실패면 덮어쓰지 않고 멈춘다(사후 리뷰 must-fix · #24482).
|
|
30
|
+
const { readTaskAgentState, TaskAgentStateError } = await import('../task-agent/task-hand.js');
|
|
31
|
+
const { join, dirname } = await import('node:path');
|
|
32
|
+
const { debug } = await import('../debug/log.js');
|
|
33
|
+
const { withFileLockSync } = await import('../storage/file-lock.js');
|
|
34
|
+
const config = getUserConfig() as ReturnType<typeof getUserConfig> & { taskAgent?: { mode?: string } };
|
|
35
|
+
const { effectiveInstanceRoot } = await import('../instance/resolve.js');
|
|
36
|
+
const statePath = join(effectiveInstanceRoot(), 'task-agent-actions.json');
|
|
37
|
+
type ActionState = { landingDay?: string; landingsToday?: number; failureCounts?: Record<string, number> };
|
|
38
|
+
let state: ActionState;
|
|
39
|
+
try { state = readTaskAgentState<ActionState>(statePath); } catch (error) {
|
|
40
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const action = JSON.parse(actionJson) as import('../task-agent/actions.js').NextAction;
|
|
45
|
+
const deps: import('../task-agent/actions.js').ActionDeps = {
|
|
46
|
+
...state,
|
|
47
|
+
failureCounts: state.failureCounts ?? {},
|
|
48
|
+
saveState: () => {
|
|
49
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
50
|
+
// 실패 횟수는 incrementFailure 가 잠금 안에서 키별로 더한다 — 여기선 덮어쓰지 않는다.
|
|
51
|
+
},
|
|
52
|
+
mode: config.taskAgent?.mode,
|
|
53
|
+
// 하루 착지 칸은 상태 파일 잠금 안에서 확보·반환한다 — 두 CLI 프로세스가 같은 잔여 칸을 쓰지 않게.
|
|
54
|
+
reserveLanding: (day, limit) => withFileLockSync(`${statePath}.lock`, () => {
|
|
55
|
+
const disk = readTaskAgentState<ActionState>(statePath);
|
|
56
|
+
const used = disk.landingDay === day ? disk.landingsToday ?? 0 : 0;
|
|
57
|
+
if (used >= limit) return false;
|
|
58
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
59
|
+
writeFileSync(statePath, JSON.stringify({ ...disk, landingDay: day, landingsToday: used + 1 }));
|
|
60
|
+
deps.landingDay = day; deps.landingsToday = used + 1;
|
|
61
|
+
return true;
|
|
62
|
+
}),
|
|
63
|
+
incrementFailure: (key) => withFileLockSync(`${statePath}.lock`, () => {
|
|
64
|
+
const disk = readTaskAgentState<ActionState>(statePath);
|
|
65
|
+
const counts = { ...(disk.failureCounts ?? {}) };
|
|
66
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
67
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
68
|
+
writeFileSync(statePath, JSON.stringify({ ...disk, failureCounts: counts }));
|
|
69
|
+
return counts[key]!;
|
|
70
|
+
}),
|
|
71
|
+
releaseLanding: (day) => withFileLockSync(`${statePath}.lock`, () => {
|
|
72
|
+
const disk = readTaskAgentState<ActionState>(statePath);
|
|
73
|
+
if (disk.landingDay !== day || !disk.landingsToday) return;
|
|
74
|
+
writeFileSync(statePath, JSON.stringify({ ...disk, landingsToday: disk.landingsToday - 1 }));
|
|
75
|
+
deps.landingsToday = disk.landingsToday - 1;
|
|
76
|
+
}),
|
|
77
|
+
observe: (_event, data) => debug.log('task-agent', 'action', data),
|
|
78
|
+
command: async args => {
|
|
79
|
+
const p = args[0] === 'gh'
|
|
80
|
+
// PR 조회는 착지할 작업 트리에서 — 다른 저장소에서 불러도 엉뚱한 PR 을 읽지 않게.
|
|
81
|
+
? spawnSync('gh', args.slice(1), { encoding: 'utf8', ...(action.cwd ? { cwd: action.cwd } : {}) })
|
|
82
|
+
// 지금 돌고 있는 진입점으로 부른다 — 저장소 밖(설치본)에서 불러도 같은 elanous 를 쓴다.
|
|
83
|
+
: spawnSync(process.execPath, [process.argv[1]!, ...(process.argv.includes('--test') ? ['--test'] : []), ...args], { encoding: 'utf8' });
|
|
84
|
+
return { status: p.status ?? 1, stdout: p.stdout ?? '', stderr: p.stderr ?? '' };
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
let result: string;
|
|
88
|
+
try { result = await executeNextAction(action, deps); } catch (error) {
|
|
89
|
+
// 잠금 안 읽기가 손상을 만나면 쓰지 않고 여기로 온다 — 실패로 끝낸다. 그 밖의 오류는 종전대로 올린다.
|
|
90
|
+
if (!(error instanceof TaskAgentStateError)) throw error;
|
|
91
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (config.taskAgent?.mode === 'live') deps.saveState?.();
|
|
96
|
+
process.stdout.write(`${result}\n`);
|
|
97
|
+
});
|
|
13
98
|
|
|
14
99
|
autopilotCmd
|
|
15
100
|
.command('run <mission...>')
|
package/src/cli/brief-cli.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import { BRIEF_DOMAINS, BRIEF_PRIORITIES, BRIEF_SLOTS, BriefItemsInputError, BriefItemsLedger, type BriefDomain, type BriefPriority } from '../briefing/brief-items.js';
|
|
2
|
+
import { BRIEF_DOMAINS, BRIEF_PRIORITIES, BRIEF_SLOTS, BriefItemsInputError, BriefItemsLedger, type BriefDomain, type BriefPriority, type BriefSlot } from '../briefing/brief-items.js';
|
|
3
|
+
import { kindRouteTarget } from '../domains/telegram-kind-route.js';
|
|
4
|
+
import { sendTelegramReturningId } from '../autopilot/mission-notify.js';
|
|
5
|
+
import { getUserConfig } from '../user-config.js';
|
|
3
6
|
|
|
4
7
|
export interface BriefCliDeps {
|
|
5
8
|
ledger?: BriefItemsLedger;
|
|
@@ -7,6 +10,7 @@ export interface BriefCliDeps {
|
|
|
7
10
|
now?: () => Date;
|
|
8
11
|
output?: (line: string) => void;
|
|
9
12
|
error?: (line: string) => void;
|
|
13
|
+
send?: (markdown: string) => number | null;
|
|
10
14
|
}
|
|
11
15
|
|
|
12
16
|
export function registerBriefCommands(program: Command, deps: BriefCliDeps = {}): void {
|
|
@@ -20,7 +24,7 @@ export function registerBriefCommands(program: Command, deps: BriefCliDeps = {})
|
|
|
20
24
|
error(`brief ${verb}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
21
25
|
process.exitCode = cause instanceof BriefItemsInputError ? 2 : 1;
|
|
22
26
|
};
|
|
23
|
-
const brief = program.command('brief').description('대표 브리핑 항목
|
|
27
|
+
const brief = program.command('brief').description('대표 브리핑 항목 원장과 슬롯 발송');
|
|
24
28
|
|
|
25
29
|
brief.command('add')
|
|
26
30
|
.description('주장 한 줄을 원장에 추가한다 (원문 그대로)')
|
|
@@ -70,4 +74,34 @@ export function registerBriefCommands(program: Command, deps: BriefCliDeps = {})
|
|
|
70
74
|
try { output(ledger().compose(opts.slot)); }
|
|
71
75
|
catch (cause) { fail('compose', cause); }
|
|
72
76
|
});
|
|
77
|
+
|
|
78
|
+
brief.command('send')
|
|
79
|
+
.description('슬롯 이전의 미발송 항목을 한 장 발송하고 성공한 항목만 표시한다')
|
|
80
|
+
.requiredOption('--slot <slot>', `슬롯 (${BRIEF_SLOTS.join('|')})`)
|
|
81
|
+
.option('--dry-run', '한 장만 출력하고 발송·표시하지 않는다')
|
|
82
|
+
.action((opts: { slot: string; dryRun?: boolean }) => {
|
|
83
|
+
try {
|
|
84
|
+
const store = ledger();
|
|
85
|
+
if (opts.dryRun) {
|
|
86
|
+
const { markdown, ids } = store.composeWithIds(opts.slot);
|
|
87
|
+
output(ids.length === 0 ? '(보낼 항목 없음)' : markdown);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const result = store.withSendLock(opts.slot, ({ markdown, ids }, markSent) => {
|
|
91
|
+
if (ids.length === 0) return '(보낼 항목 없음)';
|
|
92
|
+
const send = deps.send ?? ((text: string) => {
|
|
93
|
+
const target = kindRouteTarget(getUserConfig(), 'ops-report');
|
|
94
|
+
if (!target) return null;
|
|
95
|
+
return sendTelegramReturningId(target.botToken, target.chatId, text);
|
|
96
|
+
});
|
|
97
|
+
const messageId = send(markdown);
|
|
98
|
+
if (typeof messageId !== 'number' || !Number.isFinite(messageId) || messageId <= 0) {
|
|
99
|
+
throw new Error('발송 실패: 양수 메시지 id 없음');
|
|
100
|
+
}
|
|
101
|
+
markSent(ids, opts.slot as BriefSlot);
|
|
102
|
+
return `sent ${messageId} (${ids.length} items)`;
|
|
103
|
+
});
|
|
104
|
+
output(result);
|
|
105
|
+
} catch (cause) { fail('send', cause); }
|
|
106
|
+
});
|
|
73
107
|
}
|
package/src/cli/decisions-cli.ts
CHANGED
|
@@ -53,6 +53,7 @@ export function formatDecisionDetail(e: DecisionEntry): string {
|
|
|
53
53
|
...(e.note ? [`메모: ${e.note}`] : [])] : []),
|
|
54
54
|
...(e.withdrawnAt ? [`철회: ${kst(e.withdrawnAt)} · ${e.withdrawReason}`] : []),
|
|
55
55
|
'이력:', ...e.history.map(h => ` ${h.type} · ${h.at ? kst(h.at) : '시각 미상'} · ${h.by}${h.choice ? ` · ${h.choice}` : ''}${h.reason ? ` · ${h.reason}` : ''} · ${h.version ? version(h.version) : '판 미상'}`),
|
|
56
|
+
...(e.pendingQuestion ? [`판단 재료:\n${e.pendingQuestion}`] : []),
|
|
56
57
|
...(e.refs?.length ? [`참조: ${e.refs.join(' · ')}`] : [])].join('\n');
|
|
57
58
|
}
|
|
58
59
|
|
|
@@ -86,6 +87,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
|
|
|
86
87
|
root.command('raise').description('SCQA·선택지와 권고로 결정 항목을 올린다')
|
|
87
88
|
.requiredOption('--title <text>').requiredOption('--category <category>')
|
|
88
89
|
.requiredOption('--s <text>').requiredOption('--c <text>').option('--q <text>').option('--a <text>')
|
|
90
|
+
.option('--pending-question <text>', '판단 재료 전문 (무엇·지금까지·재측·선택지별 결과·권고·확신·그냥 두면·근거)')
|
|
89
91
|
.requiredOption('--option <key=label:consequence>', '선택지 (두 번 이상)', repeat, [] as string[])
|
|
90
92
|
.option('--recommend <key>').option('--why <text>').option('--skip-recommend <reason>')
|
|
91
93
|
.option('--xcheck <SEAT:메모>', '교차 확인 (반복)', repeat, [] as string[])
|
|
@@ -96,7 +98,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
|
|
|
96
98
|
.option('--ref <url>', '참조 (반복)', repeat, [] as string[])
|
|
97
99
|
.option('--due <when>', '기한 — UTC ISO(2026-10-04T09:00:00Z) 또는 +Nh(지금부터 N시간) · 기한 2시간 전 텔레그램·디스코드로 다시 알린다')
|
|
98
100
|
.option('--json').option('--hq-override')
|
|
99
|
-
.action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; xcheck: string[]; noXcheck?: string; alternative?: string; dissent?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean; hqOverride?: boolean }) => fail(() => {
|
|
101
|
+
.action((o: { due?: string; title: string; category: DecisionCategory; s: string; c: string; q?: string; a?: string; pendingQuestion?: string; option: string[]; recommend?: string; why?: string; skipRecommend?: string; xcheck: string[]; noXcheck?: string; alternative?: string; dissent?: string; track?: DecisionTrack; agent?: string; session?: string; resumeQuestion?: string; run?: string; ref: string[]; json?: boolean; hqOverride?: boolean }) => fail(() => {
|
|
100
102
|
if (o.run !== undefined && o.resumeQuestion === undefined) throw new Error('--run requires --resume-question');
|
|
101
103
|
if (o.skipRecommend !== undefined && (o.recommend !== undefined || o.why !== undefined)) throw new Error('choose recommendation or skip, not both');
|
|
102
104
|
if (o.skipRecommend === undefined && (!o.recommend || !o.why)) throw new Error('--recommend and --why required, or --skip-recommend <reason>');
|
|
@@ -111,6 +113,7 @@ export function registerDecisionsCommands(program: Command, config: DecisionLedg
|
|
|
111
113
|
const who = agent(o.agent);
|
|
112
114
|
if (!mayWrite('raise', o.hqOverride)) return;
|
|
113
115
|
const entry = new DecisionLedger(config).raise({ title: o.title, category: o.category, scqa: { s: o.s, c: o.c, ...(o.q ? { q: o.q } : {}), ...(o.a ? { a: o.a } : {}) }, options,
|
|
116
|
+
...(o.pendingQuestion ? { pendingQuestion: o.pendingQuestion } : {}),
|
|
114
117
|
...(crossCheck.length ? { crossCheck } : { crossCheckSkipped: o.noXcheck ?? 'missing' }),
|
|
115
118
|
...(o.alternative !== undefined ? { alternative: o.alternative } : {}), ...(o.dissent !== undefined ? { dissent: o.dissent } : {}),
|
|
116
119
|
recommendation: o.skipRecommend !== undefined ? { skipped: true, reason: o.skipRecommend } : { option: o.recommend!, why: o.why! },
|
package/src/cli/freeze-cli.ts
CHANGED
|
@@ -7,8 +7,9 @@ import { pendingFrozenMerges, sweepFrozenMerges } from '../self-implement/frozen
|
|
|
7
7
|
export function registerFreezeCommands(program: Command): void {
|
|
8
8
|
const freeze = program.command('freeze').description('착지 동결 상태 on/off/status');
|
|
9
9
|
freeze.command('on').option('--reason <text>', '동결 이유').option('--until <timestamp>', '끝 시각(타임존 포함 ISO)')
|
|
10
|
+
.option('--hold-launches', '착지와 함께 새 하니스 launch도 보류')
|
|
10
11
|
.option('--wait-seconds <n>', '이미 동결 검사를 지난 병합이 끝나기를 기다릴 최대 초', '600')
|
|
11
|
-
.action(async (opts: { reason?: string; until?: string; waitSeconds: string }) => {
|
|
12
|
+
.action(async (opts: { reason?: string; until?: string; holdLaunches?: boolean; waitSeconds: string }) => {
|
|
12
13
|
const waitSeconds = Number(opts.waitSeconds);
|
|
13
14
|
if (!/^\d+$/.test(opts.waitSeconds.trim()) || !Number.isSafeInteger(waitSeconds) || waitSeconds > 86_400) {
|
|
14
15
|
throw new CliUserError(`잘못된 --wait-seconds: ${opts.waitSeconds}`, '0 이상 86400 이하의 정수 초');
|
|
@@ -34,7 +35,7 @@ export function registerFreezeCommands(program: Command): void {
|
|
|
34
35
|
const state = readLandingFreeze();
|
|
35
36
|
const pending = pendingFrozenMerges();
|
|
36
37
|
if (opts.json) console.log(JSON.stringify({ frozen: state !== null, freeze: state, pendingMerges: pending }));
|
|
37
|
-
else console.log(`${state ? landingFreezeMessage(state) : '동결 없음'} · 보류 병합 ${pending}건`);
|
|
38
|
+
else console.log(`${state ? landingFreezeMessage(state) : '동결 없음'} · launch 보류 ${state?.holdLaunches === true ? '예' : '아니오'} · 보류 병합 ${pending}건`);
|
|
38
39
|
});
|
|
39
40
|
}
|
|
40
41
|
|
package/src/cli/intake-cli.ts
CHANGED
|
@@ -12,6 +12,9 @@ import { runGitCommand } from '../git-fs/runner.js';
|
|
|
12
12
|
import { debug } from '../debug/log.js';
|
|
13
13
|
import { getUserConfig } from '../user-config.js';
|
|
14
14
|
import { registerIntakeSourceCommands } from './intake-source-cli.js';
|
|
15
|
+
import type { IntakeItem } from '../intake-plane/items.js';
|
|
16
|
+
import type { TaskCard } from '../task-cards/card-store.js';
|
|
17
|
+
import { collectFunnelCandidates, type FunnelDirectiveRow } from '../intake-plane/requirement-funnel.js';
|
|
15
18
|
|
|
16
19
|
export interface IntakeToTasksCliDeps {
|
|
17
20
|
root: string;
|
|
@@ -21,6 +24,62 @@ export interface IntakeToTasksCliDeps {
|
|
|
21
24
|
llm?: (input: IntakeTaskInput) => Promise<string>;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
export interface IntakeCandidatesCliDeps {
|
|
28
|
+
directives?: () => FunnelDirectiveRow[] | Promise<FunnelDirectiveRow[]>;
|
|
29
|
+
intakeItems?: () => IntakeItem[] | Promise<IntakeItem[]>;
|
|
30
|
+
cards?: () => TaskCard[] | Promise<TaskCard[]>;
|
|
31
|
+
write?: (text: string) => Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** `--limit` 원 문자열 → 0 이상의 정수. 숫자만(`/^\d+$/`) 받고 나머지('' · 공백 · 음수 · 소수)는 거부한다. */
|
|
35
|
+
export function parseCandidatesLimit(raw: string): number {
|
|
36
|
+
const limit = /^\d+$/.test(raw) ? Number(raw) : NaN;
|
|
37
|
+
if (!Number.isSafeInteger(limit)) throw new Error(`--limit 는 0 이상의 정수여야 한다: ${JSON.stringify(raw)}`);
|
|
38
|
+
return limit;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runIntakeCandidatesCli(
|
|
42
|
+
opts: { json?: boolean; limit?: string },
|
|
43
|
+
deps: IntakeCandidatesCliDeps = {},
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
// 원 문자열을 «먼저» 본다 — `Number('')`·`Number(' ')` 는 0 이라 빈 목록으로 조용히 통과한다.
|
|
46
|
+
const limit = opts.limit === undefined ? undefined : parseCandidatesLimit(opts.limit);
|
|
47
|
+
const directives = deps.directives ?? (async () => {
|
|
48
|
+
const [{ Database }, { existsSync }, { directiveDbPath }] = await Promise.all([
|
|
49
|
+
import('bun:sqlite'), import('node:fs'), import('../directives/directive-index.js'),
|
|
50
|
+
]);
|
|
51
|
+
const path = directiveDbPath();
|
|
52
|
+
if (!existsSync(path)) return [];
|
|
53
|
+
const db = new Database(path, { readonly: true });
|
|
54
|
+
try { return db.query('SELECT * FROM directives ORDER BY ts DESC').all() as FunnelDirectiveRow[]; }
|
|
55
|
+
finally { db.close(); }
|
|
56
|
+
});
|
|
57
|
+
const intakeItems = deps.intakeItems ?? (async () => {
|
|
58
|
+
const [{ listIntakeItems }, { effectiveInstanceRoot }] = await Promise.all([
|
|
59
|
+
import('../intake-plane/items.js'), import('../instance/resolve.js'),
|
|
60
|
+
]);
|
|
61
|
+
return listIntakeItems(effectiveInstanceRoot());
|
|
62
|
+
});
|
|
63
|
+
const cards = deps.cards ?? (async () => {
|
|
64
|
+
const [{ existsSync }, { CardStore, cardIndexPath }] = await Promise.all([
|
|
65
|
+
import('node:fs'), import('../task-cards/card-store.js'),
|
|
66
|
+
]);
|
|
67
|
+
if (!existsSync(cardIndexPath())) return [];
|
|
68
|
+
const store = new CardStore(undefined, true);
|
|
69
|
+
try { return store.listCards({ open: true }); }
|
|
70
|
+
finally { store.close(); }
|
|
71
|
+
});
|
|
72
|
+
const all = collectFunnelCandidates({
|
|
73
|
+
directives: await directives(), intakeItems: await intakeItems(), cards: await cards(),
|
|
74
|
+
});
|
|
75
|
+
const candidates = limit === undefined ? all : all.slice(0, limit);
|
|
76
|
+
if (opts.json) await (deps.write ?? writeStdoutFully)(JSON.stringify({ candidates }) + '\n');
|
|
77
|
+
else await (deps.write ?? writeStdoutFully)([
|
|
78
|
+
`요구 후보 ${candidates.length}건`,
|
|
79
|
+
...candidates.map((row) => `${row.source}\t${row.id}\t${row.score ?? '-'}\t${row.grade ?? '-'}\t${row.title}`),
|
|
80
|
+
].join('\n') + '\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
24
83
|
export interface IntakeDigestCliDeps {
|
|
25
84
|
root?: string;
|
|
26
85
|
annotateLens?: (root: string, day: string) => Promise<unknown>;
|
|
@@ -102,7 +161,7 @@ export async function runIntakeDigestCli(
|
|
|
102
161
|
}
|
|
103
162
|
const d = (deps.buildDigest ?? buildIntakeDigest)(root, day);
|
|
104
163
|
if (opts.telegram) {
|
|
105
|
-
if (!d.absorbed.length) { console.log(`텔레그램: ${day} 흡수 0 — 보내지 않음`); return { empty: true }; }
|
|
164
|
+
if (!d.absorbed.length && !d.news?.length) { console.log(`텔레그램: ${day} 흡수 0 — 보내지 않음`); return { empty: true }; }
|
|
106
165
|
const text = renderDigestTelegram(d, { ...(opts.vault ? { vaultRoot: opts.vault } : {}), ...(opts.note ? { notePath: opts.note } : {}) });
|
|
107
166
|
const sent = deps.sendTelegram
|
|
108
167
|
? await deps.sendTelegram(text)
|
|
@@ -134,7 +193,7 @@ export function resolveIntakeCheckRoot(
|
|
|
134
193
|
}
|
|
135
194
|
}
|
|
136
195
|
|
|
137
|
-
export function registerIntakeCommands(program: Command): void {
|
|
196
|
+
export function registerIntakeCommands(program: Command, candidateDeps: IntakeCandidatesCliDeps = {}): void {
|
|
138
197
|
// ── intake check — 바깥 사실을 elanous 현재와 대조 (태스크 등록 없음) ──
|
|
139
198
|
const intakeCmd = program.command('intake').description('바깥 사실·문서를 elanous 현재와 대조하거나 태스크로 받는다');
|
|
140
199
|
registerIntakeSourceCommands(intakeCmd);
|
|
@@ -229,6 +288,19 @@ export function registerIntakeCommands(program: Command): void {
|
|
|
229
288
|
}
|
|
230
289
|
});
|
|
231
290
|
|
|
291
|
+
intakeCmd
|
|
292
|
+
.command('candidates')
|
|
293
|
+
.description('지시·흡수·소원에서 읽기 전용 요구 후보를 모은다')
|
|
294
|
+
.option('--json', '구조화 출력')
|
|
295
|
+
.option('--limit <n>', '출력할 후보 상한')
|
|
296
|
+
.action(async (opts: { json?: boolean; limit?: string }) => {
|
|
297
|
+
if (opts.limit !== undefined) {
|
|
298
|
+
try { parseCandidatesLimit(opts.limit); }
|
|
299
|
+
catch (error) { console.error((error as Error).message); process.exitCode = 2; return; }
|
|
300
|
+
}
|
|
301
|
+
await runIntakeCandidatesCli(opts, candidateDeps);
|
|
302
|
+
});
|
|
303
|
+
|
|
232
304
|
// 정기 외부 흡수 원장 — RFC-regular-external-intake-and-normalization-pipeline §3 (① 모양 · ② 중복).
|
|
233
305
|
intakeCmd
|
|
234
306
|
.command('collect-pod <dir>')
|