elanous 0.2.13 → 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/{7334.a6679d9168f37aa8.js → 1268.0840ff91e083fcbd.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1307-33ffcc49ab45123b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/1826-5d279da64bbbc2de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{9656-c1540088f5859c80.js → 1844-9ec4b1d12f6e80eb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/1866-e7a6999f25548b30.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{390.c373e31b3b9d0c41.js → 1948.15f356b15a625597.js} +2 -2
- package/apps/pwa/out/_next/static/chunks/211-9c9090c59e2c5907.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2196-4700653d12a44b40.js +1 -0
- package/apps/pwa/out/_next/static/chunks/224-798346143007ff65.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2699-63b73fa4d3bcf6ff.js +1 -0
- package/apps/pwa/out/_next/static/chunks/2957-96ffab3d521fb08f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/311.e685738f92e6e2ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3194-807b212fb8eae065.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3324-d1d2979821dfa845.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3558-bbb5ab553a0471d3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/3916-3d02dd1fc257dd3c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4144-a0041c8dc3be57ac.js +1 -0
- package/apps/pwa/out/_next/static/chunks/4554-3565eea1f1495ba7.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{4685-c3c1c625b71accd1.js → 4614-21403aac730d4586.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{8039-df92e08fede5de8f.js → 4795-f372490b178583fc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/5135-44008fe8b1f2aa41.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5233-bf28bd6bd7a32099.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5360-be1f1d8f9fcc9755.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5977-c952712c87a1f757.js +1 -0
- package/apps/pwa/out/_next/static/chunks/5994-ae92c0de046ab9ba.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1452.93700483b9d4fc61.js → 6110.1297d4093fdfbb0d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/6158.9c0449a9a8d0064b.js +2 -0
- package/apps/pwa/out/_next/static/chunks/6877-7c6208705c284124.js +1 -0
- package/apps/pwa/out/_next/static/chunks/726.83532d69452ee24b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7290-f09c928a06c52e2d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{1829-b453a1b22d3999bb.js → 7327-2d69fb321ba73389.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/7395-164e238f471b5d7b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/7787-02a1c0b04e25e811.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8060-cc008cc6fedfa818.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8085-d065d0393c1760f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8682-7061dd4fa7bd62ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8827-e2f5b09a8d37e83b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8868-10a3a4440fb0ccd8.js +25 -0
- package/apps/pwa/out/_next/static/chunks/{8421-1c4acfb1395a0edf.js → 8949-ecb4128a0858b911.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/899-46946fe8363e1262.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9070.067c106528ec1ae6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/9103-7a3ae7f60af3b99f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{3869-3cd46526879f454c.js → 9247-29d889b809288e65.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/935.4ddaa99fc1d1126c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{79f8be9e-957d68913b590b08.js → a9f2b54d-7865aba0f4d2ee49.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-0e2c57fe6add4b2a.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e05a77df7e005b04.js +1 -0
- 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-b8c49f6b4fa2df91.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-96398e364fbd4548.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-976813c91d7394ea.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/page-28b9448091776c06.js +1 -0
- 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-60c7a8e1a4fe62ce.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/page-2a52b316cb143d9c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/field/page-c3ce483c8d5db370.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/inside/page-13c7b53e0c2519a9.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/page-96f074f6538e8a2c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/layout-e97be5334d83a9f5.js +1 -0
- 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-955ec2da3be4c560.js +1 -0
- 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-c1343f9499d86f3f.js +1 -0
- 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-d23b12bb92242558.js +1 -0
- 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-e2ee2896811e98d0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-83b19dae8619de05.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-5efbf81124c474ae.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-85f1a1fc0a82738c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-da6c08aae0f92768.js +1 -0
- 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-153c2b3bcc89e8de.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-87a11e6a05f15199.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-c1a96669158471ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/setup/page-7d4182964407c09c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/share/page-0d6d23b40263a7ea.js +1 -0
- 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-63d14a4d8fe1e1c1.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-73735b64bfe1ae67.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/vault/page-8fcec4c9ea2e7f70.js +1 -0
- 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-9817c7f9007466cc.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-97f9f43172a3a90e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/{5cd7e3a9-404fd6b8d0ecd44c.js → f6231284-45726550aba08fca.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/{f48170cc-d05d28bf87a43bf4.js → f6b7c9ba.64a895b8b2fedbdc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/framework-5c169757551134ed.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-59fe95fed40c7a01.js +1 -0
- package/apps/pwa/out/_next/static/chunks/main-app-4ca74dbbee7057c6.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_app-13a0a886df4d97a1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/pages/_error-6af8591cd3a3f038.js +1 -0
- package/apps/pwa/out/_next/static/chunks/webpack-539cde99554be20f.js +1 -0
- 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 +23 -23
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +23 -23
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +23 -23
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +22 -22
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +22 -22
- 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 +22 -22
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +23 -23
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +23 -23
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +23 -23
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +23 -23
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +23 -23
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +23 -23
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +22 -22
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +22 -22
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +23 -23
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +23 -23
- package/apps/pwa/out/loops/index.html +1 -0
- package/apps/pwa/out/loops/index.txt +30 -0
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +22 -22
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +22 -22
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +23 -23
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +23 -23
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +22 -22
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +22 -22
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +22 -22
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +22 -22
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +23 -23
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +23 -23
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +22 -22
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +23 -23
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +23 -23
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +23 -23
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +23 -23
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +23 -23
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +22 -22
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +23 -23
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +23 -23
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +23 -23
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +23 -23
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +23 -23
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +23 -23
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +23 -23
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +23 -23
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +23 -23
- package/graphs/heal/heal-loop.yaml +1 -1
- package/graphs/implement-loop.yaml +1 -1
- package/graphs/orchestrator/orchestrator.yaml +59 -0
- package/graphs/orchestrator/recipes.yaml +18 -0
- package/graphs/release/recipes.yaml +6 -0
- package/graphs/release/release-loop.yaml +6 -1
- package/graphs/rhythm/daily.yaml +38 -0
- package/graphs/rhythm/recipes.yaml +18 -0
- package/graphs/rhythm/weekly.yaml +39 -0
- package/graphs/video/recipes.yaml +3 -0
- package/graphs/video/youtube-summary.yaml +17 -0
- package/package.json +1 -1
- package/scripts/audit-test-state-writes.ts +72 -17
- package/scripts/brand/check.ts +188 -0
- package/scripts/ci-typecheck-changed.ts +50 -18
- package/scripts/coord-post.sh +11 -1
- package/scripts/docs/generate-graph-anatomy.ts +315 -0
- package/scripts/docs-cli-check.ts +23 -6
- package/scripts/expose-rubric-check.ts +118 -0
- package/scripts/expose-rubric-suggest.ts +169 -0
- package/scripts/heal/run-role.ts +14 -0
- package/scripts/hq/standby-verify.ts +11 -7
- package/scripts/lecture-note/node.ts +31 -11
- package/scripts/lessons/handbook-page.ts +112 -0
- 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 +89 -14
- package/scripts/release-loop/publish-node.ts +14 -1
- package/scripts/release-loop/unattended-release.ts +23 -1
- package/scripts/release-loop/vault-note-node.ts +428 -0
- package/scripts/release-story/draft.ts +220 -0
- package/scripts/rhythm/daily.ts +570 -0
- package/scripts/rhythm/weekly.ts +315 -0
- package/scripts/skills/public-check.ts +169 -0
- package/scripts/smart-merge/resync-shadow.ts +147 -0
- package/scripts/test-deterministic.ts +4 -8
- package/scripts/test-diet/effectiveness.ts +195 -0
- package/scripts/test-diet/lib.ts +16 -8
- package/scripts/test-diet/node.ts +77 -9
- package/scripts/ux/demo-rehearsal.ts +2 -2
- package/scripts/ux/lib/rehearsal-verdict.ts +1 -0
- package/scripts/video/youtube-summary.ts +133 -0
- package/skills/diagram-master/SKILL.md +42 -49
- package/skills/diagram-master/references/pyproject.toml +4 -1
- package/skills/diagram-master/references/routing-guide.md +4 -2
- package/skills/grill-me/SKILL.md +1 -0
- package/skills/youtube-master/SKILL.md +13 -13
- package/skills/youtube-master/package-lock.json +0 -545
- package/skills/youtube-master/package.json +2 -5
- package/skills/youtube-master/scripts/main.ts +2 -2
- package/skills/youtube-master/scripts/transcribe-local.ts +1 -1
- package/skills/youtube-master/src/util.ts +30 -7
- package/src/agent-env/profile.ts +161 -0
- package/src/agent-mission/driver.ts +3 -3
- 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 +73 -10
- package/src/autopilot/build/merge-intent.ts +53 -0
- package/src/briefing/brief-items.ts +121 -0
- package/src/chat/index.ts +2 -0
- package/src/claims/claims-ledger.ts +232 -0
- package/src/claims/claims-render.ts +166 -0
- package/src/cli/agent-env-cli.ts +21 -0
- package/src/cli/claims-cli.ts +94 -0
- package/src/cli/decisions-cli.ts +44 -3
- package/src/cli/docs-cli.ts +2 -0
- package/src/cli/doctor-cli.ts +23 -1
- package/src/cli/doctor-node-modules.ts +147 -0
- package/src/cli/flow-cli.ts +11 -0
- package/src/cli/freeze-cli.ts +45 -0
- package/src/cli/intake-cli.ts +46 -17
- package/src/cli/lesson-cli.ts +108 -0
- package/src/cli/logs-cli.ts +104 -2
- package/src/cli/logs-model-input.ts +131 -0
- package/src/cli/pod-cli.ts +2 -0
- package/src/cli/pr-cli.ts +29 -1
- package/src/cli/private-ledger-commands.ts +54 -4
- package/src/cli/pty-drive-verdict.ts +11 -2
- package/src/cli/release-cli.ts +190 -33
- package/src/cli/rfc-status.ts +91 -0
- package/src/cli/seat-requests-cli.ts +52 -0
- package/src/connectors/linear.ts +5 -3
- package/src/context-bus/condense.ts +15 -20
- package/src/context-bus/context-now-public.ts +96 -0
- package/src/context-bus/context-now-surfaces.ts +55 -2
- package/src/context-bus/context-now.ts +6 -3
- package/src/context-bus/coord-events.ts +6 -4
- package/src/context-bus/long-term-memory.ts +92 -11
- package/src/dashboard/index.ts +75 -22
- package/src/dashboard/slash-runtime/chat-persona.ts +66 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +99 -0
- package/src/dashboard/slash-runtime/loops-table.ts +77 -0
- package/src/decisions/decision-cards.ts +33 -7
- package/src/decisions/decision-ledger.ts +40 -1
- 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/sprint21-runtime.ts +16 -0
- package/src/discord-self-message.ts +8 -0
- package/src/discord-slash-wire.ts +31 -2
- 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/domains/outbound-alert.ts +1 -1
- package/src/elanous-config-dir.ts +29 -4
- package/src/execution-loop/launch-gate.ts +29 -14
- package/src/flow/cards.ts +239 -0
- package/src/graph-runner/graph-tick.ts +4 -0
- package/src/graph-runner/runner.ts +30 -5
- package/src/harness/harness-cli-command.ts +259 -38
- package/src/harness/harness-incidents.ts +45 -12
- package/src/harness/harness-pod-dispatch.ts +102 -11
- package/src/harness/harness-queue-child.ts +3 -3
- package/src/harness/harness-queue.ts +583 -65
- package/src/harness/helper-repair.ts +87 -0
- package/src/harness/helper-scan.ts +151 -0
- 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/model-input-observation.ts +37 -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 +63 -5
- package/src/hq/lease.ts +4 -4
- package/src/hq/move-plan.ts +126 -0
- package/src/hq/promote.ts +332 -0
- package/src/index.ts +159 -27
- 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/linear-wish.ts +39 -11
- package/src/intake-plane/route.ts +145 -1
- package/src/intake-plane/wish-benchmark-ledger.ts +129 -0
- package/src/intake-plane/wish-card.ts +53 -2
- package/src/intake-plane/wish-folder.ts +5 -2
- package/src/intake-plane/wish-reply.ts +72 -0
- package/src/launch-head/seat-goal-queue-lock.fixture.ts +15 -0
- package/src/launch-head/seat-goal-queue.ts +198 -0
- package/src/lessons/lesson-import.ts +106 -0
- package/src/lessons/lesson-ledger.ts +206 -0
- package/src/loops/activity.ts +203 -0
- package/src/loops/budget.ts +63 -0
- package/src/loops/checker.ts +5 -3
- package/src/loops/contract/examples/orchestrator.loop.yaml +1 -1
- package/src/loops/loop-cli.ts +36 -4
- package/src/loops/neighbors.ts +40 -0
- package/src/loops/observe.ts +10 -0
- package/src/loops/orchestrator/author-depth.ts +220 -0
- package/src/loops/orchestrator/author-input-check.ts +82 -0
- package/src/loops/orchestrator/author-ledger.ts +232 -0
- package/src/loops/orchestrator/finish-rate.ts +175 -0
- package/src/loops/orchestrator/overlap.ts +51 -12
- package/src/loops/orchestrator/resync-shadow.ts +227 -0
- package/src/loops/orchestrator/seat-share.ts +56 -0
- package/src/loops/orchestrator/tick.ts +309 -0
- package/src/loops/orchestrator/traffic.ts +134 -13
- package/src/loops/registry.ts +535 -9
- package/src/loops/status-rows.ts +118 -0
- package/src/loops/verdict.ts +17 -5
- package/src/maturity/bot-slash-catalog.ts +66 -0
- package/src/maturity/feature-maturity.ts +9 -5
- package/src/maturity/tui-slash-availability.ts +31 -0
- package/src/mss/logging/log-store.ts +35 -4
- package/src/nexus/api/dashboard.ts +6 -1
- package/src/nexus/api/http-server.ts +33 -3
- package/src/nexus/api/loop-edges.ts +173 -0
- package/src/nexus/api/merge-approvals-gate.ts +1 -1
- package/src/nexus/api/schedules-read.ts +18 -1
- package/src/nexus/api/seat-requests.ts +79 -56
- package/src/nexus/api/task-cards-api.ts +44 -5
- package/src/nexus/api/workflows.ts +2 -1
- package/src/prompt-enhance/enhance.ts +6 -1
- package/src/release-loop/auto-start.ts +51 -0
- package/src/release-loop/checklist.ts +22 -4
- package/src/release-loop/feature-store.ts +71 -32
- package/src/release-loop/landing-freeze.ts +121 -0
- package/src/release-loop/merged-pr-checklist.ts +125 -0
- package/src/release-loop/placement.ts +302 -0
- package/src/release-loop/release-schedule.ts +3 -1
- package/src/seat-dispatch/ceo-commands.ts +2 -2
- package/src/seat-dispatch/seat-ask.ts +16 -3
- package/src/seat-dispatch/seat-questions.ts +58 -0
- 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 +531 -40
- package/src/self-dev/dev-pipeline.ts +14 -6
- package/src/self-dev/draft-sweep.ts +162 -6
- 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-store.ts +73 -1
- package/src/self-dev/run-supervisor.ts +57 -2
- package/src/self-dev/self-orchestrate-runtime.ts +5 -1
- package/src/self-implement/budget-gate.ts +29 -0
- package/src/self-implement/expose-gate.ts +47 -0
- package/src/self-implement/frozen-merges.ts +295 -0
- package/src/self-implement/gate-cli.ts +10 -4
- package/src/self-implement/gate-scope.ts +46 -9
- package/src/self-implement/goal-archive.ts +222 -0
- package/src/self-implement/goal-author.ts +43 -5
- package/src/self-implement/goal-clarification-escalation.ts +5 -0
- package/src/self-implement/goal-lookup.ts +147 -0
- package/src/self-implement/heal-intake.ts +197 -0
- package/src/self-implement/host-regate.ts +32 -2
- package/src/self-implement/l8-merge-queue.ts +132 -0
- package/src/self-implement/orchestrator.ts +299 -48
- package/src/self-implement/pipeline-shape.ts +1 -1
- package/src/self-implement/revert-guard.ts +97 -0
- package/src/self-implement/run-ledger.ts +3 -1
- package/src/self-implement/run-status-mapping.ts +2 -0
- package/src/self-implement/seams.ts +29 -9
- package/src/self-implement/self-implement-cli.ts +1 -0
- package/src/self-implement/speed-claims.ts +338 -0
- package/src/session/index.ts +2 -0
- package/src/steward/triage.ts +74 -3
- package/src/steward/working-backwards.ts +4 -1
- package/src/task-cards/card-cli.ts +10 -2
- package/src/task-cards/card-store.ts +10 -6
- 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-lease.ts +5 -2
- package/src/task-orchestrator/surfaces/pod-ledger-collect.ts +46 -6
- package/src/task-orchestrator/surfaces/pod-pool.ts +60 -8
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +224 -49
- package/src/task-orchestrator/surfaces/self-implement.ts +5 -1
- package/src/telegram-commands.ts +27 -3
- package/src/telegram-loops-command.ts +44 -0
- package/src/telegram.ts +25 -4
- package/src/user-config.ts +146 -18
- package/src/version/packed-revision.json +1 -1
- package/src/workflow-runtime/nodes/bash.ts +9 -1
- package/src/workflow-runtime/storage.ts +31 -13
- package/apps/pwa/out/_next/static/D51l8jUBplJrIpWvENsDK/_buildManifest.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1159-0c5ef8bc21cb6afb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/1394-931c9827067b2276.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2176-c379e3ebce874151.js +0 -1
- package/apps/pwa/out/_next/static/chunks/2426.8c6ee0c668d53def.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3170-7ec60a56fa0e4728.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3278-ae6ef084769f6195.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3454-988a53dac60a1763.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3638-aa80b4641ea439db.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3735-fe3402553ac1c9c6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/3992-fe5c06d6b31f8b25.js +0 -25
- package/apps/pwa/out/_next/static/chunks/4215-c9d91dd644ec1719.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4508-5d0161d1e724d735.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4733-623a79345b171e1c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/4883.3bd969ea1d3aaaff.js +0 -2
- package/apps/pwa/out/_next/static/chunks/5250-c78ca295585bbf95.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5532.6899a12958a89b72.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5845-e6c003ce8d5fd36a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/5978-b27004c677119115.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6048-4c5c33579bc4ef78.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6303-422dbf0f67c72aac.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6511-ff8360be3d65aaa4.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6588-ba380869b67c4fa7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/6751-bb3595948b2e311b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7274-19b7279f18b47ab3.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7551-a0e0707b79164ddd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7758-85c2b53f731da024.js +0 -1
- package/apps/pwa/out/_next/static/chunks/7777-79f845045aa1ed86.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8025-cc24ee5d8f2fb6f8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8206-c6684dcde12be721.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8605-9da4678f8a310d18.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8725.bf46bb1395dd0fa2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8734-252d511cd276c0dc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/8834-ca1cb09c1cb1a6bf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9269.d2a4795bc6812717.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9538-2816b90923606459.js +0 -1
- package/apps/pwa/out/_next/static/chunks/9906-e7b8b5310a169dfe.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/_not-found/page-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-e5b4e1b256f41076.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-e49b9b76c565a0b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b4f032c893b21cc9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-f14987d5850a31e0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-b433308aad13ceb6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-4c1dbb21861beab2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-c85b816e3b269dbb.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/control/page-f6bc66b919b2ea7d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-60603c1e056c9b25.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-a0ea8f5a2341ee6b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/exec/page-45c5bf04b4a605e6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/field/page-36c6c4183353ac9e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/healthz/route-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-cb2eb0fd809980da.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/intake/page-31012264da8a341f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-50309aeb0bdd554a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-bc00e964e855f4df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/market/page-0696ee3fac9fe56c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-66c27608f651b866.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/page-6ddd2109b7fc3a80.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/not-found-4e50b7298c1f69a5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-7a2bf830038a2264.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-1f327f18694fabca.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-e5cef2fe6e4028df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-cf596128d2b420ba.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-f672536f14f0969f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-b52365662232db89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-e7a6c80f288b1948.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/scheduler/page-29a709616df9b050.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-fee842d7c2151129.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/page-78b40a344ed22f24.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/page-75d199f448171014.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/page-a011dd5b0bff29c5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/layout-d16993889b125c42.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/page-d92e32d51ab5157e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-5f21aed6932ef375.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/page-1e1a53946804debf.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-a1833cd43d832601.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/today/page-2a511d320f762e06.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/page-1c3e03f88dac1e21.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-c4d5289c2ea97a89.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-b91f8952deaeaf27.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/page-71b43a2588489987.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-2b256feed331546f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-505bb96d69a8622c.js +0 -1
- package/apps/pwa/out/_next/static/chunks/framework-172630e6aa5e2d7f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-app-f0fe2d7477845181.js +0 -1
- package/apps/pwa/out/_next/static/chunks/main-fc98ac70b44eccb0.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_app-5bd7f10271f72413.js +0 -1
- package/apps/pwa/out/_next/static/chunks/pages/_error-97d0d85ea05fef9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/webpack-9e4d02d567b98240.js +0 -1
- package/apps/pwa/out/_next/static/css/128fb7fb670b2fbf.css +0 -1
- /package/apps/pwa/out/_next/static/{D51l8jUBplJrIpWvENsDK → LZcstaO9_NXypPFmujolv}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// 흡수 렌즈 — outbox 항목마다 «우리에게 무엇인가»를 판정해 outbox/lens/<day>.jsonl 에만 남긴다.
|
|
2
|
+
// 원 갈래(goals·manual·review·release)는 읽기만 한다. LLM 은 judge 한 번(id 당).
|
|
3
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { debug } from '../debug/log.js';
|
|
6
|
+
import { judge } from '../llm/judge-layer.js';
|
|
7
|
+
import { intakeOutboxDir } from './route.js';
|
|
8
|
+
|
|
9
|
+
export const LENS_VERDICTS = ['대체 후보', '보강', '경쟁 대조', '참고'] as const;
|
|
10
|
+
export type LensVerdict = (typeof LENS_VERDICTS)[number];
|
|
11
|
+
|
|
12
|
+
export const LENS_BRANCHES = ['goals', 'manual', 'review', 'release'] as const;
|
|
13
|
+
export const LENS_DAILY_CAP = 20;
|
|
14
|
+
export const LENS_FACT_CAP = 12;
|
|
15
|
+
|
|
16
|
+
export interface LensRecord {
|
|
17
|
+
id: string;
|
|
18
|
+
at: string;
|
|
19
|
+
lensVerdict: LensVerdict;
|
|
20
|
+
why: string;
|
|
21
|
+
target: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LensJudgeInput {
|
|
25
|
+
id: string;
|
|
26
|
+
facts: { fact: string; current: string }[];
|
|
27
|
+
url?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type LensJudge = (input: LensJudgeInput) => Promise<unknown>;
|
|
31
|
+
|
|
32
|
+
export interface AnnotateIntakeLensDeps {
|
|
33
|
+
judge?: LensJudge;
|
|
34
|
+
now?: () => string;
|
|
35
|
+
log?: (event: string, data: Record<string, unknown>) => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface AnnotateIntakeLensResult {
|
|
39
|
+
judged: number;
|
|
40
|
+
skipped: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
capped: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface GroupedItem {
|
|
46
|
+
id: string;
|
|
47
|
+
facts: { fact: string; current: string }[];
|
|
48
|
+
url?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readJsonl(file: string): Record<string, unknown>[] {
|
|
52
|
+
if (!existsSync(file)) return [];
|
|
53
|
+
return readFileSync(file, 'utf8').split('\n').filter(Boolean).flatMap((line) => {
|
|
54
|
+
try { const row = JSON.parse(line) as unknown; return row && typeof row === 'object' && !Array.isArray(row) ? [row as Record<string, unknown>] : []; }
|
|
55
|
+
catch { return []; }
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isTestProcess(): boolean {
|
|
60
|
+
return process.env.NODE_ENV === 'test' || Boolean(process.env.ELANOUS_TEST_HOME);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isLensVerdict(value: unknown): value is LensVerdict {
|
|
64
|
+
return typeof value === 'string' && (LENS_VERDICTS as readonly string[]).includes(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 스키마 밖이면 null — 기록하지 않는다. why 는 한 문장이다. */
|
|
68
|
+
export function coerceLensVerdict(raw: unknown): Omit<LensRecord, 'id' | 'at'> | null {
|
|
69
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
70
|
+
const row = raw as Record<string, unknown>;
|
|
71
|
+
const why = typeof row.why === 'string' ? row.why.trim() : '';
|
|
72
|
+
const target = typeof row.target === 'string' ? row.target.trim() : '';
|
|
73
|
+
if (!isLensVerdict(row.lensVerdict) || !why || !target) return null;
|
|
74
|
+
if (/[\r\n]/.test(why)) return null;
|
|
75
|
+
return { lensVerdict: row.lensVerdict, why, target };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function groupOutbox(root: string, day: string): GroupedItem[] {
|
|
79
|
+
const out = intakeOutboxDir(root);
|
|
80
|
+
const order: string[] = [];
|
|
81
|
+
const groups = new Map<string, GroupedItem>();
|
|
82
|
+
for (const branch of LENS_BRANCHES) {
|
|
83
|
+
for (const row of readJsonl(join(out, branch, `${day}.jsonl`))) {
|
|
84
|
+
const id = typeof row.id === 'string' ? row.id.trim() : '';
|
|
85
|
+
const fact = typeof row.fact === 'string' ? row.fact.trim() : '';
|
|
86
|
+
const current = typeof row.current === 'string' ? row.current.trim() : '';
|
|
87
|
+
if (!id || !fact) continue;
|
|
88
|
+
let group = groups.get(id);
|
|
89
|
+
if (!group) {
|
|
90
|
+
group = { id, facts: [] };
|
|
91
|
+
groups.set(id, group);
|
|
92
|
+
order.push(id);
|
|
93
|
+
}
|
|
94
|
+
if (!group.url && typeof row.url === 'string' && row.url.trim()) group.url = row.url.trim();
|
|
95
|
+
if (group.facts.length < LENS_FACT_CAP) group.facts.push({ fact, current });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return order.map((id) => groups.get(id)!);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function judgedIds(file: string): Set<string> {
|
|
102
|
+
return new Set(readJsonl(file).flatMap((row) => typeof row.id === 'string' && row.id ? [row.id] : []));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function lensPrompt(item: GroupedItem): string {
|
|
106
|
+
const lines = item.facts.map((row, i) => `${i + 1}. fact: ${row.fact}\n current: ${row.current || '(없음)'}`);
|
|
107
|
+
return [
|
|
108
|
+
'SYSTEM:',
|
|
109
|
+
'흡수 항목이 우리에게 무엇인가를 한 번만 판정한다. 경로를 나열하지 말고, 왜 우리에게 그 판정인지를 한 문장으로 말한다.',
|
|
110
|
+
'lensVerdict 는 다음 넷 중 하나다: 대체 후보 · 보강 · 경쟁 대조 · 참고.',
|
|
111
|
+
'target 은 닿는 우리 쪽 이름 하나(기능·칸·파일)다.',
|
|
112
|
+
'JSON 만 답한다: {"lensVerdict":"...","why":"...","target":"..."}',
|
|
113
|
+
'',
|
|
114
|
+
`id: ${item.id}`,
|
|
115
|
+
...(item.url ? [`url: ${item.url}`] : []),
|
|
116
|
+
...lines,
|
|
117
|
+
].join('\n');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function defaultJudge(item: GroupedItem): Promise<unknown> {
|
|
121
|
+
const decision = await judge({
|
|
122
|
+
site: 'intake.lens',
|
|
123
|
+
prompt: lensPrompt(item),
|
|
124
|
+
schema: (value) => coerceLensVerdict(value),
|
|
125
|
+
});
|
|
126
|
+
if (!decision.ok) {
|
|
127
|
+
if (decision.reason === 'schema') return null;
|
|
128
|
+
throw decision.error instanceof Error ? decision.error : new Error(String(decision.error));
|
|
129
|
+
}
|
|
130
|
+
return decision.value;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 그날 outbox 를 id 로 묶어 아직 없는 판정만 남긴다. 원 갈래 파일은 쓰지 않는다. */
|
|
134
|
+
export async function annotateIntakeLens(
|
|
135
|
+
root: string,
|
|
136
|
+
day: string,
|
|
137
|
+
deps: AnnotateIntakeLensDeps = {},
|
|
138
|
+
): Promise<AnnotateIntakeLensResult> {
|
|
139
|
+
const log = deps.log ?? ((event, data) => { debug.log('intake.lens', event, data); });
|
|
140
|
+
const empty = { judged: 0, skipped: 0, failed: 0, capped: false };
|
|
141
|
+
if (!deps.judge && isTestProcess()) {
|
|
142
|
+
log('skipped-test', {});
|
|
143
|
+
return empty;
|
|
144
|
+
}
|
|
145
|
+
const items = groupOutbox(root, day);
|
|
146
|
+
const file = join(intakeOutboxDir(root), 'lens', `${day}.jsonl`);
|
|
147
|
+
const done = judgedIds(file);
|
|
148
|
+
const pending = items.filter((item) => !done.has(item.id));
|
|
149
|
+
const capped = pending.length > LENS_DAILY_CAP;
|
|
150
|
+
const batch = pending.slice(0, LENS_DAILY_CAP);
|
|
151
|
+
if (capped) log('capped', { day, total: pending.length, judged: batch.length });
|
|
152
|
+
const judgeOne = deps.judge ?? ((input: LensJudgeInput) => defaultJudge(input));
|
|
153
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
154
|
+
let judged = 0;
|
|
155
|
+
let failed = 0;
|
|
156
|
+
for (const item of batch) {
|
|
157
|
+
let raw: unknown;
|
|
158
|
+
try {
|
|
159
|
+
raw = await judgeOne({ id: item.id, facts: item.facts, ...(item.url ? { url: item.url } : {}) });
|
|
160
|
+
} catch (error) {
|
|
161
|
+
failed++;
|
|
162
|
+
log('judge-failed', { id: item.id, reason: error instanceof Error ? error.message : String(error) });
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const verdict = coerceLensVerdict(raw);
|
|
166
|
+
if (!verdict) {
|
|
167
|
+
failed++;
|
|
168
|
+
log('judge-failed', { id: item.id, reason: 'schema' });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
mkdirSync(join(intakeOutboxDir(root), 'lens'), { recursive: true });
|
|
172
|
+
const record: LensRecord = { id: item.id, at: now(), ...verdict };
|
|
173
|
+
appendFileSync(file, `${JSON.stringify(record)}\n`);
|
|
174
|
+
judged++;
|
|
175
|
+
}
|
|
176
|
+
return { judged, skipped: items.length - pending.length, failed, capped };
|
|
177
|
+
}
|
|
@@ -6,24 +6,51 @@ import { createWishCard } from './wish-card.js';
|
|
|
6
6
|
|
|
7
7
|
export const LINEAR_WISH_KEY_MISSING = 'Linear 키가 없습니다 — elanous connector linear 로 먼저 등록하세요';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
fetchIssues?: typeof fetchLinearIssues;
|
|
16
|
-
};
|
|
17
|
-
}): Promise<{ added: number; duplicate: number; failed: number }> {
|
|
9
|
+
type LinearWishDeps = {
|
|
10
|
+
getApiKey?: () => Promise<string | undefined>;
|
|
11
|
+
fetchIssues?: typeof fetchLinearIssues;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
async function readLinearWishes(teamKey: string, options: { labelOrPrefix?: string; excludeLabel?: string }, deps: LinearWishDeps) {
|
|
18
15
|
const apiKey = await (deps.getApiKey ?? (() => getSecretAsync('connector.linear.apiKey')))();
|
|
19
16
|
if (!apiKey?.trim()) throw new Error(LINEAR_WISH_KEY_MISSING);
|
|
20
|
-
let issues: Awaited<ReturnType<typeof fetchLinearIssues>>;
|
|
21
17
|
try {
|
|
22
|
-
|
|
18
|
+
return await (deps.fetchIssues ?? fetchLinearIssues)({ apiKey, teamKey, ...options });
|
|
23
19
|
} catch (error) {
|
|
24
20
|
const message = error instanceof Error ? error.message : String(error);
|
|
25
21
|
throw new Error(message.replaceAll(apiKey, '[redacted]'));
|
|
26
22
|
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface LinearWishSuggestion { identifier: string; title: string; reason: string }
|
|
26
|
+
|
|
27
|
+
const WISH_PHRASES = [
|
|
28
|
+
{ pattern: /해\s*줘(?:요|라)?/, reason: '요청' },
|
|
29
|
+
{ pattern: /만들어(?:\s*줘(?:요)?)?/, reason: '제작' },
|
|
30
|
+
{ pattern: /원해(?:요)?/, reason: '희망' },
|
|
31
|
+
{ pattern: /있으면\s*좋겠다/, reason: '희망' },
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export async function suggestLinearWishes({ teamKey, deps = {} }: {
|
|
35
|
+
teamKey: string;
|
|
36
|
+
deps?: LinearWishDeps;
|
|
37
|
+
}): Promise<LinearWishSuggestion[]> {
|
|
38
|
+
const issues = await readLinearWishes(teamKey, { excludeLabel: 'wish' }, deps);
|
|
39
|
+
return issues.flatMap(issue => {
|
|
40
|
+
const match = WISH_PHRASES.find(({ pattern }) => pattern.test(`${issue.title}\n${issue.body}`));
|
|
41
|
+
return match && issue.identifier
|
|
42
|
+
? [{ identifier: issue.identifier, title: Array.from(issue.title).slice(0, 40).join(''), reason: match.reason }]
|
|
43
|
+
: [];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function scanLinearWishes({ teamKey, label = 'wish', store, deps = {} }: {
|
|
48
|
+
teamKey: string;
|
|
49
|
+
label?: string;
|
|
50
|
+
store: CardStore;
|
|
51
|
+
deps?: LinearWishDeps;
|
|
52
|
+
}): Promise<{ added: number; duplicate: number; failed: number }> {
|
|
53
|
+
const issues = await readLinearWishes(teamKey, { labelOrPrefix: label }, deps);
|
|
27
54
|
let added = 0;
|
|
28
55
|
let duplicate = 0;
|
|
29
56
|
let failed = 0;
|
|
@@ -32,6 +59,7 @@ export async function scanLinearWishes({ teamKey, label = 'wish', store, deps =
|
|
|
32
59
|
const result = createWishCard({
|
|
33
60
|
text: `${issue.title}\n\n${issue.body}\n\n${issue.url}`,
|
|
34
61
|
source: 'linear', ref: issue.identifier ?? '',
|
|
62
|
+
replyTo: { surface: 'linear', issueId: issue.ref },
|
|
35
63
|
}, store);
|
|
36
64
|
if (result.created) added++;
|
|
37
65
|
else duplicate++;
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
// ⛔ 원장의 `text`(개인 메모일 수 있다)는 큐에 싣지 않는다 — 대조가 낸 사실·근거와 노트 경로만.
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { Database } from 'bun:sqlite';
|
|
6
7
|
import { join } from 'node:path';
|
|
7
8
|
import { debug } from '../debug/log.js';
|
|
8
|
-
import {
|
|
9
|
+
import { getToxRuntimeDeps } from '../task-orchestrator/runtime-deps.js';
|
|
10
|
+
import type { TaskStore } from '../task-orchestrator/store.js';
|
|
11
|
+
import type { TaskStatus } from '../task-orchestrator/types.js';
|
|
12
|
+
import { canonicalUrl, ingestIntakeItems, intakeItemId, intakeLedgerDir, loadIntakeLedger, markIntakeItem, type IntakeItem } from './items.js';
|
|
9
13
|
|
|
10
14
|
/** `elanous intake check --json` 산출 중 갈래가 읽는 칸. */
|
|
11
15
|
export interface IntakeCheckJson {
|
|
@@ -29,6 +33,146 @@ export const gitLastCommitOf: LastCommitOf = (tree, path) => {
|
|
|
29
33
|
|
|
30
34
|
export function intakeOutboxDir(root: string): string { return join(intakeLedgerDir(root), 'outbox'); }
|
|
31
35
|
|
|
36
|
+
export const INTAKE_EVENT_DEFERRED_REPLY = '일일 즉시 흡수 상한을 넘어 다음 일일 판으로 미뤘습니다.';
|
|
37
|
+
export type IntakeEventResult = { status: 'scheduled' | 'deferred' | 'duplicate'; reason: string; url: string; reply?: string };
|
|
38
|
+
|
|
39
|
+
// A lost claim must become eligible at the next daily pass; live tasks remain excluded.
|
|
40
|
+
const INTAKE_EVENT_LEASE_MS = 23 * 3600_000;
|
|
41
|
+
|
|
42
|
+
// A non-terminal task that has not been touched for this long is treated as a dead worker.
|
|
43
|
+
const INTAKE_EVENT_TASK_STALE_MS = 2 * 3600_000;
|
|
44
|
+
|
|
45
|
+
/** A probe may return the bare status, or the status with the task's last update (epoch ms). */
|
|
46
|
+
export type IntakeEventTaskProbe = (id: string) => TaskStatus | { status: TaskStatus; updatedAt?: number } | undefined;
|
|
47
|
+
|
|
48
|
+
function eventTaskStatus(taskId: string): { status: TaskStatus; updatedAt?: number } | undefined {
|
|
49
|
+
const deps = getToxRuntimeDeps();
|
|
50
|
+
const graphTask = deps.getGraph()?.getTask(taskId) ?? (deps.getStore?.() as TaskStore | null | undefined)?.getTask(taskId);
|
|
51
|
+
return graphTask ? { status: graphTask.status, updatedAt: graphTask.updatedAt } : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function probeStatus(probe: IntakeEventTaskProbe, id: string): { status?: TaskStatus; updatedAt?: number } {
|
|
55
|
+
const value = probe(id);
|
|
56
|
+
return typeof value === 'string' ? { status: value } : value ?? {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type EventClaim = { status: string; task_id: string | null; claimed_at: string | null; day: string };
|
|
60
|
+
|
|
61
|
+
function activeEventClaim(row: EventClaim, now: string, taskStatus: IntakeEventTaskProbe): boolean {
|
|
62
|
+
if (row.status !== 'scheduled' && row.status !== 'pending') return false;
|
|
63
|
+
const { status, updatedAt } = row.task_id ? probeStatus(taskStatus, row.task_id) : {};
|
|
64
|
+
if (status === 'done') return true;
|
|
65
|
+
if (status === 'failed' || status === 'cancelled' || status === 'superseded') return false;
|
|
66
|
+
const claimedAt = Date.parse(row.claimed_at ?? `${row.day}T00:00:00.000+09:00`);
|
|
67
|
+
const withinLease = Number.isFinite(claimedAt) && Date.parse(now) - claimedAt < INTAKE_EVENT_LEASE_MS;
|
|
68
|
+
if (!status) return withinLease;
|
|
69
|
+
// A status alone is not liveness: a crashed worker leaves «running» behind, so require a recent update.
|
|
70
|
+
if (updatedAt !== undefined && Number.isFinite(updatedAt)) return Date.parse(now) - updatedAt < INTAKE_EVENT_TASK_STALE_MS;
|
|
71
|
+
return withinLease;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Claim an event slot before posting. The unique URL and KST-day quota share one SQLite transaction. */
|
|
75
|
+
export async function scheduleIntakeEvent(
|
|
76
|
+
root: string,
|
|
77
|
+
url: string,
|
|
78
|
+
opts: {
|
|
79
|
+
eventMax?: number;
|
|
80
|
+
now?: string;
|
|
81
|
+
post: (input: { url: string }) => Promise<{ taskId: string }>;
|
|
82
|
+
},
|
|
83
|
+
): Promise<IntakeEventResult> {
|
|
84
|
+
const normalized = canonicalUrl(url);
|
|
85
|
+
if (!normalized) throw new Error('intake event requires an HTTP(S) URL');
|
|
86
|
+
const now = opts.now ?? new Date().toISOString();
|
|
87
|
+
const day = kstDay(now);
|
|
88
|
+
const id = intakeItemId('telegram-bot', { url: normalized });
|
|
89
|
+
const max = opts.eventMax ?? 10;
|
|
90
|
+
if (!Number.isSafeInteger(max) || max < 0) throw new RangeError('intake.eventMax must be a non-negative integer');
|
|
91
|
+
const existing = loadIntakeLedger(root).items.get(id);
|
|
92
|
+
if (existing && ['queued', 'absorbed', 'checked', 'routed', 'discarded'].includes(existing.status)) {
|
|
93
|
+
return { status: 'duplicate', url: normalized, reason: 'already-in-ledger' };
|
|
94
|
+
}
|
|
95
|
+
mkdirSync(intakeLedgerDir(root), { recursive: true });
|
|
96
|
+
const db = new Database(join(intakeLedgerDir(root), 'events.sqlite'), { create: true });
|
|
97
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
98
|
+
try {
|
|
99
|
+
db.exec(`CREATE TABLE IF NOT EXISTS intake_events (
|
|
100
|
+
url TEXT PRIMARY KEY, day TEXT NOT NULL, status TEXT NOT NULL, task_id TEXT, claimed_at TEXT
|
|
101
|
+
)`);
|
|
102
|
+
const columns = db.query('PRAGMA table_info(intake_events)').all() as { name: string }[];
|
|
103
|
+
if (!columns.some((column) => column.name === 'claimed_at')) db.exec('ALTER TABLE intake_events ADD COLUMN claimed_at TEXT');
|
|
104
|
+
// One row per claim that reached the task queue — the KST-day quota counts these, not current statuses.
|
|
105
|
+
db.exec('CREATE TABLE IF NOT EXISTS intake_event_claims (url TEXT NOT NULL, day TEXT NOT NULL, claimed_at TEXT NOT NULL, PRIMARY KEY (url, claimed_at))');
|
|
106
|
+
const claim = db.transaction((): 'scheduled' | 'deferred' | 'duplicate' => {
|
|
107
|
+
const previous = db.query('SELECT status, task_id, claimed_at, day FROM intake_events WHERE url = ?').get(normalized) as EventClaim | null;
|
|
108
|
+
if (previous && activeEventClaim(previous, now, eventTaskStatus)) {
|
|
109
|
+
if (previous.task_id && eventTaskStatus(previous.task_id)?.status === 'done') {
|
|
110
|
+
db.query("UPDATE intake_events SET status = 'completed' WHERE url = ?").run(normalized);
|
|
111
|
+
}
|
|
112
|
+
return 'duplicate';
|
|
113
|
+
}
|
|
114
|
+
if (previous && previous.status !== 'failed' && previous.status !== 'expired'
|
|
115
|
+
&& previous.status !== 'pending' && previous.status !== 'scheduled') {
|
|
116
|
+
return previous.status === 'deferred' ? 'deferred' : 'duplicate';
|
|
117
|
+
}
|
|
118
|
+
const count = db.query('SELECT count(*) AS n FROM intake_event_claims WHERE day = ?').get(day) as { n: number };
|
|
119
|
+
const status = count.n >= max ? 'deferred' : 'pending';
|
|
120
|
+
db.query(`INSERT INTO intake_events (url, day, status, claimed_at) VALUES (?, ?, ?, ?)
|
|
121
|
+
ON CONFLICT(url) DO UPDATE SET day = excluded.day, status = excluded.status, claimed_at = excluded.claimed_at, task_id = NULL`)
|
|
122
|
+
.run(normalized, day, status, now);
|
|
123
|
+
if (status === 'pending') db.query('INSERT OR IGNORE INTO intake_event_claims (url, day, claimed_at) VALUES (?, ?, ?)').run(normalized, day, now);
|
|
124
|
+
return status === 'pending' ? 'scheduled' : 'deferred';
|
|
125
|
+
})();
|
|
126
|
+
if (claim === 'duplicate') return { status: 'duplicate', url: normalized, reason: 'already-claimed' };
|
|
127
|
+
if (claim === 'deferred') {
|
|
128
|
+
ingestIntakeItems(root, 'telegram-bot', [{ url: normalized }], now);
|
|
129
|
+
debug.log('intake.event', 'deferred', { url: normalized, reason: 'event-max' });
|
|
130
|
+
return { status: 'deferred', url: normalized, reason: 'event-max', reply: INTAKE_EVENT_DEFERRED_REPLY };
|
|
131
|
+
}
|
|
132
|
+
let posted: { taskId: string };
|
|
133
|
+
try {
|
|
134
|
+
ingestIntakeItems(root, 'telegram-bot', [{ url: normalized }], now);
|
|
135
|
+
posted = await opts.post({ url: normalized });
|
|
136
|
+
if (!posted.taskId) throw new Error('intake event post returned no taskId');
|
|
137
|
+
} catch (error) {
|
|
138
|
+
db.query('DELETE FROM intake_events WHERE url = ? AND status = ? AND claimed_at = ?').run(normalized, 'pending', now);
|
|
139
|
+
db.query('DELETE FROM intake_event_claims WHERE url = ? AND claimed_at = ?').run(normalized, now);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
db.query("UPDATE intake_events SET status = 'scheduled', task_id = ? WHERE url = ? AND status = 'pending' AND claimed_at = ?")
|
|
143
|
+
.run(posted.taskId, normalized, now);
|
|
144
|
+
debug.log('intake.event', 'scheduled', { url: normalized, reason: 'telegram-absorb' });
|
|
145
|
+
return { status: 'scheduled', url: normalized, reason: 'telegram-absorb' };
|
|
146
|
+
} finally {
|
|
147
|
+
db.close();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Only an active task or a fresh claim excludes a URL from the daily queue. */
|
|
152
|
+
export function wasIntakeEventScheduled(root: string, url: string, now = new Date().toISOString(), taskStatus: IntakeEventTaskProbe = eventTaskStatus): boolean {
|
|
153
|
+
const file = join(intakeLedgerDir(root), 'events.sqlite');
|
|
154
|
+
if (!existsSync(file)) return false;
|
|
155
|
+
const normalized = canonicalUrl(url);
|
|
156
|
+
if (!normalized) return false;
|
|
157
|
+
const db = new Database(file);
|
|
158
|
+
try {
|
|
159
|
+
const row = db.query('SELECT status, task_id, claimed_at, day FROM intake_events WHERE url = ?').get(normalized) as EventClaim | null;
|
|
160
|
+
if (!row || (row.status !== 'scheduled' && row.status !== 'pending')) return row?.status === 'completed';
|
|
161
|
+
if (activeEventClaim(row, now, taskStatus)) {
|
|
162
|
+
if (row.task_id && probeStatus(taskStatus, row.task_id).status === 'done') {
|
|
163
|
+
db.query("UPDATE intake_events SET status = 'completed' WHERE url = ?").run(normalized);
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
const status = row.task_id ? probeStatus(taskStatus, row.task_id).status : undefined;
|
|
168
|
+
db.query("UPDATE intake_events SET status = ? WHERE url = ? AND status IN ('pending', 'scheduled')")
|
|
169
|
+
.run(status === 'failed' || status === 'cancelled' || status === 'superseded' ? 'failed' : 'expired', normalized);
|
|
170
|
+
return false;
|
|
171
|
+
} finally {
|
|
172
|
+
db.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
32
176
|
function appendJsonl(file: string, rows: unknown[]): void {
|
|
33
177
|
if (!rows.length) return;
|
|
34
178
|
mkdirSync(join(file, '..'), { recursive: true });
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { debug } from '../debug/log.js';
|
|
2
|
+
import { constants, closeSync, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, writeSync } from 'node:fs';
|
|
3
|
+
import { basename, dirname, join, parse, resolve } from 'node:path';
|
|
4
|
+
import { dlopen, FFIType } from 'bun:ffi';
|
|
5
|
+
import { elanousStateRoot } from '../autopilot/state-paths.js';
|
|
6
|
+
|
|
7
|
+
/** Bench samples live in the instance's private state, never in a repository or card response. */
|
|
8
|
+
export function wishBenchmarkLedgerPath(root: string = elanousStateRoot()): string {
|
|
9
|
+
// OP D 길 원장(채널 10-04 22:48)과 같은 파일·같은 줄 꼴 — OP 가 손으로 더하는 줄과 섞여 한 표본이 된다.
|
|
10
|
+
return join(root, 'bench', 'goal-bench-requirements.jsonl');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function privateDirectory(path: string, privateMode: boolean): void {
|
|
14
|
+
try { mkdirSync(path, { mode: 0o700 }); }
|
|
15
|
+
catch (error) { if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error; }
|
|
16
|
+
const state = lstatSync(path);
|
|
17
|
+
if (state.isSymbolicLink() || !state.isDirectory()) throw new Error(`Unsafe wish benchmark directory: ${path}`);
|
|
18
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
19
|
+
try {
|
|
20
|
+
if (privateMode) fchmodSync(fd, 0o700);
|
|
21
|
+
const opened = fstatSync(fd);
|
|
22
|
+
if (opened.dev !== state.dev || opened.ino !== state.ino) {
|
|
23
|
+
throw new Error(`Wish benchmark directory changed while opening: ${path}`);
|
|
24
|
+
}
|
|
25
|
+
} finally { closeSync(fd); }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function realExistingPrefix(path: string): string {
|
|
29
|
+
let head = path; const tail: string[] = [];
|
|
30
|
+
for (;;) {
|
|
31
|
+
try { return join(realpathSync(head), ...tail.reverse()); }
|
|
32
|
+
catch (error) {
|
|
33
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
34
|
+
const parent = dirname(head); if (parent === head) return path;
|
|
35
|
+
tail.push(basename(head)); head = parent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function preparePrivateDirectory(root: string): string {
|
|
41
|
+
// 맥에서 `/var` 는 `/private/var` 링크다 — 이미 있는 상위 경로는 실제 경로로 풀고, 새로 만들 아래 칸만 링크를 막는다.
|
|
42
|
+
const absolute = realExistingPrefix(resolve(root));
|
|
43
|
+
let current = parse(absolute).root;
|
|
44
|
+
const parts = absolute.slice(current.length).split(/[\\/]/).filter(Boolean);
|
|
45
|
+
for (const part of parts) {
|
|
46
|
+
current = join(current, part);
|
|
47
|
+
// A sample cannot be written inside a source checkout, even if the state root was overridden.
|
|
48
|
+
try {
|
|
49
|
+
lstatSync(join(current, '.git'));
|
|
50
|
+
throw new Error(`Wish benchmark state root is inside a repository: ${current}`);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
53
|
+
}
|
|
54
|
+
privateDirectory(current, false);
|
|
55
|
+
}
|
|
56
|
+
for (const part of ['bench']) {
|
|
57
|
+
current = join(current, part);
|
|
58
|
+
privateDirectory(current, true);
|
|
59
|
+
}
|
|
60
|
+
return current;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Advisory locks belong to the open descriptor, not the pathname: SIGKILL closes it automatically.
|
|
64
|
+
// Keep the lock file itself in place so no competing writer can lock a different inode.
|
|
65
|
+
const libc = dlopen(process.platform === 'darwin' ? '/usr/lib/libSystem.B.dylib' : 'libc.so.6', {
|
|
66
|
+
flock: { args: [FFIType.int, FFIType.int], returns: FFIType.int },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function withLedgerLock(dir: string, action: () => void): void {
|
|
70
|
+
const lock = join(dir, '.wish-cards.lock');
|
|
71
|
+
const fd = openSync(lock, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
|
|
72
|
+
try {
|
|
73
|
+
const opened = fstatSync(fd);
|
|
74
|
+
if (!opened.isFile() || opened.nlink !== 1) throw new Error('Unsafe wish benchmark lock');
|
|
75
|
+
fchmodSync(fd, 0o600);
|
|
76
|
+
// LOCK_EX blocks rather than stealing a live writer's lock. Kernel releases it on crash.
|
|
77
|
+
if (libc.symbols.flock(fd, 2) !== 0) throw new Error('Wish benchmark ledger lock failed');
|
|
78
|
+
const current = lstatSync(lock);
|
|
79
|
+
if (opened.dev !== current.dev || opened.ino !== current.ino) throw new Error('Wish benchmark lock changed while waiting');
|
|
80
|
+
action();
|
|
81
|
+
} finally {
|
|
82
|
+
closeSync(fd);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function recordWishBenchmarkSample(
|
|
87
|
+
sample: { cardId: string; at: string; surface: string; text: string },
|
|
88
|
+
): void {
|
|
89
|
+
const stateRoot = elanousStateRoot();
|
|
90
|
+
const dir = preparePrivateDirectory(stateRoot);
|
|
91
|
+
const expected = lstatSync(dir);
|
|
92
|
+
const path = wishBenchmarkLedgerPath(stateRoot);
|
|
93
|
+
withLedgerLock(dir, () => {
|
|
94
|
+
const current = lstatSync(dir);
|
|
95
|
+
if (!current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) {
|
|
96
|
+
throw new Error('Wish benchmark directory changed while waiting for lock');
|
|
97
|
+
}
|
|
98
|
+
// Re-check under the lock: another process may have recorded this card while we waited.
|
|
99
|
+
const fd = openSync(path, constants.O_RDWR | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
|
|
100
|
+
try {
|
|
101
|
+
const state = fstatSync(fd);
|
|
102
|
+
if (!state.isFile() || state.nlink !== 1) throw new Error('Wish benchmark ledger is not a private regular file');
|
|
103
|
+
fchmodSync(fd, 0o600);
|
|
104
|
+
if (readFileSync(fd, 'utf8').split('\n').some(line => {
|
|
105
|
+
if (!line) return false;
|
|
106
|
+
const row = JSON.parse(line) as { id?: string; cardId?: string };
|
|
107
|
+
return (row.id ?? row.cardId) === sample.cardId;
|
|
108
|
+
})) return;
|
|
109
|
+
// 줄 꼴 = OP 원장과 같다: id · at · surface · text(원문 그대로) · chars · op_cells(참조 칸 — OP 가 채운다) · note.
|
|
110
|
+
const line = JSON.stringify({ id: sample.cardId, at: sample.at, surface: sample.surface, text: sample.text,
|
|
111
|
+
chars: Array.from(sample.text).length, op_cells: [], note: 'wish-card' }) + '\n';
|
|
112
|
+
writeSync(fd, line);
|
|
113
|
+
} finally {
|
|
114
|
+
closeSync(fd);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 카드 생성 경로용: 벤치 원장 쓰기가 실패해도(저장소 안 상태 루트 · 권한 · 링크) 소원 카드는 만들어져야 한다.
|
|
120
|
+
* 실패는 관측 한 줄로만 남긴다(원문은 싣지 않는다). */
|
|
121
|
+
export function recordWishBenchmarkSampleSafely(sample: Parameters<typeof recordWishBenchmarkSample>[0]): boolean {
|
|
122
|
+
try {
|
|
123
|
+
recordWishBenchmarkSample(sample);
|
|
124
|
+
return true;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
debug.log('intake.wish-bench', 'skipped', { cardId: sample.cardId, surface: sample.surface, reason: String((error as Error)?.message ?? error).split(':')[0] });
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -1,13 +1,54 @@
|
|
|
1
1
|
import { debug } from '../debug/log.js';
|
|
2
2
|
import { CardStore } from '../task-cards/card-store.js';
|
|
3
|
+
import { recordWishBenchmarkSampleSafely } from './wish-benchmark-ledger.js';
|
|
4
|
+
|
|
5
|
+
export type WishReplyTarget =
|
|
6
|
+
| { surface: 'telegram'; chatId: string; threadId?: string; botId?: string }
|
|
7
|
+
| { surface: 'pwa'; sessionId: string }
|
|
8
|
+
| { surface: 'linear'; issueId: string }
|
|
9
|
+
| { surface: 'tui' };
|
|
10
|
+
|
|
11
|
+
/** `intake:reply:0` 섹션 꼴 — surface ⊕ 표면 안 주소(텔레그램 대화 · PWA 세션 · Linear 이슈 · TUI 없음). */
|
|
12
|
+
export function replySection(target: WishReplyTarget): { surface: WishReplyTarget['surface']; address: string | null } {
|
|
13
|
+
switch (target.surface) {
|
|
14
|
+
case 'telegram': return { surface: 'telegram', address: target.threadId ? `${target.chatId}:${target.threadId}` : target.chatId };
|
|
15
|
+
case 'pwa': return { surface: 'pwa', address: target.sessionId };
|
|
16
|
+
case 'linear': return { surface: 'linear', address: target.issueId };
|
|
17
|
+
case 'tui': return { surface: 'tui', address: null };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type WishInput = {
|
|
22
|
+
text: string;
|
|
23
|
+
source: WishReplyTarget['surface'];
|
|
24
|
+
ref: string;
|
|
25
|
+
replyTo?: WishReplyTarget;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function replyTarget(input: WishInput): WishReplyTarget {
|
|
29
|
+
if (input.replyTo) {
|
|
30
|
+
if (input.replyTo.surface !== input.source) throw new Error('소원 출처와 회신 대상이 다릅니다');
|
|
31
|
+
return input.replyTo;
|
|
32
|
+
}
|
|
33
|
+
switch (input.source) {
|
|
34
|
+
case 'telegram': return { surface: 'telegram', chatId: input.ref.split(':', 1)[0]! };
|
|
35
|
+
case 'pwa':
|
|
36
|
+
case 'linear': throw new Error('소원 회신 대상을 명시하세요');
|
|
37
|
+
case 'tui': return { surface: 'tui' };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
3
40
|
|
|
4
41
|
export function createWishCard(
|
|
5
|
-
{ text, source, ref }:
|
|
42
|
+
{ text, source, ref, replyTo }: WishInput,
|
|
6
43
|
store: CardStore = new CardStore(),
|
|
7
44
|
): { cardId: string; title: string; created: boolean } {
|
|
8
45
|
const title = text.trim().split(/\r?\n/, 1)[0]!.trim().slice(0, 80);
|
|
9
46
|
if (!title) throw new Error('소원 글을 입력하세요');
|
|
10
47
|
if (!ref?.trim()) throw new Error('소원 참조가 필요합니다');
|
|
48
|
+
const target = replyTarget({ text, source, ref, replyTo });
|
|
49
|
+
if (target.surface === 'telegram' && !/^-?\d+$/.test(target.chatId)
|
|
50
|
+
|| target.surface === 'pwa' && !target.sessionId.trim()
|
|
51
|
+
|| target.surface === 'linear' && !target.issueId.trim()) throw new Error('소원 회신 대상이 필요합니다');
|
|
11
52
|
const goalId = `wish:${source}:${ref}`;
|
|
12
53
|
const old = store.listCards().find(card => card.goalId === goalId);
|
|
13
54
|
const card = old ?? store.createCard({ goalId, title });
|
|
@@ -22,12 +63,22 @@ export function createWishCard(
|
|
|
22
63
|
key: 'intake:wish:0',
|
|
23
64
|
owner: 'steward',
|
|
24
65
|
content: JSON.stringify({
|
|
25
|
-
source, ref, title: card.title, text: sameRequest ? text : null, at: new Date().toISOString(),
|
|
66
|
+
source, ref, replyTo: target, title: card.title, text: sameRequest ? text : null, at: new Date().toISOString(),
|
|
26
67
|
...(old ? { recovered: true } : {}), ...(sameRequest ? {} : { mismatch: true }),
|
|
27
68
|
}),
|
|
28
69
|
});
|
|
29
70
|
if (!sameRequest) debug.log('intake.wish', 'recovered-mismatch', { source, cardId: card.id });
|
|
30
71
|
}
|
|
72
|
+
// FLOW1 원장 약속(TC 21:15): 회신 주소를 `intake:reply:0 {surface, address}` 로도(원장 섹션 허용 목록상 `intake` 아래) 남긴다 — FLOW1a 는 이 꼴을 읽고
|
|
73
|
+
// 보내기는 `sendCardReply(cardId, text)` 만 부른다(wish-reply.ts). 원천은 위 intake 섹션의 replyTo 그대로.
|
|
74
|
+
if (!card.sections.some(section => section.key === 'intake:reply:0')) {
|
|
75
|
+
store.appendSection(card.id, { key: 'intake:reply:0', owner: 'steward', content: JSON.stringify(replySection(target)) });
|
|
76
|
+
}
|
|
77
|
+
const originalIntake = store.getCard(card.id)?.sections.find(section => section.key === 'intake:wish:0');
|
|
78
|
+
if (originalIntake) {
|
|
79
|
+
const first = JSON.parse(originalIntake.content) as { source: WishInput['source']; text: string | null; at: string };
|
|
80
|
+
if (first.text !== null) recordWishBenchmarkSampleSafely({ cardId: card.id, at: first.at, surface: first.source, text: first.text });
|
|
81
|
+
}
|
|
31
82
|
debug.log('intake.wish', created ? 'created' : 'duplicate', { source, cardId: card.id });
|
|
32
83
|
return { cardId: card.id, title: card.title, created };
|
|
33
84
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync, type Dirent } from 'node:fs';
|
|
2
2
|
import { basename, join, relative } from 'node:path';
|
|
3
3
|
import { CardStore } from '../task-cards/card-store.js';
|
|
4
|
+
import { recordWishBenchmarkSampleSafely } from './wish-benchmark-ledger.js';
|
|
4
5
|
|
|
5
6
|
export interface WishScanResult {
|
|
6
7
|
added: number;
|
|
@@ -52,9 +53,11 @@ export function scanWishFolder({ dir, store }: { dir: string; store: CardStore }
|
|
|
52
53
|
result.skipped++;
|
|
53
54
|
continue;
|
|
54
55
|
}
|
|
55
|
-
const
|
|
56
|
+
const text = readFileSync(path, 'utf8');
|
|
57
|
+
const title = /^# (.+)$/m.exec(text)?.[1]?.trim() || basename(path, '.md');
|
|
56
58
|
const card = old ?? store.createCard({ goalId, title });
|
|
57
|
-
|
|
59
|
+
if (intakeSections.length === 0) recordWishBenchmarkSampleSafely({ cardId: card.id, at: card.createdAt, surface: 'wish', text });
|
|
60
|
+
const intake = { source: 'wish', path: relPath, mtime, title, text };
|
|
58
61
|
store.appendSection(card.id, {
|
|
59
62
|
key: `intake:wish:${intakeSections.length}`,
|
|
60
63
|
owner: 'steward',
|