elanous 0.2.27 → 0.2.28
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/{1454-a9c338f5bb556f42.js → 1454-29d1d773d5996a49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{599-a914cc0468f56143.js → 599-56f2c0a5cc4a7295.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-5ab1e47978577c5e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-07110da72c45e68f.js → page-24453f6a302816b4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/{page-d2d76d4a7eae91bd.js → page-6c131d30bb1cf871.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-fa46d4588e8442c2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-e5a9302c105725a3.js → page-dc0772b11861af51.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-66ad46ed0279feb9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-5159dc8752c6cd29.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c5a2478861c08b87.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/page-f722a9dec084e08b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-6a16a48d1aa6e91f.js → page-256ffd365d32e100.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-e0de78ae7b99f01c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-39f82acf619cca23.js → page-4c741eb760f2f77c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-7c2c8f81b185f9c0.js → page-e9ba98f5f1440a21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/{page-ae7aff07e32676bb.js → page-dafc901bb1b72b5f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-482c60c1bc540d0a.js → page-2f6f303fa114ec24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-7cb996c1aa08b6f5.js → layout-1634081c5272006c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/{page-623e08fe4477d606.js → page-ced0bcfc6542b3fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-db460cd8aecb8490.js → page-bb1f05cd51c38944.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-ff27902d0b11ef4f.js → page-646b50f63b6fe732.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-db069b48a59c8c00.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-1917ea87ea71ab6b.js → page-f736324afa2117c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-25a302273b054fd7.js → page-6f131b51193a36f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-670510eda8b9443f.js → not-found-435f5e2faf02bc01.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-497e0eedf20e7890.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-a8d28fdf995e16ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4bb7556e2e97a750.js → page-8966452228753c5e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-fd5d18656cb432b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-5663779cb1a37609.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/{page-4eb96fc7241c2456.js → page-66a67eb19ebd976b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-3b492aa78cf3c4a6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-0d64ff212dc55d03.js → page-c71267e7681da982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-7f096248993fc2d4.js → page-a21d1e1797172692.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-e47fc72049149a38.js → page-cb249e81ed339318.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-f7d808e31e005895.js → page-781cfc5114e097da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-5c2b24b5496dad73.js → page-478157214db6a023.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-76cdb531668b5191.js → page-86141f12cf090b7f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-7256c73bb1c36751.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-5c738ca7adef3de5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-0138c8091ec17224.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-cc2f24f16d85e735.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-dab163b338e3b966.js → page-979d318a534e92da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-182d394f7e6e096b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-0260ffc7eda50db1.js → page-21050928a697ed7a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-44b9d807005a9030.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-99c4e6c7345a9156.js → page-601fb77340e63c9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-75d3fb5d47af0edf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-11bf9e5725a72c22.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +13 -13
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/graphs/implement-loop.yaml +38 -1
- package/graphs/steward/recipes.yaml +6 -0
- package/graphs/steward/release-day.yaml +23 -0
- package/graphs/steward/steward.yaml +9 -1
- package/graphs/work/code-pr.yaml +23 -0
- package/package.json +1 -1
- package/scripts/browser-observe-cycle.ts +76 -2
- package/scripts/ci-typecheck-changed.ts +50 -17
- package/scripts/detect-test-interference.ts +31 -1
- package/scripts/release-loop/docs-node.ts +51 -22
- package/scripts/release-loop/gate-node.ts +37 -4
- package/scripts/release-loop/node-verdict.ts +3 -1
- package/scripts/release-loop/publish-node.ts +3 -0
- package/scripts/release-loop/tui-node-checks.ts +5 -2
- package/scripts/skills/public-check.ts +5 -2
- package/scripts/test-deterministic.ts +520 -37
- package/scripts/ux-sim.ts +7 -1
- package/scripts/verify-kpack6-live.ts +230 -0
- package/src/acp/codex-plugins.ts +17 -0
- package/src/agent/task-notification.ts +2 -1
- package/src/agent-mission/in-agent-plugin.ts +8 -3
- package/src/agent-substrate/self-review-cli.ts +34 -10
- package/src/briefing/brief-items.ts +151 -35
- package/src/budget/llm-cost.ts +25 -2
- package/src/budget/usage-outcomes.ts +32 -0
- package/src/claims/claims-ledger.ts +23 -0
- package/src/claims/claims-render.ts +22 -2
- package/src/cli/brief-cli.ts +131 -10
- package/src/cli/claims-cli.ts +24 -4
- package/src/cli/msg-cli.ts +165 -9
- package/src/cli/plugin-cli.ts +11 -3
- package/src/cli/pr-cli.ts +80 -18
- package/src/cli/pr-land-refusal.ts +20 -0
- package/src/cli/release-cli.ts +2 -0
- package/src/cli/rfc-cli.ts +24 -0
- package/src/cli/tailscale-serve.ts +5 -8
- package/src/cli/tasks-cli.ts +393 -26
- package/src/cli/usage-cli.ts +9 -4
- package/src/dashboard/first-screen-band.ts +63 -13
- package/src/dashboard/index.ts +8 -3
- package/src/dashboard/skill-detection-runtime.ts +24 -4
- package/src/domains/morning-report.ts +3 -1
- package/src/github/pr-labels.ts +10 -4
- package/src/graph-runner/runner.ts +101 -14
- package/src/harness/harness-clean.ts +21 -3
- package/src/harness/harness-cli-command.ts +22 -6
- package/src/harness/harness-pod-dispatch.ts +11 -4
- package/src/hq/seats.ts +17 -3
- package/src/index.ts +34 -5
- package/src/intelligence-map/model-bench-scores.ts +102 -0
- package/src/loops/orchestrator/inbox.ts +73 -0
- package/src/loops/orchestrator/resync-shadow.ts +100 -12
- package/src/loops/orchestrator/tick.ts +73 -2
- package/src/maturity/feature-maturity.ts +1 -1
- package/src/msg/msg-sse-server.ts +141 -0
- package/src/msg/msg-store.ts +84 -14
- package/src/patents/ledger.ts +128 -0
- package/src/plugins/install/market-fetch.ts +32 -1
- package/src/pod-lease/admission.ts +116 -41
- package/src/release-loop/checklist.ts +11 -0
- package/src/release-loop/merged-pr-checklist.ts +24 -5
- package/src/release-loop/rubric.ts +70 -0
- package/src/rfc-registry/registry.ts +88 -0
- package/src/seat-dispatch/seat-request-ledger.ts +4 -0
- package/src/seat-loop/knowhow-lint.ts +233 -0
- package/src/seat-loop/seat-pack.ts +16 -4
- package/src/seat-loop/traffic-signal.ts +7 -0
- package/src/self-dev/ask-launch-flow.ts +48 -8
- package/src/self-dev/ask-path-candidates.ts +69 -14
- package/src/self-dev/decompose-proposal.ts +43 -1
- package/src/self-dev/graph-journey-nodes.ts +2 -0
- package/src/self-dev/launch-preflight-text.ts +5 -1
- package/src/self-dev/launch-preflight.ts +143 -10
- package/src/self-implement/gate-cli.ts +6 -2
- package/src/self-implement/goal-author.ts +2 -1
- package/src/self-implement/goal-new-paths.ts +4 -5
- package/src/self-implement/graph-authority.ts +17 -3
- package/src/self-implement/graph-yaml.ts +15 -7
- package/src/self-implement/headless-elanous-driver.ts +16 -1
- package/src/self-implement/host-regate.ts +44 -9
- package/src/self-implement/orchestrator.ts +3 -10
- package/src/self-implement/rework-policy.ts +80 -2
- package/src/self-implement/speed-claims.ts +145 -6
- package/src/skills/tools/persistent-grounding.ts +59 -4
- package/src/skills/tools/shell.ts +79 -1
- package/src/steward/rejudge-tick.ts +12 -1
- package/src/steward/release-day.ts +158 -0
- package/src/steward/resync-tick.ts +123 -0
- package/src/task-agent/actions.ts +1 -0
- package/src/task-agent/adopt-pr.ts +140 -0
- package/src/task-agent/board.ts +11 -1
- package/src/task-agent/card-evidence.ts +37 -5
- package/src/task-agent/cover.ts +84 -17
- package/src/task-agent/failure-notify.ts +61 -0
- package/src/task-agent/green-sweep.ts +170 -0
- package/src/task-agent/land-worktree.ts +52 -5
- package/src/task-agent/live-move-history.ts +1 -0
- package/src/task-agent/live-moves.ts +43 -12
- package/src/task-agent/parent-host.ts +1 -1
- package/src/task-agent/postland-card.ts +139 -0
- package/src/task-agent/rejudge-on-head.ts +106 -7
- package/src/task-agent/remote-rejudge-bind.ts +62 -0
- package/src/task-agent/repair-branch-cas.ts +182 -0
- package/src/task-agent/repair-cycle.ts +275 -0
- package/src/task-agent/repair-next-move.ts +31 -0
- package/src/task-agent/repair-review.ts +24 -0
- package/src/task-agent/repair-worker.ts +87 -0
- package/src/task-agent/review-result-capture.ts +2 -1
- package/src/task-agent/shadow.ts +13 -7
- package/src/task-agent/task-hand.ts +115 -6
- package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +70 -12
- package/src/telegram.ts +30 -1
- package/src/user-config.ts +8 -1
- package/src/ux-sim/fault-sqlite-busy.ts +50 -0
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-a1ce306364f08da9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-20662900d6b4b455.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f707a509515ac50c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-8397a5c6ca569bf4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-15c0a4095baddfba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-d376e0c83edc041a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-08019efef111ed2e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-980ce6fe36ad95eb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-0ab42e0f05823013.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +0 -1
- /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Ev2jivDMTSoEc-5rIXdcy → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
package/src/budget/llm-cost.ts
CHANGED
|
@@ -303,7 +303,19 @@ export function estimateLlmCost(
|
|
|
303
303
|
|
|
304
304
|
/** Discriminated cost fields to spread onto an `llm.usage` observation. */
|
|
305
305
|
/** 과금 경로(BACKLOG C6 · hermes `resolve_billing_route`). 구독(OAuth)·local 은 «토큰당 청구가 없다». */
|
|
306
|
-
export type BillingRoute = 'subscription' | 'local' | 'api' | 'unknown';
|
|
306
|
+
export type BillingRoute = 'subscription' | 'local' | 'credit' | 'api' | 'unknown';
|
|
307
|
+
|
|
308
|
+
/** Credit is known only when the quota window is exhausted and credits are present. */
|
|
309
|
+
export function creditRouteFromQuotaSignal(signal: {
|
|
310
|
+
usedPercent?: number | null;
|
|
311
|
+
rateLimitReached?: boolean | null;
|
|
312
|
+
hasCredits?: boolean | null;
|
|
313
|
+
} | null): 'credit' | null {
|
|
314
|
+
if (!signal || signal.hasCredits !== true) return null;
|
|
315
|
+
const exhausted = signal.rateLimitReached === true
|
|
316
|
+
|| (typeof signal.usedPercent === 'number' && Number.isFinite(signal.usedPercent) && signal.usedPercent >= 100);
|
|
317
|
+
return exhausted ? 'credit' : null;
|
|
318
|
+
}
|
|
307
319
|
|
|
308
320
|
/** 구독·local 호출의 비용 — 청구 0 이지만 «API 로 샀다면」 값은 따로 남긴다(환산가 · 모르면 칸이 없다). */
|
|
309
321
|
/** provider 가 보고한 실제 청구액(OpenRouter `usage.cost`) — 카탈로그 추정과 다르면 이것이 맞다. */
|
|
@@ -323,14 +335,25 @@ export interface IncludedLlmCost {
|
|
|
323
335
|
apiEquivalentUsd?: number;
|
|
324
336
|
}
|
|
325
337
|
|
|
338
|
+
export interface CreditLlmCost {
|
|
339
|
+
kind: 'credit';
|
|
340
|
+
model: string;
|
|
341
|
+
usd: null;
|
|
342
|
+
apiEquivalentUsd?: number;
|
|
343
|
+
}
|
|
344
|
+
|
|
326
345
|
export function llmUsageCostFields(
|
|
327
346
|
model: string,
|
|
328
347
|
usage: Omit<LlmCostUsage, 'model'>,
|
|
329
348
|
opts?: EstimateLlmCostOptions,
|
|
330
349
|
billing?: BillingRoute,
|
|
331
|
-
): { cost: LlmCostEstimate | IncludedLlmCost | ActualLlmCost | ByokActualCost | ByokPartialCost } {
|
|
350
|
+
): { cost: LlmCostEstimate | IncludedLlmCost | CreditLlmCost | ActualLlmCost | ByokActualCost | ByokPartialCost } {
|
|
332
351
|
const resolved = opts ?? { configPricing: readLlmConfigPricing() };
|
|
333
352
|
const estimate = estimateLlmCost({ model, ...usage }, resolved);
|
|
353
|
+
// An explicit credit route owns this cost row; API/BYOK amounts belong to other routes.
|
|
354
|
+
if (billing === 'credit') {
|
|
355
|
+
return { cost: { kind: 'credit', model, usd: null, ...(estimate.kind === 'known' || estimate.kind === 'partial' ? { apiEquivalentUsd: estimate.usd } : {}) } };
|
|
356
|
+
}
|
|
334
357
|
// ⛔⭐ 구독을 API 단가로 «known» 이라 적지 않는다 — 09-25 벤치에서 codex 구독 팔이 `$7.81 known` 으로 보였다.
|
|
335
358
|
// ⭐ BACKLOG C7 — provider 가 실어 보낸 «실제 청구액»이 있으면 그것이 비용이다(추정은 곁에 남긴다).
|
|
336
359
|
// OR-BYOK-COST — BYOK 면 «OR 청구 + 상류 청구»가 비용이다(상류를 모르면 partial · 0 으로 가정하지 않는다).
|
|
@@ -33,6 +33,14 @@ export interface UsageOutcomesReport {
|
|
|
33
33
|
readonly unreadableLedgers: number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export interface AutoLandRatioRow {
|
|
37
|
+
readonly selfLanded: number;
|
|
38
|
+
readonly handMerges: number | null;
|
|
39
|
+
readonly totalMerges: number | null;
|
|
40
|
+
readonly autoLandRatio: number | null;
|
|
41
|
+
readonly blockReasons: Readonly<Record<string, number>>;
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
export interface UsageOutcomesDeps {
|
|
37
45
|
readonly dir?: string;
|
|
38
46
|
readonly nowMs?: number;
|
|
@@ -139,6 +147,30 @@ export function aggregateUsageOutcomes(
|
|
|
139
147
|
return { sinceDays, sinceMs, rows, unreadableLedgers };
|
|
140
148
|
}
|
|
141
149
|
|
|
150
|
+
/** Join the existing run-ledger aggregate with an externally measured hand-merge count. */
|
|
151
|
+
export function measureAutoLandRatio(report: UsageOutcomesReport, handMerges: number | null): AutoLandRatioRow {
|
|
152
|
+
const selfLanded = report.rows.reduce((sum, row) => sum + row.landed, 0);
|
|
153
|
+
const hand = handMerges !== null && Number.isSafeInteger(handMerges) && handMerges >= 0 ? handMerges : null;
|
|
154
|
+
const sum = hand === null ? null : selfLanded + hand;
|
|
155
|
+
// An empty report also represents a missing ledger directory; do not turn that into 0%.
|
|
156
|
+
const totalMerges = report.rows.length === 0 || report.unreadableLedgers > 0 || sum === null || !Number.isSafeInteger(sum) ? null : sum;
|
|
157
|
+
const autoLandRatio = totalMerges === null || totalMerges === 0 ? null : selfLanded / totalMerges;
|
|
158
|
+
const reasons = new Map<string, number>();
|
|
159
|
+
for (const row of report.rows) {
|
|
160
|
+
for (const [reason, count] of Object.entries(row.blockReasons)) {
|
|
161
|
+
reasons.set(reason, (reasons.get(reason) ?? 0) + count);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const blockReasons = Object.fromEntries([...reasons].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))) as Record<string, number>;
|
|
165
|
+
return { selfLanded, handMerges: hand, totalMerges, autoLandRatio, blockReasons };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function formatAutoLandRatio(row: AutoLandRatioRow): string {
|
|
169
|
+
const ratio = row.autoLandRatio === null ? '-' : `${Math.round(row.autoLandRatio * 1000) / 10}%`;
|
|
170
|
+
const reasons = Object.entries(row.blockReasons).map(([reason, count]) => `${reason}=${count}`).join(',') || '-';
|
|
171
|
+
return `자율 착지 ${row.selfLanded} / 전체 ${row.totalMerges ?? '-'} (${ratio}) · 손 착지 ${row.handMerges ?? '-'} · 멈춤 사유 ${reasons}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
142
174
|
export function queryUsageOutcomes(sinceDays: number, deps: UsageOutcomesDeps = {}): UsageOutcomesReport {
|
|
143
175
|
const days = Number.isFinite(sinceDays) && sinceDays > 0 ? sinceDays : 2;
|
|
144
176
|
const nowMs = deps.nowMs ?? Date.now();
|
|
@@ -3,6 +3,7 @@ import { chmodSync, mkdirSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
5
5
|
import { debug } from '../debug/log.js';
|
|
6
|
+
import type { PatentCandidate, PatentLedger } from '../patents/ledger.js';
|
|
6
7
|
|
|
7
8
|
export type ClaimStatus = 'draft' | 'verified' | 'public' | 'stale' | 'retracted';
|
|
8
9
|
|
|
@@ -41,6 +42,19 @@ function iso(value: string, field: string): string {
|
|
|
41
42
|
return date.toISOString();
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
function patentClaimRow(candidate: PatentCandidate, owner: string, audience: string): ClaimRow {
|
|
46
|
+
return {
|
|
47
|
+
id: candidate.id,
|
|
48
|
+
claim: candidate.title,
|
|
49
|
+
audience,
|
|
50
|
+
status: 'public',
|
|
51
|
+
owner,
|
|
52
|
+
contrast: null,
|
|
53
|
+
created_at: candidate.created_at,
|
|
54
|
+
updated_at: candidate.updated_at,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
export class ClaimsLedger {
|
|
45
59
|
readonly path: string;
|
|
46
60
|
private readonly now: () => Date;
|
|
@@ -229,4 +243,13 @@ export class ClaimsLedger {
|
|
|
229
243
|
};
|
|
230
244
|
}));
|
|
231
245
|
}
|
|
246
|
+
|
|
247
|
+
/** 특허 원장의 공개 허용 상태 후보만 읽어 공개 주장 행으로 내온다 — 조사·명세·출원 상태 후보는 공개 주장으로 나오지 않는다. */
|
|
248
|
+
patentClaims(patents: PatentLedger, options: { owner: string; audience: string }): ClaimRow[] {
|
|
249
|
+
const owner = required(options.owner, 'owner');
|
|
250
|
+
const audience = required(options.audience, 'audience');
|
|
251
|
+
const rows = patents.listPublicCandidates().map(candidate => patentClaimRow(candidate, owner, audience));
|
|
252
|
+
debug.log('claims.ledger', 'patent-claims', { count: rows.length });
|
|
253
|
+
return rows;
|
|
254
|
+
}
|
|
232
255
|
}
|
|
@@ -6,6 +6,7 @@ import { BRAND_RULES_UNMEASURED, checkBrand } from '../../scripts/brand/check.js
|
|
|
6
6
|
import { debug } from '../debug/log.js';
|
|
7
7
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
8
8
|
import type { ClaimsLedger, ClaimEvidence } from './claims-ledger.js';
|
|
9
|
+
import type { PatentLedger } from '../patents/ledger.js';
|
|
9
10
|
|
|
10
11
|
export type ClaimSurface = 'deck' | 'site' | 'notice';
|
|
11
12
|
export interface RenderedClaims {
|
|
@@ -49,12 +50,14 @@ export function representativeNumber(value: string): string | undefined {
|
|
|
49
50
|
return matches[0];
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
export function renderClaims(ledger: ClaimsLedger, options: { surface: ClaimSurface; audience?: string; rulesPath?: string }): RenderedClaims {
|
|
53
|
+
export function renderClaims(ledger: ClaimsLedger, options: { surface: ClaimSurface; audience?: string; rulesPath?: string; patents?: { ledger: PatentLedger; owner: string; audience: string } }): RenderedClaims {
|
|
53
54
|
const { surface, audience } = options;
|
|
54
55
|
if (!(surface in headings)) throw new Error(`invalid claims surface: ${surface}`);
|
|
55
56
|
const excluded: RenderedClaims['excluded'] = [];
|
|
56
57
|
const candidates: Array<{ id: string; line: string }> = [];
|
|
57
|
-
|
|
58
|
+
const rows = ledger.list();
|
|
59
|
+
const known = new Set(rows.map(row => row.id));
|
|
60
|
+
for (const row of rows) {
|
|
58
61
|
if (audience && !row.audience.split(',').includes(audience)) continue;
|
|
59
62
|
if (row.status !== 'public') {
|
|
60
63
|
excluded.push({ id: row.id, reason: row.status === 'stale' ? 'stale' : row.status === 'retracted' ? 'retracted' : 'not-public' });
|
|
@@ -83,6 +86,23 @@ export function renderClaims(ledger: ClaimsLedger, options: { surface: ClaimSurf
|
|
|
83
86
|
candidates.push({ id: row.id, line: `- ${claim} — ${number} <!-- claim:${row.id} measured:${evidence.measured_at} cmd:${commentValue(evidence.command)}${evidence.source ? ` source:${commentValue(evidence.source)}` : ''} --> <!-- src:${commentValue(evidence.source ?? evidence.command)} -->` });
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
// 공개 허용 상태는 렌더 시점에 특허 원장에서 읽는다. 임의의 ClaimRow 배열은 받지 않는다.
|
|
90
|
+
// 저장된 주장과 같은 식별자를 가진 유도 행은 원장 행이 이긴다.
|
|
91
|
+
for (const row of options.patents ? ledger.patentClaims(options.patents.ledger, options.patents) : []) {
|
|
92
|
+
if (known.has(row.id)) continue;
|
|
93
|
+
if (audience && !row.audience.split(',').includes(audience)) continue;
|
|
94
|
+
if (row.status !== 'public') {
|
|
95
|
+
excluded.push({ id: row.id, reason: 'not-public' });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const patentClaim = firstSentence(row.claim);
|
|
99
|
+
if (!patentClaim) {
|
|
100
|
+
excluded.push({ id: row.id, reason: 'brand' });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
candidates.push({ id: row.id, line: `- ${patentClaim} <!-- claim:${row.id} patent --> <!-- src:patents -->` });
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
const rulesMissing = checkBrand(scopes[surface], [], options.rulesPath).missing;
|
|
87
107
|
if (rulesMissing) {
|
|
88
108
|
excluded.push(...candidates.map(({ id }) => ({ id, reason: 'rules-missing' as const })));
|
package/src/cli/brief-cli.ts
CHANGED
|
@@ -1,9 +1,97 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { BRIEF_DOMAINS, BRIEF_PRIORITIES, BRIEF_REACTIONS, BRIEF_SLOTS, BriefItemsInputError, BriefItemsLedger, type BriefDomain, type BriefPriority, type BriefReaction, type BriefSendSlot } from '../briefing/brief-items.js';
|
|
3
3
|
import { kindRouteTarget } from '../domains/telegram-kind-route.js';
|
|
4
|
-
import {
|
|
4
|
+
import { sendTelegramButtonsTo, type TgButton } from '../autopilot/mission-notify.js';
|
|
5
5
|
import { getUserConfig } from '../user-config.js';
|
|
6
6
|
|
|
7
|
+
export function briefFeedbackButtons(ids: readonly number[]): TgButton[][] {
|
|
8
|
+
return ids.map(id => [
|
|
9
|
+
{ text: `👀 ${id} 읽음`, data: `brief-item:${id}:read` },
|
|
10
|
+
{ text: `✅ ${id} 움직임`, data: `brief-item:${id}:button` },
|
|
11
|
+
{ text: `➖ ${id} 넘김`, data: `brief-item:${id}:ignore` },
|
|
12
|
+
]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BriefTelegramUpdate {
|
|
16
|
+
botId: string;
|
|
17
|
+
chatId: number;
|
|
18
|
+
messageId: number;
|
|
19
|
+
updateId: string;
|
|
20
|
+
text?: string;
|
|
21
|
+
callbackData?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function recordBriefTelegramUpdate(ledger: BriefItemsLedger, update: BriefTelegramUpdate): boolean {
|
|
25
|
+
if (update.callbackData !== undefined) {
|
|
26
|
+
const match = /^brief-item:([1-9]\d*):(read|button|ignore)$/.exec(update.callbackData);
|
|
27
|
+
if (!match) return false;
|
|
28
|
+
return ledger.recordTelegramFeedback({ botId: update.botId, chatId: update.chatId,
|
|
29
|
+
messageId: update.messageId, updateId: update.updateId, itemId: Number(match[1]),
|
|
30
|
+
reaction: match[2] === 'read' ? '열람' : match[2] === 'ignore' ? '무시' : '버튼' });
|
|
31
|
+
}
|
|
32
|
+
if (!update.text?.trim()) return false;
|
|
33
|
+
const text = update.text.trim();
|
|
34
|
+
const itemId = /^(?:#|항목\s*)([1-9]\d*)(?=\s|[::]|$)/.exec(text)?.[1];
|
|
35
|
+
return ledger.recordTelegramFeedback({ botId: update.botId, chatId: update.chatId,
|
|
36
|
+
messageId: update.messageId, updateId: update.updateId,
|
|
37
|
+
...(itemId ? { itemId: Number(itemId) } : {}),
|
|
38
|
+
reaction: /그게 아니라|(?:^|\s)정정\s*[::]/.test(text) ? '정정' : '답장' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Prefix each item line with `#<id>` so the 대표 can name the reply target ("#12 답장", "#12 그게 아니라 …"). */
|
|
42
|
+
function withItemNumbers(text: string, itemLines: ReadonlyMap<string, readonly number[]>): string {
|
|
43
|
+
const linePositions = new Map<string, number>();
|
|
44
|
+
return text.split('\n').map(line => {
|
|
45
|
+
const ids = itemLines.get(line);
|
|
46
|
+
if (!ids) return line;
|
|
47
|
+
const position = linePositions.get(line) ?? 0;
|
|
48
|
+
linePositions.set(line, position + 1);
|
|
49
|
+
const itemId = ids[position];
|
|
50
|
+
return itemId === undefined ? line : `#${itemId} ${line}`;
|
|
51
|
+
}).join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Split only feedback-enabled pages; each Telegram message gets its own real item coordinate.
|
|
55
|
+
* Item lines carry their `#<id>` so a plain text reply can name its target item.
|
|
56
|
+
*/
|
|
57
|
+
function briefDeliveryParts(markdown: string, itemLines: ReadonlyMap<string, readonly number[]>): Array<{ text: string; itemId: number }> {
|
|
58
|
+
const parts: Array<{ text: string; itemId: number }> = [];
|
|
59
|
+
const linePositions = new Map<string, number>();
|
|
60
|
+
let pending = '';
|
|
61
|
+
for (const line of markdown.trimEnd().split('\n')) {
|
|
62
|
+
const ids = itemLines.get(line);
|
|
63
|
+
const position = linePositions.get(line) ?? 0;
|
|
64
|
+
const itemId = ids?.[position];
|
|
65
|
+
if (itemId === undefined) {
|
|
66
|
+
pending += `${line}\n`;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
linePositions.set(line, position + 1);
|
|
70
|
+
const numbered = `#${itemId} ${line}`;
|
|
71
|
+
// Leave room for the Telegram sender's newline and keep a button on every fragment.
|
|
72
|
+
while (pending.length >= 3800) {
|
|
73
|
+
parts.push({ text: pending.slice(0, 3800), itemId });
|
|
74
|
+
pending = pending.slice(3800);
|
|
75
|
+
}
|
|
76
|
+
let remainder = pending + numbered;
|
|
77
|
+
do {
|
|
78
|
+
parts.push({ text: remainder.slice(0, 3800), itemId });
|
|
79
|
+
remainder = remainder.slice(3800);
|
|
80
|
+
} while (remainder.length);
|
|
81
|
+
pending = '';
|
|
82
|
+
}
|
|
83
|
+
if (pending && parts.length) {
|
|
84
|
+
const last = parts[parts.length - 1]!;
|
|
85
|
+
if (last.text.length + pending.length <= 3800) last.text += pending;
|
|
86
|
+
else {
|
|
87
|
+
const room = 3800 - last.text.length;
|
|
88
|
+
last.text += pending.slice(0, room);
|
|
89
|
+
for (let i = room; i < pending.length; i += 3800) parts.push({ text: pending.slice(i, i + 3800), itemId: last.itemId });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return parts;
|
|
93
|
+
}
|
|
94
|
+
|
|
7
95
|
export interface BriefCliDeps {
|
|
8
96
|
ledger?: BriefItemsLedger;
|
|
9
97
|
stateDir?: string;
|
|
@@ -11,6 +99,10 @@ export interface BriefCliDeps {
|
|
|
11
99
|
output?: (line: string) => void;
|
|
12
100
|
error?: (line: string) => void;
|
|
13
101
|
send?: (markdown: string) => number | null;
|
|
102
|
+
/** Test seam for the Telegram button sender (one actual message per invocation). */
|
|
103
|
+
sendWithButtons?: (text: string, buttons: TgButton[][]) => number | null;
|
|
104
|
+
/** Allows an injected sender to exercise the same Telegram attribution as the real sender. */
|
|
105
|
+
telegramTarget?: { botToken: string; chatId: number };
|
|
14
106
|
}
|
|
15
107
|
|
|
16
108
|
export function registerBriefCommands(program: Command, deps: BriefCliDeps = {}): void {
|
|
@@ -31,18 +123,47 @@ export function registerBriefCommands(program: Command, deps: BriefCliDeps = {})
|
|
|
31
123
|
output(`이번 주 브리핑 중 대표가 움직인 것 ${acted}/${total}`);
|
|
32
124
|
} catch (cause) { fail('', cause); }
|
|
33
125
|
});
|
|
34
|
-
const deliver = (slot: BriefSendSlot): string => ledger().withSendLock(slot, ({ markdown, ids }, markSent) => {
|
|
126
|
+
const deliver = (slot: BriefSendSlot): string => ledger().withSendLock(slot, ({ markdown, ids, itemLines }, markSent) => {
|
|
35
127
|
if (ids.length === 0) return '(보낼 항목 없음)';
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
128
|
+
const target = deps.telegramTarget ?? (deps.send ? null : kindRouteTarget(getUserConfig(), 'ops-report'));
|
|
129
|
+
const numberedMarkdown = withItemNumbers(markdown, itemLines);
|
|
130
|
+
const oneMessage = numberedMarkdown.length + numberedMarkdown.split('\n').length <= 3900 && ids.length <= 30;
|
|
131
|
+
const send = (text: string, itemIds: readonly number[]) => deps.sendWithButtons
|
|
132
|
+
? deps.sendWithButtons(text, briefFeedbackButtons(itemIds))
|
|
133
|
+
: deps.send ? deps.send(text)
|
|
134
|
+
: target ? sendTelegramButtonsTo(target.botToken, target.chatId, text, briefFeedbackButtons(itemIds)) : null;
|
|
135
|
+
const botId = target ? target.botToken.split(':')[0]! : '';
|
|
136
|
+
if (target && !oneMessage) {
|
|
137
|
+
// A sent fragment is not a completed item. Persist only items whose every fragment
|
|
138
|
+
// succeeded; an incomplete item must remain available to the next send.
|
|
139
|
+
const parts = briefDeliveryParts(markdown, itemLines);
|
|
140
|
+
const remainingParts = new Map<number, number>();
|
|
141
|
+
for (const part of parts) remainingParts.set(part.itemId, (remainingParts.get(part.itemId) ?? 0) + 1);
|
|
142
|
+
const coordinates: Array<{ botId: string; chatId: number; messageId: number; itemId: number }> = [];
|
|
143
|
+
let sendThrew = false;
|
|
144
|
+
let sendCause: unknown;
|
|
145
|
+
for (const part of parts) {
|
|
146
|
+
let partMessageId: number | null;
|
|
147
|
+
try { partMessageId = send(part.text, [part.itemId]); }
|
|
148
|
+
catch (cause) { sendThrew = true; sendCause = cause; break; }
|
|
149
|
+
if (typeof partMessageId !== 'number' || !Number.isSafeInteger(partMessageId) || partMessageId <= 0) break;
|
|
150
|
+
coordinates.push({ botId, chatId: target.chatId, messageId: partMessageId, itemId: part.itemId });
|
|
151
|
+
remainingParts.set(part.itemId, remainingParts.get(part.itemId)! - 1);
|
|
152
|
+
}
|
|
153
|
+
const deliveredIds = [...new Set(coordinates.map(row => row.itemId))].filter(id => remainingParts.get(id) === 0);
|
|
154
|
+
if (deliveredIds.length > 0) markSent(deliveredIds, slot, coordinates.filter(row => deliveredIds.includes(row.itemId)));
|
|
155
|
+
if (coordinates.length < parts.length) {
|
|
156
|
+
if (sendThrew) throw sendCause;
|
|
157
|
+
throw new Error(`발송 실패: ${deliveredIds.length}/${ids.length} 항목 전송 후 중단 — 나머지는 다음 발송에서 이어진다`);
|
|
158
|
+
}
|
|
159
|
+
const lastMessageId = coordinates[coordinates.length - 1]!.messageId;
|
|
160
|
+
return `sent ${lastMessageId} (${ids.length} items)`;
|
|
161
|
+
}
|
|
162
|
+
const messageId = target ? send(numberedMarkdown, ids) : send(markdown, ids);
|
|
163
|
+
if (typeof messageId !== 'number' || !Number.isSafeInteger(messageId) || messageId <= 0) {
|
|
43
164
|
throw new Error('발송 실패: 양수 메시지 id 없음');
|
|
44
165
|
}
|
|
45
|
-
markSent(ids, slot);
|
|
166
|
+
markSent(ids, slot, target ? { botId, chatId: target.chatId, messageId } : undefined);
|
|
46
167
|
return `sent ${messageId} (${ids.length} items)`;
|
|
47
168
|
});
|
|
48
169
|
|
package/src/cli/claims-cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { ClaimsInputError, ClaimsLedger, type ClaimsLedgerOptions, type ClaimRow, type ClaimDetail } from '../claims/claims-ledger.js';
|
|
3
3
|
import { renderClaims, queueStaleRechecks, type ClaimSurface } from '../claims/claims-render.js';
|
|
4
|
+
import { PatentLedger, PatentsInputError, type PatentStatus, type PatentTrack } from '../patents/ledger.js';
|
|
4
5
|
|
|
5
6
|
function actor(explicit?: string): string {
|
|
6
7
|
const by = explicit?.trim() || process.env.AI_AGENT?.trim();
|
|
@@ -19,7 +20,7 @@ function handle<Args extends unknown[]>(command: string, action: (...args: Args)
|
|
|
19
20
|
return (...args) => {
|
|
20
21
|
try { action(...args); }
|
|
21
22
|
catch (error) {
|
|
22
|
-
if (!(error instanceof ClaimsInputError)) throw error;
|
|
23
|
+
if (!(error instanceof ClaimsInputError) && !(error instanceof PatentsInputError)) throw error;
|
|
23
24
|
const message = error.message.replace(/\r/g, '\\r').replace(/\n/g, '\\n')
|
|
24
25
|
.replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
|
|
25
26
|
process.stderr.write(`claims ${command}: ${message}\n`);
|
|
@@ -41,7 +42,19 @@ function detail(row: ClaimDetail): string {
|
|
|
41
42
|
export function registerClaimsCommands(program: Command, options: ClaimsLedgerOptions & { instanceRoot?: string } = {}, out: Pick<Console, 'log'> = console): void {
|
|
42
43
|
const root = program.command('claims').description('소구점 · 실측 근거 원장');
|
|
43
44
|
const ledger = () => new ClaimsLedger(options);
|
|
45
|
+
const patentLedger = () => new PatentLedger(options);
|
|
44
46
|
const emit = (value: unknown, json: boolean | undefined, text: string) => out.log(json ? JSON.stringify(value) : text);
|
|
47
|
+
const patents = root.command('patents').description('비공개 특허 후보 원장');
|
|
48
|
+
patents.command('add <id>').requiredOption('--title <title>').requiredOption('--track <track>')
|
|
49
|
+
.option('--deadline <YYYY-MM-DD>').action(handle('patents add', (id: string, o: { title: string; track: PatentTrack; deadline?: string }) => {
|
|
50
|
+
const row = patentLedger().add({ id, title: o.title, track: o.track, ...(o.deadline ? { deadline: o.deadline } : {}) });
|
|
51
|
+
emit(row, false, `${row.id} · ${row.status}`);
|
|
52
|
+
}));
|
|
53
|
+
patents.command('status <id>').requiredOption('--status <status>')
|
|
54
|
+
.action(handle('patents status', (id: string, o: { status: PatentStatus }) => {
|
|
55
|
+
const row = patentLedger().setStatus(id, o.status);
|
|
56
|
+
emit(row, false, `${row.id} · ${row.status}`);
|
|
57
|
+
}));
|
|
45
58
|
root.command('add <id>').requiredOption('--claim <text>').requiredOption('--audience <list>').requiredOption('--owner <seat>')
|
|
46
59
|
.option('--contrast <text>').action(handle('add', (id: string, o: { claim: string; audience: string; owner: string; contrast?: string }) => {
|
|
47
60
|
const row = ledger().add({ id, claim: o.claim, audience: o.audience, owner: o.owner, ...(o.contrast ? { contrast: o.contrast } : {}) });
|
|
@@ -79,9 +92,16 @@ export function registerClaimsCommands(program: Command, options: ClaimsLedgerOp
|
|
|
79
92
|
emit(row, o.json, detail(row));
|
|
80
93
|
}));
|
|
81
94
|
root.command('render').requiredOption('--surface <surface>', 'deck | site | notice')
|
|
82
|
-
.option('--audience <audience>').option('--
|
|
83
|
-
.
|
|
84
|
-
|
|
95
|
+
.option('--audience <audience>').option('--include-patents', '명시적으로 공개 허용 특허 후보를 포함')
|
|
96
|
+
.option('--patent-owner <owner>', '특허 후보의 확인된 담당').option('--json')
|
|
97
|
+
.action(handle('render', (o: { surface: ClaimSurface; audience?: string; includePatents?: boolean; patentOwner?: string; json?: boolean }) => {
|
|
98
|
+
if (o.includePatents && (!o.patentOwner?.trim() || !o.audience?.trim())) {
|
|
99
|
+
throw new ClaimsInputError('--include-patents requires --patent-owner and --audience');
|
|
100
|
+
}
|
|
101
|
+
if (!o.includePatents && o.patentOwner) throw new ClaimsInputError('--patent-owner requires --include-patents');
|
|
102
|
+
const patents = o.includePatents
|
|
103
|
+
? { ledger: patentLedger(), owner: o.patentOwner!, audience: o.audience! } : undefined;
|
|
104
|
+
const result = renderClaims(ledger(), { surface: o.surface, ...(o.audience ? { audience: o.audience } : {}), ...(patents ? { patents } : {}) });
|
|
85
105
|
// Review follow-up: rendering is where stale claims surface — queue their re-measurement in the same act.
|
|
86
106
|
const rechecks = result.excluded.some(item => item.reason === 'stale')
|
|
87
107
|
? queueStaleRechecks(ledger(), { root: options.instanceRoot, now: options.now?.() }) : 0;
|
package/src/cli/msg-cli.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import {
|
|
2
|
+
import { ensureAuthToken } from '../auth/acp-token.js';
|
|
3
|
+
import { canonicalSeatId, openMsgStore, openMsgStoreReadOnly, type Message, type MsgStore } from '../msg/msg-store.js';
|
|
4
|
+
import { startMsgSseServer } from '../msg/msg-sse-server.js';
|
|
3
5
|
|
|
4
6
|
export interface MsgCliDeps {
|
|
5
7
|
openStore?: () => MsgStore;
|
|
8
|
+
openReadStore?: () => MsgStore;
|
|
6
9
|
out?: Pick<Console, 'log' | 'error'>;
|
|
7
10
|
readStdin?: () => Promise<string>;
|
|
8
11
|
sleep?: (ms: number) => Promise<void>;
|
|
9
12
|
setExitCode?: (code: number) => void;
|
|
10
13
|
signal?: AbortSignal;
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
authToken?: () => string;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
type CommonOptions = { json?: boolean };
|
|
@@ -33,6 +38,7 @@ function formatMessage(message: Message): string {
|
|
|
33
38
|
/** Register the durable, recipient-addressed CLI. Reading never advances the acknowledgement cursor. */
|
|
34
39
|
export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): void {
|
|
35
40
|
const open = deps.openStore ?? (() => openMsgStore());
|
|
41
|
+
const openRead = deps.openReadStore ?? (() => openMsgStoreReadOnly());
|
|
36
42
|
const out = deps.out ?? console;
|
|
37
43
|
const readStdin = deps.readStdin ?? (() => Bun.stdin.text());
|
|
38
44
|
const sleep = deps.sleep ?? ((ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms)));
|
|
@@ -58,13 +64,16 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
|
|
|
58
64
|
|
|
59
65
|
msg.command('post [body]').description('Post a message; use - or omit body to read stdin')
|
|
60
66
|
.option('--from <seat>').option('--to <seat>').option('--kind <kind>').option('--body <text>')
|
|
67
|
+
.option('--reply-to <id>', 'point this reply at a question message ID')
|
|
61
68
|
.option('--origin-key <key>', 'deduplicate a channel mirror by coord:<commentId>:<recipient>').option('--json')
|
|
62
|
-
.action((body: string | undefined, opts: CommonOptions & { from?: string; to?: string; kind?: string; body?: string; originKey?: string }) => run(async () => {
|
|
69
|
+
.action((body: string | undefined, opts: CommonOptions & { from?: string; to?: string; kind?: string; body?: string; replyTo?: string; originKey?: string }) => run(async () => {
|
|
63
70
|
const from = seat(opts.from, '--from');
|
|
64
71
|
const to = seat(opts.to, '--to');
|
|
65
72
|
if (body !== undefined && body !== '-' && opts.body !== undefined) throw new Error('provide body once (argument or --body)');
|
|
66
73
|
const text = opts.body ?? (body !== undefined && body !== '-' ? body : await readStdin());
|
|
67
|
-
const envelope = { from, to, body: text,
|
|
74
|
+
const envelope = { from, to, body: text,
|
|
75
|
+
...(opts.kind === undefined ? {} : { kind: opts.kind }),
|
|
76
|
+
...(opts.replyTo === undefined ? {} : { replyTo: integer(opts.replyTo, '--reply-to', 0, 1, Number.MAX_SAFE_INTEGER) }) };
|
|
68
77
|
const posted = withStore(store => opts.originKey === undefined
|
|
69
78
|
? store.post(envelope)
|
|
70
79
|
: store.appendWithProvenance(envelope, opts.originKey));
|
|
@@ -110,12 +119,13 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
|
|
|
110
119
|
});
|
|
111
120
|
})());
|
|
112
121
|
|
|
113
|
-
msg.command('watch').description('
|
|
122
|
+
msg.command('watch').description('Watch durable messages; SSE accelerates delivery and polling covers outages')
|
|
114
123
|
.option('--to <seat>').option('--after <id>', 'exclusive message ID (default: acknowledged cursor)')
|
|
115
124
|
.option('--since-ack', 'start from the recipient seat’s acknowledged cursor')
|
|
116
125
|
.option('--interval <ms>', 'poll interval in milliseconds', '1000')
|
|
126
|
+
.option('--sse <url>', 'post office base URL (fall back to ledger polling on disconnect)')
|
|
117
127
|
.option('--once', 'poll once and exit').option('--json')
|
|
118
|
-
.action((opts: RecipientOptions & { interval?: string; once?: boolean; sinceAck?: boolean }) => run(async () => {
|
|
128
|
+
.action((opts: RecipientOptions & { interval?: string; once?: boolean; sinceAck?: boolean; sse?: string }) => run(async () => {
|
|
119
129
|
if (opts.sinceAck && opts.after !== undefined) throw new Error('--since-ack cannot be used with --after');
|
|
120
130
|
const to = seat(opts.to, '--to');
|
|
121
131
|
const interval = integer(opts.interval, '--interval', 1000, 1, 60000);
|
|
@@ -123,7 +133,7 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
|
|
|
123
133
|
const store = open();
|
|
124
134
|
try {
|
|
125
135
|
let cursor = after ?? store.getCursor(to);
|
|
126
|
-
|
|
136
|
+
const poll = () => {
|
|
127
137
|
let messages: Message[];
|
|
128
138
|
do {
|
|
129
139
|
messages = store.list(to, cursor, 1000);
|
|
@@ -132,12 +142,158 @@ export function registerMsgCommands(program: Command, deps: MsgCliDeps = {}): vo
|
|
|
132
142
|
cursor = message.id;
|
|
133
143
|
}
|
|
134
144
|
} while (messages.length === 1000 && !deps.signal?.aborted);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
};
|
|
146
|
+
if (!opts.sse || opts.once) {
|
|
147
|
+
do {
|
|
148
|
+
poll();
|
|
149
|
+
if (opts.once || deps.signal?.aborted) break;
|
|
150
|
+
await sleep(interval);
|
|
151
|
+
} while (!deps.signal?.aborted);
|
|
152
|
+
} else {
|
|
153
|
+
const url = new URL('/stream', opts.sse);
|
|
154
|
+
if (!['http:', 'https:'].includes(url.protocol)) throw new Error('--sse requires an HTTP URL');
|
|
155
|
+
if (url.protocol === 'http:' && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))
|
|
156
|
+
throw new Error('--sse requires HTTPS outside localhost');
|
|
157
|
+
url.searchParams.set('to', to);
|
|
158
|
+
const token = (deps.authToken ?? (() => ensureAuthToken().token))();
|
|
159
|
+
let outage = false;
|
|
160
|
+
while (!deps.signal?.aborted) {
|
|
161
|
+
poll();
|
|
162
|
+
url.searchParams.set('since', String(cursor));
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const onAbort = () => controller.abort();
|
|
165
|
+
deps.signal?.addEventListener('abort', onAbort, { once: true });
|
|
166
|
+
try {
|
|
167
|
+
let connectTimer: ReturnType<typeof setTimeout> | undefined;
|
|
168
|
+
const response = await Promise.race([
|
|
169
|
+
(deps.fetch ?? fetch)(url, { headers: {
|
|
170
|
+
Authorization: `Bearer ${token}`, 'Last-Event-ID': String(cursor),
|
|
171
|
+
}, signal: controller.signal }),
|
|
172
|
+
new Promise<never>((_, reject) => { connectTimer = setTimeout(() => {
|
|
173
|
+
controller.abort();
|
|
174
|
+
reject(new Error('SSE connect timeout'));
|
|
175
|
+
}, Math.max(1000, Math.min(5000, interval * 2))); }),
|
|
176
|
+
]).finally(() => clearTimeout(connectTimer));
|
|
177
|
+
if (response.status === 401) throw new Error('SSE unauthorized (401)');
|
|
178
|
+
if (!response.ok || !response.body) throw new Error(`SSE unavailable (${response.status})`);
|
|
179
|
+
outage = false;
|
|
180
|
+
const reader = response.body.getReader();
|
|
181
|
+
const decoder = new TextDecoder();
|
|
182
|
+
let pending = '';
|
|
183
|
+
while (!deps.signal?.aborted) {
|
|
184
|
+
let idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
185
|
+
const chunk = await Promise.race([
|
|
186
|
+
reader.read(),
|
|
187
|
+
new Promise<never>((_, reject) => { idleTimer = setTimeout(() => {
|
|
188
|
+
controller.abort();
|
|
189
|
+
reject(new Error('SSE idle timeout'));
|
|
190
|
+
}, Math.max(1000, Math.min(5000, interval * 2))); }),
|
|
191
|
+
]).finally(() => clearTimeout(idleTimer));
|
|
192
|
+
if (chunk.done) break;
|
|
193
|
+
pending += decoder.decode(chunk.value, { stream: true });
|
|
194
|
+
pending = pending.replace(/\r\n/g, '\n');
|
|
195
|
+
if (pending.length > 1_000_000) throw new Error('SSE event exceeds limit');
|
|
196
|
+
let boundary: number;
|
|
197
|
+
while ((boundary = pending.indexOf('\n\n')) >= 0) {
|
|
198
|
+
const event = pending.slice(0, boundary);
|
|
199
|
+
pending = pending.slice(boundary + 2);
|
|
200
|
+
if (event.split('\n').some((line) => line.startsWith('id: '))) poll();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error instanceof Error && error.message === 'SSE unauthorized (401)') throw error;
|
|
205
|
+
if (!outage && !deps.signal?.aborted) out.error(`msg: SSE disconnected; polling ledger (${error instanceof Error ? error.message : String(error)})`);
|
|
206
|
+
outage = true;
|
|
207
|
+
} finally { deps.signal?.removeEventListener('abort', onAbort); controller.abort(); }
|
|
208
|
+
if (deps.signal?.aborted) break;
|
|
209
|
+
poll();
|
|
210
|
+
await sleep(interval);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
138
213
|
} finally { store.close(); }
|
|
139
214
|
})());
|
|
140
215
|
|
|
216
|
+
msg.command('ask <recipient> <question>').description('Post a question and wait for its first reply')
|
|
217
|
+
.option('--from <seat>', 'asking seat (required)').option('--wait <seconds>', 'seconds to wait for a reply (default 60)', '60')
|
|
218
|
+
.option('--json')
|
|
219
|
+
.action((recipient: string, question: string, opts: CommonOptions & { from?: string; wait?: string }) => run(async () => {
|
|
220
|
+
const from = seat(opts.from, '--from');
|
|
221
|
+
const to = canonicalSeatId(recipient);
|
|
222
|
+
const waitSeconds = integer(opts.wait, '--wait', 60, 0, 86400);
|
|
223
|
+
const interrupt = deps.signal === undefined ? new AbortController() : undefined;
|
|
224
|
+
const signal = deps.signal ?? interrupt!.signal;
|
|
225
|
+
const onInterrupt = () => interrupt?.abort();
|
|
226
|
+
if (interrupt) {
|
|
227
|
+
process.once('SIGINT', onInterrupt);
|
|
228
|
+
process.once('SIGTERM', onInterrupt);
|
|
229
|
+
}
|
|
230
|
+
let store: MsgStore | undefined;
|
|
231
|
+
try {
|
|
232
|
+
const asked = withStore(ledger => ledger.post({ from, to, body: question }));
|
|
233
|
+
const deadline = Date.now() + waitSeconds * 1000;
|
|
234
|
+
let reply: Message | undefined;
|
|
235
|
+
store = open();
|
|
236
|
+
do {
|
|
237
|
+
reply = store.listReplies(asked.id, 1)[0];
|
|
238
|
+
if (reply !== undefined || signal.aborted || Date.now() >= deadline) break;
|
|
239
|
+
await sleep(1000);
|
|
240
|
+
} while (true);
|
|
241
|
+
if (reply === undefined && signal.aborted) throw new Error(`ask interrupted after posting #${asked.id}`);
|
|
242
|
+
if (reply === undefined) throw new Error(`no reply to #${asked.id} within ${waitSeconds}s`);
|
|
243
|
+
emit(reply, opts.json, () => out.log(formatMessage(reply)));
|
|
244
|
+
} finally {
|
|
245
|
+
store?.close();
|
|
246
|
+
if (interrupt) {
|
|
247
|
+
process.removeListener('SIGINT', onInterrupt);
|
|
248
|
+
process.removeListener('SIGTERM', onInterrupt);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
})());
|
|
252
|
+
|
|
253
|
+
msg.command('thread <id>').description('Show one question and its replies in time order')
|
|
254
|
+
.option('--json')
|
|
255
|
+
.action((id: string, opts: CommonOptions) => run(() => {
|
|
256
|
+
const target = integer(id, 'message ID', 0, 1, Number.MAX_SAFE_INTEGER);
|
|
257
|
+
const thread = withStore(store => {
|
|
258
|
+
const question = store.getMessage(target);
|
|
259
|
+
if (question === null) throw new Error(`message not found: #${target}`);
|
|
260
|
+
// Keep paging until a short page so a thread longer than one store page is never truncated.
|
|
261
|
+
const replies: Message[] = [];
|
|
262
|
+
let after = 0;
|
|
263
|
+
let page: Message[];
|
|
264
|
+
do {
|
|
265
|
+
page = store.listReplies(target, 100, after);
|
|
266
|
+
replies.push(...page);
|
|
267
|
+
after = page.at(-1)?.id ?? after;
|
|
268
|
+
} while (page.length === 100);
|
|
269
|
+
return { question, replies };
|
|
270
|
+
});
|
|
271
|
+
emit(thread, opts.json, () => {
|
|
272
|
+
out.log(formatMessage(thread.question));
|
|
273
|
+
for (const reply of thread.replies) out.log(formatMessage(reply));
|
|
274
|
+
});
|
|
275
|
+
})());
|
|
276
|
+
|
|
277
|
+
msg.command('serve').description('Run the independent, read-only post office SSE service')
|
|
278
|
+
.option('--sse', 'serve GET /stream')
|
|
279
|
+
.option('--port <port>', 'port (or ELANOUS_MSG_SSE_PORT)')
|
|
280
|
+
.option('--host <host>', 'bind address', '127.0.0.1')
|
|
281
|
+
.action((opts: { sse?: boolean; port?: string; host?: string }) => run(() => {
|
|
282
|
+
if (!opts.sse) throw new Error('serve requires --sse');
|
|
283
|
+
const rawPort = opts.port ?? process.env.ELANOUS_MSG_SSE_PORT;
|
|
284
|
+
if (rawPort === undefined) throw new Error('--port or ELANOUS_MSG_SSE_PORT is required');
|
|
285
|
+
const port = integer(rawPort, '--port', 0, 0, 65535);
|
|
286
|
+
const store = openRead();
|
|
287
|
+
try {
|
|
288
|
+
const service = startMsgSseServer({ store, token: (deps.authToken ?? (() => ensureAuthToken().token))(),
|
|
289
|
+
port, hostname: opts.host });
|
|
290
|
+
out.log(`msg SSE listening on ${opts.host}:${service.port}`);
|
|
291
|
+
const stop = () => { service.stop(); store.close(); };
|
|
292
|
+
process.once('SIGINT', stop);
|
|
293
|
+
process.once('SIGTERM', stop);
|
|
294
|
+
} catch (error) { store.close(); throw error; }
|
|
295
|
+
})());
|
|
296
|
+
|
|
141
297
|
// Commander reports option/argument errors before action handlers run. Scope their exit code
|
|
142
298
|
// to msg so unrelated commands keep their own error and help behavior.
|
|
143
299
|
for (const command of [msg, ...msg.commands]) {
|