@triflux/remote 10.50.0 → 10.50.2
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/cto/collect.mjs +16 -174
- package/cto/current.schema.json +4 -16
- package/cto/events.mjs +13 -294
- package/cto/hygiene.mjs +26 -346
- package/cto/lake-root.mjs +114 -16
- package/cto/status.mjs +31 -78
- package/hub/store-adapter.mjs +1 -1
- package/hub/team/conductor.mjs +1 -1
- package/hub/team/execution-mode.mjs +1 -1
- package/hub/team/headless.mjs +1 -1
- package/hub/team/psmux.mjs +16 -402
- package/hub/team/session.mjs +1 -28
- package/hub/team/terminal-opener.mjs +11 -35
- package/package.json +1 -1
- package/scripts/lib/gemini-profiles.mjs +1 -1
- package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
- package/scripts/lib/psmux-info.mjs +18 -49
- package/scripts/lib/skill-template.mjs +0 -26
- package/cto/hygiene-actions.mjs +0 -355
- package/cto/hygiene-notify.mjs +0 -170
- package/hub/workers/gemini-worker.mjs +0 -217
- package/scripts/lib/agent-json.mjs +0 -27
- package/scripts/lib/async.mjs +0 -174
- package/scripts/lib/cli-agy.mjs +0 -85
- package/scripts/lib/cli-codex.mjs +0 -62
- package/scripts/lib/cli-gemini.mjs +0 -37
- package/scripts/lib/env.mjs +0 -14
- package/scripts/lib/hub.mjs +0 -48
- package/scripts/lib/pid.mjs +0 -63
- package/scripts/lib/quota.mjs +0 -47
- package/scripts/lib/skill-state.mjs +0 -220
- package/scripts/lib/team.mjs +0 -75
- package/scripts/lib/timeout.mjs +0 -66
- package/scripts/lib/tmp.mjs +0 -26
- package/scripts/lib/toml.mjs +0 -132
package/cto/hygiene.mjs
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
statSync,
|
|
9
|
-
unlinkSync,
|
|
10
|
-
writeFileSync,
|
|
11
|
-
} from "node:fs";
|
|
12
|
-
import { homedir } from "node:os";
|
|
13
|
-
import { basename, dirname, join } from "node:path";
|
|
14
|
-
import { getCtoHygieneApplyMode } from "../hub/lib/cto-env.mjs";
|
|
15
|
-
import { appendCtoEvent } from "./events.mjs";
|
|
16
|
-
import { applyHygieneArchiveActions } from "./hygiene-actions.mjs";
|
|
17
|
-
import { resolveLakeRootDir } from "./lake-root.mjs";
|
|
4
|
+
normalizeLiveSession,
|
|
5
|
+
readJsonLines,
|
|
6
|
+
readSynapseSnapshot,
|
|
7
|
+
resolveLakeRootDir,
|
|
8
|
+
} from "./lake-root.mjs";
|
|
18
9
|
|
|
19
10
|
const COUNT_KEYS = [
|
|
20
11
|
"active_tasks",
|
|
@@ -29,10 +20,6 @@ function hasFlag(args, flag) {
|
|
|
29
20
|
return Array.isArray(args) && args.includes(flag);
|
|
30
21
|
}
|
|
31
22
|
|
|
32
|
-
function sleep(ms) {
|
|
33
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
34
|
-
}
|
|
35
|
-
|
|
36
23
|
function writeJson(stdout, payload) {
|
|
37
24
|
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
38
25
|
}
|
|
@@ -41,39 +28,6 @@ function readJson(filePath) {
|
|
|
41
28
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
42
29
|
}
|
|
43
30
|
|
|
44
|
-
function firstExisting(paths) {
|
|
45
|
-
return paths.filter(Boolean).find((path) => existsSync(path)) || null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function toIsoTime(value) {
|
|
49
|
-
if (typeof value === "string" && value.trim()) return value;
|
|
50
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
51
|
-
return new Date(value).toISOString();
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function normalizeLiveSession(session) {
|
|
57
|
-
return {
|
|
58
|
-
sessionId: String(session?.sessionId || session?.session_id || ""),
|
|
59
|
-
phase:
|
|
60
|
-
typeof session?.phase === "string"
|
|
61
|
-
? session.phase
|
|
62
|
-
: typeof session?.status === "string"
|
|
63
|
-
? session.status
|
|
64
|
-
: "active",
|
|
65
|
-
agent_id:
|
|
66
|
-
typeof session?.agent_id === "string"
|
|
67
|
-
? session.agent_id
|
|
68
|
-
: typeof session?.agentId === "string"
|
|
69
|
-
? session.agentId
|
|
70
|
-
: null,
|
|
71
|
-
started_at: toIsoTime(
|
|
72
|
-
session?.started_at ?? session?.startedAt ?? session?.lastHeartbeat,
|
|
73
|
-
),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
31
|
function normalizeOverlay(value) {
|
|
78
32
|
const sessions = Array.isArray(value)
|
|
79
33
|
? value
|
|
@@ -89,28 +43,10 @@ function normalizeOverlay(value) {
|
|
|
89
43
|
};
|
|
90
44
|
}
|
|
91
45
|
|
|
92
|
-
async function readSynapseWithRegistry(opts = {}) {
|
|
93
|
-
const rootDir = opts.rootDir || process.cwd();
|
|
94
|
-
const persistPath = firstExisting([
|
|
95
|
-
opts.synapsePersistPath,
|
|
96
|
-
join(rootDir, ".triflux", "synapse-registry.json"),
|
|
97
|
-
join(rootDir, ".triflux", "synapse", "registry.json"),
|
|
98
|
-
join(homedir(), ".claude", "cache", "tfx-hub", "synapse-sessions.json"),
|
|
99
|
-
join(homedir(), ".claude", "cache", "tfx-hub", "synapse-registry.json"),
|
|
100
|
-
]);
|
|
101
|
-
if (!persistPath) return { live_sessions: [] };
|
|
102
|
-
|
|
103
|
-
const { createSynapseRegistry } = await import(
|
|
104
|
-
"../hub/team/synapse-registry.mjs"
|
|
105
|
-
);
|
|
106
|
-
const registry = createSynapseRegistry({ persistPath });
|
|
107
|
-
return normalizeOverlay(registry.getActive());
|
|
108
|
-
}
|
|
109
|
-
|
|
110
46
|
async function readLiveOverlay(opts = {}) {
|
|
111
47
|
if (opts.overlay) return normalizeOverlay(opts.overlay);
|
|
112
48
|
try {
|
|
113
|
-
const reader = opts.synapseReader ||
|
|
49
|
+
const reader = opts.synapseReader || readSynapseSnapshot;
|
|
114
50
|
return normalizeOverlay(
|
|
115
51
|
await reader({
|
|
116
52
|
rootDir: opts.rootDir,
|
|
@@ -123,21 +59,6 @@ async function readLiveOverlay(opts = {}) {
|
|
|
123
59
|
}
|
|
124
60
|
}
|
|
125
61
|
|
|
126
|
-
function readJsonLines(filePath) {
|
|
127
|
-
if (!existsSync(filePath)) return [];
|
|
128
|
-
return readFileSync(filePath, "utf8")
|
|
129
|
-
.split(/\r?\n/u)
|
|
130
|
-
.map((line) => line.trim())
|
|
131
|
-
.filter(Boolean)
|
|
132
|
-
.flatMap((line) => {
|
|
133
|
-
try {
|
|
134
|
-
return [JSON.parse(line)];
|
|
135
|
-
} catch {
|
|
136
|
-
return [];
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
62
|
function eventTime(entry) {
|
|
142
63
|
return typeof entry?.ts === "string" ? entry.ts : "";
|
|
143
64
|
}
|
|
@@ -429,271 +350,30 @@ export function projectCtoHygiene({
|
|
|
429
350
|
};
|
|
430
351
|
}
|
|
431
352
|
|
|
432
|
-
function
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
.toLowerCase()
|
|
436
|
-
.replace(/[^a-z0-9._-]+/gu, "-")
|
|
437
|
-
.replace(/^-+|-+$/gu, "")
|
|
438
|
-
.slice(0, 40) || "project"
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function rootHash(rootDir) {
|
|
443
|
-
return createHash("sha256")
|
|
444
|
-
.update(String(rootDir || ""))
|
|
445
|
-
.digest("hex")
|
|
446
|
-
.slice(0, 12);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
export function ctoHygieneStewardLockPath(rootDir, lakeRoot) {
|
|
450
|
-
const label = safeLabel(basename(rootDir || "project"));
|
|
451
|
-
return join(lakeRoot, "stewards", `${label}-${rootHash(rootDir)}.apply.lock`);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
export async function acquireCtoHygieneStewardLock(
|
|
455
|
-
rootDir,
|
|
456
|
-
lakeRoot,
|
|
457
|
-
opts = {},
|
|
458
|
-
) {
|
|
459
|
-
const lockPath =
|
|
460
|
-
opts.lockPath || ctoHygieneStewardLockPath(rootDir, lakeRoot);
|
|
461
|
-
const timeoutMs = Math.max(0, Number(opts.timeoutMs) || 3000);
|
|
462
|
-
const retryMs = Math.max(1, Number(opts.retryMs) || 50);
|
|
463
|
-
const staleMs = Math.max(1000, Number(opts.staleMs) || 10 * 60_000);
|
|
464
|
-
const start = Date.now();
|
|
465
|
-
|
|
466
|
-
mkdirSync(dirname(lockPath), { recursive: true });
|
|
467
|
-
|
|
468
|
-
while (true) {
|
|
469
|
-
let fd = null;
|
|
470
|
-
try {
|
|
471
|
-
fd = openSync(lockPath, "wx", 0o600);
|
|
472
|
-
writeFileSync(
|
|
473
|
-
fd,
|
|
474
|
-
`${JSON.stringify(
|
|
475
|
-
{
|
|
476
|
-
pid: process.pid,
|
|
477
|
-
project_root: rootDir,
|
|
478
|
-
project_root_hash: rootHash(rootDir),
|
|
479
|
-
created_at: new Date().toISOString(),
|
|
480
|
-
},
|
|
481
|
-
null,
|
|
482
|
-
2,
|
|
483
|
-
)}\n`,
|
|
484
|
-
"utf8",
|
|
485
|
-
);
|
|
486
|
-
return {
|
|
487
|
-
path: lockPath,
|
|
488
|
-
project_root: rootDir,
|
|
489
|
-
project_root_hash: rootHash(rootDir),
|
|
490
|
-
release() {
|
|
491
|
-
try {
|
|
492
|
-
closeSync(fd);
|
|
493
|
-
} catch {}
|
|
494
|
-
try {
|
|
495
|
-
unlinkSync(lockPath);
|
|
496
|
-
} catch {}
|
|
497
|
-
},
|
|
498
|
-
};
|
|
499
|
-
} catch (error) {
|
|
500
|
-
if (fd !== null) {
|
|
501
|
-
try {
|
|
502
|
-
closeSync(fd);
|
|
503
|
-
} catch {}
|
|
504
|
-
}
|
|
505
|
-
if (error?.code !== "EEXIST") throw error;
|
|
506
|
-
try {
|
|
507
|
-
if (Date.now() - statSync(lockPath).mtimeMs > staleMs) {
|
|
508
|
-
unlinkSync(lockPath);
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
} catch {}
|
|
512
|
-
if (Date.now() - start >= timeoutMs) {
|
|
513
|
-
throw new Error(`cto hygiene steward lock busy: ${lockPath}`);
|
|
514
|
-
}
|
|
515
|
-
await sleep(retryMs);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const APPLICABLE_STATUSES = new Set([
|
|
521
|
-
"active",
|
|
522
|
-
"stale",
|
|
523
|
-
"superseded",
|
|
524
|
-
"orphaned",
|
|
525
|
-
"hidden",
|
|
526
|
-
"completed",
|
|
527
|
-
]);
|
|
528
|
-
|
|
529
|
-
function isApplicableRow(row) {
|
|
530
|
-
return Boolean(row?.action) || APPLICABLE_STATUSES.has(row?.status);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const ARCHIVE_ACK_STATUSES = new Set(["archived", "already_archived"]);
|
|
534
|
-
|
|
535
|
-
function hygieneApplyActor(opts = {}) {
|
|
536
|
-
const actor = { cli: "tfx cto hygiene --apply" };
|
|
537
|
-
if (typeof opts.actorSessionId === "string" && opts.actorSessionId.trim()) {
|
|
538
|
-
actor.session_id = opts.actorSessionId.trim();
|
|
353
|
+
export async function runHygiene(args = [], opts = {}) {
|
|
354
|
+
if (args.some((arg) => !["--dry-run", "--json"].includes(arg))) {
|
|
355
|
+
throw new Error("tfx cto hygiene supports only --dry-run and --json");
|
|
539
356
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
async function applyHygieneRows({
|
|
544
|
-
rootDir,
|
|
545
|
-
lakeRoot,
|
|
546
|
-
projection,
|
|
547
|
-
steward,
|
|
548
|
-
active,
|
|
549
|
-
approvals,
|
|
550
|
-
opts,
|
|
551
|
-
}) {
|
|
552
|
-
const rows = projection.rows.filter(isApplicableRow);
|
|
553
|
-
const appended = [];
|
|
554
|
-
const now = opts.now || new Date().toISOString();
|
|
555
|
-
const actions = await applyHygieneArchiveActions({
|
|
556
|
-
rootDir,
|
|
557
|
-
lakeRoot,
|
|
558
|
-
projection: { ...projection, rows },
|
|
559
|
-
dryRun: false,
|
|
560
|
-
now,
|
|
561
|
-
applyMode: opts.applyMode || getCtoHygieneApplyMode(),
|
|
562
|
-
active,
|
|
563
|
-
humanAck: opts.humanAck,
|
|
564
|
-
humanGate: opts.humanGate,
|
|
565
|
-
actorSessionId: opts.actorSessionId,
|
|
566
|
-
approvals,
|
|
567
|
-
fsOps: opts.fsOps,
|
|
568
|
-
});
|
|
569
|
-
const operationByKey = new Map(
|
|
570
|
-
(actions.operations || []).map((operation) => [
|
|
571
|
-
operation.hygiene_key,
|
|
572
|
-
operation,
|
|
573
|
-
]),
|
|
574
|
-
);
|
|
575
|
-
for (const row of rows) {
|
|
576
|
-
const hygieneKey = hygieneKeyForRow(row);
|
|
577
|
-
const operation = operationByKey.get(hygieneKey);
|
|
578
|
-
if (operation && !ARCHIVE_ACK_STATUSES.has(operation.status)) continue;
|
|
579
|
-
const result = await appendCtoEvent(
|
|
580
|
-
lakeRoot,
|
|
581
|
-
{
|
|
582
|
-
event: "hygiene_applied",
|
|
583
|
-
now,
|
|
584
|
-
source: "tfx_cto_hygiene_apply",
|
|
585
|
-
project_root: rootDir,
|
|
586
|
-
status: row.status,
|
|
587
|
-
hygiene_key: hygieneKey,
|
|
588
|
-
hygiene_kind: row.kind,
|
|
589
|
-
hygiene_id: row.id,
|
|
590
|
-
hygiene_action: row.action || `acknowledge_${row.status}`,
|
|
591
|
-
summary: `hygiene apply ${row.kind}:${row.id} ${row.status}`,
|
|
592
|
-
actor: hygieneApplyActor(opts),
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
stderr: opts.stderr,
|
|
596
|
-
lockRetries: opts.ledgerLockRetries,
|
|
597
|
-
lockRetryDelayMs: opts.ledgerLockRetryDelayMs,
|
|
598
|
-
},
|
|
599
|
-
);
|
|
600
|
-
if (result.appended) appended.push(result.event);
|
|
357
|
+
if (opts.dryRun !== true && !hasFlag(args, "--dry-run")) {
|
|
358
|
+
throw new Error("tfx cto hygiene requires --dry-run");
|
|
601
359
|
}
|
|
602
|
-
return {
|
|
603
|
-
steward: {
|
|
604
|
-
lock_path: steward.path,
|
|
605
|
-
project_root: steward.project_root,
|
|
606
|
-
project_root_hash: steward.project_root_hash,
|
|
607
|
-
},
|
|
608
|
-
applicable_count: rows.length,
|
|
609
|
-
applied_count: appended.length,
|
|
610
|
-
actions,
|
|
611
|
-
events: appended,
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
export async function runHygiene(args = [], opts = {}) {
|
|
616
360
|
const rootDir = opts.rootDir || resolveLakeRootDir(process.cwd());
|
|
617
361
|
const lakeRoot = opts.lakeRoot || join(rootDir, ".triflux", "lake");
|
|
618
362
|
const stdout = opts.stdout || process.stdout;
|
|
619
|
-
const
|
|
620
|
-
const
|
|
621
|
-
const
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const current = existsSync(currentPath) ? readJson(currentPath) : {};
|
|
632
|
-
const ledger = readJsonLines(join(lakeRoot, "ledger.jsonl"));
|
|
633
|
-
const overlay = await readLiveOverlay({ ...opts, rootDir, lakeRoot });
|
|
634
|
-
return {
|
|
635
|
-
ledger,
|
|
636
|
-
overlay,
|
|
637
|
-
projection: projectCtoHygiene({
|
|
638
|
-
current,
|
|
639
|
-
ledger: ledger.length > 0 ? ledger : null,
|
|
640
|
-
overlay,
|
|
641
|
-
}),
|
|
642
|
-
};
|
|
643
|
-
};
|
|
644
|
-
|
|
645
|
-
let steward = null;
|
|
646
|
-
let projection;
|
|
647
|
-
let overlay = { live_sessions: [] };
|
|
648
|
-
let ledger = [];
|
|
649
|
-
let applyResult = null;
|
|
650
|
-
if (apply) {
|
|
651
|
-
steward = await acquireCtoHygieneStewardLock(rootDir, lakeRoot, {
|
|
652
|
-
timeoutMs: opts.stewardLockTimeoutMs,
|
|
653
|
-
retryMs: opts.stewardLockRetryMs,
|
|
654
|
-
staleMs: opts.stewardLockStaleMs,
|
|
655
|
-
lockPath: opts.stewardLockPath,
|
|
656
|
-
});
|
|
657
|
-
try {
|
|
658
|
-
({ projection, overlay, ledger } = await buildProjection());
|
|
659
|
-
applyResult = await applyHygieneRows({
|
|
660
|
-
rootDir,
|
|
661
|
-
lakeRoot,
|
|
662
|
-
projection,
|
|
663
|
-
steward,
|
|
664
|
-
active: overlay,
|
|
665
|
-
approvals: ledger.filter((entry) => entry?.event === "hygiene_applied"),
|
|
666
|
-
opts,
|
|
667
|
-
});
|
|
668
|
-
projection = { ...projection, dry_run: false, apply: applyResult };
|
|
669
|
-
} finally {
|
|
670
|
-
steward.release();
|
|
671
|
-
}
|
|
672
|
-
} else {
|
|
673
|
-
({ projection, overlay } = await buildProjection());
|
|
674
|
-
projection = {
|
|
675
|
-
...projection,
|
|
676
|
-
actions: await applyHygieneArchiveActions({
|
|
677
|
-
rootDir,
|
|
678
|
-
lakeRoot,
|
|
679
|
-
projection,
|
|
680
|
-
dryRun: true,
|
|
681
|
-
now: opts.now || new Date().toISOString(),
|
|
682
|
-
applyMode: opts.applyMode || getCtoHygieneApplyMode(),
|
|
683
|
-
active: overlay,
|
|
684
|
-
fsOps: opts.fsOps,
|
|
685
|
-
}),
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
if (jsonOut) writeJson(stdout, projection);
|
|
690
|
-
else {
|
|
363
|
+
const currentPath = join(lakeRoot, "current.json");
|
|
364
|
+
const current = existsSync(currentPath) ? readJson(currentPath) : {};
|
|
365
|
+
const ledger = readJsonLines(join(lakeRoot, "ledger.jsonl"));
|
|
366
|
+
const overlay = await readLiveOverlay({ ...opts, rootDir, lakeRoot });
|
|
367
|
+
const projection = projectCtoHygiene({
|
|
368
|
+
current,
|
|
369
|
+
ledger: ledger.length ? ledger : null,
|
|
370
|
+
overlay,
|
|
371
|
+
});
|
|
372
|
+
if (opts.json === true || hasFlag(args, "--json"))
|
|
373
|
+
writeJson(stdout, projection);
|
|
374
|
+
else
|
|
691
375
|
stdout.write(
|
|
692
|
-
|
|
693
|
-
? `cto hygiene apply: ${applyResult.applied_count} event(s) appended for ${applyResult.applicable_count} actionable row(s)\n`
|
|
694
|
-
: `cto hygiene dry-run: ${projection.counts.active_tasks} active tasks, ${projection.counts.stale_sessions} stale sessions, ${projection.counts.orphan_worktrees} orphan worktrees\n`,
|
|
376
|
+
`cto hygiene dry-run: ${projection.counts.active_tasks} active tasks, ${projection.counts.stale_sessions} stale sessions, ${projection.counts.orphan_worktrees} orphan worktrees\n`,
|
|
695
377
|
);
|
|
696
|
-
}
|
|
697
|
-
|
|
698
378
|
return projection;
|
|
699
379
|
}
|
package/cto/lake-root.mjs
CHANGED
|
@@ -1,19 +1,12 @@
|
|
|
1
|
-
// cto/lake-root.mjs - resolve the canonical project root for the CTO lake.
|
|
2
|
-
//
|
|
3
|
-
// CTO lake (.triflux/lake/current.json) 는 repo 루트에 하나만 둔다. 그런데
|
|
4
|
-
// runStatus/runCollect/runDashboard 는 process.cwd() 를 기본 rootDir 로 쓰므로,
|
|
5
|
-
// repo 루트가 아닌 하위 폴더(예: packages/triflux)에서 `tfx cto` 를 부르면 lake
|
|
6
|
-
// 를 못 찾아 "run tfx cto collect" 가 반복되고, collect 는 엉뚱한 하위 폴더에
|
|
7
|
-
// 새 .triflux/lake 를 만든다. cwd 에서 `.git` 마커를 위로 탐색해 toplevel 로
|
|
8
|
-
// 올려 이 불일치를 없앤다.
|
|
9
|
-
//
|
|
10
|
-
// git worktree 는 별도 프로젝트가 아니라 같은 프로젝트의 작업 디렉터리다. 따라서
|
|
11
|
-
// linked worktree 안에서 호출하더라도 `git rev-parse --git-common-dir` 로 canonical
|
|
12
|
-
// project root(일반적으로 main checkout)를 찾아 같은 CTO lake 를 공유한다. git 명령을
|
|
13
|
-
// 사용할 수 없으면 기존의 `.git` 상향 탐색으로 fallback 한다.
|
|
14
|
-
|
|
15
1
|
import { execFileSync as defaultExecFileSync } from "node:child_process";
|
|
16
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
existsSync as defaultExistsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
17
10
|
import { basename, dirname, join, parse } from "node:path";
|
|
18
11
|
|
|
19
12
|
const MAX_DEPTH = 64;
|
|
@@ -66,7 +59,7 @@ export function resolveLakeRootDir(cwd, opts = {}) {
|
|
|
66
59
|
const exists = opts?.existsSync || defaultExistsSync;
|
|
67
60
|
const execFileSync = opts?.execFileSync || defaultExecFileSync;
|
|
68
61
|
// 비문자열(객체/숫자 등 truthy 포함) 또는 빈 문자열이면 항상 "" 를 반환해
|
|
69
|
-
// @returns {string} 계약을
|
|
62
|
+
// @returns {string} 계약을 지킨다. truthy 비문자열을 그대로 누설하지 않는다.
|
|
70
63
|
if (typeof cwd !== "string" || !cwd) return "";
|
|
71
64
|
|
|
72
65
|
const gitRoot = resolveViaGitCommonDir(cwd, execFileSync);
|
|
@@ -83,3 +76,108 @@ export function resolveLakeRootDir(cwd, opts = {}) {
|
|
|
83
76
|
}
|
|
84
77
|
return cwd;
|
|
85
78
|
}
|
|
79
|
+
|
|
80
|
+
export function firstExisting(paths) {
|
|
81
|
+
return paths.filter(Boolean).find((path) => defaultExistsSync(path)) || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function toIsoTime(value) {
|
|
85
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
86
|
+
if (value instanceof Date) return value.toISOString();
|
|
87
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
88
|
+
return new Date(value).toISOString();
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function shortHash(value) {
|
|
94
|
+
const str = String(value ?? "");
|
|
95
|
+
let h = 5381;
|
|
96
|
+
for (let i = 0; i < str.length; i++) {
|
|
97
|
+
h = (h * 33) ^ str.charCodeAt(i);
|
|
98
|
+
}
|
|
99
|
+
return (h >>> 0).toString(36);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function pathLabel(value) {
|
|
103
|
+
const str = String(value ?? "").replace(/[/\\]+$/u, "");
|
|
104
|
+
if (!str) return "";
|
|
105
|
+
const segments = str.split(/[/\\]+/u);
|
|
106
|
+
return segments[segments.length - 1] || "";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function normalizeLiveSession(session) {
|
|
110
|
+
return {
|
|
111
|
+
sessionId: String(session?.sessionId || session?.session_id || ""),
|
|
112
|
+
agent_id:
|
|
113
|
+
typeof session?.agent_id === "string"
|
|
114
|
+
? session.agent_id
|
|
115
|
+
: typeof session?.agentId === "string"
|
|
116
|
+
? session.agentId
|
|
117
|
+
: null,
|
|
118
|
+
phase:
|
|
119
|
+
typeof session?.phase === "string"
|
|
120
|
+
? session.phase
|
|
121
|
+
: typeof session?.status === "string"
|
|
122
|
+
? session.status
|
|
123
|
+
: "active",
|
|
124
|
+
started_at: toIsoTime(
|
|
125
|
+
session?.started_at ?? session?.startedAt ?? session?.lastHeartbeat,
|
|
126
|
+
),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function readSynapseSnapshot(opts = {}) {
|
|
131
|
+
const rootDir = opts.rootDir || process.cwd();
|
|
132
|
+
const persistPath = firstExisting([
|
|
133
|
+
opts.synapsePersistPath,
|
|
134
|
+
join(rootDir, ".triflux", "synapse-registry.json"),
|
|
135
|
+
join(rootDir, ".triflux", "synapse", "registry.json"),
|
|
136
|
+
join(homedir(), ".claude", "cache", "tfx-hub", "synapse-sessions.json"),
|
|
137
|
+
join(homedir(), ".claude", "cache", "tfx-hub", "synapse-registry.json"),
|
|
138
|
+
]);
|
|
139
|
+
if (!persistPath) return { sessions: [], active_shards: [] };
|
|
140
|
+
const data = JSON.parse(readFileSync(persistPath, "utf8"));
|
|
141
|
+
// 상태 전이는 hub 소유이며 조회는 저장된 live 상태만 읽는다.
|
|
142
|
+
const sessions = Object.entries(data).flatMap(([key, session]) => {
|
|
143
|
+
const sessionId = String(session?.sessionId ?? key).trim();
|
|
144
|
+
if (!sessionId || ["stale", "expired"].includes(session?.status)) return [];
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
...session,
|
|
148
|
+
sessionId,
|
|
149
|
+
status: session?.status === "idle" ? "idle" : "active",
|
|
150
|
+
lastHeartbeat:
|
|
151
|
+
typeof session?.lastHeartbeat === "number"
|
|
152
|
+
? session.lastHeartbeat
|
|
153
|
+
: Date.now(),
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
});
|
|
157
|
+
return { sessions, active_shards: [] };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function readJsonLines(filePath, limit = Infinity) {
|
|
161
|
+
if (!defaultExistsSync(filePath)) return [];
|
|
162
|
+
const lines = readFileSync(filePath, "utf8")
|
|
163
|
+
.split(/\r?\n/u)
|
|
164
|
+
.map((line) => line.trim())
|
|
165
|
+
.filter(Boolean);
|
|
166
|
+
return lines.slice(-limit).flatMap((line) => {
|
|
167
|
+
try {
|
|
168
|
+
return [JSON.parse(line)];
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function writeAtomic(filePath, body) {
|
|
176
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
177
|
+
const tmpPath = join(
|
|
178
|
+
dirname(filePath),
|
|
179
|
+
`.${basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
|
|
180
|
+
);
|
|
181
|
+
writeFileSync(tmpPath, body, "utf8");
|
|
182
|
+
renameSync(tmpPath, filePath);
|
|
183
|
+
}
|