elanous 0.2.25 → 0.2.27
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/chunks/{1454-37fd16f479f5554a.js → 1454-a9c338f5bb556f42.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/{page-9360b1285b2a346e.js → page-a1ce306364f08da9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-07110da72c45e68f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/board/page-d2d76d4a7eae91bd.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-30007380f2e2a3c1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-e5a9302c105725a3.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-b93c04b5311c97ec.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/chat/page-718a49e86bf25f9c.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/consult/{page-44eaad486bc98a45.js → page-20662900d6b4b455.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/control/{page-97fa6cf3d34c7eb9.js → page-f707a509515ac50c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/{page-305cd294e754906e.js → page-6a16a48d1aa6e91f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-fcb06e305f515527.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/editor/page-6acf5ec4e6c6d1af.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-45889e7070d11126.js → page-39f82acf619cca23.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-16a2e893f83ae4d0.js → page-7c2c8f81b185f9c0.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-ae7aff07e32676bb.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-56888bc744e08dac.js → page-482c60c1bc540d0a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-7cb996c1aa08b6f5.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live/{page-952c6ceec6d9de38.js → page-623e08fe4477d606.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/{page-6d0202af568f4c63.js → page-db460cd8aecb8490.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-ff27902d0b11ef4f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/market/{page-74366ae2693a7d95.js → page-8397a5c6ca569bf4.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/{page-4fb6fc34e20c07b7.js → page-1917ea87ea71ab6b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-e9502921f4f01b1f.js → page-25a302273b054fd7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-b6169ea2f09cf287.js → not-found-670510eda8b9443f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-d73c3f1e00d93986.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-65caacf54389081e.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4bb7556e2e97a750.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-77e04b4f8762c666.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/outputs/{page-b9cc8a10bc6078b4.js → page-15c0a4095baddfba.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{page-3d3fc73dd187ed52.js → page-4eb96fc7241c2456.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-63e272a4cd3b6634.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-9c04dac0831c2bbb.js → page-0d64ff212dc55d03.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-7f096248993fc2d4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-04de9ca6d52a3893.js → page-e47fc72049149a38.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-62527677bbc2fdc4.js → page-f7d808e31e005895.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-9a2e09c4a311764c.js → page-5c2b24b5496dad73.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-bfeb35d69e8b24bb.js → page-76cdb531668b5191.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-e2b63345bba1a11f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-b008aff96be56654.js → page-d376e0c83edc041a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d6db55fe2c04d128.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-87b88f551f421d8b.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-6445ac4cbb8dd869.js → page-dab163b338e3b966.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-852170303c6cd755.js → page-08019efef111ed2e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-0260ffc7eda50db1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/{page-97c7361cc2a19319.js → page-980ce6fe36ad95eb.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-20162174b77f96cf.js → page-99c4e6c7345a9156.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/{page-8c6901833f897496.js → page-0ab42e0f05823013.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-ad9220422d20ba33.js +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +13 -13
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +13 -13
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +13 -13
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +13 -13
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +13 -13
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +13 -13
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +13 -13
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +13 -13
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +13 -13
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +13 -13
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +13 -13
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +13 -13
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +13 -13
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +13 -13
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +13 -13
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +13 -13
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +13 -13
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +13 -13
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +13 -13
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +13 -13
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +13 -13
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +13 -13
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +13 -13
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +13 -13
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +13 -13
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +13 -13
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +13 -13
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +13 -13
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +14 -14
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +13 -13
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +13 -13
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +13 -13
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +13 -13
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +13 -13
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +13 -13
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +13 -13
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +13 -13
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +13 -13
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +13 -13
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +13 -13
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +13 -13
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +13 -13
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +13 -13
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +13 -13
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +13 -13
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +13 -13
- package/package.json +1 -1
- package/scripts/ci-typecheck-changed.ts +81 -2
- package/scripts/coord-msg-bridge.ts +209 -0
- package/scripts/coord-post.sh +40 -7
- package/scripts/coord-requests-bridge.ts +10 -5
- package/scripts/expose-rubric-suggest.ts +1 -1
- package/scripts/measure-tsc-affected-roots.ts +10 -1
- package/scripts/release-loop/export-check-node.ts +8 -5
- package/scripts/release-loop/node-verdict.ts +7 -2
- package/src/agent/task-notification.ts +30 -1
- package/src/agent-substrate/review-observation.ts +9 -2
- package/src/agent-substrate/self-review-cli.ts +28 -6
- package/src/boot/daemon-tools/self-implement.ts +72 -4
- package/src/budget/llm-cost.ts +5 -1
- package/src/cli/control-cli.ts +29 -0
- package/src/cli/doctor-cli.ts +90 -8
- package/src/cli/msg-cli.ts +15 -5
- package/src/cli/pr-cli.ts +53 -0
- package/src/cli/pr-land-overlap-evidence.ts +18 -10
- package/src/cli/release-cli.ts +32 -5
- package/src/cli/seat-cli.ts +121 -0
- package/src/cli/self-utterances-cli.ts +140 -0
- package/src/cli/tasks-cli.ts +139 -9
- package/src/context-card/cell.ts +40 -0
- package/src/dashboard/boot-timing.ts +35 -0
- package/src/dashboard/index.ts +86 -24
- package/src/dashboard/input/chat-main-plain-turn-runtime.ts +26 -0
- package/src/dashboard/slash-runtime/dashboard-handlers.ts +6 -0
- package/src/debug/log.ts +1 -1
- package/src/domains/release-tool.ts +29 -12
- package/src/domains/surface-events.ts +16 -0
- package/src/elanous-config-dir.ts +2 -2
- package/src/fleet/host-doctor.ts +73 -2
- package/src/graph-runner/runner.ts +3 -1
- package/src/harness/harness-cli-command.ts +46 -4
- package/src/harness/harness-stop.ts +11 -7
- package/src/index.ts +35 -4
- package/src/instance/current.ts +2 -6
- package/src/instance/resolve.ts +5 -6
- package/src/llm/openai-sampling-fields.ts +89 -0
- package/src/llm/runtime-model-family.ts +61 -0
- package/src/llm/zai-coding-plan.ts +27 -1
- package/src/llm.ts +13 -89
- package/src/machines/machine-ledger.ts +31 -3
- package/src/maturity/cli-maturity.ts +13 -3
- package/src/msg/msg-store.ts +69 -7
- package/src/ops/overview-collect.ts +3 -2
- package/src/ops/overview-owner.ts +29 -0
- package/src/plugin-hooks/shell-hook.ts +17 -2
- package/src/plugin-missions/evaluator-runner.ts +18 -2
- package/src/plugins/install/plugin-install.ts +11 -1
- package/src/prompt-enhance/enhance.ts +11 -0
- package/src/release-loop/checklist.ts +41 -4
- package/src/release-loop/feature-store.ts +10 -9
- package/src/seat-loop/seat-loop.ts +5 -1
- package/src/self-dev/draft-sweep.ts +23 -14
- package/src/self-dev/orchestrate.ts +6 -2
- package/src/self-dev/run-supervisor.ts +35 -1
- package/src/self-implement/goal-author.ts +3 -27
- package/src/self-implement/goal-command-execution.ts +26 -0
- package/src/self-implement/orchestrator.ts +2 -1
- package/src/self-implement/pwa-import-graph.ts +194 -7
- package/src/self-implement/self-implement-runtime.ts +126 -21
- package/src/session/index.ts +14 -4
- package/src/session-runtime/index.ts +55 -11
- package/src/task-agent/cover.ts +28 -9
- package/src/task-agent/live-moves.ts +14 -6
- package/src/task-agent/rejudge-on-head.ts +220 -23
- package/src/task-agent/review-result-capture.ts +64 -1
- package/src/task-agent/shadow.ts +137 -22
- package/src/task-agent/task-hand.ts +18 -2
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +1 -0
- package/src/tool-runtime/tool-search-runtime.ts +25 -0
- package/src/tui.ts +20 -1
- package/src/user-config.ts +3 -59
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/app/autopilot/page-f08f6529d09ffae1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-f000d54d258f9ef6.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-b12149c51f7d04b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-22f674e7e4c2c9d5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ceo/page-8888733e40cea6d1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/page-9717ec6964b92276.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/design-check/page-dea4b795741126fd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-35e05d7450461a67.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-b2638013c0bf7dfd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/layout-c44f93a41167536f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/loops/page-1b810474507f3702.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/page-6a14333d1b1b3967.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/page-8c91a9a66c9ec90a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-4e634c5b98fd4e93.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/page-ed3070d637850548.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-128458c8d86fbb5f.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/page-cf0c2b0183991d09.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/share/page-89cb33bced824e4a.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-814394dcbae789b1.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-6a87325c87f09dd2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/page-087ceeb975ef272b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-05a4b2c9b91034aa.js +0 -1
- /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → Ev2jivDMTSoEc-5rIXdcy}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{dRS6lEhSRRBr1abbzp7fJ → Ev2jivDMTSoEc-5rIXdcy}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
5
|
+
import { redactSecretText } from '../debug/log.js';
|
|
6
|
+
import { defaultMsgStorePath, readUnreadCount } from '../msg/msg-store.js';
|
|
7
|
+
|
|
8
|
+
export const SEAT_ROLES = ['OP', 'TC', 'MK', 'UX'] as const;
|
|
9
|
+
export type SeatRole = typeof SEAT_ROLES[number];
|
|
10
|
+
|
|
11
|
+
const MAX_FIELD = 160;
|
|
12
|
+
const MAX_ROWS = 12;
|
|
13
|
+
const clean = (value: unknown): string => redactSecretText(String(value ?? '').replace(/[\r\n\t]+/g, ' ').trim()).slice(0, MAX_FIELD);
|
|
14
|
+
const safeObject = (value: unknown, keys: readonly string[]): Record<string, string> => {
|
|
15
|
+
const source = value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
16
|
+
return Object.fromEntries(keys.filter(key => source[key] !== undefined && source[key] !== null).map(key => [key, clean(source[key])]));
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function isRegularSeatPath(home: string, root: string, path: string): boolean {
|
|
20
|
+
const suffix = relative(root, path);
|
|
21
|
+
if (!suffix || suffix === '..' || suffix.startsWith(`..${sep}`) || suffix.startsWith(sep)) return false;
|
|
22
|
+
if (!lstatSync(home).isDirectory()) return false;
|
|
23
|
+
let parent = home;
|
|
24
|
+
for (const part of relative(home, dirname(path)).split(sep).filter(Boolean)) {
|
|
25
|
+
parent = join(parent, part);
|
|
26
|
+
if (!lstatSync(parent).isDirectory()) return false;
|
|
27
|
+
}
|
|
28
|
+
return lstatSync(path).isFile();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function metadata(home: string, root: string, path: string, kind: 'markdown' | 'tsv' | 'memory'): { source: string; updated: string; rows: string[]; reason?: string } {
|
|
32
|
+
if (!existsSync(path)) return { source: path, updated: '미상', rows: [], reason: '미관측: 파일 없음' };
|
|
33
|
+
try {
|
|
34
|
+
if (!isRegularSeatPath(home, root, path)) return { source: path, updated: '미상', rows: [], reason: '미관측: 일반 경로가 아님' };
|
|
35
|
+
const updated = statSync(path).mtime.toISOString();
|
|
36
|
+
const text = readFileSync(path, 'utf8').slice(0, 256_000);
|
|
37
|
+
let rows: string[] = [];
|
|
38
|
+
if (kind === 'tsv') {
|
|
39
|
+
const headers = (text.split(/\r?\n/, 1)[0] ?? '').split('\t');
|
|
40
|
+
const allowed = kind === 'tsv' && path.endsWith('harvest-claims.tsv') ? ['pr', 'claimer', 'at', 'status'] : ['at', 'what', 'then'];
|
|
41
|
+
const indexes = allowed.map(key => headers.indexOf(key));
|
|
42
|
+
rows = text.split(/\r?\n/).slice(1).filter(Boolean).slice(0, MAX_ROWS).map(line => {
|
|
43
|
+
const cells = line.split('\t');
|
|
44
|
+
return allowed.map((key, i) => indexes[i] < 0 ? '' : `${key}=${clean(cells[indexes[i]])}`).filter(Boolean).join(' · ');
|
|
45
|
+
}).filter(Boolean);
|
|
46
|
+
} else if (kind === 'memory') {
|
|
47
|
+
rows = text.split(/\r?\n/).filter(line => /^#{1,6}\s/.test(line)).slice(0, MAX_ROWS).map(line => clean(line.replace(/^#+\s*/, '')));
|
|
48
|
+
} else {
|
|
49
|
+
rows = text.split(/\r?\n/).filter(line => (/^#{1,6}\s/.test(line) || /^(?:[-*]\s*)?(?:from|발신|시각|at)\s*[::]/i.test(line)) && !/[`$;|<>]/.test(line)).slice(0, MAX_ROWS).map(line => {
|
|
50
|
+
const from = line.match(/^(?:[-*]\s*)?(?:from|발신)\s*[::]\s*(OP|TC|MK|UX)\b/i);
|
|
51
|
+
if (/^(?:[-*]\s*)?(?:from|발신)\s*[::]/i.test(line)) return from ? `From: ${from[1]!.toUpperCase()}` : '';
|
|
52
|
+
return clean(line.replace(/^#{1,6}\s*/, ''));
|
|
53
|
+
}).filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
return { source: path, updated, rows, ...(rows.length ? {} : { reason: '미관측: 허용된 메타데이터 없음' }) };
|
|
56
|
+
} catch {
|
|
57
|
+
return { source: path, updated: '미상', rows: [], reason: '미관측: 읽기 실패' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function contextFacts(snapshot: unknown): string[] {
|
|
62
|
+
const root = snapshot !== null && typeof snapshot === 'object' && !Array.isArray(snapshot) ? snapshot as Record<string, unknown> : {};
|
|
63
|
+
const facts: string[] = [];
|
|
64
|
+
const sourceFacts = Array.isArray(root.facts) ? root.facts as unknown[] : [];
|
|
65
|
+
for (const item of sourceFacts.slice(0, MAX_ROWS)) {
|
|
66
|
+
const value = item !== null && typeof item === 'object' && !Array.isArray(item) ? item as Record<string, unknown> : {};
|
|
67
|
+
const keys = value.kind === 'cell' ? ['id', 'status', 'owner', 'version', 'source']
|
|
68
|
+
: value.kind === 'seat' ? ['seat', 'at', 'status', 'id', 'source']
|
|
69
|
+
: value.kind === 'run' ? ['phase', 'elapsed', 'source'] : [];
|
|
70
|
+
const values = safeObject(value, keys);
|
|
71
|
+
if (Object.keys(values).length) facts.push(`${clean(value.kind)}: ${Object.entries(values).map(([k, v]) => `${k}=${v}`).join(' · ')}`);
|
|
72
|
+
}
|
|
73
|
+
const sourceEvents = Array.isArray(root.events) ? root.events as unknown[] : [];
|
|
74
|
+
for (const item of sourceEvents.slice(0, MAX_ROWS)) {
|
|
75
|
+
const values = safeObject(item, ['at', 'kind', 'source', 'summary']);
|
|
76
|
+
if (Object.keys(values).length) facts.push(`event: ${Object.entries(values).map(([k, v]) => `${k}=${v}`).join(' · ')}`);
|
|
77
|
+
}
|
|
78
|
+
return facts.slice(0, MAX_ROWS);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function renderSeatBootCard(seat: SeatRole, sections: ReadonlyArray<{ title: string; source: string; updated: string; rows: readonly string[]; reason?: string }>, facts: readonly string[], contextUpdated = '미상'): string {
|
|
82
|
+
const out = [`# ${seat} 자리 부팅 카드`, '', '## 현재 구조화 관측', '출처: elanous://context-now', `갱신: ${clean(contextUpdated)}`, ...(facts.length ? facts.map(row => `- ${clean(row)}`) : ['- 미관측: contextNow 구조화 facts/events 없음'])];
|
|
83
|
+
for (const section of sections) out.push('', `## ${section.title}`, `출처: ${clean(section.source)}`, `갱신: ${clean(section.updated)}`, ...(section.rows.length ? section.rows.map(row => `- ${clean(row)}`) : [`- ${clean(section.reason ?? '미관측')}`]));
|
|
84
|
+
return out.join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function registerSeatBootCommands(command: Command, options: { home?: () => string; contextNow?: () => unknown; msgPath?: () => string } = {}): void {
|
|
88
|
+
command.command('boot <seat>').description('현재 자리 카드 — 허용된 메타데이터만 읽기 전용 표시')
|
|
89
|
+
.action(async (rawSeat: string) => {
|
|
90
|
+
const seat = rawSeat.toUpperCase();
|
|
91
|
+
if (!(SEAT_ROLES as readonly string[]).includes(seat)) { console.error(`seat boot: unknown seat ${rawSeat} (OP|TC|MK|UX)`); process.exitCode = 2; return; }
|
|
92
|
+
const home = options.home?.() ?? homedir();
|
|
93
|
+
const root = join(home, 'elanous-hq', 'seat-state');
|
|
94
|
+
let facts: string[] = [];
|
|
95
|
+
let contextUpdated = '미상';
|
|
96
|
+
try {
|
|
97
|
+
const snapshot = options.contextNow ? options.contextNow() : await (async () => {
|
|
98
|
+
const mod = await import('../context-bus/context-now.js');
|
|
99
|
+
return (mod.contextNow as unknown as (options: { audience: string }) => unknown)({ audience: 'operator' });
|
|
100
|
+
})();
|
|
101
|
+
const context = snapshot !== null && typeof snapshot === 'object' ? snapshot as Record<string, unknown> : {};
|
|
102
|
+
contextUpdated = typeof context.at === 'string' ? clean(context.at) : '시각 미관측';
|
|
103
|
+
facts = contextFacts(snapshot);
|
|
104
|
+
} catch { facts = []; contextUpdated = '미관측: contextNow 조회 실패'; }
|
|
105
|
+
const seatDir = join(root, seat);
|
|
106
|
+
const msgPath = options.msgPath?.() ?? defaultMsgStorePath();
|
|
107
|
+
let unread: number | null = null;
|
|
108
|
+
try { unread = readUnreadCount(seat, msgPath); }
|
|
109
|
+
catch { /* unavailable is not a measured zero */ }
|
|
110
|
+
const sections = [
|
|
111
|
+
{ title: 'msg 원장', source: msgPath, updated: unread === null ? '미상' : new Date().toISOString(), rows: [`안 읽은 msg ${unread ?? '미관측'} (${seat})`] },
|
|
112
|
+
{ title: 'HANDOFF', ...metadata(home, root, join(seatDir, 'HANDOFF.md'), 'markdown') },
|
|
113
|
+
{ title: 'inbox', ...metadata(home, root, join(seatDir, 'inbox.md'), 'markdown') },
|
|
114
|
+
{ title: '수확 주장', ...metadata(home, root, join(seatDir, 'harvest-claims.tsv'), 'tsv') },
|
|
115
|
+
{ title: '예약', ...metadata(home, root, join(seatDir, 'scheduled.tsv'), 'tsv') },
|
|
116
|
+
{ title: '공통 메모리 색인', ...metadata(home, root, join(root, 'memory', 'MEMORY.md'), 'memory') },
|
|
117
|
+
];
|
|
118
|
+
console.log(renderSeatBootCard(seat as SeatRole, sections, facts, contextUpdated));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { Database } from 'bun:sqlite';
|
|
3
|
+
import { redactSecretText } from '../debug/log.js';
|
|
4
|
+
import { openSurfaceEventsDb, querySurfaceUtterances, surfaceEventsDbPath, type SurfaceEventRow } from '../domains/surface-events.js';
|
|
5
|
+
import { injectUtterance, type UtteranceInput } from '../domains/self-awareness.js';
|
|
6
|
+
import { writeStdoutJson } from './stdout-json.js';
|
|
7
|
+
|
|
8
|
+
const PAGE_SIZE = 50;
|
|
9
|
+
type UtteranceOrigin = 'human' | 'agent' | 'unknown';
|
|
10
|
+
|
|
11
|
+
function refsOf(raw: string | null): Record<string, unknown> {
|
|
12
|
+
if (!raw) return {};
|
|
13
|
+
try {
|
|
14
|
+
const value: unknown = JSON.parse(raw);
|
|
15
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
|
16
|
+
} catch { return {}; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function redact(value: string): string {
|
|
20
|
+
return redactSecretText(value)
|
|
21
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[REDACTED_EMAIL]')
|
|
22
|
+
.replace(/(https?:\/\/[^\s?#]+)\?[^\s#]*/gi, '$1?[REDACTED_QUERY]');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isAutomatic(row: SurfaceEventRow): boolean {
|
|
26
|
+
const refs = refsOf(row.refs);
|
|
27
|
+
if (row.surface === 'coord:channel' || row.surface === 'coord:seat' || refs.origin === 'coord:channel') return true;
|
|
28
|
+
const seatSource = typeof refs.origin === 'string' && /^(?:seat|seat-notice|notification|wake|coord:seat)$/i.test(refs.origin);
|
|
29
|
+
if (seatSource) return true;
|
|
30
|
+
if (refs.origin === 'observer' || refs.origin === 'observer-generated' || /observer-generated/i.test(row.tags ?? '')) return true;
|
|
31
|
+
if (row.kind === 'qna' && /^(?:telegram|discord)$/i.test(row.surface)) return false;
|
|
32
|
+
if (refs.origin === 'human' || refs.origin === 'user') return false;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function originOf(row: SurfaceEventRow): UtteranceOrigin {
|
|
37
|
+
if (row.direction !== 'inbound') return 'unknown';
|
|
38
|
+
const refs = refsOf(row.refs);
|
|
39
|
+
if (refs.utteranceOrigin === 'human' || refs.utteranceOrigin === 'agent') return refs.utteranceOrigin;
|
|
40
|
+
const explicit = refs.origin;
|
|
41
|
+
if (typeof explicit === 'string') {
|
|
42
|
+
if (/^(?:human|user|tui|pwa|telegram|discord|cli)$/i.test(explicit)) return 'human';
|
|
43
|
+
if (/^(?:claude-code|codex|gemini|agent|external:)/i.test(explicit)) return 'agent';
|
|
44
|
+
}
|
|
45
|
+
// recordInboundTurn marks its recorder as elanous-self; on chat Q&A the speaker is the human.
|
|
46
|
+
if (row.kind === 'qna' && /^(?:telegram|discord)$/i.test(row.surface)) return 'human';
|
|
47
|
+
if (explicit === 'elanous-self') return 'agent';
|
|
48
|
+
if (/^ext:/i.test(row.surface)) return 'agent';
|
|
49
|
+
if (/^(?:cli|tui|pwa|telegram|discord|ipad|watch)$/i.test(row.surface)) return 'human';
|
|
50
|
+
return 'unknown';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function utteranceText(row: SurfaceEventRow): { text: string; extract: 'direct' | 'qna' | 'ambiguous' | 'summary'; truncated: boolean; truncationEvidence: 'cap-reached' | 'over-cap' | null; possibleLoss: boolean } {
|
|
54
|
+
if (row.kind !== 'qna') return { text: row.text, extract: 'direct', truncated: false, truncationEvidence: null, possibleLoss: false };
|
|
55
|
+
const firstAnswer = row.text.startsWith('Q: ') ? row.text.indexOf('\nA: ', 3) : -1;
|
|
56
|
+
if (firstAnswer >= 0) {
|
|
57
|
+
const ambiguous = row.text.indexOf('\nQ: ', 3) >= 0 || row.text.indexOf('\nA: ', firstAnswer + 4) >= 0;
|
|
58
|
+
return { text: row.text.slice(3, firstAnswer), extract: ambiguous ? 'ambiguous' : 'qna', truncated: false, truncationEvidence: null, possibleLoss: ambiguous };
|
|
59
|
+
}
|
|
60
|
+
const summary = row.summary ?? '';
|
|
61
|
+
return { text: summary.slice(0, 150), extract: 'summary', truncated: summary.length > 150, truncationEvidence: summary.length > 150 ? 'over-cap' : summary.length === 150 ? 'cap-reached' : null, possibleLoss: false };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validDate(value: string, name: string): string {
|
|
65
|
+
const date = new Date(value);
|
|
66
|
+
if (!value || !Number.isFinite(date.getTime()) || !/^\d{4}-\d\d-\d\d(?:T.*)?$/.test(value)) throw new Error(`--${name} must be a valid ISO date/time`);
|
|
67
|
+
return date.toISOString();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface SelfUtterancesOptions {
|
|
71
|
+
from: string; to: string; surface?: string; origin?: UtteranceOrigin; page?: string; json?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function collectSelfUtterances(opts: SelfUtterancesOptions, db: ReturnType<typeof openSurfaceEventsDb>) {
|
|
75
|
+
const from = validDate(opts.from, 'from');
|
|
76
|
+
const to = validDate(opts.to, 'to');
|
|
77
|
+
if (from >= to) throw new Error('--from must be earlier than --to');
|
|
78
|
+
const page = opts.page === undefined ? 1 : Number(opts.page);
|
|
79
|
+
if (!Number.isSafeInteger(page) || page < 1) throw new Error('--page must be a positive integer');
|
|
80
|
+
const rows = querySurfaceUtterances(db, { from, to, ...(opts.surface ? { surface: opts.surface } : {}) });
|
|
81
|
+
let excludedAutomatic = 0;
|
|
82
|
+
let excludedByOrigin = 0;
|
|
83
|
+
let ambiguous = 0;
|
|
84
|
+
let truncated = 0;
|
|
85
|
+
let capReached = 0;
|
|
86
|
+
let possibleLoss = 0;
|
|
87
|
+
let excludedUnextractable = 0;
|
|
88
|
+
const utterances = rows.flatMap((row) => {
|
|
89
|
+
if (isAutomatic(row)) { excludedAutomatic += 1; return []; }
|
|
90
|
+
const origin = originOf(row);
|
|
91
|
+
if (opts.origin && origin !== opts.origin) { excludedByOrigin += 1; return []; }
|
|
92
|
+
const refs = refsOf(row.refs);
|
|
93
|
+
const cwd = typeof refs.cwd === 'string' && refs.cwd.length > 0 && !/[\u0000-\u001f\u007f]/.test(refs.cwd) ? redact(refs.cwd) : null;
|
|
94
|
+
const extracted = utteranceText(row);
|
|
95
|
+
if (row.kind === 'qna' && extracted.text.trim() === '') { excludedUnextractable += 1; return []; }
|
|
96
|
+
if (extracted.extract === 'ambiguous') ambiguous += 1;
|
|
97
|
+
if (extracted.truncated) truncated += 1;
|
|
98
|
+
if (extracted.truncationEvidence === 'cap-reached') capReached += 1;
|
|
99
|
+
if (extracted.possibleLoss) possibleLoss += 1;
|
|
100
|
+
return [{ id: redact(row.id), ts: redact(row.ts), surface: redact(row.surface), session: row.session_id ? redact(row.session_id) : null, cwd, origin, source: typeof refs.origin === 'string' ? redact(refs.origin) : 'unrecorded', text: redact(extracted.text), extract: extracted.extract, truncated: extracted.truncated, truncationEvidence: extracted.truncationEvidence, possibleLoss: extracted.possibleLoss }];
|
|
101
|
+
});
|
|
102
|
+
const total = utterances.length;
|
|
103
|
+
const start = (page - 1) * PAGE_SIZE;
|
|
104
|
+
return { source: 'surface_events.events (read-only)', filters: { from, to, surface: opts.surface ?? null, origin: opts.origin ?? null, automatic: 'excluded' }, counts: { matched: total, excludedAutomatic, excludedByOrigin, excludedUnextractable, ambiguous, truncated, capReached, possibleLoss }, page: { number: page, size: PAGE_SIZE, total, hasNext: start + PAGE_SIZE < total }, utterances: utterances.slice(start, start + PAGE_SIZE) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function recordSelfUtterance(input: UtteranceInput & { utteranceOrigin?: 'human' | 'agent' }, db?: Database): { eventId: string } {
|
|
108
|
+
const store = db ?? openSurfaceEventsDb();
|
|
109
|
+
try {
|
|
110
|
+
const result = injectUtterance(input, store);
|
|
111
|
+
if (input.utteranceOrigin === 'human') {
|
|
112
|
+
store.prepare("UPDATE events SET refs = json_set(refs, '$.utteranceOrigin', 'human') WHERE id = ?").run(result.eventId);
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
} finally { if (!db) store.close(); }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function registerSelfUtterancesCommand(selfCmd: Command): void {
|
|
119
|
+
selfCmd.command('utterances').description('Read paginated original utterances from surface_events (read-only)')
|
|
120
|
+
.requiredOption('--from <ISO>', 'Inclusive ISO date/time')
|
|
121
|
+
.requiredOption('--to <ISO>', 'Exclusive ISO date/time')
|
|
122
|
+
.option('--surface <surface>', 'Filter by surface')
|
|
123
|
+
.option('--origin <origin>', 'Filter by known origin: human|agent')
|
|
124
|
+
.option('--page <page>', 'One-based page number', '1')
|
|
125
|
+
.option('--json', 'Emit JSON')
|
|
126
|
+
.action(async (opts: SelfUtterancesOptions) => {
|
|
127
|
+
if (opts.origin && opts.origin !== 'human' && opts.origin !== 'agent') throw new Error('--origin must be human or agent');
|
|
128
|
+
const db = new Database(surfaceEventsDbPath(), { readonly: true });
|
|
129
|
+
try {
|
|
130
|
+
const result = collectSelfUtterances(opts, db);
|
|
131
|
+
if (opts.json) { await writeStdoutJson(JSON.stringify(result, null, 2) + '\n'); return; }
|
|
132
|
+
console.log(`source=${result.source} matched=${result.counts.matched} excludedAutomatic=${result.counts.excludedAutomatic} excludedByOrigin=${result.counts.excludedByOrigin} excludedUnextractable=${result.counts.excludedUnextractable} page=${result.page.number} size=${result.page.size} hasNext=${result.page.hasNext}`);
|
|
133
|
+
console.log(`filters=${JSON.stringify(result.filters)}`);
|
|
134
|
+
for (const item of result.utterances) console.log(JSON.stringify(item));
|
|
135
|
+
console.log(`ambiguous=${result.counts.ambiguous} truncated=${result.counts.truncated} cap-reached=${result.counts.capReached} possibleLoss=${result.counts.possibleLoss}`);
|
|
136
|
+
} finally { db.close(); }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export { PAGE_SIZE };
|
package/src/cli/tasks-cli.ts
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { loadRunLedger as loadRunLedgerSync } from '../self-implement/run-ledger.js';
|
|
4
|
+
import { goalRunDbPath, loadGoalRunQuery } from '../self-implement/goal-run-store.js';
|
|
5
|
+
import { loadGoalAskRecords } from '../self-implement/goal-ask-store.js';
|
|
4
6
|
import { join, resolve } from 'node:path';
|
|
5
7
|
import { InvalidArgumentError, Option, type Command } from 'commander';
|
|
6
8
|
import { debug } from '../debug/log.js';
|
|
7
9
|
import { getElanousConfigDir } from '../elanous-config-dir.js';
|
|
8
10
|
import { readNexusRuntime } from '../nexus/runtime.js';
|
|
9
11
|
import { checklistDevVersion } from '../release-loop/checklist.js';
|
|
12
|
+
import { assignedVersions, releasedVersion, validateVersion } from '../release-loop/feature-store.js';
|
|
13
|
+
import { compareVersions } from '../directives/version-at.js';
|
|
10
14
|
import { cardsBoard, countBoardTasks, INTERNAL_PACK_PROJECT, mergeBoards, releasePackBoard, type BoardNode } from '../task-agent/board.js';
|
|
11
15
|
import type { Task } from '../task-orchestrator/types.js';
|
|
12
|
-
import { cardPrNumber, CLOSED_BY_HAND_EVENT, closeTaskCard, COMPLETION_KINDS, handTask, updateTaskCard, LIVE_MOVE_HISTORY_EVENT, LIVE_MOVE_RESULT_EVENT, nextMoveFor, resolveHandText, readTaskAgentState, readTaskCard, taskAgentStatePath as taskStatePathDefault, shellQuote,
|
|
16
|
+
import { cardPrNumber, CLOSED_BY_HAND_EVENT, closeTaskCard, COMPLETION_KINDS, handTask, updateTaskCard, LIVE_MOVE_HISTORY_EVENT, LIVE_MOVE_RESULT_EVENT, nextMoveFor, resolveHandText, readTaskAgentState, readTaskCard, taskAgentStatePath as taskStatePathDefault, shellQuote, TASK_CARD_PREFIX, TASK_SEATS, type CompletionKind, type TaskCard, type TaskLaunchContext, type TaskLaunchReceipt, type TaskLauncher, type TaskSeat } from '../task-agent/task-hand.js';
|
|
13
17
|
import { readCardSalvageBranch, recordWiringLiveVerified, refreshCardRunBinding, type CardRunEvidenceDeps } from '../task-agent/card-evidence.js';
|
|
14
18
|
import { captureReviewResults } from '../task-agent/review-result-capture.js';
|
|
19
|
+
import { replayRemoteCardMoves } from '../task-agent/shadow.js';
|
|
15
20
|
import { HAND_MOVE_KINDS, recordHandMove } from '../task-agent/live-move-history.js';
|
|
16
21
|
import { advanceMission, handMission, MissionAdvanceError, openMissionIds, recordPieceRef, type AdvanceMissionResult, type MissionDecompose, type PieceEvidenceReader } from '../task-agent/mission.js';
|
|
17
22
|
import { formatSevenVerbLive, formatTaskAgentCover, measureSevenVerbLive, measureTaskAgentCover, type CoverRow, type SevenVerbLive, type TaskAgentCoverInput } from '../task-agent/cover.js';
|
|
@@ -38,7 +43,9 @@ export interface TasksCliDeps {
|
|
|
38
43
|
/** `task advance` 의 착지 근거 읽기(시험 주입). 기본 = `predecessorState`(gh · 런 원장). */
|
|
39
44
|
pieceEvidence?: PieceEvidenceReader;
|
|
40
45
|
/** `task board` 의 내부 팩 어댑터(시험 주입). 기본 = 현재 판(`checklistDevVersion()`)의 `releasePackBoard` — 읽기만. */
|
|
41
|
-
releaseBoard?: () => BoardNode[];
|
|
46
|
+
releaseBoard?: (version?: string) => BoardNode[];
|
|
47
|
+
/** `tasks hand --checklist` 판 목록(시험 주입). 기본은 발행 전 체크리스트가 있는 판 전부. */
|
|
48
|
+
releaseVersions?: () => string[];
|
|
42
49
|
/** `tasks show`·`advance` 의 런 근거 읽기(시험 주입) — 기본 = 런 원장(이 우주 ⊕ 연합) ⊕ 호스트 logs 의 Pod job-finished. */
|
|
43
50
|
cardRunEvidence?: CardRunEvidenceDeps;
|
|
44
51
|
/** `tasks live-verified` 가 적는 런 원장 디렉터리(시험 주입). 기본 = `runLedgerDir()`. */
|
|
@@ -54,10 +61,16 @@ export interface TasksCliDeps {
|
|
|
54
61
|
coverLogs?: (query: LogQuery) => LogStoreRow[] | CoverLogRead;
|
|
55
62
|
/** `tasks cover --by-card` 카드 원장 읽기 seam(읽기 전용). 플래그가 있을 때만 부른다. 기본 = TA 카드 원장(task-agent-actions.json). null = 못 읽음. */
|
|
56
63
|
coverCards?: () => CoverCardRow[] | null | Promise<CoverCardRow[] | null>;
|
|
64
|
+
/** Read-only authored goal provenance; null means the authoring ledger could not be read. */
|
|
65
|
+
coverGoalAsks?: () => { authorRunId: string; goalId: string; createdAt: string }[] | null;
|
|
66
|
+
/** Read-only terminal goal records; null means the goal ledger could not be read. */
|
|
67
|
+
coverGoalRuns?: () => { goalId: string; outcome: string; stage?: string; startedAt: string; completedAt?: string }[] | null;
|
|
57
68
|
/** TA-REJUDGE-ON-HEAD — `tasks rejudge` 의 판단 의존(시험 주입 · 기본 = 설정 liveMoves ⊕ 실물 gh·리뷰·재게이트·land). */
|
|
58
69
|
rejudge?: Partial<import('../task-agent/rejudge-on-head.js').RejudgeDeps>;
|
|
59
70
|
/** TA-REJUDGE-ON-HEAD — `tasks regate-run` 의 재게이트(시험 주입 · 기본 `runHostRegate`). */
|
|
60
71
|
hostRegate?: (input: import('../self-implement/host-regate.js').HostRegateInput) => Promise<import('../self-implement/host-regate.js').HostRegateResult>;
|
|
72
|
+
/** Isolated regate-run tests can disable the asynchronous completion callback explicitly. */
|
|
73
|
+
regateCompletion?: false;
|
|
61
74
|
}
|
|
62
75
|
|
|
63
76
|
/** `<조각id>=<값>` 반복 인자. */
|
|
@@ -152,8 +165,8 @@ async function tryRemoteParent(args: string[], cwd: string | undefined, context:
|
|
|
152
165
|
try { debug.log('task-agent', 'parent-host-fallback', { host: target.host, runId: context.runId, gaps, facts }); } catch { /* fail-soft */ }
|
|
153
166
|
notice(`[parent-host] ${target.host} 로 못 띄움 — ${multi ? '다음 호스트 또는 로컬 발사로 간다' : '로컬 발사로 간다'}: ${gaps.join(' · ')}`);
|
|
154
167
|
};
|
|
155
|
-
if (cwd
|
|
156
|
-
failed(targets[0]!, [
|
|
168
|
+
if (cwd) {
|
|
169
|
+
failed(targets[0]!, ['project-target-is-local']);
|
|
157
170
|
return null;
|
|
158
171
|
}
|
|
159
172
|
let launchHoldActive = false;
|
|
@@ -502,6 +515,7 @@ export function readTaskAgentCover(
|
|
|
502
515
|
window: { sinceMs?: number; untilMs?: number } = {},
|
|
503
516
|
query: NonNullable<TasksCliDeps['coverLogs']> = queryCoverLogs,
|
|
504
517
|
taskCards?: readonly { id: string }[] | null,
|
|
518
|
+
goalReaders: Pick<TasksCliDeps, 'coverGoalAsks' | 'coverGoalRuns'> = {},
|
|
505
519
|
): { rows: CoverRow[]; sevenVerbs: SevenVerbLive; limitReached: boolean; unreadableStores: number } {
|
|
506
520
|
let unreadableStores = 0;
|
|
507
521
|
const read = (category: 'task-agent' | 'pr.land', event: 'action' | 'step'): { rows: LogStoreRow[]; limitReached: boolean } | null => {
|
|
@@ -517,16 +531,51 @@ export function readTaskAgentCover(
|
|
|
517
531
|
const actions = actionRead?.rows ?? null;
|
|
518
532
|
const lands = landRead?.rows ?? null;
|
|
519
533
|
const truncated = { taskAgentActions: actionRead?.limitReached === true, allLands: landRead?.limitReached === true };
|
|
534
|
+
const inWindow = (value: string): boolean => {
|
|
535
|
+
const time = Date.parse(value);
|
|
536
|
+
return Number.isFinite(time) && (window.sinceMs === undefined || time >= window.sinceMs) && (window.untilMs === undefined || time <= window.untilMs);
|
|
537
|
+
};
|
|
538
|
+
let goalAsks: ReturnType<NonNullable<TasksCliDeps['coverGoalAsks']>> = null;
|
|
539
|
+
let goalRuns: ReturnType<NonNullable<TasksCliDeps['coverGoalRuns']>> = null;
|
|
540
|
+
let goalAsksTruncated = false;
|
|
541
|
+
let goalRunsTruncated = false;
|
|
542
|
+
try {
|
|
543
|
+
if (goalReaders.coverGoalAsks) goalAsks = goalReaders.coverGoalAsks();
|
|
544
|
+
else {
|
|
545
|
+
const asks = loadGoalAskRecords({ limit: COVER_QUERY_LIMIT }, goalRunDbPath());
|
|
546
|
+
goalAsksTruncated = asks.length >= COVER_QUERY_LIMIT;
|
|
547
|
+
goalAsks = asks;
|
|
548
|
+
}
|
|
549
|
+
} catch { goalAsks = null; }
|
|
550
|
+
try {
|
|
551
|
+
if (goalReaders.coverGoalRuns) goalRuns = goalReaders.coverGoalRuns();
|
|
552
|
+
else {
|
|
553
|
+
const runs = loadGoalRunQuery({ limit: COVER_QUERY_LIMIT }, goalRunDbPath());
|
|
554
|
+
goalRunsTruncated = runs?.truncated ?? false;
|
|
555
|
+
goalRuns = runs?.records.map(({ goalId, record }) => ({
|
|
556
|
+
goalId, outcome: record.outcome, stage: record.stage, startedAt: record.startedAt ?? '', completedAt: record.completedAt,
|
|
557
|
+
})) ?? null;
|
|
558
|
+
}
|
|
559
|
+
} catch { goalRuns = null; }
|
|
560
|
+
const authoredGoalIds = new Set(goalAsks?.map((ask) => ask.goalId) ?? []);
|
|
520
561
|
const input: TaskAgentCoverInput = {
|
|
521
562
|
taskAgentActions: actions === null ? null : coverEvents(actions, 'task-agent', 'action', (data) =>
|
|
522
563
|
typeof data.kind === 'string' && typeof data.result === 'string'
|
|
523
564
|
? { kind: data.kind as NonNullable<TaskAgentCoverInput['taskAgentActions']>[number]['kind'], result: data.result, ...(typeof data.taskId === 'string' ? { taskId: data.taskId } : {}) } : null),
|
|
524
565
|
allLands: lands === null ? null : coverEvents(lands, 'pr.land', 'step', (data) =>
|
|
525
566
|
typeof data.step === 'string' && typeof data.ok === 'boolean' ? { step: data.step, ok: data.ok } : null),
|
|
526
|
-
truncated,
|
|
567
|
+
truncated: { ...truncated, goalAsks: goalAsksTruncated, goalRuns: goalRunsTruncated },
|
|
568
|
+
// Authoring may start before this window; completion is the event being counted.
|
|
569
|
+
goalAsks,
|
|
570
|
+
goalRuns: goalRuns && (window.sinceMs !== undefined || window.untilMs !== undefined)
|
|
571
|
+
? goalRuns.some((run) => authoredGoalIds.has(run.goalId)
|
|
572
|
+
&& run.stage === 'merged' && (run.outcome === 'completed' || run.outcome === 'merged')
|
|
573
|
+
&& !Number.isFinite(Date.parse(run.completedAt ?? '')))
|
|
574
|
+
? null : goalRuns.filter((run) => inWindow(run.completedAt ?? ''))
|
|
575
|
+
: goalRuns,
|
|
527
576
|
...(taskCards === undefined ? {} : { taskCards }),
|
|
528
577
|
};
|
|
529
|
-
return { rows: measureTaskAgentCover(input), sevenVerbs: measureSevenVerbLive(input), limitReached: truncated.taskAgentActions || truncated.allLands, unreadableStores };
|
|
578
|
+
return { rows: measureTaskAgentCover(input), sevenVerbs: measureSevenVerbLive(input), limitReached: truncated.taskAgentActions || truncated.allLands || goalAsksTruncated || goalRunsTruncated, unreadableStores };
|
|
530
579
|
}
|
|
531
580
|
|
|
532
581
|
export function registerTasksCommands(program: Command, deps: TasksCliDeps = {}): void {
|
|
@@ -568,7 +617,7 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
|
|
|
568
617
|
}
|
|
569
618
|
taskCards = coverCardsInWindow(cards, { sinceMs, untilMs });
|
|
570
619
|
}
|
|
571
|
-
const { rows, sevenVerbs, limitReached, unreadableStores } = readTaskAgentCover({ sinceMs, untilMs }, deps.coverLogs, taskCards);
|
|
620
|
+
const { rows, sevenVerbs, limitReached, unreadableStores } = readTaskAgentCover({ sinceMs, untilMs }, deps.coverLogs, taskCards, deps);
|
|
572
621
|
const limitWarning = limitReached ? `조회 상한 ${COVER_QUERY_LIMIT} 도달 — 더 오래된 사건이 있을 수 있다(절단된 수를 전체로 읽지 말 것)` : null;
|
|
573
622
|
const storeWarning = unreadableStores > 0 ? `logs.db ${unreadableStores}개를 못 읽었다 — 그 우주의 사건은 수에 없다(«없다»로 읽지 말 것)` : null;
|
|
574
623
|
if (opts.json) out(JSON.stringify({ rows, sevenVerbs, limitReached, unreadableStores, ...(limitWarning ? { warning: limitWarning } : {}), ...(storeWarning ? { storeWarning } : {}) }));
|
|
@@ -624,6 +673,7 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
|
|
|
624
673
|
.description('TASK-AGENT 에 과제 한 줄을 넘긴다 — 카드를 적고 첫 수(harness say --substrate pod --merge-by-host)를 고른다 · 기본 shadow(발사 0) · `--mission` 이면 조각 카드 ⊕ 선후로 쪼개 선행 없는 조각만 넘긴다')
|
|
625
674
|
.addOption(new Option('--seat <seat>', `과제 자리 (${TASK_SEATS.join('|')})`).choices([...TASK_SEATS]))
|
|
626
675
|
.option('--checklist <cellId>', '릴리스 체크리스트 칸 id')
|
|
676
|
+
.option('--version <version>', '체크리스트 판 고정(생략하면 발행 전 판을 가까운 순서로 탐색)')
|
|
627
677
|
.addOption(new Option('--completion <kind>', `종결 종류 (${COMPLETION_KINDS.join('|')} · 기본 code-pr = PR 병합)`).choices([...COMPLETION_KINDS]))
|
|
628
678
|
.option('--mission <text>', '미션 문면 — 조각(`- …`·`① …`·`1. …` ⊕ `after: 1`)으로 나열돼 있으면 결정론, 아니면 분해기로 쪼갠다')
|
|
629
679
|
.option('--project <id>', '프로젝트 id — --target 과 함께')
|
|
@@ -635,7 +685,72 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
|
|
|
635
685
|
.option('--child-llm <provider/model>', 'TASKS-HAND-CHILD-LLM — 구현 자식 LLM(예: grok/grok-4.7) → 발사 인자 --child-llm-provider <provider> --child-llm-model <model> · «첫» `/` 에서만 가른다(모델 id 의 `/` 는 보존) · openrouter 는 모델 id 가 openrouter/ 접두를 포함하므로 전체를 모델로(openrouter/z-ai/glm-5.3 → --child-llm-model openrouter/z-ai/glm-5.3) · 카드 childLlm · 안 주면 명령·카드 무변경')
|
|
636
686
|
.option('--child-llm-effort <level>', '구현 자식 추론 노력 → 발사 인자 --child-llm-effort(검증은 하니스 · --child-llm 과 함께)')
|
|
637
687
|
.option('--json', 'JSON 출력')
|
|
638
|
-
.action(async (text: string | undefined, opts: { seat?: TaskSeat; checklist?: string; completion?: CompletionKind; mission?: string; project?: string; target?: string; goal?: string; milestone?: string; live?: boolean; taLand?: boolean; childLlm?: string; childLlmEffort?: string; json?: boolean }) => {
|
|
688
|
+
.action(async (text: string | undefined, opts: { seat?: TaskSeat; checklist?: string; version?: string; completion?: CompletionKind; mission?: string; project?: string; target?: string; goal?: string; milestone?: string; live?: boolean; taLand?: boolean; childLlm?: string; childLlmEffort?: string; json?: boolean }) => {
|
|
689
|
+
const suppliedText = text !== undefined;
|
|
690
|
+
if (opts.version !== undefined && opts.checklist === undefined) {
|
|
691
|
+
out('--version 은 --checklist 와 함께 준다');
|
|
692
|
+
process.exitCode = 1;
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (opts.checklist !== undefined) {
|
|
696
|
+
const findTitle = (nodes: BoardNode[]): string | undefined => {
|
|
697
|
+
for (const node of nodes) {
|
|
698
|
+
if (node.level === 'task' && node.id === opts.checklist) return node.title;
|
|
699
|
+
const nested = node.children ? findTitle(node.children) : undefined;
|
|
700
|
+
if (nested !== undefined) return nested;
|
|
701
|
+
}
|
|
702
|
+
return undefined;
|
|
703
|
+
};
|
|
704
|
+
if (opts.version !== undefined) {
|
|
705
|
+
try { validateVersion(opts.version); }
|
|
706
|
+
catch (error) {
|
|
707
|
+
out(error instanceof Error ? error.message : String(error));
|
|
708
|
+
process.exitCode = 1;
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const versions = opts.version !== undefined ? [opts.version]
|
|
713
|
+
: [checklistDevVersion(), ...(deps.releaseVersions?.() ?? (deps.releaseBoard ? [] : (() => {
|
|
714
|
+
const released = releasedVersion();
|
|
715
|
+
return assignedVersions().filter((version) => !released || compareVersions(version, released) > 0);
|
|
716
|
+
})()))];
|
|
717
|
+
const current = checklistDevVersion();
|
|
718
|
+
const orderedVersions = [...new Set(versions)].sort((a, b) => {
|
|
719
|
+
if (a === current) return -1;
|
|
720
|
+
if (b === current) return 1;
|
|
721
|
+
const aFuture = compareVersions(a, current) > 0;
|
|
722
|
+
const bFuture = compareVersions(b, current) > 0;
|
|
723
|
+
if (aFuture !== bFuture) return aFuture ? -1 : 1;
|
|
724
|
+
return aFuture ? compareVersions(a, b) : compareVersions(b, a);
|
|
725
|
+
});
|
|
726
|
+
const matches: Array<{ version: string; title: string }> = [];
|
|
727
|
+
for (const version of orderedVersions) {
|
|
728
|
+
try {
|
|
729
|
+
const title = findTitle((deps.releaseBoard ?? releasePackBoard)(version));
|
|
730
|
+
if (title !== undefined) matches.push({ version, title });
|
|
731
|
+
} catch (error) {
|
|
732
|
+
if (opts.version !== undefined) {
|
|
733
|
+
out(`체크리스트 판 읽기 실패: ${version}`);
|
|
734
|
+
process.exitCode = 1;
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
console.error(`⚠ 체크리스트 판 ${version} 읽기 실패 — 다른 판 계속 조회: ${error instanceof Error ? error.message : String(error)}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const title = matches[0]?.title;
|
|
741
|
+
if (title === undefined) {
|
|
742
|
+
out(`체크리스트 칸 없음: ${opts.checklist}`);
|
|
743
|
+
process.exitCode = 1;
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (matches.length > 1) console.error(`⚠ 체크리스트 칸 ${opts.checklist} 중복: ${matches.map((match) => match.version).join(', ')} — 가장 가까운 판 ${matches[0]!.version} 선택 (--version 으로 고정 가능)`);
|
|
747
|
+
if (opts.mission === undefined && text === undefined) text = title;
|
|
748
|
+
}
|
|
749
|
+
if (opts.checklist !== undefined && opts.mission === undefined && suppliedText && !text!.split(/\r?\n/, 1)[0]!.startsWith('대상 경로:')) {
|
|
750
|
+
out('칸 문면을 쓰려면 문면 인자를 빼라');
|
|
751
|
+
process.exitCode = 1;
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
639
754
|
await registerTaskAgentSink(deps.registerSink);
|
|
640
755
|
if ((text === undefined) === (opts.mission === undefined)) {
|
|
641
756
|
out('과제 한 줄 «또는» --mission <문면> 중 하나만 준다');
|
|
@@ -712,7 +827,7 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
|
|
|
712
827
|
out(`${result.card.id}\t${result.mode}\t${result.launched ? 'launched' : 'not launched'}`);
|
|
713
828
|
out(`elanous ${result.move.command!.map(shellQuote).join(' ')}`);
|
|
714
829
|
if (result.cwd) out(`(cwd: ${result.cwd})`);
|
|
715
|
-
if (result.card.taLand) out('(ta-land — 발사 env ELANOUS_TA_LAND=1 · 로컬 부모 · taskAgent.liveMoves 에 propose-land 가 있으면 병합은 TASK-AGENT 몫, 없으면 종전대로 호스트 병합)');
|
|
830
|
+
if (result.card.taLand) out('(ta-land — 발사 env ELANOUS_TA_LAND=1 · 원격 또는 로컬 부모 · taskAgent.liveMoves 에 propose-land 가 있으면 병합은 TASK-AGENT 몫, 없으면 종전대로 호스트 병합)');
|
|
716
831
|
if (!result.launched) out('(shadow — 띄우지 않았다 · 띄우려면 --live)');
|
|
717
832
|
}
|
|
718
833
|
} catch (error) {
|
|
@@ -961,6 +1076,9 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
|
|
|
961
1076
|
for (const cardId of bindable) {
|
|
962
1077
|
// HARNESS-PARENT-ON-MSB1: 원격 부모 런은 원장이 그 호스트에 있다 — HQ 거울로 먼저 당긴다(읽기 전용).
|
|
963
1078
|
await mirrorRemoteCardLedger(cardId, deps);
|
|
1079
|
+
try { await replayRemoteCardMoves(cardId, deps.taskStatePath ?? taskStatePathDefault(), deps.cardRunEvidence ?? {}); } catch (error) {
|
|
1080
|
+
try { debug.log('task-agent', 'card-binding-replay-failed', { card: cardId, reason: error instanceof Error ? error.message : String(error) }); } catch { /* fail-soft */ }
|
|
1081
|
+
}
|
|
964
1082
|
try { await refreshCardRunBinding(cardId, deps.taskStatePath ?? taskStatePathDefault(), deps.cardRunEvidence ?? {}); } catch (error) {
|
|
965
1083
|
// 근거 없음으로 보인다 — 원장 부재와 갈리게 실패는 남긴다.
|
|
966
1084
|
try { debug.log('task-agent', 'card-bind-failed', { card: cardId, via: 'show', error: error instanceof Error ? error.message : String(error) }); } catch { /* fail-soft */ }
|
|
@@ -1040,6 +1158,10 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
|
|
|
1040
1158
|
const liveMoves = deps.rejudge?.liveMoves ?? await configuredTaskAgentLiveMoves();
|
|
1041
1159
|
const all = [];
|
|
1042
1160
|
for (const cardId of ids) {
|
|
1161
|
+
await mirrorRemoteCardLedger(cardId, deps);
|
|
1162
|
+
try { await replayRemoteCardMoves(cardId, statePath, deps.cardRunEvidence ?? {}); } catch (error) {
|
|
1163
|
+
try { debug.log('task-agent', 'card-binding-replay-failed', { card: cardId, reason: error instanceof Error ? error.message : String(error) }); } catch { /* fail-soft */ }
|
|
1164
|
+
}
|
|
1043
1165
|
let outcomes: Awaited<ReturnType<typeof rejudgeCardOnHead>>;
|
|
1044
1166
|
try { outcomes = await rejudgeCardOnHead(cardId, { ...deps.rejudge, liveMoves, live: { ...deps.rejudge?.live, statePath } }); } catch (error) {
|
|
1045
1167
|
// 카드 하나의 예외가 --all 의 나머지 카드 판단을 멈추지 않게 — 사유를 남기고 다음 카드로.
|
|
@@ -1073,6 +1195,14 @@ export function registerTasksCommands(program: Command, deps: TasksCliDeps = {})
|
|
|
1073
1195
|
try { debug.log('task-agent', 'regate-run', { card: opts.card, ...record, failures: result.failures.slice(0, 3) }); } catch { /* fail-soft */ }
|
|
1074
1196
|
mkdir(dir(opts.out), { recursive: true });
|
|
1075
1197
|
write(opts.out, JSON.stringify(record));
|
|
1198
|
+
if (deps.regateCompletion !== false) {
|
|
1199
|
+
try {
|
|
1200
|
+
const { rejudgeCompletedTaskRegate } = await import('../task-agent/rejudge-on-head.js');
|
|
1201
|
+
await rejudgeCompletedTaskRegate(opts.card, pr, opts.head, opts.out, deps.taskStatePath ?? taskStatePathDefault(), undefined, deps.rejudge);
|
|
1202
|
+
} catch (error) {
|
|
1203
|
+
try { debug.log('task-agent', 'regate-completion-rejudge-failed', { card: opts.card, pr, head: opts.head, reason: String(error).slice(-300) }, { level: 'warn' }); } catch { /* fail-soft */ }
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1076
1206
|
if (!result.passed) process.exitCode = 1;
|
|
1077
1207
|
});
|
|
1078
1208
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ContextCard } from './index.js';
|
|
2
|
+
|
|
3
|
+
export interface CellCardItem {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly title: string;
|
|
6
|
+
readonly status: string;
|
|
7
|
+
readonly owner?: string | null;
|
|
8
|
+
readonly disposition?: string;
|
|
9
|
+
readonly evidence?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const POINTER_PATTERN = /#\d+|run-[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/giu;
|
|
13
|
+
|
|
14
|
+
function extractPointers(text: string): string[] {
|
|
15
|
+
return [...text.matchAll(POINTER_PATTERN)].map(([pointer]) => pointer);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Build a compact checklist-cell card; evidence is used only to derive stable source pointers. */
|
|
19
|
+
export function buildCellCard(version: string, item: CellCardItem, asOf: string): ContextCard {
|
|
20
|
+
const pointers = [...new Set([
|
|
21
|
+
...extractPointers(item.evidence ?? ''),
|
|
22
|
+
...extractPointers(item.title),
|
|
23
|
+
`checklist:${version}/${item.id}`,
|
|
24
|
+
])];
|
|
25
|
+
const reasons = [
|
|
26
|
+
item.owner ? `담당 ${item.owner}` : undefined,
|
|
27
|
+
item.disposition ? `처분 ${item.disposition}` : undefined,
|
|
28
|
+
].filter((value): value is string => value !== undefined);
|
|
29
|
+
return {
|
|
30
|
+
id: `cell:${version}/${item.id}`,
|
|
31
|
+
kind: 'cell',
|
|
32
|
+
conclusion: (item.title.split(/[\r\n\u2028\u2029]/u, 1)[0] ?? '').trim(),
|
|
33
|
+
why: reasons.join(' · '),
|
|
34
|
+
verdict: item.status,
|
|
35
|
+
remaining: [],
|
|
36
|
+
pointers,
|
|
37
|
+
source: `release-checklist:${version}`,
|
|
38
|
+
updatedAt: asOf,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { debug } from '../debug/log.js';
|
|
2
|
+
|
|
3
|
+
export interface BootTimer {
|
|
4
|
+
begin(): void;
|
|
5
|
+
phase(name: string): void;
|
|
6
|
+
done(): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createBootTimer(now: () => number = performance.now): BootTimer {
|
|
10
|
+
const startedAt = now();
|
|
11
|
+
let phaseStartedAt = startedAt;
|
|
12
|
+
let lastPhase: string | undefined;
|
|
13
|
+
let slowest: { phase: string; ms: number } | undefined;
|
|
14
|
+
let completed = false;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
begin(): void {
|
|
18
|
+
phaseStartedAt = now();
|
|
19
|
+
},
|
|
20
|
+
phase(name: string): void {
|
|
21
|
+
const current = now();
|
|
22
|
+
const ms = current - phaseStartedAt;
|
|
23
|
+
phaseStartedAt = current;
|
|
24
|
+
lastPhase = name;
|
|
25
|
+
if (!slowest || ms > slowest.ms) slowest = { phase: name, ms };
|
|
26
|
+
try { debug.log('tui.boot', 'phase', { phase: name, ms }); } catch { /* logging must not block startup */ }
|
|
27
|
+
},
|
|
28
|
+
done(): void {
|
|
29
|
+
if (completed) return;
|
|
30
|
+
completed = true;
|
|
31
|
+
const totalMs = now() - startedAt;
|
|
32
|
+
try { debug.log('tui.boot', 'summary', { totalMs, slowest: slowest ?? (lastPhase ? { phase: lastPhase, ms: totalMs } : null) }); } catch { /* logging must not block startup */ }
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|