elanous 0.2.14 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-b46d45b705e8c9ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9103-7a3ae7f60af3b99f.js → 9103-60fbef33e66f0a46.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-79c3ed84baaedd03.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-3cb52e5ea8fc2fa5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-a424ef80885d1743.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-c1e20d7d338713f1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-482c5dbde699947c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-1e962b31d1f11081.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-8c2d9526164f5d09.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-ffff6d819fbc6e0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-e6b84ab07d3567d5.js → page-a0e2a6d1bfb52fc5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-c53598cfce6156f2.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-13a63953f53c6a23.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-5d3c8429d04f3831.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-3193bdc0bbab047a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-76270efaee638f17.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-6107ca8ddd5e3bdd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-dce686e582c23937.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d3d53b146416e422.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-160089123a40dd91.js → page-1a61c7ca1ceb3ae7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-51f811fa119308b6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-dacff46049e1557e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-4167348049166852.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-de52e8fdf9095f7a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-36a7d994f4ca0299.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-ca3952cd78a83ca2.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/{page-e7e10cd3386222e5.js → page-5b042736aa113756.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-acb749f1e432bcf3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-cd78862707999911.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-54b9ef8afe6278fd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-1ebfe049c640e171.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-57924393e3e4e539.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-9e0ce6c8129fa641.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-31fa1ce4c4aff9ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-0f5cd57ca928f42b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-abe8e3b74bea1ffc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-4364ef9f4e7c7f0f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-d28573d6792b4275.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-58043db78ba519ae.js → page-59f03fda76c03d22.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/{page-357750dfb91bf960.js → page-d751de5f8062ff5e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-fe1759f94003b32b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-d64c984e855c8c68.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/page-86914b0dee8ab7f8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-9afdee66b820c76e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-d4aef67abf264874.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-f8da5dc6f3752276.js → page-1ea64acadd704ace.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-3544f154c87b43ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-30c2073bfc21c004.js +1 -0
- package/apps/pwa/out/_next/static/css/001d5fbaee388a25.css +1 -0
- package/apps/pwa/out/_next/static/css/{04dabcbcc4bfa6db.css → 085673d826b98b62.css} +1 -1
- package/apps/pwa/out/_next/static/rv9cierV0jSOvDBm1g0XB/_buildManifest.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +15 -15
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +15 -15
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +15 -15
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +15 -15
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +15 -15
- package/apps/pwa/out/ceo/index.html +1 -0
- package/apps/pwa/out/ceo/index.txt +33 -0
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +15 -15
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +15 -15
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +15 -15
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +15 -15
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +15 -15
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +15 -15
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +15 -15
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +15 -15
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +15 -15
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +15 -15
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +15 -15
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +15 -15
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +15 -15
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +15 -15
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +15 -15
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +15 -15
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +15 -15
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +15 -15
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +15 -15
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +15 -15
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +16 -16
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +15 -15
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +15 -15
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +15 -15
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +15 -15
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +15 -15
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +15 -15
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +15 -15
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +15 -15
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +15 -15
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +15 -15
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +15 -15
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +15 -15
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +15 -15
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +15 -15
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +15 -15
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +15 -15
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +15 -15
- package/graphs/doc-gen/doc-gen.yaml +24 -0
- package/graphs/doc-gen/recipes.yaml +3 -0
- package/graphs/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/orchestrator/orchestrator.yaml +7 -0
- package/graphs/orchestrator/recipes.yaml +3 -0
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -2
- package/graphs/steward/recipes.yaml +2 -1
- package/package.json +1 -1
- package/scripts/brand/check.ts +7 -5
- package/scripts/ci-public-export-import-gate.ts +154 -0
- package/scripts/ci-test-interference-gate.ts +5 -1
- package/scripts/ci-typecheck-changed.ts +82 -31
- package/scripts/docs/doc-rot.ts +164 -0
- package/scripts/docs/generate-graph-anatomy.ts +437 -0
- package/scripts/docs-cli-check.ts +4 -4
- package/scripts/expose-rubric-suggest.ts +18 -12
- package/scripts/goals/goal-census.ts +343 -0
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +30 -3
- package/scripts/marketing/generate-ax-brief.ts +140 -0
- package/scripts/marketing/generate-feature-map.ts +118 -0
- package/scripts/measure/author-section-contribution.ts +84 -0
- package/scripts/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/cut-branch.ts +43 -13
- package/scripts/release-loop/gate-node.ts +82 -14
- package/scripts/release-loop/vault-note-node.ts +257 -8
- package/scripts/release-story/draft.ts +57 -9
- package/scripts/rhythm/daily.ts +263 -76
- package/scripts/rhythm/monthly.ts +97 -0
- package/scripts/rhythm/weekly.ts +30 -9
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/src/acp/client.ts +17 -2
- package/src/agent-env/profile.ts +161 -0
- package/src/agent-mission/driver.ts +51 -1
- package/src/agent-mission/supervisor-metrics-cli.ts +40 -0
- package/src/agent-mission/supervisor-metrics.ts +218 -0
- package/src/agent-substrate/pr-reviewer.ts +2 -0
- package/src/ask-user-question/pending-questions.ts +5 -0
- package/src/auth/github-app-token.ts +112 -13
- package/src/autopilot/build/llm-conflict-merge.ts +32 -6
- package/src/autopilot/track-agent-shadow.ts +2 -1
- package/src/briefing/brief-items.ts +276 -0
- package/src/budget/usage-outcomes.ts +204 -0
- package/src/claims/claims-render.ts +14 -5
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/brief-cli.ts +73 -0
- package/src/cli/decisions-cli.ts +9 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/intake-cli.ts +46 -17
- package/src/cli/know-cli.ts +20 -0
- package/src/cli/logs-cli.ts +43 -1
- package/src/cli/pod-cli.ts +2 -0
- package/src/cli/pr-cli.ts +80 -12
- package/src/cli/release-cli.ts +66 -29
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/cli/self-update.ts +200 -1
- package/src/cli/usage-cli.ts +26 -0
- package/src/context-bus/context-now-public.ts +16 -6
- package/src/context-bus/context-now.ts +3 -1
- package/src/context-bus/coord-events.ts +6 -4
- package/src/decisions/decision-cards.ts +33 -7
- package/src/decisions/proact-meter.ts +286 -0
- package/src/decisions/proact1-lite.ts +86 -0
- package/src/decisions/web-push-decision-cards.ts +1 -1
- package/src/discord-self-message.ts +9 -0
- package/src/domains/alert-audit.ts +271 -0
- package/src/domains/core-tools.ts +4 -3
- package/src/domains/ops-facts-tool.ts +31 -1
- package/src/flow-loop/split.ts +24 -0
- package/src/harness/harness-cli-command.ts +147 -17
- package/src/harness/harness-incidents.ts +2 -0
- package/src/harness/harness-pod-dispatch.ts +67 -8
- package/src/harness/harness-queue.ts +380 -50
- package/src/harness/harness-seams.ts +13 -3
- package/src/harness/helper-repair.ts +218 -0
- package/src/harness/helper-scan.ts +27 -4
- package/src/harness/launch-stamp.ts +236 -0
- package/src/harness/ledger-write-guard-child.fixture.ts +12 -0
- package/src/harness/ledger-write-guard.ts +35 -0
- package/src/harness/nested-elanous-policy.ts +114 -0
- package/src/harness/pod-memory-policy.ts +15 -0
- package/src/harness/repository-name.ts +55 -0
- package/src/hq/fence-audit.ts +90 -0
- package/src/hq/hq.ts +70 -6
- package/src/hq/lease.ts +9 -7
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +31 -0
- package/src/hq/transfer.ts +185 -0
- package/src/index.ts +189 -13
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +65 -21
- package/src/intake-plane/front-classifier.ts +6 -0
- package/src/intake-plane/items.ts +3 -2
- package/src/intake-plane/lens.ts +177 -0
- package/src/intake-plane/route.ts +145 -1
- package/src/knowledge/know-find.ts +105 -0
- package/src/llm.ts +75 -19
- package/src/loops/budget.ts +36 -5
- package/src/loops/checker.ts +2 -2
- package/src/loops/orchestrator/author-depth.ts +65 -9
- package/src/loops/orchestrator/author-input-check.ts +8 -3
- package/src/loops/orchestrator/author-ledger.ts +3 -1
- package/src/loops/orchestrator/finish-rate.ts +14 -3
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +107 -29
- package/src/loops/orchestrator/traffic.ts +117 -11
- package/src/loops/registry.ts +142 -61
- package/src/maturity/bot-slash-catalog.ts +11 -2
- package/src/maturity/feature-maturity.ts +4 -3
- package/src/maturity/tui-slash-availability.ts +40 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/http-server.ts +28 -14
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/schedules-read.ts +13 -0
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/oauth/overload-failover-launch.ts +75 -0
- package/src/oauth/overload-failover.ts +137 -0
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +6 -3
- package/src/release-loop/checklist.ts +19 -5
- package/src/release-loop/feature-store.ts +59 -31
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +98 -29
- package/src/seat-dispatch/seat-ask.ts +33 -6
- package/src/seat-dispatch/seat-request-eligibility.ts +19 -0
- package/src/seat-dispatch/seat-request-ledger.ts +133 -0
- package/src/seat-loop/seat-loop.ts +215 -12
- package/src/self-dev/ask-launch-flow.ts +22 -1
- package/src/self-dev/authored-goal-trace.ts +64 -0
- package/src/self-dev/dev-cli.ts +19 -1
- package/src/self-dev/dev-pipeline.ts +14 -9
- package/src/self-dev/draft-sweep.ts +90 -10
- package/src/self-dev/draft-triage-rules.ts +25 -8
- package/src/self-dev/entrance-registry.ts +10 -6
- package/src/self-dev/old-door.ts +73 -0
- package/src/self-dev/orchestrate.ts +15 -1
- package/src/self-dev/review-provider-fallback.ts +6 -1
- package/src/self-dev/run-supervisor.ts +49 -2
- package/src/self-implement/app-api-budget.ts +166 -0
- package/src/self-implement/budget-gate.ts +7 -5
- package/src/self-implement/follow-up-goals.ts +289 -0
- package/src/self-implement/frozen-merges.ts +3 -2
- package/src/self-implement/goal-author.ts +50 -6
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/heal-intake.ts +120 -2
- package/src/self-implement/heal-triage.ts +44 -0
- package/src/self-implement/host-regate.ts +9 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +325 -47
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +42 -7
- package/src/self-implement/run-ledger.ts +6 -0
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +45 -16
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/session-runtime/index.ts +2 -0
- package/src/session-runtime/retry-api.ts +87 -0
- package/src/session-runtime/retry-policy.ts +107 -6
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +20 -2
- package/src/task-cards/card-cli.ts +25 -0
- package/src/task-cards/card-store.ts +5 -3
- package/src/task-orchestrator/surfaces/pod-artifact-return.ts +63 -48
- package/src/task-orchestrator/surfaces/pod-command-job.ts +84 -9
- package/src/task-orchestrator/surfaces/pod-failure-reason.ts +48 -0
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +4 -3
- package/src/task-orchestrator/surfaces/pod-ledger-prod-sink.ts +75 -0
- package/src/task-orchestrator/surfaces/pod-pool.ts +70 -9
- package/src/task-orchestrator/surfaces/pod-source-receive.ts +37 -1
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +166 -46
- package/src/task-orchestrator/surfaces/self-implement.ts +7 -1
- package/src/telegram-commands.ts +5 -0
- package/src/telegram.ts +72 -4
- package/src/user-config.ts +64 -7
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/apps/pwa/out/_next/static/chunks/4562-071d8bac44a01069.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5977-2df489622a52a33a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-74383f890f075511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-c496bb16da85385f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-1abfe3aec3be6bdc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-fb260b9283dbf646.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-61ec1d2e92bdd50a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-fd7e200d4d83fbd7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-d47266766766d227.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-2ce5471926761fdd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-e995d30f6800b6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-39f89c53038a51d4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-5a753bb4ec0cc511.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-04250f0e2c0d03df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-341480ed8e3c1ea8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-8ac8d71c2adf9c8a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-e1e47d8d940aba3d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-94dc383d3d54db1a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-b3759cc14da80908.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-194dce4a83143fce.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-35bdeae2e2c17147.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-134c1bc762aee62c.js +0 -1
- package/apps/pwa/out/_next/static/css/2e33713c444d5b39.css +0 -1
- package/apps/pwa/out/_next/static/x6mjI6vqOkVVu0jFuDkz1/_buildManifest.js +0 -1
- /package/apps/pwa/out/_next/static/{x6mjI6vqOkVVu0jFuDkz1 → rv9cierV0jSOvDBm1g0XB}/_ssgManifest.js +0 -0
|
@@ -5,18 +5,23 @@ import { CliUserError } from '../cli/cli-user-error.js';
|
|
|
5
5
|
import { getElanousConfigDirOverride } from '../elanous-config-dir.js';
|
|
6
6
|
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
7
7
|
import { userConfigPath } from '../user-config.js';
|
|
8
|
-
import { addItem, listChecklist, ownerMatches, parseOwner, setItem, type ChecklistItem } from './checklist.js';
|
|
8
|
+
import { addItem, listChecklist, ownerMatches, parseOwner, setItem, validateCeoLoad, type ChecklistItem } from './checklist.js';
|
|
9
9
|
import { move, releasedVersion } from './feature-store.js';
|
|
10
10
|
import { listSchedules, type ReleaseSchedule } from './release-schedule.js';
|
|
11
11
|
|
|
12
12
|
export type PlacementPriority = 'P0' | 'P1' | 'P2';
|
|
13
|
-
export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string }
|
|
13
|
+
export interface PlacementCell { id: string; title: string; owner: string; priority: PlacementPriority; predecessors: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string; accelerator?: boolean }
|
|
14
14
|
export interface PlacementDecision { id: string; from: string | null; version: string; reason: string; displaced: Array<{ id: string; from: string; to: string; reason: string }> }
|
|
15
|
+
export interface RebalanceResult {
|
|
16
|
+
decisions: PlacementDecision[];
|
|
17
|
+
blocked: Array<{ id: string; from: string; to: string; reason: string }>;
|
|
18
|
+
}
|
|
15
19
|
export interface PlacementDeps {
|
|
16
20
|
now?: Date;
|
|
17
21
|
schedules?: ReleaseSchedule[];
|
|
18
22
|
merged24h?: number;
|
|
19
23
|
seatCap?: Record<string, number>;
|
|
24
|
+
ceoDailyCap?: number;
|
|
20
25
|
checklist?: typeof listChecklist;
|
|
21
26
|
released?: string;
|
|
22
27
|
dryRun?: boolean;
|
|
@@ -47,11 +52,38 @@ export function mergedPrsLast24h(now = new Date()): number {
|
|
|
47
52
|
return rows.filter((row) => Date.parse(row.mergedAt) >= Date.parse(since) && Date.parse(row.mergedAt) <= now.getTime()).length;
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
function placementConfig(): { seatCap?: Record<string, number>; ceoDailyCap?: number } {
|
|
51
56
|
const path = getElanousConfigDirOverride() ? join(effectiveInstanceRoot(), 'config.json') : userConfigPath();
|
|
52
57
|
if (!existsSync(path)) return {};
|
|
53
|
-
const raw = JSON.parse(readFileSync(path, 'utf8')) as { release?: { placement?: { seatCap?: Record<string, number
|
|
54
|
-
return raw.release?.placement
|
|
58
|
+
const raw = JSON.parse(readFileSync(path, 'utf8')) as { release?: { placement?: { seatCap?: Record<string, number>; ceoDailyCap?: number } } };
|
|
59
|
+
return raw.release?.placement ?? {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function placementSeatCap(): Record<string, number> { return placementConfig().seatCap ?? {}; }
|
|
63
|
+
function validCeoCap(cap: number): number {
|
|
64
|
+
if (!Number.isSafeInteger(cap) || cap < 0) throw new CliUserError('잘못된 대표 하루 상한: release.placement.ceoDailyCap');
|
|
65
|
+
return cap;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function placementCeoDailyCap(): number { return validCeoCap(placementConfig().ceoDailyCap ?? 30); }
|
|
69
|
+
|
|
70
|
+
/** Explicit day wins; otherwise a cell uses the target release's landing day in KST. */
|
|
71
|
+
function ceoDay(item: { ceoDate?: string }, row: ReleaseSchedule): string {
|
|
72
|
+
return item.ceoDate ?? new Date(Date.parse(row.landBy!) + 9 * 3_600_000).toISOString().slice(0, 10);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type CeoWork = Pick<ChecklistItem, 'id' | 'ceoMinutes' | 'ceoDate'>;
|
|
76
|
+
function ceoOverload(item: CeoWork, row: ReleaseSchedule, snapshots: Map<string, { items: CeoWork[] }>, schedules: ReleaseSchedule[], cap: number): string | null {
|
|
77
|
+
if (!item.ceoMinutes) return null;
|
|
78
|
+
const day = ceoDay(item, row);
|
|
79
|
+
const peers = [...snapshots].flatMap(([version, snapshot]) => {
|
|
80
|
+
const schedule = schedules.find((candidate) => candidate.version === version);
|
|
81
|
+
return snapshot.items.filter((other) => other.id !== item.id && other.ceoMinutes
|
|
82
|
+
&& (other.ceoDate === day || (!other.ceoDate && schedule?.landBy && ceoDay(other, schedule) === day)));
|
|
83
|
+
});
|
|
84
|
+
const total = item.ceoMinutes + peers.reduce((sum, other) => sum + other.ceoMinutes!, 0);
|
|
85
|
+
if (total <= cap) return null;
|
|
86
|
+
return `대표 손 과부하 ${day} KST: ${[...peers.map((peer) => `${peer.id} ${peer.ceoMinutes}분`), `${item.id} ${item.ceoMinutes}분`].join(' + ')} = ${total}분 > 하루 상한 ${cap}분 — 늦추기(다음 날짜/판) · 자리 대행(대표 분량 축소) · 묶기(촬영·승인 합산 분량 축소)를 제안`;
|
|
55
87
|
}
|
|
56
88
|
|
|
57
89
|
function dependencyViolation(
|
|
@@ -87,8 +119,10 @@ function available(row: ReleaseSchedule, now: number, backlog: readonly string[]
|
|
|
87
119
|
export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): PlacementDecision {
|
|
88
120
|
if (!input.id.trim() || !input.title.trim()) throw new CliUserError('칸 id 와 제목이 필요하다');
|
|
89
121
|
if (!Array.isArray(input.predecessors) || input.predecessors.some((id) => typeof id !== 'string' || !id.trim() || id === input.id)) throw new CliUserError('선행 칸 id 가 잘못됐다');
|
|
122
|
+
validateCeoLoad(input);
|
|
90
123
|
const seat = parseOwner(input.owner).seat;
|
|
91
124
|
if (!['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
|
|
125
|
+
if (input.accelerator !== undefined && input.accelerator !== true && input.accelerator !== false) throw new CliUserError('가속 등급은 true 이거나 생략한다');
|
|
92
126
|
const now = (deps.now ?? new Date()).getTime();
|
|
93
127
|
const schedules = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
|
|
94
128
|
const released = deps.released ?? releasedVersion();
|
|
@@ -109,6 +143,9 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
109
143
|
if (from && released && versionOrder(from, released) <= 0) throw new CliUserError(`이미 발행된 판의 칸은 배치하지 않는다: ${input.id}`);
|
|
110
144
|
if (existing?.status === 'done') throw new CliUserError(`끝난 칸은 배치하지 않는다: ${input.id}`);
|
|
111
145
|
if (existing && (existing.title !== input.title || existing.owner !== input.owner)) throw new CliUserError(`기존 칸 제목·담당 불일치: ${input.id}`);
|
|
146
|
+
const accelerator = input.accelerator === true || (input.accelerator === undefined && existing?.accelerator === true);
|
|
147
|
+
const load = { id: input.id, ceoMinutes: input.ceoMinutes ?? existing?.ceoMinutes, ceoDate: input.ceoDate ?? existing?.ceoDate };
|
|
148
|
+
const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
|
|
112
149
|
const predecessors = input.predecessors.map((id) => {
|
|
113
150
|
const version = locations.get(id);
|
|
114
151
|
if (!version) throw new CliUserError(`없는 선행 칸: ${id}`);
|
|
@@ -133,46 +170,70 @@ export function placeCell(input: PlacementCell, deps: PlacementDeps = {}): Place
|
|
|
133
170
|
const capacity = (row: ReleaseSchedule) => Math.floor(rate * (Date.parse(row.landBy!) - now) / 86_400_000 * 0.7);
|
|
134
171
|
const displaced: PlacementDecision['displaced'] = [];
|
|
135
172
|
let chosen: ReleaseSchedule | undefined;
|
|
136
|
-
|
|
173
|
+
let loadIssue: string | null = null;
|
|
174
|
+
const feasible = candidates.filter((row) => !dependencyViolation(snapshots, locations, new Map([[input.id, row.version]]), input));
|
|
175
|
+
// 판은 «마감 안 가장 이른 판»부터 본다(기존 불변식). 가속 등급은 그 판이 찼을 때 같은 우선순위 비가속 칸을 밀 수 있게만 한다.
|
|
176
|
+
for (const row of feasible) {
|
|
137
177
|
const placement = new Map([[input.id, row.version]]);
|
|
138
|
-
if (dependencyViolation(snapshots, locations, placement, input)) continue;
|
|
139
178
|
const items = occupied(row.version);
|
|
140
179
|
const seatItems = items.filter((item) => ownerMatches(item.owner, seat));
|
|
141
|
-
|
|
142
|
-
if (
|
|
180
|
+
const directIssue = ceoOverload(load, row, snapshots, schedules, ceoCap);
|
|
181
|
+
if (items.length < capacity(row) && seatItems.length < seatLimit) {
|
|
182
|
+
if (directIssue) { loadIssue ??= directIssue; continue; }
|
|
183
|
+
chosen = row;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
const mayDisplace = input.priority === 'P1' ? from !== row.version : accelerator && from !== row.version;
|
|
187
|
+
if (!mayDisplace) { loadIssue ??= directIssue; continue; }
|
|
143
188
|
const next = open.find((later) => versionOrder(later.version, row.version) > 0);
|
|
144
|
-
if (!next || items.length - 1 >= capacity(row)) continue;
|
|
189
|
+
if (!next || items.length - 1 >= capacity(row)) { loadIssue ??= directIssue; continue; }
|
|
190
|
+
const victimOk = (priority: PlacementPriority | undefined) => (input.priority === 'P1' && priority === 'P2')
|
|
191
|
+
|| (accelerator && priority === input.priority);
|
|
145
192
|
const p2 = [...items].reverse().find((item) => {
|
|
146
|
-
if (item.
|
|
193
|
+
if (item.accelerator === true) return false;
|
|
194
|
+
if (!victimOk(item.priority) || dependencyViolation(snapshots, locations, new Map([...placement, [item.id, next.version]]), input)) return false;
|
|
147
195
|
if (item.deadlineVersion && versionOrder(next.version, item.deadlineVersion) > 0) return false;
|
|
148
196
|
if (items.length < capacity(row) && !(seatItems.length >= seatLimit && ownerMatches(item.owner, seat))) return false;
|
|
149
197
|
if (!item.owner) return false;
|
|
150
198
|
const shiftedSeat = parseOwner(item.owner).seat;
|
|
151
|
-
|
|
199
|
+
if (occupied(next.version).length >= capacity(next) || occupied(next.version).filter((other) => ownerMatches(other.owner, shiftedSeat)).length >= (caps[shiftedSeat] ?? Infinity)) return false;
|
|
200
|
+
if (!(seatItems.length < seatLimit || (ownerMatches(item.owner, seat) && seatItems.length - 1 < seatLimit))) return false;
|
|
201
|
+
|
|
202
|
+
// Evaluate both changed cells against the same post-placement state, before either ledger write.
|
|
203
|
+
const proposed = new Map<string, { items: CeoWork[] }>([...snapshots].map(([version, snapshot]) => [version, { items: snapshot.items.filter((cell) => cell.id !== input.id && cell.id !== item.id) }]));
|
|
204
|
+
proposed.get(row.version)!.items.push(load);
|
|
205
|
+
proposed.get(next.version)!.items.push(item);
|
|
206
|
+
const issue = ceoOverload(load, row, proposed, schedules, ceoCap)
|
|
207
|
+
?? ceoOverload(item, next, proposed, schedules, ceoCap);
|
|
208
|
+
if (issue) { loadIssue ??= issue; return false; }
|
|
209
|
+
return true;
|
|
152
210
|
});
|
|
153
|
-
if (p2
|
|
154
|
-
displaced.push({ id: p2.id, from: row.version, to: next.version, reason:
|
|
211
|
+
if (p2) {
|
|
212
|
+
displaced.push({ id: p2.id, from: row.version, to: next.version, reason: accelerator
|
|
213
|
+
? `가속 등급 ${input.id} 를 같은 우선순위 안에서 먼저 두기 위해 ${p2.priority ?? 'P2'} ${p2.id} 이월`
|
|
214
|
+
: `P1 ${input.id} 마감 판 ${row.version} 용량 확보를 위해 P2 이월` });
|
|
155
215
|
chosen = row;
|
|
156
216
|
break;
|
|
157
217
|
}
|
|
218
|
+
loadIssue ??= directIssue;
|
|
158
219
|
}
|
|
159
|
-
if (!chosen) throw new CliUserError(`${input.id} 배치할 판이 없다 — 용량·선행·마감·동결을 확인하라`);
|
|
160
|
-
const reason = `${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
|
|
220
|
+
if (!chosen) throw new CliUserError(loadIssue ?? `${input.id} 배치할 판이 없다 — 용량·선행·마감·동결을 확인하라`);
|
|
221
|
+
const reason = `${accelerator ? '가속 등급 · ' : ''}${input.priority} ${input.priority === 'P0' ? '사고·회귀·발행 막음: 다음 판' : input.priority === 'P1' ? `지시·행사 마감 판 ${deadlineVersion}` : '착지 마감 내 가장 이른 여유 판'}${accelerator ? ' · 같은 우선순위 안에서 가속 등급을 먼저' : ''} · PR/24h ${rate} · 용량 ${capacity(chosen)} · 자리 ${seatLimit}`;
|
|
161
222
|
const decision = { id: input.id, from, version: chosen.version, reason, displaced };
|
|
162
223
|
if (!deps.dryRun) {
|
|
163
224
|
const by = deps.by ?? 'OP';
|
|
164
225
|
for (const shifted of displaced) move(shifted.id, shifted.from, shifted.to, by, undefined, undefined, shifted.reason);
|
|
165
226
|
if (from && from !== chosen.version) move(input.id, from, chosen.version, by, undefined, undefined, reason);
|
|
166
|
-
if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion)))
|
|
167
|
-
setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}) }, by);
|
|
168
|
-
if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion });
|
|
227
|
+
if (from && (existing?.priority !== input.priority || JSON.stringify(existing.predecessors ?? []) !== JSON.stringify(input.predecessors) || (input.deadlineVersion !== undefined && existing.deadlineVersion !== input.deadlineVersion) || (input.ceoMinutes !== undefined && existing.ceoMinutes !== input.ceoMinutes) || (input.ceoDate !== undefined && existing.ceoDate !== input.ceoDate) || (input.accelerator !== undefined && (existing?.accelerator === true) !== (input.accelerator === true))))
|
|
228
|
+
setItem(chosen.version, input.id, { priority: input.priority, predecessors: input.predecessors, ...(input.deadlineVersion ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), ...(input.accelerator !== undefined ? { accelerator: input.accelerator === true ? true : null } : {}) }, by);
|
|
229
|
+
if (!from) addItem(chosen.version, { id: input.id, title: input.title, owner: input.owner, priority: input.priority, predecessors: input.predecessors, deadlineVersion, ...(load.ceoMinutes !== undefined ? { ceoMinutes: load.ceoMinutes } : {}), ...(load.ceoDate !== undefined ? { ceoDate: load.ceoDate } : {}), ...(accelerator ? { accelerator: true } : {}) });
|
|
169
230
|
}
|
|
170
231
|
return decision;
|
|
171
232
|
}
|
|
172
233
|
|
|
173
234
|
function moveConstraint(
|
|
174
235
|
item: ChecklistItem, to: ReleaseSchedule, backlog: readonly string[],
|
|
175
|
-
snapshots: Map<string, ChecklistItem[]>, now: number, rate: number, caps: Record<string, number>,
|
|
236
|
+
snapshots: Map<string, ChecklistItem[]>, now: number, rate: number, caps: Record<string, number>, rows: ReleaseSchedule[], ceoCap: number,
|
|
176
237
|
): string | null {
|
|
177
238
|
if (!to.landBy || !available(to, now, backlog)) return '착지 마감이 지났거나 백로그 판이라 이동 불가';
|
|
178
239
|
if (item.deadlineVersion && versionOrder(to.version, item.deadlineVersion) > 0) return '마감 판 뒤로 이월 불가';
|
|
@@ -181,6 +242,9 @@ function moveConstraint(
|
|
|
181
242
|
const destination = snapshots.get(to.version) ?? [];
|
|
182
243
|
if (destination.filter((cell) => cell.status !== 'done').length >= Math.floor(rate * (Date.parse(to.landBy) - now) / 86_400_000 * 0.7)) return '다음 판 PR 용량 초과';
|
|
183
244
|
if (destination.filter((cell) => cell.status !== 'done' && ownerMatches(cell.owner, seat)).length >= (caps[seat] ?? Infinity)) return '다음 판 자리 용량 초과';
|
|
245
|
+
const loadIssue = ceoOverload(item, to, new Map([...snapshots].map(([version, items]) => [version, { items }])),
|
|
246
|
+
rows, ceoCap);
|
|
247
|
+
if (loadIssue) return loadIssue;
|
|
184
248
|
const locations = new Map<string, string>();
|
|
185
249
|
for (const [version, items] of snapshots) for (const cell of items) {
|
|
186
250
|
if (locations.has(cell.id)) return `여러 판의 같은 칸: ${cell.id}`;
|
|
@@ -198,30 +262,35 @@ function moveConstraint(
|
|
|
198
262
|
}
|
|
199
263
|
|
|
200
264
|
/** Only unstarted cells inside the two-hour pre-deadline window move; a dry run never writes. */
|
|
201
|
-
export function rebalance(version: string, deps: PlacementDeps = {}):
|
|
265
|
+
export function rebalance(version: string, deps: PlacementDeps = {}): RebalanceResult {
|
|
202
266
|
const rows = [...(deps.schedules ?? listSchedules())].sort((a, b) => versionOrder(a.version, b.version));
|
|
203
267
|
const current = rows.find((row) => row.version === version);
|
|
204
268
|
if (!current?.landBy) throw new CliUserError(`착지 마감이 없는 판: ${version}`);
|
|
205
269
|
const now = (deps.now ?? new Date()).getTime();
|
|
206
|
-
if (Date.parse(current.landBy) - now > 7_200_000 || Date.parse(current.landBy) <= now) return [];
|
|
270
|
+
if (Date.parse(current.landBy) - now > 7_200_000 || Date.parse(current.landBy) <= now) return { decisions: [], blocked: [] };
|
|
207
271
|
const backlog = deps.backlog ?? BACKLOG_VERSIONS;
|
|
208
272
|
const next = rows.find((row) => versionOrder(row.version, version) > 0 && !backlog.includes(row.version));
|
|
209
273
|
const snapshots = new Map(rows.map((row) => [row.version, [...(deps.checklist ?? listChecklist)(row.version).items]]));
|
|
210
274
|
const unstarted = snapshots.get(version)!.filter((item) => item.status === 'yellow' && !item.evidence);
|
|
211
275
|
if (unstarted.length && !next) throw new CliUserError(`${version} 다음 판이 없다`);
|
|
212
|
-
if (!unstarted.length) return [];
|
|
276
|
+
if (!unstarted.length) return { decisions: [], blocked: [] };
|
|
213
277
|
const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
|
|
214
278
|
const caps = deps.seatCap ?? placementSeatCap();
|
|
215
|
-
const
|
|
279
|
+
const ceoCap = validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap());
|
|
280
|
+
const result: RebalanceResult = { decisions: [], blocked: [] };
|
|
216
281
|
for (const item of unstarted) {
|
|
217
|
-
|
|
282
|
+
const violation = moveConstraint(item, next!, backlog, snapshots, now, rate, caps, rows, ceoCap);
|
|
283
|
+
if (violation) {
|
|
284
|
+
result.blocked.push({ id: item.id, from: version, to: next!.version, reason: violation });
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
218
287
|
const reason = `${version} 착지 마감 2시간 전 미시작 칸 이월`;
|
|
219
|
-
decisions.push({ id: item.id, from: version, version: next!.version, reason, displaced: [] });
|
|
288
|
+
result.decisions.push({ id: item.id, from: version, version: next!.version, reason, displaced: [] });
|
|
220
289
|
snapshots.set(version, snapshots.get(version)!.filter((cell) => cell.id !== item.id));
|
|
221
290
|
snapshots.get(next!.version)!.push(item);
|
|
222
291
|
}
|
|
223
|
-
if (!deps.dryRun) for (const decision of decisions) move(decision.id, version, decision.version, deps.by ?? 'OP', undefined, undefined, decision.reason);
|
|
224
|
-
return
|
|
292
|
+
if (!deps.dryRun) for (const decision of result.decisions) move(decision.id, version, decision.version, deps.by ?? 'OP', undefined, undefined, decision.reason);
|
|
293
|
+
return result;
|
|
225
294
|
}
|
|
226
295
|
|
|
227
296
|
/** A seat may defer its own cell one release, never pull another owner's cell forward. */
|
|
@@ -236,7 +305,7 @@ export function seatMove(id: string, from: string, to: string, by: string, reaso
|
|
|
236
305
|
if (!ownerMatches(item.owner, parseOwner(by).seat) || next?.version !== to) throw new CliUserError('남의 칸 당기기 거부 — COO 에 요청');
|
|
237
306
|
const now = (deps.now ?? new Date()).getTime();
|
|
238
307
|
const rate = deps.merged24h ?? mergedPrsLast24h(new Date(now));
|
|
239
|
-
const violation = moveConstraint(item, next, backlog, snapshots, now, rate, deps.seatCap ?? placementSeatCap());
|
|
308
|
+
const violation = moveConstraint(item, next, backlog, snapshots, now, rate, deps.seatCap ?? placementSeatCap(), rows, validCeoCap(deps.ceoDailyCap ?? placementCeoDailyCap()));
|
|
240
309
|
if (violation) throw new CliUserError(`${id}: ${violation}`);
|
|
241
310
|
if (!deps.dryRun) move(id, from, to, by, undefined, undefined, reason);
|
|
242
311
|
}
|
|
@@ -20,7 +20,7 @@ function createTable(store: MsgStore): void {
|
|
|
20
20
|
store.db.exec(`CREATE TABLE IF NOT EXISTS seat_asks (
|
|
21
21
|
id TEXT PRIMARY KEY, seat TEXT NOT NULL, origin TEXT NOT NULL,
|
|
22
22
|
deadline INTEGER NOT NULL, timeout_minutes INTEGER NOT NULL DEFAULT 120, status TEXT NOT NULL DEFAULT 'pending',
|
|
23
|
-
answer TEXT, answered_at INTEGER
|
|
23
|
+
answer TEXT, answered_at INTEGER, asked_at INTEGER
|
|
24
24
|
); CREATE TABLE IF NOT EXISTS seat_ask_outbox (
|
|
25
25
|
id TEXT PRIMARY KEY, origin TEXT NOT NULL, text TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
|
|
26
26
|
claim_until INTEGER NOT NULL DEFAULT 0, claim_token TEXT
|
|
@@ -28,6 +28,7 @@ function createTable(store: MsgStore): void {
|
|
|
28
28
|
const askColumns = store.db.query('PRAGMA table_info(seat_asks)').all() as Array<{ name: string }>;
|
|
29
29
|
if (!askColumns.some(({ name }) => name === 'answer')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answer TEXT');
|
|
30
30
|
if (!askColumns.some(({ name }) => name === 'answered_at')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN answered_at INTEGER');
|
|
31
|
+
if (!askColumns.some(({ name }) => name === 'asked_at')) store.db.exec('ALTER TABLE seat_asks ADD COLUMN asked_at INTEGER');
|
|
31
32
|
const columns = store.db.query('PRAGMA table_info(seat_ask_outbox)').all() as Array<{ name: string }>;
|
|
32
33
|
if (!columns.some(({ name }) => name === 'claim_until')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_until INTEGER NOT NULL DEFAULT 0');
|
|
33
34
|
if (!columns.some(({ name }) => name === 'claim_token')) store.db.exec('ALTER TABLE seat_ask_outbox ADD COLUMN claim_token TEXT');
|
|
@@ -87,8 +88,9 @@ export async function askSeat(text: string, origin: AskOrigin, command: CeoComma
|
|
|
87
88
|
const store = (deps.open ?? openMsgStore)();
|
|
88
89
|
try {
|
|
89
90
|
createTable(store);
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
const askedAt = (deps.now ?? Date.now)();
|
|
92
|
+
store.db.query('INSERT INTO seat_asks (id, seat, origin, deadline, timeout_minutes, status, asked_at) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
93
|
+
.run(id, 'TC', JSON.stringify(origin), askedAt + timeout, minutes, 'pending', askedAt);
|
|
92
94
|
} finally { store.close(); }
|
|
93
95
|
try {
|
|
94
96
|
const result = await (deps.dispatch ?? dispatchCeoTask)('TC', `질문: ${body}\n답장 요청: ${id}\n회신: elanous seat answer ${id} "<답>"`, command,
|
|
@@ -119,12 +121,25 @@ export function answerSeatAsk(id: string, text: string, deps: Pick<SeatAskDeps,
|
|
|
119
121
|
} finally { store.close(); }
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/** Return only the recorded reply's return address and measured ask time. */
|
|
125
|
+
export function seatAskReplyInfo(id: string, open: () => MsgStore = openMsgStore): { origin: AskOrigin; askedAt: number | null; answeredAt: number } {
|
|
126
|
+
const store = open();
|
|
127
|
+
try {
|
|
128
|
+
createTable(store);
|
|
129
|
+
const row = store.db.query('SELECT origin, asked_at, answered_at FROM seat_asks WHERE id = ? AND answer IS NOT NULL')
|
|
130
|
+
.get(id) as { origin: string; asked_at: number | null; answered_at: number } | null;
|
|
131
|
+
if (!row) throw new Error(`seat ask answer not recorded: ${id}`);
|
|
132
|
+
return { origin: JSON.parse(row.origin) as AskOrigin, askedAt: row.asked_at, answeredAt: row.answered_at };
|
|
133
|
+
} finally { store.close(); }
|
|
134
|
+
}
|
|
135
|
+
|
|
122
136
|
/** Deliver recorded seat answers or expiration notices via the original surface. */
|
|
123
|
-
export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
137
|
+
export async function deliverSeatAnswers(deps: SeatAskDeps & { askId?: string }): Promise<void> {
|
|
124
138
|
const store = (deps.open ?? openMsgStore)();
|
|
125
139
|
try {
|
|
126
140
|
createTable(store);
|
|
127
|
-
const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE status = 'pending'
|
|
141
|
+
const pending = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE status = 'pending' AND (? IS NULL OR id = ?)")
|
|
142
|
+
.all(deps.askId ?? null, deps.askId ?? null) as AskRow[];
|
|
128
143
|
const decide = store.db.transaction((id: string, now: number): 'answered' | 'expired' | null => {
|
|
129
144
|
const ask = store.db.query("SELECT id, seat, origin, deadline, timeout_minutes, answer, answered_at FROM seat_asks WHERE id = ? AND status = 'pending'").get(id) as AskRow | null;
|
|
130
145
|
if (!ask) return null;
|
|
@@ -150,7 +165,8 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
|
150
165
|
const status = decide(ask.id, (deps.now ?? Date.now)());
|
|
151
166
|
if (status) debug.log('seat.ask', status, { seat: ask.seat, id: ask.id, via: origin.channel });
|
|
152
167
|
}
|
|
153
|
-
const outbox = store.db.query("SELECT id, origin, text FROM seat_ask_outbox WHERE status = 'pending'
|
|
168
|
+
const outbox = store.db.query("SELECT id, origin, text FROM seat_ask_outbox WHERE status = 'pending' AND (? IS NULL OR id = ?)")
|
|
169
|
+
.all(deps.askId ?? null, deps.askId ?? null) as Array<{ id: string; origin: string; text: string }>;
|
|
154
170
|
for (const item of outbox) {
|
|
155
171
|
const origin = JSON.parse(item.origin) as AskOrigin;
|
|
156
172
|
if (deps.channel && origin.channel !== deps.channel) continue;
|
|
@@ -177,6 +193,17 @@ export async function deliverSeatAnswers(deps: SeatAskDeps): Promise<void> {
|
|
|
177
193
|
} finally { store.close(); }
|
|
178
194
|
}
|
|
179
195
|
|
|
196
|
+
/** A pending outbox with a live claim is being delivered by another worker. */
|
|
197
|
+
export function seatAskDeliveryInProgress(id: string, open: () => MsgStore = openMsgStore): boolean {
|
|
198
|
+
const store = open();
|
|
199
|
+
try {
|
|
200
|
+
createTable(store);
|
|
201
|
+
const row = store.db.query("SELECT claim_until FROM seat_ask_outbox WHERE id = ? AND status = 'pending' AND claim_token IS NOT NULL")
|
|
202
|
+
.get(id) as { claim_until: number } | null;
|
|
203
|
+
return row !== null && row.claim_until > Date.now();
|
|
204
|
+
} finally { store.close(); }
|
|
205
|
+
}
|
|
206
|
+
|
|
180
207
|
/** Confirm only this browser's displayed answers; polling alone never removes them. */
|
|
181
208
|
export function acknowledgeSeatAnswers(clientId: string, ids: string[], open: () => MsgStore = openMsgStore): void {
|
|
182
209
|
const store = open();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const DEFAULT_MAX_REQUEST_AGE_HOURS = 48;
|
|
2
|
+
|
|
3
|
+
export type SeatRequestEligibility =
|
|
4
|
+
| { eligible: true }
|
|
5
|
+
| { eligible: false; reason: 'empty-body' | 'too-old' | 'slot-closed' };
|
|
6
|
+
|
|
7
|
+
/** Unknown/unreadable slot status does not establish that a slot has closed. */
|
|
8
|
+
export function seatRequestEligibility(
|
|
9
|
+
request: { text: string; queuedAt: string },
|
|
10
|
+
options: { now: Date; maxRequestAgeHours?: number; slotStatus?: string | null },
|
|
11
|
+
): SeatRequestEligibility {
|
|
12
|
+
if (!request.text.trim()) return { eligible: false, reason: 'empty-body' };
|
|
13
|
+
const maxAgeHours = options.maxRequestAgeHours ?? DEFAULT_MAX_REQUEST_AGE_HOURS;
|
|
14
|
+
if (!Number.isFinite(maxAgeHours) || maxAgeHours < 0) throw new Error('invalid seat request max age hours');
|
|
15
|
+
const age = options.now.getTime() - Date.parse(request.queuedAt);
|
|
16
|
+
if (!Number.isFinite(age) || age > maxAgeHours * 60 * 60_000) return { eligible: false, reason: 'too-old' };
|
|
17
|
+
if (options.slotStatus === 'green' || options.slotStatus === 'done') return { eligible: false, reason: 'slot-closed' };
|
|
18
|
+
return { eligible: true };
|
|
19
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { Database } from 'bun:sqlite';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
|
|
6
|
+
export type SeatRequestStatus = 'pending' | 'queued' | 'rejected' | 'done';
|
|
7
|
+
export type SeatRequestRow = {
|
|
8
|
+
key: string;
|
|
9
|
+
seat: string;
|
|
10
|
+
status: SeatRequestStatus;
|
|
11
|
+
text: string;
|
|
12
|
+
queuedAt: string;
|
|
13
|
+
reason?: string;
|
|
14
|
+
closedAt?: string;
|
|
15
|
+
[field: string]: unknown;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type SeatRequestFilters = { seat?: string; status?: SeatRequestStatus };
|
|
19
|
+
export type CloseSeatRequestsOptions = {
|
|
20
|
+
reason: string;
|
|
21
|
+
status?: 'rejected' | 'done';
|
|
22
|
+
/** Minimum age in milliseconds, measured from queuedAt; only strictly older requests qualify. */
|
|
23
|
+
olderThan?: number;
|
|
24
|
+
now?: Date;
|
|
25
|
+
dryRun?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ledgerPath = (root: string): string => join(root, 'seat-requests', 'requests.jsonl');
|
|
29
|
+
|
|
30
|
+
/** SQLite releases its transaction lock on process death, including SIGKILL. */
|
|
31
|
+
export function withSeatRequestLedgerLock<T>(path: string, work: () => T): T {
|
|
32
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
33
|
+
const db = new Database(`${path}.lock.sqlite`);
|
|
34
|
+
try {
|
|
35
|
+
db.exec('PRAGMA busy_timeout = 10000');
|
|
36
|
+
db.exec('BEGIN IMMEDIATE');
|
|
37
|
+
try {
|
|
38
|
+
const result = work();
|
|
39
|
+
db.exec('COMMIT');
|
|
40
|
+
return result;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
db.exec('ROLLBACK');
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
} finally { db.close(); }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Called under the journal transaction lock; never expose a partial JSON line. */
|
|
49
|
+
export function appendSeatRequestRows<T>(path: string, rows: readonly T[],
|
|
50
|
+
write: (fd: number, data: Buffer, offset: number, length: number) => number = writeSync): void {
|
|
51
|
+
if (!rows.length) return;
|
|
52
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
53
|
+
const prior = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
54
|
+
if (prior && !prior.endsWith('\n')) throw new Error('invalid seat request journal terminator');
|
|
55
|
+
const data = Buffer.from(prior + rows.map((row) => `${JSON.stringify(row)}\n`).join(''));
|
|
56
|
+
const temp = `${path}.${randomUUID()}.tmp`;
|
|
57
|
+
let fd: number | undefined;
|
|
58
|
+
try {
|
|
59
|
+
fd = openSync(temp, 'wx', 0o600);
|
|
60
|
+
let offset = 0;
|
|
61
|
+
while (offset < data.length) {
|
|
62
|
+
const count = write(fd, data, offset, data.length - offset);
|
|
63
|
+
if (count <= 0) throw new Error('seat request journal write stalled');
|
|
64
|
+
offset += count;
|
|
65
|
+
}
|
|
66
|
+
fsyncSync(fd);
|
|
67
|
+
closeSync(fd);
|
|
68
|
+
fd = undefined;
|
|
69
|
+
renameSync(temp, path);
|
|
70
|
+
const directory = openSync(dirname(path), 'r');
|
|
71
|
+
try { fsyncSync(directory); } finally { closeSync(directory); }
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (fd !== undefined) closeSync(fd);
|
|
74
|
+
try { unlinkSync(temp); } catch { /* temp was not created or already renamed */ }
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function latestRows(text: string): Map<string, SeatRequestRow> {
|
|
80
|
+
const latest = new Map<string, SeatRequestRow>();
|
|
81
|
+
for (const line of text.split('\n')) {
|
|
82
|
+
if (!line.trim()) continue;
|
|
83
|
+
const row: unknown = JSON.parse(line);
|
|
84
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) throw new Error('invalid seat request row');
|
|
85
|
+
const request = row as Record<string, unknown>;
|
|
86
|
+
if (typeof request.key !== 'string' || !request.key || typeof request.seat !== 'string' || !request.seat
|
|
87
|
+
|| !['pending', 'queued', 'rejected', 'done'].includes(String(request.status))
|
|
88
|
+
|| typeof request.text !== 'string' || typeof request.queuedAt !== 'string') {
|
|
89
|
+
throw new Error('invalid seat request row');
|
|
90
|
+
}
|
|
91
|
+
latest.set(request.key, request as SeatRequestRow);
|
|
92
|
+
}
|
|
93
|
+
return latest;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Filters apply to the latest row, never to a historical row superseded by a closing event. */
|
|
97
|
+
export function listSeatRequests(root: string, filters: SeatRequestFilters = {}): SeatRequestRow[] {
|
|
98
|
+
const path = ledgerPath(root);
|
|
99
|
+
return [...latestRows(existsSync(path) ? readFileSync(path, 'utf8') : '').values()].filter((row) =>
|
|
100
|
+
(filters.seat === undefined || row.seat === filters.seat)
|
|
101
|
+
&& (filters.status === undefined || row.status === filters.status));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Append at most one terminal event for each distinct, currently open request key. */
|
|
105
|
+
export function closeSeatRequests(root: string, keys: readonly string[], options: CloseSeatRequestsOptions): SeatRequestRow[] {
|
|
106
|
+
const reason = options.reason?.trim();
|
|
107
|
+
if (!reason) throw new Error('seat request close requires a reason');
|
|
108
|
+
const status = options.status ?? 'rejected';
|
|
109
|
+
if (status !== 'rejected' && status !== 'done') throw new Error('invalid seat request close status');
|
|
110
|
+
if (options.olderThan !== undefined && (!Number.isSafeInteger(options.olderThan) || options.olderThan < 0)) {
|
|
111
|
+
throw new Error('invalid seat request older-than duration');
|
|
112
|
+
}
|
|
113
|
+
const now = options.now ?? new Date();
|
|
114
|
+
if (!Number.isFinite(now.getTime())) throw new Error('invalid seat request close time');
|
|
115
|
+
const path = ledgerPath(root);
|
|
116
|
+
const close = (): SeatRequestRow[] => {
|
|
117
|
+
const latest = latestRows(existsSync(path) ? readFileSync(path, 'utf8') : '');
|
|
118
|
+
const closed = [...new Set(keys)].flatMap((key) => {
|
|
119
|
+
const row = latest.get(key);
|
|
120
|
+
if (!row || (row.status !== 'pending' && row.status !== 'queued')) return [];
|
|
121
|
+
if (options.olderThan !== undefined) {
|
|
122
|
+
const queuedAt = Date.parse(row.queuedAt);
|
|
123
|
+
if (!Number.isFinite(queuedAt) || now.getTime() - queuedAt <= options.olderThan) return [];
|
|
124
|
+
}
|
|
125
|
+
return [{ ...row, status, reason, closedAt: now.toISOString(),
|
|
126
|
+
...(row.status === 'queued' && typeof row.ref !== 'string' && typeof row.receiptId === 'string'
|
|
127
|
+
? { ref: row.receiptId } : {}) } satisfies SeatRequestRow];
|
|
128
|
+
});
|
|
129
|
+
if (!options.dryRun) appendSeatRequestRows(path, closed);
|
|
130
|
+
return closed;
|
|
131
|
+
};
|
|
132
|
+
return options.dryRun ? close() : withSeatRequestLedgerLock(path, close);
|
|
133
|
+
}
|