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/scripts/rhythm/daily.ts
CHANGED
|
@@ -7,12 +7,15 @@ import { effectiveInstanceRoot, prodInstanceRoot } from '../../src/instance/reso
|
|
|
7
7
|
import { listSchedules } from '../../src/release-loop/release-schedule.js';
|
|
8
8
|
import { devVersion, listChecklist } from '../../src/release-loop/checklist.js';
|
|
9
9
|
import { DecisionLedger } from '../../src/decisions/decision-ledger.js';
|
|
10
|
+
import { selectProact1Lite, type ProactSignals } from '../../src/decisions/proact1-lite.js';
|
|
10
11
|
import { trafficTick, seatOfTree } from '../../src/loops/orchestrator/traffic.js';
|
|
11
|
-
import { defaultListHarnessProcesses } from '../../src/harness/harness-cli-command.js';
|
|
12
|
+
import { defaultListHarnessProcesses, githubDraftSweepAdapters } from '../../src/harness/harness-cli-command.js';
|
|
12
13
|
import { getUserConfig, ORCHESTRATOR_DEFAULTS } from '../../src/user-config.js';
|
|
13
14
|
import { loopEvent } from '../../src/loops/observe.js';
|
|
14
|
-
import {
|
|
15
|
+
import { kindRouteTarget, mainHomeTarget } from '../../src/domains/telegram-kind-route.js';
|
|
16
|
+
import { recordOutbound } from '../../src/domains/outbound-alert.js';
|
|
15
17
|
import { listSelfDevRuns, runSummaryLine, selfDevRunsDir } from '../../src/self-dev/run-store.js';
|
|
18
|
+
import { runDraftSweep } from '../../src/self-dev/draft-sweep.js';
|
|
16
19
|
|
|
17
20
|
export type Section<T> = { status: 'ok'; value: T } | { status: 'unreadable'; reason: string };
|
|
18
21
|
export type Landing = { title: string; seat: string; mergedAt: string; prNumber?: number };
|
|
@@ -32,7 +35,10 @@ export type DailyReview = { markdown: string; header: string; risks: Risk[]; sec
|
|
|
32
35
|
const repo = join(import.meta.dir, '../..');
|
|
33
36
|
const kstDay = (date: Date) => new Date(date.getTime() + 9 * 3_600_000).toISOString().slice(0, 10);
|
|
34
37
|
const reasonOf = (error: unknown) => error instanceof Error ? error.message : String(error);
|
|
35
|
-
const
|
|
38
|
+
const shortReason = (reason: string) => reason.split(/\r?\n/, 1)[0]!.trim().slice(0, 200);
|
|
39
|
+
const unreadable = <T>(reason: string): Section<T> => ({ status: 'unreadable', reason: shortReason(reason) });
|
|
40
|
+
const adArticle = (title: string, body: string) => /토토|카지노|배팅|베팅|입플|슬롯|도박|성인|바카라|스포츠.?토토|성인물|야동/i.test(`${title} ${body}`);
|
|
41
|
+
const cleanNews = (items: News[]) => items.filter(item => !adArticle(item.title, item.implication));
|
|
36
42
|
const record = (value: unknown): Record<string, unknown> => value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
37
43
|
const rows = (value: unknown): Record<string, unknown>[] => Array.isArray(value) ? value.map(record) : [];
|
|
38
44
|
const string = (value: unknown): string => typeof value === 'string' ? value : '';
|
|
@@ -69,18 +75,44 @@ export function collectRunSummaries(now: Date, root = effectiveInstanceRoot(), l
|
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
/** 기본은 어제 KST 00:00–24:00, 창을 주면 그 안에 실제 병합된 PR만 센다. */
|
|
72
|
-
export async function collectLandings(now: Date, window?: { from: Date; to: Date }): Promise<Landing[]> {
|
|
78
|
+
export async function collectLandings(now: Date, window?: { from: Date; to: Date }, source?: { repoRoot?: string; repoName?: string; stateRoot?: string }): Promise<Landing[]> {
|
|
79
|
+
const configuredTree = source?.repoRoot ?? getUserConfig().ops?.cronRepoRoot;
|
|
80
|
+
const tree = configuredTree || repo;
|
|
81
|
+
const remote = source?.repoName ?? (() => {
|
|
82
|
+
if (!existsSync(join(tree, '.git'))) return null;
|
|
83
|
+
const result = spawnSync('git', ['-C', tree, 'remote', 'get-url', 'origin'], { encoding: 'utf8', timeout: 5_000 });
|
|
84
|
+
const path = result.status === 0 ? /(?:github\.com[:/])([^/\s]+\/[^/\s]+)\s*$/.exec(result.stdout)?.[1] : null;
|
|
85
|
+
return path?.replace(/\.git$/, '') ?? null;
|
|
86
|
+
})();
|
|
87
|
+
if (!remote) throw new Error('저장소 미지정');
|
|
73
88
|
const yesterday = kstDay(new Date(now.getTime() - 86_400_000));
|
|
74
89
|
const from = window?.from ?? new Date(`${yesterday}T00:00:00+09:00`);
|
|
75
90
|
const to = window?.to ?? new Date(from.getTime() + 86_400_000);
|
|
76
91
|
// GitHub's merged: date search is UTC; include the UTC date containing KST midnight, then filter precisely below.
|
|
77
92
|
const searchDay = window ? from.toISOString().slice(0, 10) : yesterday;
|
|
78
|
-
const data = cli('gh', 'pr', 'list', '--state', 'merged', '--search', `merged:>=${searchDay}`, '--limit', '500', '--json', 'number,title,mergedAt');
|
|
93
|
+
const data = cli('gh', 'pr', 'list', '--repo', remote, '--state', 'merged', '--search', `merged:>=${searchDay}`, '--limit', '500', '--json', 'number,title,mergedAt');
|
|
79
94
|
if (!Array.isArray(data)) throw new Error('병합 목록 형식 오류');
|
|
80
95
|
// 주간 창은 잘린 목록을 정확한 합계로 취급할 수 없다. 데일리 기본 경로는 그대로 둔다.
|
|
81
96
|
if (window && data.length >= 500) throw new Error('병합 목록 500건 조회 상한 도달 · 주간 착지 집계 불완전');
|
|
97
|
+
const seatByPr = new Map<number, string>();
|
|
98
|
+
// A PR number is local to its repository. A bare prNumber cannot establish which repository it belongs to.
|
|
99
|
+
const targetRepo = remote.replace(/\.git$/i, '').toLowerCase();
|
|
100
|
+
// Newest checkpoint wins when a PR appears in more than one run; titles are not seat evidence.
|
|
101
|
+
for (const run of listSelfDevRuns(selfDevRunsDir(source?.stateRoot ?? effectiveInstanceRoot()))) {
|
|
102
|
+
if (!['OP', 'TC', 'MK', 'UX'].includes(run.seat ?? '')) continue;
|
|
103
|
+
for (const result of run.results) {
|
|
104
|
+
let url: URL;
|
|
105
|
+
try { url = new URL(result.prUrl ?? ''); } catch { continue; }
|
|
106
|
+
if (url.protocol !== 'https:' || url.hostname.toLowerCase() !== 'github.com') continue;
|
|
107
|
+
const match = /^\/([^/]+)\/([^/]+)\/pull\/(\d+)\/?$/.exec(url.pathname);
|
|
108
|
+
if (!match || `${match[1]}/${match[2]}`.toLowerCase() !== targetRepo) continue;
|
|
109
|
+
const number = Number(match[3]);
|
|
110
|
+
if (result.prNumber !== undefined && result.prNumber !== number) continue;
|
|
111
|
+
if (Number.isSafeInteger(number) && number > 0 && !seatByPr.has(number)) seatByPr.set(number, run.seat!);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
82
114
|
return rows(data).filter(r => typeof r.title === 'string' && typeof r.mergedAt === 'string' && Number.isFinite(Date.parse(r.mergedAt as string)) && Date.parse(r.mergedAt as string) >= from.getTime() && Date.parse(r.mergedAt as string) < to.getTime())
|
|
83
|
-
.map(r => ({ title: string(r.title), mergedAt: string(r.mergedAt), seat:
|
|
115
|
+
.map(r => ({ title: string(r.title), mergedAt: string(r.mergedAt), seat: typeof r.number === 'number' ? seatByPr.get(r.number) ?? '미분류' : '미분류',
|
|
84
116
|
...(typeof r.number === 'number' ? { prNumber: r.number } : {}) }))
|
|
85
117
|
.sort((a, b) => b.mergedAt.localeCompare(a.mergedAt) || sortNames(a.title, b.title));
|
|
86
118
|
}
|
|
@@ -154,6 +186,7 @@ export async function collectNews(): Promise<News[]> {
|
|
|
154
186
|
for (const result of rows(data.results)) for (const item of rows(result.items)) {
|
|
155
187
|
const url = string(item.url);
|
|
156
188
|
if (!/^https?:\/\//.test(url) || seen.has(url)) continue;
|
|
189
|
+
if (adArticle(string(item.title), string(item.text))) continue;
|
|
157
190
|
seen.add(url);
|
|
158
191
|
found.push({ title: string(item.title) || string(item.text).slice(0, 100) || url, url,
|
|
159
192
|
implication: `${query}: ${string(item.text).replace(/\s+/g, ' ').slice(0, 120) || '원문 확인 필요'}` });
|
|
@@ -185,25 +218,33 @@ export function composeDaily(parts: DailyParts, now: Date): DailyReview {
|
|
|
185
218
|
if (parts.grid.status === 'ok') for (const seat of parts.grid.value) if (seat.idle) risks.push({ name: seat.name, score: 1, reason: '노는 자리' });
|
|
186
219
|
risks.sort((a, b) => b.score - a.score || sortNames(a.name, b.name));
|
|
187
220
|
const top = risks.slice(0, 5);
|
|
188
|
-
const unread = <T>(section: Section<T>) => section.status === 'unreadable' ? `못 읽음 · ${section.reason}` : '';
|
|
189
|
-
const
|
|
221
|
+
const unread = <T>(section: Section<T>) => section.status === 'unreadable' ? `못 읽음 · ${shortReason(section.reason)}` : '';
|
|
222
|
+
const seenPr = new Set<number>();
|
|
223
|
+
const landings = parts.landings.status === 'ok' ? [...parts.landings.value]
|
|
224
|
+
.sort((a, b) => b.mergedAt.localeCompare(a.mergedAt) || sortNames(a.title, b.title))
|
|
225
|
+
.filter(item => {
|
|
226
|
+
if (item.prNumber === undefined) return true;
|
|
227
|
+
if (seenPr.has(item.prNumber)) return false;
|
|
228
|
+
seenPr.add(item.prNumber);
|
|
229
|
+
return true;
|
|
230
|
+
}) : [];
|
|
231
|
+
const land = parts.landings.status === 'ok' ? `어제 착지 ${landings.length}건` : `어제 착지 ${unread(parts.landings)}`;
|
|
190
232
|
const rel = parts.release.status === 'ok' ? `판 ${parts.release.value.version} ${parts.release.value.green}/${parts.release.value.total}` : `판 ${unread(parts.release)}`;
|
|
191
233
|
const action = top.length ? `${top[0]!.name} (${top[0]!.reason}) 확인` : '못 읽은 절을 확인하고 오늘 첫 착지를 고른다';
|
|
192
|
-
const header = [`S: ${land} · ${rel}
|
|
234
|
+
const header = [`S: ${land} · ${rel}`.slice(0, 200), `C: ${top[0] ? `${top[0].name} (${top[0].score}점 · ${top[0].reason})` : '확인된 위험 없음(못 읽은 절은 별도 확인)'}`, 'Q: 오늘 무엇을 먼저 할 것인가?', `A: ${action}`].join('\n');
|
|
193
235
|
const section = <T>(key: keyof DailyParts, value: Section<T>, text: () => string) => `## ${labels[key]}\n${value.status === 'unreadable' ? unread(value) : text()}`;
|
|
194
236
|
const runs = parts.landings.runSummaries ?? [];
|
|
195
237
|
const landingText = (() => {
|
|
196
|
-
if (parts.landings.status === 'unreadable') {
|
|
197
|
-
|
|
198
|
-
return `${unread(parts.landings)}${yesterdayRuns.length ? `\n하니스 런\n${yesterdayRuns.map(run => run.line).join('\n')}` : ''}`;
|
|
199
|
-
}
|
|
200
|
-
const list = parts.landings.value;
|
|
238
|
+
if (parts.landings.status === 'unreadable') return `못 읽음 · ${shortReason(parts.landings.reason).slice(0, 200 - '못 읽음 · '.length)}`;
|
|
239
|
+
const list = landings;
|
|
201
240
|
const counts = [...new Set(list.map(i => i.seat))].sort(sortNames).map(seat => `${seat} ${list.filter(i => i.seat === seat).length}`).join(' · ');
|
|
202
241
|
const byPr = new Map(runs.flatMap(run => run.prNumbers.map(number => [number, run.line] as const)));
|
|
203
|
-
const shown = list.slice(0,
|
|
242
|
+
const shown = list.slice(0, 10);
|
|
204
243
|
const listed = shown.map(i => i.prNumber !== undefined && byPr.has(i.prNumber) ? byPr.get(i.prNumber)! : `- ${i.title}`).join('\n') || '착지 없음';
|
|
205
|
-
const
|
|
206
|
-
|
|
244
|
+
const omitted = list.length - shown.length;
|
|
245
|
+
const withoutLanding = runs.filter(run => run.updatedYesterday && !list.some(i => i.prNumber !== undefined && run.prNumbers.includes(i.prNumber)));
|
|
246
|
+
const shownRuns = withoutLanding.slice(0, 10);
|
|
247
|
+
return `총 ${list.length}건 · 자리별 ${counts || '없음'}\n${listed}${omitted ? `\n외 ${omitted}건` : ''}${withoutLanding.length ? `\n하니스 런\n${shownRuns.map(run => run.line).join('\n')}${withoutLanding.length > 10 ? `\n외 ${withoutLanding.length - 10}개` : ''}` : ''}`;
|
|
207
248
|
})();
|
|
208
249
|
const markdown = [
|
|
209
250
|
`# 데일리 리뷰 — ${kstDay(now)} (KST)`, header,
|
|
@@ -222,68 +263,109 @@ export function composeDaily(parts: DailyParts, now: Date): DailyReview {
|
|
|
222
263
|
return { markdown, header, risks: top, sections: Object.fromEntries(Object.entries(parts).map(([k, v]) => [k, v.status])) as DailyReview['sections'] };
|
|
223
264
|
}
|
|
224
265
|
|
|
225
|
-
type
|
|
266
|
+
export type DailySendRequest = { channel: 'telegram'; chatId: number; botToken: string; kind: 'ops-report'; text: string };
|
|
267
|
+
export type DailySendConfirmation = { chatId: number; messageId: number };
|
|
268
|
+
type DeliveryReceipt = DailySendConfirmation | 'not-sent' | 'unknown';
|
|
269
|
+
type DeliveryResult = { sent: boolean; sendError: string | null; deliveryState: string; target: string; recordError?: string | null };
|
|
226
270
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
271
|
+
// The final transport checks Telegram's returned recipient and message, not sendOutbound's
|
|
272
|
+
// boolean (which can also mean "queued" or "accepted by daemon").
|
|
273
|
+
function sendTelegram(request: DailySendRequest): DailySendConfirmation | null {
|
|
274
|
+
const quote = (value: string) => `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r')}"`;
|
|
275
|
+
const config = [
|
|
276
|
+
`url = ${quote(`https://api.telegram.org/bot${request.botToken}/sendMessage`)}`,
|
|
277
|
+
'request = "POST"',
|
|
278
|
+
`data-urlencode = ${quote(`chat_id=${request.chatId}`)}`,
|
|
279
|
+
`data-urlencode = ${quote(`text=${request.text}`)}`,
|
|
280
|
+
].join('\n') + '\n';
|
|
281
|
+
const response = spawnSync('curl', ['-sS', '-m', '25', '--config', '-'],
|
|
282
|
+
{ input: config, encoding: 'utf8', timeout: 30_000, maxBuffer: 2_000_000 });
|
|
283
|
+
if (response.error || response.status !== 0) return null;
|
|
234
284
|
try {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
} catch { return
|
|
285
|
+
const body = record(JSON.parse(response.stdout));
|
|
286
|
+
const message = record(body.result);
|
|
287
|
+
const chat = record(message.chat);
|
|
288
|
+
return body.ok === true && chat.id === request.chatId && Number.isSafeInteger(message.message_id) && (message.message_id as number) > 0
|
|
289
|
+
? { chatId: request.chatId, messageId: message.message_id as number } : null;
|
|
290
|
+
} catch { return null; }
|
|
241
291
|
}
|
|
242
292
|
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
|
|
293
|
+
function recipient(deps: DailyDeps): { chatId: number; botToken: string } | null {
|
|
294
|
+
const target = deps.target ?? (() => {
|
|
295
|
+
const config = getUserConfig();
|
|
296
|
+
return config.telegram.channels?.length ? kindRouteTarget(config, 'ops-report') : mainHomeTarget(config);
|
|
297
|
+
})();
|
|
298
|
+
return target && Number.isSafeInteger(target.chatId) && target.botToken.trim() ? target : null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function sendReview(file: string, text: string, target: { chatId: number; botToken: string },
|
|
302
|
+
send: (request: DailySendRequest) => DailySendConfirmation | null,
|
|
303
|
+
receipt: (key: string) => DeliveryReceipt,
|
|
304
|
+
recordDelivery?: (text: string, kind: 'ops-report') => void): DeliveryResult {
|
|
305
|
+
const key = file.slice(file.lastIndexOf('/') + 1).replace(/\.md$/, '');
|
|
306
|
+
const label = `telegram:${target.chatId}`;
|
|
246
307
|
const db = new Database(join(dirname(file), 'delivery.sqlite'));
|
|
247
308
|
try {
|
|
248
|
-
db.run("CREATE TABLE IF NOT EXISTS deliveries (day TEXT PRIMARY KEY, state TEXT NOT NULL CHECK(state IN ('pending','unknown','sent')))");
|
|
309
|
+
db.run("CREATE TABLE IF NOT EXISTS deliveries (day TEXT PRIMARY KEY, state TEXT NOT NULL CHECK(state IN ('pending','unknown','sent')), chat_id TEXT, message_id TEXT)");
|
|
310
|
+
const cols = db.prepare('PRAGMA table_info(deliveries)').all() as { name: string }[];
|
|
311
|
+
if (!cols.some(c => c.name === 'chat_id')) db.run('ALTER TABLE deliveries ADD COLUMN chat_id TEXT');
|
|
312
|
+
if (!cols.some(c => c.name === 'message_id')) db.run('ALTER TABLE deliveries ADD COLUMN message_id TEXT');
|
|
249
313
|
db.run("INSERT OR IGNORE INTO deliveries(day, state) VALUES (?, 'pending')", [key]);
|
|
250
|
-
const state =
|
|
251
|
-
if (state === 'sent')
|
|
252
|
-
|
|
314
|
+
const state = db.prepare('SELECT state, chat_id, message_id FROM deliveries WHERE day = ?').get(key) as { state: string; chat_id: string | null; message_id: string | null };
|
|
315
|
+
if (state.state === 'sent') {
|
|
316
|
+
if (state.chat_id === String(target.chatId) && state.message_id && Number(state.message_id) > 0)
|
|
317
|
+
return { sent: false, sendError: null, deliveryState: 'already-sent', target: label };
|
|
318
|
+
return { sent: false, sendError: '수신자 확인 불가 · 기존 발송 기록 확인 필요', deliveryState: 'unknown', target: 'unknown' };
|
|
319
|
+
}
|
|
320
|
+
if (state.state === 'unknown') {
|
|
253
321
|
const verdict = receipt(key);
|
|
254
|
-
if (verdict === '
|
|
255
|
-
db.run("UPDATE deliveries SET state = 'sent' WHERE day = ?", [key]);
|
|
256
|
-
return { sent: false, sendError: null };
|
|
322
|
+
if (typeof verdict === 'object' && verdict.chatId === target.chatId && Number.isSafeInteger(verdict.messageId) && verdict.messageId > 0) {
|
|
323
|
+
db.run("UPDATE deliveries SET state = 'sent', chat_id = ?, message_id = ? WHERE day = ?", [String(target.chatId), String(verdict.messageId), key]);
|
|
324
|
+
return { sent: false, sendError: null, deliveryState: 'receipt-sent', target: label };
|
|
257
325
|
}
|
|
258
|
-
if (verdict !== 'not-sent') return { sent: false, sendError: `발송 결과 불명 · RHYTHM-DAILY:${key} 영수증 확인
|
|
326
|
+
if (verdict !== 'not-sent') return { sent: false, sendError: `발송 결과 불명 · RHYTHM-DAILY:${key} 영수증 확인 필요`, deliveryState: 'unknown', target: 'unknown' };
|
|
259
327
|
db.run("UPDATE deliveries SET state = 'pending' WHERE day = ? AND state = 'unknown'", [key]);
|
|
260
328
|
}
|
|
261
|
-
// Atomic claim across concurrent runs. A process dying before this write made no send.
|
|
262
329
|
if (!db.prepare("UPDATE deliveries SET state = 'unknown' WHERE day = ? AND state = 'pending'").run(key).changes)
|
|
263
|
-
return { sent: false, sendError: `발송 결과 불명 · RHYTHM-DAILY:${key} 영수증 확인
|
|
330
|
+
return { sent: false, sendError: `발송 결과 불명 · RHYTHM-DAILY:${key} 영수증 확인 필요`, deliveryState: 'unknown', target: 'unknown' };
|
|
264
331
|
try {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
332
|
+
const request: DailySendRequest = { channel: 'telegram', chatId: target.chatId, botToken: target.botToken,
|
|
333
|
+
kind: 'ops-report', text: `${text}\nRHYTHM-DAILY:${key}` };
|
|
334
|
+
const confirmation = send(request);
|
|
335
|
+
if (!confirmation || confirmation.chatId !== target.chatId || !Number.isSafeInteger(confirmation.messageId) || confirmation.messageId <= 0)
|
|
336
|
+
return { sent: false, sendError: '발송 결과 불명 · 수신자/메시지 영수증 확인 필요', deliveryState: 'unknown', target: 'unknown' };
|
|
337
|
+
db.run("UPDATE deliveries SET state = 'sent', chat_id = ?, message_id = ? WHERE day = ?", [String(confirmation.chatId), String(confirmation.messageId), key]);
|
|
338
|
+
let recordError: string | null = null;
|
|
339
|
+
try {
|
|
340
|
+
if (recordDelivery) recordDelivery(request.text, request.kind);
|
|
341
|
+
else if (send === sendTelegram && process.env.NODE_ENV !== 'test') recordOutbound(request.text, request.kind);
|
|
342
|
+
} catch (error) { recordError = `보조 기록 실패 · ${shortReason(reasonOf(error))}`; }
|
|
343
|
+
return { sent: true, sendError: null, deliveryState: 'sent', target: label, recordError };
|
|
344
|
+
} catch (error) { return { sent: false, sendError: `발송 결과 불명 · ${shortReason(reasonOf(error))}`, deliveryState: 'unknown', target: 'unknown' }; }
|
|
345
|
+
} catch (error) { return { sent: false, sendError: `발송 상태 못 읽음 · ${shortReason(reasonOf(error))}`, deliveryState: 'unknown', target: 'unknown' }; }
|
|
270
346
|
finally { db.close(); }
|
|
271
347
|
}
|
|
272
348
|
|
|
273
|
-
export type DailyDeps = { now?: () => Date; root?: string; vaultRoot?: string | null; sendEnabled?: boolean;
|
|
349
|
+
export type DailyDeps = { now?: () => Date; root?: string; vaultRoot?: string | null; sendEnabled?: boolean; repoRoot?: string; repoName?: string;
|
|
274
350
|
landings?: (now: Date) => Promise<Landing[]>; release?: () => Promise<Release>; loops?: () => Promise<Loop[]>;
|
|
275
351
|
grid?: () => Promise<Seat[]>; decisions?: () => Promise<Pending[]>; news?: () => Promise<News[]>;
|
|
276
|
-
|
|
352
|
+
target?: { chatId: number; botToken: string };
|
|
353
|
+
send?: (request: DailySendRequest) => DailySendConfirmation | null; receipt?: (key: string) => DeliveryReceipt;
|
|
354
|
+
recordDelivery?: (text: string, kind: 'ops-report') => void;
|
|
277
355
|
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
356
|
+
proactSignals?: () => Promise<ProactSignals>;
|
|
357
|
+
proactPreviouslySuggested?: ReadonlySet<string>;
|
|
278
358
|
print?: (line: string) => void };
|
|
279
359
|
|
|
280
360
|
// 볼트 사본은 필수 산출물이다 — 쓰기 실패·설정 못 읽음은 vaultFatal 로 올려 그래프가 done 으로 가지 않게 한다.
|
|
281
|
-
// 볼트 미설정·격리
|
|
361
|
+
// 볼트 미설정·격리 인스턴스의 외부 볼트 건너뜀은 «의도»라 fatal 이 아니다.
|
|
282
362
|
function copyToVault(markdown: string, now: Date, deps: DailyDeps): { vaultError: string | null; vaultFatal: boolean } {
|
|
283
363
|
let vaultRoot: string | null | undefined = deps.vaultRoot;
|
|
284
364
|
if (vaultRoot === undefined) try {
|
|
285
|
-
|
|
286
|
-
|
|
365
|
+
const config = getUserConfig();
|
|
366
|
+
const obsidian = record(config.raw.obsidian);
|
|
367
|
+
vaultRoot = string(obsidian.vault) || string(obsidian.vaultRoot) || config.obsidian.vault || null;
|
|
368
|
+
if (vaultRoot && effectiveInstanceRoot() !== prodInstanceRoot() && !vaultRoot.startsWith(`${effectiveInstanceRoot()}/`)) return { vaultError: '격리 인스턴스 — 운영 볼트 사본 건너뜀', vaultFatal: false };
|
|
287
369
|
} catch (e) { return { vaultError: reasonOf(e), vaultFatal: true }; }
|
|
288
370
|
if (!vaultRoot) return { vaultError: null, vaultFatal: false };
|
|
289
371
|
try {
|
|
@@ -295,10 +377,12 @@ function copyToVault(markdown: string, now: Date, deps: DailyDeps): { vaultError
|
|
|
295
377
|
|
|
296
378
|
export async function runDaily(options: { dryRun?: boolean; noNews?: boolean; stage?: 'collect' | 'compose' | 'deliver' } = {}, deps: DailyDeps = {}) {
|
|
297
379
|
const now = (deps.now ?? (() => new Date()))();
|
|
380
|
+
const receiptKey = `RHYTHM-DAILY:${kstDay(now)}`;
|
|
298
381
|
if (options.stage === 'compose' || options.stage === 'deliver') {
|
|
299
382
|
const file = join(deps.root ?? effectiveInstanceRoot(), 'rhythm', 'daily', `${kstDay(now)}.md`);
|
|
300
383
|
if (!existsSync(file)) throw new Error(`수집 파일 없음: ${file}`);
|
|
301
384
|
const markdown = readFileSync(file, 'utf8');
|
|
385
|
+
const suggestionBlock = markdown.split('## PROACT1-LITE 제안\n')[1]?.split('\n\n## ')[0]?.trim() ?? '';
|
|
302
386
|
const header = markdown.split('\n').slice(2, 6).join('\n');
|
|
303
387
|
if (!header.startsWith('S: ') || !markdown.includes('## ⑥ 위험 톱 5')) throw new Error(`리뷰 파일 형식 오류: ${file}`);
|
|
304
388
|
const riskBlock = markdown.split('## ⑥ 위험 톱 5\n')[1]!.split('\n\n## ')[0]!;
|
|
@@ -310,31 +394,51 @@ export async function runDaily(options: { dryRun?: boolean; noNews?: boolean; st
|
|
|
310
394
|
const sections = Object.fromEntries(Object.entries(labels).map(([key, label]) => [key,
|
|
311
395
|
markdown.split(`## ${label}\n`)[1]?.split('\n\n## ')[0]?.startsWith('못 읽음 ·') ? 'unreadable' : 'ok'])) as DailyReview['sections'];
|
|
312
396
|
sections.news = markdown.split('## ⑦ 외부 동향\n')[1]?.startsWith('못 읽음 ·') ? 'unreadable' : 'ok';
|
|
313
|
-
const tick = { sections, risks: risks.length, sent: false };
|
|
397
|
+
const tick: Record<string, unknown> = { sections, risks: risks.length, sent: false };
|
|
314
398
|
const vault = options.stage === 'compose' && !options.dryRun ? copyToVault(markdown, now, deps) : { vaultError: null, vaultFatal: false };
|
|
315
399
|
let sent = false;
|
|
400
|
+
let deliveryState = options.stage === 'compose' ? 'pending' : options.dryRun ? 'dry-run' : 'send-disabled';
|
|
401
|
+
let chars = 0;
|
|
402
|
+
let target = 'unknown';
|
|
316
403
|
let sendError: string | null = null;
|
|
404
|
+
let recordError: string | null = null;
|
|
317
405
|
let enabled = deps.sendEnabled === true;
|
|
318
406
|
if (deps.sendEnabled === undefined && options.stage === 'deliver' && !options.dryRun) {
|
|
319
407
|
try { enabled = record(record(record(getUserConfig().raw.loops).rhythm).daily).send === true; }
|
|
320
|
-
catch (error) { sendError = `발송 설정 못 읽음 · ${reasonOf(error)}`; }
|
|
408
|
+
catch (error) { sendError = `발송 설정 못 읽음 · ${shortReason(reasonOf(error))}`; deliveryState = 'unknown'; }
|
|
321
409
|
}
|
|
322
410
|
if (options.stage === 'deliver' && !options.dryRun && enabled) {
|
|
323
|
-
const
|
|
411
|
+
const text = `${header}\n\n## 위험 톱 5\n${riskBlock}${suggestionBlock ? `\n\n## PROACT1-LITE 제안\n${suggestionBlock}` : ''}\n\n${file}`;
|
|
412
|
+
chars = `${text}\n${receiptKey}`.length;
|
|
413
|
+
let resolved: ReturnType<typeof recipient> = null;
|
|
414
|
+
try { resolved = recipient(deps); }
|
|
415
|
+
catch (error) { sendError = `수신자 설정 못 읽음 · ${shortReason(reasonOf(error))}`; }
|
|
416
|
+
const delivery = resolved ? sendReview(file, text, resolved, deps.send ?? sendTelegram, deps.receipt ?? (() => 'unknown'), deps.recordDelivery)
|
|
417
|
+
: { sent: false, sendError: '수신자 미설정', deliveryState: 'no-recipient', target: 'unknown' };
|
|
418
|
+
target = delivery.target;
|
|
324
419
|
sent = delivery.sent;
|
|
325
|
-
sendError = delivery.sendError;
|
|
420
|
+
sendError = sendError ?? delivery.sendError;
|
|
421
|
+
recordError = delivery.recordError ?? null;
|
|
422
|
+
deliveryState = delivery.deliveryState;
|
|
326
423
|
}
|
|
327
424
|
tick.sent = sent;
|
|
425
|
+
const status = vault.vaultFatal || Object.values(sections).includes('unreadable') ? 'degraded'
|
|
426
|
+
: deliveryState === 'pending' ? 'pending'
|
|
427
|
+
: ['sent', 'already-sent', 'receipt-sent'].includes(deliveryState) ? 'ok' : 'degraded';
|
|
428
|
+
const deliveryLine = `${options.stage} ${status} · target=${target} · ${deliveryState} · sent=${sent} · chars=${chars} · receipt=${receiptKey}${recordError ? ` · ${recordError}` : ''}`;
|
|
328
429
|
if (options.stage === 'deliver') {
|
|
329
|
-
|
|
330
|
-
|
|
430
|
+
Object.assign(tick, { status, deliveryState, target, chars, receiptKey, deliveryLine, recordError });
|
|
431
|
+
if (!options.dryRun) {
|
|
432
|
+
if (deps.log) deps.log('loop.rhythm-daily', 'tick', tick);
|
|
433
|
+
else loopEvent('rhythm-daily', 'tick', tick);
|
|
434
|
+
}
|
|
331
435
|
}
|
|
332
|
-
return { file, markdown, header, risks, sections, sent, ...vault, sendError };
|
|
436
|
+
return { file, markdown, header, risks, sections, sent, ...vault, sendError, recordError, status, deliveryState, deliveryLine };
|
|
333
437
|
}
|
|
334
438
|
const safe = async <T>(fetch: () => Promise<T>): Promise<Section<T>> => {
|
|
335
439
|
try { return { status: 'ok', value: await fetch() }; } catch (e) { return unreadable(reasonOf(e)); }
|
|
336
440
|
};
|
|
337
|
-
const landings = await safe(() =>
|
|
441
|
+
const landings = await safe(() => deps.landings ? deps.landings(now) : collectLandings(now, undefined, { repoRoot: deps.repoRoot, repoName: deps.repoName, stateRoot: deps.root }));
|
|
338
442
|
const parts: DailyParts = {
|
|
339
443
|
landings: { ...landings, runSummaries: collectRunSummaries(now, deps.root ?? effectiveInstanceRoot(),
|
|
340
444
|
landings.status === 'ok' ? landings.value.flatMap(item => item.prNumber === undefined ? [] : [item.prNumber]) : []) },
|
|
@@ -342,40 +446,123 @@ export async function runDaily(options: { dryRun?: boolean; noNews?: boolean; st
|
|
|
342
446
|
loops: await safe(deps.loops ?? collectLoops),
|
|
343
447
|
grid: await safe(deps.grid ?? collectGrid),
|
|
344
448
|
decisions: await safe(deps.decisions ?? collectDecisions),
|
|
345
|
-
news: options.noNews ? unreadable('건너뜀 (--no-news)') : await safe(deps.news ?? collectNews),
|
|
449
|
+
news: options.noNews ? unreadable('건너뜀 (--no-news)') : await safe(async () => cleanNews(await (deps.news ?? collectNews)())),
|
|
346
450
|
};
|
|
347
451
|
const review = composeDaily(parts, now);
|
|
452
|
+
const root = deps.root ?? effectiveInstanceRoot();
|
|
453
|
+
const observed = await safe(async (): Promise<ProactSignals> => {
|
|
454
|
+
if (deps.proactSignals) return deps.proactSignals();
|
|
455
|
+
const signals: { -readonly [K in keyof ProactSignals]: ProactSignals[K] } = {};
|
|
456
|
+
try {
|
|
457
|
+
const schedules = listSchedules(root);
|
|
458
|
+
const yellow = schedules.flatMap(schedule => {
|
|
459
|
+
try { return listChecklist(schedule.version, root).items.filter(item => item.status === 'yellow')
|
|
460
|
+
.map(item => ({ version: schedule.version, item })); }
|
|
461
|
+
catch { return []; }
|
|
462
|
+
});
|
|
463
|
+
signals.schedules = schedules;
|
|
464
|
+
signals.yellow = yellow;
|
|
465
|
+
} catch { /* source unreadable */ }
|
|
466
|
+
try { signals.decisions = new DecisionLedger({ stateDir: root }).list({ status: 'open' }); } catch { /* source unreadable */ }
|
|
467
|
+
try {
|
|
468
|
+
const remote = deps.repoName ?? (deps.root ? undefined : (() => {
|
|
469
|
+
const tree = deps.repoRoot ?? getUserConfig().ops?.cronRepoRoot ?? repo;
|
|
470
|
+
const result = spawnSync('git', ['-C', tree, 'remote', 'get-url', 'origin'], { encoding: 'utf8', timeout: 5_000 });
|
|
471
|
+
return result.status === 0 ? /(?:github\.com[:/])([^/\s]+\/[^/\s]+)\s*$/.exec(result.stdout)?.[1]?.replace(/\.git$/, '') : undefined;
|
|
472
|
+
})());
|
|
473
|
+
if (remote) {
|
|
474
|
+
const inventory = cli('gh', 'pr', 'list', '--repo', remote, '--state', 'open', '--limit', '500', '--json', 'number,title,isDraft');
|
|
475
|
+
if (Array.isArray(inventory) && inventory.length < 500) {
|
|
476
|
+
const drafts = rows(inventory).filter(pr => pr.isDraft === true && typeof pr.number === 'number' && typeof pr.title === 'string')
|
|
477
|
+
.map(pr => ({ number: pr.number as number, title: pr.title as string }));
|
|
478
|
+
// draft 정리 판정은 열린 draft 전수에 GitHub 호출을 돌려 10-05 운영에서 600초 넘게 끝나지 않았다(크론 수집 노드가 멎는다).
|
|
479
|
+
// 그래서 기본은 끈다 — loops.rhythm.daily.draftSweep=true 일 때만 돈다. 오래된 draft 정리는 DRAFT-TRIAGE 몫.
|
|
480
|
+
let sweepEnabled = false;
|
|
481
|
+
try { sweepEnabled = record(record(record(getUserConfig().raw.loops).rhythm).daily).draftSweep === true; } catch { /* config unreadable → off */ }
|
|
482
|
+
if (!sweepEnabled) loopEvent('rhythm-daily', 'draft-sweep-skipped', { drafts: drafts.length, reason: 'disabled-by-default' });
|
|
483
|
+
if (drafts.length && sweepEnabled) {
|
|
484
|
+
const sweep = await runDraftSweep({ repository: remote, apply: false, now, adapters: githubDraftSweepAdapters() });
|
|
485
|
+
signals.draftAssessment = { drafts, sweep };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
} catch { /* source unreadable */ }
|
|
490
|
+
return signals;
|
|
491
|
+
});
|
|
492
|
+
const signals: ProactSignals = observed.status === 'ok' ? observed.value : {};
|
|
493
|
+
const previousFile = join(root, 'rhythm', 'daily', `${kstDay(now)}.md`);
|
|
494
|
+
const retained = existsSync(previousFile) ? readFileSync(previousFile, 'utf8').split('## PROACT1-LITE 제안\n')[1]?.split('\n\n## ')[0]?.trim() ?? '' : '';
|
|
495
|
+
const previouslySuggested = new Set(deps.proactPreviouslySuggested ?? []);
|
|
496
|
+
// 같은 제안은 하루 한 번이 아니라 «지난 7일 안에 한 번»: 오늘 ⊕ 앞선 6일 리뷰 파일의 제안 표지를 모두 읽는다.
|
|
497
|
+
const history = [retained];
|
|
498
|
+
for (let back = 1; back < 7; back++) {
|
|
499
|
+
const file = join(root, 'rhythm', 'daily', `${kstDay(new Date(now.getTime() - back * 86_400_000))}.md`);
|
|
500
|
+
if (!existsSync(file)) continue;
|
|
501
|
+
try { history.push(readFileSync(file, 'utf8').split('## PROACT1-LITE 제안\n')[1]?.split('\n\n## ')[0] ?? ''); } catch { /* unreadable day is skipped, not treated as empty history */ }
|
|
502
|
+
}
|
|
503
|
+
for (const line of history.join('\n').split('\n')) {
|
|
504
|
+
const key = /^- .* \[proact:([^\]]+)\]$/.exec(line)?.[1];
|
|
505
|
+
if (key) previouslySuggested.add(key);
|
|
506
|
+
}
|
|
507
|
+
const suggestions = selectProact1Lite({ ...signals,
|
|
508
|
+
...(parts.loops.status === 'ok' ? { loops: parts.loops.value.map(loop => ({ name: loop.name, state: loop.status, scope: loop.scope })) } : {}),
|
|
509
|
+
}, { now, previouslySuggested });
|
|
510
|
+
const newSuggestions = suggestions.slice(0, Math.max(0, 3 - retained.split('\n').filter(line => line.startsWith('- ')).length))
|
|
511
|
+
.map(suggestion => `- ${suggestion.title} — ${suggestion.reason} (${suggestion.source}) [proact:${suggestion.key}]`).join('\n');
|
|
512
|
+
const recorded = [retained, newSuggestions].filter(Boolean).join('\n');
|
|
513
|
+
if (recorded) review.markdown += `\n## PROACT1-LITE 제안\n${recorded}\n`;
|
|
348
514
|
const file = join(deps.root ?? effectiveInstanceRoot(), 'rhythm', 'daily', `${kstDay(now)}.md`);
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
515
|
+
if (!options.dryRun) {
|
|
516
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
517
|
+
writeFileSync(file, review.markdown);
|
|
518
|
+
}
|
|
519
|
+
const { vaultError, vaultFatal } = options.dryRun ? { vaultError: null, vaultFatal: false } : copyToVault(review.markdown, now, deps);
|
|
352
520
|
let enabled = deps.sendEnabled === true;
|
|
353
521
|
let sendError: string | null = null;
|
|
522
|
+
let deliveryState = options.stage === 'collect' ? 'pending' : options.dryRun ? 'dry-run' : 'send-disabled';
|
|
354
523
|
if (deps.sendEnabled === undefined && !options.dryRun && options.stage !== 'collect') {
|
|
355
524
|
try { enabled = record(record(record(getUserConfig().raw.loops).rhythm).daily).send === true; }
|
|
356
|
-
catch (error) { sendError = `발송 설정 못 읽음 · ${reasonOf(error)}`; }
|
|
525
|
+
catch (error) { sendError = `발송 설정 못 읽음 · ${shortReason(reasonOf(error))}`; deliveryState = 'unknown'; }
|
|
357
526
|
}
|
|
358
527
|
let sent = false;
|
|
528
|
+
let recordError: string | null = null;
|
|
529
|
+
let chars = 0;
|
|
530
|
+
let target = 'unknown';
|
|
359
531
|
if (!options.dryRun && options.stage !== 'collect' && enabled) {
|
|
360
|
-
const
|
|
532
|
+
const text = `${review.header}\n\n## 위험 톱 5\n${review.risks.map((r, i) => `${i + 1}. ${r.name} — ${r.score}점 · ${r.reason}`).join('\n') || '확인된 위험 없음'}${recorded ? `\n\n## PROACT1-LITE 제안\n${recorded}` : ''}\n\n${file}`;
|
|
533
|
+
chars = `${text}\n${receiptKey}`.length;
|
|
534
|
+
let resolved: ReturnType<typeof recipient> = null;
|
|
535
|
+
try { resolved = recipient(deps); }
|
|
536
|
+
catch (error) { sendError = `수신자 설정 못 읽음 · ${shortReason(reasonOf(error))}`; }
|
|
537
|
+
const delivery = resolved ? sendReview(file, text, resolved, deps.send ?? sendTelegram, deps.receipt ?? (() => 'unknown'), deps.recordDelivery)
|
|
538
|
+
: { sent: false, sendError: '수신자 미설정', deliveryState: 'no-recipient', target: 'unknown' };
|
|
539
|
+
target = delivery.target;
|
|
361
540
|
sent = delivery.sent;
|
|
362
|
-
sendError = delivery.sendError;
|
|
541
|
+
sendError = sendError ?? delivery.sendError;
|
|
542
|
+
recordError = delivery.recordError ?? null;
|
|
543
|
+
deliveryState = delivery.deliveryState;
|
|
363
544
|
}
|
|
545
|
+
const status = vaultFatal || Object.values(review.sections).includes('unreadable') ? 'degraded'
|
|
546
|
+
: deliveryState === 'pending' ? 'pending'
|
|
547
|
+
: ['sent', 'already-sent', 'receipt-sent'].includes(deliveryState) ? 'ok' : 'degraded';
|
|
548
|
+
const deliveryLine = `${options.stage === 'collect' ? 'collect' : 'deliver'} ${status} · target=${target} · ${deliveryState} · sent=${sent} · chars=${chars} · receipt=${receiptKey}${recordError ? ` · ${recordError}` : ''}`;
|
|
364
549
|
if (options.stage !== 'collect') {
|
|
365
|
-
const tick = { sections: review.sections, risks: review.risks.length, sent };
|
|
366
|
-
if (
|
|
367
|
-
|
|
550
|
+
const tick = { sections: review.sections, risks: review.risks.length, sent, status, deliveryState, target, chars, receiptKey, deliveryLine, recordError };
|
|
551
|
+
if (!options.dryRun) {
|
|
552
|
+
if (deps.log) deps.log('loop.rhythm-daily', 'tick', tick);
|
|
553
|
+
else loopEvent('rhythm-daily', 'tick', tick);
|
|
554
|
+
}
|
|
368
555
|
}
|
|
369
|
-
return { file, ...review, sent, vaultError, vaultFatal, sendError };
|
|
556
|
+
return { file, ...review, sent, vaultError, vaultFatal, sendError, recordError, status, deliveryState, deliveryLine };
|
|
370
557
|
}
|
|
371
558
|
|
|
372
559
|
export async function main(args: string[] = process.argv.slice(2), deps: DailyDeps = {}): Promise<Awaited<ReturnType<typeof runDaily>>> {
|
|
373
560
|
const stage = process.env.ELANOUS_GRAPH_DIR && ['collect', 'compose', 'deliver'].includes(args[0] ?? '') ? args.shift() as 'collect' | 'compose' | 'deliver' : undefined;
|
|
374
561
|
for (const arg of args) if (!['--dry-run', '--no-news', '--json'].includes(arg)) throw new Error(`알 수 없는 옵션: ${arg}`);
|
|
375
562
|
const result = await runDaily({ dryRun: args.includes('--dry-run'), noNews: args.includes('--no-news'), stage }, deps);
|
|
376
|
-
(deps.print ?? console.log)(args.includes('--json') ? JSON.stringify(result) : `${result.markdown}\n파일: ${result.file} ·
|
|
377
|
-
//
|
|
378
|
-
if (stage === 'compose' && result.vaultFatal) throw new Error(`볼트 사본 실패 · ${result.vaultError}`);
|
|
563
|
+
(deps.print ?? console.log)(args.includes('--json') ? JSON.stringify(result) : `${result.markdown}\n파일: ${result.file} · ${result.deliveryLine}`);
|
|
564
|
+
// 발송 설정 OFF 는 degraded 로 기록하고 done 으로 진행한다. 실제 발송 실패·볼트 사본 실패는 그래프를 멈춘다.
|
|
565
|
+
if ((stage === 'compose' || !stage) && result.vaultFatal) throw new Error(`볼트 사본 실패 · ${result.vaultError}`);
|
|
379
566
|
if (stage === 'deliver' && result.sendError) throw new Error(`발송 실패 · ${result.sendError}`);
|
|
380
567
|
return result;
|
|
381
568
|
}
|
package/scripts/rhythm/weekly.ts
CHANGED
|
@@ -7,13 +7,15 @@ import { effectiveInstanceRoot, prodInstanceRoot } from '../../src/instance/reso
|
|
|
7
7
|
import { getUserConfig } from '../../src/user-config.js';
|
|
8
8
|
import { loopEvent } from '../../src/loops/observe.js';
|
|
9
9
|
import { sendOutbound } from '../../src/domains/outbound-alert.js';
|
|
10
|
-
import {
|
|
10
|
+
import { listSchedules } from '../../src/release-loop/release-schedule.js';
|
|
11
|
+
import { listChecklist } from '../../src/release-loop/checklist.js';
|
|
12
|
+
import { collectLandings, collectDecisions, type Landing, type Release, type Pending, type Section } from './daily.js';
|
|
11
13
|
|
|
12
14
|
type Issue = { id: string; title: string; owner: string; due: string; status: 'open' | 'done'; at: string; by: string };
|
|
13
15
|
type WeeklyParts = { landings: Section<Landing[]>; release: Section<Release>; issues: Section<Issue[]>;
|
|
14
16
|
followThrough: Section<{ issue: Issue; outcome: 'done' | '기한 뒤 완료' | '기한 넘김' | '진행'; doneAt?: string }[]>;
|
|
15
17
|
decisions: Section<Pending[]>; news: Section<{ title: string; url: string }[]> };
|
|
16
|
-
export type WeeklyDeps = { now?: () => Date; root?: string; vaultRoot?: string | null; sendEnabled?: boolean;
|
|
18
|
+
export type WeeklyDeps = { now?: () => Date; root?: string; vaultRoot?: string | null; sendEnabled?: boolean; repoRoot?: string; repoName?: string;
|
|
17
19
|
landings?: (now: Date, window: { from: Date; to: Date }) => Promise<Landing[]>; release?: () => Promise<Release>;
|
|
18
20
|
decisions?: () => Promise<Pending[]>; send?: (text: string, kind: string) => boolean;
|
|
19
21
|
receipt?: (key: string) => 'sent' | 'not-sent' | 'unknown';
|
|
@@ -60,6 +62,20 @@ function appendIssue(root: string, row: Issue) {
|
|
|
60
62
|
mkdirSync(dirname(file), { recursive: true });
|
|
61
63
|
appendFileSync(file, `${JSON.stringify(row)}\n`);
|
|
62
64
|
}
|
|
65
|
+
async function collectWeeklyRelease(now: Date, root: string): Promise<Release> {
|
|
66
|
+
if (!existsSync(join(root, 'release', 'features.sqlite'))) throw new Error('판 원장 없음');
|
|
67
|
+
const schedules = listSchedules(root).sort((a, b) => a.cutAt.localeCompare(b.cutAt));
|
|
68
|
+
const index = schedules.findIndex(schedule => Date.parse(schedule.cutAt) >= now.getTime());
|
|
69
|
+
if (index < 0) throw new Error('남은 판 일정 없음');
|
|
70
|
+
const current = schedules[index]!;
|
|
71
|
+
const items = listChecklist(current.version, root).items;
|
|
72
|
+
const next = schedules[index + 1];
|
|
73
|
+
const nextItems = next ? listChecklist(next.version, root).items : null;
|
|
74
|
+
return { version: current.version, green: items.filter(i => i.status === 'green').length, total: items.length,
|
|
75
|
+
nextVersion: next?.version ?? null, nextGreen: nextItems ? nextItems.filter(i => i.status === 'green').length : null,
|
|
76
|
+
nextTotal: nextItems?.length ?? null, cutAt: current.cutAt,
|
|
77
|
+
red: items.filter(i => i.status === 'red').map(i => ({ name: `${i.id} ${i.title}` })) };
|
|
78
|
+
}
|
|
63
79
|
function readDailyNews(root: string, from: Date, to: Date): Section<{ title: string; url: string }[]> {
|
|
64
80
|
const found = new Map<string, { title: string; url: string }>();
|
|
65
81
|
const failures: string[] = [];
|
|
@@ -67,7 +83,7 @@ function readDailyNews(root: string, from: Date, to: Date): Section<{ title: str
|
|
|
67
83
|
for (let at = from.getTime(); at < to.getTime(); at += DAY) {
|
|
68
84
|
const day = kstDate(new Date(at));
|
|
69
85
|
const file = join(root, 'rhythm', 'daily', `${day}.md`);
|
|
70
|
-
if (!existsSync(file))
|
|
86
|
+
if (!existsSync(file)) continue;
|
|
71
87
|
files++;
|
|
72
88
|
try {
|
|
73
89
|
const text = readFileSync(file, 'utf8');
|
|
@@ -83,7 +99,8 @@ function readDailyNews(root: string, from: Date, to: Date): Section<{ title: str
|
|
|
83
99
|
} catch (e) { failures.push(`${day} ${reasonOf(e)}`); }
|
|
84
100
|
}
|
|
85
101
|
const news = [...found.values()].slice(0, 10);
|
|
86
|
-
if (
|
|
102
|
+
if (!files) return { status: 'unreadable', reason: '데일리 없음' };
|
|
103
|
+
if (failures.length) return { status: 'unreadable', reason: `${failures.join('; ')}${news.length ? `\n부분 수집 (완전한 주간 집계 아님):\n${news.map(n => `- ${n.title} — ${n.url}`).join('\n')}` : ''}` };
|
|
87
104
|
return { status: 'ok', value: news };
|
|
88
105
|
}
|
|
89
106
|
const labels: Record<keyof WeeklyParts, string> = {
|
|
@@ -133,8 +150,10 @@ function vaultCopy(markdown: string, key: string, deps: WeeklyDeps) {
|
|
|
133
150
|
let root = deps.vaultRoot;
|
|
134
151
|
if (root === undefined) {
|
|
135
152
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
const config = getUserConfig();
|
|
154
|
+
const obsidian = object(config.raw.obsidian);
|
|
155
|
+
root = (typeof obsidian.vault === 'string' && obsidian.vault) || (typeof obsidian.vaultRoot === 'string' && obsidian.vaultRoot) || config.obsidian.vault || null;
|
|
156
|
+
if (root && effectiveInstanceRoot() !== prodInstanceRoot() && !root.startsWith(`${effectiveInstanceRoot()}/`)) return { vaultError: '격리 인스턴스 — 운영 볼트 사본 건너뜀', vaultFatal: false };
|
|
138
157
|
} catch (e) { return { vaultError: reasonOf(e), vaultFatal: true }; }
|
|
139
158
|
}
|
|
140
159
|
if (!root) return { vaultError: null, vaultFatal: false };
|
|
@@ -181,6 +200,8 @@ export async function runWeekly(options: { dryRun?: boolean; stage?: 'collect' |
|
|
|
181
200
|
const now = (deps.now ?? (() => new Date()))();
|
|
182
201
|
const root = deps.root ?? effectiveInstanceRoot();
|
|
183
202
|
const { from, to, key } = weekOf(now);
|
|
203
|
+
const newsTo = new Date(`${kstDate(now)}T00:00:00+09:00`);
|
|
204
|
+
const newsFrom = new Date(newsTo.getTime() - 7 * DAY);
|
|
184
205
|
const file = join(root, 'rhythm', 'weekly', `${key}.md`);
|
|
185
206
|
let markdown: string;
|
|
186
207
|
let header: string;
|
|
@@ -205,8 +226,8 @@ export async function runWeekly(options: { dryRun?: boolean; stage?: 'collect' |
|
|
|
205
226
|
};
|
|
206
227
|
const issues = await safe(async () => issueHistory(root));
|
|
207
228
|
const parts: WeeklyParts = {
|
|
208
|
-
landings: await safe(() =>
|
|
209
|
-
release: await safe(deps.release ??
|
|
229
|
+
landings: await safe(() => deps.landings ? deps.landings(now, { from, to }) : collectLandings(now, { from, to }, { repoRoot: deps.repoRoot, repoName: deps.repoName })),
|
|
230
|
+
release: await safe(deps.release ?? (() => collectWeeklyRelease(now, root))),
|
|
210
231
|
issues: issues.status === 'ok' ? { status: 'ok', value: [...latest(issues.value).values()].filter(i => i.status === 'open') } : issues,
|
|
211
232
|
followThrough: issues.status === 'ok' ? { status: 'ok', value: [...new Set([
|
|
212
233
|
...[...latest(issues.value, from.getTime()).values()].filter(i => i.status === 'open').map(i => i.id),
|
|
@@ -219,7 +240,7 @@ export async function runWeekly(options: { dryRun?: boolean; stage?: 'collect' |
|
|
|
219
240
|
return { issue, outcome: Date.parse(issue.due) < now.getTime() ? '기한 넘김' as const : '진행' as const };
|
|
220
241
|
}) } : issues,
|
|
221
242
|
decisions: await safe(deps.decisions ?? collectDecisions),
|
|
222
|
-
news: readDailyNews(root,
|
|
243
|
+
news: readDailyNews(root, newsFrom, newsTo),
|
|
223
244
|
};
|
|
224
245
|
const versions = await safe(async () => publishedVersions(root, from, to));
|
|
225
246
|
if (versions.status === 'unreadable') {
|