elanous 0.2.14 → 0.2.15
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/LZcstaO9_NXypPFmujolv/_buildManifest.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1307-6e7e0377c1db0bdc.js → 1307-33ffcc49ab45123b.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/app/approvals/{page-74383f890f075511.js → page-0e2c57fe6add4b2a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-7b2b4f378354869a.js → page-e05a77df7e005b04.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f544338193a9a762.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-5491c115b1f9a923.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/{page-1512d91c7491ed62.js → page-b8c49f6b4fa2df91.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-a627db94ec6904a0.js → page-976813c91d7394ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-fb260b9283dbf646.js → page-28b9448091776c06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-1d4901eb7c2d8822.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-72d3e7ff35a8c954.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/{page-fed69f9759d248d0.js → page-60c7a8e1a4fe62ce.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-15374909d8f1aebd.js → page-2a52b316cb143d9c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-012a742f81218b01.js → page-c3ce483c8d5db370.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-99effd9d893a43dc.js → page-96f074f6538e8a2c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-938de57ae0315e46.js → layout-e97be5334d83a9f5.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-65e484302921a932.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/page-34d08facaf08f556.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-629e1ede482ec6eb.js → page-955ec2da3be4c560.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-9f88fbc15d9058d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-27e99b4e54a849d4.js → page-c1343f9499d86f3f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-8739a2e0ae793130.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-c9fc5706b8abaa11.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-32cde713be47cddb.js → page-d23b12bb92242558.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-8d47f45a0dc044ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-c7fea475488201a8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-04250f0e2c0d03df.js → page-e2ee2896811e98d0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/{page-e64caff084f7ce21.js → page-5efbf81124c474ae.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-db07eaffba0df7e3.js → page-85f1a1fc0a82738c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-8ac8d71c2adf9c8a.js → page-da6c08aae0f92768.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-06ee266953d9c879.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-86f91034bd5cabe5.js → page-153c2b3bcc89e8de.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-94dc383d3d54db1a.js → page-87a11e6a05f15199.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-b3759cc14da80908.js → page-7d4182964407c09c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-ce4d8a1215ce53a3.js → page-0d6d23b40263a7ea.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-7e8c39241f8eb6d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-008c1a96618a4887.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/{page-274703bc41b71333.js → page-f5597e894c02cd33.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-0337fdf7fb5e37b6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-35bdeae2e2c17147.js → page-73735b64bfe1ae67.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-3b7e821dc492bdfd.js → page-8fcec4c9ea2e7f70.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-f7e631e9b4daf66e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-4881b8f9f63694b4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-2f810b18d89ecba6.js → page-9817c7f9007466cc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/{page-134c1bc762aee62c.js → page-97f9f43172a3a90e.js} +1 -1
- package/apps/pwa/out/_next/static/css/1f7c958c41ffa3b9.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/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 +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/loops/index.html +1 -1
- package/apps/pwa/out/loops/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/graphs/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/release/recipes.yaml +3 -0
- package/graphs/release/release-loop.yaml +4 -2
- package/package.json +1 -1
- package/scripts/brand/check.ts +7 -5
- package/scripts/ci-typecheck-changed.ts +50 -18
- package/scripts/docs/generate-graph-anatomy.ts +315 -0
- package/scripts/expose-rubric-suggest.ts +18 -12
- 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/release-loop/checklist-gate-node.ts +53 -14
- package/scripts/release-loop/gate-node.ts +80 -14
- package/scripts/release-loop/vault-note-node.ts +240 -8
- package/scripts/release-story/draft.ts +57 -9
- package/scripts/rhythm/daily.ts +263 -76
- package/scripts/rhythm/weekly.ts +30 -9
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/src/agent-env/profile.ts +161 -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/briefing/brief-items.ts +121 -0
- package/src/claims/claims-render.ts +14 -5
- package/src/cli/agent-env-cli.ts +21 -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/pod-cli.ts +2 -0
- package/src/cli/release-cli.ts +61 -27
- package/src/cli/seat-requests-cli.ts +52 -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/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/harness/harness-cli-command.ts +111 -15
- package/src/harness/harness-incidents.ts +2 -0
- package/src/harness/harness-pod-dispatch.ts +100 -11
- package/src/harness/harness-queue.ts +380 -50
- package/src/harness/helper-repair.ts +87 -0
- package/src/harness/helper-scan.ts +16 -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/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 +62 -4
- package/src/hq/lease.ts +4 -4
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +31 -0
- package/src/index.ts +77 -7
- package/src/intake-plane/check.ts +63 -0
- package/src/intake-plane/digest.ts +37 -15
- 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/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
- package/src/launch-head/seat-goal-queue.ts +198 -0
- 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/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +11 -4
- package/src/loops/orchestrator/traffic.ts +117 -11
- package/src/loops/registry.ts +142 -61
- package/src/maturity/feature-maturity.ts +2 -1
- package/src/maturity/tui-slash-availability.ts +31 -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/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +6 -3
- package/src/release-loop/checklist.ts +13 -4
- package/src/release-loop/feature-store.ts +58 -31
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +86 -26
- 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 +126 -10
- package/src/self-dev/dev-pipeline.ts +8 -4
- package/src/self-dev/draft-sweep.ts +90 -10
- package/src/self-dev/draft-triage-rules.ts +25 -8
- 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/frozen-merges.ts +3 -2
- package/src/self-implement/goal-author.ts +43 -5
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/heal-intake.ts +120 -2
- 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 +187 -40
- 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 +2 -0
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +1 -1
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +4 -1
- 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-pool.ts +60 -8
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +147 -36
- package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
- package/src/telegram.ts +25 -4
- package/src/user-config.ts +29 -5
- 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/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/control/page-e6b84ab07d3567d5.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/loops/page-160089123a40dd91.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/release/page-e7e10cd3386222e5.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/page-341480ed8e3c1ea8.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/showroom/page-58043db78ba519ae.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-357750dfb91bf960.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/workflows/chat-ui/page-16bef3da05a900c3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-f8da5dc6f3752276.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 → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { listCoordEvents } from '../../context-bus/coord-events.js';
|
|
4
|
+
import { CardStore, cardIndexPath, type TaskCard } from '../../task-cards/card-store.js';
|
|
5
|
+
import { loadRunLedger, resolveFederatedRunLedgerDirectories, type RunLedgerEntry } from '../../self-implement/run-ledger.js';
|
|
6
|
+
import { listLoops } from '../../loops/registry.js';
|
|
7
|
+
import { listSubSeatCharters } from './sub-seat-charters.js';
|
|
8
|
+
import { SEATS } from './ops-seats.js';
|
|
9
|
+
import { jsonResponse } from './json-response.js';
|
|
10
|
+
|
|
11
|
+
export interface LoopEdge {
|
|
12
|
+
at: string;
|
|
13
|
+
kind: 'request' | 'decision' | 'report' | 'card' | 'run';
|
|
14
|
+
from: string;
|
|
15
|
+
to: string;
|
|
16
|
+
ref: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LoopEdgesDeps {
|
|
20
|
+
listCoord?: typeof listCoordEvents;
|
|
21
|
+
cardRoot?: string;
|
|
22
|
+
ledgerDir?: string;
|
|
23
|
+
listCards?: () => TaskCard[];
|
|
24
|
+
listRunStarts?: (since: string) => Array<{ runId: string; at: string; seat: string }>;
|
|
25
|
+
listLoopOwners?: () => Array<{ id: string; owner?: string | null }>;
|
|
26
|
+
seatIds?: () => string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ID = /^[a-zA-Z0-9_-]{1,128}$/;
|
|
30
|
+
const SURFACES = new Set(['telegram', 'pwa', 'linear', 'tui', 'discord', 'api', 'cli', 'voice', 'folder', 'wish']);
|
|
31
|
+
const KINDS: Record<string, LoopEdge['kind']> = { '요청': 'request', '결정': 'decision', '보고': 'report' };
|
|
32
|
+
|
|
33
|
+
function seat(value: unknown, seats: ReadonlySet<string>): value is string {
|
|
34
|
+
return typeof value === 'string' && seats.has(value);
|
|
35
|
+
}
|
|
36
|
+
function surface(value: unknown): value is string {
|
|
37
|
+
return typeof value === 'string' && SURFACES.has(value);
|
|
38
|
+
}
|
|
39
|
+
function object(value: unknown): Record<string, unknown> | null {
|
|
40
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
41
|
+
}
|
|
42
|
+
function parse(content: string): Record<string, unknown> | null {
|
|
43
|
+
try { return object(JSON.parse(content) as unknown); } catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
function inWindow(at: string, since: number, now: number): boolean {
|
|
46
|
+
const ms = Date.parse(at);
|
|
47
|
+
return Number.isFinite(ms) && ms >= since && ms <= now;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function cardEdges(card: TaskCard, since: number, now: number, seats: ReadonlySet<string>): LoopEdge[] {
|
|
51
|
+
if (!card.goalId.startsWith('wish:') || !ID.test(card.id)) return [];
|
|
52
|
+
const node = `card:${card.id}`;
|
|
53
|
+
const ref = card.id;
|
|
54
|
+
const edges: LoopEdge[] = [];
|
|
55
|
+
const intake = card.sections.find(s => s.key === 'intake:wish:0');
|
|
56
|
+
const intakeData = intake && parse(intake.content);
|
|
57
|
+
if (intake && surface(intakeData?.source) && inWindow(intake.createdAt, since, now)) {
|
|
58
|
+
edges.push({ at: intake.createdAt, kind: 'card', from: `surface:${intakeData.source}`, to: node, ref });
|
|
59
|
+
}
|
|
60
|
+
const split = card.sections.find(s => s.key === 'flow:split:0');
|
|
61
|
+
const splitData = split && parse(split.content);
|
|
62
|
+
const cells = Array.isArray(splitData?.cells) ? splitData.cells : [];
|
|
63
|
+
const owners = new Map<string, string>();
|
|
64
|
+
for (const item of cells) {
|
|
65
|
+
const cell = object(item);
|
|
66
|
+
if (typeof cell?.id === 'string' && ID.test(cell.id) && seat(cell.owner, seats)) owners.set(cell.id, cell.owner);
|
|
67
|
+
}
|
|
68
|
+
const placedOwners = new Set<string>();
|
|
69
|
+
for (const section of card.sections) {
|
|
70
|
+
if (!section.key.startsWith('flow:placed:')) continue;
|
|
71
|
+
const cellId = section.key.slice('flow:placed:'.length);
|
|
72
|
+
const saved = parse(section.content);
|
|
73
|
+
const placement = object(saved?.decision);
|
|
74
|
+
const owner = owners.get(cellId);
|
|
75
|
+
if (!owner || !placement || placement.id !== cellId || !ID.test(cellId)
|
|
76
|
+
|| typeof placement.version !== 'string' || !/^\d+\.\d+\.\d+$/.test(placement.version)) continue;
|
|
77
|
+
placedOwners.add(owner);
|
|
78
|
+
if (inWindow(section.createdAt, since, now)) {
|
|
79
|
+
const cellNode = `loop:${cellId}`;
|
|
80
|
+
edges.push({ at: section.createdAt, kind: 'card', from: node, to: cellNode, ref });
|
|
81
|
+
edges.push({ at: section.createdAt, kind: 'card', from: cellNode, to: owner, ref });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const reply = card.sections.find(s => s.key === 'intake:reply:0');
|
|
85
|
+
const replyData = reply && parse(reply.content);
|
|
86
|
+
// The destination section alone is not a send; flow:result records a completed card flow.
|
|
87
|
+
const result = card.sections.find(s => s.key === 'flow:result:0');
|
|
88
|
+
if (result && reply && surface(replyData?.surface) && inWindow(result.createdAt, since, now)) {
|
|
89
|
+
for (const owner of placedOwners) {
|
|
90
|
+
edges.push({ at: result.createdAt, kind: 'card', from: owner, to: `surface:${replyData.surface}`, ref });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return edges;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function runStarts(since: string, dir: string, owners: ReadonlyMap<string, string>, seats: ReadonlySet<string>): Array<{ runId: string; at: string; seat: string }> {
|
|
97
|
+
if (!existsSync(dir)) return [];
|
|
98
|
+
const result: Array<{ runId: string; at: string; seat: string }> = [];
|
|
99
|
+
let names: string[];
|
|
100
|
+
try { names = readdirSync(dir); } catch { return []; }
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
if (!/^run-[a-zA-Z0-9-]+\.jsonl$/.test(name)) continue;
|
|
103
|
+
const path = join(dir, name);
|
|
104
|
+
try { if (!statSync(path).isFile() || statSync(path).size > 5 * 1024 * 1024) continue; }
|
|
105
|
+
catch { continue; }
|
|
106
|
+
const runId = name.slice(0, -6);
|
|
107
|
+
let entries: RunLedgerEntry[] | null;
|
|
108
|
+
try { entries = loadRunLedger(runId, dir); } catch { continue; }
|
|
109
|
+
const start = entries?.find(entry => entry.event === 'start' && entry.runId === runId);
|
|
110
|
+
const feature = typeof start?.data.feature === 'string' ? start.data.feature : '';
|
|
111
|
+
const launchSeat = start?.data.launchSeat ?? start?.data.seat ?? owners.get(feature)
|
|
112
|
+
?? start?.data.originAgent ?? start?.data.controller;
|
|
113
|
+
if (start?.timestamp && start.timestamp >= since && seat(launchSeat, seats)) {
|
|
114
|
+
result.push({ runId, at: start.timestamp, seat: launchSeat });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** GET only; project an allowlist of fields from the three ledgers. Never serialize ledger records. */
|
|
121
|
+
export function handleLoopEdgesGet(req: Request, deps: LoopEdgesDeps = {}): Response {
|
|
122
|
+
const params = new URL(req.url).searchParams;
|
|
123
|
+
const rawSince = params.get('since');
|
|
124
|
+
const since = rawSince === null ? Date.now() - 60 * 60 * 1000 : Date.parse(rawSince);
|
|
125
|
+
const rawLimit = params.get('limit');
|
|
126
|
+
const limit = rawLimit === null ? 100 : Number(rawLimit);
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
if (rawSince !== null && !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d)$/.test(rawSince)
|
|
129
|
+
|| !Number.isFinite(since) || since > now || !Number.isSafeInteger(limit) || limit < 1 || limit > 500) {
|
|
130
|
+
return jsonResponse({ error: 'usage: GET /v1/loops/edges?since=<ISO>&limit=<1..500>' }, 400);
|
|
131
|
+
}
|
|
132
|
+
const sinceIso = new Date(since).toISOString();
|
|
133
|
+
const seats = new Set((deps.seatIds ?? (() => [...SEATS.map(s => s.seat), ...listSubSeatCharters().map(s => s.id)]))());
|
|
134
|
+
const edges: LoopEdge[] = [];
|
|
135
|
+
for (const event of (deps.listCoord ?? listCoordEvents)({ since: sinceIso, channelOnly: true })) {
|
|
136
|
+
if (!seat(event.refs.seat, seats) || !inWindow(event.at, since, now)) continue;
|
|
137
|
+
const kind = KINDS[event.refs.kind ?? ''];
|
|
138
|
+
if (!kind || !Array.isArray(event.refs.recipients)) continue;
|
|
139
|
+
const ref = typeof event.refs.url === 'string' && /^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/(?:pull|issues)\/\d+#issuecomment-\d+$/.test(event.refs.url)
|
|
140
|
+
? event.refs.url : event.id;
|
|
141
|
+
if (!ID.test(ref) && !ref.startsWith('https://github.com/')) continue;
|
|
142
|
+
for (const recipient of new Set(event.refs.recipients)) {
|
|
143
|
+
if (seat(recipient, seats)) edges.push({ at: event.at, kind, from: event.refs.seat, to: recipient, ref });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (deps.listCards) {
|
|
147
|
+
for (const card of deps.listCards()) edges.push(...cardEdges(card, since, now, seats));
|
|
148
|
+
} else {
|
|
149
|
+
const root = deps.cardRoot;
|
|
150
|
+
if (existsSync(cardIndexPath(root))) {
|
|
151
|
+
const store = new CardStore(root, true);
|
|
152
|
+
try { for (const card of store.listCards()) edges.push(...cardEdges(card, since, now, seats)); }
|
|
153
|
+
finally { store.close(); }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const readStarts = deps.listRunStarts ?? ((from: string) => {
|
|
157
|
+
const owners = new Map<string, string>();
|
|
158
|
+
try {
|
|
159
|
+
for (const entry of (deps.listLoopOwners ?? (() => listLoops()))()) {
|
|
160
|
+
if (seat(entry.owner, seats)) owners.set(entry.id, entry.owner);
|
|
161
|
+
}
|
|
162
|
+
} catch { /* A registry outage does not erase explicit launch-seat evidence. */ }
|
|
163
|
+
return (deps.ledgerDir ? [deps.ledgerDir] : resolveFederatedRunLedgerDirectories({ includeTest: false }))
|
|
164
|
+
.flatMap(dir => runStarts(from, dir, owners, seats));
|
|
165
|
+
});
|
|
166
|
+
for (const run of readStarts(sinceIso)) {
|
|
167
|
+
if (seat(run.seat, seats) && ID.test(run.runId) && inWindow(run.at, since, now)) {
|
|
168
|
+
edges.push({ at: run.at, kind: 'run', from: run.seat, to: `loop:${run.runId}`, ref: run.runId });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
edges.sort((a, b) => b.at.localeCompare(a.at) || a.kind.localeCompare(b.kind) || a.from.localeCompare(b.from) || a.to.localeCompare(b.to));
|
|
172
|
+
return jsonResponse({ edges: edges.slice(0, limit) });
|
|
173
|
+
}
|
|
@@ -15,6 +15,7 @@ import { tasksDbPath } from '../../task-orchestrator/paths.js';
|
|
|
15
15
|
import { jsonResponse } from './http-server.js';
|
|
16
16
|
import { checkAuth, type MetaApiOpts } from './meta-api.js';
|
|
17
17
|
import { listScheduleRuns } from '../../domains/schedule-runs.js';
|
|
18
|
+
import { listLoops } from '../../loops/registry.js';
|
|
18
19
|
|
|
19
20
|
export interface SchedulesReadDeps {
|
|
20
21
|
rows?: () => ScheduleRow[];
|
|
@@ -24,6 +25,7 @@ export interface SchedulesReadDeps {
|
|
|
24
25
|
logsPath?: () => string;
|
|
25
26
|
/** 발화 이력 원천(`schedule_runs`) — 기본은 schedules.db 를 읽기 전용으로 연다. */
|
|
26
27
|
schedulesDbPath?: () => string;
|
|
28
|
+
loopOwners?: () => Array<{ id: string; title: string; owner?: string | null; enabled: boolean; lastRun: { at: string; status: string } | null; jobs: Array<{ id: string }> }>;
|
|
27
29
|
now?: () => Date;
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -201,6 +203,17 @@ export function handleSchedulesList(req: Request, opts: MetaApiOpts, deps: Sched
|
|
|
201
203
|
if (!checkAuth(req, opts)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
202
204
|
const cards = readSchedulesInventory(deps);
|
|
203
205
|
const schedules = new URL(req.url).searchParams.get('includeOff') === '1' ? cards : cards.filter(c => c.state !== 'off');
|
|
206
|
+
if (new URL(req.url).searchParams.get('includeOwners') === '1') {
|
|
207
|
+
try {
|
|
208
|
+
const owners = (deps.loopOwners ?? (() => listLoops({ schedules: registryRows() })))().map(loop => ({ id: loop.id, title: redactSecretText(loop.title), owner: loop.owner ?? null,
|
|
209
|
+
enabled: loop.enabled, lastRun: loop.lastRun ? { at: loop.lastRun.at, status: loop.lastRun.status } : null,
|
|
210
|
+
jobs: loop.jobs.map(job => job.id) }));
|
|
211
|
+
return jsonResponse({ schedules, count: schedules.length, owners }, 200);
|
|
212
|
+
} catch {
|
|
213
|
+
// The ordinary inventory stays readable; an opted-in map must not mistake an outage for unowned loops.
|
|
214
|
+
return jsonResponse({ schedules, count: schedules.length, owners: null }, 200);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
204
217
|
return jsonResponse({ schedules, count: schedules.length }, 200);
|
|
205
218
|
}
|
|
206
219
|
|
|
@@ -6,6 +6,7 @@ import { resolveAttachmentPath } from '../../boot/attachment-store.js';
|
|
|
6
6
|
import { runGh } from '../../decisions/decision-cards.js';
|
|
7
7
|
import { openMsgStore, canonicalSeatId } from '../../msg/msg-store.js';
|
|
8
8
|
import { dispatchCeoTask, type CeoCommandDeps } from '../../seat-dispatch/ceo-commands.js';
|
|
9
|
+
import { appendSeatRequestRows, withSeatRequestLedgerLock } from '../../seat-dispatch/seat-request-ledger.js';
|
|
9
10
|
import { acknowledgeSeatAnswers, askSeat, deliverSeatAnswers, parseSeatAsk, type SeatAskDeps } from '../../seat-dispatch/seat-ask.js';
|
|
10
11
|
import { getUserConfig } from '../../user-config.js';
|
|
11
12
|
import { effectiveInstanceRoot } from '../../instance/resolve.js';
|
|
@@ -36,10 +37,12 @@ interface PendingRequest {
|
|
|
36
37
|
queuedAt: string;
|
|
37
38
|
ref: string;
|
|
38
39
|
attachments?: string[];
|
|
40
|
+
reason?: string;
|
|
41
|
+
closedAt?: string;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
interface RejectedRequest extends Omit<PendingRequest, 'status'> {
|
|
42
|
-
status: 'rejected';
|
|
45
|
+
status: 'rejected' | 'done';
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
type RecordEntry = (SeatRequestItem & { key: string }) | PendingRequest | RejectedRequest;
|
|
@@ -73,19 +76,7 @@ function file(root: string): string {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
function append(path: string, entry: RecordEntry): void {
|
|
76
|
-
|
|
77
|
-
// Atomically replace the journal so a failed write cannot leave a truncated JSON line.
|
|
78
|
-
const previous = existsSync(path) ? readFileSync(path, 'utf8') : '';
|
|
79
|
-
const temp = `${path}.${randomUUID()}.tmp`;
|
|
80
|
-
try {
|
|
81
|
-
writeFileSync(temp, `${previous}${JSON.stringify(entry)}\n`);
|
|
82
|
-
const fd = openSync(temp, 'r');
|
|
83
|
-
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
84
|
-
renameSync(temp, path);
|
|
85
|
-
} catch (error) {
|
|
86
|
-
try { unlinkSync(temp); } catch { /* no temp file to remove */ }
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
79
|
+
appendSeatRequestRows(path, [entry]);
|
|
89
80
|
}
|
|
90
81
|
|
|
91
82
|
function records(path: string): Map<string, RecordEntry> {
|
|
@@ -100,7 +91,7 @@ function records(path: string): Map<string, RecordEntry> {
|
|
|
100
91
|
if (!line) continue;
|
|
101
92
|
const entry = JSON.parse(line) as RecordEntry;
|
|
102
93
|
if (typeof entry.key !== 'string' || typeof entry.seat !== 'string' || typeof entry.text !== 'string'
|
|
103
|
-
|| typeof entry.queuedAt !== 'string' || !['pending', 'queued', 'rejected'].includes(entry.status)
|
|
94
|
+
|| typeof entry.queuedAt !== 'string' || !['pending', 'queued', 'rejected', 'done'].includes(entry.status)
|
|
104
95
|
|| (entry.status === 'queued' && typeof entry.receiptId !== 'string')
|
|
105
96
|
|| (entry.status !== 'queued' && typeof entry.ref !== 'string')) {
|
|
106
97
|
throw new Error('invalid seat request journal');
|
|
@@ -121,7 +112,7 @@ function records(path: string): Map<string, RecordEntry> {
|
|
|
121
112
|
throw new Error('invalid seat request outcome');
|
|
122
113
|
}
|
|
123
114
|
const current = entries.get(item.key);
|
|
124
|
-
if (current?.status === 'queued') continue;
|
|
115
|
+
if (current?.status === 'queued' || current?.status === 'done' || current?.closedAt) continue;
|
|
125
116
|
if (current?.status === 'pending') {
|
|
126
117
|
if (item.status === 'rejected' && current.ref !== item.ref) continue;
|
|
127
118
|
if (item.status === 'queued' && current.queuedAt !== item.queuedAt) continue;
|
|
@@ -135,27 +126,32 @@ function outcomeFile(path: string, key: string): string {
|
|
|
135
126
|
return join(dirname(path), 'outcomes', `${key}.json`);
|
|
136
127
|
}
|
|
137
128
|
|
|
138
|
-
function persistOutcome(path: string, entry: RecordEntry, write: typeof append):
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
} catch {
|
|
144
|
-
// The primary journal may be unavailable while the outcome is already committed
|
|
145
|
-
// to the intake queue. Keep a durable sidecar rather than a process-local receipt.
|
|
146
|
-
const target = outcomeFile(path, entry.key);
|
|
147
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
148
|
-
const temp = `${target}.${randomUUID()}.tmp`;
|
|
129
|
+
function persistOutcome(path: string, entry: RecordEntry, write: typeof append): RecordEntry {
|
|
130
|
+
return withSeatRequestLedgerLock(path, () => {
|
|
131
|
+
const current = records(path).get(entry.key);
|
|
132
|
+
if (current && ('closedAt' in current || current.status === 'done')) return current;
|
|
133
|
+
if (current?.status === 'queued') return current;
|
|
149
134
|
try {
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
135
|
+
write(path, entry);
|
|
136
|
+
const sidecar = outcomeFile(path, entry.key);
|
|
137
|
+
if (existsSync(sidecar)) unlinkSync(sidecar);
|
|
138
|
+
} catch {
|
|
139
|
+
// A committed external delivery still needs a durable outcome if the journal write failed.
|
|
140
|
+
const target = outcomeFile(path, entry.key);
|
|
141
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
142
|
+
const temp = `${target}.${randomUUID()}.tmp`;
|
|
143
|
+
try {
|
|
144
|
+
writeFileSync(temp, JSON.stringify(entry));
|
|
145
|
+
const fd = openSync(temp, 'r');
|
|
146
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
147
|
+
renameSync(temp, target);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
try { unlinkSync(temp); } catch { /* no temp file */ }
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
157
152
|
}
|
|
158
|
-
|
|
153
|
+
return entry;
|
|
154
|
+
});
|
|
159
155
|
}
|
|
160
156
|
|
|
161
157
|
function receipt(item: SeatRequestItem): Response {
|
|
@@ -298,13 +294,18 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
|
|
|
298
294
|
const gate = new Promise<void>((resolve) => { start = resolve; });
|
|
299
295
|
const task = (async (): Promise<Response> => {
|
|
300
296
|
await gate;
|
|
301
|
-
|
|
302
|
-
|
|
297
|
+
// A settled request answers from the journal before attachments are resolved — a retry
|
|
298
|
+
// must return its receipt even when the uploaded file is gone. The locked re-check below still guards races.
|
|
299
|
+
let settled: RecordEntry | undefined;
|
|
300
|
+
try { settled = records(path).get(key); }
|
|
303
301
|
catch { return unavailable(); }
|
|
304
|
-
if (
|
|
305
|
-
if (
|
|
306
|
-
|| JSON.stringify(
|
|
307
|
-
if (
|
|
302
|
+
if (settled) {
|
|
303
|
+
if (settled.seat !== seat.id || settled.text !== text
|
|
304
|
+
|| JSON.stringify(settled.attachments ?? []) !== JSON.stringify(attachmentIds)) return jsonResponse({ error: 'idempotency-conflict' }, 409);
|
|
305
|
+
if ('closedAt' in settled || settled.status === 'done') {
|
|
306
|
+
return jsonResponse({ error: 'request-closed', status: settled.status, reason: 'reason' in settled ? settled.reason : undefined }, 409);
|
|
307
|
+
}
|
|
308
|
+
if (settled.status === 'queued') return receipt(settled);
|
|
308
309
|
}
|
|
309
310
|
const attachments: Array<{ name: string; path: string }> = [];
|
|
310
311
|
for (const id of attachmentIds) {
|
|
@@ -316,18 +317,36 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
|
|
|
316
317
|
const metadata = (input.attachments as Array<{ name?: unknown }>)[attachments.length];
|
|
317
318
|
attachments.push({ name: typeof metadata?.name === 'string' && metadata.name.trim() ? metadata.name : basename(resolved), path: resolved });
|
|
318
319
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
320
|
+
let initial: { intent?: PendingRequest; response?: Response };
|
|
321
|
+
try {
|
|
322
|
+
initial = withSeatRequestLedgerLock(path, () => {
|
|
323
|
+
const existing = records(path).get(key);
|
|
324
|
+
if (existing) {
|
|
325
|
+
if (existing.seat !== seat.id || existing.text !== text
|
|
326
|
+
|| JSON.stringify(existing.attachments ?? []) !== JSON.stringify(attachmentIds)) {
|
|
327
|
+
return { response: jsonResponse({ error: 'idempotency-conflict' }, 409) };
|
|
328
|
+
}
|
|
329
|
+
if ('closedAt' in existing || existing.status === 'done') {
|
|
330
|
+
return { response: jsonResponse({ error: 'request-closed', status: existing.status,
|
|
331
|
+
reason: 'reason' in existing ? existing.reason : undefined }, 409) };
|
|
332
|
+
}
|
|
333
|
+
if (existing.status === 'queued') return { response: receipt(existing) };
|
|
328
334
|
}
|
|
329
|
-
|
|
330
|
-
|
|
335
|
+
const intent: PendingRequest = existing?.status === 'pending'
|
|
336
|
+
? existing : { key, seat: seat.id, text, queuedAt: now, status: 'pending', ref: `pwa:${randomUUID()}`,
|
|
337
|
+
...(attachmentIds.length ? { attachments: attachmentIds } : {}) };
|
|
338
|
+
if (existing?.status !== 'pending') {
|
|
339
|
+
(deps.append ?? append)(path, intent);
|
|
340
|
+
if (existing?.status === 'rejected') {
|
|
341
|
+
const sidecar = outcomeFile(path, key);
|
|
342
|
+
if (existsSync(sidecar)) unlinkSync(sidecar);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { intent };
|
|
346
|
+
});
|
|
347
|
+
} catch { return unavailable(); }
|
|
348
|
+
if (initial.response) return initial.response;
|
|
349
|
+
const intent = initial.intent!;
|
|
331
350
|
const ceoSeat = canonicalSeatId(seat.id);
|
|
332
351
|
if (['OP', 'TC', 'MK', 'UX'].includes(ceoSeat)) {
|
|
333
352
|
let result: Awaited<ReturnType<typeof dispatchCeoTask>>;
|
|
@@ -342,10 +361,12 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
|
|
|
342
361
|
...(attachmentIds.length ? { attachments: attachmentIds } : {}), channel: result.channel,
|
|
343
362
|
...(result.channelError ? { channelError: result.channelError } : {}),
|
|
344
363
|
};
|
|
345
|
-
|
|
364
|
+
let persisted: RecordEntry;
|
|
365
|
+
try { persisted = persistOutcome(path, item, deps.append ?? append); }
|
|
346
366
|
catch { return unavailable(); }
|
|
367
|
+
if (persisted.status !== 'queued') return jsonResponse({ error: 'request-closed', status: persisted.status, reason: persisted.reason }, 409);
|
|
347
368
|
publishInsideEvent({ kind: 'seat', seat: item.seat, receiptId: item.receiptId, status: item.status, ts: item.queuedAt });
|
|
348
|
-
return receipt(
|
|
369
|
+
return receipt(persisted);
|
|
349
370
|
}
|
|
350
371
|
let result: Awaited<ReturnType<typeof submitIntakeWork>>;
|
|
351
372
|
try {
|
|
@@ -363,11 +384,13 @@ export async function handleSeatRequests(req: Request, deps: SeatRequestsDeps =
|
|
|
363
384
|
}
|
|
364
385
|
const item: RecordEntry & SeatRequestItem = { key, receiptId: result.acceptanceId, seat: seat.id, text, queuedAt: intent.queuedAt,
|
|
365
386
|
status: 'queued', ...(attachmentIds.length ? { attachments: attachmentIds } : {}) };
|
|
366
|
-
|
|
387
|
+
let persisted: RecordEntry;
|
|
388
|
+
try { persisted = persistOutcome(path, item, deps.append ?? append); }
|
|
367
389
|
catch { return unavailable(); }
|
|
390
|
+
if (persisted.status !== 'queued') return jsonResponse({ error: 'request-closed', status: persisted.status, reason: persisted.reason }, 409);
|
|
368
391
|
debug.log('seat-address.pwa', 'enqueued', { seat: seat.id, receiptId: item.receiptId });
|
|
369
392
|
publishInsideEvent({ kind: 'seat', seat: item.seat, receiptId: item.receiptId, status: item.status, ts: item.queuedAt });
|
|
370
|
-
return receipt(
|
|
393
|
+
return receipt(persisted);
|
|
371
394
|
})();
|
|
372
395
|
inFlight.set(lock, { payload, task });
|
|
373
396
|
start();
|
|
@@ -99,6 +99,8 @@ export interface EnhanceOpts {
|
|
|
99
99
|
reasoningEffort?: 'low' | 'medium' | 'high';
|
|
100
100
|
/** 산출물 유형 힌트(선택) — 예: 'PPT 발표덱', 'PLAN 문서'. 제약 문구 조정. */
|
|
101
101
|
deliverableHint?: string;
|
|
102
|
+
/** Optional usage observer for the caller's authoring phase; missing usage stays unknown. */
|
|
103
|
+
onUsage?: (usage: LLMUsage) => void;
|
|
102
104
|
/** 인핸싱 끄기(순수 verbatim). true 면 원문만 fenced 로 감싸 반환. */
|
|
103
105
|
disabled?: boolean;
|
|
104
106
|
/** Ask 원문 밖 스캐폴드에만 렌더할, 저작기가 관측한 디렉터리 실물 수. */
|
|
@@ -368,7 +370,10 @@ export async function enhancePrompt(raw: string, opts: EnhanceOpts = {}): Promis
|
|
|
368
370
|
const rawOut = await streamLLM(messages, () => {}, {
|
|
369
371
|
model,
|
|
370
372
|
reasoningEffort: opts.reasoningEffort ?? 'high',
|
|
371
|
-
onUsage: (usage) =>
|
|
373
|
+
onUsage: (usage) => {
|
|
374
|
+
logLlmUsage(model, usage);
|
|
375
|
+
try { opts.onUsage?.(usage); } catch { /* usage observation must not change enhancement */ }
|
|
376
|
+
},
|
|
372
377
|
});
|
|
373
378
|
const parsed = parseEnhanceJson(rawOut);
|
|
374
379
|
if (parsed) {
|
|
@@ -15,10 +15,12 @@ export function selectScheduledCut(schedules: readonly ReleaseSchedule[], now: D
|
|
|
15
15
|
.sort((a, b) => Date.parse(a.cutAt) - Date.parse(b.cutAt) || a.version.localeCompare(b.version))[0] ?? null;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export type AutoStartDeferral = { deferred: true; reason: Exclude<ReleaseReadiness, { ready: true }>['reason'] | 'frozen' };
|
|
19
|
+
|
|
18
20
|
export type AutoStartResult =
|
|
19
21
|
| { status: 'no-cut' }
|
|
20
22
|
| { status: 'planned'; version: string; cutAt: string }
|
|
21
|
-
| { status: 'skipped'; version: string; reason:
|
|
23
|
+
| { status: 'skipped'; version: string; reason: AutoStartDeferral['reason'] }
|
|
22
24
|
| { status: 'started'; version: string; cutAt: string };
|
|
23
25
|
|
|
24
26
|
export interface AutoStartOptions {
|
|
@@ -28,7 +30,7 @@ export interface AutoStartOptions {
|
|
|
28
30
|
ledgerRoot?: string;
|
|
29
31
|
schedules?: (root: string) => ReleaseSchedule[];
|
|
30
32
|
readiness?: (version: string, ledgerRoot: string, now: Date) => ReleaseReadiness;
|
|
31
|
-
launch?: (version: string) => void | Promise<void>;
|
|
33
|
+
launch?: (version: string) => void | AutoStartDeferral | Promise<void | AutoStartDeferral>;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/** Select from the release schedule; the existing `release run --if-ready` owns the launch-time lock and guards. */
|
|
@@ -43,6 +45,7 @@ export async function autoStartScheduledRelease(options: AutoStartOptions): Prom
|
|
|
43
45
|
const readiness = (options.readiness ?? ((v, ledgerRoot, at) => releaseReadiness(v, { ledgerRoot, now: () => at })))(version, root, now);
|
|
44
46
|
if (!readiness.ready) return { status: 'skipped', version, reason: readiness.reason };
|
|
45
47
|
if (!options.launch) throw new Error('auto-start apply requires a launch callback');
|
|
46
|
-
await options.launch(version);
|
|
48
|
+
const launched = await options.launch(version);
|
|
49
|
+
if (launched?.deferred) return { status: 'skipped', version, reason: launched.reason };
|
|
47
50
|
return { status: 'started', version, cutAt };
|
|
48
51
|
}
|
|
@@ -16,6 +16,8 @@ export interface ChecklistItem {
|
|
|
16
16
|
priority?: 'P0' | 'P1' | 'P2';
|
|
17
17
|
predecessors?: string[];
|
|
18
18
|
deadlineVersion?: string;
|
|
19
|
+
ceoMinutes?: number;
|
|
20
|
+
ceoDate?: string;
|
|
19
21
|
evidence?: string;
|
|
20
22
|
disposition?: ChecklistDisposition;
|
|
21
23
|
kind?: ChecklistKind;
|
|
@@ -96,7 +98,12 @@ function change(data: Checklist, id: string, field: string, from: unknown, to: u
|
|
|
96
98
|
debug.log('release-loop.checklist', 'change', { version: data.version, id, field, from: from ?? null, to: to ?? null, by });
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
export function
|
|
101
|
+
export function validateCeoLoad(input: { ceoMinutes?: number; ceoDate?: string }): void {
|
|
102
|
+
if (input.ceoMinutes !== undefined && (!Number.isSafeInteger(input.ceoMinutes) || input.ceoMinutes < 0)) throw new CliUserError('대표 손 분량은 0 이상의 정수 분이어야 한다');
|
|
103
|
+
if (input.ceoDate !== undefined && (!/^\d{4}-\d{2}-\d{2}$/.test(input.ceoDate) || !Number.isFinite(Date.parse(`${input.ceoDate}T00:00:00Z`)) || new Date(`${input.ceoDate}T00:00:00Z`).toISOString().slice(0, 10) !== input.ceoDate)) throw new CliUserError('대표 손 날짜는 YYYY-MM-DD 이어야 한다');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function addItem(v: string, input: { id: string; title: string; owner?: string; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }, options: { allowDuplicateId?: boolean } = {}): Checklist {
|
|
100
107
|
if (!input.id.trim()) throw new CliUserError('칸 id 가 비었다');
|
|
101
108
|
if (!input.title.trim()) throw new CliUserError('칸 제목이 비었다');
|
|
102
109
|
if (input.kind !== undefined && input.kind !== 'screen') throw new CliUserError(`잘못된 종류: ${input.kind}`, 'screen');
|
|
@@ -104,6 +111,7 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
|
|
|
104
111
|
if (input.priority !== undefined && !['P0', 'P1', 'P2'].includes(input.priority)) throw new CliUserError(`잘못된 우선순위: ${input.priority}`);
|
|
105
112
|
if (input.predecessors !== undefined && (!Array.isArray(input.predecessors) || input.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === input.id))) throw new CliUserError('잘못된 선행 칸');
|
|
106
113
|
if (input.deadlineVersion !== undefined) store.validateVersion(input.deadlineVersion);
|
|
114
|
+
validateCeoLoad(input);
|
|
107
115
|
return mutate(v, (data, otherItems) => {
|
|
108
116
|
if (data.items.some((item) => item.id === input.id)) throw new CliUserError(`이미 있는 칸: ${input.id}`, 'set <id> 로 고친다');
|
|
109
117
|
const collisions = otherItems(input.id);
|
|
@@ -114,14 +122,14 @@ export function addItem(v: string, input: { id: string; title: string; owner?: s
|
|
|
114
122
|
}
|
|
115
123
|
const by = process.env.ELANOUS_TRACK || 'cli';
|
|
116
124
|
const at = new Date().toISOString();
|
|
117
|
-
const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), updatedAt: at, updatedBy: by };
|
|
125
|
+
const item: ChecklistItem = { id: input.id, title: input.title, status: 'yellow', ...(input.owner !== undefined ? { owner: input.owner } : {}), ...(input.kind !== undefined ? { kind: input.kind } : {}), ...(input.priority !== undefined ? { priority: input.priority } : {}), ...(input.predecessors !== undefined ? { predecessors: input.predecessors } : {}), ...(input.deadlineVersion !== undefined ? { deadlineVersion: input.deadlineVersion } : {}), ...(input.ceoMinutes !== undefined ? { ceoMinutes: input.ceoMinutes } : {}), ...(input.ceoDate !== undefined ? { ceoDate: input.ceoDate } : {}), updatedAt: at, updatedBy: by };
|
|
118
126
|
data.items.push(item);
|
|
119
127
|
change(data, item.id, 'add', null, item, by, at);
|
|
120
128
|
return true;
|
|
121
129
|
});
|
|
122
130
|
}
|
|
123
131
|
|
|
124
|
-
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string }, by: string): Checklist {
|
|
132
|
+
export function setItem(v: string, id: string, patch: { status?: ChecklistStatus; evidence?: string; owner?: string; disposition?: ChecklistDisposition; kind?: ChecklistKind; priority?: ChecklistItem['priority']; predecessors?: string[]; deadlineVersion?: string; ceoMinutes?: number; ceoDate?: string }, by: string): Checklist {
|
|
125
133
|
return mutate(v, (data) => {
|
|
126
134
|
const item = data.items.find((i) => i.id === id);
|
|
127
135
|
if (!item) throw new CliUserError(`없는 칸: ${id}`, 'list 로 칸 목록을 본다');
|
|
@@ -131,13 +139,14 @@ export function setItem(v: string, id: string, patch: { status?: ChecklistStatus
|
|
|
131
139
|
if (patch.priority !== undefined && !['P0', 'P1', 'P2'].includes(patch.priority)) throw new CliUserError(`잘못된 우선순위: ${patch.priority}`);
|
|
132
140
|
if (patch.predecessors !== undefined && (!Array.isArray(patch.predecessors) || patch.predecessors.some((p) => typeof p !== 'string' || !p.trim() || p === id))) throw new CliUserError('잘못된 선행 칸');
|
|
133
141
|
if (patch.deadlineVersion !== undefined) store.validateVersion(patch.deadlineVersion);
|
|
142
|
+
validateCeoLoad(patch);
|
|
134
143
|
if (patch.owner !== undefined) {
|
|
135
144
|
parseOwner(patch.owner);
|
|
136
145
|
if (item.owner && patch.owner !== item.owner) {
|
|
137
146
|
throw new CliUserError(`지금 주인: ${item.owner} — --force 로만 바꾼다`, 'release checklist claim <id> --by <자리> --force');
|
|
138
147
|
}
|
|
139
148
|
}
|
|
140
|
-
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field]));
|
|
149
|
+
const fields = (['evidence', 'owner', 'status', 'disposition', 'kind', 'priority', 'predecessors', 'deadlineVersion', 'ceoMinutes', 'ceoDate'] as const).filter((field) => patch[field] !== undefined && JSON.stringify(patch[field]) !== JSON.stringify(item[field]));
|
|
141
150
|
if (fields.length === 0) return false;
|
|
142
151
|
const at = new Date().toISOString();
|
|
143
152
|
for (const field of fields) {
|