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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readPodMemoryMeasurement, type MemoryMeasurement } from '../../scripts/measure-pod-memory-by-goal.js';
|
|
2
|
+
|
|
3
|
+
export type PodMemoryAdvice = {
|
|
4
|
+
window: MemoryMeasurement['window'];
|
|
5
|
+
sources: MemoryMeasurement['sources'];
|
|
6
|
+
byGoalType: Array<{
|
|
7
|
+
goalType: MemoryMeasurement['byGoalType'][number]['goalType'];
|
|
8
|
+
recommended: 'standard' | 'high';
|
|
9
|
+
limit: '16Gi' | '32Gi';
|
|
10
|
+
reason: string;
|
|
11
|
+
evidence: MemoryMeasurement['byGoalType'][number];
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Advice only: the launch selector and its configuration remain independent. */
|
|
16
|
+
export function advisePodMemory(measurement: MemoryMeasurement): PodMemoryAdvice {
|
|
17
|
+
return {
|
|
18
|
+
window: measurement.window,
|
|
19
|
+
sources: measurement.sources,
|
|
20
|
+
byGoalType: measurement.byGoalType.map((evidence) => {
|
|
21
|
+
const high = evidence.oomKilled > 0 || (evidence.peakMiB.p95 !== null && evidence.peakMiB.p95 >= 16 * 1024);
|
|
22
|
+
const reason = evidence.oomKilled > 0
|
|
23
|
+
? `${evidence.oomKilled}/${evidence.runs} OOMKilled; measured peak p50=${evidence.peakMiB.p50 ?? 'n/a'} MiB, p95=${evidence.peakMiB.p95 ?? 'n/a'} MiB (${evidence.measured}/${evidence.runs} measured)`
|
|
24
|
+
: evidence.measured === 0
|
|
25
|
+
? `No measured peaks (${evidence.missingPeak}/${evidence.runs} missing); standard is provisional, not evidence of safety`
|
|
26
|
+
: `Measured peak p50=${evidence.peakMiB.p50} MiB, p95=${evidence.peakMiB.p95} MiB (${evidence.measured}/${evidence.runs} measured, ${evidence.missingPeak} missing); ${high ? 'at or above' : 'below'} 16Gi`;
|
|
27
|
+
return { goalType: evidence.goalType, recommended: high ? 'high' as const : 'standard' as const,
|
|
28
|
+
limit: high ? '32Gi' as const : '16Gi' as const, reason, evidence };
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readPodMemoryAdvice(path?: string): PodMemoryAdvice {
|
|
34
|
+
return advisePodMemory(readPodMemoryMeasurement(path));
|
|
35
|
+
}
|
package/src/cli/pr-cli.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
} from './pr-granularity.js';
|
|
54
54
|
import { branchLineageSlug, findSiblingPrs } from './pr-lineage.js';
|
|
55
55
|
import { parseReleaseNoteSection } from '../release-loop/release-note.js';
|
|
56
|
+
import { enqueueL8ShadowQueue, runL8ShadowQueue, shadowPrNumber, statusL8ShadowQueue, type L8ShadowQueueDeps } from '../self-implement/l8-merge-queue.js';
|
|
56
57
|
|
|
57
58
|
export { branchLineageSlug, findSiblingPrs };
|
|
58
59
|
|
|
@@ -1635,8 +1636,21 @@ export async function runPrLand(opts: PrLandOpts = {}, deps: PrLandDeps = {}): P
|
|
|
1635
1636
|
return 0;
|
|
1636
1637
|
}
|
|
1637
1638
|
|
|
1638
|
-
export function registerPrCommands(program: Command, deps: PrLandDeps = {}): void {
|
|
1639
|
+
export function registerPrCommands(program: Command, deps: PrLandDeps = {}, queueDeps: L8ShadowQueueDeps = {}): void {
|
|
1639
1640
|
const pr = program.command('pr').description('PR landing commands');
|
|
1641
|
+
const queue = pr.command('queue').description('Shadow merge queue (never lands PRs)');
|
|
1642
|
+
const queueAction = async (action: () => Promise<unknown>) => {
|
|
1643
|
+
try {
|
|
1644
|
+
(deps.out ?? liveOut).log(JSON.stringify(await action()));
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
(deps.out ?? liveOut).error(`✗ merge-queue: ${String(error)}`);
|
|
1647
|
+
(deps.setExitCode ?? ((code: number) => { process.exitCode = code; }))(1);
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
queue.command('status').description('Show pending PRs and shadow verdicts').action(() => queueAction(() => statusL8ShadowQueue(process.cwd(), queueDeps)));
|
|
1651
|
+
queue.command('enqueue <pr>').description('Pin a ready PR at the tail of the shadow queue').action((prNumber: string) => queueAction(() => enqueueL8ShadowQueue(process.cwd(), shadowPrNumber(prNumber), queueDeps)));
|
|
1652
|
+
queue.command('run').description('Evaluate the first PR without merging').requiredOption('--shadow', 'Only evaluate; never land').action(() => queueAction(() => runL8ShadowQueue(process.cwd(), queueDeps)));
|
|
1653
|
+
|
|
1640
1654
|
pr.command('land')
|
|
1641
1655
|
.description('현재 브랜치 PR을 생성 또는 갱신해 ready 상태로 만들고 squash merge')
|
|
1642
1656
|
.option('--base <branch>', 'PR base branch')
|
package/src/cli/release-cli.ts
CHANGED
|
@@ -23,12 +23,15 @@ import { debug } from '../debug/log.js';
|
|
|
23
23
|
import { effectiveInstanceRoot, releaseLedgerRoot } from '../instance/resolve.js';
|
|
24
24
|
import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
25
25
|
import { envLiteral } from '../platform/env-literal.js';
|
|
26
|
-
import { userConfigPath } from '../user-config.js';
|
|
26
|
+
import { getUserConfig, userConfigPath } from '../user-config.js';
|
|
27
27
|
import { addItem, claimItem, decodeClaimHistoryEntry, devVersion, listChecklist, ownerMatches, parseOwner, parityGap, removeItem, seedFromRoadmap, setItem, summarizeChecklist, type ChecklistStatus, type ChecklistDisposition, type ChecklistKind } from '../release-loop/checklist.js';
|
|
28
28
|
import * as features from '../release-loop/feature-store.js';
|
|
29
|
+
import { evidencePlan, landedButYellow, type MergedChecklistPr } from '../release-loop/landed-but-yellow.js';
|
|
29
30
|
import { formatSchedule, getSchedule, listSchedules, setSchedule } from '../release-loop/release-schedule.js';
|
|
30
31
|
import { writeStdoutJson } from './stdout-json.js';
|
|
31
32
|
import { CliUserError } from './cli-user-error.js';
|
|
33
|
+
import { hqCliWriteAllowed, type HqDeps } from '../hq/hq.js';
|
|
34
|
+
import { fileLeaseStore } from '../hq/lease.js';
|
|
32
35
|
import { runUnattendedRelease, type UnattendedReleaseDeps } from '../../scripts/release-loop/unattended-release.js';
|
|
33
36
|
import { cutReleaseBranch } from '../../scripts/release-loop/cut-branch.js';
|
|
34
37
|
import { releaseReadiness } from '../../scripts/release-loop/release-readiness.js';
|
|
@@ -499,10 +502,48 @@ async function checklistOutput(v: string, codenames: Record<string, string>, mod
|
|
|
499
502
|
}
|
|
500
503
|
}
|
|
501
504
|
|
|
502
|
-
export
|
|
505
|
+
export interface LandedButYellowDeps {
|
|
506
|
+
run?: Runner;
|
|
507
|
+
checklist?: typeof listChecklist;
|
|
508
|
+
now?: () => Date;
|
|
509
|
+
evidenceAdd?: typeof features.evidenceAdd;
|
|
510
|
+
schedules?: typeof features.readSchedules;
|
|
511
|
+
released?: typeof features.releasedVersion;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function printLandedRows(rows: ReturnType<typeof landedButYellow>, items: ReturnType<typeof listChecklist>['items']): void {
|
|
515
|
+
const updatedAtById = new Map(items.map((item) => [item.id, item.updatedAt]));
|
|
516
|
+
const date = (iso: string) => {
|
|
517
|
+
const parts = new Intl.DateTimeFormat('en-US', { timeZone: 'Asia/Seoul', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).formatToParts(new Date(iso));
|
|
518
|
+
const part = (name: string) => parts.find((entry) => entry.type === name)?.value;
|
|
519
|
+
return `${part('month')}-${part('day')} ${part('hour')}:${part('minute')}`;
|
|
520
|
+
};
|
|
521
|
+
for (const row of rows) {
|
|
522
|
+
const icon = row.status === 'yellow' ? '🟡' : '🔴';
|
|
523
|
+
console.log(`${row.id} ${row.owner ?? '-'} ${icon} ← ${row.prs.map((pr, i) => `#${pr.number} «${pr.title}» (${date(pr.mergedAt)} · 근거에 ${row.alreadyInEvidence[i] ? '있음' : '없음'}${pr.mergedAt > (updatedAtById.get(row.id) ?? '') ? ' · 칸보다 새것' : ''})`).join(' · ')}`);
|
|
524
|
+
}
|
|
525
|
+
console.log(`${rows.length}칸 · 그중 근거에 없는 PR 이 있는 칸 ${rows.filter((row) => row.alreadyInEvidence.includes(false)).length}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function compareReleaseVersions(a: string, b: string): number {
|
|
529
|
+
const parse = (version: string) => {
|
|
530
|
+
const [core, pre] = version.split('-');
|
|
531
|
+
const [major, minor, patch] = core!.split('.').map(Number);
|
|
532
|
+
const [stage, sequence] = pre?.split('.') ?? [];
|
|
533
|
+
return [major!, minor!, patch!, stage === undefined ? 3 : ({ alpha: 0, beta: 1, rc: 2 } as Record<string, number>)[stage]!, Number(sequence ?? 0)];
|
|
534
|
+
};
|
|
535
|
+
const left = parse(a), right = parse(b);
|
|
536
|
+
for (let i = 0; i < left.length; i++) if (left[i] !== right[i]) return left[i]! - right[i]!;
|
|
537
|
+
return 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export function registerReleaseCommands(program: Command, releaseRunDeps: UnattendedReleaseDeps = {}, landedDeps: LandedButYellowDeps = {}, hqDeps: HqDeps = {}): void {
|
|
503
541
|
const release = program.command('release').description('공개 배포 한 판 — prepare(로컬) → publish(--yes) → verify (docs/manual/MANUAL-versioning-and-release-2026-09-25.md)');
|
|
504
542
|
const schedule = release.command('schedule').description('판별 컷·착지 마감 조회·갱신');
|
|
505
543
|
const scheduleLedgerRoot = () => releaseRunDeps.ledgerRoot ?? releaseLedgerRoot();
|
|
544
|
+
const ledgerFenceDeps = (): HqDeps => getElanousConfigDirOverride() && !hqDeps.store && !getUserConfig().hq?.arbiter
|
|
545
|
+
? { ...hqDeps, store: fileLeaseStore(join(effectiveInstanceRoot(), 'hq', 'lease.json')), seenPath: join(effectiveInstanceRoot(), 'hq', 'seen-generation'), localPath: join(effectiveInstanceRoot(), 'hq', 'local.json') }
|
|
546
|
+
: hqDeps;
|
|
506
547
|
schedule.command('list').option('--json', '결과 JSON').action((o: { json?: boolean }) => {
|
|
507
548
|
const rows = listSchedules(scheduleLedgerRoot());
|
|
508
549
|
if (o.json) console.log(JSON.stringify(rows));
|
|
@@ -515,21 +556,25 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
515
556
|
if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
|
|
516
557
|
});
|
|
517
558
|
schedule.command('set').requiredOption('--version <v>', '갱신할 판')
|
|
518
|
-
.option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감').option('--json', '결과 JSON')
|
|
519
|
-
.action((o: { version: string; cutAt?: string; landBy?: string; json?: boolean }) => {
|
|
559
|
+
.option('--cut-at <iso>', '오프셋 포함 컷 시각').option('--land-by <iso>', '오프셋 포함 착지 마감').option('--json', '결과 JSON').option('--hq-override')
|
|
560
|
+
.action((o: { version: string; cutAt?: string; landBy?: string; json?: boolean; hqOverride?: boolean }) => {
|
|
520
561
|
if (o.cutAt === undefined && o.landBy === undefined) throw new CliUserError('갱신할 시각을 지정하라', '--cut-at <iso> 또는 --land-by <iso>');
|
|
562
|
+
if (!hqCliWriteAllowed('release schedule set', Boolean(o.hqOverride), ledgerFenceDeps())) return;
|
|
521
563
|
const row = setSchedule(o.version, { cutAt: o.cutAt, landBy: o.landBy }, process.env.ELANOUS_TRACK || 'cli', scheduleLedgerRoot());
|
|
522
564
|
if (o.json) console.log(JSON.stringify(row)); else console.log(formatSchedule(row));
|
|
523
565
|
});
|
|
524
566
|
const checklist = release.command('checklist').description('판별 확인표 조회·갱신')
|
|
525
567
|
.option('--version <v>', '판 또는 별칭(기본: package.json 의 개발판에서 -dev.N 제거)')
|
|
526
|
-
.option('--json', '결과 JSON')
|
|
568
|
+
.option('--json', '결과 JSON')
|
|
569
|
+
.option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
|
|
570
|
+
const mayWrite = (cmd: Command) => hqCliWriteAllowed(`release checklist ${cmd.name()}`, Boolean(cmd.optsWithGlobals().hqOverride), ledgerFenceDeps());
|
|
527
571
|
const context = (cmd: Command) => {
|
|
528
572
|
const opts = { ...(cmd.parent?.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.parent?.opts() as { version?: string; json?: boolean }), ...(cmd.opts() as { version?: string; json?: boolean }) };
|
|
529
573
|
const codenames = checklistCodenames();
|
|
530
574
|
return { version: checklistVersion(opts.version, codenames), codenames, json: opts.json };
|
|
531
575
|
};
|
|
532
|
-
const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON');
|
|
576
|
+
const withContext = (cmd: Command) => cmd.option('--version <v>', '판 또는 별칭').option('--json', '결과 JSON').option('--hq-override', '본부 임대 거부를 관측하며 수동 우회');
|
|
577
|
+
const actor = () => process.env.ELANOUS_TRACK || 'cli';
|
|
533
578
|
withContext(checklist.command('list').description('모든 칸')).option('--owner <owner>', '자리 또는 하위 자리로 거르기').action(async (opts: { owner?: string }, cmd: Command) => {
|
|
534
579
|
const { version, codenames, json } = context(cmd);
|
|
535
580
|
await checklistOutput(version, codenames, 'list', json, opts.owner);
|
|
@@ -538,10 +583,78 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
538
583
|
const { version, codenames, json } = context(cmd);
|
|
539
584
|
await checklistOutput(version, codenames, 'status', json, opts.owner);
|
|
540
585
|
});
|
|
586
|
+
withContext(checklist.command('landed-but-yellow').description('병합 PR 이 있지만 노랑·빨강인 칸(기본 읽기 전용)'))
|
|
587
|
+
.option('--owner <owner>', '자리 또는 하위 자리로 거르기')
|
|
588
|
+
.option('--open', '미발행 일정 판 중 칸이 있는 판 전부')
|
|
589
|
+
.option('--apply-evidence', '확실한 PR 짝의 근거를 붙인다(상태는 그대로)')
|
|
590
|
+
.option('--dry-run', '근거 추가 없이 계획만 출력')
|
|
591
|
+
.action(async (opts: { owner?: string; open?: boolean; applyEvidence?: boolean; dryRun?: boolean }, cmd: Command) => {
|
|
592
|
+
try {
|
|
593
|
+
const inherited = cmd.parent?.opts() as { version?: string; json?: boolean } | undefined;
|
|
594
|
+
const local = cmd.opts() as { version?: string; json?: boolean };
|
|
595
|
+
if (opts.open && (local.version !== undefined || inherited?.version !== undefined)) throw new CliUserError('--open 과 --version 을 함께 쓸 수 없다');
|
|
596
|
+
const json = local.json ?? inherited?.json;
|
|
597
|
+
if (opts.owner !== undefined) parseOwner(opts.owner);
|
|
598
|
+
const released = opts.open ? (landedDeps.released ?? features.releasedVersion)() : '';
|
|
599
|
+
const openSnapshots = opts.open
|
|
600
|
+
? (landedDeps.schedules ?? features.readSchedules)()
|
|
601
|
+
.filter(({ version }) => isReleaseVersion(version) && (!released || compareReleaseVersions(version, released) > 0))
|
|
602
|
+
.sort((a, b) => compareReleaseVersions(a.version, b.version))
|
|
603
|
+
.map(({ version }) => (landedDeps.checklist ?? listChecklist)(version)).filter((snapshot) => snapshot.items.length > 0)
|
|
604
|
+
: [];
|
|
605
|
+
if (opts.open && !openSnapshots.length) {
|
|
606
|
+
if (json) await writeStdoutJson('[]\n'); else console.log('열린 판 없음');
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (opts.applyEvidence && !opts.dryRun && !mayWrite(cmd)) return;
|
|
610
|
+
const since = new Date((landedDeps.now ?? (() => new Date()))().getTime() - 7 * 86_400_000).toISOString().slice(0, 10);
|
|
611
|
+
const raw = must((landedDeps.run ?? defaultRunner)('gh', ['pr', 'list', '--state', 'merged', '--search', `merged:>=${since}`, '--limit', '400', '--json', 'number,title,body,mergedAt'], process.cwd()), '병합 PR 조회(gh)');
|
|
612
|
+
const prs = JSON.parse(raw) as MergedChecklistPr[];
|
|
613
|
+
if (!Array.isArray(prs)) throw new Error('병합 PR 조회(gh): 배열이 아니다');
|
|
614
|
+
const snapshots = opts.open ? openSnapshots : [(landedDeps.checklist ?? listChecklist)(context(cmd).version)];
|
|
615
|
+
const results: Array<{ version: string; rows?: ReturnType<typeof landedButYellow>; plan?: ReturnType<typeof evidencePlan> }> = [];
|
|
616
|
+
let totalAdded = 0, totalSkipped = 0;
|
|
617
|
+
for (const snapshot of snapshots) {
|
|
618
|
+
const { version, items } = snapshot;
|
|
619
|
+
const rows = landedButYellow(items, prs, { owner: opts.owner });
|
|
620
|
+
const skippedMention = new Set(rows.flatMap((row) => row.prs.flatMap((pr) =>
|
|
621
|
+
pr.basis === 'mention' ? [JSON.stringify([row.id, pr.number])] : []))).size;
|
|
622
|
+
if (opts.open) totalSkipped += skippedMention;
|
|
623
|
+
if (opts.open && !json) console.log(`— ${version} —`);
|
|
624
|
+
if (opts.applyEvidence || opts.dryRun) {
|
|
625
|
+
const plan = evidencePlan(rows);
|
|
626
|
+
const log = json ? console.error : console.log;
|
|
627
|
+
for (const { id, ref } of plan) {
|
|
628
|
+
if (!opts.dryRun) (landedDeps.evidenceAdd ?? features.evidenceAdd)(id, version, ref, actor(), snapshot.released, snapshot.dev);
|
|
629
|
+
log(`${opts.dryRun ? '·' : '✅'} ${id} 근거 ${ref}`);
|
|
630
|
+
}
|
|
631
|
+
const added = opts.dryRun ? 0 : plan.length;
|
|
632
|
+
totalAdded += added;
|
|
633
|
+
log(`붙임 ${added} · 건너뜀(언급만) ${skippedMention}`);
|
|
634
|
+
if (!opts.open && !opts.dryRun) debug.log('release.checklist', 'evidence-synced', { added, skippedMention });
|
|
635
|
+
results.push({ version, plan });
|
|
636
|
+
} else {
|
|
637
|
+
if (!json) printLandedRows(rows, items);
|
|
638
|
+
results.push({ version, rows });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (opts.open) {
|
|
642
|
+
if (opts.applyEvidence && !opts.dryRun) debug.log('release.checklist', 'evidence-synced', { versions: snapshots.map(({ version }) => version), added: totalAdded, skippedMention: totalSkipped });
|
|
643
|
+
if (!json) console.log(`판 ${snapshots.length} · 붙임 합 ${totalAdded} · 건너뜀(언급만) 합 ${totalSkipped}`);
|
|
644
|
+
else await writeStdoutJson(`${JSON.stringify(results)}\n`);
|
|
645
|
+
} else if (json) await writeStdoutJson(`${JSON.stringify(opts.applyEvidence || opts.dryRun ? results[0]!.plan : results[0]!.rows)}\n`);
|
|
646
|
+
if (prs.length >= 400) console.error('⚠ 병합 PR 400개 상한: 목록이 잘렸을 수 있다');
|
|
647
|
+
} catch (error) {
|
|
648
|
+
console.error(`⛔ ${error instanceof Error ? error.message : String(error)}`);
|
|
649
|
+
process.exitCode = 1;
|
|
650
|
+
}
|
|
651
|
+
});
|
|
541
652
|
withContext(checklist.command('add <id> <title>').description('칸 추가')).option('--owner <owner>', '담당').option('--kind <kind>', 'screen = 다섯 화면 짝 칸 · 근거에 짝: PWA … · 데스크톱 … · 폴드 … · 아이폰 … · 아이패드 … 한 줄')
|
|
542
|
-
.
|
|
653
|
+
.option('--allow-duplicate-id', '다른 판에 같은 id 가 있어도 경고 후 추가')
|
|
654
|
+
.action((id: string, title: string, opts: { owner?: string; kind?: string; allowDuplicateId?: boolean }, cmd: Command) => {
|
|
543
655
|
const { version, json } = context(cmd);
|
|
544
|
-
|
|
656
|
+
if (!mayWrite(cmd)) return;
|
|
657
|
+
const data = addItem(version, { id, title, ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, { allowDuplicateId: opts.allowDuplicateId });
|
|
545
658
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 추가`);
|
|
546
659
|
});
|
|
547
660
|
withContext(checklist.command('set <id>').description('칸 상태·근거·담당·처분 갱신'))
|
|
@@ -552,6 +665,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
552
665
|
if (opts.status !== undefined && !['green', 'yellow', 'red', 'done'].includes(opts.status)) throw new CliUserError(`잘못된 상태: ${opts.status}`, 'green|yellow|red|done');
|
|
553
666
|
if (opts.disposition !== undefined && !['move', 'known-issue', 'block'].includes(opts.disposition)) throw new CliUserError(`잘못된 처분: ${opts.disposition}`, 'move|known-issue|block');
|
|
554
667
|
if (opts.status === undefined && opts.evidence === undefined && opts.owner === undefined && opts.disposition === undefined && opts.kind === undefined) throw new CliUserError('갱신할 칸을 지정하라', '--status · --evidence · --owner · --disposition · --kind 중 하나');
|
|
668
|
+
if (!mayWrite(cmd)) return;
|
|
555
669
|
const data = setItem(version, id, { ...(opts.status !== undefined ? { status: opts.status as ChecklistStatus } : {}), ...(opts.evidence !== undefined ? { evidence: opts.evidence } : {}), ...(opts.owner !== undefined ? { owner: opts.owner } : {}), ...(opts.disposition !== undefined ? { disposition: opts.disposition as ChecklistDisposition } : {}), ...(opts.kind !== undefined ? { kind: opts.kind as ChecklistKind } : {}) }, process.env.ELANOUS_TRACK || 'cli');
|
|
556
670
|
const item = data.items.find((entry) => entry.id === id);
|
|
557
671
|
const why = item?.kind === 'screen' && item.status === 'green' ? parityGap(item.evidence) : null;
|
|
@@ -565,40 +679,45 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
565
679
|
.requiredOption('--by <owner>', '잡을 자리 또는 하위 자리').option('--force', '기존 주인에서 강제로 바꾼다')
|
|
566
680
|
.action((id: string, opts: { by: string; force?: boolean }, cmd: Command) => {
|
|
567
681
|
const { version, json } = context(cmd);
|
|
682
|
+
if (!mayWrite(cmd)) return;
|
|
568
683
|
const data = claimItem(version, id, opts.by, { force: opts.force });
|
|
569
684
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 주인 ${opts.by}`);
|
|
570
685
|
});
|
|
571
686
|
withContext(checklist.command('rm <id>').description('칸 삭제')).action((id: string, _opts: unknown, cmd: Command) => {
|
|
572
687
|
const { version, json } = context(cmd);
|
|
688
|
+
if (!mayWrite(cmd)) return;
|
|
573
689
|
const data = removeItem(version, id, process.env.ELANOUS_TRACK || 'cli');
|
|
574
690
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${id} 삭제`);
|
|
575
691
|
});
|
|
576
692
|
withContext(checklist.command('seed').description('마크다운 로드맵에서 빈 칸 들이기')).requiredOption('--from <file>', '로드맵 파일')
|
|
577
693
|
.action((opts: { from: string }, cmd: Command) => {
|
|
578
694
|
const { version, json } = context(cmd);
|
|
695
|
+
if (!mayWrite(cmd)) return;
|
|
579
696
|
const data = seedFromRoadmap(version, readFileSync(opts.from, 'utf8'));
|
|
580
697
|
if (json) console.log(JSON.stringify(data)); else console.log(`✅ ${version} ${data.items.length}칸`);
|
|
581
698
|
});
|
|
582
|
-
const actor = () => process.env.ELANOUS_TRACK || 'cli';
|
|
583
699
|
withContext(checklist.command('move <id>').description('칸을 한 트랜잭션으로 다른 판에 옮긴다'))
|
|
584
|
-
.requiredOption('--from <v>', '현재 판').requiredOption('--to <v>', '새 판')
|
|
585
|
-
.action((id: string, opts: { from: string; to: string }, cmd: Command) => {
|
|
700
|
+
.requiredOption('--from <v>', '현재 판').requiredOption('--to <v>', '새 판').option('--reason <reason>', '이동 사유')
|
|
701
|
+
.action((id: string, opts: { from: string; to: string; reason?: string }, cmd: Command) => {
|
|
586
702
|
const { codenames, json } = context(cmd);
|
|
587
703
|
const from = checklistVersion(opts.from, codenames), to = checklistVersion(opts.to, codenames);
|
|
588
|
-
|
|
704
|
+
if (!mayWrite(cmd)) return;
|
|
705
|
+
features.move(id, from, to, actor(), undefined, undefined, opts.reason);
|
|
589
706
|
if (json) console.log(JSON.stringify(listChecklist(to))); else console.log(`✅ ${id} ${from} → ${to}`);
|
|
590
707
|
});
|
|
591
708
|
withContext(checklist.command('retitle <id> <title>').description('칸 제목 수정')).action((id: string, title: string, _opts: unknown, cmd: Command) => {
|
|
592
709
|
const { version, json } = context(cmd);
|
|
710
|
+
if (!mayWrite(cmd)) return;
|
|
593
711
|
const snapshot = listChecklist(version);
|
|
594
712
|
features.retitle(id, title, actor(), snapshot.released, snapshot.dev);
|
|
595
713
|
if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${id} 제목 수정`);
|
|
596
714
|
});
|
|
597
715
|
const evidence = checklist.command('evidence').description('칸의 PR·커밋 근거');
|
|
598
716
|
withContext(evidence.command('add <id> <ref>').description('근거 참조 추가')).action((id: string, ref: string, _opts: unknown, cmd: Command) => {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
717
|
+
const { version, json } = context(cmd);
|
|
718
|
+
if (!mayWrite(cmd)) return;
|
|
719
|
+
const snapshot = listChecklist(version);
|
|
720
|
+
features.evidenceAdd(id, version, ref, actor(), snapshot.released, snapshot.dev);
|
|
602
721
|
if (json) console.log(JSON.stringify(features.history(id).map(decodeClaimHistoryEntry))); else console.log(`✅ ${id} 근거 ${ref}`);
|
|
603
722
|
});
|
|
604
723
|
withContext(checklist.command('history <id>').description('칸의 판 이동·변경 이력')).action((id: string, _opts: unknown, cmd: Command) => {
|
|
@@ -614,6 +733,7 @@ export function registerReleaseCommands(program: Command, releaseRunDeps: Unatte
|
|
|
614
733
|
});
|
|
615
734
|
withContext(checklist.command('export').description('SQLite 에서 checklist.json 스냅샷 생성')).action((_opts: unknown, cmd: Command) => {
|
|
616
735
|
const { version, json } = context(cmd);
|
|
736
|
+
if (!mayWrite(cmd)) return;
|
|
617
737
|
const snapshot = listChecklist(version);
|
|
618
738
|
features.exportJson(version, snapshot.released, snapshot.dev);
|
|
619
739
|
if (json) console.log(JSON.stringify(listChecklist(version))); else console.log(`✅ ${version} checklist.json 내보냄`);
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync } from 'node:fs';
|
|
3
|
+
import { dlopen, FFIType } from 'bun:ffi';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { listMemories, memoryRoot, saveMemory, type MemoryEntry } from '../memory.js';
|
|
6
|
+
import { surfaceEventsDbPath } from '../domains/surface-events.js';
|
|
7
|
+
import { DecisionLedger } from '../decisions/decision-ledger.js';
|
|
8
|
+
import { listCoordEvents } from './coord-events.js';
|
|
9
|
+
import { condenseContextWindowWithReport, type CondenseSkipped, type MemoryDeps, type MemoryItem, type MemorySource, type MemorySummary } from './long-term-memory.js';
|
|
10
|
+
|
|
11
|
+
const summarize = async (source: MemorySource): Promise<MemorySummary> =>
|
|
12
|
+
(await import('./nightly-condense.js')).summarize(source);
|
|
13
|
+
|
|
14
|
+
/** Reuse the context-bus reader against a strictly read-only SQLite handle. */
|
|
15
|
+
function readContextEvents(since: string) {
|
|
16
|
+
const path = surfaceEventsDbPath();
|
|
17
|
+
if (!existsSync(path)) return [];
|
|
18
|
+
const db = new Database(path, { readonly: true, strict: true });
|
|
19
|
+
try { return listCoordEvents({ since }, { db }); }
|
|
20
|
+
finally { db.close(); }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CondenseReport {
|
|
24
|
+
since: string;
|
|
25
|
+
until: string;
|
|
26
|
+
cards: MemoryItem[];
|
|
27
|
+
conflicts: number;
|
|
28
|
+
skipped: CondenseSkipped;
|
|
29
|
+
folded: number;
|
|
30
|
+
markdown: string;
|
|
31
|
+
applied?: { written: number; retired: number; decisions: string[] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function mdCell(value: string): string {
|
|
35
|
+
return value.replaceAll('|', '\\|').replaceAll('\n', ' ');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function renderCondenseMarkdown(report: Pick<CondenseReport, 'since' | 'until' | 'cards' | 'conflicts' | 'skipped'>): string {
|
|
39
|
+
const lines = [
|
|
40
|
+
'# Long-term context memory candidates',
|
|
41
|
+
'',
|
|
42
|
+
`Window: ${report.since} → ${report.until} · Cards: ${report.cards.length} · Conflicts: ${report.conflicts}`,
|
|
43
|
+
`Skipped: ${JSON.stringify(report.skipped)}`,
|
|
44
|
+
'',
|
|
45
|
+
];
|
|
46
|
+
for (const card of report.cards) {
|
|
47
|
+
lines.push(`## ${mdCell(card.project)} · ${mdCell(card.seat)} · ${mdCell(card.topic)}`,
|
|
48
|
+
`- Updated: ${card.updatedAt}`,
|
|
49
|
+
`- Candidate: ${mdCell(card.summary)}`,
|
|
50
|
+
`- Source: ${card.source}`);
|
|
51
|
+
if (card.conflict) lines.push(`- 충돌 (${card.conflict.owner}): ${card.conflict.sources.join(' ↔ ')}`);
|
|
52
|
+
lines.push('');
|
|
53
|
+
}
|
|
54
|
+
return lines.join('\n').trimEnd() + '\n';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type CondenseStore = {
|
|
58
|
+
list(): MemoryEntry[];
|
|
59
|
+
save(input: Parameters<typeof saveMemory>[0]): MemoryEntry;
|
|
60
|
+
raise(input: Parameters<DecisionLedger['raiseOnce']>[0], ref: string): string;
|
|
61
|
+
withLock<T>(action: () => T): T;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Persistent inode in the existing memory directory: all condense writers share one cross-process lock. */
|
|
65
|
+
function withMemoryLock<T>(dir: string, action: () => T): T {
|
|
66
|
+
mkdirSync(dir, { recursive: true });
|
|
67
|
+
const libc = process.platform === 'darwin' ? '/usr/lib/libSystem.B.dylib' : process.platform === 'linux' ? 'libc.so.6' : null;
|
|
68
|
+
if (!libc) throw new Error('context memory locking unavailable on this platform');
|
|
69
|
+
const lib = dlopen(libc, { flock: { args: [FFIType.i32, FFIType.i32], returns: FFIType.i32 } });
|
|
70
|
+
const flock = lib.symbols.flock as (fd: number, op: number) => number;
|
|
71
|
+
const path = join(dir, '.context-condense.lock');
|
|
72
|
+
let fd: number | undefined;
|
|
73
|
+
let acquired = false;
|
|
74
|
+
try {
|
|
75
|
+
fd = openSync(path, constants.O_CREAT | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
|
|
76
|
+
const stat = fstatSync(fd);
|
|
77
|
+
const current = lstatSync(path);
|
|
78
|
+
if (!stat.isFile() || stat.ino !== current.ino || stat.dev !== current.dev || (stat.mode & 0o077) !== 0) {
|
|
79
|
+
throw new Error('context memory lock must be a private regular file');
|
|
80
|
+
}
|
|
81
|
+
const deadline = Date.now() + 10_000;
|
|
82
|
+
const pause = new Int32Array(new SharedArrayBuffer(4));
|
|
83
|
+
while (flock(fd, 2 | 4) !== 0) {
|
|
84
|
+
if (Date.now() >= deadline) throw new Error('context memory lock timed out');
|
|
85
|
+
Atomics.wait(pause, 0, 0, 10);
|
|
86
|
+
}
|
|
87
|
+
acquired = true;
|
|
88
|
+
const locked = lstatSync(path);
|
|
89
|
+
if (stat.ino !== locked.ino || stat.dev !== locked.dev) throw new Error('context memory lock changed');
|
|
90
|
+
return action();
|
|
91
|
+
} finally {
|
|
92
|
+
if (fd !== undefined) {
|
|
93
|
+
try { if (acquired) flock(fd, 8); } finally { closeSync(fd); }
|
|
94
|
+
}
|
|
95
|
+
lib.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function defaultStore(root?: string): CondenseStore {
|
|
100
|
+
const memoryDir = root ? join(root, 'elanous', 'memory') : memoryRoot();
|
|
101
|
+
const ledger = new DecisionLedger(root ? { stateDir: root } : {});
|
|
102
|
+
return {
|
|
103
|
+
list: () => listMemories({}, memoryDir),
|
|
104
|
+
save: input => saveMemory(input, memoryDir),
|
|
105
|
+
raise: (input, ref) => ledger.raiseOnce(input, ref).id,
|
|
106
|
+
withLock: action => withMemoryLock(memoryDir, action),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cardKey(card: Pick<MemoryItem, 'project' | 'seat' | 'topic'>): string {
|
|
111
|
+
return JSON.stringify([card.project, card.seat, card.topic]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function newestActive(entries: readonly MemoryEntry[]): MemoryEntry | undefined {
|
|
115
|
+
return entries.reduce<MemoryEntry | undefined>((best, entry) => {
|
|
116
|
+
const current = entry.contextCard!;
|
|
117
|
+
const previous = best?.contextCard;
|
|
118
|
+
return !previous || current.updatedAt > previous.updatedAt
|
|
119
|
+
|| (current.updatedAt === previous.updatedAt && current.source > previous.source)
|
|
120
|
+
|| (current.updatedAt === previous.updatedAt && current.source === previous.source && entry.id > best!.id)
|
|
121
|
+
? entry : best;
|
|
122
|
+
}, undefined);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Apply candidate cards through the existing curated memory and decision ledger paths. */
|
|
126
|
+
export function applyCondensedCards(cards: readonly MemoryItem[], store: CondenseStore): NonNullable<CondenseReport['applied']> {
|
|
127
|
+
const decisions: string[] = [];
|
|
128
|
+
let written = 0;
|
|
129
|
+
let retired = 0;
|
|
130
|
+
for (const card of cards) {
|
|
131
|
+
if (card.conflict) {
|
|
132
|
+
const ref = `context-condense:${JSON.stringify([card.project, card.seat, card.topic, card.source, card.updatedAt])}`;
|
|
133
|
+
decisions.push(store.raise({
|
|
134
|
+
title: `Context conflict: ${card.project} / ${card.seat} / ${card.topic}`.slice(0, 120),
|
|
135
|
+
category: 'scope',
|
|
136
|
+
scqa: { s: `Candidate (${card.updatedAt}): ${card.summary}`.slice(0, 220), c: `Conflicting sources: ${card.conflict.sources.join(' ↔ ')}`.slice(0, 220) },
|
|
137
|
+
options: [
|
|
138
|
+
{ key: 'a', label: 'Keep the existing claim', consequence: 'Candidate is not stored' },
|
|
139
|
+
{ key: 'b', label: 'Reconcile and apply the new claim', consequence: 'Resolve source disagreement before memory update' },
|
|
140
|
+
],
|
|
141
|
+
recommendation: { skipped: true, reason: 'Conflicting claims require OP judgment' },
|
|
142
|
+
raisedBy: { agent: 'context-condense', track: 'OP' },
|
|
143
|
+
refs: [ref, ...card.conflict.sources],
|
|
144
|
+
}, ref));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (card.status !== 'active') continue;
|
|
148
|
+
const applyTopic = () => {
|
|
149
|
+
const prior = store.list().filter(entry => entry.contextCard?.status === 'active' && cardKey(entry.contextCard) === cardKey(card));
|
|
150
|
+
const winner = newestActive(prior);
|
|
151
|
+
const alreadyPresent = prior.some(entry => entry.contextCard!.updatedAt === card.updatedAt && entry.contextCard!.source === card.source);
|
|
152
|
+
if (!alreadyPresent && (!winner || winner.contextCard!.updatedAt < card.updatedAt
|
|
153
|
+
|| (winner.contextCard!.updatedAt === card.updatedAt && winner.contextCard!.source < card.source))) {
|
|
154
|
+
store.save({ type: 'project', name: `${card.project} · ${card.seat} · ${card.topic}`,
|
|
155
|
+
description: card.summary, body: `${card.summary}\n\nSource: ${card.source}\nUpdated: ${card.updatedAt}\n`,
|
|
156
|
+
contextCard: { project: card.project, seat: card.seat, topic: card.topic, source: card.source, updatedAt: card.updatedAt, status: 'active' } });
|
|
157
|
+
written++;
|
|
158
|
+
}
|
|
159
|
+
const active = store.list().filter(entry => entry.contextCard?.status === 'active' && cardKey(entry.contextCard) === cardKey(card));
|
|
160
|
+
const keep = newestActive(active);
|
|
161
|
+
for (const old of active) {
|
|
162
|
+
if (old.id === keep?.id) continue;
|
|
163
|
+
store.save({ type: old.type, name: old.name, description: old.description, body: old.body, id: old.id,
|
|
164
|
+
pinned: false, priority: old.priority, contextCard: { ...old.contextCard!, status: 'retired' } });
|
|
165
|
+
retired++;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
store.withLock(applyTopic);
|
|
169
|
+
}
|
|
170
|
+
return { written, retired, decisions };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Produce a rolling-window candidate snapshot; no store is opened for writing in dry-run. */
|
|
174
|
+
export async function runContextCondense(options: {
|
|
175
|
+
hours: number; apply?: boolean; now?: Date; root?: string;
|
|
176
|
+
}, deps: MemoryDeps = { summarize }, store?: CondenseStore): Promise<CondenseReport> {
|
|
177
|
+
if (!Number.isSafeInteger(options.hours) || options.hours <= 0 || options.hours > 24 * 30) {
|
|
178
|
+
throw new Error('--since must be a positive number of hours (at most 720h)');
|
|
179
|
+
}
|
|
180
|
+
const now = options.now ?? new Date();
|
|
181
|
+
const until = now.toISOString();
|
|
182
|
+
const since = new Date(now.getTime() - options.hours * 3_600_000).toISOString();
|
|
183
|
+
const { cards, skipped, folded } = await condenseContextWindowWithReport(since, until, [], {
|
|
184
|
+
summarize: deps.summarize,
|
|
185
|
+
events: deps.events ?? readContextEvents,
|
|
186
|
+
decisions: deps.decisions ?? (() => new DecisionLedger(options.root ? { stateDir: options.root } : {}).list({ status: 'all' })
|
|
187
|
+
.filter(decision => decision.raisedBy.agent !== 'context-condense')),
|
|
188
|
+
}, now);
|
|
189
|
+
const conflicts = cards.filter(card => card.conflict !== undefined).length;
|
|
190
|
+
const report: CondenseReport = { since, until, cards, conflicts, skipped, folded,
|
|
191
|
+
markdown: renderCondenseMarkdown({ since, until, cards, conflicts, skipped }) };
|
|
192
|
+
if (options.apply === true) report.applied = applyCondensedCards(cards, store ?? defaultStore(options.root));
|
|
193
|
+
return report;
|
|
194
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { surfaceEventsDbPath } from '../domains/surface-events.js';
|
|
4
|
+
|
|
5
|
+
export const CONTEXT_DAY_SOURCES = ['OP', 'TC', 'MK', 'UX', 'claude-code', 'codex-agent-mission', 'harness-child'] as const;
|
|
6
|
+
export type ContextDaySource = typeof CONTEXT_DAY_SOURCES[number];
|
|
7
|
+
export type ContextDayKind = 'task-claimed' | 'done' | 'asked' | 'guidance-changed';
|
|
8
|
+
export interface ContextDayRow {
|
|
9
|
+
source: ContextDaySource;
|
|
10
|
+
counts: Record<ContextDayKind, number>;
|
|
11
|
+
lastAt: string | null;
|
|
12
|
+
status: '들어옴' | '안 들어옴';
|
|
13
|
+
}
|
|
14
|
+
export interface ContextDayReport { since: string; until: string; rows: ContextDayRow[] }
|
|
15
|
+
|
|
16
|
+
/** Reject malformed windows instead of silently changing the time range being judged. */
|
|
17
|
+
export function contextDayHours(since: string): number {
|
|
18
|
+
if (!/^[1-9]\d*h$/.test(since)) throw new Error('--since requires positive hours, e.g. 24h');
|
|
19
|
+
const hours = Number(since.slice(0, -1));
|
|
20
|
+
if (!Number.isSafeInteger(hours) || hours * 3_600_000 > 8.64e15) throw new Error('--since is out of range');
|
|
21
|
+
return hours;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function kindFor(surface: string, kind: string): ContextDayKind | null {
|
|
25
|
+
if (surface === 'context:external') {
|
|
26
|
+
if (kind === 'started' || kind === 'claimed') return 'task-claimed';
|
|
27
|
+
if (kind === 'finished' || kind === 'done') return 'done';
|
|
28
|
+
if (kind === 'guide-changed') return 'guidance-changed';
|
|
29
|
+
}
|
|
30
|
+
if (kind === 'task-claimed' || kind === 'claimed') return 'task-claimed';
|
|
31
|
+
if (kind === 'task-done' || kind === 'done') return 'done';
|
|
32
|
+
if (kind === 'asked' || (surface === 'coord:channel' && kind === '요청')) return 'asked';
|
|
33
|
+
if (kind === 'guidance-changed' || kind === 'guide-changed') return 'guidance-changed';
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Read only the context journal, not recallEvents (which mutates recall counts) or the writable DB opener. */
|
|
38
|
+
export function contextDay(since = '24h', deps: { now?: () => Date; dbPath?: string } = {}): ContextDayReport {
|
|
39
|
+
const hours = contextDayHours(since);
|
|
40
|
+
const now = (deps.now ?? (() => new Date()))();
|
|
41
|
+
const until = now.toISOString();
|
|
42
|
+
const from = new Date(now.getTime() - hours * 3_600_000).toISOString();
|
|
43
|
+
const rows: ContextDayRow[] = CONTEXT_DAY_SOURCES.map(source => ({
|
|
44
|
+
source, counts: { 'task-claimed': 0, done: 0, asked: 0, 'guidance-changed': 0 }, lastAt: null, status: '안 들어옴',
|
|
45
|
+
}));
|
|
46
|
+
const bySource = new Map(rows.map(row => [row.source, row]));
|
|
47
|
+
const path = deps.dbPath ?? surfaceEventsDbPath();
|
|
48
|
+
if (!existsSync(path)) return { since: from, until, rows };
|
|
49
|
+
const db = new Database(path, { readonly: true });
|
|
50
|
+
try {
|
|
51
|
+
const events = db.prepare(`SELECT surface, kind, ts, refs FROM events
|
|
52
|
+
WHERE surface IN ('coord:channel', 'context:session', 'context:external')
|
|
53
|
+
AND direction='outbound' AND ts>=? AND ts<=? ORDER BY ts ASC`).all(from, until) as Array<{
|
|
54
|
+
surface: string; kind: string; ts: string; refs: string | null;
|
|
55
|
+
}>;
|
|
56
|
+
for (const event of events) {
|
|
57
|
+
let refs: { seat?: unknown; origin?: unknown };
|
|
58
|
+
try { refs = JSON.parse(event.refs ?? '{}') as typeof refs; } catch { continue; }
|
|
59
|
+
const source = event.surface === 'context:external' ? refs.origin : refs.seat;
|
|
60
|
+
if (typeof source !== 'string') continue;
|
|
61
|
+
const row = bySource.get(source as ContextDaySource);
|
|
62
|
+
if (!row) continue;
|
|
63
|
+
// Do not attribute an external hook to a seat (or a seat event to an external origin).
|
|
64
|
+
if ((event.surface === 'context:external') !== (rows.indexOf(row) >= 4)) continue;
|
|
65
|
+
row.lastAt = event.ts;
|
|
66
|
+
row.status = '들어옴';
|
|
67
|
+
const kind = kindFor(event.surface, event.kind);
|
|
68
|
+
if (kind) row.counts[kind]++;
|
|
69
|
+
}
|
|
70
|
+
} finally { db.close(); }
|
|
71
|
+
return { since: from, until, rows };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function renderContextDay(report: ContextDayReport): string {
|
|
75
|
+
return [
|
|
76
|
+
`맥락 이벤트 ${report.since} ~ ${report.until} (UTC)`,
|
|
77
|
+
'| 출처 | task-claimed | done | asked | guidance-changed | 마지막 시각 (UTC) | 상태 |',
|
|
78
|
+
'| --- | ---: | ---: | ---: | ---: | --- | --- |',
|
|
79
|
+
...report.rows.map(row => `| ${row.source} | ${row.counts['task-claimed']} | ${row.counts.done} | ${row.counts.asked} | ${row.counts['guidance-changed']} | ${row.lastAt ?? '—'} | ${row.status} |`),
|
|
80
|
+
'안 들어옴 = 이 시간 창에서 해당 출처 이벤트 0건 (Claude Code 훅 미설치 가능성; 설치 여부는 미확인). 카운트 열은 네 종류만 셈.',
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
@@ -4,6 +4,23 @@ export function readSlashContextNow(args: string[], deps?: ContextNowDeps): Cont
|
|
|
4
4
|
return contextNow({ topic: args.join(' ').trim() }, deps);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export function seatsNowLine(answer: ContextNowAnswer, now: number): string | null {
|
|
8
|
+
const seats = [ ['OP', 'COO'], ['MK', 'CMO'], ['TC', 'CTO'], ['UX', 'CXO'] ] as const;
|
|
9
|
+
const facts = answer.facts.filter(fact => fact.kind === 'seat');
|
|
10
|
+
const lines = seats.flatMap(([seat, label]) => {
|
|
11
|
+
const latest = facts.filter(fact => fact.seat === seat)
|
|
12
|
+
.sort((a, b) => b.at.localeCompare(a.at))[0];
|
|
13
|
+
if (!latest) return [];
|
|
14
|
+
const minutes = Math.max(0, Math.floor((now - Date.parse(latest.at)) / 60_000));
|
|
15
|
+
const age = minutes >= 24 * 60 ? '하루 넘음'
|
|
16
|
+
: minutes >= 60 ? `${Math.floor(minutes / 60)}시간 전`
|
|
17
|
+
: minutes > 0 ? `${minutes}분 전` : '방금';
|
|
18
|
+
const title = latest.title?.split(/\r?\n/, 1)[0]?.trim();
|
|
19
|
+
return [`${label} ${title ? title.slice(0, 20) : latest.status} · ${age}`];
|
|
20
|
+
});
|
|
21
|
+
return lines.length ? `지금 자리들: ${lines.join(' | ')}` : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
7
24
|
function factText(fact: ContextFact): string {
|
|
8
25
|
switch (fact.kind) {
|
|
9
26
|
case 'version': return fact.version;
|
|
@@ -26,10 +43,23 @@ export function renderTelegramNow(answer: ContextNowAnswer): string {
|
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
export function renderTuiNow(answer: ContextNowAnswer): string[] {
|
|
46
|
+
const shortText = (text: string) => {
|
|
47
|
+
const characters = Array.from(text);
|
|
48
|
+
return characters.length > 100 ? `${characters.slice(0, 100).join('')}…` : text;
|
|
49
|
+
};
|
|
50
|
+
const shortSource = (source: string) => {
|
|
51
|
+
if (/^https?:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+#issuecomment-/.test(source)) return '채널';
|
|
52
|
+
if (source.startsWith('elanous://release/')) return '원장';
|
|
53
|
+
return Array.from(source).slice(0, 40).join('');
|
|
54
|
+
};
|
|
55
|
+
const factLabels = { version: '판', cell: '칸', decision: '결정', seat: '자리' } as const;
|
|
56
|
+
const eventLabels: Record<string, string> = { report: '보고', dispatch: '발사', decision: '결정' };
|
|
57
|
+
const events = answer.events.slice().sort((a, b) => Date.parse(b.at) - Date.parse(a.at));
|
|
29
58
|
const rows = [
|
|
30
|
-
...answer.facts.map(f => [f.kind, factText(f), f.source]),
|
|
31
|
-
...
|
|
59
|
+
...answer.facts.map(f => [factLabels[f.kind], shortText(factText(f)), shortSource(f.source)]),
|
|
60
|
+
...events.slice(0, 8).map(e => [eventLabels[e.kind] ?? '소식', shortText(e.summary), shortSource(e.source)]),
|
|
32
61
|
...answer.guide.map(guide => ['안내', guide, '']),
|
|
62
|
+
...(events.length > 8 ? [['안내', `… 사건 ${events.length - 8}개 더(/now <주제> 로 좁히기)`, '']] : []),
|
|
33
63
|
];
|
|
34
64
|
return [
|
|
35
65
|
`지금${answer.topic ? ` · ${answer.topic}` : ''} (${answer.at})`,
|