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,126 @@
|
|
|
1
|
+
// Read-only inventory of work tied to the current HQ host before moving it.
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { homedir, hostname } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { inventoryCrontab, listSchedules, openSchedulesDb, parseCronLine } from '../domains/schedule-registry.js';
|
|
7
|
+
import { cronLaunchedActionsWithFence, listAllLoops, type AllLoopEntry, type LoopRegistryOptions } from '../loops/registry.js';
|
|
8
|
+
import { defaultSshRunner, type SshRunner } from './lease.js';
|
|
9
|
+
import { FENCE_ROLES } from './hq.js';
|
|
10
|
+
|
|
11
|
+
type Measurement<T> = { status: 'measured'; value: T } | { status: '못 잼'; reason: string };
|
|
12
|
+
interface MovePlan {
|
|
13
|
+
to: string;
|
|
14
|
+
from: string | null;
|
|
15
|
+
unfencedCron: Measurement<Array<{ raw: string; cron: string; command: string }>>;
|
|
16
|
+
hostLoops: Measurement<AllLoopEntry[]>;
|
|
17
|
+
targetHost: Measurement<{ exists: boolean; name: string | null }>;
|
|
18
|
+
nexusLaunchd: Measurement<{ present: boolean; measuredOn: 'local' }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type CommandResult = { status: number | null; stdout: string; stderr: string };
|
|
22
|
+
interface MovePlanDeps {
|
|
23
|
+
crontab?: () => CommandResult;
|
|
24
|
+
loops?: typeof listAllLoops;
|
|
25
|
+
loopOptions?: LoopRegistryOptions;
|
|
26
|
+
ssh?: SshRunner;
|
|
27
|
+
launchctl?: () => CommandResult;
|
|
28
|
+
localHost?: () => string;
|
|
29
|
+
hostFile?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const measured = <T>(value: T): Measurement<T> => ({ status: 'measured', value });
|
|
33
|
+
const unknown = <T>(reason: string): Measurement<T> => ({ status: '못 잼', reason });
|
|
34
|
+
const run = (command: string, args: string[]): CommandResult => {
|
|
35
|
+
const r = spawnSync(command, args, { encoding: 'utf8', timeout: 15_000, env: process.env });
|
|
36
|
+
return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? String(r.error ?? '') };
|
|
37
|
+
};
|
|
38
|
+
const message = (error: unknown) => error instanceof Error ? error.message : String(error);
|
|
39
|
+
|
|
40
|
+
// Only known HQ jobs qualify by script name; a random scripts/*.ts is not HQ work.
|
|
41
|
+
const HQ_SCRIPTS = new Set([
|
|
42
|
+
'steward.ts', 'draft-cleanup.ts', 'test-diet.ts', 'contract-coordinator.ts',
|
|
43
|
+
'market-posture.ts', 'mission-request-judge.ts',
|
|
44
|
+
]);
|
|
45
|
+
function isHqAction(script: string): boolean {
|
|
46
|
+
if (/(?:^|\/)(?:eln|elanous|elanous\.mjs)$/i.test(script)) return true;
|
|
47
|
+
if (/(?:^|\/)\.elanous(?:\/|$)/i.test(script)) return true;
|
|
48
|
+
const match = script.match(/(?:^|\/)scripts\/([^/\s]+\.ts)$/);
|
|
49
|
+
return !!match && HQ_SCRIPTS.has(match[1]!);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function hqMovePlan(to: string, deps: MovePlanDeps = {}): MovePlan {
|
|
53
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(to) || to.includes('..')) throw new Error('hq move-plan: invalid host');
|
|
54
|
+
let unfencedCron: MovePlan['unfencedCron'];
|
|
55
|
+
let hostLoops: MovePlan['hostLoops'];
|
|
56
|
+
let localName: string | undefined;
|
|
57
|
+
let localHostError: string | undefined;
|
|
58
|
+
try {
|
|
59
|
+
const value = readFileSync(deps.hostFile ?? join(homedir(), '.elanous-hq', 'host'), 'utf8');
|
|
60
|
+
const name = value.endsWith('\n') ? value.slice(0, -1) : value;
|
|
61
|
+
if (!name || name !== name.trim() || /[\r\n]/.test(name)) throw new Error('invalid local HQ host file');
|
|
62
|
+
localName = name;
|
|
63
|
+
} catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ENOENT') localHostError = message(error); }
|
|
64
|
+
const from = deps.localHost?.() ?? localName ?? null;
|
|
65
|
+
let text = '';
|
|
66
|
+
try {
|
|
67
|
+
const r = (deps.crontab ?? (() => run('crontab', ['-l'])))();
|
|
68
|
+
if (r.status !== 0 && !(r.status === 1 && /no crontab for /i.test(r.stderr))) {
|
|
69
|
+
throw new Error(r.stderr.trim() || `crontab -l exit=${r.status}`);
|
|
70
|
+
}
|
|
71
|
+
text = r.status === 0 ? r.stdout : '';
|
|
72
|
+
const rows = text.split('\n').map(raw => ({ raw, parsed: parseCronLine(raw) }))
|
|
73
|
+
.filter((row): row is { raw: string; parsed: { cron: string; command: string } } => row.parsed !== null);
|
|
74
|
+
unfencedCron = measured(rows.filter(({ parsed }) =>
|
|
75
|
+
cronLaunchedActionsWithFence(parsed.command).some(action =>
|
|
76
|
+
!!action.script && !(FENCE_ROLES as readonly string[]).includes(action.fenceRole ?? '')
|
|
77
|
+
&& isHqAction(action.script)))
|
|
78
|
+
.map(({ raw, parsed }) => ({ raw, cron: parsed.cron, command: parsed.command })));
|
|
79
|
+
} catch (error) { unfencedCron = unknown(message(error)); }
|
|
80
|
+
try {
|
|
81
|
+
if (unfencedCron.status !== 'measured') throw new Error(unfencedCron.reason);
|
|
82
|
+
// The registry accepts an already-inventoried schedule set. Never open the production DB.
|
|
83
|
+
const db = openSchedulesDb(':memory:');
|
|
84
|
+
let schedules: ReturnType<typeof listSchedules>;
|
|
85
|
+
try { inventoryCrontab(db, { crontab: text }); schedules = listSchedules(db); }
|
|
86
|
+
finally { db.close(); }
|
|
87
|
+
if (localHostError) throw new Error(localHostError);
|
|
88
|
+
if (!from) throw new Error('local HQ host identity not confirmed (~/.elanous-hq/host missing)');
|
|
89
|
+
// Cron rows use os.hostname(); match that label only after confirming this machine's HQ identity.
|
|
90
|
+
const names = new Set([from, hostname()]);
|
|
91
|
+
hostLoops = measured((deps.loops ?? listAllLoops)({ ...deps.loopOptions, schedules })
|
|
92
|
+
.filter(loop => !!loop.host && names.has(loop.host)));
|
|
93
|
+
} catch (error) { hostLoops = unknown(message(error)); }
|
|
94
|
+
let targetHost: MovePlan['targetHost'];
|
|
95
|
+
try {
|
|
96
|
+
const r = (deps.ssh ?? defaultSshRunner)(to,
|
|
97
|
+
'if [ -f "$HOME/.elanous-hq/host" ]; then printf "present\\n"; cat "$HOME/.elanous-hq/host"; elif [ ! -e "$HOME/.elanous-hq/host" ]; then printf "absent\\n"; else exit 2; fi');
|
|
98
|
+
if (r.status !== 0) throw new Error(r.stderr.trim() || `ssh exit=${r.status}`);
|
|
99
|
+
const [presence, ...lines] = r.stdout.split('\n');
|
|
100
|
+
if (presence === 'absent') targetHost = measured({ exists: false, name: null });
|
|
101
|
+
else if (presence === 'present' && lines.join('\n').trim() && !/[\r\n]/.test(lines.join('\n').trim()))
|
|
102
|
+
targetHost = measured({ exists: true, name: lines.join('\n').trim() });
|
|
103
|
+
else throw new Error('target host file unreadable or empty');
|
|
104
|
+
} catch (error) { targetHost = unknown(message(error)); }
|
|
105
|
+
let nexusLaunchd: MovePlan['nexusLaunchd'];
|
|
106
|
+
try {
|
|
107
|
+
const r = (deps.launchctl ?? (() => run('launchctl', ['list', 'com.elanous.nexus'])))();
|
|
108
|
+
if (r.status === 0) nexusLaunchd = measured({ present: true, measuredOn: 'local' });
|
|
109
|
+
else if (r.status !== null && /Could not find service|service not found|not found/i.test(r.stderr)) nexusLaunchd = measured({ present: false, measuredOn: 'local' });
|
|
110
|
+
else throw new Error(r.stderr.trim() || `launchctl exit=${r.status}`);
|
|
111
|
+
} catch (error) { nexusLaunchd = unknown(message(error)); }
|
|
112
|
+
return { to, from, unfencedCron, hostLoops, targetHost, nexusLaunchd };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function formatMovePlan(plan: MovePlan): string {
|
|
116
|
+
const lines = [`hq move-plan: ${plan.from ?? '못 잼'} → ${plan.to} (read-only)`];
|
|
117
|
+
const section = <T>(name: string, result: Measurement<T>, render: (value: T) => string[]) => {
|
|
118
|
+
lines.push(`${name}: ${result.status === 'measured' ? '' : `못 잼 (${result.reason})`}`.trimEnd());
|
|
119
|
+
if (result.status === 'measured') lines.push(...render(result.value));
|
|
120
|
+
};
|
|
121
|
+
section('① unfenced elanous crontab', plan.unfencedCron, rows => rows.length ? rows.map(row => ` ${row.raw}`) : [' none']);
|
|
122
|
+
section('② local host loops (loop status --all)', plan.hostLoops, rows => rows.length ? rows.map(row => ` ${row.id} · ${row.host} · ${row.title}`) : [' none']);
|
|
123
|
+
section('③ target ~/.elanous-hq/host', plan.targetHost, host => [` ${host.exists ? host.name : 'absent'}`]);
|
|
124
|
+
section('④ local launchd com.elanous.nexus', plan.nexusLaunchd, service => [` ${service.present ? 'present' : 'absent'} (measured on local machine)`]);
|
|
125
|
+
return lines.join('\n');
|
|
126
|
+
}
|
package/src/hq/promote.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// HQ promotion runbook: read-only preview by default; all remote mutations are behind --apply.
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { getUserConfig } from '../user-config.js';
|
|
4
|
+
import { debug } from '../debug/log.js';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { effectiveInstanceRoot } from '../instance/resolve.js';
|
|
7
|
+
import { appendSeatRequestRows, listSeatRequests, withSeatRequestLedgerLock } from '../seat-dispatch/seat-request-ledger.js';
|
|
4
8
|
import { parseLease, sshLeaseStore, defaultSshRunner, localShellRunner } from './lease.js';
|
|
5
9
|
|
|
6
10
|
export interface PromoteRunner {
|
|
@@ -295,6 +299,33 @@ export function promoteHq(host: string, apply = false, runner: PromoteRunner = d
|
|
|
295
299
|
}
|
|
296
300
|
}
|
|
297
301
|
|
|
302
|
+
/** One OP seat request (never a CEO decision card · 대표 10-05 10:03); idempotent by key. */
|
|
303
|
+
export type HqOpRequest = { key: string; text: string; [field: string]: unknown };
|
|
304
|
+
export type HqOpRequestWriter = (request: HqOpRequest) => { key: string; created: boolean };
|
|
305
|
+
|
|
306
|
+
export function writeOpSeatRequest(request: HqOpRequest, root = effectiveInstanceRoot()): { key: string; created: boolean } {
|
|
307
|
+
const path = join(root, 'seat-requests', 'requests.jsonl');
|
|
308
|
+
return withSeatRequestLedgerLock(path, () => {
|
|
309
|
+
if (listSeatRequests(root).some((row) => row.key === request.key)) return { key: request.key, created: false };
|
|
310
|
+
appendSeatRequestRows(path, [{ ...request, seat: 'OP', status: 'pending' as const, queuedAt: new Date().toISOString(), source: 'hq-arbiter' }]);
|
|
311
|
+
return { key: request.key, created: true };
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Propose promotion to the OP seat for a silent lease generation; the runbook runs only under explicit opt-in (see hqArbiterCheck). */
|
|
316
|
+
export function proposeHqPromotion(
|
|
317
|
+
input: { holder: string; standby: string; generation: number; streak: number },
|
|
318
|
+
deps: { request?: HqOpRequestWriter; log?: typeof debug.log } = {},
|
|
319
|
+
): { promoted: false; requestKey: string; created: boolean } {
|
|
320
|
+
const key = `hq:promote:${input.holder}:${input.generation}`;
|
|
321
|
+
const written = (deps.request ?? writeOpSeatRequest)({ key,
|
|
322
|
+
text: `본부 ${input.holder}(임대 세대 ${input.generation}) 연속 무응답 ${input.streak}회 — ${input.standby} 승격을 OP 가 판단한다(자동 승격 꺼짐). 승격하면 \`eln hq promote --to ${input.standby} --apply\`.`,
|
|
323
|
+
holder: input.holder, standby: input.standby, generation: input.generation, streak: input.streak });
|
|
324
|
+
try { (deps.log ?? debug.log.bind(debug))('hq.arbiter', 'promote-proposed', { ...input, ref: key, mode: 'op-request', created: written.created }); }
|
|
325
|
+
catch { /* observation cannot change the proposal */ }
|
|
326
|
+
return { promoted: false, requestKey: key, created: written.created };
|
|
327
|
+
}
|
|
328
|
+
|
|
298
329
|
export function formatPromote(result: PromoteResult): string {
|
|
299
330
|
return [`hq promote --to ${result.host} ${result.apply ? '--apply' : '(dry-run)'}`,
|
|
300
331
|
...result.lines.map(line => `${line.step}: ${line.status} · ${line.measurement}${line.status === 'ready' ? ` · ${line.command}` : ''}`)].join('\n');
|
package/src/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { collectCommandEntrances, renderCommandEntrances } from './self-dev/entr
|
|
|
31
31
|
import { isGoalAuthorFileName } from './self-implement/goal-document.js';
|
|
32
32
|
import { DEV_PIPELINE_SINK_SURFACE } from './self-implement/self-cli-sink-surface.js';
|
|
33
33
|
import { registerPrivateLedgerCommands } from './cli/private-ledger-commands.js';
|
|
34
|
+
import { registerSeatRequestsCommands } from './cli/seat-requests-cli.js';
|
|
34
35
|
import { applyTestStateDirFlagFromArgv } from './cli/test-state-dir-flag.js';
|
|
35
36
|
import { applyTestFlagFromArgv, observeTestFlagOwnership, uncoveredTestFlagPaths, staleTestFlagPaths } from './cli/test-flag.js';
|
|
36
37
|
import { registerPtyTakeoverCommands } from './cli/pty-takeover-cli.js';
|
|
@@ -46,6 +47,7 @@ import { registerPublishCommands } from './cli/publish-cli.js';
|
|
|
46
47
|
import { registerAutopilotCommands } from './cli/autopilot-cli.js';
|
|
47
48
|
import { registerRoleCommands } from './cli/role-cli.js';
|
|
48
49
|
import { registerMachineCommands } from './cli/machine-cli.js';
|
|
50
|
+
import { registerAgentEnvCommands } from './cli/agent-env-cli.js';
|
|
49
51
|
import { registerScheduleCommands } from './cli/schedule-cli.js';
|
|
50
52
|
export { runSchedule, scheduleCreatePlan } from './cli/schedule-cli.js';
|
|
51
53
|
export type { ScheduleDispatch } from './cli/schedule-cli.js';
|
|
@@ -80,6 +82,7 @@ import { registerStartCommand } from './cli/start-cli.js';
|
|
|
80
82
|
import { registerGroundingSourcesCli } from './grounding/sources-cli.js';
|
|
81
83
|
import { registerGraphCommands } from './graph-runner/graph-cli.js';
|
|
82
84
|
import { registerLoopCommands } from './loops/loop-cli.js';
|
|
85
|
+
import { registerHqMovePlanCommand } from './hq/hq.js';
|
|
83
86
|
import { registerCardCommand } from './task-cards/card-cli.js';
|
|
84
87
|
import { registerLaunchHeadCommands } from './launch-head/launch-head-cli.js';
|
|
85
88
|
import { acpServerHelpText } from './boot/acp-server-help.js';
|
|
@@ -856,6 +859,7 @@ registerPrCommands(program);
|
|
|
856
859
|
registerRepoCommands(program);
|
|
857
860
|
registerRoleCommands(program);
|
|
858
861
|
registerMachineCommands(program);
|
|
862
|
+
registerAgentEnvCommands(program);
|
|
859
863
|
// CLI entry: where action → resolveCurrentInstance → resolveInstance (instance universe).
|
|
860
864
|
registerWhereCommand(program);
|
|
861
865
|
registerShadowCommand(program);
|
|
@@ -937,6 +941,18 @@ hqCmd.command('seen').description('이 호스트가 본 가장 높은 임대 세
|
|
|
937
941
|
else console.log(result.seen ? `hq seen: generation ${result.generation}` : 'hq seen: never');
|
|
938
942
|
process.exitCode = result.seen ? 0 : 3;
|
|
939
943
|
});
|
|
944
|
+
registerHqMovePlanCommand(hqCmd);
|
|
945
|
+
hqCmd.command('fence-audit').description('운영 crontab 의 HQ 울타리 밖 쓰기 잡 목록과 감싼 줄 제안(읽기 전용)')
|
|
946
|
+
.option('--json', 'JSON 출력')
|
|
947
|
+
.action(async (opts: { json?: boolean }) => {
|
|
948
|
+
try {
|
|
949
|
+
await hqSink();
|
|
950
|
+
const rows = (await import('./hq/fence-audit.js')).hqFenceAudit();
|
|
951
|
+
if (opts.json) await writeStdoutJson(`${JSON.stringify(rows)}\n`);
|
|
952
|
+
else if (!rows.length) console.log('hq fence-audit: unfenced 0');
|
|
953
|
+
else for (const row of rows) console.log(`${row.raw}\n role: ${row.recommendedRole}\n suggested: ${row.suggestedLine}`);
|
|
954
|
+
} catch (error) { console.error(`hq fence-audit: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
|
|
955
|
+
});
|
|
940
956
|
hqCmd.command('fence').description('이 호스트가 임대를 쥐었을 때만 명령 실행(아니면 건너뜀 · exit 0)')
|
|
941
957
|
.requiredOption('--role <role>', 'telegram-poller|cron|seat-loop|release-run|conatus|git-push')
|
|
942
958
|
.argument('<command...>', '-- 뒤의 명령')
|
|
@@ -947,6 +963,7 @@ hqCmd.command('fence').description('이 호스트가 임대를 쥐었을 때만
|
|
|
947
963
|
process.exitCode = hqFenceRun(opts.role as 'cron', command);
|
|
948
964
|
});
|
|
949
965
|
const seatCmd = program.command('seat').description('분배된 자리의 하루 루프와 보고');
|
|
966
|
+
registerSeatRequestsCommands(seatCmd);
|
|
950
967
|
seatCmd.command('answer <askId> <answer>').description('자리 질문에 답을 남기고 원래 표면으로 회신 예약')
|
|
951
968
|
.action(async (askId: string, answer: string) => {
|
|
952
969
|
try {
|
|
@@ -962,10 +979,10 @@ seatCmd.command('loop').requiredOption('--seat <SEAT>', 'MK|OP|TC|UX')
|
|
|
962
979
|
try {
|
|
963
980
|
// V3 F5: without the sink, the seat loop's judgment lines (seat.loop) never reach the log store when cron runs it.
|
|
964
981
|
await (await import('./domains/standalone-log-sink.js')).registerStandaloneLogSink('seat-loop');
|
|
965
|
-
const { runSeatLoopTurn } = await import('./seat-loop/seat-loop.js');
|
|
982
|
+
const { runSeatLoopTurn, seatLoopTickLine } = await import('./seat-loop/seat-loop.js');
|
|
966
983
|
const result = await runSeatLoopTurn(opts.seat);
|
|
967
984
|
if (opts.json) await writeStdoutJson(`${JSON.stringify(result)}\n`);
|
|
968
|
-
else console.log(
|
|
985
|
+
else console.log(seatLoopTickLine(result));
|
|
969
986
|
} catch (error) { console.error(`seat loop: ${error instanceof Error ? error.message : String(error)}`); process.exitCode = 1; }
|
|
970
987
|
});
|
|
971
988
|
seatCmd.command('report').requiredOption('--seat <SEAT>', 'MK|OP|TC|UX')
|
|
@@ -3457,6 +3474,22 @@ selfCmd
|
|
|
3457
3474
|
}
|
|
3458
3475
|
});
|
|
3459
3476
|
|
|
3477
|
+
selfCmd
|
|
3478
|
+
.command('speed-claims')
|
|
3479
|
+
.description('런 원장 발사→종결 시간의 자리별·결과별 분포를 읽기 전용으로 출력한다')
|
|
3480
|
+
.option('--json', '구조화된 JSON으로 출력한다')
|
|
3481
|
+
.action(async (opts: { json?: boolean }) => {
|
|
3482
|
+
try {
|
|
3483
|
+
const { measureSpeedClaims, renderSpeedClaims } = await import('./self-implement/speed-claims.js');
|
|
3484
|
+
const result = measureSpeedClaims();
|
|
3485
|
+
if (opts.json) await writeStdoutJson(JSON.stringify(result) + '\n');
|
|
3486
|
+
else console.log(renderSpeedClaims(result));
|
|
3487
|
+
} catch (error) {
|
|
3488
|
+
console.error(`speed-claims measurement failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3489
|
+
process.exitCode = 1;
|
|
3490
|
+
}
|
|
3491
|
+
});
|
|
3492
|
+
|
|
3460
3493
|
selfCmd
|
|
3461
3494
|
.command('goal-source-distribution')
|
|
3462
3495
|
.description('self-implement 런 원장의 start goalSource 분포를 읽기 전용으로 출력한다')
|
|
@@ -4379,6 +4412,9 @@ const selfOrchestrateCmd = selfCmd
|
|
|
4379
4412
|
.option('--no-pod-rebuild', 'pod: 이미지 판(elanous.commit)이 HEAD 와 달라도 다시 굽지 않는다 — 측정은 «이미지 판»을 잰다')
|
|
4380
4413
|
.option('--pod-pass-env <keys>', 'pod: 호스트 env 에서 Pod 로 넘길 키(쉼표) — 예 OPENROUTER_API_KEY,ANTHROPIC_API_KEY(벤치마크 과금 경로)')
|
|
4381
4414
|
.option('--bench-arms <spec>', 'pod 벤치마크: 골 1개를 팔마다 «라벨 한 줄만 다르게» 복제해 동시에 — "id=provider[:model][@KEY+KEY];…" (예 codex=openai-codex;or-kimi=openrouter:openrouter/moonshotai/kimi-k3@OPENROUTER_API_KEY) · --auto-merge 거부 · RFC fleet 슈퍼바이저 §A3')
|
|
4415
|
+
.addOption(new Option('--child-llm-provider <id>', 'pod: 구현 자식 LLM provider(openai-codex|grok) — 하니스 --child-llm-provider 가 이 자리로 온다(10-05)').hideHelp())
|
|
4416
|
+
.addOption(new Option('--child-llm-model <id>', 'pod: 구현 자식 LLM model(--child-llm-provider 와 함께)').hideHelp())
|
|
4417
|
+
.addOption(new Option('--child-llm-effort <level>', 'pod: 구현 자식 추론 노력(--child-llm-provider 와 함께)').hideHelp())
|
|
4382
4418
|
.option('--help-all', '모든 orchestrate 옵션 표시')
|
|
4383
4419
|
.action(function (this: Command, parts: string[], opts: { goalFile?: string[]; concurrency?: string; autoMerge?: boolean; autoReview?: boolean; openPr?: boolean; base?: string; teardown?: boolean; resume?: string; board?: boolean; decompose?: boolean; fabricDecompose?: boolean; maxTasks?: string; supervise?: boolean; superviseRounds?: string; json?: boolean }) {
|
|
4384
4420
|
return (async () => {
|
|
@@ -4613,6 +4649,19 @@ const selfOrchestrateCmd = selfCmd
|
|
|
4613
4649
|
let rotationAccounts: readonly string[] | undefined;
|
|
4614
4650
|
let podProvider: 'openai-codex' | 'grok' = 'openai-codex';
|
|
4615
4651
|
let grokApiKeyOptIn = false;
|
|
4652
|
+
{
|
|
4653
|
+
// 10-05 PODPROVIDER: refuse child choices this Pod launch cannot honour instead of dropping them.
|
|
4654
|
+
const child = opts as { childLlmProvider?: string; childLlmModel?: string; childLlmEffort?: string };
|
|
4655
|
+
const named = child.childLlmProvider?.trim();
|
|
4656
|
+
const refuse = !named && (child.childLlmModel?.trim() || child.childLlmEffort?.trim())
|
|
4657
|
+
? 'pod: --child-llm-model/--child-llm-effort 는 --child-llm-provider 와 함께'
|
|
4658
|
+
: named && benchArms
|
|
4659
|
+
? 'pod: --child-llm-provider 는 --bench-arms 와 함께 못 쓴다(팔마다 모델이 하나다)'
|
|
4660
|
+
: named === 'grok' && explicitPodAccount
|
|
4661
|
+
? 'pod: --child-llm-provider grok 은 --pod-account 와 함께 못 쓴다(그것은 codex 계정을 고정한다)'
|
|
4662
|
+
: undefined;
|
|
4663
|
+
if (refuse) { ui.error(refuse); debug.log('self-implement.pod', 'child-provider-refused', { provider: named ?? null, reason: refuse }); process.exit(2); }
|
|
4664
|
+
}
|
|
4616
4665
|
if (!explicitPodAccount && !benchArms) {
|
|
4617
4666
|
const { inspectCodexRotation } = await import('./oauth/codex-account-store.js');
|
|
4618
4667
|
const { planPodProvider, makePodAccountBroker } = await import('./task-orchestrator/surfaces/pod-account-broker.js');
|
|
@@ -4651,17 +4700,31 @@ const selfOrchestrateCmd = selfCmd
|
|
|
4651
4700
|
if (!opts.json) ui.warn(podGrokSkippedLine(grokCheck));
|
|
4652
4701
|
}
|
|
4653
4702
|
}
|
|
4654
|
-
|
|
4703
|
+
// 10-05 PODPROVIDER: a launch that named the child provider gets it, or is refused — never a silent codex.
|
|
4704
|
+
const namedChild = (opts as { childLlmProvider?: string }).childLlmProvider?.trim();
|
|
4705
|
+
const { podNamedChildProvider, podFallbackCredentials, podGrokSubscriptionUsable: namedGrokUsable } = await import('./task-orchestrator/surfaces/self-implement-pod.js');
|
|
4706
|
+
const namedChildDecision = podNamedChildProvider(namedChild, {
|
|
4707
|
+
grokSubscription: namedChild === 'grok' && credential?.kind === 'subscription' && (grokSubscriptionUsable || namedGrokUsable().usable),
|
|
4708
|
+
grokApiKey: credential?.kind === 'api_key' && grokApiKeyOptIn,
|
|
4709
|
+
});
|
|
4710
|
+
if (namedChildDecision.refuse) { ui.error(namedChildDecision.refuse); debug.log('self-implement.pod', 'child-provider-refused', { provider: namedChild, reason: namedChildDecision.refuse }); process.exit(2); }
|
|
4711
|
+
// A named codex child must stay codex — the plan may not fall back to grok (post-review must-fix).
|
|
4712
|
+
const namedCodex = namedChildDecision.provider === 'openai-codex';
|
|
4713
|
+
const plan: import('./task-orchestrator/surfaces/pod-account-broker.js').PodProviderPlan = namedChildDecision.provider === 'grok'
|
|
4714
|
+
? { provider: 'grok', excluded: [], grokSubscriptionEligible: credential?.kind === 'subscription' }
|
|
4715
|
+
: planPodProvider({
|
|
4655
4716
|
codexCandidates: inspectCodexRotation().candidates,
|
|
4656
4717
|
// Per-account caps (대표 default:97) override the Pod default cap — without them default was dropped at 60%.
|
|
4657
4718
|
thresholdPercentByAccount: getUserConfig().llm?.codexAccountRotationThresholdPercentByAccount,
|
|
4658
4719
|
// 대표 한도 정책 한 값(`llm.codexQuotaPolicy`) — 크레딧·폴백을 같이 정한다.
|
|
4659
4720
|
creditsAllowed: codexPolicyAllowsCredits(quotaPolicy),
|
|
4660
|
-
|
|
4661
|
-
|
|
4721
|
+
...podFallbackCredentials(namedChildDecision.provider, {
|
|
4722
|
+
grokSubscription: codexPolicyAllowsFallback(quotaPolicy) && grokSubscriptionUsable,
|
|
4723
|
+
grokApiKey: codexPolicyAllowsFallback(quotaPolicy) && credential?.kind === 'api_key',
|
|
4724
|
+
}),
|
|
4662
4725
|
grokApiKeyOptIn,
|
|
4663
4726
|
});
|
|
4664
|
-
if (plan.provider === null) { ui.error(`pod: 쓸 codex 계정이 없다 — ${plan.reasons.join(' · ')} · 계정을 명시하려면 --pod-account <이름>`); process.exit(2); }
|
|
4727
|
+
if (plan.provider === null) { ui.error(`pod: 쓸 codex 계정이 없다 — ${plan.reasons.join(' · ')}${namedCodex ? ' · child provider 를 openai-codex 로 지정해 grok 으로 넘기지 않는다' : ''} · 계정을 명시하려면 --pod-account <이름>`); process.exit(2); }
|
|
4665
4728
|
podProvider = plan.provider;
|
|
4666
4729
|
if (plan.provider === 'openai-codex') {
|
|
4667
4730
|
accountBroker = makePodAccountBroker({ usable: plan.accounts, excluded: plan.excluded });
|
|
@@ -4670,6 +4733,10 @@ const selfOrchestrateCmd = selfCmd
|
|
|
4670
4733
|
{ const { emitDecision } = await import('./live/detail-switch.js');
|
|
4671
4734
|
emitDecision({ kind: 'ROUTE', what: `Pod 계정 배분 ${plan.accounts.join(' → ')}`, reason: plan.excluded.length ? `뺌 ${plan.excluded.map((x) => `${x.name}(${x.why})`).join(', ')}` : '모든 계정 여유', purpose: plan.creditAccounts ? '구독 잔량 0 — 정책상 크레딧으로 병렬 조각을 돌린다' : '병렬 조각이 한 계정에 몰리지 않게 잔량 순으로 돌려 준다', phase: 'dispatch', target: `Pod ${plan.accounts.length}계정`, paths: plan.accounts.length + plan.excluded.length }); }
|
|
4672
4735
|
if (!opts.json) ui.info(`[pod] 계정 배분(잔량 순 · 돌려 가며): ${plan.accounts.join(' → ')}${plan.creditAccounts ? ' · 💳 크레딧(구독 잔량 0 · 허가됨)' : ''}${plan.excluded.length ? ` · 뺌 ${plan.excluded.map((x) => `${x.name}(${x.why})`).join(', ')}` : ''}`);
|
|
4736
|
+
} else if (namedChildDecision.provider === 'grok') {
|
|
4737
|
+
const model = (opts as { childLlmModel?: string }).childLlmModel?.trim() || defaultGrokModel().id;
|
|
4738
|
+
debug.log('self-implement.pod', 'provider-named', { provider: 'grok', model, credential: credential?.kind ?? null });
|
|
4739
|
+
if (!opts.json) ui.info(`[pod] child provider = grok(모델 ${model}) — 발사 인자로 지정됨`);
|
|
4673
4740
|
} else {
|
|
4674
4741
|
const model = defaultGrokModel().id;
|
|
4675
4742
|
debug.log('self-implement.pod', 'provider-fallback', { from: 'openai-codex', to: 'grok', excluded: plan.excluded, model });
|
|
@@ -4697,7 +4764,10 @@ const selfOrchestrateCmd = selfCmd
|
|
|
4697
4764
|
...(podSource.kind === 'bundle' ? { headCommit: podSource.headCommit, sizeBytes: podSource.sizeBytes } : {}),
|
|
4698
4765
|
});
|
|
4699
4766
|
}
|
|
4700
|
-
const
|
|
4767
|
+
const childFlags = opts as { childLlmProvider?: string; childLlmModel?: string; childLlmEffort?: string };
|
|
4768
|
+
const podChildLlm = childFlags.childLlmProvider?.trim() ? { childProviderExplicit: true, ...(childFlags.childLlmModel?.trim() ? { childModel: childFlags.childLlmModel.trim() } : {}), ...(childFlags.childLlmEffort?.trim() ? { childEffort: childFlags.childLlmEffort.trim() } : {}) } : {};
|
|
4769
|
+
if (childFlags.childLlmProvider?.trim()) debug.log('self-implement.pod', 'child-provider-named', { provider: childFlags.childLlmProvider.trim(), model: childFlags.childLlmModel ?? null, podProvider });
|
|
4770
|
+
const podBase = { hostSupervised: opts.supervise === true, ...podChildLlm, ...(remoteOnlyPool ? { imageCommit: image.imageCommit } : {}), account: podProvider === 'grok' ? 'grok' : explicitPodAccount ?? 'team', ...(accountBroker ? { accountBroker, rotationAccounts } : {}), ...(podProvider === 'grok' ? { provider: 'grok' as const, grokApiKeyOptIn } : {}), passEnv, ...(pool ? { pool } : {}), ...((opts as { podSkillEnv?: boolean }).podSkillEnv ? { skillEnv: true } : {}), ...(podSource ? { source: podSource } : {}) };
|
|
4701
4771
|
if (benchArms) {
|
|
4702
4772
|
const { benchPodSpawn } = await import('./task-orchestrator/surfaces/self-implement-pod.js');
|
|
4703
4773
|
podSpawn = benchPodSpawn(benchArms, podBase);
|
|
@@ -61,8 +61,22 @@ export interface IntakeCheckEvidence {
|
|
|
61
61
|
readonly contrary?: boolean;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export interface IntakeReplacementCandidate {
|
|
65
|
+
readonly tool: string;
|
|
66
|
+
readonly paidDependency: string;
|
|
67
|
+
readonly status: '현장 시험 필요';
|
|
68
|
+
/** A comparison claim is not evidence that any of these checks have passed. */
|
|
69
|
+
readonly trial: {
|
|
70
|
+
readonly installation: '미검증';
|
|
71
|
+
readonly sampleRun: '미검증';
|
|
72
|
+
readonly license: '미검증';
|
|
73
|
+
readonly maintenanceStatus: '미검증';
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
export interface IntakeCheckItem {
|
|
65
78
|
readonly fact: string;
|
|
79
|
+
readonly replacementCandidate?: IntakeReplacementCandidate;
|
|
66
80
|
/** Source identity preserved from the supplied fact for card-backed goal evidence. */
|
|
67
81
|
readonly sourceRef?: string;
|
|
68
82
|
readonly originalClaims?: readonly string[];
|
|
@@ -1053,6 +1067,40 @@ function factKey(text: string): string {
|
|
|
1053
1067
|
return text.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
1054
1068
|
}
|
|
1055
1069
|
|
|
1070
|
+
/** A stated possibility is a trial candidate, not proof of a working replacement. */
|
|
1071
|
+
function replacementCandidate(fact: string): IntakeReplacementCandidate | undefined {
|
|
1072
|
+
if (!/(?:대체(?:하|한|할| 가능| 후보|재)|replace|replacement|alternative to)/i.test(fact)
|
|
1073
|
+
|| /(?:대체하지\s*(?:는|도|조차|를)?\s*(?:않|못)|대체\s*못|대체할\s*수\s*(?:는|도|가)?\s*없|대체(?:가|는| 후보가)?\s*(?:아니|불가)|cannot replace|can't replace|does not replace|not a replacement)/i.test(fact)) return undefined;
|
|
1074
|
+
const paid = fact.match(/(?:유료|paid)\s*(?:(?:서비스|의존성|도구|툴|API|dependency|service|tool)\s*)?`?([a-z][\w.-]+)`?/i);
|
|
1075
|
+
// firecrawl is a named paid-dependency example even when the claim omits the word "paid".
|
|
1076
|
+
const paidDependency = paid?.[1] ?? fact.match(/(?:^|[^\w])`?(firecrawl)`?(?=$|[^\w])/i)?.[1];
|
|
1077
|
+
if (!paidDependency) return undefined;
|
|
1078
|
+
const names = [...fact.matchAll(/`([a-z][\w.-]+)`/gi)].map((match) => match[1]!);
|
|
1079
|
+
const explicit = names.filter((name) => name.toLowerCase() !== paidDependency.toLowerCase());
|
|
1080
|
+
const plain = fact.match(/^\s*([a-z][\w.-]+)\s*(?:가|이|는|은|으로|로|can\b|could\b|may\b)/i)?.[1];
|
|
1081
|
+
const tool = explicit.length === 1 ? explicit[0] : explicit.length === 0 ? plain : undefined;
|
|
1082
|
+
if (!tool || tool.toLowerCase() === paidDependency.toLowerCase() || tool.toLowerCase() === 'elanous') return undefined;
|
|
1083
|
+
// Names appearing together do not establish the direction of replacement.
|
|
1084
|
+
const clause = fact.replace(/`/g, '').replace(/(?:유료|paid)\s*(?:(?:서비스|의존성|도구|툴|API|dependency|service|tool)\s*)?/gi, '');
|
|
1085
|
+
const escape = (name: string): string => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1086
|
+
const name = (value: string): string => `(?<![\\w.-])${escape(value)}(?![\\w.-])`;
|
|
1087
|
+
const toolName = name(tool);
|
|
1088
|
+
const dependencyName = name(paidDependency);
|
|
1089
|
+
const replaces = (subject: string, object: string): boolean => [
|
|
1090
|
+
new RegExp(`${subject}\\s*(?:이|가|은|는)?\\s+${object}\\s*(?:을|를)\\s*대체(?:하|한|할| 가능| 후보|재)`, 'i'),
|
|
1091
|
+
new RegExp(`${object}\\s*(?:을|를)\\s+${subject}\\s*(?:이|가|은|는)\\s*대체(?:하|한|할| 가능| 후보|재)`, 'i'),
|
|
1092
|
+
new RegExp(`${subject}\\s+(?:can |could |may )?replace(?:s|d)?\\s+${object}`, 'i'),
|
|
1093
|
+
new RegExp(`${subject}\\s+(?:is |could be |may be )?(?:an? )?(?:replacement|alternative)\\s+to\\s+${object}`, 'i'),
|
|
1094
|
+
].some((pattern) => pattern.test(clause));
|
|
1095
|
+
if (!replaces(toolName, dependencyName) || replaces(dependencyName, toolName)) return undefined;
|
|
1096
|
+
return {
|
|
1097
|
+
tool,
|
|
1098
|
+
paidDependency,
|
|
1099
|
+
status: '현장 시험 필요',
|
|
1100
|
+
trial: { installation: '미검증', sampleRun: '미검증', license: '미검증', maintenanceStatus: '미검증' },
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1056
1104
|
function isLens(value: string): value is PreprocessLens {
|
|
1057
1105
|
return (PREPROCESS_LENSES as readonly string[]).includes(value);
|
|
1058
1106
|
}
|
|
@@ -1414,8 +1462,17 @@ export function runIntakeCheck(
|
|
|
1414
1462
|
}
|
|
1415
1463
|
// src/index.ts intake check .action → runIntakeCheck / runIntakeCheckDocument → runIntakeCheck → judgeFact.
|
|
1416
1464
|
const judged = judgeFact(fact, ruler, deps);
|
|
1465
|
+
const candidate = replacementCandidate(fact.text);
|
|
1466
|
+
const verdict = candidate && judged.verdict !== '못 쟀다' ? '판단 필요' : judged.verdict;
|
|
1467
|
+
const current = candidate && judged.verdict !== '못 쟀다'
|
|
1468
|
+
? `${judged.current} · 대체 후보 ${candidate.tool} → ${candidate.paidDependency}: 설치·샘플 실행·라이선스·유지보수 상태 현장 시험 필요`
|
|
1469
|
+
: judged.current;
|
|
1417
1470
|
const item: IntakeCheckItem = {
|
|
1418
1471
|
...judged,
|
|
1472
|
+
verdict,
|
|
1473
|
+
current,
|
|
1474
|
+
line: `${fact.text} → ${current} → ${verdict}`,
|
|
1475
|
+
...(candidate ? { replacementCandidate: candidate } : {}),
|
|
1419
1476
|
quotes: [quote],
|
|
1420
1477
|
...(fact.sourceRef ? { sourceRef: fact.sourceRef } : {}),
|
|
1421
1478
|
...(fact.originalClaim ? { originalClaims: [fact.originalClaim] } : {}),
|
|
@@ -1711,6 +1768,11 @@ export function renderIntakeCheckReport(report: IntakeCheckReport): string {
|
|
|
1711
1768
|
for (const item of report.items) {
|
|
1712
1769
|
lines.push(item.line);
|
|
1713
1770
|
lines.push(` 판정: ${item.verdict}`);
|
|
1771
|
+
if (item.replacementCandidate) {
|
|
1772
|
+
const candidate = item.replacementCandidate;
|
|
1773
|
+
lines.push(` 대체 후보: ${candidate.tool} → 유료 의존성 ${candidate.paidDependency} (${candidate.status})`);
|
|
1774
|
+
lines.push(' 현장 시험: 설치 미검증 · 샘플 실행 미검증 · 라이선스 미검증 · 유지보수 상태 미검증');
|
|
1775
|
+
}
|
|
1714
1776
|
if (item.goalDraftPath) lines.push(` 골 초안: ${item.goalDraftPath}`);
|
|
1715
1777
|
for (const quote of item.quotes) lines.push(` 인용: ${quote}`);
|
|
1716
1778
|
for (const original of item.originalClaims ?? []) lines.push(` 원 주장: ${original}`);
|
|
@@ -1746,6 +1808,7 @@ export function intakeCheckReportJson(report: IntakeCheckReport): Record<string,
|
|
|
1746
1808
|
current: item.current,
|
|
1747
1809
|
verdict: item.verdict,
|
|
1748
1810
|
line: item.line,
|
|
1811
|
+
...(item.replacementCandidate ? { replacementCandidate: item.replacementCandidate } : {}),
|
|
1749
1812
|
quotes: item.quotes,
|
|
1750
1813
|
evidence: item.evidence,
|
|
1751
1814
|
patterns: item.patterns,
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// 흡수 하루 다이제스트 — 그날 흡수한 것을 사람이 «발견»하는 자리. 노트 절(마크다운) ⊕ 텔레그램 짧은 판.
|
|
2
2
|
// 결정론만: 요약 한 줄은 각 노트가 이미 쓴 「한 줄 결론」을 뽑는다(LLM 을 다시 부르지 않는다).
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
-
import { basename, join
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
5
|
import { loadIntakeLedger, type IntakeItem } from './items.js';
|
|
6
6
|
import { readSavedCursorAt } from './collect-telegram-saved.js';
|
|
7
7
|
import { intakeOutboxDir, kstDay } from './route.js';
|
|
8
8
|
|
|
9
|
-
export interface DigestEntry { id: string; sources: string[]; url?: string; note?: string; noteName?: string; oneLiner?: string; axis: string }
|
|
9
|
+
export interface DigestEntry { id: string; sources: string[]; url?: string; note?: string; noteName?: string; oneLiner?: string; axis: string; impact?: { fact: string; current: string; action: string } }
|
|
10
10
|
export interface IntakeDigest { day: string; absorbed: DigestEntry[]; goals: { fact: string; url?: string }[]; review?: { fact: string; note?: string }[]; grounding: number; release: number; manual: number; savedSilence?: { days: number; lastNewAt: string } }
|
|
11
11
|
|
|
12
12
|
const AXIS_LABEL: Record<string, string> = {
|
|
@@ -43,17 +43,38 @@ const SAVED_SILENCE_MS = 48 * 3600_000;
|
|
|
43
43
|
export function buildIntakeDigest(root: string, day: string, readFile: (p: string) => string | undefined = (p) => (existsSync(p) ? readFileSync(p, 'utf8') : undefined), now: Date = new Date()): IntakeDigest {
|
|
44
44
|
const items = [...loadIntakeLedger(root).items.values()]
|
|
45
45
|
.filter((i) => (i.status === 'absorbed' || i.status === 'routed') && kstDay(i.lastSeenAt) === day && i.outputs.some((o) => o.kind === 'note'));
|
|
46
|
+
const out = intakeOutboxDir(root);
|
|
47
|
+
const relevant = new Map<string, NonNullable<DigestEntry['impact']>>();
|
|
48
|
+
for (const [folder, action] of [
|
|
49
|
+
['goals', '없는 기능의 골 후보를 세운다'],
|
|
50
|
+
['manual', '문서·약속을 현재 동작에 맞게 보강한다'],
|
|
51
|
+
['review', '대조 근거를 추가로 확인한다'],
|
|
52
|
+
['release', '이미 착지한 기능의 바깥 맥락을 릴리스 노트에 반영한다'],
|
|
53
|
+
] as const) {
|
|
54
|
+
for (const row of readJsonl(join(out, folder, `${day}.jsonl`))) {
|
|
55
|
+
if (typeof row.id !== 'string' || relevant.has(row.id)) continue;
|
|
56
|
+
const fact = folder === 'release' ? row.title : row.fact;
|
|
57
|
+
if (typeof fact !== 'string' || !fact.trim()) continue;
|
|
58
|
+
relevant.set(row.id, {
|
|
59
|
+
fact,
|
|
60
|
+
current: typeof (folder === 'release' ? row.summary : row.current) === 'string'
|
|
61
|
+
? String(folder === 'release' ? row.summary : row.current) : '대조 근거 없음',
|
|
62
|
+
action,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
46
66
|
const absorbed = items.map((i: IntakeItem): DigestEntry => {
|
|
47
67
|
const note = [...i.outputs].reverse().find((o) => o.kind === 'note')?.ref;
|
|
48
68
|
const md = note ? readFile(note) : undefined;
|
|
69
|
+
const impact = relevant.get(i.id);
|
|
49
70
|
return {
|
|
50
71
|
id: i.id, sources: i.sources, ...(i.url ? { url: i.url } : {}),
|
|
51
72
|
...(note ? { note, noteName: basename(note, '.md') } : {}),
|
|
52
73
|
...(md ? { oneLiner: noteOneLiner(md) } : {}),
|
|
53
74
|
axis: AXIS_LABEL[i.judgement?.axis ?? i.axis ?? ''] ?? (i.sources.includes('telegram-saved') ? '내가 저장한 것' : '그 밖'),
|
|
75
|
+
...(impact ? { impact } : {}),
|
|
54
76
|
};
|
|
55
77
|
});
|
|
56
|
-
const out = intakeOutboxDir(root);
|
|
57
78
|
const goals = readJsonl(join(out, 'goals', `${day}.jsonl`)).map((g) => ({ fact: String(g.fact ?? ''), ...(g.url ? { url: String(g.url) } : {}) }));
|
|
58
79
|
const review = readJsonl(join(out, 'review', `${day}.jsonl`)).map((r) => ({ fact: String(r.fact ?? ''), ...(r.note ? { note: String(r.note) } : {}) }));
|
|
59
80
|
const grounding = readJsonl(join(out, 'grounding.jsonl')).filter((g) => typeof g.at === 'string' && kstDay(g.at) === day).length;
|
|
@@ -94,18 +115,19 @@ export function renderDigestMarkdown(d: IntakeDigest): string {
|
|
|
94
115
|
return L.join('\n');
|
|
95
116
|
}
|
|
96
117
|
|
|
97
|
-
/** 텔레그램
|
|
98
|
-
export function renderDigestTelegram(d: IntakeDigest,
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
/** 텔레그램 — 흡수 렌즈가 대조한 항목만 SCQA 짧은 판으로 낸다. */
|
|
119
|
+
export function renderDigestTelegram(d: IntakeDigest, _opts: { vaultRoot?: string; notePath?: string } = {}): string {
|
|
120
|
+
const touching = d.absorbed.filter((e) => e.impact);
|
|
121
|
+
const L = [`흡수 ${d.absorbed.length} → 우리에게 닿는 것 ${touching.length}`];
|
|
122
|
+
if (!touching.length) return L[0];
|
|
123
|
+
for (const e of touching.slice(0, 3)) {
|
|
124
|
+
L.push('',
|
|
125
|
+
`S 무엇: ${e.impact!.fact}`,
|
|
126
|
+
`C 우리에게 왜: ${e.impact!.current}`,
|
|
127
|
+
`A 그래서 무엇을 하나: ${e.impact!.action}`,
|
|
128
|
+
`🔗 원문 링크: ${e.url?.trim() || '링크 없음'}`,
|
|
129
|
+
);
|
|
109
130
|
}
|
|
131
|
+
if (d.absorbed.length > touching.length) L.push('', `그 밖 ${d.absorbed.length - touching.length}건 · 닿지 않음`);
|
|
110
132
|
return L.join('\n');
|
|
111
133
|
}
|
|
@@ -18,6 +18,12 @@ export interface FrontClassifierDecision extends IntakeFrontRouteDecision {
|
|
|
18
18
|
urlRoute: UrlRouteDecision | null;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** Only Telegram links explicitly routed to saved/absorbed work enter the event lane. */
|
|
22
|
+
export function telegramAbsorbUrls(input: FrontClassifierInput, decision: FrontClassifierDecision): string[] {
|
|
23
|
+
if (input.surface !== 'telegram' || decision.track !== 'absorb' || !decision.urlRoute) return [];
|
|
24
|
+
return decision.urlRoute.urls;
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
const QUESTION_WORD_RE = new RegExp(
|
|
22
28
|
`(?:${INTAKE_FRONT_QUESTION_WORDS.map((word) => /^[a-z]+$/i.test(word) ? `(?<![\\p{L}\\p{N}_])${word}(?![\\p{L}\\p{N}_])` : word).join('|')})[.!?。?!]*$`,
|
|
23
29
|
'iu',
|
|
@@ -5,6 +5,7 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { debug } from '../debug/log.js';
|
|
7
7
|
import { findTrack } from '../autopilot/mission-phase-track.js';
|
|
8
|
+
import { wasIntakeEventScheduled } from './route.js';
|
|
8
9
|
|
|
9
10
|
export const INTAKE_SOURCES = ['x', 'youtube', 'github', 'telegram-saved', 'telegram-bot', 'memo', 'pwa'] as const;
|
|
10
11
|
export type RegisteredIntakeSourceKind = 'rss' | 'command';
|
|
@@ -243,13 +244,13 @@ export function parseRawIntakeJsonl(text: string): { raws: RawIntakeItem[]; bad:
|
|
|
243
244
|
*/
|
|
244
245
|
const STALE_QUEUED_MS = 24 * 3600_000;
|
|
245
246
|
|
|
246
|
-
export function pickAbsorbQueue(instanceRoot: string, opts: { max: number; laneMax?: number; kind?: IntakeKind; dryRun?: boolean }, now = new Date().toISOString()): IntakeItem[] {
|
|
247
|
+
export function pickAbsorbQueue(instanceRoot: string, opts: { max: number; laneMax?: number; kind?: IntakeKind; dryRun?: boolean }, now = new Date().toISOString(), taskStatus?: Parameters<typeof wasIntakeEventScheduled>[3]): IntakeItem[] {
|
|
247
248
|
const userLeft = (i: IntakeItem) => i.sources.some((s) => s === 'telegram-saved' || s === 'memo');
|
|
248
249
|
const score = (i: IntakeItem) => Math.max(0, ...Object.entries(i.signals).filter(([k]) => k.endsWith('.score')).map(([, v]) => v));
|
|
249
250
|
const eligible = [...loadIntakeLedger(instanceRoot).items.values()]
|
|
250
251
|
// queued 로 옮긴 뒤 흡수·표시가 없이 하루가 지난 것(흡수 도중 죽은 판)은 다시 고른다.
|
|
251
252
|
.filter((i) => (i.status === 'new' || (i.status === 'queued' && Date.parse(now) - Date.parse(i.lastSeenAt) > STALE_QUEUED_MS))
|
|
252
|
-
&& !!i.url && (!opts.kind || i.kind === opts.kind))
|
|
253
|
+
&& !!i.url && !wasIntakeEventScheduled(instanceRoot, i.url, now, taskStatus) && (!opts.kind || i.kind === opts.kind))
|
|
253
254
|
.sort((a, b) => Number(userLeft(b)) - Number(userLeft(a)) || b.sources.length - a.sources.length || score(b) - score(a) || b.lastSeenAt.localeCompare(a.lastSeenAt));
|
|
254
255
|
const laneMax = opts.laneMax ?? 30;
|
|
255
256
|
const lane = eligible.filter((i) => i.sources.includes('telegram-saved')).slice(0, Math.max(0, laneMax));
|