elanous 0.2.11 → 0.2.13
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/{1394-556de6d71bbdb0fb.js → 1394-931c9827067b2276.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/3869-3cd46526879f454c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3992-2e522a8ac5f2afde.js → 3992-fe5c06d6b31f8b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{8605-ea5bee9dfae512c7.js → 8605-9da4678f8a310d18.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-6a061bd97f0b9398.js → page-e49b9b76c565a0b1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-7c97dcdfe2e37a60.js → page-4c1dbb21861beab2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-5721537ca8a3598c.js → page-f6bc66b919b2ea7d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-b654240a572e4c48.js → page-60603c1e056c9b25.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-b9750abc897508c3.js → page-a0ea8f5a2341ee6b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-c8b1ccda6ba7ae98.js → page-31012264da8a341f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-d6b1d156fb95e74b.js → layout-50309aeb0bdd554a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9688f5b4f1ff950f.js → page-0696ee3fac9fe56c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-35414e0a007b4082.js → page-6ddd2109b7fc3a80.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-e001823c35c9dd67.js → page-7a2bf830038a2264.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-371423d1a573d64b.js → page-1f327f18694fabca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-4b764b125339ac17.js → page-e5cef2fe6e4028df.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-cdf8e4b01067bcf1.js → page-cf596128d2b420ba.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-3a24072b35470157.js → page-f672536f14f0969f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-d37cb0d52f9b6bd5.js → page-29a709616df9b050.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-686da0df9bf23e5c.js → page-fee842d7c2151129.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-4dc614f3ce043e63.js → page-78b40a344ed22f24.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-d02c782fc792e2af.js → page-a011dd5b0bff29c5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-239c17b911b84e72.js → page-d92e32d51ab5157e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-dc6a79fb003f746c.js → page-5f21aed6932ef375.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-0fffb84cafdc9da6.js → page-1e1a53946804debf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-1218753c82d585f6.js → page-a1833cd43d832601.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-4b5935e67531efb5.js → page-63d14a4d8fe1e1c1.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/{page-3285bc87caf1eaa5.js → page-2a511d320f762e06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-ad553b0e06505ecd.js → page-1c3e03f88dac1e21.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-fca096e3e860306f.js → page-c4d5289c2ea97a89.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-9ebfa78f0d9885c3.js → page-b91f8952deaeaf27.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-3dc5f21f7e39622d.js → page-71b43a2588489987.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +1 -0
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/examples/teams/personas/backend.yaml +4 -0
- package/examples/teams/personas/pm.yaml +4 -0
- package/examples/teams/personas/qa.yaml +4 -0
- package/examples/teams/small-product.yaml +20 -0
- package/graphs/context/nightly-condense.yaml +21 -0
- package/graphs/context/recipes.yaml +12 -0
- package/graphs/demo/inside-seed.yaml +20 -0
- package/graphs/demo/recipes.yaml +12 -0
- package/graphs/lecture/lecture-note.yaml +28 -0
- package/graphs/lecture/recipes.yaml +17 -0
- package/package.json +2 -1
- package/plugins/card-followup/examples/sample-card.png +0 -0
- package/scripts/context-hooks/emit.ts +36 -14
- package/scripts/coord-channel-watch.sh +1 -0
- package/scripts/discord-seat-probe.ts +144 -0
- package/scripts/hq/hq-standby.sh +65 -0
- package/scripts/hq/standby-promote.ts +173 -0
- package/scripts/hq/standby-role.ts +59 -0
- package/scripts/hq/standby-snapshot.ts +226 -0
- package/scripts/hq/standby-verify.ts +219 -0
- package/scripts/lecture-note/node.ts +256 -0
- package/scripts/lib/telegram-inject-args.ts +31 -0
- package/scripts/measure-pod-memory-by-goal.ts +130 -0
- package/scripts/release-loop/auto-approve-node.ts +1 -1
- package/scripts/release-loop/checklist-gate-node.ts +2 -1
- package/scripts/release-loop/gate-node.ts +61 -17
- package/scripts/release-loop/ops-upgrade-node.ts +59 -6
- package/scripts/release-loop/publish-node.ts +4 -1
- package/scripts/release-loop/version-node.ts +13 -3
- package/scripts/telegram-inject.ts +14 -6
- package/scripts/test-deterministic.ts +53 -3
- package/scripts/test-diet/remeasure-list.ts +112 -0
- package/scripts/ux/demo-rehearsal.ts +17 -13
- package/scripts/ux/demo-seed.ts +108 -0
- package/scripts/ux/lib/rehearsal-verdict.ts +12 -5
- package/skills/omni-crawl/SKILL.md +16 -16
- package/skills/omni-crawl/package-lock.json +1 -544
- package/skills/omni-crawl/package.json +3 -4
- package/skills/omni-crawl/scripts/main.ts +13 -13
- package/skills/omni-crawl/scripts/monitor.ts +8 -8
- package/skills/omni-crawl/src/apify.ts +13 -2
- package/skills/omni-crawl/src/capture.ts +6 -0
- package/skills/omni-crawl/src/registry.ts +2 -2
- package/skills/omni-crawl/src/stdout-json.ts +4 -0
- package/skills/omni-digest/SKILL.md +10 -11
- package/skills/omni-digest/package-lock.json +0 -545
- package/skills/omni-digest/package.json +2 -5
- package/skills/omni-digest/scripts/main.ts +3 -3
- package/skills/omni-digest/src/delegate.ts +5 -5
- package/skills/omni-digest/src/fetch.ts +2 -2
- package/skills/omni-digest/src/media.ts +25 -11
- package/skills/omni-digest/src/visual.ts +3 -2
- package/skills/omni-market/scripts/main.ts +1 -1
- package/skills/omni-market/scripts/stdout-json.ts +4 -0
- package/src/agent/identity-env.ts +11 -0
- package/src/agent-mission/driver.ts +150 -23
- package/src/agent-mission/duo.ts +94 -0
- package/src/agent-mission/pty-decision.ts +2 -2
- package/src/autopilot/pty-control-loop.ts +4 -1
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/autopilot/track-agent.ts +4 -1
- package/src/boot/daemon-prompt-request.ts +3 -0
- package/src/boot/daemon-prompt-turn.ts +6 -2
- package/src/boot/daemon-runtime.ts +48 -16
- package/src/chat/index.ts +1 -0
- package/src/cli/decisions-cli.ts +51 -19
- package/src/cli/outputs-cli.ts +92 -0
- package/src/cli/persona-cli.ts +45 -13
- package/src/cli/pod-cli.ts +27 -3
- package/src/cli/pod-memory-advice.ts +35 -0
- package/src/cli/pr-cli.ts +15 -1
- package/src/cli/release-cli.ts +135 -15
- package/src/context-bus/condense.ts +194 -0
- package/src/context-bus/context-day.ts +82 -0
- package/src/context-bus/context-now-surfaces.ts +32 -2
- package/src/context-bus/context-now.ts +2 -2
- package/src/context-bus/coord-events.ts +2 -2
- package/src/context-bus/long-term-memory.ts +118 -22
- package/src/context-bus/nightly-condense.ts +153 -0
- package/src/context-bus/session-events.ts +35 -0
- package/src/dashboard/dashboard-status-lines.ts +2 -0
- package/src/dashboard/index.ts +8 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +24 -0
- package/src/decisions/decision-cards.ts +13 -7
- package/src/decisions/decision-ledger.ts +43 -17
- package/src/domains/outbound-alert.ts +99 -31
- package/src/domains/outbound-log-probe.ts +8 -0
- package/src/execution-loop/launch-gate.ts +17 -8
- package/src/field-note/telegram.ts +142 -0
- package/src/graph-kinds/registry.ts +2 -2
- package/src/graph-runner/graph-approval-card.ts +2 -2
- package/src/graph-runner/graph-grow-llm.ts +23 -13
- package/src/graph-runner/graph-grow.ts +69 -13
- package/src/graph-runner/graph-tick.ts +9 -2
- package/src/graph-runner/runner.ts +92 -24
- package/src/harness/harness-cli-command.ts +165 -17
- package/src/harness/harness-queue-child.ts +37 -0
- package/src/harness/harness-queue.ts +306 -0
- package/src/hooks/consult-intake.ts +153 -0
- package/src/hooks/github-shadow.ts +72 -2
- package/src/hooks/receiver.ts +82 -3
- package/src/hq/hq.ts +318 -0
- package/src/hq/lease.ts +210 -0
- package/src/hq/ledger-write-target.ts +41 -0
- package/src/index.ts +232 -16
- package/src/intake-plane/discord-seat-work.ts +36 -3
- package/src/intake-plane/linear-wish.ts +44 -0
- package/src/intake-plane/persona-answer.ts +5 -0
- package/src/intake-plane/telegram-seat-work.ts +13 -6
- package/src/intake-plane/wish-card.ts +33 -0
- package/src/llm/difficulty.ts +1 -1
- package/src/loops/checker.ts +178 -0
- package/src/loops/loop-cli.ts +37 -3
- package/src/loops/observe.ts +49 -0
- package/src/loops/orchestrator/overlap.ts +141 -0
- package/src/loops/orchestrator/traffic.ts +150 -0
- package/src/loops/package/verify.ts +125 -0
- package/src/loops/verdict.ts +22 -0
- package/src/maturity/feature-maturity.ts +5 -4
- package/src/memory.ts +37 -4
- package/src/nexus/api/card-followup-route.ts +118 -0
- package/src/nexus/api/decisions-route.ts +68 -0
- package/src/nexus/api/http-server.ts +78 -8
- package/src/nexus/api/outbound-report.ts +40 -7
- package/src/nexus/api/personas.ts +97 -54
- package/src/nexus/api/skill-exec-route.ts +97 -0
- package/src/nexus/api/task-cards-api.ts +19 -0
- package/src/nexus/api/workflow-history.ts +88 -0
- package/src/nexus/api/workflows.ts +52 -0
- package/src/nexus/outbound/delivery-ledger.ts +17 -8
- package/src/nexus/outbound/router.ts +8 -3
- package/src/persona/loader.ts +18 -0
- package/src/persona/persona-todo.ts +14 -9
- package/src/persona/presets.ts +10 -2
- package/src/persona/team.ts +105 -0
- package/src/persona/types.ts +4 -0
- package/src/plugins/maker/market-bundle.ts +2 -1
- package/src/pod-lease/host-lease.ts +130 -20
- package/src/release-loop/checklist.ts +15 -3
- package/src/release-loop/feature-store.ts +24 -10
- package/src/release-loop/landed-but-yellow.ts +60 -0
- package/src/seat-dispatch/seat-ask.ts +57 -12
- package/src/seat-loop/seat-loop.ts +253 -18
- package/src/seat-loop/seat-pack.ts +127 -0
- package/src/self-dev/ask-launch-flow.ts +3 -0
- package/src/self-dev/draft-sweep.ts +44 -10
- package/src/self-dev/draft-triage-rules.ts +54 -2
- package/src/self-implement/budget-gate.ts +29 -12
- package/src/self-implement/gate-candidate-ranking.ts +60 -0
- package/src/self-implement/gate-scope.ts +9 -8
- package/src/self-implement/headless-elanous-driver.ts +7 -2
- package/src/self-implement/l8-merge-queue.ts +157 -1
- package/src/self-implement/orchestrator.ts +15 -0
- package/src/self-implement/test-seams.ts +1 -0
- package/src/steward/launch.ts +5 -1
- package/src/steward/readiness.ts +125 -0
- package/src/steward/rescue.ts +75 -0
- package/src/steward/triage.ts +48 -9
- package/src/task-cards/card-cli.ts +78 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +14 -10
- package/src/task-orchestrator/surfaces/pod-lease.ts +18 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +27 -10
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +46 -8
- package/src/telegram-commands.ts +21 -0
- package/src/telegram-dispatch-forward.ts +1 -1
- package/src/telegram-fabric-plan.ts +90 -0
- package/src/telegram-project-command.ts +84 -4
- package/src/telegram.ts +37 -5
- package/src/tool-runtime/elanous-control-runtimes.ts +2 -1
- package/src/user-config.ts +101 -13
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/executor.ts +4 -0
- package/src/workflow-runtime/node-catalog.ts +13 -0
- package/src/workflow-runtime/nodes/knowledge.ts +38 -0
- package/src/workflow-runtime/schema.ts +17 -1
- package/src/workflow-runtime/types.ts +13 -1
- package/apps/pwa/out/_next/static/chunks/2310-7a59f043f6db207e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2548-6e316138d347cefe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3869-a39d1d9921e10d43.js +0 -1
- package/apps/pwa/out/_next/static/chunks/507-5acd98bd6dab2d8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5445-745eb795c5963d79.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-482c6619b3fdda20.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-1dcdc58fc2de2454.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-cb95a7432c9ee744.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12b6d2f8d2d1aea.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b9efe868bfd36255.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-8fd36984f549270a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-4cd1de371f8ad048.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-d99f89657828421c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-50cb2a0eb15e6c83.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-2a9932ddb62d76bd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-6b15dd6456c5df09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e7a529e0507ac86c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-94c82eb0ae5abb32.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-0be8d4b2ced75ba0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-53f80edc6b8dc07c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-c6da2b468454532d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-93bc70e36f595414.js +0 -1
- package/apps/pwa/out/_next/static/css/543f56bcb1f79fca.css +0 -1
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{Y4zKzL62sakG5momz3SoQ → D51l8jUBplJrIpWvENsDK}/_ssgManifest.js +0 -0
|
@@ -66,7 +66,7 @@ export function contextNow(options: { topic?: string; limit?: number } = {}, dep
|
|
|
66
66
|
? redactSecretText(entry.item.title.split(/\r?\n/, 1)[0] ?? '').slice(0, 120) : null, source });
|
|
67
67
|
}
|
|
68
68
|
const rawEvents = (deps.events ?? (since => listCoordEvents({ since })))(new Date(now.getTime() - 7 * 86_400_000).toISOString());
|
|
69
|
-
const chronologicalEvents = rawEvents.slice().sort((a, b) => a.at.localeCompare(b.at)
|
|
69
|
+
const chronologicalEvents = rawEvents.slice().sort((a, b) => a.at.localeCompare(b.at));
|
|
70
70
|
const matches = (text: string) => !topic || text.toLocaleLowerCase().includes(topic.toLocaleLowerCase());
|
|
71
71
|
const selectedFacts = facts.filter(f => !topic || (f.kind === 'cell' && matches(`${f.id} ${f.title}`))
|
|
72
72
|
|| (f.kind === 'seat' && matches(`${f.id ?? ''} ${f.title ?? ''}`)));
|
|
@@ -95,7 +95,7 @@ export function contextNow(options: { topic?: string; limit?: number } = {}, dep
|
|
|
95
95
|
source: event.refs.source ?? event.refs.url ?? `elanous://context/event/${encodeURIComponent(event.id)}`,
|
|
96
96
|
}));
|
|
97
97
|
const selectedEvents = summaries.filter(event => matches(event.summary)).reverse().slice(0, limit);
|
|
98
|
-
const guide = summaries.filter(event => event.kind === 'guide-changed' || event.summary.startsWith('📌안내'))
|
|
98
|
+
const guide = summaries.filter(event => event.kind === 'guide-changed' || event.kind === 'guidance-changed' || event.summary.startsWith('📌안내'))
|
|
99
99
|
.filter(event => matches(event.summary)).reverse().slice(0, limit)
|
|
100
100
|
.map(event => `📌 ${event.summary} — ${event.source}`);
|
|
101
101
|
const answer = { at, topic, facts: boundedFacts, events: selectedEvents, guide };
|
|
@@ -22,7 +22,7 @@ export interface CoordEvent {
|
|
|
22
22
|
text: string;
|
|
23
23
|
summary: string;
|
|
24
24
|
kind: string;
|
|
25
|
-
refs: Pick<CoordEventInput, 'seat' | 'recipients' | 'all' | 'kind' | 'slot' | 'deadline' | 'url'> & { origin?: string; source?: string };
|
|
25
|
+
refs: Pick<CoordEventInput, 'seat' | 'recipients' | 'all' | 'kind' | 'slot' | 'deadline' | 'url'> & { origin?: string; source?: string; ref?: string | null };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function seatIds(): Set<string> {
|
|
@@ -104,7 +104,7 @@ export function listCoordEvents(opts: { since: string; seat?: string }, deps: {
|
|
|
104
104
|
const db = deps.db ?? openSurfaceEventsDb();
|
|
105
105
|
try {
|
|
106
106
|
const rows = db.prepare(`SELECT id, ts, text, summary, kind, refs FROM events
|
|
107
|
-
WHERE surface IN ('coord:channel', 'context:external') AND direction='outbound' AND ts>=?
|
|
107
|
+
WHERE surface IN ('coord:channel', 'context:external', 'context:session') AND direction='outbound' AND ts>=?
|
|
108
108
|
${opts.seat ? "AND json_extract(refs,'$.seat')=?" : ''} ORDER BY ts ASC, rowid ASC`)
|
|
109
109
|
.all(...(opts.seat ? [opts.since, opts.seat] : [opts.since])) as Array<{
|
|
110
110
|
id: string; ts: string; text: string; summary: string; kind: string; refs: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { redactSecretText } from '../debug/log.js';
|
|
1
|
+
import { debug, redactSecretText } from '../debug/log.js';
|
|
2
2
|
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
3
3
|
import { listCoordEvents, type CoordEvent } from './coord-events.js';
|
|
4
4
|
|
|
@@ -33,6 +33,14 @@ export interface MemoryDeps {
|
|
|
33
33
|
decisions?: () => DecisionEntry[];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export type CondenseSkipReason = 'no-seat' | 'no-project' | 'no-topic' | 'no-summary' | 'bad-link' | 'bad-claim' | 'summarize-failed';
|
|
37
|
+
export type CondenseSkipped = Partial<Record<CondenseSkipReason, number>>;
|
|
38
|
+
export interface CondenseWindowReport {
|
|
39
|
+
cards: MemoryItem[];
|
|
40
|
+
skipped: CondenseSkipped;
|
|
41
|
+
folded: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
36
44
|
const DAY_MS = 86_400_000;
|
|
37
45
|
const oneLine = (text: string) => redactSecretText(text.split(/\r?\n/, 1)[0] ?? '').trim();
|
|
38
46
|
|
|
@@ -43,40 +51,125 @@ export async function condenseContextDay(
|
|
|
43
51
|
if (!/^\d{4}-\d\d-\d\d$/.test(day) || new Date(`${day}T00:00:00.000Z`).toISOString().slice(0, 10) !== day) {
|
|
44
52
|
throw new Error('day must be a valid UTC YYYY-MM-DD');
|
|
45
53
|
}
|
|
46
|
-
|
|
54
|
+
return condenseContextWindow(`${day}T00:00:00.000Z`, new Date(Date.parse(`${day}T00:00:00.000Z`) + DAY_MS).toISOString(), previous, deps, now);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read-only condensation over an exact rolling UTC window; the caller owns any persistence. */
|
|
58
|
+
export async function condenseContextWindow(
|
|
59
|
+
since: string, until: string, previous: readonly MemoryItem[], deps: MemoryDeps, now: Date = new Date(),
|
|
60
|
+
): Promise<MemoryItem[]> {
|
|
61
|
+
return (await condenseContextWindowWithReport(since, until, previous, deps, now)).cards;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function condenseContextWindowWithReport(
|
|
65
|
+
since: string, until: string, previous: readonly MemoryItem[], deps: MemoryDeps, now: Date = new Date(),
|
|
66
|
+
): Promise<CondenseWindowReport> {
|
|
47
67
|
const start = Date.parse(since);
|
|
48
|
-
const
|
|
49
|
-
|
|
68
|
+
const end = Date.parse(until);
|
|
69
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) throw new Error('invalid context window');
|
|
70
|
+
// Fetch the previous local day as well: the store compares timestamp text rather than instants.
|
|
50
71
|
const events = (deps.events ?? (from => listCoordEvents({ since: from })))(new Date(start - DAY_MS).toISOString());
|
|
51
72
|
const decisions = (deps.decisions ?? (() => new DecisionLedger().list({ status: 'all' })))();
|
|
73
|
+
const inWindow = (at: string) => {
|
|
74
|
+
const time = Date.parse(at);
|
|
75
|
+
return Number.isFinite(time) && time >= start && time < end;
|
|
76
|
+
};
|
|
77
|
+
const harnessRuns: Array<{ events: CoordEvent[]; locators: Set<string> }> = [];
|
|
78
|
+
const earlierStarts: CoordEvent[] = [];
|
|
79
|
+
const otherEvents: CoordEvent[] = [];
|
|
80
|
+
let harnessEvents = 0;
|
|
81
|
+
for (const event of events) {
|
|
82
|
+
if (!inWindow(event.at)) {
|
|
83
|
+
const time = Date.parse(event.at);
|
|
84
|
+
if (event.kind === 'started' && Number.isFinite(time) && time >= start - DAY_MS && time < start) {
|
|
85
|
+
earlierStarts.push(event);
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
// Harness lifecycle = started/finished, or any harness-child event that is not a report (review round 3).
|
|
90
|
+
const isLifecycle = event.kind === 'started' || event.kind === 'finished'
|
|
91
|
+
|| (event.refs.seat === 'harness-child' && event.kind !== '보고');
|
|
92
|
+
if (!isLifecycle) {
|
|
93
|
+
otherEvents.push(event);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
harnessEvents++;
|
|
97
|
+
const locators = new Set([event.refs.source, event.refs.url].filter((ref): ref is string => !!ref));
|
|
98
|
+
const group = { events: [event], locators };
|
|
99
|
+
// Either locator can identify the run, including a bridge through a third event.
|
|
100
|
+
for (let i = 0; i < harnessRuns.length;) {
|
|
101
|
+
const prior = harnessRuns[i]!;
|
|
102
|
+
if (locators.size && [...prior.locators].some(ref => locators.has(ref))) {
|
|
103
|
+
group.events.push(...prior.events);
|
|
104
|
+
for (const ref of prior.locators) locators.add(ref);
|
|
105
|
+
harnessRuns.splice(i, 1);
|
|
106
|
+
i = 0;
|
|
107
|
+
} else i++;
|
|
108
|
+
}
|
|
109
|
+
harnessRuns.push(group);
|
|
110
|
+
}
|
|
111
|
+
const eventSource = (event: CoordEvent): MemorySource => ({ kind: 'event', at: event.at, seat: event.refs.seat,
|
|
112
|
+
text: event.summary, source: event.refs.source ?? event.refs.url ?? `elanous://context/event/${encodeURIComponent(event.id)}` });
|
|
113
|
+
const foldedSources: MemorySource[] = [];
|
|
114
|
+
for (const group of harnessRuns) {
|
|
115
|
+
group.events.sort((a, b) => Date.parse(a.at) - Date.parse(b.at) || a.id.localeCompare(b.id));
|
|
116
|
+
const finish = group.events.filter(event => event.kind === 'finished').at(-1);
|
|
117
|
+
const inWindowStart = group.events.find(event => event.kind === 'started');
|
|
118
|
+
const begin = inWindowStart ?? (finish ? earlierStarts.filter(event =>
|
|
119
|
+
(group.locators.has(event.refs.source ?? '') || group.locators.has(event.refs.url ?? '')))
|
|
120
|
+
.sort((a, b) => Date.parse(b.at) - Date.parse(a.at)).at(0) : undefined);
|
|
121
|
+
const winner = finish ?? inWindowStart ?? group.events.at(-1)!;
|
|
122
|
+
const frequency = (ref: string) => group.events.filter(event => event.refs.source === ref || event.refs.url === ref).length;
|
|
123
|
+
const run = [...group.locators].sort((a, b) => frequency(b) - frequency(a))[0] ?? eventSource(winner).source;
|
|
124
|
+
const duration = finish && begin && Date.parse(finish.at) >= Date.parse(begin.at)
|
|
125
|
+
? `${Math.floor((Date.parse(finish.at) - Date.parse(begin.at)) / 60_000)}m ${Math.floor(((Date.parse(finish.at) - Date.parse(begin.at)) % 60_000) / 1_000)}s`
|
|
126
|
+
: '알 수 없음';
|
|
127
|
+
const result = finish ? oneLine(finish.summary).replace(/^Harness child finished:\s*/i, '') || '종료'
|
|
128
|
+
: '진행 중';
|
|
129
|
+
foldedSources.push({ ...eventSource(winner), seat: winner.refs.seat || begin?.refs.seat || '',
|
|
130
|
+
source: run, text: `${run} ${result} · ${duration}` });
|
|
131
|
+
}
|
|
132
|
+
const folded = harnessEvents - foldedSources.length;
|
|
133
|
+
try { debug.log('context.condense', 'folded', { harnessEvents, kept: foldedSources.length }); }
|
|
134
|
+
catch { /* Observation must not stop condensation. */ }
|
|
52
135
|
const sources: MemorySource[] = [
|
|
53
|
-
...
|
|
54
|
-
text: event.summary, source: event.refs.source ?? event.refs.url ?? `elanous://context/event/${encodeURIComponent(event.id)}` })),
|
|
136
|
+
...otherEvents.map(eventSource), ...foldedSources,
|
|
55
137
|
...decisions.map(decision => ({ kind: 'decision' as const,
|
|
56
138
|
at: decision.decidedAt ?? decision.raisedAt ?? decision.importedAt ?? '',
|
|
57
139
|
seat: decision.raisedBy.track ?? 'OP',
|
|
58
140
|
text: decision.status === 'decided' ? `${decision.title}: ${decision.options.find(o => o.key === decision.choice)?.label ?? decision.note ?? decision.scqa.a ?? '결정됨'}` : `${decision.title}: ${decision.status}`,
|
|
59
141
|
source: `elanous://decisions/${encodeURIComponent(decision.id)}` })),
|
|
60
|
-
].filter(source =>
|
|
61
|
-
|
|
62
|
-
return Number.isFinite(at) && at >= start && at < until;
|
|
63
|
-
}).sort((a, b) => Date.parse(a.at) - Date.parse(b.at) || a.source.localeCompare(b.source));
|
|
142
|
+
].filter(source => inWindow(source.at))
|
|
143
|
+
.sort((a, b) => Date.parse(a.at) - Date.parse(b.at) || a.source.localeCompare(b.source));
|
|
64
144
|
|
|
65
145
|
const items = new Map<string, MemoryItem>();
|
|
146
|
+
const skipped: CondenseSkipped = {};
|
|
147
|
+
let kept = 0;
|
|
148
|
+
const skip = (reason: CondenseSkipReason, source: MemorySource) => {
|
|
149
|
+
skipped[reason] = (skipped[reason] ?? 0) + 1;
|
|
150
|
+
try { debug.log('context.condense', 'source-skipped', { reason, kind: source.kind, at: source.at }); }
|
|
151
|
+
catch { /* Observation must not stop condensation. */ }
|
|
152
|
+
};
|
|
153
|
+
const safeLine = (value: unknown) => typeof value === 'string' ? oneLine(value) : '';
|
|
66
154
|
const keyFor = (item: Pick<MemoryItem, 'project' | 'seat' | 'topic'>) => JSON.stringify([item.project, item.seat, item.topic]);
|
|
67
155
|
for (const item of previous) items.set(keyFor(item), { ...item });
|
|
68
156
|
for (const source of sources) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (
|
|
157
|
+
const seat = safeLine(source.seat);
|
|
158
|
+
if (!seat) { skip('no-seat', source); continue; }
|
|
159
|
+
let summary: MemorySummary;
|
|
160
|
+
try { summary = await deps.summarize(source); }
|
|
161
|
+
catch { skip('summarize-failed', source); continue; }
|
|
162
|
+
const project = safeLine(summary?.project);
|
|
163
|
+
const topic = safeLine(summary?.topic);
|
|
164
|
+
const text = safeLine(summary?.summary);
|
|
165
|
+
const link = safeLine(source.source);
|
|
166
|
+
if (!project) { skip('no-project', source); continue; }
|
|
167
|
+
if (!topic) { skip('no-topic', source); continue; }
|
|
168
|
+
if (!text) { skip('no-summary', source); continue; }
|
|
169
|
+
if (!/^(?:https?:\/\/|elanous:\/\/)[^\s]+$/.test(link)) { skip('bad-link', source); continue; }
|
|
170
|
+
const claim = summary.claim && { key: safeLine(summary.claim.key), value: safeLine(summary.claim.value) };
|
|
171
|
+
if (claim && (!claim.key || !claim.value)) { skip('bad-claim', source); continue; }
|
|
172
|
+
kept++;
|
|
80
173
|
const key = keyFor({ project, seat, topic });
|
|
81
174
|
const old = items.get(key);
|
|
82
175
|
// Equal timestamps are resolved by the source link, so shuffled input does not change the winner.
|
|
@@ -103,6 +196,9 @@ export async function condenseContextDay(
|
|
|
103
196
|
finalItems[index] = { ...item, conflict: { owner: 'OP', sources: [conflicting.source, item.source] } };
|
|
104
197
|
}
|
|
105
198
|
}
|
|
106
|
-
|
|
199
|
+
const cards = finalItems.sort((a, b) => a.project.localeCompare(b.project) || a.seat.localeCompare(b.seat)
|
|
107
200
|
|| a.topic.localeCompare(b.topic));
|
|
201
|
+
try { debug.log('context.condense', 'window', { sources: sources.length, kept, skipped }); }
|
|
202
|
+
catch { /* Observation must not stop condensation. */ }
|
|
203
|
+
return { cards, skipped, folded };
|
|
108
204
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, mkdirSync, writeFileSync, renameSync, existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { streamLLM } from '../llm.js';
|
|
4
|
+
import { condenseContextDay, type MemoryItem, type MemorySource, type MemorySummary } from './long-term-memory.js';
|
|
5
|
+
import { listCoordEvents, type CoordEvent } from './coord-events.js';
|
|
6
|
+
import { DecisionLedger, type DecisionEntry } from '../decisions/decision-ledger.js';
|
|
7
|
+
import { acquireLockAsync } from '../storage/file-lock.js';
|
|
8
|
+
|
|
9
|
+
type Stage = 'collect' | 'condense' | 'record' | 'conflict-candidates';
|
|
10
|
+
type Input = { day?: string; events?: CoordEvent[]; decisions?: DecisionEntry[]; summaries?: Record<string, MemorySummary> };
|
|
11
|
+
type Context = { graphId?: string; nodeId?: string; input: Input | null; outputs: Record<string, { day?: string; events?: CoordEvent[]; decisions?: DecisionEntry[]; items?: MemoryItem[] } | null> };
|
|
12
|
+
|
|
13
|
+
const dayBefore = (date: Date) => new Date(date.getTime() - 86_400_000).toISOString().slice(0, 10);
|
|
14
|
+
const fileName = (value: string) => encodeURIComponent(value).replace(/\./g, '%2E');
|
|
15
|
+
|
|
16
|
+
function snapshotPath(root: string, project: string, seat: string): string {
|
|
17
|
+
if (project === '.' || project === '..' || seat === '.' || seat === '..') throw new Error('invalid memory path');
|
|
18
|
+
return join(root, 'context-memory', fileName(project), `${fileName(seat)}.json`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function save(file: string, value: unknown): void {
|
|
22
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
23
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
24
|
+
writeFileSync(temp, JSON.stringify(value, null, 2) + '\n');
|
|
25
|
+
renameSync(temp, file);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function withCurrentDay<T>(root: string, day: string, write: () => T): Promise<T> {
|
|
29
|
+
const dir = join(root, 'context-memory');
|
|
30
|
+
mkdirSync(dir, { recursive: true });
|
|
31
|
+
const lock = await acquireLockAsync(join(dir, '.nightly-write.lock'));
|
|
32
|
+
try {
|
|
33
|
+
const marker = join(dir, 'latest-day');
|
|
34
|
+
const latest = latestDay(root);
|
|
35
|
+
if ((latest && latest > day) || storedItems(root).some(item =>
|
|
36
|
+
Date.parse(item.updatedAt) >= Date.parse(`${day}T00:00:00Z`) + 86_400_000)) {
|
|
37
|
+
throw new Error(`cannot replay ${day}: later project/seat memory exists`);
|
|
38
|
+
}
|
|
39
|
+
if (!lock.stillHeld()) throw new Error('nightly memory write lock lost');
|
|
40
|
+
// Reserve the day before touching either output, so a failed writer cannot let an older run overwrite it.
|
|
41
|
+
if (!latest || latest < day) save(marker, day);
|
|
42
|
+
return write();
|
|
43
|
+
} finally { lock.release(); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function latestDay(root: string): string | undefined {
|
|
47
|
+
const marker = join(root, 'context-memory', 'latest-day');
|
|
48
|
+
return existsSync(marker) ? JSON.parse(readFileSync(marker, 'utf8')) as string : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function storedItems(root: string): MemoryItem[] {
|
|
52
|
+
const dir = join(root, 'context-memory');
|
|
53
|
+
if (!existsSync(dir)) return [];
|
|
54
|
+
const items: MemoryItem[] = [];
|
|
55
|
+
for (const project of readdirSync(dir, { withFileTypes: true })) {
|
|
56
|
+
if (!project.isDirectory()) continue;
|
|
57
|
+
for (const seat of readdirSync(join(dir, project.name))) {
|
|
58
|
+
if (!seat.endsWith('.json')) continue;
|
|
59
|
+
const content: unknown = JSON.parse(readFileSync(join(dir, project.name, seat), 'utf8'));
|
|
60
|
+
if (!Array.isArray(content)) throw new Error('invalid context memory file');
|
|
61
|
+
items.push(...content as MemoryItem[]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return items;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function summarize(source: MemorySource, injected?: Record<string, MemorySummary>): Promise<MemorySummary> {
|
|
68
|
+
if (injected) {
|
|
69
|
+
const result = injected[source.source];
|
|
70
|
+
if (!result) throw new Error(`missing test summary: ${source.source}`);
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
const response = await streamLLM([
|
|
74
|
+
{ role: 'system', content: 'Summarize this one-line context event as a compact project/seat memory item. Return ONLY JSON: {"project":"...","topic":"...","summary":"...","claim":{"key":"...","value":"..."}}. Omit claim when the event makes no comparable factual assertion. Never invent a project, topic or claim.' },
|
|
75
|
+
{ role: 'user', content: JSON.stringify(source) },
|
|
76
|
+
], () => {}, { maxTokens: 350, usageRole: 'research' });
|
|
77
|
+
const parsed: unknown = JSON.parse(response.replace(/^```(?:json)?\s*|\s*```$/g, '').trim());
|
|
78
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('invalid memory summary');
|
|
79
|
+
const value = parsed as Record<string, unknown>;
|
|
80
|
+
if (typeof value.project !== 'string' || typeof value.topic !== 'string' || typeof value.summary !== 'string' ||
|
|
81
|
+
(value.claim !== undefined && (!value.claim || typeof value.claim !== 'object' || Array.isArray(value.claim) ||
|
|
82
|
+
typeof (value.claim as Record<string, unknown>).key !== 'string' || typeof (value.claim as Record<string, unknown>).value !== 'string'))) {
|
|
83
|
+
throw new Error('invalid memory summary');
|
|
84
|
+
}
|
|
85
|
+
return value as unknown as MemorySummary;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Stage outputs travel only through the graph runner's per-run contexts. No decision ledger write occurs here. */
|
|
89
|
+
export async function runNightlyStage(stage: Stage, context: Context, root: string, dryRun: boolean, now = new Date()): Promise<Record<string, unknown>> {
|
|
90
|
+
const input = context.input ?? {};
|
|
91
|
+
if (dryRun && stage !== 'collect' && (!input.events || !input.decisions || !input.summaries)) {
|
|
92
|
+
throw new Error('dry-run requires fake events, decisions and summaries');
|
|
93
|
+
}
|
|
94
|
+
const day = context.outputs.collect?.day ?? input.day ?? dayBefore(now);
|
|
95
|
+
if (!/^\d{4}-\d\d-\d\d$/.test(day) || new Date(`${day}T00:00:00Z`).toISOString().slice(0, 10) !== day) {
|
|
96
|
+
throw new Error('invalid UTC day');
|
|
97
|
+
}
|
|
98
|
+
if (stage === 'collect') {
|
|
99
|
+
if (!dryRun && ((latestDay(root) ?? '') > day || storedItems(root).some(item =>
|
|
100
|
+
Date.parse(item.updatedAt) >= Date.parse(`${day}T00:00:00Z`) + 86_400_000))) {
|
|
101
|
+
throw new Error(`cannot replay ${day}: later project/seat memory exists`);
|
|
102
|
+
}
|
|
103
|
+
if (dryRun && (!input.events || !input.decisions || !input.summaries)) {
|
|
104
|
+
throw new Error('dry-run requires fake events, decisions and summaries');
|
|
105
|
+
}
|
|
106
|
+
const events = input.events ?? listCoordEvents({ since: new Date(Date.parse(`${day}T00:00:00Z`) - 86_400_000).toISOString() });
|
|
107
|
+
const decisions = input.decisions ?? (dryRun ? [] : new DecisionLedger().list({ status: 'all' }));
|
|
108
|
+
return { outcome: 'ok', day, events, decisions };
|
|
109
|
+
}
|
|
110
|
+
const collected = context.outputs.collect;
|
|
111
|
+
if (!collected?.day || !collected.events || !collected.decisions) throw new Error('collect output missing');
|
|
112
|
+
if (stage === 'condense') {
|
|
113
|
+
const items = await condenseContextDay(collected.day, dryRun ? [] : storedItems(root).filter(item => Date.parse(item.updatedAt) < Date.parse(`${collected.day}T00:00:00Z`)), {
|
|
114
|
+
events: () => collected.events!, decisions: () => collected.decisions!,
|
|
115
|
+
summarize: source => summarize(source, input.summaries),
|
|
116
|
+
}, now);
|
|
117
|
+
return { outcome: 'ok', items };
|
|
118
|
+
}
|
|
119
|
+
const items = context.outputs.condense?.items;
|
|
120
|
+
if (!items) throw new Error('condense output missing');
|
|
121
|
+
if (stage === 'record') {
|
|
122
|
+
const grouped = new Map<string, MemoryItem[]>();
|
|
123
|
+
for (const item of items) {
|
|
124
|
+
const file = snapshotPath(root, item.project, item.seat);
|
|
125
|
+
grouped.set(file, [...grouped.get(file) ?? [], item]);
|
|
126
|
+
}
|
|
127
|
+
if (!dryRun) await withCurrentDay(root, day, () => {
|
|
128
|
+
for (const [file, group] of grouped) save(file, group);
|
|
129
|
+
});
|
|
130
|
+
return { outcome: 'ok', files: [...grouped.keys()], items: items.length, memoryItems: dryRun ? items : undefined, dryRun };
|
|
131
|
+
}
|
|
132
|
+
if (stage === 'conflict-candidates') {
|
|
133
|
+
if (!context.outputs.record) throw new Error('record output missing');
|
|
134
|
+
const candidates = items.filter(item => item.conflict && item.status === 'active').map(item => ({
|
|
135
|
+
project: item.project, topic: item.topic, owner: item.conflict!.owner,
|
|
136
|
+
sources: item.conflict!.sources, status: 'candidate' as const,
|
|
137
|
+
}));
|
|
138
|
+
if (!dryRun) await withCurrentDay(root, day, () => save(join(root, 'context-memory', 'conflict-candidates.json'), candidates));
|
|
139
|
+
return { outcome: 'ok', candidates, published: 0 };
|
|
140
|
+
}
|
|
141
|
+
throw new Error(`unknown stage: ${stage}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (import.meta.main) {
|
|
145
|
+
const stage = process.argv[2] as Stage;
|
|
146
|
+
const path = process.env.ELANOUS_GRAPH_CONTEXT;
|
|
147
|
+
if (!path || !['collect', 'condense', 'record', 'conflict-candidates'].includes(stage)) throw new Error('invalid nightly graph stage');
|
|
148
|
+
const context = JSON.parse(readFileSync(path, 'utf8')) as Context;
|
|
149
|
+
if (context.graphId !== 'nightly-condense' || context.nodeId !== stage) throw new Error('nightly graph context mismatch');
|
|
150
|
+
const statePath = resolve(path, '..', '..', '..', '..');
|
|
151
|
+
const result = await runNightlyStage(stage, context, statePath, process.env.ELANOUS_GRAPH_DRY_RUN === '1');
|
|
152
|
+
console.log(JSON.stringify(result));
|
|
153
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import { redactSecretText } from '../debug/log.js';
|
|
3
|
+
import { openSurfaceEventsDb, recordEvent } from '../domains/surface-events.js';
|
|
4
|
+
|
|
5
|
+
export const SESSION_EVENT_KINDS = ['task-claimed', 'task-done', 'asked', 'guidance-changed'] as const;
|
|
6
|
+
export type SessionEventKind = typeof SESSION_EVENT_KINDS[number];
|
|
7
|
+
|
|
8
|
+
export interface SessionEventInput {
|
|
9
|
+
kind: SessionEventKind;
|
|
10
|
+
seat: string;
|
|
11
|
+
text: string;
|
|
12
|
+
ref?: string;
|
|
13
|
+
at?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Publish a one-line external-agent session signal to the existing context journal. */
|
|
17
|
+
export function emitSessionEvent(input: SessionEventInput, deps: { db?: Database } = {}): string {
|
|
18
|
+
if (!SESSION_EVENT_KINDS.includes(input.kind)) throw new Error('unknown context session kind');
|
|
19
|
+
if (!['OP', 'TC', 'MK', 'UX'].includes(input.seat)) throw new Error('context session seat must be OP|TC|MK|UX');
|
|
20
|
+
if (/\r|\n/.test(input.text) || !input.text.trim()) throw new Error('context session text must be one nonempty line');
|
|
21
|
+
if (input.ref !== undefined && (/\r|\n/.test(input.ref) || !input.ref.trim())) throw new Error('context session ref must be one nonempty line');
|
|
22
|
+
const summary = redactSecretText(input.text).trim().slice(0, 120);
|
|
23
|
+
const ref = input.ref ?? null;
|
|
24
|
+
const db = deps.db ?? openSurfaceEventsDb();
|
|
25
|
+
try {
|
|
26
|
+
return recordEvent(db, {
|
|
27
|
+
surface: 'context:session', direction: 'outbound', domain: 'elanous', category: 'coordination',
|
|
28
|
+
kind: input.kind, text: input.text, summary,
|
|
29
|
+
refs: JSON.stringify({ seat: input.seat, recipients: [], all: false, kind: input.kind,
|
|
30
|
+
slot: null, deadline: null, url: null, ref,
|
|
31
|
+
...(ref ? { source: `elanous://context/ref/${encodeURIComponent(ref)}` } : {}) }),
|
|
32
|
+
...(input.at ? { ts: input.at } : {}),
|
|
33
|
+
});
|
|
34
|
+
} finally { if (!deps.db) db.close(); }
|
|
35
|
+
}
|
|
@@ -5,6 +5,7 @@ export interface DashboardStatusLinesInput {
|
|
|
5
5
|
readonly daemonAddress?: string | null;
|
|
6
6
|
readonly daemonConnected: boolean;
|
|
7
7
|
readonly accountName?: string | null;
|
|
8
|
+
readonly seatsNow?: string | null;
|
|
8
9
|
readonly sessionSurfaceLines: readonly string[];
|
|
9
10
|
readonly view: string;
|
|
10
11
|
readonly focus: string;
|
|
@@ -31,6 +32,7 @@ export function buildDashboardStatusLines(input: DashboardStatusLinesInput): str
|
|
|
31
32
|
`모델: ${known(input.provider)}/${known(input.model)} (${known(input.reasoning)})`,
|
|
32
33
|
`연결: 데몬 ${address} ${input.daemonConnected ? '붙음' : '안 붙음'}`,
|
|
33
34
|
`계정: ${input.accountName?.trim() === 'default' ? '기본' : known(input.accountName)}`,
|
|
35
|
+
...(input.seatsNow ? [input.seatsNow] : []),
|
|
34
36
|
'',
|
|
35
37
|
'자세히',
|
|
36
38
|
...input.sessionSurfaceLines.map(line => ` ${line}`),
|
package/src/dashboard/index.ts
CHANGED
|
@@ -884,6 +884,7 @@ import { getInputHistoryStore, inputHistoryDbPath, inputHistoryJsonPath } from '
|
|
|
884
884
|
import { inspectActiveProvider } from '../provider-summary.js';
|
|
885
885
|
import { activeCodexAccountView } from '../oauth/codex-account-store.js';
|
|
886
886
|
import { buildDashboardStatusLines } from './dashboard-status-lines.js';
|
|
887
|
+
import { readSlashContextNow, seatsNowLine } from '../context-bus/context-now-surfaces.js';
|
|
887
888
|
import { buildFirstScreenBand } from './first-screen-band.js';
|
|
888
889
|
import { runNexusShow, type NexusShowResult } from '../cli/nexus-show.js';
|
|
889
890
|
import {
|
|
@@ -6405,7 +6406,14 @@ Mode- and sync-specific instructions are injected per-turn when relevant — do
|
|
|
6405
6406
|
const active = inspectActiveProvider(cfg);
|
|
6406
6407
|
const reasoningProvider = (['openai-codex', 'openai', 'anthropic', 'gemini', 'grok', 'local', 'openrouter'] as const)
|
|
6407
6408
|
.find(provider => active.provider === provider || active.provider === `auto:${provider}`);
|
|
6409
|
+
let seatsNow: string | null = null;
|
|
6410
|
+
try {
|
|
6411
|
+
seatsNow = seatsNowLine(readSlashContextNow([]), Date.now());
|
|
6412
|
+
} catch (error) {
|
|
6413
|
+
debug.log('dashboard.status', 'seats-unreadable', { error: String(error) });
|
|
6414
|
+
}
|
|
6408
6415
|
return buildDashboardStatusLines({
|
|
6416
|
+
seatsNow,
|
|
6409
6417
|
provider: active.provider,
|
|
6410
6418
|
model: active.model,
|
|
6411
6419
|
reasoning: reasoningProvider
|
|
@@ -273,6 +273,8 @@
|
|
|
273
273
|
// of circular dependency on dashboard/index.ts.
|
|
274
274
|
|
|
275
275
|
import { tuiNowSlash } from '../../context-bus/context-now-surfaces.js';
|
|
276
|
+
import { createWishCard } from '../../intake-plane/wish-card.js';
|
|
277
|
+
import { CardStore } from '../../task-cards/card-store.js';
|
|
276
278
|
import type { ContextNowDeps } from '../../context-bus/context-now.js';
|
|
277
279
|
import { runRebindCommand } from '../../input-core/index.js';
|
|
278
280
|
// B4 (TUI half) — /design reuses the CLI's own resolution so the three
|
|
@@ -422,6 +424,8 @@ export interface DashboardSlashContext {
|
|
|
422
424
|
/** Existing TUI structured-question capability. Absent means /ask keeps
|
|
423
425
|
* the self-dev noninteractive deferred-clarification contract. */
|
|
424
426
|
surfaceUx?: Pick<SurfaceUx, 'question'>;
|
|
427
|
+
/** Optional wish-card store for isolated slash dispatch tests. */
|
|
428
|
+
wishCardStore?: CardStore;
|
|
425
429
|
attachmentRowMap: { clear(): void };
|
|
426
430
|
pushDebugLine(line: string): void;
|
|
427
431
|
pushChatLine(line: string): void;
|
|
@@ -1367,6 +1371,26 @@ export function buildDashboardSlashRegistry(nowDeps?: ContextNowDeps): SlashComm
|
|
|
1367
1371
|
for (const line of tuiNowSlash(args, nowDeps)) ctx.pushChatLine(line);
|
|
1368
1372
|
});
|
|
1369
1373
|
|
|
1374
|
+
registry.register('wish', (args, ctx) => {
|
|
1375
|
+
const text = args.join(' ').trim();
|
|
1376
|
+
if (!text) {
|
|
1377
|
+
ctx.pushChatLine(ctx.warning(' usage: /wish <소원 한 줄>'));
|
|
1378
|
+
ctx.setChatScrollOffset(-1);
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
let store: CardStore | undefined;
|
|
1382
|
+
try {
|
|
1383
|
+
store = ctx.wishCardStore ?? new CardStore();
|
|
1384
|
+
const card = createWishCard({ text, source: 'tui', ref: crypto.randomUUID() }, store);
|
|
1385
|
+
ctx.pushChatLine(ctx.success(` 소원 카드로 남겼습니다 — ${card.title}`));
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
ctx.pushChatLine(ctx.error(` 소원 카드 등록 실패 — ${err instanceof Error ? err.message : String(err)}`));
|
|
1388
|
+
} finally {
|
|
1389
|
+
if (!ctx.wishCardStore) store?.close();
|
|
1390
|
+
}
|
|
1391
|
+
ctx.setChatScrollOffset(-1);
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1370
1394
|
// ── B-1.a pilot ────────────────────────────────────────────────────
|
|
1371
1395
|
registry.register(['quit', 'q', 'exit'], (_args, ctx) => {
|
|
1372
1396
|
(ctx.exitTui ?? ctx.closeTui)();
|
|
@@ -35,21 +35,27 @@ export function isIrreversible(entry: Pick<DecisionEntry, 'category'>): boolean
|
|
|
35
35
|
|
|
36
36
|
const kst = (at: string) => new Intl.DateTimeFormat('ko-KR', { timeZone: 'Asia/Seoul', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date(at));
|
|
37
37
|
|
|
38
|
-
/** The card
|
|
38
|
+
/** The card opens with a short pitch; the full question and choice buttons remain below it. */
|
|
39
39
|
export function renderCardText(e: DecisionEntry, extra: { note?: string; confirm?: string } = {}): string {
|
|
40
|
+
const recommendedKey = 'option' in e.recommendation ? e.recommendation.option : undefined;
|
|
41
|
+
const recommended = e.options.find(o => o.key === recommendedKey);
|
|
42
|
+
const alternative = e.options.find(o => o.key === e.alternative);
|
|
40
43
|
const lines = [
|
|
41
44
|
`🗳 결정 요청 ${e.id}${isIrreversible(e) ? ' · ⚠️ 되돌릴 수 없음' : ''}`,
|
|
42
45
|
`«${e.title}» · 올린 자리: ${e.raisedBy.agent}${e.raisedBy.track ? `(${e.raisedBy.track})` : ''}`,
|
|
43
46
|
'',
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
`S: ${e.scqa.s.replace(/\s+/g, ' ')}`,
|
|
48
|
+
`C: ${e.scqa.c.replace(/\s+/g, ' ')}`,
|
|
49
|
+
`Q: ${(e.scqa.q ?? '(비움)').replace(/\s+/g, ' ')}`,
|
|
50
|
+
`A: ${(e.scqa.a ?? '(비움)').replace(/\s+/g, ' ')}`,
|
|
51
|
+
'skipped' in e.recommendation ? `권고 없음: ${e.recommendation.reason}` : `권고: ${recommended?.label ?? e.recommendation.option} — ${e.recommendation.why}`,
|
|
52
|
+
...(alternative ? [`대안: ${alternative.label} — ${alternative.consequence}`] : []),
|
|
53
|
+
e.crossCheck?.length ? `교차 확인: ${e.crossCheck.map(check => `${check.seat} ✓ ${check.note.replace(/\s+/g, ' ')}`).join(' · ')}` : `교차 확인 없음(${e.crossCheckSkipped ?? '미기재'})`,
|
|
54
|
+
...(e.dissent ? [`이견: ${e.dissent}`] : []),
|
|
55
|
+
...(e.dueAt ? [`기한: ${kst(e.dueAt)} KST`] : []),
|
|
46
56
|
...(e.pendingQuestion ? ['', `대기 질문 전문:\n${e.pendingQuestion}`] : []),
|
|
47
|
-
...(e.scqa.q ? [`질문: ${e.scqa.q}`] : []),
|
|
48
|
-
...(e.scqa.a ? [`제안: ${e.scqa.a}`] : []),
|
|
49
57
|
'',
|
|
50
58
|
...e.options.map((o) => `${o.key.toUpperCase()}) ${o.label} — ${o.consequence}`),
|
|
51
|
-
'skipped' in e.recommendation ? `권고 없음: ${e.recommendation.reason}` : `권고: ${e.recommendation.option.toUpperCase()} — ${e.recommendation.why}`,
|
|
52
|
-
...(e.dueAt ? [`기한: ${kst(e.dueAt)}`] : []),
|
|
53
59
|
...(extra.note ? ['', `📝 메모: ${extra.note}`] : []),
|
|
54
60
|
];
|
|
55
61
|
if (extra.confirm) {
|
|
@@ -22,11 +22,15 @@ export interface DecisionEntry {
|
|
|
22
22
|
pendingQuestion?: string;
|
|
23
23
|
/** DEC-TG — optional deadline (UTC). Cards remind the owner two hours before it. */
|
|
24
24
|
dueAt?: string;
|
|
25
|
+
crossCheck?: Array<{ seat: string; at: string; note: string }>;
|
|
26
|
+
crossCheckSkipped?: string;
|
|
27
|
+
alternative?: string;
|
|
28
|
+
dissent?: string;
|
|
25
29
|
decidedAt?: string; decidedBy?: DecisionActor; choice?: string; note?: string; versionAtDecision?: Versions;
|
|
26
30
|
withdrawnAt?: string; withdrawReason?: string;
|
|
27
31
|
history: Array<{ type: 'raised' | 'options-added' | 'decided' | 'withdrawn'; at?: string; by: string; version?: Versions; choice?: string; reason?: string }>;
|
|
28
32
|
}
|
|
29
|
-
export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs' | 'dueAt' | 'resume' | 'pendingQuestion'> & { raisedAt?: string };
|
|
33
|
+
export type RaiseInput = Pick<DecisionEntry, 'title' | 'category' | 'scqa' | 'options' | 'recommendation' | 'raisedBy' | 'refs' | 'dueAt' | 'resume' | 'pendingQuestion' | 'crossCheck' | 'crossCheckSkipped' | 'alternative' | 'dissent'> & { raisedAt?: string };
|
|
30
34
|
type Event = { type: 'raised'; entry: DecisionEntry } | { type: 'options-added'; id: string; at: string; options: DecisionOption[]; by: string } | { type: 'decided'; id: string; at?: string; by: DecisionActor; choice?: string; version?: Versions; note?: string } | { type: 'withdrawn'; id: string; at: string; reason: string; version: Versions };
|
|
31
35
|
export interface DecisionLedgerOptions extends VersionOptions { stateDir?: string; now?: () => Date; resolveVersion?: (at: string) => Versions; writeAnswer?: typeof writePendingQuestionAnswer }
|
|
32
36
|
|
|
@@ -89,13 +93,27 @@ function validate(input: RaiseInput, historical = false): RaiseInput {
|
|
|
89
93
|
? { skipped: true as const, reason: safe(single(input.recommendation.reason, 'skip reason')) }
|
|
90
94
|
: { option: input.recommendation.option, why: safe(single(input.recommendation.why, 'recommendation why')) };
|
|
91
95
|
if ('option' in recommendation && !options.some(o => o.key === recommendation.option)) throw new Error('recommended option not found');
|
|
96
|
+
const alternative = input.alternative === undefined ? undefined : single(input.alternative, 'alternative');
|
|
97
|
+
if (alternative && !options.some(o => o.key === alternative)) throw new Error('alternative option not found');
|
|
98
|
+
if (alternative && 'option' in recommendation && alternative === recommendation.option) throw new Error('alternative must differ from recommendation');
|
|
99
|
+
if (input.crossCheck?.length && input.crossCheckSkipped !== undefined) throw new Error('cross-check and skipped reason cannot coexist');
|
|
100
|
+
if (input.crossCheck !== undefined && !Array.isArray(input.crossCheck)) throw new Error('cross-check must be a list');
|
|
101
|
+
const crossCheck = input.crossCheck?.map(check => {
|
|
102
|
+
const seat = single(check.seat, 'cross-check seat');
|
|
103
|
+
if (!['OP', 'MK', 'TC', 'UX'].includes(seat)) throw new Error('invalid cross-check seat');
|
|
104
|
+
return { seat, at: utc(check.at), note: safe(single(check.note, 'cross-check note')) };
|
|
105
|
+
});
|
|
106
|
+
const crossCheckSkipped = input.crossCheckSkipped === undefined ? undefined : safe(single(input.crossCheckSkipped, 'cross-check skipped reason'));
|
|
107
|
+
const dissent = input.dissent === undefined ? undefined : safe(single(input.dissent, 'dissent'));
|
|
92
108
|
if (input.resume && !/^(?:auq:[0-9a-z]+:[0-9a-z]{5}|execution:[A-Za-z0-9_-]+:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?![\s\S])/.test(input.resume.questionId)) throw new Error('invalid resume questionId');
|
|
93
109
|
const resume = input.resume ? { questionId: input.resume.questionId,
|
|
94
110
|
...(input.resume.runId === undefined ? {} : { runId: single(input.resume.runId, 'resume runId') }) } : undefined;
|
|
95
111
|
if (input.pendingQuestion !== undefined) required(input.pendingQuestion, 'pending question');
|
|
96
112
|
return { ...input, title, scqa: { s, c, ...(q ? { q } : {}), ...(a ? { a } : {}) }, options, recommendation,
|
|
97
113
|
...(input.pendingQuestion === undefined ? {} : { pendingQuestion: safe(input.pendingQuestion) }),
|
|
98
|
-
...(resume ? { resume } : {}),
|
|
114
|
+
...(resume ? { resume } : {}), ...(crossCheck ? { crossCheck } : {}),
|
|
115
|
+
...(crossCheckSkipped !== undefined ? { crossCheckSkipped } : {}),
|
|
116
|
+
...(alternative !== undefined ? { alternative } : {}), ...(dissent !== undefined ? { dissent } : {}),
|
|
99
117
|
raisedBy: { agent: safe(single(input.raisedBy.agent, 'agent')), ...(input.raisedBy.track ? { track: input.raisedBy.track } : {}), ...(input.raisedBy.session ? { session: safe(single(input.raisedBy.session, 'session')) } : {}) },
|
|
100
118
|
...(input.refs ? { refs: input.refs.map(r => safe(single(r, 'ref'))) } : {}),
|
|
101
119
|
...(input.dueAt ? { dueAt: utc(input.dueAt) } : {}) };
|
|
@@ -206,21 +224,29 @@ export class DecisionLedger {
|
|
|
206
224
|
show(id: string): DecisionEntry { const entry = this.list({ status: 'all' }).find(e => e.id === id); if (!entry) throw new Error(`decision not found: ${id}`); return entry; }
|
|
207
225
|
raise(input: RaiseInput): DecisionEntry {
|
|
208
226
|
const clean = validate(input);
|
|
209
|
-
return this.locked(() =>
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
227
|
+
return this.locked(() => this.raiseLocked(clean));
|
|
228
|
+
}
|
|
229
|
+
/** Reuse a card with the same source reference, including across competing raisers. */
|
|
230
|
+
raiseOnce(input: RaiseInput, ref: string): DecisionEntry {
|
|
231
|
+
const clean = validate(input);
|
|
232
|
+
if (!clean.refs?.includes(ref)) throw new Error('decision source reference required');
|
|
233
|
+
return this.locked(() => this.list({ status: 'all' }).find(entry => entry.refs?.includes(ref)) ?? this.raiseLocked(clean));
|
|
234
|
+
}
|
|
235
|
+
private raiseLocked(clean: RaiseInput): DecisionEntry {
|
|
236
|
+
const at = utc(clean.raisedAt ?? this.now().toISOString());
|
|
237
|
+
const day = at.slice(0, 10).replaceAll('-', '');
|
|
238
|
+
const prefix = `D-${day}-`;
|
|
239
|
+
let next = 1;
|
|
240
|
+
for (const entry of this.list({ status: 'all' })) {
|
|
241
|
+
if (entry.id.startsWith(prefix)) next = Math.max(next, Number(entry.id.slice(prefix.length)) + 1);
|
|
242
|
+
}
|
|
243
|
+
const id = `${prefix}${String(next).padStart(2, '0')}`;
|
|
244
|
+
const version = this.version(at);
|
|
245
|
+
const entry: DecisionEntry = { ...clean, id, raisedAt: at, version, status: 'open', history: [{ type: 'raised', at, by: clean.raisedBy.agent, version }] };
|
|
246
|
+
this.append({ type: 'raised', entry });
|
|
247
|
+
debug.log('decisions', 'raised', { id, category: entry.category, by: entry.raisedBy.agent });
|
|
248
|
+
debug.log('decisions', 'raised-crosscheck', { id, seats: entry.crossCheck?.length ?? 0, skipped: entry.crossCheckSkipped ?? null });
|
|
249
|
+
return entry;
|
|
224
250
|
}
|
|
225
251
|
/** Record the decision and say whether its answer reached the waiting run (null = the decision resumes nothing). */
|
|
226
252
|
decideWithDelivery(id: string, choice: string, by: DecisionActor, note?: string, decidedAt?: string): { entry: DecisionEntry; delivery: AnswerDelivery } {
|