elanous 0.2.25 → 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-37fd16f479f5554a.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-24453f6a302816b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-6c131d30bb1cf871.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/{page-b12149c51f7d04b1.js → page-fa46d4588e8442c2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-dc0772b11861af51.js +1 -0
- 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-305cd294e754906e.js → page-256ffd365d32e100.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-dea4b795741126fd.js → page-e0de78ae7b99f01c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-cfbdc44b708d9801.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-45889e7070d11126.js → page-4c741eb760f2f77c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-16a2e893f83ae4d0.js → page-e9ba98f5f1440a21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-dafc901bb1b72b5f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-56888bc744e08dac.js → page-2f6f303fa114ec24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-1634081c5272006c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-952c6ceec6d9de38.js → page-ced0bcfc6542b3fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-6d0202af568f4c63.js → page-bb1f05cd51c38944.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-646b50f63b6fe732.js +1 -0
- 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-4fb6fc34e20c07b7.js → page-f736324afa2117c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-e9502921f4f01b1f.js → page-6f131b51193a36f9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-b6169ea2f09cf287.js → not-found-435f5e2faf02bc01.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-6a14333d1b1b3967.js → page-497e0eedf20e7890.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-8c91a9a66c9ec90a.js → page-a8d28fdf995e16ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8966452228753c5e.js +1 -0
- 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-3d3fc73dd187ed52.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-9c04dac0831c2bbb.js → page-c71267e7681da982.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-a21d1e1797172692.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-04de9ca6d52a3893.js → page-cb249e81ed339318.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-62527677bbc2fdc4.js → page-781cfc5114e097da.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-9a2e09c4a311764c.js → page-478157214db6a023.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-bfeb35d69e8b24bb.js → page-86141f12cf090b7f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-89cb33bced824e4a.js → page-7256c73bb1c36751.js} +1 -1
- 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-6445ac4cbb8dd869.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-21050928a697ed7a.js +1 -0
- 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-20162174b77f96cf.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-05a4b2c9b91034aa.js → page-11bf9e5725a72c22.js} +1 -1
- 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 +119 -7
- package/scripts/coord-msg-bridge.ts +209 -0
- package/scripts/coord-post.sh +40 -7
- package/scripts/coord-requests-bridge.ts +10 -5
- package/scripts/detect-test-interference.ts +31 -1
- package/scripts/expose-rubric-suggest.ts +1 -1
- package/scripts/measure-tsc-affected-roots.ts +10 -1
- package/scripts/release-loop/docs-node.ts +51 -22
- package/scripts/release-loop/export-check-node.ts +8 -5
- package/scripts/release-loop/gate-node.ts +37 -4
- package/scripts/release-loop/node-verdict.ts +10 -3
- 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 +32 -2
- package/src/agent-mission/in-agent-plugin.ts +8 -3
- package/src/agent-substrate/review-observation.ts +9 -2
- package/src/agent-substrate/self-review-cli.ts +62 -16
- package/src/boot/daemon-tools/self-implement.ts +72 -4
- package/src/briefing/brief-items.ts +151 -35
- package/src/budget/llm-cost.ts +30 -3
- 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/control-cli.ts +29 -0
- package/src/cli/doctor-cli.ts +90 -8
- package/src/cli/msg-cli.ts +177 -11
- package/src/cli/plugin-cli.ts +11 -3
- package/src/cli/pr-cli.ts +133 -18
- package/src/cli/pr-land-overlap-evidence.ts +18 -10
- package/src/cli/pr-land-refusal.ts +20 -0
- package/src/cli/release-cli.ts +34 -5
- package/src/cli/rfc-cli.ts +24 -0
- package/src/cli/seat-cli.ts +121 -0
- package/src/cli/self-utterances-cli.ts +140 -0
- package/src/cli/tailscale-serve.ts +5 -8
- package/src/cli/tasks-cli.ts +528 -31
- package/src/cli/usage-cli.ts +9 -4
- package/src/context-card/cell.ts +40 -0
- package/src/dashboard/boot-timing.ts +35 -0
- package/src/dashboard/first-screen-band.ts +63 -13
- package/src/dashboard/index.ts +94 -27
- package/src/dashboard/input/chat-main-plain-turn-runtime.ts +26 -0
- package/src/dashboard/skill-detection-runtime.ts +24 -4
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +6 -0
- package/src/debug/log.ts +1 -1
- package/src/domains/morning-report.ts +3 -1
- package/src/domains/release-tool.ts +29 -12
- package/src/domains/surface-events.ts +16 -0
- package/src/elanous-config-dir.ts +2 -2
- package/src/fleet/host-doctor.ts +73 -2
- package/src/github/pr-labels.ts +10 -4
- package/src/graph-runner/runner.ts +104 -15
- package/src/harness/harness-clean.ts +21 -3
- package/src/harness/harness-cli-command.ts +68 -10
- package/src/harness/harness-pod-dispatch.ts +11 -4
- package/src/harness/harness-stop.ts +11 -7
- package/src/hq/seats.ts +17 -3
- package/src/index.ts +69 -9
- package/src/instance/current.ts +2 -6
- package/src/instance/resolve.ts +5 -6
- package/src/intelligence-map/model-bench-scores.ts +102 -0
- package/src/llm/openai-sampling-fields.ts +89 -0
- package/src/llm/runtime-model-family.ts +61 -0
- package/src/llm/zai-coding-plan.ts +27 -1
- package/src/llm.ts +13 -89
- 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/machines/machine-ledger.ts +31 -3
- package/src/maturity/cli-maturity.ts +13 -3
- package/src/maturity/feature-maturity.ts +1 -1
- package/src/msg/msg-sse-server.ts +141 -0
- package/src/msg/msg-store.ts +149 -17
- package/src/ops/overview-collect.ts +3 -2
- package/src/ops/overview-owner.ts +29 -0
- package/src/patents/ledger.ts +128 -0
- package/src/plugin-hooks/shell-hook.ts +17 -2
- package/src/plugin-missions/evaluator-runner.ts +18 -2
- package/src/plugins/install/market-fetch.ts +32 -1
- package/src/plugins/install/plugin-install.ts +11 -1
- package/src/pod-lease/admission.ts +116 -41
- package/src/prompt-enhance/enhance.ts +11 -0
- package/src/release-loop/checklist.ts +52 -4
- package/src/release-loop/feature-store.ts +10 -9
- 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-loop.ts +5 -1
- 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/draft-sweep.ts +23 -14
- 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-dev/orchestrate.ts +6 -2
- package/src/self-dev/run-supervisor.ts +35 -1
- package/src/self-implement/gate-cli.ts +6 -2
- package/src/self-implement/goal-author.ts +5 -28
- package/src/self-implement/goal-command-execution.ts +26 -0
- 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 +5 -11
- package/src/self-implement/pwa-import-graph.ts +194 -7
- package/src/self-implement/rework-policy.ts +80 -2
- package/src/self-implement/self-implement-runtime.ts +126 -21
- package/src/self-implement/speed-claims.ts +145 -6
- package/src/session/index.ts +14 -4
- package/src/session-runtime/index.ts +55 -11
- 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 +105 -19
- 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 +56 -17
- 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 +321 -25
- 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 +66 -2
- package/src/task-agent/shadow.ts +150 -29
- package/src/task-agent/task-hand.ts +133 -8
- package/src/task-orchestrator/surfaces/pod-repair-commit.ts +96 -0
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +71 -12
- package/src/telegram.ts +30 -1
- package/src/tool-runtime/tool-search-runtime.ts +25 -0
- package/src/tui.ts +20 -1
- package/src/user-config.ts +11 -60
- 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-9360b1285b2a346e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f08f6529d09ffae1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f000d54d258f9ef6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-22f674e7e4c2c9d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-8888733e40cea6d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9717ec6964b92276.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-44eaad486bc98a45.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-97fa6cf3d34c7eb9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-35e05d7450461a67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-b2638013c0bf7dfd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-c44f93a41167536f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1b810474507f3702.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-74366ae2693a7d95.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4e634c5b98fd4e93.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ed3070d637850548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-b9cc8a10bc6078b4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-128458c8d86fbb5f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-cf0c2b0183991d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-b008aff96be56654.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-814394dcbae789b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-6a87325c87f09dd2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-852170303c6cd755.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-087ceeb975ef272b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-97c7361cc2a19319.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-8c6901833f897496.js +0 -1
- /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → y1ltt7oCZxVpC6zVPhZ2O}/_ssgManifest.js +0 -0
package/src/msg/msg-store.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
5
5
|
|
|
@@ -8,6 +8,7 @@ export interface MessageEnvelope {
|
|
|
8
8
|
to: string;
|
|
9
9
|
body: string;
|
|
10
10
|
kind?: string;
|
|
11
|
+
replyTo?: number;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export interface Message extends MessageEnvelope {
|
|
@@ -20,7 +21,7 @@ export interface UnreadRecipient {
|
|
|
20
21
|
count: number;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
const SEAT_ALIASES: Readonly<Record<string, string>> = {
|
|
24
|
+
export const SEAT_ALIASES: Readonly<Record<string, string>> = {
|
|
24
25
|
S: 'OP', OP: 'OP', COO: 'OP',
|
|
25
26
|
T: 'MK', MK: 'MK', CMO: 'MK',
|
|
26
27
|
O: 'TC', TC: 'TC', CTO: 'TC',
|
|
@@ -41,12 +42,21 @@ export function validateMessageEnvelope(input: unknown): MessageEnvelope {
|
|
|
41
42
|
const record = input as Record<string, unknown>;
|
|
42
43
|
const from = canonicalSeatId(record.from as string);
|
|
43
44
|
const to = canonicalSeatId(record.to as string);
|
|
45
|
+
if (from === 'ALL') throw new Error('ALL cannot send messages');
|
|
44
46
|
if (typeof record.body !== 'string' || !record.body.trim()) throw new Error('message body must be nonempty');
|
|
45
47
|
if (record.kind !== undefined && (typeof record.kind !== 'string' || !/^[a-z][a-z0-9_-]{0,63}$/.test(record.kind))) {
|
|
46
48
|
throw new Error('invalid message kind');
|
|
47
49
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
const replyTo = record.replyTo;
|
|
51
|
+
if (replyTo !== undefined && (typeof replyTo !== 'number' || !Number.isSafeInteger(replyTo) || replyTo < 1)) {
|
|
52
|
+
throw new Error('replyTo must be a positive integer message ID');
|
|
53
|
+
}
|
|
54
|
+
if (Object.keys(record).some(key => !['from', 'to', 'body', 'kind', 'replyTo'].includes(key))) throw new Error('unknown message field');
|
|
55
|
+
return {
|
|
56
|
+
from, to, body: record.body,
|
|
57
|
+
...(record.kind === undefined ? {} : { kind: record.kind as string }),
|
|
58
|
+
...(replyTo === undefined ? {} : { replyTo }),
|
|
59
|
+
};
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
function validCursor(cursor: number): number {
|
|
@@ -54,48 +64,109 @@ function validCursor(cursor: number): number {
|
|
|
54
64
|
return cursor;
|
|
55
65
|
}
|
|
56
66
|
|
|
67
|
+
function unreadCountOn(db: Database, to: string): number {
|
|
68
|
+
return (db.query(`SELECT COUNT(*) AS count FROM msg_messages m
|
|
69
|
+
WHERE m.id > COALESCE((SELECT last_id FROM msg_cursors WHERE recipient = ?), 0)
|
|
70
|
+
AND (m.recipient = ? OR (m.recipient = 'ALL' AND m.sender <> ?))`)
|
|
71
|
+
.get(to, to, to) as { count: number }).count;
|
|
72
|
+
}
|
|
73
|
+
|
|
57
74
|
interface MessageRow {
|
|
58
75
|
id: number;
|
|
59
76
|
sender: string;
|
|
60
77
|
recipient: string;
|
|
61
78
|
body: string;
|
|
62
79
|
kind: string | null;
|
|
80
|
+
reply_to: number | null;
|
|
63
81
|
created_at: string;
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
function fromRow(row: MessageRow): Message {
|
|
67
85
|
return { id: row.id, from: row.sender, to: row.recipient, body: row.body,
|
|
68
|
-
...(row.kind === null ? {} : { kind: row.kind }),
|
|
86
|
+
...(row.kind === null ? {} : { kind: row.kind }),
|
|
87
|
+
...(row.reply_to == null ? {} : { replyTo: row.reply_to }),
|
|
88
|
+
createdAt: row.created_at };
|
|
69
89
|
}
|
|
70
90
|
|
|
71
91
|
/** SQLite owns both append order and per-recipient acknowledgement progress. */
|
|
72
92
|
export class MsgStore {
|
|
73
93
|
readonly db: Database;
|
|
74
94
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
this.db
|
|
95
|
+
// A read-only SSE connection may outlive another process's in-place migration.
|
|
96
|
+
private get hasReplyTo(): boolean {
|
|
97
|
+
return (this.db.query(`SELECT COUNT(*) AS count FROM pragma_table_info('msg_messages') WHERE name = 'reply_to'`)
|
|
98
|
+
.get() as { count: number }).count > 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private readWithSchema<T>(read: (hasReplyTo: boolean) => T): T {
|
|
102
|
+
// The schema projection and rows must share one SQLite snapshot. Otherwise a writer can
|
|
103
|
+
// migrate between them and a reader could advance past a reply with its link erased.
|
|
104
|
+
return this.db.transaction(() => read(this.hasReplyTo)).deferred();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
constructor(path: string = defaultMsgStorePath(), options: { readOnly?: boolean } = {}) {
|
|
108
|
+
if (options.readOnly && (path === ':memory:' || !existsSync(path))) throw new Error('message ledger unavailable for read-only SSE service');
|
|
109
|
+
if (!options.readOnly && path !== ':memory:') mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
110
|
+
this.db = options.readOnly ? new Database(path, { readonly: true }) : new Database(path);
|
|
78
111
|
this.db.exec('PRAGMA busy_timeout = 5000');
|
|
79
|
-
this.db.exec(`CREATE TABLE IF NOT EXISTS msg_messages (
|
|
112
|
+
if (!options.readOnly) this.db.exec(`CREATE TABLE IF NOT EXISTS msg_messages (
|
|
80
113
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
81
114
|
sender TEXT NOT NULL,
|
|
82
115
|
recipient TEXT NOT NULL,
|
|
83
116
|
body TEXT NOT NULL,
|
|
84
117
|
kind TEXT,
|
|
118
|
+
reply_to INTEGER,
|
|
85
119
|
created_at TEXT NOT NULL
|
|
86
120
|
);
|
|
87
121
|
CREATE INDEX IF NOT EXISTS idx_msg_messages_recipient_id ON msg_messages(recipient, id);
|
|
88
122
|
CREATE TABLE IF NOT EXISTS msg_cursors (
|
|
89
123
|
recipient TEXT PRIMARY KEY,
|
|
90
124
|
last_id INTEGER NOT NULL DEFAULT 0 CHECK(last_id >= 0)
|
|
125
|
+
);
|
|
126
|
+
CREATE TABLE IF NOT EXISTS msg_provenance (
|
|
127
|
+
provenance_key TEXT PRIMARY KEY,
|
|
128
|
+
message_id INTEGER NOT NULL REFERENCES msg_messages(id)
|
|
91
129
|
);`);
|
|
130
|
+
// Serialize the schema check and ALTER across processes opening an old ledger at once.
|
|
131
|
+
if (!options.readOnly) this.db.transaction(() => {
|
|
132
|
+
const replyToColumn = (this.db.query(`SELECT COUNT(*) AS count FROM pragma_table_info('msg_messages') WHERE name = 'reply_to'`)
|
|
133
|
+
.get() as { count: number }).count;
|
|
134
|
+
if (replyToColumn === 0) this.db.exec('ALTER TABLE msg_messages ADD COLUMN reply_to INTEGER');
|
|
135
|
+
this.db.exec('CREATE INDEX IF NOT EXISTS idx_msg_messages_reply_to ON msg_messages(reply_to, id)');
|
|
136
|
+
}).immediate();
|
|
92
137
|
}
|
|
93
138
|
|
|
94
139
|
append(input: MessageEnvelope): Message {
|
|
140
|
+
return this.insertMessage(input);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Append once for a coordination comment or a task failure incident. */
|
|
144
|
+
appendWithProvenance(input: MessageEnvelope, provenanceKey: string): Message {
|
|
145
|
+
if (typeof provenanceKey !== 'string' || !/^(?:coord|ta-failure):[^:]+:[^:]+$/.test(provenanceKey)) {
|
|
146
|
+
throw new Error('invalid message provenance key');
|
|
147
|
+
}
|
|
95
148
|
const msg = validateMessageEnvelope(input);
|
|
149
|
+
if (provenanceKey.split(':')[2] !== msg.to) throw new Error('provenance recipient does not match message recipient');
|
|
150
|
+
return this.db.transaction(() => {
|
|
151
|
+
const existing = this.db.query(`SELECT m.id, m.sender, m.recipient, m.body, m.kind, m.reply_to, m.created_at
|
|
152
|
+
FROM msg_provenance p JOIN msg_messages m ON m.id = p.message_id WHERE p.provenance_key = ?`)
|
|
153
|
+
.get(provenanceKey) as MessageRow | null;
|
|
154
|
+
if (existing) return fromRow(existing);
|
|
155
|
+
const message = this.insertMessage(msg);
|
|
156
|
+
this.db.query('INSERT INTO msg_provenance(provenance_key, message_id) VALUES (?, ?)')
|
|
157
|
+
.run(provenanceKey, message.id);
|
|
158
|
+
return message;
|
|
159
|
+
}).immediate();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private insertMessage(input: MessageEnvelope): Message {
|
|
163
|
+
const msg = validateMessageEnvelope(input);
|
|
164
|
+
if (msg.replyTo !== undefined && this.db.query('SELECT 1 FROM msg_messages WHERE id = ?').get(msg.replyTo) === null) {
|
|
165
|
+
throw new Error(`reply target not found: #${msg.replyTo}`);
|
|
166
|
+
}
|
|
96
167
|
const createdAt = new Date().toISOString();
|
|
97
|
-
const result = this.db.query(`INSERT INTO msg_messages(sender, recipient, body, kind, created_at)
|
|
98
|
-
VALUES (?, ?, ?, ?, ?)`).run(msg.from, msg.to, msg.body, msg.kind ?? null, createdAt);
|
|
168
|
+
const result = this.db.query(`INSERT INTO msg_messages(sender, recipient, body, kind, reply_to, created_at)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(msg.from, msg.to, msg.body, msg.kind ?? null, msg.replyTo ?? null, createdAt);
|
|
99
170
|
return { id: Number(result.lastInsertRowid), ...msg, createdAt };
|
|
100
171
|
}
|
|
101
172
|
|
|
@@ -104,24 +175,61 @@ export class MsgStore {
|
|
|
104
175
|
/** Messages strictly after the supplied cursor, ordered by durable insertion ID. */
|
|
105
176
|
listByRecipient(recipient: string, after: number = 0, limit: number = 100): Message[] {
|
|
106
177
|
const to = canonicalSeatId(recipient);
|
|
178
|
+
if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
|
|
107
179
|
validCursor(after);
|
|
108
180
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) throw new Error('limit must be between 1 and 1000');
|
|
109
|
-
return (this.db.query(`SELECT id, sender, recipient, body, kind, created_at FROM msg_messages
|
|
110
|
-
WHERE
|
|
181
|
+
return this.readWithSchema(hasReplyTo => (this.db.query(`SELECT id, sender, recipient, body, kind, ${hasReplyTo ? 'reply_to' : 'NULL AS reply_to'}, created_at FROM msg_messages
|
|
182
|
+
WHERE id > ? AND (recipient = ? OR (recipient = 'ALL' AND sender <> ?))
|
|
183
|
+
ORDER BY id ASC LIMIT ?`).all(after, to, to, limit) as MessageRow[]).map(fromRow));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** One message by durable ID, or null when absent. */
|
|
187
|
+
getMessage(messageId: number): Message | null {
|
|
188
|
+
if (!Number.isSafeInteger(messageId) || messageId < 1) throw new Error('message ID must be a positive integer');
|
|
189
|
+
return this.readWithSchema(hasReplyTo => {
|
|
190
|
+
const row = this.db.query(`SELECT id, sender, recipient, body, kind, ${hasReplyTo ? 'reply_to' : 'NULL AS reply_to'}, created_at FROM msg_messages
|
|
191
|
+
WHERE id = ?`).get(messageId) as MessageRow | null;
|
|
192
|
+
return row === null ? null : fromRow(row);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Replies pointing at the supplied message, ordered by durable insertion ID. */
|
|
197
|
+
listReplies(messageId: number, limit: number = 100, afterId: number = 0): Message[] {
|
|
198
|
+
if (!Number.isSafeInteger(messageId) || messageId < 1) throw new Error('message ID must be a positive integer');
|
|
199
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) throw new Error('limit must be between 1 and 1000');
|
|
200
|
+
validCursor(afterId);
|
|
201
|
+
return this.readWithSchema(hasReplyTo => hasReplyTo
|
|
202
|
+
? (this.db.query(`SELECT id, sender, recipient, body, kind, reply_to, created_at FROM msg_messages
|
|
203
|
+
WHERE reply_to = ? AND id > ? ORDER BY id ASC LIMIT ?`).all(messageId, afterId, limit) as MessageRow[]).map(fromRow)
|
|
204
|
+
: []);
|
|
111
205
|
}
|
|
112
206
|
|
|
113
207
|
list(recipient: string, after: number = 0, limit: number = 100): Message[] {
|
|
114
208
|
return this.listByRecipient(recipient, after, limit);
|
|
115
209
|
}
|
|
116
210
|
|
|
211
|
+
/** One ordered scan for every SSE subscriber; recipient filtering happens in the server. */
|
|
212
|
+
listAfter(after: number, limit: number = 1000): Message[] {
|
|
213
|
+
validCursor(after);
|
|
214
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1000) throw new Error('limit must be between 1 and 1000');
|
|
215
|
+
return this.readWithSchema(hasReplyTo => (this.db.query(`SELECT id, sender, recipient, body, kind, ${hasReplyTo ? 'reply_to' : 'NULL AS reply_to'}, created_at FROM msg_messages
|
|
216
|
+
WHERE id > ? ORDER BY id ASC LIMIT ?`).all(after, limit) as MessageRow[]).map(fromRow));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
latestId(): number {
|
|
220
|
+
return (this.db.query('SELECT COALESCE(MAX(id), 0) AS id FROM msg_messages').get() as { id: number }).id;
|
|
221
|
+
}
|
|
222
|
+
|
|
117
223
|
getCursor(recipient: string): number {
|
|
118
224
|
const to = canonicalSeatId(recipient);
|
|
225
|
+
if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
|
|
119
226
|
return (this.db.query('SELECT last_id FROM msg_cursors WHERE recipient = ?').get(to) as { last_id: number } | null)?.last_id ?? 0;
|
|
120
227
|
}
|
|
121
228
|
|
|
122
229
|
/** Atomically reject future IDs and keep concurrent or stale acknowledgements from rewinding. */
|
|
123
230
|
advanceCursor(recipient: string, cursor: number): number {
|
|
124
231
|
const to = canonicalSeatId(recipient);
|
|
232
|
+
if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
|
|
125
233
|
validCursor(cursor);
|
|
126
234
|
const result = this.db.query(`INSERT INTO msg_cursors(recipient, last_id)
|
|
127
235
|
SELECT ?, ? WHERE ? <= (SELECT COALESCE(MAX(id), 0) FROM msg_messages)
|
|
@@ -133,11 +241,24 @@ export class MsgStore {
|
|
|
133
241
|
|
|
134
242
|
ack(recipient: string, cursor: number): number { return this.advanceCursor(recipient, cursor); }
|
|
135
243
|
|
|
244
|
+
unreadCount(recipient: string): number {
|
|
245
|
+
const to = canonicalSeatId(recipient);
|
|
246
|
+
if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
|
|
247
|
+
return unreadCountOn(this.db, to);
|
|
248
|
+
}
|
|
249
|
+
|
|
136
250
|
unreadRecipients(): UnreadRecipient[] {
|
|
137
|
-
return this.db.query(`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
251
|
+
return this.db.query(`WITH recipients(recipient) AS (
|
|
252
|
+
SELECT 'OP' UNION SELECT 'MK' UNION SELECT 'TC' UNION SELECT 'UX'
|
|
253
|
+
UNION SELECT recipient FROM msg_messages WHERE recipient <> 'ALL'
|
|
254
|
+
UNION SELECT recipient FROM msg_cursors WHERE recipient <> 'ALL'
|
|
255
|
+
)
|
|
256
|
+
SELECT r.recipient AS recipient, COUNT(m.id) AS count
|
|
257
|
+
FROM recipients r
|
|
258
|
+
LEFT JOIN msg_cursors c ON c.recipient = r.recipient
|
|
259
|
+
JOIN msg_messages m ON m.id > COALESCE(c.last_id, 0)
|
|
260
|
+
AND (m.recipient = r.recipient OR (m.recipient = 'ALL' AND m.sender <> r.recipient))
|
|
261
|
+
GROUP BY r.recipient ORDER BY r.recipient ASC`).all() as UnreadRecipient[];
|
|
141
262
|
}
|
|
142
263
|
|
|
143
264
|
unread(): UnreadRecipient[] { return this.unreadRecipients(); }
|
|
@@ -147,3 +268,14 @@ export class MsgStore {
|
|
|
147
268
|
|
|
148
269
|
export function defaultMsgStorePath(): string { return join(elanousStateRoot(), 'msg', 'messages.db'); }
|
|
149
270
|
export function openMsgStore(path?: string): MsgStore { return new MsgStore(path); }
|
|
271
|
+
export function openMsgStoreReadOnly(path?: string): MsgStore { return new MsgStore(path, { readOnly: true }); }
|
|
272
|
+
|
|
273
|
+
/** Observe one recipient without creating a store or advancing its acknowledgement. */
|
|
274
|
+
export function readUnreadCount(recipient: string, path: string = defaultMsgStorePath()): number | null {
|
|
275
|
+
const to = canonicalSeatId(recipient);
|
|
276
|
+
if (to === 'ALL') throw new Error('ALL is a topic, not a recipient');
|
|
277
|
+
if (!existsSync(path)) return null;
|
|
278
|
+
const db = new Database(path, { readonly: true });
|
|
279
|
+
try { return unreadCountOn(db, to); }
|
|
280
|
+
finally { db.close(); }
|
|
281
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type OverviewSnapshot, type OwnerTaskRow, type ParentsInfo, type PodMemberRow, type ReleaseRunInfo, type ScheduleInfo,
|
|
17
17
|
} from './overview.js';
|
|
18
18
|
import { SEAT_ERA_SOURCE, readDispatcherBeats, readDispatcherCapacity, readSeatEraHanded, seatEraFeederDir } from './overview-seat-era.js';
|
|
19
|
+
import { matchesOverviewOwner, normalizeOverviewOwner } from './overview-owner.js';
|
|
19
20
|
|
|
20
21
|
const GB = 1024 ** 3;
|
|
21
22
|
/** 하트비트 주기 30 s × 2 (RFC §A2 «낡음»). */
|
|
@@ -77,8 +78,8 @@ export function ownerRows(cards: readonly TaskCard[], sinceIso: string, ownerFil
|
|
|
77
78
|
const rows: Record<string, OwnerTaskRow> = {};
|
|
78
79
|
for (const card of cards) {
|
|
79
80
|
if (!card || typeof card.createdAt !== 'string' || card.createdAt < sinceIso) continue;
|
|
80
|
-
const owner = card.seat
|
|
81
|
-
if (ownerFilter
|
|
81
|
+
const owner = normalizeOverviewOwner(card.seat);
|
|
82
|
+
if (ownerFilter !== undefined && !matchesOverviewOwner(owner, ownerFilter)) continue;
|
|
82
83
|
const row = rows[owner] ??= { handed: 0, launched: 0, prBound: 0, failed: 0, launchFailed: 0 };
|
|
83
84
|
if (card.status === 'handed') row.handed++;
|
|
84
85
|
else if (card.status === 'launched') row.launched++;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Owner labels used by ops overview task rows and the --owner filter. */
|
|
2
|
+
type OverviewOwnerKind = 'seat' | 'steward' | 'orchestrator' | 'external';
|
|
3
|
+
type OverviewOwner = `${OverviewOwnerKind}:${string}` | 'unassigned';
|
|
4
|
+
|
|
5
|
+
const OWNER_KINDS = new Set<OverviewOwnerKind>(['seat', 'steward', 'orchestrator', 'external']);
|
|
6
|
+
|
|
7
|
+
/** Normalize a task-card owner into a stable kind-prefixed overview key. */
|
|
8
|
+
export function normalizeOverviewOwner(value: unknown): OverviewOwner {
|
|
9
|
+
if (typeof value !== 'string' || value.trim() === '') return 'unassigned';
|
|
10
|
+
const normalized = value.trim();
|
|
11
|
+
const separator = normalized.indexOf(':');
|
|
12
|
+
if (separator >= 0) {
|
|
13
|
+
const kind = normalized.slice(0, separator).toLowerCase() as OverviewOwnerKind;
|
|
14
|
+
const name = normalized.slice(separator + 1).trim();
|
|
15
|
+
if (OWNER_KINDS.has(kind) && name) return `${kind}:${name}`;
|
|
16
|
+
}
|
|
17
|
+
const kind = normalized.toLowerCase();
|
|
18
|
+
if (kind === 'steward' || kind === 'orchestrator' || kind === 'external') return `${kind}:default`;
|
|
19
|
+
return `seat:${normalized}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Whether a normalized owner key matches a CLI filter, accepting either the full key or its kind. */
|
|
23
|
+
export function matchesOverviewOwner(owner: OverviewOwner, filter?: string): boolean {
|
|
24
|
+
if (!filter || filter === 'all') return true;
|
|
25
|
+
const normalizedFilter = filter.trim();
|
|
26
|
+
if (!normalizedFilter) return true;
|
|
27
|
+
if (normalizedFilter.toLowerCase() === 'unassigned') return owner === 'unassigned';
|
|
28
|
+
return owner === normalizeOverviewOwner(normalizedFilter) || owner.startsWith(`${normalizedFilter.toLowerCase()}:`);
|
|
29
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { chmodSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
5
|
+
import { debug } from '../debug/log.js';
|
|
6
|
+
|
|
7
|
+
/** 특허 후보의 갈래 — 구체 후보와 기전은 비공개 원장에만 둔다. */
|
|
8
|
+
const PATENT_TRACKS = ['비공개', '출원', '신규'] as const;
|
|
9
|
+
/** 특허 후보의 상태 — 조사/명세/출원/공개 허용 중 공개 허용 상태만 공개 읽기 통로에 나온다. */
|
|
10
|
+
const PATENT_STATUSES = ['조사', '명세', '출원', '공개 허용'] as const;
|
|
11
|
+
|
|
12
|
+
export type PatentTrack = (typeof PATENT_TRACKS)[number];
|
|
13
|
+
export type PatentStatus = (typeof PATENT_STATUSES)[number];
|
|
14
|
+
|
|
15
|
+
export class PatentsInputError extends Error {
|
|
16
|
+
override name = 'PatentsInputError';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PatentCandidate {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
track: PatentTrack;
|
|
23
|
+
status: PatentStatus;
|
|
24
|
+
deadline: string | null;
|
|
25
|
+
created_at: string;
|
|
26
|
+
updated_at: string;
|
|
27
|
+
}
|
|
28
|
+
export interface PatentLedgerOptions { stateDir?: string; now?: () => Date }
|
|
29
|
+
|
|
30
|
+
function required(value: string, field: string): string {
|
|
31
|
+
if (typeof value !== 'string' || !value.trim()) throw new PatentsInputError(`${field} is required`);
|
|
32
|
+
return value.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function trackOf(value: PatentTrack): PatentTrack {
|
|
36
|
+
if (!PATENT_TRACKS.includes(value)) throw new PatentsInputError(`invalid track: ${value}`);
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function statusOf(value: PatentStatus): PatentStatus {
|
|
41
|
+
if (!PATENT_STATUSES.includes(value)) throw new PatentsInputError(`invalid status: ${value}`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isoDate(value: string, field: string): string {
|
|
46
|
+
if (typeof value !== 'string' || !/^\d{4}-\d\d-\d\d$/.test(value)) throw new PatentsInputError(`invalid ${field}: ${value}`);
|
|
47
|
+
const date = new Date(`${value}T00:00:00.000Z`);
|
|
48
|
+
if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value) throw new PatentsInputError(`invalid ${field}: ${value}`);
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 후보 식별자·갈래·상태·마감일의 비공개 원장 — 공개 허용 상태 후보만 읽기 통로로 노출한다. */
|
|
53
|
+
export class PatentLedger {
|
|
54
|
+
readonly path: string;
|
|
55
|
+
private readonly now: () => Date;
|
|
56
|
+
|
|
57
|
+
constructor(options: PatentLedgerOptions = {}) {
|
|
58
|
+
this.path = join(options.stateDir ?? elanousStateRoot(), 'patents', 'patents.sqlite');
|
|
59
|
+
this.now = options.now ?? (() => new Date());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private open(): Database {
|
|
63
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
64
|
+
const db = new Database(this.path, { create: true, strict: true });
|
|
65
|
+
try {
|
|
66
|
+
chmodSync(this.path, 0o600);
|
|
67
|
+
db.exec('PRAGMA busy_timeout = 10000');
|
|
68
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
69
|
+
db.exec(`CREATE TABLE IF NOT EXISTS patent_candidates (
|
|
70
|
+
id TEXT PRIMARY KEY, title TEXT NOT NULL,
|
|
71
|
+
track TEXT NOT NULL CHECK(track IN ('비공개','출원','신규')),
|
|
72
|
+
status TEXT NOT NULL CHECK(status IN ('조사','명세','출원','공개 허용')),
|
|
73
|
+
deadline TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);`);
|
|
74
|
+
return db;
|
|
75
|
+
} catch (error) { db.close(); throw error; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private using<T>(work: (db: Database) => T): T {
|
|
79
|
+
const db = this.open();
|
|
80
|
+
try { return work(db); } finally { db.close(); }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private transaction<T>(db: Database, work: () => T): T {
|
|
84
|
+
db.exec('BEGIN IMMEDIATE');
|
|
85
|
+
try { const result = work(); db.exec('COMMIT'); return result; }
|
|
86
|
+
catch (error) { db.exec('ROLLBACK'); throw error; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private row(db: Database, id: string): PatentCandidate {
|
|
90
|
+
const row = db.query('SELECT id, title, track, status, deadline, created_at, updated_at FROM patent_candidates WHERE id = ?').get(id) as PatentCandidate | null;
|
|
91
|
+
if (!row) throw new PatentsInputError(`patent candidate not found: ${id}`);
|
|
92
|
+
return row;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
add(input: { id: string; title: string; track: PatentTrack; status?: PatentStatus; deadline?: string }): PatentCandidate {
|
|
96
|
+
const id = required(input.id, 'id');
|
|
97
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(id)) throw new PatentsInputError('invalid patent candidate id');
|
|
98
|
+
const title = required(input.title, 'title');
|
|
99
|
+
const track = trackOf(input.track);
|
|
100
|
+
const status = statusOf(input.status ?? '조사');
|
|
101
|
+
const deadline = input.deadline === undefined ? null : isoDate(input.deadline, 'deadline');
|
|
102
|
+
return this.using(db => this.transaction(db, () => {
|
|
103
|
+
if (db.query('SELECT 1 FROM patent_candidates WHERE id = ?').get(id)) throw new PatentsInputError(`patent candidate already exists: ${id}`);
|
|
104
|
+
const at = this.now().toISOString();
|
|
105
|
+
db.query('INSERT INTO patent_candidates (id, title, track, status, deadline, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
106
|
+
.run(id, title, track, status, deadline, at, at);
|
|
107
|
+
debug.log('patents.ledger', 'add', { id, status });
|
|
108
|
+
return this.row(db, id);
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setStatus(id: string, status: PatentStatus): PatentCandidate {
|
|
113
|
+
const next = statusOf(status);
|
|
114
|
+
return this.using(db => this.transaction(db, () => {
|
|
115
|
+
this.row(db, id);
|
|
116
|
+
const at = this.now().toISOString();
|
|
117
|
+
db.query('UPDATE patent_candidates SET status = ?, updated_at = ? WHERE id = ?').run(next, at, id);
|
|
118
|
+
debug.log('patents.ledger', 'status', { id, status: next });
|
|
119
|
+
return this.row(db, id);
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 이 원장의 유일한 공개 읽기 통로 — 공개 허용 상태 후보만 나온다(조사·명세·출원 상태 후보는 나오지 않는다). */
|
|
124
|
+
listPublicCandidates(): PatentCandidate[] {
|
|
125
|
+
return this.using(db =>
|
|
126
|
+
db.query(`SELECT id, title, track, status, deadline, created_at, updated_at FROM patent_candidates WHERE status = '공개 허용' ORDER BY created_at, id`).all() as PatentCandidate[]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -118,6 +118,7 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
|
|
|
118
118
|
let stdout = '';
|
|
119
119
|
let stderrBuffer = '';
|
|
120
120
|
let killed = false;
|
|
121
|
+
let stdinError: string | undefined;
|
|
121
122
|
const onAbort = () => {
|
|
122
123
|
if (!killed) {
|
|
123
124
|
killed = true;
|
|
@@ -132,6 +133,14 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
|
|
|
132
133
|
opts.abortSignal.addEventListener('abort', onAbort, { once: true });
|
|
133
134
|
|
|
134
135
|
child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
|
|
136
|
+
// A short-lived command can close its read end before end() finishes.
|
|
137
|
+
// EPIPE says only that stdin was unread; stdout and exit status still
|
|
138
|
+
// determine the hook result. Other pipe errors are real I/O failures.
|
|
139
|
+
child.stdin?.on('error', (err: NodeJS.ErrnoException) => {
|
|
140
|
+
if (err.code === 'EPIPE') return;
|
|
141
|
+
stdinError = `stdin error: ${err.message}`;
|
|
142
|
+
opts.onStderr(stdinError);
|
|
143
|
+
});
|
|
135
144
|
child.stderr?.on('data', (chunk) => {
|
|
136
145
|
stderrBuffer += String(chunk);
|
|
137
146
|
let idx;
|
|
@@ -145,13 +154,19 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
|
|
|
145
154
|
clearTimeout(timer);
|
|
146
155
|
if (stderrBuffer.trim()) opts.onStderr(stderrBuffer.trim());
|
|
147
156
|
// killed => treat as a non-zero exit so dispatcher abort.
|
|
148
|
-
resolve({ stdout, exitCode: killed ? 124 : (code ?? 1) });
|
|
157
|
+
resolve({ stdout, exitCode: killed ? 124 : stdinError ? 127 : (code ?? 1), error: stdinError });
|
|
149
158
|
});
|
|
150
159
|
child.on('error', (err) => {
|
|
151
160
|
clearTimeout(timer);
|
|
152
161
|
opts.onStderr(`spawn error: ${err.message}`);
|
|
153
162
|
resolve({ stdout, exitCode: 127 });
|
|
154
163
|
});
|
|
155
|
-
try { child.stdin?.end(opts.stdin); }
|
|
164
|
+
try { child.stdin?.end(opts.stdin); }
|
|
165
|
+
catch (err) {
|
|
166
|
+
if ((err as NodeJS.ErrnoException).code !== 'EPIPE') {
|
|
167
|
+
stdinError = `stdin error: ${(err as Error).message}`;
|
|
168
|
+
opts.onStderr(stdinError);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
156
171
|
});
|
|
157
172
|
}
|
|
@@ -137,6 +137,7 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
|
|
|
137
137
|
let stderrBuf = '';
|
|
138
138
|
let killed = false;
|
|
139
139
|
let timedOut = false;
|
|
140
|
+
let stdinError: string | undefined;
|
|
140
141
|
const kill = (timeout: boolean) => {
|
|
141
142
|
if (killed) return;
|
|
142
143
|
killed = true;
|
|
@@ -150,6 +151,14 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
|
|
|
150
151
|
opts.abortSignal.addEventListener('abort', () => kill(false), { once: true });
|
|
151
152
|
|
|
152
153
|
child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
|
|
154
|
+
// Early exit may close stdin before end() finishes. A broken pipe must
|
|
155
|
+
// not replace the child's stdout/exit result; other write errors must
|
|
156
|
+
// surface as evaluator errors.
|
|
157
|
+
child.stdin?.on('error', (err: NodeJS.ErrnoException) => {
|
|
158
|
+
if (err.code === 'EPIPE') return;
|
|
159
|
+
stdinError = `stdin error: ${err.message}`;
|
|
160
|
+
opts.onStderr(stdinError);
|
|
161
|
+
});
|
|
153
162
|
child.stderr?.on('data', (chunk) => {
|
|
154
163
|
stderrBuf += String(chunk);
|
|
155
164
|
let idx;
|
|
@@ -164,8 +173,9 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
|
|
|
164
173
|
if (stderrBuf.trim()) opts.onStderr(stderrBuf.trim());
|
|
165
174
|
resolve({
|
|
166
175
|
stdout,
|
|
167
|
-
exitCode: timedOut ? 124 : (code ?? 1),
|
|
176
|
+
exitCode: timedOut ? 124 : stdinError ? 127 : (code ?? 1),
|
|
168
177
|
timedOut,
|
|
178
|
+
error: stdinError,
|
|
169
179
|
});
|
|
170
180
|
});
|
|
171
181
|
child.on('error', (err) => {
|
|
@@ -173,6 +183,12 @@ function runShell(opts: RunShellOpts): Promise<RunShellResult> {
|
|
|
173
183
|
opts.onStderr(`spawn error: ${err.message}`);
|
|
174
184
|
resolve({ stdout, exitCode: 127, timedOut: false });
|
|
175
185
|
});
|
|
176
|
-
try { child.stdin?.end(opts.stdin); }
|
|
186
|
+
try { child.stdin?.end(opts.stdin); }
|
|
187
|
+
catch (err) {
|
|
188
|
+
if ((err as NodeJS.ErrnoException).code !== 'EPIPE') {
|
|
189
|
+
stdinError = `stdin error: ${(err as Error).message}`;
|
|
190
|
+
opts.onStderr(stdinError);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
177
193
|
});
|
|
178
194
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { withFileLockSync } from '../../storage/file-lock.js';
|
|
4
|
-
import {
|
|
4
|
+
import { loadPluginManifestFromDir } from '../core/manifest.js';
|
|
5
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
6
|
import { elanousStateRoot } from '../../autopilot/state-paths.js';
|
|
6
7
|
import { userConfigPath } from '../../user-config.js';
|
|
7
8
|
import { OFFICIAL_INDEX_KEYS } from '../../market/official-keys.js';
|
|
@@ -37,6 +38,36 @@ export interface MarketIndex {
|
|
|
37
38
|
signature: 'ok';
|
|
38
39
|
keyId: string;
|
|
39
40
|
}
|
|
41
|
+
/** Resolve only an installed, available Codex plugin package to the local-path input
|
|
42
|
+
* already accepted by `plugin add`. Codex marketplace records are not Elanous signed indices. */
|
|
43
|
+
export function codexPluginPackagePath(response: unknown, name: string, marketplace: string): string {
|
|
44
|
+
if (!NAME.test(name) || !NAME.test(marketplace)) throw new MarketFetchError('io', 'invalid Codex plugin or marketplace name');
|
|
45
|
+
const inventory = response as { marketplaces?: unknown } | null;
|
|
46
|
+
if (!inventory || !Array.isArray(inventory.marketplaces)) throw new MarketFetchError('io', 'invalid Codex plugin/list response');
|
|
47
|
+
const matches: string[] = [];
|
|
48
|
+
for (const rawMarket of inventory.marketplaces) {
|
|
49
|
+
if (!rawMarket || typeof rawMarket !== 'object') throw new MarketFetchError('io', 'invalid Codex marketplace');
|
|
50
|
+
const market = rawMarket as { name?: unknown; plugins?: unknown };
|
|
51
|
+
if (market.name !== marketplace) continue;
|
|
52
|
+
if (!Array.isArray(market.plugins)) throw new MarketFetchError('io', 'invalid Codex marketplace plugins');
|
|
53
|
+
for (const rawPlugin of market.plugins) {
|
|
54
|
+
if (!rawPlugin || typeof rawPlugin !== 'object') continue;
|
|
55
|
+
const plugin = rawPlugin as Record<string, unknown>;
|
|
56
|
+
if (plugin.name !== name || plugin.installed !== true) continue;
|
|
57
|
+
if (typeof plugin.availability === 'string' && plugin.availability.toUpperCase() !== 'AVAILABLE') continue;
|
|
58
|
+
const path = plugin.installPath ?? plugin.path;
|
|
59
|
+
if (typeof path !== 'string' || !isAbsolute(path) || !existsSync(path) || !statSync(path).isDirectory()) {
|
|
60
|
+
throw new MarketFetchError('io', 'Codex installed plugin package directory unavailable');
|
|
61
|
+
}
|
|
62
|
+
const manifest = loadPluginManifestFromDir(path, { id: name }).manifest;
|
|
63
|
+
if (manifest.id !== name) throw new MarketFetchError('io', 'Codex package name differs from inventory');
|
|
64
|
+
matches.push(path);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (matches.length !== 1) throw new MarketFetchError('io', `Codex plugin not installed or ambiguous: ${name}@${marketplace}`);
|
|
68
|
+
return matches[0]!;
|
|
69
|
+
}
|
|
70
|
+
|
|
40
71
|
export class MarketFetchError extends Error {
|
|
41
72
|
constructor(public readonly reason: 'io' | 'signature', message: string) {
|
|
42
73
|
super(message);
|
|
@@ -438,6 +438,13 @@ function declaresMain(packageRoot: string): boolean {
|
|
|
438
438
|
return !existsSync(join(packageRoot, 'plugin.json')) && !existsSync(join(packageRoot, '.codex-plugin', 'plugin.json'));
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
/** [wiring] Caller: installPlugin. The PluginManifest normalized by
|
|
442
|
+
* loadPluginManifestFromDir is handed into the install path here — the install
|
|
443
|
+
* destination follows the manifest identity, never the source spec/dir name. */
|
|
444
|
+
export function pluginInstallPath(root: string, market: string, manifest: PluginManifest): string {
|
|
445
|
+
return join(root, 'plugins', market, manifest.id, manifest.version);
|
|
446
|
+
}
|
|
447
|
+
|
|
441
448
|
export async function installPlugin(spec: string, opts: InstallOptions = {}): Promise<InstalledPlugin> {
|
|
442
449
|
const root = resolve(opts.root ?? elanousStateRoot());
|
|
443
450
|
const stageRoot = join(root, 'plugins', '.staging');
|
|
@@ -466,6 +473,10 @@ export async function installPlugin(spec: string, opts: InstallOptions = {}): Pr
|
|
|
466
473
|
const manifest: PluginManifest = loadPluginManifestFromDir(join(stage, 'package'), { id: source.expectedName ?? basename(spec) }).manifest;
|
|
467
474
|
if (!NAME.test(manifest.id) || !VERSION.test(manifest.version)) fail('io', 'invalid plugin name or version');
|
|
468
475
|
if (source.expectedName && (manifest.id !== source.expectedName || manifest.version !== source.expectedVersion)) fail('conflict', 'market name or version differs from manifest');
|
|
476
|
+
// [wiring] installPlugin is the caller: the PluginManifest normalized above by
|
|
477
|
+
// loadPluginManifestFromDir (present manifest or the loader's manifest-absent
|
|
478
|
+
// fallback) is handed into the install path here, past the NAME/VERSION gates.
|
|
479
|
+
const destination = pluginInstallPath(root, source.market, manifest);
|
|
469
480
|
const packageRoot = join(stage, 'package');
|
|
470
481
|
const mainPath = inside(packageRoot, manifest.main);
|
|
471
482
|
// A skills/graphs-only pack (official `elanous-basics`) declares no `main` — the default `./plugin.ts` is not a promise.
|
|
@@ -480,7 +491,6 @@ export async function installPlugin(spec: string, opts: InstallOptions = {}): Pr
|
|
|
480
491
|
const path = inside(packageRoot, asset);
|
|
481
492
|
if (!existsSync(path) || !realpathSync(path).startsWith(`${realpathSync(packageRoot)}${sep}`)) fail('io', `plugin asset not found or unsafe: ${asset}`);
|
|
482
493
|
}
|
|
483
|
-
const destination = join(root, 'plugins', source.market, name, version);
|
|
484
494
|
const emit = (event: InstallEvent) => opts.onEvent?.(event);
|
|
485
495
|
emit({ event: 'resolve', plugin: name, version, sha256: source.sha256 });
|
|
486
496
|
let security: ReturnType<typeof inspectPluginSecurity>;
|