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
package/src/loops/registry.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface LoopEntry {
|
|
|
28
28
|
title: string;
|
|
29
29
|
description: string | null;
|
|
30
30
|
owner?: string | null;
|
|
31
|
-
ownerSource?: 'header' | 'config' | 'seat' | 'default';
|
|
31
|
+
ownerSource?: 'header' | 'config' | 'seat' | 'seat-arg' | 'default';
|
|
32
32
|
mode?: string | null;
|
|
33
33
|
file: string;
|
|
34
34
|
trigger: { cron: string | null; events: string[] };
|
|
@@ -46,6 +46,8 @@ export interface LoopRegistryOptions {
|
|
|
46
46
|
/** An already-inventoried registry; keeps focused tests isolated. */
|
|
47
47
|
schedules?: ScheduleRow[];
|
|
48
48
|
scheduleAction?: (action: 'create' | 'enable' | 'disable', args: Record<string, unknown>) => Promise<unknown>;
|
|
49
|
+
/** Home used to expand `~`/`$HOME` in cron redirects; tests inject a temp dir instead of writing the real HOME. */
|
|
50
|
+
home?: string;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
function graphFiles(dir: string): string[] {
|
|
@@ -136,29 +138,43 @@ function schedules(opts: LoopRegistryOptions): ScheduleRow[] {
|
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
type OwnerSource = NonNullable<LoopEntry['ownerSource']>;
|
|
139
|
-
function resolveOwner(id: string, title: string, config: UserConfig, header?: unknown, seat?: string): { owner: string; ownerSource: OwnerSource } {
|
|
141
|
+
function resolveOwner(id: string, title: string, config: UserConfig, header?: unknown, seat?: string, seatSource: OwnerSource = 'seat'): { owner: string; ownerSource: OwnerSource } {
|
|
140
142
|
if (typeof header === 'string' && /^(OP|TC|MK|UX)$/.test(header)) return { owner: header, ownerSource: 'header' };
|
|
141
143
|
const owners = config.loops?.owners;
|
|
142
144
|
const configured = owners && (Object.hasOwn(owners, id) ? owners[id] : Object.hasOwn(owners, title) ? owners[title] : undefined);
|
|
143
145
|
if (configured) return { owner: configured, ownerSource: 'config' };
|
|
144
|
-
if (seat) return { owner: seat, ownerSource:
|
|
146
|
+
if (seat) return { owner: seat, ownerSource: seatSource };
|
|
145
147
|
return { owner: config.loops?.defaultOwner ?? 'OP', ownerSource: 'default' };
|
|
146
148
|
}
|
|
147
149
|
|
|
148
|
-
/**
|
|
149
|
-
function cronLogMtime(command: string): string
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
150
|
+
/** Output redirects in the installed cron line (including a quoted hq-fence payload) estimate runs, not success. */
|
|
151
|
+
function cronLogMtime(command: string, home: string = homedir()): { at?: string; targets: number } {
|
|
152
|
+
const expandHome = (path: string) => path.replace(/^~(?=\/)/, home).replace(/^\$HOME(?=\/)/, home);
|
|
153
|
+
const operand = String.raw`(?:'([^']+)'|"([^"]+)"|([^\s;|&'"<>]+))`;
|
|
154
|
+
const cdPattern = new RegExp(String.raw`(?:^|[\s'"])cd\s+${operand}\s*&&`, 'g');
|
|
155
|
+
const redirectPattern = new RegExp(String.raw`(?:[12]|&)?>{1,2}\s*${operand}`, 'g');
|
|
156
|
+
const cds: Array<{ index: number; cwd: string }> = [];
|
|
157
|
+
for (const match of command.matchAll(cdPattern)) {
|
|
158
|
+
const path = expandHome(match[1] ?? match[2] ?? match[3]!);
|
|
159
|
+
cds.push({ index: match.index, cwd: resolve(cds.at(-1)?.cwd ?? home, path) });
|
|
160
|
+
}
|
|
161
|
+
let latest: number | undefined;
|
|
162
|
+
let targets = 0;
|
|
163
|
+
for (const match of command.matchAll(redirectPattern)) {
|
|
164
|
+
const path = expandHome(match[1] ?? match[2] ?? match[3]!);
|
|
165
|
+
if (path === '/dev/null') continue;
|
|
166
|
+
const cwd = [...cds].reverse().find(cd => cd.index < match.index)?.cwd ?? home;
|
|
167
|
+
const target = resolve(cwd, path);
|
|
168
|
+
targets++;
|
|
169
|
+
try {
|
|
170
|
+
const stat = statSync(target);
|
|
171
|
+
if (stat.isFile()) latest = Math.max(latest ?? -Infinity, stat.mtimeMs);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
debug.log('loops.registry', 'cron-log-unavailable', { path: target, error: String(error) });
|
|
174
|
+
}
|
|
161
175
|
}
|
|
176
|
+
if (latest === undefined) debug.log('loops.registry', 'cron-log-unavailable', { command, targets, reason: 'no-output-files' });
|
|
177
|
+
return { at: latest === undefined ? undefined : new Date(latest).toISOString(), targets };
|
|
162
178
|
}
|
|
163
179
|
|
|
164
180
|
/** Where loop graphs live and where their cron line `cd`s — the same rule as the other elanous crons:
|
|
@@ -213,7 +229,7 @@ export function listLoops(opts: LoopRegistryOptions = {}): LoopEntry[] {
|
|
|
213
229
|
}
|
|
214
230
|
|
|
215
231
|
export interface AllLoopEntry extends CheckEntry {
|
|
216
|
-
kind: 'graph' | 'seat' | 'cron-shell' | 'orchestrator';
|
|
232
|
+
kind: 'graph' | 'seat' | 'cron-shell' | 'orchestrator' | 'unregistered';
|
|
217
233
|
title: string;
|
|
218
234
|
cron?: string;
|
|
219
235
|
command?: string;
|
|
@@ -268,16 +284,22 @@ function elanousActionStart(args: LaunchWord[]): number {
|
|
|
268
284
|
}
|
|
269
285
|
|
|
270
286
|
/** Include every launched segment of a wrapper's quoted shell payload, in execution order. */
|
|
271
|
-
function
|
|
272
|
-
const launches: LaunchWord[]
|
|
273
|
-
|
|
274
|
-
if (last.length) launches.push(last);
|
|
287
|
+
function launchedCommandsWithFence(command: string): Array<{ words: LaunchWord[]; fenceRole?: string }> {
|
|
288
|
+
const launches: Array<{ words: LaunchWord[]; fenceRole?: string }> = [];
|
|
289
|
+
launchedCommand(command, undefined, (words, fenceRole) => { if (words.length) launches.push({ words, ...(fenceRole ? { fenceRole } : {}) }); });
|
|
275
290
|
return launches;
|
|
276
291
|
}
|
|
277
292
|
|
|
293
|
+
function launchedCommands(command: string): LaunchWord[][] {
|
|
294
|
+
return launchedCommandsWithFence(command).map(({ words }) => words);
|
|
295
|
+
}
|
|
296
|
+
|
|
278
297
|
/** Only executable-prefix wrappers are stripped; a quoted argument mentioning a loop is not a launch. */
|
|
279
|
-
function launchedCommand(command: string, onFence?: (role: string) => void,
|
|
298
|
+
function launchedCommand(command: string, onFence?: (role: string) => void,
|
|
299
|
+
onLaunch?: (words: LaunchWord[], fenceRole?: string) => void, inheritedFenceRole?: string): LaunchWord[] {
|
|
280
300
|
const words = launchedWords(command);
|
|
301
|
+
let activeFenceRole = inheritedFenceRole;
|
|
302
|
+
const markFence = (role: string) => { activeFenceRole = role; onFence?.(role); };
|
|
281
303
|
const wrappers: Record<string, { values: readonly string[]; positionals: number }> = {
|
|
282
304
|
env: { values: ['-u', '--unset', '-C', '--chdir', '-S', '--split-string'], positionals: 0 },
|
|
283
305
|
flock: { values: ['-w', '--wait', '-E', '--conflict-exit-code'], positionals: 1 },
|
|
@@ -303,7 +325,7 @@ function launchedCommand(command: string, onFence?: (role: string) => void, onLa
|
|
|
303
325
|
for (let i = 2; i < separator; i++) {
|
|
304
326
|
const role = args[i]?.text === '--role' && i + 1 < separator ? args[i + 1]?.text
|
|
305
327
|
: args[i]?.text.startsWith('--role=') ? args[i]!.text.slice('--role='.length) : undefined;
|
|
306
|
-
if (role) {
|
|
328
|
+
if (role) { markFence(role); break; }
|
|
307
329
|
}
|
|
308
330
|
offset = words.length - args.length + separator + 1;
|
|
309
331
|
continue;
|
|
@@ -313,20 +335,19 @@ function launchedCommand(command: string, onFence?: (role: string) => void, onLa
|
|
|
313
335
|
if (words[commandAt]?.text === '--role' && !words[commandAt]?.quoted) {
|
|
314
336
|
const role = words[commandAt + 1]?.text;
|
|
315
337
|
if (role && FENCE_ROLES.includes(role as typeof FENCE_ROLES[number])) {
|
|
316
|
-
|
|
338
|
+
markFence(role);
|
|
317
339
|
commandAt += words[commandAt + 2]?.text === '--' ? 3 : 2;
|
|
318
340
|
}
|
|
319
341
|
} else if (FENCE_ROLES.includes(words[commandAt]?.text as typeof FENCE_ROLES[number]) && !words[commandAt]?.quoted) {
|
|
320
|
-
|
|
342
|
+
markFence(words[commandAt]!.text);
|
|
321
343
|
commandAt++;
|
|
322
344
|
}
|
|
323
345
|
if (words[commandAt]?.quoted && words[commandAt]!.text.includes(' ')) {
|
|
324
346
|
const parts = shellSegments(words[commandAt]!.text);
|
|
325
347
|
for (const part of parts.slice(0, -1)) {
|
|
326
|
-
|
|
327
|
-
if (launch.length) onLaunch?.(launch);
|
|
348
|
+
launchedCommand(part, onFence, onLaunch, activeFenceRole);
|
|
328
349
|
}
|
|
329
|
-
return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch);
|
|
350
|
+
return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch, activeFenceRole);
|
|
330
351
|
}
|
|
331
352
|
offset = commandAt;
|
|
332
353
|
continue;
|
|
@@ -348,15 +369,16 @@ function launchedCommand(command: string, onFence?: (role: string) => void, onLa
|
|
|
348
369
|
// The payload is its own shell line: each segment may launch a loop.
|
|
349
370
|
const parts = shellSegments(payload);
|
|
350
371
|
for (const part of parts.slice(0, -1)) {
|
|
351
|
-
|
|
352
|
-
if (launch.length) onLaunch?.(launch);
|
|
372
|
+
launchedCommand(part, onFence, onLaunch, activeFenceRole);
|
|
353
373
|
}
|
|
354
|
-
return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch);
|
|
374
|
+
return launchedCommand(parts.at(-1) ?? '', onFence, onLaunch, activeFenceRole);
|
|
355
375
|
}
|
|
356
376
|
if (index >= words.length) break;
|
|
357
377
|
offset = index;
|
|
358
378
|
}
|
|
359
|
-
|
|
379
|
+
const launch = words.slice(offset);
|
|
380
|
+
if (launch.length) onLaunch?.(launch, activeFenceRole);
|
|
381
|
+
return launch;
|
|
360
382
|
}
|
|
361
383
|
|
|
362
384
|
/** Split a shell line at top-level `&&`, `||`, `;` and `|` only — separators inside quotes stay in their word. */
|
|
@@ -408,6 +430,15 @@ function launchedScript(words: LaunchWord[]): { script: string; args: LaunchWord
|
|
|
408
430
|
return { script: words[offset]?.text ?? '', args: words.slice(offset + 1) };
|
|
409
431
|
}
|
|
410
432
|
|
|
433
|
+
/** Return each launched action with its own fence role; one fenced segment cannot cover a sibling. */
|
|
434
|
+
export function cronLaunchedActionsWithFence(command: string): Array<{ script: string; args: string[]; fenceRole?: string }> {
|
|
435
|
+
return shellSegments(command).flatMap(segment => launchedCommandsWithFence(segment).map(({ words, fenceRole }) => {
|
|
436
|
+
const { script, args } = launchedScript(words);
|
|
437
|
+
return { script, args: args.slice(isElanousExecutable(script) ? elanousActionStart(args) : 0).map(word => word.text),
|
|
438
|
+
...(fenceRole ? { fenceRole } : {}) };
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
|
|
411
442
|
/** Recognize direct .sh execution or sh/bash/zsh with option flags and a script or -c payload. */
|
|
412
443
|
function isCronShellCommand(command: string): boolean {
|
|
413
444
|
return shellSegments(command).some(segment => launchedCommands(segment).some(words => {
|
|
@@ -444,13 +475,31 @@ function launchedSeat(command: string): string | undefined {
|
|
|
444
475
|
return undefined;
|
|
445
476
|
}
|
|
446
477
|
|
|
478
|
+
function seatLoopCommand(command: string): { seat?: string; matched: boolean } {
|
|
479
|
+
for (const segment of shellSegments(command)) {
|
|
480
|
+
for (const words of launchedCommands(segment)) {
|
|
481
|
+
const { script, args } = launchedScript(words);
|
|
482
|
+
if (!isElanousExecutable(script)) continue;
|
|
483
|
+
const action = args.slice(elanousActionStart(args));
|
|
484
|
+
if (action[0]?.quoted || action[0]?.text !== 'seat' || action[1]?.quoted || action[1]?.text !== 'loop') continue;
|
|
485
|
+
if (!action.slice(2).some(word => !word.quoted && word.text === '--once')) continue;
|
|
486
|
+
const seatAt = action.findIndex((word, index) => index >= 2 && !word.quoted && word.text === '--seat');
|
|
487
|
+
const seat = seatAt >= 0 && !action[seatAt + 1]?.quoted ? action[seatAt + 1]?.text
|
|
488
|
+
: action.slice(2).find(word => !word.quoted && word.text.startsWith('--seat='))?.text.slice('--seat='.length);
|
|
489
|
+
return { matched: true, ...(seat && /^(OP|TC|MK|UX)$/.test(seat) ? { seat } : {}) };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return { matched: false };
|
|
493
|
+
}
|
|
494
|
+
|
|
447
495
|
function launchedOrchestrator(command: string): boolean {
|
|
448
496
|
return shellSegments(command).some(segment => launchedCommands(segment).some(words =>
|
|
449
497
|
/(?:^|\/)(?:\w+-)?orchestrator(?:\.[\w-]+)?$/i.test(launchedScript(words).script)));
|
|
450
498
|
}
|
|
451
499
|
|
|
452
|
-
/**
|
|
500
|
+
/** Name only recognizable CLI entry points. Unknown actions remain visible via the unregistered cron warning. */
|
|
453
501
|
function elanousCronName(command: string): string | undefined {
|
|
502
|
+
const known = new Set(['hq', 'harness', 'card', 'loop', 'graph', 'config', 'logs', 'where', '--version']);
|
|
454
503
|
for (const segment of shellSegments(command)) {
|
|
455
504
|
for (const words of launchedCommands(segment)) {
|
|
456
505
|
const { script, args } = launchedScript(words);
|
|
@@ -458,15 +507,9 @@ function elanousCronName(command: string): string | undefined {
|
|
|
458
507
|
const start = elanousActionStart(args);
|
|
459
508
|
const first = args[start];
|
|
460
509
|
const second = args[start + 1];
|
|
461
|
-
if (!first ||
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
harness: ['queue'],
|
|
465
|
-
card: ['intake-scan'],
|
|
466
|
-
loop: ['status', 'list'],
|
|
467
|
-
};
|
|
468
|
-
if (actions[first.text]?.includes(second.text)
|
|
469
|
-
&& (first.text !== 'harness' || args[start + 2]?.text === 'tick')) return `${first.text} ${second.text}`;
|
|
510
|
+
if (!first || first.quoted || !first.text || !known.has(first.text)) continue;
|
|
511
|
+
return second && !second.quoted && !second.text.startsWith('-') && !/^(?:\d*>|\d*>>|<|<<|&>|\|)$/.test(second.text)
|
|
512
|
+
? `${first.text} ${second.text}` : first.text;
|
|
470
513
|
}
|
|
471
514
|
}
|
|
472
515
|
return undefined;
|
|
@@ -476,12 +519,20 @@ function elanousCronName(command: string): string | undefined {
|
|
|
476
519
|
function namedCronIds(rows: ScheduleRow[], graphClaimed: Set<string>): Map<string, string> {
|
|
477
520
|
const counts = new Map<string, number>();
|
|
478
521
|
const ids = new Map<string, string>();
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
522
|
+
const candidates = rows.filter(row => !graphClaimed.has(row.id) && row.source === 'crontab'
|
|
523
|
+
&& row.run_via === 'crontab' && row.disabled_reason !== 'vanished'
|
|
524
|
+
&& !launchedSeat(row.command ?? '') && !seatLoopCommand(row.command ?? '').matched && !launchedOrchestrator(row.command ?? '')
|
|
525
|
+
&& !isCronShellCommand(row.command ?? '') && !!elanousCronName(row.command ?? ''));
|
|
526
|
+
// Reserve the existing owner-key slots before assigning newly recognized commands.
|
|
527
|
+
const established = (command: string): boolean => {
|
|
483
528
|
const name = elanousCronName(command);
|
|
484
|
-
if (!name)
|
|
529
|
+
if (!name) return false;
|
|
530
|
+
if (name === 'harness queue') return /\bqueue\s+tick\b/.test(command);
|
|
531
|
+
return ['hq heartbeat', 'hq arbiter-check', 'card intake-scan', 'loop status', 'loop list'].includes(name);
|
|
532
|
+
};
|
|
533
|
+
for (const row of [...candidates.filter(row => established(row.command ?? '')),
|
|
534
|
+
...candidates.filter(row => !established(row.command ?? ''))]) {
|
|
535
|
+
const name = elanousCronName(row.command ?? '')!;
|
|
485
536
|
const base = `elanous:${name.replace(/\s+/g, '-')}`;
|
|
486
537
|
const count = (counts.get(base) ?? 0) + 1;
|
|
487
538
|
counts.set(base, count);
|
|
@@ -552,32 +603,47 @@ export function listAllLoops(opts: LoopRegistryOptions = {}): AllLoopEntry[] {
|
|
|
552
603
|
if (claimed.has(row.id)) continue;
|
|
553
604
|
const command = row.command ?? '';
|
|
554
605
|
const seat = launchedSeat(command);
|
|
606
|
+
const seatArg = seatLoopCommand(command).seat;
|
|
555
607
|
const orchestrator = launchedOrchestrator(command);
|
|
556
608
|
const shell = isCronShellCommand(command);
|
|
557
609
|
const namedId = namedIds.get(row.id);
|
|
558
610
|
const fenceRole = cronFenceRole(command);
|
|
559
611
|
const fencedGitPush = fenceRole === 'git-push' && fenceCommandTitle(command, fenceRole) === 'git push (hq-fence git-push)';
|
|
560
|
-
const kind: AllLoopEntry['kind'] = seat ? 'seat' : orchestrator ? 'orchestrator' : 'cron-shell';
|
|
612
|
+
const kind: AllLoopEntry['kind'] = seat || seatArg ? 'seat' : orchestrator ? 'orchestrator' : 'cron-shell';
|
|
561
613
|
if (kind === 'cron-shell' && !shell && !namedId && !fencedGitPush) continue;
|
|
562
614
|
const cron = row.cron;
|
|
563
615
|
const period = cronPeriod(cron, now);
|
|
564
616
|
const dueAt = cron ? dueBefore(cron, now) : null;
|
|
565
|
-
const id = seat ? `${seat.toLowerCase()}-seat:${row.id}` : namedId ?? row.id;
|
|
566
|
-
|
|
567
|
-
const
|
|
617
|
+
const id = seatArg ? `elanous:seat-loop:${seatArg}` : seat ? `${seat.toLowerCase()}-seat:${row.id}` : namedId ?? row.id;
|
|
618
|
+
if (seatArg && all.some(entry => entry.id === id)) continue;
|
|
619
|
+
const title = seatArg ? `${seatArg} seat loop` : seat ? `${seat} seat` : row.note || (fenceRole ? fenceCommandTitle(command, fenceRole) : undefined) || (namedId ? elanousCronName(command) : undefined) || row.name;
|
|
620
|
+
const { at: logMtime, targets } = (kind === 'cron-shell' || seatArg) && command ? cronLogMtime(command, opts.home) : { at: undefined, targets: 0 };
|
|
621
|
+
const recorded = row.last_run && Number.isFinite(Date.parse(row.last_run)) ? row.last_run : undefined;
|
|
622
|
+
const picked = logMtime && (!recorded || Date.parse(logMtime) >= Date.parse(recorded)) ? 'log-mtime'
|
|
623
|
+
: recorded ? 'registry' : 'none';
|
|
624
|
+
debug.log('loops.registry', 'cron-last-run', { id, targets, picked });
|
|
568
625
|
all.push({ kind, id, title,
|
|
569
|
-
...resolveOwner(id, title, config, undefined, seat), ...(fenceRole ? { fenceRole } : {}),
|
|
626
|
+
...resolveOwner(id, title, config, undefined, seatArg ?? seat, seatArg ? 'seat-arg' : 'seat'), ...(fenceRole ? { fenceRole } : {}),
|
|
570
627
|
host: hostname(), observationCategory: row.category,
|
|
571
628
|
enabled: !!row.enabled, registered: true,
|
|
572
629
|
...(cron ? { cron } : {}), ...(command ? { command } : {}),
|
|
573
630
|
...periodFields(period),
|
|
574
631
|
...(dueAt ? { dueAt } : {}),
|
|
575
|
-
...(
|
|
576
|
-
:
|
|
632
|
+
...(picked === 'log-mtime' ? { lastRunAt: logMtime, evidence: picked }
|
|
633
|
+
: picked === 'registry' ? { lastRunAt: recorded, evidence: picked } : {}),
|
|
577
634
|
...(row.last_status ? { lastStatus: row.last_status } : {}),
|
|
578
635
|
recentStatuses: row.last_status ? [row.last_status] : [] });
|
|
579
636
|
}
|
|
580
|
-
const
|
|
637
|
+
const warnings = unregisteredCronLoops(all, graphOpts);
|
|
638
|
+
for (const row of warnings) {
|
|
639
|
+
const title = `미등록: ${row.command ?? row.id}`;
|
|
640
|
+
all.push({ kind: 'unregistered', id: row.id, title,
|
|
641
|
+
...resolveOwner(row.id, title, config), host: hostname(), observationCategory: row.category,
|
|
642
|
+
enabled: !!row.enabled, registered: false,
|
|
643
|
+
...(row.cron ? { cron: row.cron } : {}), ...(row.command ? { command: row.command } : {}),
|
|
644
|
+
...periodFields(cronPeriod(row.cron, now)), recentStatuses: [] });
|
|
645
|
+
}
|
|
646
|
+
const bySource: Record<OwnerSource, number> = { header: 0, config: 0, seat: 0, 'seat-arg': 0, default: 0 };
|
|
581
647
|
for (const loop of all) bySource[loop.ownerSource!]++;
|
|
582
648
|
debug.log('loop.check', 'ownership', { total: all.length, bySource });
|
|
583
649
|
debug.log('loops.registry', 'list-all', { count: all.length });
|
|
@@ -595,13 +661,28 @@ export function unregisteredCronLoops(entries: AllLoopEntry[], opts: LoopRegistr
|
|
|
595
661
|
const rows = schedules(opts);
|
|
596
662
|
const graphClaimed = new Set([...graphJobs.values()].flat());
|
|
597
663
|
const namedRows = new Map([...namedCronIds(rows, graphClaimed)].map(([rowId, name]) => [name, rowId]));
|
|
598
|
-
const
|
|
664
|
+
const accounted = entries.flatMap(entry => entry.kind === 'graph'
|
|
599
665
|
? graphJobs.get(entry.id) ?? []
|
|
600
|
-
:
|
|
601
|
-
:
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
666
|
+
: entry.kind === 'unregistered' ? []
|
|
667
|
+
: [entry.kind === 'seat' && entry.id.startsWith('elanous:seat-loop:')
|
|
668
|
+
? rows.find(row => row.command === entry.command && row.cron === entry.cron)?.id ?? entry.id
|
|
669
|
+
: entry.kind === 'seat' ? entry.id.slice(entry.id.indexOf(':') + 1)
|
|
670
|
+
: namedRows.get(entry.id) ?? entry.id]);
|
|
671
|
+
const claimed = new Set(accounted);
|
|
672
|
+
const related = rows.filter(row => row.source === 'crontab' && row.run_via === 'crontab'
|
|
673
|
+
&& row.disabled_reason !== 'vanished' && row.disabled_reason !== 'manual'
|
|
674
|
+
&& !!row.cron && looksElanousRelated(row.command ?? ''));
|
|
675
|
+
const warnings = related.filter(row => !claimed.has(row.id));
|
|
676
|
+
const relatedIds = new Set(related.map(row => row.id));
|
|
677
|
+
const registeredCount = accounted.filter(id => relatedIds.has(id)).length;
|
|
678
|
+
const counts = new Map<string, number>();
|
|
679
|
+
for (const id of accounted) counts.set(id, (counts.get(id) ?? 0) + 1);
|
|
680
|
+
const lines = related.filter(row => (counts.get(row.id) ?? 0) > 1);
|
|
681
|
+
if (related.length !== registeredCount + warnings.length || lines.length) {
|
|
682
|
+
debug.log('loops.registry', 'unaccounted', { lines: lines.map(row => row.raw ?? row.command ?? row.id) });
|
|
683
|
+
for (const row of lines) if (!warnings.includes(row)) warnings.push(row);
|
|
684
|
+
}
|
|
685
|
+
return warnings;
|
|
605
686
|
}
|
|
606
687
|
|
|
607
688
|
export function loopStatus(id: string, opts: LoopRegistryOptions = {}): LoopEntry & { recentRuns: LoopRun[] } {
|
|
@@ -45,6 +45,7 @@ export const FEATURE_MATURITY = {
|
|
|
45
45
|
'/ops/release': { pwa: 'ops' },
|
|
46
46
|
'/ops/seats': { pwa: 'ops' },
|
|
47
47
|
'/ops/checklist': { pwa: 'ops' },
|
|
48
|
+
'/ceo': { pwa: 'ops' },
|
|
48
49
|
'/dashboard': { pwa: 'ops' },
|
|
49
50
|
'/bots': { pwa: 'ops' },
|
|
50
51
|
'/botlab': { pwa: 'ops' },
|
|
@@ -55,7 +56,7 @@ export const FEATURE_MATURITY = {
|
|
|
55
56
|
},
|
|
56
57
|
// Canonical CLI command names only; aliases inherit their command's grade.
|
|
57
58
|
cliRoot: {
|
|
58
|
-
pty: 'beta', pr: 'beta', repo: 'beta', role: 'beta', machine: 'beta',
|
|
59
|
+
pty: 'beta', pr: 'beta', repo: 'beta', role: 'beta', machine: 'beta', 'agent-env': 'beta',
|
|
59
60
|
where: 'stable', shadow: 'beta', llm: 'beta', storage: 'beta', leader: 'beta',
|
|
60
61
|
pod: 'beta', browser: 'beta', questions: 'beta', usage: 'stable', live: 'beta',
|
|
61
62
|
research: 'beta', release: 'beta', freeze: 'beta', flow: 'beta', seat: 'beta', hq: 'beta', steward: 'beta', coord: 'beta', context: 'beta', 'model-watch': 'beta', doctor: 'stable',
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SLASH_COMMANDS, type SlashCommand } from '../chat/index.js';
|
|
2
|
+
import { FEATURE_MATURITY, type Maturity } from './feature-maturity.js';
|
|
3
|
+
|
|
4
|
+
export interface TuiSlashAvailability {
|
|
5
|
+
name: string;
|
|
6
|
+
maturity: Maturity;
|
|
7
|
+
telegram: boolean;
|
|
8
|
+
discord: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Project canonical TUI commands onto the maturity map and bot-surface grade maps.
|
|
12
|
+
* An absent bot grade means unavailable; a missing TUI grade is a registry error.
|
|
13
|
+
*/
|
|
14
|
+
export function deriveTuiSlashAvailability(
|
|
15
|
+
commands: readonly Pick<SlashCommand, 'name'>[] = SLASH_COMMANDS,
|
|
16
|
+
grades: {
|
|
17
|
+
tuiSlash: Readonly<Record<string, Maturity>>;
|
|
18
|
+
telegramCommand: Readonly<Record<string, Maturity>>;
|
|
19
|
+
discordCommand: Readonly<Record<string, Maturity>>;
|
|
20
|
+
} = FEATURE_MATURITY,
|
|
21
|
+
): TuiSlashAvailability[] {
|
|
22
|
+
return commands.map(({ name }) => {
|
|
23
|
+
if (!Object.hasOwn(grades.tuiSlash, name)) throw new Error(`TUI slash command /${name} has no maturity grade`);
|
|
24
|
+
return {
|
|
25
|
+
name,
|
|
26
|
+
maturity: grades.tuiSlash[name]!,
|
|
27
|
+
telegram: Object.hasOwn(grades.telegramCommand, name),
|
|
28
|
+
discord: Object.hasOwn(grades.discordCommand, name),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -270,23 +270,54 @@ export class LogStore {
|
|
|
270
270
|
this.db.run('CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level, ts_ms)');
|
|
271
271
|
this.db.run('CREATE INDEX IF NOT EXISTS idx_logs_surface ON logs(surface, ts_ms)');
|
|
272
272
|
this.db.run('CREATE INDEX IF NOT EXISTS idx_logs_category ON logs(category, ts_ms)');
|
|
273
|
+
this.db.run(`CREATE TABLE IF NOT EXISTS pod_log_imports (
|
|
274
|
+
job TEXT NOT NULL,
|
|
275
|
+
line INTEGER NOT NULL,
|
|
276
|
+
PRIMARY KEY (job, line)
|
|
277
|
+
)`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Insert each returned Pod line at most once per job, atomically with its receipt. */
|
|
281
|
+
insertPodBatch(job: string, rows: Array<{ rec: LogRecord; surface: string; line: number }>): number {
|
|
282
|
+
if (rows.length === 0) return 0;
|
|
283
|
+
const receipt = this.db.prepare('INSERT OR IGNORE INTO pod_log_imports (job, line) VALUES (?, ?)');
|
|
284
|
+
return this.db.transaction(() => {
|
|
285
|
+
const unseen = rows.filter(({ line }) => receipt.run(job, line).changes > 0);
|
|
286
|
+
this.insertBatch(unseen, { preserveRows: true });
|
|
287
|
+
return unseen.length;
|
|
288
|
+
})();
|
|
273
289
|
}
|
|
274
290
|
|
|
275
291
|
/** 배치 insert(단일 트랜잭션). 실패는 throw — fail-soft 는 호출측(StoreSink)
|
|
276
292
|
* 책임(ops-log 의 recordOpsEvent/Safe 분업 동형). */
|
|
277
|
-
insertBatch(rows: Array<{ rec: LogRecord; surface: string }
|
|
293
|
+
insertBatch(rows: Array<{ rec: LogRecord; surface: string }>, opts: { preserveRows?: boolean } = {}): void {
|
|
278
294
|
if (rows.length === 0) return;
|
|
279
295
|
const stmt = this.db.prepare(
|
|
280
296
|
`INSERT INTO logs (ts, ts_ms, level, instance, host_id, surface, category, event, session_id, trace_id, data)
|
|
281
297
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
282
298
|
);
|
|
299
|
+
const decisionRow = this.db.prepare(`SELECT id FROM logs WHERE instance = ? AND category = 'loop.steward' AND event = 'decision'
|
|
300
|
+
AND json_valid(data) AND json_extract(data, '$.runId') = ? ORDER BY id DESC LIMIT 1`);
|
|
301
|
+
const updateDecision = this.db.prepare(`UPDATE logs SET ts = ?, ts_ms = ?, level = ?, surface = ?, data = ? WHERE id = ?`);
|
|
283
302
|
const insertAll = this.db.transaction((batch: Array<{ rec: LogRecord; surface: string }>) => {
|
|
284
303
|
for (const { rec, surface } of batch) {
|
|
285
304
|
const tsMs = Date.parse(rec.ts);
|
|
305
|
+
const timestamp = Number.isFinite(tsMs) ? tsMs : Date.now();
|
|
306
|
+
const level = deriveLogLevel(rec);
|
|
307
|
+
const data = rec.data !== undefined ? JSON.stringify(rec.data) : null;
|
|
308
|
+
const runId = rec.category === 'loop.steward' && rec.event === 'decision' &&
|
|
309
|
+
rec.data && typeof rec.data === 'object' && !Array.isArray(rec.data)
|
|
310
|
+
? (rec.data as Record<string, unknown>).runId : undefined;
|
|
311
|
+
const existing = !opts.preserveRows && typeof runId === 'string' && runId
|
|
312
|
+
? decisionRow.get(this.instance, runId) as { id: number } | undefined : undefined;
|
|
313
|
+
if (existing) {
|
|
314
|
+
updateDecision.run(rec.ts, timestamp, level, surface, data, existing.id);
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
286
317
|
stmt.run(
|
|
287
318
|
rec.ts,
|
|
288
|
-
|
|
289
|
-
|
|
319
|
+
timestamp,
|
|
320
|
+
level,
|
|
290
321
|
this.instance,
|
|
291
322
|
rec.data && typeof rec.data === 'object' && !Array.isArray(rec.data)
|
|
292
323
|
&& typeof (rec.data as Record<string, unknown>).hostId === 'string'
|
|
@@ -297,7 +328,7 @@ export class LogStore {
|
|
|
297
328
|
rec.event,
|
|
298
329
|
(rec as { session_id?: string }).session_id ?? null,
|
|
299
330
|
rec.trace_id ?? null,
|
|
300
|
-
|
|
331
|
+
data,
|
|
301
332
|
);
|
|
302
333
|
}
|
|
303
334
|
});
|
|
@@ -162,6 +162,7 @@ import {
|
|
|
162
162
|
} from './vault-api.js';
|
|
163
163
|
import { handleBuildsGet, parseBuildsPath } from './builds-api.js';
|
|
164
164
|
import { handleHarnessAskPost, handleHarnessAskStatusGet, handleHarnessRunEventsGet, handleHarnessRunScreenGet, handleHarnessRunsGet, handleHarnessStopPost } from './harness-api.js';
|
|
165
|
+
import { handleLoopEdgesGet } from './loop-edges.js';
|
|
165
166
|
import { handleFabricPlans, type FabricPlansRouteOpts } from './fabric-plans.js';
|
|
166
167
|
import { dispatchPersonaRoute } from './personas.js';
|
|
167
168
|
import { handleMe } from './operator.js';
|
|
@@ -1295,6 +1296,15 @@ export async function routeRequest(
|
|
|
1295
1296
|
return handleDevices(req, opts.devices);
|
|
1296
1297
|
}
|
|
1297
1298
|
|
|
1299
|
+
if (pathname === '/v1/loops/edges' && method === 'GET') {
|
|
1300
|
+
if (!opts.metaApi?.bearerToken) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1301
|
+
const offered = req.headers.get('authorization');
|
|
1302
|
+
if (!offered?.startsWith('Bearer ') || !compareTokenConstTime(offered.slice('Bearer '.length).trim(), opts.metaApi.bearerToken)) {
|
|
1303
|
+
return jsonResponse({ error: 'unauthorized' }, 401);
|
|
1304
|
+
}
|
|
1305
|
+
return handleLoopEdgesGet(req);
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1298
1308
|
if (pathname === '/v1/harness/runs' && method === 'GET') {
|
|
1299
1309
|
if (!opts.metaApi) return jsonResponse({ error: 'meta-api-runtime-not-wired' }, 503);
|
|
1300
1310
|
if (!checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
@@ -2882,20 +2892,24 @@ export async function routeRequest(
|
|
|
2882
2892
|
if (result) return result;
|
|
2883
2893
|
}
|
|
2884
2894
|
if (pathname === '/v1/context/now') {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2895
|
+
try {
|
|
2896
|
+
if (!opts.metaApi || !checkAuth(req, opts.metaApi)) return jsonResponse({ error: 'unauthorized' }, 401);
|
|
2897
|
+
const audiences = url.searchParams.getAll('audience');
|
|
2898
|
+
if (audiences.length > 1) return jsonResponse({ error: 'invalid_audience' }, 400);
|
|
2899
|
+
const requested = audiences[0];
|
|
2900
|
+
if (requested !== undefined && requested !== 'operator' && requested !== 'user' && requested !== 'public-demo') {
|
|
2901
|
+
return jsonResponse({ error: 'invalid_audience' }, 400);
|
|
2902
|
+
}
|
|
2903
|
+
const forced = getUserConfig().nexus?.demoMode === true;
|
|
2904
|
+
const audience: ContextNowAudience = forced ? 'public-demo' : requested ?? 'operator';
|
|
2905
|
+
const answer = contextNow({ audience, ...(url.searchParams.has('topic') ? { topic: url.searchParams.get('topic') ?? '' } : {}) }, opts.contextNowDeps);
|
|
2906
|
+
debug.log('context.now', 'served', { audience, hidden: answer.hiddenCount ?? 0, forced });
|
|
2907
|
+
return jsonResponse(url.searchParams.get('format') === 'voice'
|
|
2908
|
+
? { text: renderVoiceNow(answer, audience), audience, hiddenCount: answer.hiddenCount ?? 0 }
|
|
2909
|
+
: { ...answer, audience, hiddenCount: answer.hiddenCount ?? 0 });
|
|
2910
|
+
} catch {
|
|
2911
|
+
return jsonResponse({ error: 'context-now-failed' }, 500);
|
|
2912
|
+
}
|
|
2899
2913
|
}
|
|
2900
2914
|
// §6.3 (2026-05-09) — context URL fetch (showroom).
|
|
2901
2915
|
// POST /v1/context/fetch-url
|