elanous 0.2.15 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1307-33ffcc49ab45123b.js → 1307-aac7332576c4d6fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{6877-7c6208705c284124.js → 6877-fe94e76a5841ee9b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/8401-4ce1c42c8d89903f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-f41be85e8a680651.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9247-269e4464cc4b491c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-0e2c57fe6add4b2a.js → page-3143b07a817d1da9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-e05a77df7e005b04.js → page-17ba086ef82d339b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-7eb661ff23af7da2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-6b8444914da74ca6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-b8c49f6b4fa2df91.js → page-6e5c3a867557298b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-7110f732f45bf944.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-976813c91d7394ea.js → page-9a41d65355fc25b9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-5e77cd1802567c89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-1d4901eb7c2d8822.js → page-41afec0b255ed2bb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-72d3e7ff35a8c954.js → page-4187666132fc98c4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-60c7a8e1a4fe62ce.js → page-09328d499162bae4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-2a52b316cb143d9c.js → page-29ff3a8f84f03b6a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-c3ce483c8d5db370.js → page-80ec7401bc83f511.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ab93b240f687d69a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-96f074f6538e8a2c.js → page-fc7d86467291ae38.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-e97be5334d83a9f5.js → layout-de43a186e5ba5683.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-8b08cafad27759c9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-0e2d6985fb42cd2f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/page-13e4108999f58d13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-e8296ee284aa1293.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-c1343f9499d86f3f.js → page-043a9111e74f6b24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-8739a2e0ae793130.js → not-found-23a4601ceb7f452e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-1ed1324b1dc36754.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-d23b12bb92242558.js → page-10d958605a77b3bd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-166745e51ac0c7b0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-c7fea475488201a8.js → page-a04260b8ccc968d4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-7a1d85e121065cc8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-1f50e3d4e13435f4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-5efbf81124c474ae.js → page-12916918988ac97e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-d8a3347a9d0c3f4a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a9117d2cd3093d36.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-06ee266953d9c879.js → page-5a64cb5208c6a8fd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-153c2b3bcc89e8de.js → page-1407ec83ea64c1f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-1d824510df025d18.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-6e878ebbe1737d00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-0d6d23b40263a7ea.js → page-7f49fe645d5132f7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-81a4e7f8dbfe74cb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-27d71f131acd9320.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-3ab59d19913b200e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/page-bf85b2f5bf077925.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-73735b64bfe1ae67.js → page-424659bacfb8cdd7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-ab32a8f522a60371.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-f7e631e9b4daf66e.js → page-fc2df261f8a2c7df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-09461366af82ef13.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-9817c7f9007466cc.js → page-5d4fef9ccd7d406b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ea90a8ac73ce2749.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +15 -15
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/intake/intake-daily.yaml +9 -2
- package/graphs/intake/news-sources.yaml +4 -0
- package/graphs/intake/recipes.yaml +6 -1
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/__fixtures__/intake-news.xml +7 -0
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +34 -15
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +131 -9
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/intake-news.ts +169 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/docs-land-node.ts +69 -10
- package/scripts/release-loop/gate-node.ts +3 -1
- package/scripts/release-loop/publish-node.ts +2 -2
- package/scripts/release-loop/release-docs.ts +134 -0
- package/scripts/release-loop/vault-note-node.ts +19 -2
- package/scripts/release-loop/version-node.ts +7 -4
- package/scripts/release-story/draft.ts +42 -14
- package/scripts/rhythm/monthly.ts +97 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-mission/driver.ts +104 -11
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +5 -3
- package/src/briefing/brief-items.ts +191 -36
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/cli/autopilot-cli.ts +66 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/decisions-cli.ts +4 -1
- package/src/cli/intake-cli.ts +1 -1
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/log-event-names.ts +1 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pr-cli.ts +92 -13
- package/src/cli/provider-cli.ts +14 -0
- package/src/cli/release-cli.ts +11 -8
- package/src/cli/self-update-canary.ts +46 -0
- package/src/cli/self-update.ts +399 -1
- package/src/cli/usage-cli.ts +26 -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 +9 -0
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +178 -12
- package/src/harness/harness-pod-dispatch.ts +36 -55
- package/src/harness/harness-queue.ts +86 -7
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +139 -8
- package/src/harness/helper-scan.ts +13 -2
- package/src/harness/nested-elanous-policy.ts +133 -0
- package/src/hq/fence-wrapper.ts +123 -0
- package/src/hq/hq.ts +16 -2
- package/src/hq/lease.ts +5 -3
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +168 -13
- package/src/intake-plane/digest.ts +85 -28
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/tick.ts +96 -25
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +2 -2
- package/src/maturity/tui-slash-availability.ts +9 -0
- package/src/nexus/api/grid.ts +61 -0
- package/src/nexus/api/http-server.ts +19 -1
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/oauth/codex.ts +82 -2
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/release-loop/checklist.ts +10 -5
- package/src/release-loop/feature-store.ts +10 -9
- package/src/release-loop/landing-freeze.ts +18 -10
- package/src/release-loop/placement.ts +17 -8
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-loop/seat-loop.ts +172 -21
- package/src/self-dev/ask-launch-flow.ts +48 -8
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +6 -5
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-dev/run-store.ts +5 -1
- package/src/self-dev/run-supervisor.ts +7 -1
- package/src/self-dev/supervisor-verdict-edges.ts +46 -0
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/frozen-merges.ts +25 -20
- 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 +107 -25
- 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/heal-triage.ts +44 -0
- package/src/self-implement/host-regate.ts +3 -1
- package/src/self-implement/orchestrator.ts +157 -8
- package/src/self-implement/run-ledger.ts +4 -0
- package/src/self-implement/seams.ts +44 -15
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/working-backwards.ts +16 -1
- package/src/task-agent/actions.ts +118 -0
- package/src/task-agent/judge.ts +96 -0
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +9 -4
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +33 -4
- package/src/task-orchestrator/surfaces/pod-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +95 -18
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +168 -37
- package/src/task-orchestrator/surfaces/self-implement.ts +2 -0
- package/src/telegram-commands.ts +6 -1
- package/src/telegram-loops-command.ts +21 -14
- package/src/telegram.ts +47 -0
- package/src/user-config.ts +59 -4
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.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/board/page-f544338193a9a762.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-955ec2da3be4c560.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-e2ee2896811e98d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-f5597e894c02cd33.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +0 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.css +0 -1
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +0 -15
- package/src/launch-head/seat-goal-queue.ts +0 -198
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LZcstaO9_NXypPFmujolv → yRA5ur6DcPl0YHSRheH2O}/_ssgManifest.js +0 -0
|
@@ -2,32 +2,45 @@ import { Database } from 'bun:sqlite';
|
|
|
2
2
|
import { chmodSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
5
|
+
import { debug } from '../debug/log.js';
|
|
6
|
+
import { dateKey } from '../time/format.js';
|
|
5
7
|
|
|
6
|
-
export
|
|
8
|
+
export const BRIEF_DOMAINS = ['운영', '판', '흡수', '시장', '행정', '코나투스'] as const;
|
|
9
|
+
export const BRIEF_PRIORITIES = ['P0', 'P1', 'P2'] as const;
|
|
10
|
+
export const BRIEF_SLOTS = ['08:30', 'after-release', '22:00'] as const;
|
|
11
|
+
|
|
12
|
+
export type BriefDomain = (typeof BRIEF_DOMAINS)[number];
|
|
13
|
+
export type BriefPriority = (typeof BRIEF_PRIORITIES)[number];
|
|
14
|
+
export type BriefSlot = (typeof BRIEF_SLOTS)[number];
|
|
15
|
+
|
|
16
|
+
const KST = 'Asia/Seoul';
|
|
7
17
|
|
|
8
18
|
export interface BriefItem {
|
|
9
19
|
id: number;
|
|
10
20
|
text: string;
|
|
11
|
-
domain:
|
|
21
|
+
domain: BriefDomain;
|
|
12
22
|
priority: BriefPriority;
|
|
13
23
|
deadline: string | null;
|
|
14
24
|
evidence: string | null;
|
|
15
25
|
source: string;
|
|
16
26
|
created_at: string;
|
|
27
|
+
sent_at: string | null;
|
|
17
28
|
}
|
|
18
29
|
|
|
19
30
|
export interface AddBriefItem {
|
|
20
31
|
text: string;
|
|
21
|
-
domain:
|
|
32
|
+
domain: BriefDomain;
|
|
22
33
|
priority: BriefPriority;
|
|
23
34
|
deadline?: string | null;
|
|
24
35
|
evidence?: string | null;
|
|
25
36
|
source: string;
|
|
37
|
+
createdAt?: string;
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
export interface BriefItemsOptions {
|
|
29
41
|
stateDir?: string;
|
|
30
42
|
now?: () => Date;
|
|
43
|
+
log?: (category: string, event: string, data?: unknown) => void;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
export class BriefItemsInputError extends Error {
|
|
@@ -43,14 +56,121 @@ function optional(value: string | null | undefined, field: string): string | nul
|
|
|
43
56
|
return value == null ? null : required(value, field);
|
|
44
57
|
}
|
|
45
58
|
|
|
46
|
-
/**
|
|
59
|
+
/** Collapse whitespace so the same claim, however spaced, counts once. */
|
|
60
|
+
export function normalizeClaim(text: string): string {
|
|
61
|
+
return text.normalize('NFKC').trim().replace(/\s+/g, ' ').toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function deadlineDate(deadline: string): string {
|
|
65
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(deadline) ? deadline : dateKey(deadline, { timeZone: KST });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Unsent items created strictly before the slot instant (KST).
|
|
70
|
+
* `08:30` and `22:00` are that clock today. `after-release` has no fixed
|
|
71
|
+
* clock — a release can land at any hour — so its instant is `now`.
|
|
72
|
+
* Tomorrow's items stay out of every slot.
|
|
73
|
+
*/
|
|
74
|
+
export function beforeSlot(createdAt: string, slot: BriefSlot, now: Date): boolean {
|
|
75
|
+
const createdMs = Date.parse(createdAt);
|
|
76
|
+
if (!Number.isFinite(createdMs)) return false;
|
|
77
|
+
const today = dateKey(now, { timeZone: KST });
|
|
78
|
+
const cutoff = slot === 'after-release' ? now.getTime() : Date.parse(`${today}T${slot}:00+09:00`);
|
|
79
|
+
return createdMs < cutoff;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function needsDecision(item: BriefItem, today: string): boolean {
|
|
83
|
+
return item.priority === 'P0' || (item.deadline !== null && deadlineDate(item.deadline) === today);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* One briefing page. Item text is copied verbatim.
|
|
88
|
+
* Order: decisions due (P0, then a deadline of today) → domain groups →
|
|
89
|
+
* duplicate claims dropped (same normalized sentence, most urgent kept).
|
|
90
|
+
*/
|
|
91
|
+
export function composeBriefMarkdown(items: readonly BriefItem[], slot: BriefSlot, now: Date): string {
|
|
92
|
+
const today = dateKey(now, { timeZone: KST });
|
|
93
|
+
const eligible = items.filter(item => item.sent_at === null && beforeSlot(item.created_at, slot, now));
|
|
94
|
+
// 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
|
+
const rank = (item: BriefItem) => [needsDecision(item, today) ? 0 : 1, BRIEF_PRIORITIES.indexOf(item.priority), item.id];
|
|
96
|
+
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]!; };
|
|
97
|
+
const best = new Map<string, BriefItem>();
|
|
98
|
+
for (const item of eligible) {
|
|
99
|
+
const key = normalizeClaim(item.text);
|
|
100
|
+
const kept = best.get(key);
|
|
101
|
+
if (!kept || better(item, kept) < 0) best.set(key, item);
|
|
102
|
+
}
|
|
103
|
+
const unique = [...best.values()].sort((a, b) => a.id - b.id);
|
|
104
|
+
const decisions = unique.filter(item => needsDecision(item, today))
|
|
105
|
+
.sort((a, b) => Number(b.priority === 'P0') - Number(a.priority === 'P0') || a.id - b.id);
|
|
106
|
+
const decisionIds = new Set(decisions.map(item => item.id));
|
|
107
|
+
const lines = [`# 브리핑 — ${slot}`, ''];
|
|
108
|
+
if (decisions.length > 0) {
|
|
109
|
+
lines.push('## 결정이 필요한 것', '');
|
|
110
|
+
for (const item of decisions) lines.push(itemLine(item));
|
|
111
|
+
lines.push('');
|
|
112
|
+
}
|
|
113
|
+
for (const domain of BRIEF_DOMAINS) {
|
|
114
|
+
const group = unique.filter(item => item.domain === domain && !decisionIds.has(item.id));
|
|
115
|
+
if (group.length === 0) continue;
|
|
116
|
+
lines.push(`## ${domain}`, '');
|
|
117
|
+
for (const item of group.sort((a, b) => a.id - b.id)) lines.push(itemLine(item));
|
|
118
|
+
lines.push('');
|
|
119
|
+
}
|
|
120
|
+
return `${lines.join('\n').trimEnd()}\n`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function itemLine(item: BriefItem): string {
|
|
124
|
+
const extra = [
|
|
125
|
+
item.deadline ? `마감: ${item.deadline}` : '',
|
|
126
|
+
item.evidence ? `근거: ${item.evidence}` : '',
|
|
127
|
+
`출처: ${item.source}`,
|
|
128
|
+
].filter(Boolean).join(' · ');
|
|
129
|
+
return `- [${item.priority}] ${item.text} (${extra})`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The first ledger (#24193) used priorities high/medium/low and had no sent_at column. Keep its rows: move the old
|
|
134
|
+
* table aside and copy them into the current shape (high→P0 · medium→P1 · low→P2) — append-only, nothing is lost.
|
|
135
|
+
*/
|
|
136
|
+
function migrateLegacyItems(db: Database, log: (category: string, event: string, data?: unknown) => void): void {
|
|
137
|
+
const columns = db.query("SELECT name FROM pragma_table_info('items')").all() as { name: string }[];
|
|
138
|
+
if (columns.length === 0 || columns.some(column => column.name === 'sent_at')) return;
|
|
139
|
+
db.transaction(() => {
|
|
140
|
+
db.exec('DROP TRIGGER IF EXISTS items_no_update; DROP TRIGGER IF EXISTS items_no_delete; ALTER TABLE items RENAME TO items_v1;');
|
|
141
|
+
db.exec(`CREATE TABLE items (
|
|
142
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
143
|
+
text TEXT NOT NULL,
|
|
144
|
+
domain TEXT NOT NULL CHECK(domain IN ('운영', '판', '흡수', '시장', '행정', '코나투스')),
|
|
145
|
+
priority TEXT NOT NULL CHECK(priority IN ('P0', 'P1', 'P2')),
|
|
146
|
+
deadline TEXT,
|
|
147
|
+
evidence TEXT,
|
|
148
|
+
source TEXT NOT NULL,
|
|
149
|
+
created_at TEXT NOT NULL,
|
|
150
|
+
sent_at TEXT
|
|
151
|
+
)`);
|
|
152
|
+
db.exec(`INSERT INTO items (id, text, domain, priority, deadline, evidence, source, created_at, sent_at)
|
|
153
|
+
SELECT id, text,
|
|
154
|
+
CASE WHEN domain IN ('운영', '판', '흡수', '시장', '행정', '코나투스') THEN domain ELSE '운영' END,
|
|
155
|
+
CASE priority WHEN 'high' THEN 'P0' WHEN 'medium' THEN 'P1' ELSE 'P2' END,
|
|
156
|
+
deadline, evidence,
|
|
157
|
+
-- An unknown legacy domain keeps its meaning in the source label instead of vanishing (ACP must-fix).
|
|
158
|
+
CASE WHEN domain IN ('운영', '판', '흡수', '시장', '행정', '코나투스') THEN source ELSE source || ' · 옛 도메인 ' || domain END,
|
|
159
|
+
created_at, NULL FROM items_v1`);
|
|
160
|
+
})();
|
|
161
|
+
log('briefing.items', 'migrated-legacy', { rows: (db.query('SELECT COUNT(*) AS n FROM items_v1').get() as { n: number }).n });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Append-only briefing item ledger at `<state>/briefing/items.sqlite`. */
|
|
47
165
|
export class BriefItemsLedger {
|
|
48
166
|
readonly path: string;
|
|
49
167
|
private readonly now: () => Date;
|
|
168
|
+
private readonly log: (category: string, event: string, data?: unknown) => void;
|
|
50
169
|
|
|
51
170
|
constructor(options: BriefItemsOptions = {}) {
|
|
52
171
|
this.path = join(options.stateDir ?? elanousStateRoot(), 'briefing', 'items.sqlite');
|
|
53
172
|
this.now = options.now ?? (() => new Date());
|
|
173
|
+
this.log = options.log ?? ((category, event, data) => debug.log(category, event, data));
|
|
54
174
|
}
|
|
55
175
|
|
|
56
176
|
private using<T>(work: (db: Database) => T): T {
|
|
@@ -59,16 +179,27 @@ export class BriefItemsLedger {
|
|
|
59
179
|
try {
|
|
60
180
|
chmodSync(this.path, 0o600);
|
|
61
181
|
db.exec('PRAGMA busy_timeout = 10000');
|
|
182
|
+
migrateLegacyItems(db, this.log);
|
|
62
183
|
db.exec(`CREATE TABLE IF NOT EXISTS items (
|
|
63
184
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
64
|
-
text TEXT NOT NULL,
|
|
65
|
-
|
|
66
|
-
|
|
185
|
+
text TEXT NOT NULL,
|
|
186
|
+
domain TEXT NOT NULL CHECK(domain IN ('운영', '판', '흡수', '시장', '행정', '코나투스')),
|
|
187
|
+
priority TEXT NOT NULL CHECK(priority IN ('P0', 'P1', 'P2')),
|
|
188
|
+
deadline TEXT,
|
|
189
|
+
evidence TEXT,
|
|
190
|
+
source TEXT NOT NULL,
|
|
191
|
+
created_at TEXT NOT NULL,
|
|
192
|
+
sent_at TEXT
|
|
67
193
|
);
|
|
68
194
|
CREATE TRIGGER IF NOT EXISTS items_no_update BEFORE UPDATE ON items
|
|
69
195
|
BEGIN SELECT RAISE(ABORT, 'brief items are immutable'); END;
|
|
70
196
|
CREATE TRIGGER IF NOT EXISTS items_no_delete BEFORE DELETE ON items
|
|
71
|
-
BEGIN SELECT RAISE(ABORT, 'brief items are immutable'); END
|
|
197
|
+
BEGIN SELECT RAISE(ABORT, 'brief items are immutable'); END;
|
|
198
|
+
CREATE TABLE IF NOT EXISTS sends (
|
|
199
|
+
item_id INTEGER NOT NULL REFERENCES items(id),
|
|
200
|
+
slot TEXT NOT NULL,
|
|
201
|
+
sent_at TEXT NOT NULL
|
|
202
|
+
);`);
|
|
72
203
|
return work(db);
|
|
73
204
|
} finally {
|
|
74
205
|
db.close();
|
|
@@ -77,45 +208,69 @@ export class BriefItemsLedger {
|
|
|
77
208
|
|
|
78
209
|
add(input: AddBriefItem): BriefItem {
|
|
79
210
|
const text = required(input.text, 'text');
|
|
80
|
-
|
|
211
|
+
// A brief item is one claim line; a line break would break the composed Markdown structure (ACP must-fix).
|
|
212
|
+
if (/[\r\n]/.test(text)) throw new BriefItemsInputError('text must be a single line');
|
|
213
|
+
if (!(BRIEF_DOMAINS as readonly string[]).includes(input.domain)) throw new BriefItemsInputError('invalid domain');
|
|
214
|
+
if (!(BRIEF_PRIORITIES as readonly string[]).includes(input.priority)) throw new BriefItemsInputError('invalid priority');
|
|
81
215
|
const source = required(input.source, 'source');
|
|
82
|
-
if (!['high', 'medium', 'low'].includes(input.priority)) throw new BriefItemsInputError('invalid priority');
|
|
83
216
|
const deadline = optional(input.deadline, 'deadline');
|
|
217
|
+
// Every field that reaches the composed Markdown is one line, so no stored item can forge a heading or entry (ACP must-fix).
|
|
218
|
+
for (const [field, value] of [['source', source], ['evidence', input.evidence ?? null], ['deadline', deadline]] as const) {
|
|
219
|
+
if (value !== null && /[\r\n]/.test(value)) throw new BriefItemsInputError(`${field} must be a single line`);
|
|
220
|
+
}
|
|
84
221
|
if (deadline !== null) {
|
|
85
222
|
const format = /^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2}))?$/;
|
|
86
223
|
if (!format.test(deadline) || !Number.isFinite(Date.parse(deadline))) throw new BriefItemsInputError('invalid deadline');
|
|
87
224
|
}
|
|
88
225
|
const evidence = optional(input.evidence, 'evidence');
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
226
|
+
const createdAt = input.createdAt ?? this.now().toISOString();
|
|
227
|
+
if (!Number.isFinite(Date.parse(createdAt))) throw new BriefItemsInputError('invalid createdAt');
|
|
228
|
+
const item = this.using(db => {
|
|
229
|
+
const result = db.query(
|
|
230
|
+
'INSERT INTO items (text, domain, priority, deadline, evidence, source, created_at, sent_at) VALUES (?, ?, ?, ?, ?, ?, ?, NULL)',
|
|
231
|
+
).run(text, input.domain, input.priority, deadline, evidence, source, createdAt);
|
|
232
|
+
return db.query(
|
|
233
|
+
'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 WHERE id = ?',
|
|
234
|
+
).get(result.lastInsertRowid) as BriefItem;
|
|
95
235
|
});
|
|
236
|
+
this.log('briefing.items', 'added', { id: item.id, domain: item.domain, priority: item.priority, source: item.source });
|
|
237
|
+
return item;
|
|
96
238
|
}
|
|
97
239
|
|
|
98
|
-
/**
|
|
99
|
-
list(filters: { domain?:
|
|
100
|
-
|
|
101
|
-
? db.query('SELECT id, text, domain, priority, deadline, evidence, source, created_at FROM items').all()
|
|
102
|
-
: db.query('SELECT id, text, domain, priority, deadline, evidence, source, created_at FROM items WHERE domain = ?').all(filters.domain)) as BriefItem[]);
|
|
103
|
-
const rank: Record<BriefPriority, number> = { high: 0, medium: 1, low: 2 };
|
|
104
|
-
return items.sort((a, b) => rank[a.priority] - rank[b.priority]
|
|
105
|
-
|| Number(a.deadline === null) - Number(b.deadline === null)
|
|
106
|
-
|| (a.deadline !== null && b.deadline !== null ? Date.parse(a.deadline) - Date.parse(b.deadline) : 0)
|
|
107
|
-
|| a.id - b.id);
|
|
240
|
+
/** Insertion order. Optional domain filter. Item text is the stored original. */
|
|
241
|
+
list(filters: { domain?: BriefDomain } = {}): BriefItem[] {
|
|
242
|
+
return this.using(db => (filters.domain === undefined
|
|
243
|
+
? 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()
|
|
244
|
+
: 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 WHERE domain = ? ORDER BY id').all(filters.domain)) as BriefItem[]);
|
|
108
245
|
}
|
|
109
246
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
247
|
+
/**
|
|
248
|
+
* 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.
|
|
250
|
+
*/
|
|
251
|
+
compose(slot: string): string {
|
|
252
|
+
if (!(BRIEF_SLOTS as readonly string[]).includes(slot)) throw new BriefItemsInputError('invalid slot');
|
|
253
|
+
const items = this.list();
|
|
254
|
+
const markdown = composeBriefMarkdown(items, slot as BriefSlot, this.now());
|
|
255
|
+
const included = items.filter(item => item.sent_at === null && beforeSlot(item.created_at, slot as BriefSlot, this.now()));
|
|
256
|
+
this.log('briefing.items', 'composed', { slot, candidates: included.length, bytes: Buffer.byteLength(markdown) });
|
|
257
|
+
return markdown;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Records that these items went out in a slot (append-only); later composes leave them out (ACP must-fix).
|
|
262
|
+
* Only an actual sender calls this — compose itself stays read-only (no flag marks items it did not send).
|
|
263
|
+
*/
|
|
264
|
+
markSent(ids: readonly number[], slot: BriefSlot): number {
|
|
265
|
+
if (!(BRIEF_SLOTS as readonly string[]).includes(slot)) throw new BriefItemsInputError('invalid slot');
|
|
266
|
+
const at = this.now().toISOString();
|
|
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;
|
|
120
275
|
}
|
|
121
276
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// `elanous usage outcomes` — 런 원장의 childProvider × 결과.
|
|
2
|
+
// provider 가 기록되지 않은 런은 «불명». 이름을 지어 세지 않는다.
|
|
3
|
+
|
|
4
|
+
import { readdirSync } from 'node:fs';
|
|
5
|
+
import {
|
|
6
|
+
loadRunLedger,
|
|
7
|
+
runLedgerDir,
|
|
8
|
+
type RunLedgerEntry,
|
|
9
|
+
type RunLedgerReader,
|
|
10
|
+
} from '../self-implement/run-ledger.js';
|
|
11
|
+
|
|
12
|
+
export const UNKNOWN_PROVIDER_LABEL = '불명';
|
|
13
|
+
|
|
14
|
+
export type OutcomeKind = 'landed' | 'draft' | 'failed' | 'open';
|
|
15
|
+
|
|
16
|
+
export interface ProviderOutcomeRow {
|
|
17
|
+
readonly provider: string;
|
|
18
|
+
readonly runs: number;
|
|
19
|
+
readonly landed: number;
|
|
20
|
+
readonly draft: number;
|
|
21
|
+
readonly failed: number;
|
|
22
|
+
readonly open: number;
|
|
23
|
+
/** landed / runs. 런이 0이면 0. */
|
|
24
|
+
readonly unattendedLandRate: number;
|
|
25
|
+
readonly rounds: number;
|
|
26
|
+
readonly blockReasons: Readonly<Record<string, number>>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface UsageOutcomesReport {
|
|
30
|
+
readonly sinceDays: number;
|
|
31
|
+
readonly sinceMs: number;
|
|
32
|
+
readonly rows: readonly ProviderOutcomeRow[];
|
|
33
|
+
readonly unreadableLedgers: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UsageOutcomesDeps {
|
|
37
|
+
readonly dir?: string;
|
|
38
|
+
readonly nowMs?: number;
|
|
39
|
+
readonly list?: (path: string) => string[];
|
|
40
|
+
readonly read?: RunLedgerReader;
|
|
41
|
+
readonly load?: (runId: string, dir: string, read?: RunLedgerReader) => RunLedgerEntry[] | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const LEDGER_FILE = /\.jsonl$/;
|
|
45
|
+
|
|
46
|
+
function stringField(data: Record<string, unknown>, key: string): string | undefined {
|
|
47
|
+
const value = data[key];
|
|
48
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function entryMs(entry: RunLedgerEntry): number | undefined {
|
|
52
|
+
if (!entry.timestamp) return undefined;
|
|
53
|
+
const ms = Date.parse(entry.timestamp);
|
|
54
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 기록된 childProvider 만. 없거나 공백이면 «불명». */
|
|
58
|
+
export function providerLabel(entries: readonly RunLedgerEntry[]): string {
|
|
59
|
+
const origin = entries.find((entry) => entry.event === 'run-origin');
|
|
60
|
+
const recorded = origin ? stringField(origin.data, 'childProvider') : undefined;
|
|
61
|
+
return recorded ?? UNKNOWN_PROVIDER_LABEL;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function blockReason(entry: RunLedgerEntry): string | undefined {
|
|
65
|
+
return stringField(entry.data, 'stopReason')
|
|
66
|
+
?? stringField(entry.data, 'reason')
|
|
67
|
+
?? stringField(entry.data, 'verdict')
|
|
68
|
+
?? stringField(entry.data, 'error');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface ClassifiedRun {
|
|
72
|
+
readonly provider: string;
|
|
73
|
+
readonly outcome: OutcomeKind;
|
|
74
|
+
readonly rounds: number;
|
|
75
|
+
readonly blockReason?: string;
|
|
76
|
+
readonly atMs: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function classifyLedgerRun(entries: readonly RunLedgerEntry[], nowMs: number): ClassifiedRun | null {
|
|
80
|
+
if (entries.length === 0) return null;
|
|
81
|
+
const times = entries.map(entryMs).filter((ms): ms is number => ms !== undefined);
|
|
82
|
+
const atMs = times.length > 0 ? Math.max(...times) : nowMs;
|
|
83
|
+
const rounds = new Set(
|
|
84
|
+
entries
|
|
85
|
+
.map((entry) => entry.data.round)
|
|
86
|
+
.filter((round): round is number => typeof round === 'number' && Number.isFinite(round)),
|
|
87
|
+
).size;
|
|
88
|
+
const merged = [...entries].reverse().find((entry) => entry.event === 'merged' && entry.data.merged === true);
|
|
89
|
+
const drafted = [...entries].reverse().find((entry) => entry.event === 'pr-opened' && entry.data.draft !== false);
|
|
90
|
+
const failed = [...entries].reverse().find((entry) => entry.event === 'failed' || entry.event === 'run-status' && (entry.data.runStatus === 'failed' || entry.data.runStatus === 'stopped'));
|
|
91
|
+
const outcome: OutcomeKind = merged ? 'landed' : drafted ? 'draft' : failed ? 'failed' : 'open';
|
|
92
|
+
const reason = outcome === 'failed' || outcome === 'open'
|
|
93
|
+
? (failed ? blockReason(failed) : undefined)
|
|
94
|
+
?? entries.map(blockReason).reverse().find((value) => value !== undefined)
|
|
95
|
+
: undefined;
|
|
96
|
+
return {
|
|
97
|
+
provider: providerLabel(entries),
|
|
98
|
+
outcome,
|
|
99
|
+
rounds,
|
|
100
|
+
...(reason ? { blockReason: reason } : {}),
|
|
101
|
+
atMs,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function aggregateUsageOutcomes(
|
|
106
|
+
runs: readonly ClassifiedRun[],
|
|
107
|
+
sinceDays: number,
|
|
108
|
+
sinceMs: number,
|
|
109
|
+
unreadableLedgers = 0,
|
|
110
|
+
): UsageOutcomesReport {
|
|
111
|
+
const byProvider = new Map<string, {
|
|
112
|
+
runs: number;
|
|
113
|
+
landed: number;
|
|
114
|
+
draft: number;
|
|
115
|
+
failed: number;
|
|
116
|
+
open: number;
|
|
117
|
+
rounds: number;
|
|
118
|
+
blockReasons: Record<string, number>;
|
|
119
|
+
}>();
|
|
120
|
+
for (const run of runs) {
|
|
121
|
+
const row = byProvider.get(run.provider) ?? {
|
|
122
|
+
runs: 0, landed: 0, draft: 0, failed: 0, open: 0, rounds: 0, blockReasons: {},
|
|
123
|
+
};
|
|
124
|
+
row.runs += 1;
|
|
125
|
+
row[run.outcome] += 1;
|
|
126
|
+
row.rounds += run.rounds;
|
|
127
|
+
if (run.blockReason && run.outcome !== 'landed' && run.outcome !== 'draft') {
|
|
128
|
+
row.blockReasons[run.blockReason] = (row.blockReasons[run.blockReason] ?? 0) + 1;
|
|
129
|
+
}
|
|
130
|
+
byProvider.set(run.provider, row);
|
|
131
|
+
}
|
|
132
|
+
const rows = [...byProvider.entries()]
|
|
133
|
+
.map(([provider, row]): ProviderOutcomeRow => ({
|
|
134
|
+
provider,
|
|
135
|
+
...row,
|
|
136
|
+
unattendedLandRate: row.runs === 0 ? 0 : row.landed / row.runs,
|
|
137
|
+
}))
|
|
138
|
+
.sort((a, b) => a.provider.localeCompare(b.provider, 'en'));
|
|
139
|
+
return { sinceDays, sinceMs, rows, unreadableLedgers };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function queryUsageOutcomes(sinceDays: number, deps: UsageOutcomesDeps = {}): UsageOutcomesReport {
|
|
143
|
+
const days = Number.isFinite(sinceDays) && sinceDays > 0 ? sinceDays : 2;
|
|
144
|
+
const nowMs = deps.nowMs ?? Date.now();
|
|
145
|
+
const sinceMs = nowMs - days * 24 * 60 * 60 * 1000;
|
|
146
|
+
const dir = deps.dir ?? runLedgerDir();
|
|
147
|
+
const list = deps.list ?? readdirSync;
|
|
148
|
+
const load = deps.load ?? loadRunLedger;
|
|
149
|
+
let names: string[];
|
|
150
|
+
try {
|
|
151
|
+
names = list(dir);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
154
|
+
return aggregateUsageOutcomes([], days, sinceMs, 0);
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
const runs: ClassifiedRun[] = [];
|
|
159
|
+
let unreadableLedgers = 0;
|
|
160
|
+
for (const name of names) {
|
|
161
|
+
if (!LEDGER_FILE.test(name)) continue;
|
|
162
|
+
const runId = name.slice(0, -'.jsonl'.length);
|
|
163
|
+
let ledger: RunLedgerEntry[] | null;
|
|
164
|
+
try {
|
|
165
|
+
ledger = load(runId, dir, deps.read);
|
|
166
|
+
} catch {
|
|
167
|
+
unreadableLedgers += 1;
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (!ledger) {
|
|
171
|
+
unreadableLedgers += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const classified = classifyLedgerRun(ledger, nowMs);
|
|
175
|
+
if (!classified || classified.atMs < sinceMs) continue;
|
|
176
|
+
runs.push(classified);
|
|
177
|
+
}
|
|
178
|
+
return aggregateUsageOutcomes(runs, days, sinceMs, unreadableLedgers);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function formatUsageOutcomes(report: UsageOutcomesReport): string {
|
|
182
|
+
const header = `최근 ${report.sinceDays}일 · 런 원장 childProvider × 결과`;
|
|
183
|
+
const verdict = outcomeTableHasCodexGrokUnknown(report)
|
|
184
|
+
? '판정: codex·grok·불명'
|
|
185
|
+
: '판정: codex·grok·불명 아님';
|
|
186
|
+
if (report.rows.length === 0) return `${header}\n${verdict}\n(해당 런 없음)`;
|
|
187
|
+
const lines = [header, verdict, 'provider runs landed draft failed open landRate rounds blockReasons'];
|
|
188
|
+
for (const row of report.rows) {
|
|
189
|
+
const reasons = Object.entries(row.blockReasons)
|
|
190
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
191
|
+
.map(([reason, count]) => `${reason}=${count}`)
|
|
192
|
+
.join(',');
|
|
193
|
+
const rate = `${Math.round(row.unattendedLandRate * 1000) / 10}%`;
|
|
194
|
+
lines.push(`${row.provider} ${row.runs} ${row.landed} ${row.draft} ${row.failed} ${row.open} ${rate} ${row.rounds} ${reasons || '-'}`);
|
|
195
|
+
}
|
|
196
|
+
if (report.unreadableLedgers > 0) lines.push(`unreadable ledgers: ${report.unreadableLedgers}`);
|
|
197
|
+
return lines.join('\n');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 판정이 요구하는 최근 이틀 표에 codex·grok·불명 세 줄이 있는지. */
|
|
201
|
+
export function outcomeTableHasCodexGrokUnknown(report: UsageOutcomesReport): boolean {
|
|
202
|
+
const names = new Set(report.rows.map((row) => row.provider));
|
|
203
|
+
return names.has('codex') && names.has('grok') && names.has(UNKNOWN_PROVIDER_LABEL) && report.rows.length >= 3;
|
|
204
|
+
}
|
package/src/cli/autopilot-cli.ts
CHANGED
|
@@ -7,9 +7,75 @@ import { writeStdoutJson } from './stdout-json.js';
|
|
|
7
7
|
// AutopilotLoopDriver against the mission, prints agent deltas on
|
|
8
8
|
// stdout + envelope/termination summary on stderr.
|
|
9
9
|
export function registerAutopilotCommands(program: Command): void {
|
|
10
|
+
// Explicit runtime entry for a selected action; the task chooser can invoke this entry.
|
|
10
11
|
const autopilotCmd = program
|
|
11
12
|
.command('autopilot')
|
|
12
13
|
.description('Mission-driven autopilot — runs an ACP agent against a single mission with safety + budget guards');
|
|
14
|
+
autopilotCmd.command('task-agent-action <actionJson>')
|
|
15
|
+
.description('Execute a selected task-agent action; defaults to shadow')
|
|
16
|
+
.action(async (actionJson: string) => {
|
|
17
|
+
const { executeNextAction } = await import('../task-agent/actions.js');
|
|
18
|
+
const { getUserConfig } = await import('../user-config.js');
|
|
19
|
+
const { spawnSync } = await import('node:child_process');
|
|
20
|
+
const { readFileSync, writeFileSync, mkdirSync } = await import('node:fs');
|
|
21
|
+
const { join, dirname } = await import('node:path');
|
|
22
|
+
const { debug } = await import('../debug/log.js');
|
|
23
|
+
const { withFileLockSync } = await import('../storage/file-lock.js');
|
|
24
|
+
const config = getUserConfig() as ReturnType<typeof getUserConfig> & { taskAgent?: { mode?: string } };
|
|
25
|
+
const { effectiveInstanceRoot } = await import('../instance/resolve.js');
|
|
26
|
+
const statePath = join(effectiveInstanceRoot(), 'task-agent-actions.json');
|
|
27
|
+
let state: { landingDay?: string; landingsToday?: number; failureCounts?: Record<string, number> } = {};
|
|
28
|
+
try { state = JSON.parse(readFileSync(statePath, 'utf8')); } catch { /* first run */ }
|
|
29
|
+
const action = JSON.parse(actionJson) as import('../task-agent/actions.js').NextAction;
|
|
30
|
+
const deps: import('../task-agent/actions.js').ActionDeps = {
|
|
31
|
+
...state,
|
|
32
|
+
failureCounts: state.failureCounts ?? {},
|
|
33
|
+
saveState: () => {
|
|
34
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
35
|
+
// 실패 횟수는 incrementFailure 가 잠금 안에서 키별로 더한다 — 여기선 덮어쓰지 않는다.
|
|
36
|
+
},
|
|
37
|
+
mode: config.taskAgent?.mode,
|
|
38
|
+
// 하루 착지 칸은 상태 파일 잠금 안에서 확보·반환한다 — 두 CLI 프로세스가 같은 잔여 칸을 쓰지 않게.
|
|
39
|
+
reserveLanding: (day, limit) => withFileLockSync(`${statePath}.lock`, () => {
|
|
40
|
+
let disk: typeof state = {};
|
|
41
|
+
try { disk = JSON.parse(readFileSync(statePath, 'utf8')); } catch { /* first */ }
|
|
42
|
+
const used = disk.landingDay === day ? disk.landingsToday ?? 0 : 0;
|
|
43
|
+
if (used >= limit) return false;
|
|
44
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
45
|
+
writeFileSync(statePath, JSON.stringify({ ...disk, landingDay: day, landingsToday: used + 1 }));
|
|
46
|
+
deps.landingDay = day; deps.landingsToday = used + 1;
|
|
47
|
+
return true;
|
|
48
|
+
}),
|
|
49
|
+
incrementFailure: (key) => withFileLockSync(`${statePath}.lock`, () => {
|
|
50
|
+
let disk: typeof state = {};
|
|
51
|
+
try { disk = JSON.parse(readFileSync(statePath, 'utf8')); } catch { /* first */ }
|
|
52
|
+
const counts = { ...(disk.failureCounts ?? {}) };
|
|
53
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
54
|
+
mkdirSync(dirname(statePath), { recursive: true });
|
|
55
|
+
writeFileSync(statePath, JSON.stringify({ ...disk, failureCounts: counts }));
|
|
56
|
+
return counts[key]!;
|
|
57
|
+
}),
|
|
58
|
+
releaseLanding: (day) => withFileLockSync(`${statePath}.lock`, () => {
|
|
59
|
+
let disk: typeof state = {};
|
|
60
|
+
try { disk = JSON.parse(readFileSync(statePath, 'utf8')); } catch { return; }
|
|
61
|
+
if (disk.landingDay !== day || !disk.landingsToday) return;
|
|
62
|
+
writeFileSync(statePath, JSON.stringify({ ...disk, landingsToday: disk.landingsToday - 1 }));
|
|
63
|
+
deps.landingsToday = disk.landingsToday - 1;
|
|
64
|
+
}),
|
|
65
|
+
observe: (_event, data) => debug.log('task-agent', 'action', data),
|
|
66
|
+
command: async args => {
|
|
67
|
+
const p = args[0] === 'gh'
|
|
68
|
+
// PR 조회는 착지할 작업 트리에서 — 다른 저장소에서 불러도 엉뚱한 PR 을 읽지 않게.
|
|
69
|
+
? spawnSync('gh', args.slice(1), { encoding: 'utf8', ...(action.cwd ? { cwd: action.cwd } : {}) })
|
|
70
|
+
// 지금 돌고 있는 진입점으로 부른다 — 저장소 밖(설치본)에서 불러도 같은 elanous 를 쓴다.
|
|
71
|
+
: spawnSync(process.execPath, [process.argv[1]!, ...(process.argv.includes('--test') ? ['--test'] : []), ...args], { encoding: 'utf8' });
|
|
72
|
+
return { status: p.status ?? 1, stdout: p.stdout ?? '', stderr: p.stderr ?? '' };
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const result = await executeNextAction(action, deps);
|
|
76
|
+
if (config.taskAgent?.mode === 'live') deps.saveState?.();
|
|
77
|
+
process.stdout.write(`${result}\n`);
|
|
78
|
+
});
|
|
13
79
|
|
|
14
80
|
autopilotCmd
|
|
15
81
|
.command('run <mission...>')
|
|
@@ -0,0 +1,73 @@
|
|
|
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';
|
|
3
|
+
|
|
4
|
+
export interface BriefCliDeps {
|
|
5
|
+
ledger?: BriefItemsLedger;
|
|
6
|
+
stateDir?: string;
|
|
7
|
+
now?: () => Date;
|
|
8
|
+
output?: (line: string) => void;
|
|
9
|
+
error?: (line: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function registerBriefCommands(program: Command, deps: BriefCliDeps = {}): void {
|
|
13
|
+
const output = deps.output ?? ((line: string) => console.log(line));
|
|
14
|
+
const error = deps.error ?? ((line: string) => console.error(line));
|
|
15
|
+
const ledger = () => deps.ledger ?? new BriefItemsLedger({
|
|
16
|
+
...(deps.stateDir ? { stateDir: deps.stateDir } : {}),
|
|
17
|
+
...(deps.now ? { now: deps.now } : {}),
|
|
18
|
+
});
|
|
19
|
+
const fail = (verb: string, cause: unknown) => {
|
|
20
|
+
error(`brief ${verb}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
21
|
+
process.exitCode = cause instanceof BriefItemsInputError ? 2 : 1;
|
|
22
|
+
};
|
|
23
|
+
const brief = program.command('brief').description('대표 브리핑 항목 원장 — 발신자는 항목만 내고, 발송은 하지 않는다');
|
|
24
|
+
|
|
25
|
+
brief.command('add')
|
|
26
|
+
.description('주장 한 줄을 원장에 추가한다 (원문 그대로)')
|
|
27
|
+
.requiredOption('--text <text>', '주장 한 줄')
|
|
28
|
+
.requiredOption('--domain <domain>', `도메인 (${BRIEF_DOMAINS.join('|')})`)
|
|
29
|
+
.requiredOption('--priority <priority>', `중요도 (${BRIEF_PRIORITIES.join('|')})`)
|
|
30
|
+
.requiredOption('--source <source>', '출처 (루프·자리·흡수·코나투스)')
|
|
31
|
+
.option('--deadline <deadline>', '마감 (YYYY-MM-DD 또는 ISO)')
|
|
32
|
+
.option('--evidence <evidence>', '근거 링크')
|
|
33
|
+
.option('--created-at <iso>', '작성 시각 (ISO, 생략 시 지금)')
|
|
34
|
+
.option('--json', 'JSON 출력')
|
|
35
|
+
.action((opts: { text: string; domain: string; priority: string; source: string; deadline?: string; evidence?: string; createdAt?: string; json?: boolean }) => {
|
|
36
|
+
try {
|
|
37
|
+
const item = ledger().add({
|
|
38
|
+
text: opts.text,
|
|
39
|
+
domain: opts.domain as BriefDomain,
|
|
40
|
+
priority: opts.priority as BriefPriority,
|
|
41
|
+
source: opts.source,
|
|
42
|
+
...(opts.deadline !== undefined ? { deadline: opts.deadline } : {}),
|
|
43
|
+
...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}),
|
|
44
|
+
...(opts.createdAt !== undefined ? { createdAt: opts.createdAt } : {}),
|
|
45
|
+
});
|
|
46
|
+
output(opts.json ? JSON.stringify(item) : `added ${item.id} [${item.priority}] ${item.domain}`);
|
|
47
|
+
} catch (cause) { fail('add', cause); }
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
brief.command('list')
|
|
51
|
+
.description('원장 항목을 추가 순으로 본다')
|
|
52
|
+
.option('--domain <domain>', `도메인 필터 (${BRIEF_DOMAINS.join('|')})`)
|
|
53
|
+
.option('--json', 'JSON 출력')
|
|
54
|
+
.action((opts: { domain?: string; json?: boolean }) => {
|
|
55
|
+
try {
|
|
56
|
+
if (opts.domain !== undefined && !(BRIEF_DOMAINS as readonly string[]).includes(opts.domain)) {
|
|
57
|
+
throw new BriefItemsInputError('invalid domain');
|
|
58
|
+
}
|
|
59
|
+
const items = ledger().list(opts.domain === undefined ? {} : { domain: opts.domain as BriefDomain });
|
|
60
|
+
if (opts.json) output(JSON.stringify(items));
|
|
61
|
+
else if (items.length === 0) output('(항목 없음)');
|
|
62
|
+
else for (const item of items) output(`${item.id}\t${item.priority}\t${item.domain}\t${item.source}\t${item.text}`);
|
|
63
|
+
} catch (cause) { fail('list', cause); }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
brief.command('compose')
|
|
67
|
+
.description('슬롯 이전의 미발송 항목을 한 장 마크다운으로 만든다 (표준 출력만, 발송 없음)')
|
|
68
|
+
.requiredOption('--slot <slot>', `슬롯 (${BRIEF_SLOTS.join('|')})`)
|
|
69
|
+
.action((opts: { slot: string }) => {
|
|
70
|
+
try { output(ledger().compose(opts.slot)); }
|
|
71
|
+
catch (cause) { fail('compose', cause); }
|
|
72
|
+
});
|
|
73
|
+
}
|