triflux 10.29.0 → 10.30.0

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.
@@ -0,0 +1,846 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import {
3
+ appendFileSync,
4
+ closeSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ openSync,
8
+ readdirSync,
9
+ readFileSync,
10
+ renameSync,
11
+ statSync,
12
+ unlinkSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { basename, dirname, join, relative } from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+
19
+ import { renderBrief } from "./brief.mjs";
20
+
21
+ const SCHEMA_VERSION = "cto-lake.v1";
22
+ const SOURCE_REGISTRY = [
23
+ {
24
+ id: "git",
25
+ kind: "repo",
26
+ probe: "git rev-parse, git branch, git status",
27
+ enabled: true,
28
+ },
29
+ {
30
+ id: "tfx_hub",
31
+ kind: "triflux-runtime-artifact",
32
+ probe: ".triflux/hub/status.json or host tfx-hub cache",
33
+ enabled: true,
34
+ },
35
+ {
36
+ id: "tfx_swarm",
37
+ kind: "triflux-runtime-artifact",
38
+ probe: ".triflux/swarm-locks.json or .triflux/swarm/*.json",
39
+ enabled: true,
40
+ },
41
+ {
42
+ id: "tfx_team",
43
+ kind: "triflux-runtime-artifact",
44
+ probe: ".triflux/team-state.json or host tfx-hub team cache",
45
+ enabled: true,
46
+ },
47
+ {
48
+ id: "tfx_synapse",
49
+ kind: "triflux-runtime-artifact",
50
+ probe: ".triflux/synapse registry or host tfx-hub synapse cache",
51
+ enabled: true,
52
+ },
53
+ {
54
+ id: "ultragoal_omx",
55
+ kind: "durable-goal-artifact",
56
+ probe: ".omx/ultragoal/goals.json",
57
+ enabled: true,
58
+ },
59
+ {
60
+ id: "ultragoal_omc",
61
+ kind: "durable-goal-artifact",
62
+ probe: ".omc/ultragoal/goals.json",
63
+ enabled: true,
64
+ },
65
+ {
66
+ id: "handoffs",
67
+ kind: "durable-handoff-artifact",
68
+ probe: ".omx/handoffs/*",
69
+ enabled: true,
70
+ },
71
+ {
72
+ id: "session_vault",
73
+ kind: "optional-durable-ref",
74
+ probe: ".triflux/session-vault.json, sessions_v2.db, or .session-vault",
75
+ enabled: true,
76
+ },
77
+ {
78
+ id: "agy",
79
+ kind: "optional-durable-ref",
80
+ probe: ".agy/state.json or .agy/refs.json",
81
+ enabled: true,
82
+ },
83
+ {
84
+ id: "gbrain",
85
+ kind: "optional-durable-ref",
86
+ probe: ".gbrain/refs.json or .gbrain/config.json",
87
+ enabled: true,
88
+ },
89
+ ];
90
+ const SOURCE_IDS = SOURCE_REGISTRY.map((source) => source.id);
91
+
92
+ const SCHEMA_PATH = fileURLToPath(
93
+ new URL("./current.schema.json", import.meta.url),
94
+ );
95
+
96
+ function isoNow(opts) {
97
+ const value =
98
+ opts.now instanceof Date
99
+ ? opts.now
100
+ : opts.now
101
+ ? new Date(opts.now)
102
+ : new Date();
103
+ return value.toISOString();
104
+ }
105
+
106
+ function sleep(ms) {
107
+ return new Promise((resolve) => setTimeout(resolve, ms));
108
+ }
109
+
110
+ function sourceState(available, status, detail, collectedAt) {
111
+ return {
112
+ available,
113
+ status,
114
+ detail,
115
+ collected_at: collectedAt,
116
+ };
117
+ }
118
+
119
+ function missingSource(status, detail, collectedAt) {
120
+ return sourceState(false, status, detail, collectedAt);
121
+ }
122
+
123
+ function relPath(rootDir, filePath) {
124
+ const rel = relative(rootDir, filePath).replace(/\\/g, "/");
125
+ return rel && !rel.startsWith("..") ? rel : filePath;
126
+ }
127
+
128
+ function readJson(filePath) {
129
+ return JSON.parse(readFileSync(filePath, "utf8"));
130
+ }
131
+
132
+ function readJsonLines(filePath, limit = 20) {
133
+ if (!existsSync(filePath)) return [];
134
+ const lines = readFileSync(filePath, "utf8")
135
+ .split(/\r?\n/u)
136
+ .map((line) => line.trim())
137
+ .filter(Boolean);
138
+ return lines.slice(-limit).flatMap((line) => {
139
+ try {
140
+ return [JSON.parse(line)];
141
+ } catch {
142
+ return [];
143
+ }
144
+ });
145
+ }
146
+
147
+ function findFirstExisting(candidates) {
148
+ return candidates.find((candidate) => existsSync(candidate)) || null;
149
+ }
150
+
151
+ function listDirFiles(dirPath, rootDir, limit = 10) {
152
+ if (!existsSync(dirPath)) return [];
153
+ try {
154
+ return readdirSync(dirPath, { withFileTypes: true })
155
+ .filter((entry) => entry.isFile())
156
+ .map((entry) => {
157
+ const filePath = join(dirPath, entry.name);
158
+ return {
159
+ path: relPath(rootDir, filePath),
160
+ mtime: statSync(filePath).mtime.toISOString(),
161
+ };
162
+ })
163
+ .sort((a, b) => b.mtime.localeCompare(a.mtime))
164
+ .slice(0, limit);
165
+ } catch {
166
+ return [];
167
+ }
168
+ }
169
+
170
+ function safeSummary(value) {
171
+ if (value === null || value === undefined) return null;
172
+ if (Array.isArray(value)) return { type: "array", count: value.length };
173
+ if (typeof value === "object")
174
+ return { type: "object", keys: Object.keys(value).slice(0, 20) };
175
+ return String(value).slice(0, 200);
176
+ }
177
+
178
+ function execGit(args, rootDir, execFileSyncFn) {
179
+ return execFileSyncFn("git", args, {
180
+ cwd: rootDir,
181
+ encoding: "utf8",
182
+ stdio: ["ignore", "pipe", "pipe"],
183
+ windowsHide: true,
184
+ }).trim();
185
+ }
186
+
187
+ function collectGit(rootDir, collectedAt, execFileSyncFn) {
188
+ try {
189
+ const gitRoot = execGit(
190
+ ["rev-parse", "--show-toplevel"],
191
+ rootDir,
192
+ execFileSyncFn,
193
+ );
194
+ const branch =
195
+ execGit(["branch", "--show-current"], gitRoot, execFileSyncFn) || null;
196
+ const head =
197
+ execGit(["rev-parse", "--short=12", "HEAD"], gitRoot, execFileSyncFn) ||
198
+ null;
199
+ const porcelain = execGit(
200
+ ["status", "--porcelain"],
201
+ gitRoot,
202
+ execFileSyncFn,
203
+ );
204
+ const dirty = porcelain.length > 0;
205
+ const repo = { root: gitRoot, branch, head, dirty };
206
+ return {
207
+ repo,
208
+ source: sourceState(
209
+ true,
210
+ dirty ? "dirty" : "clean",
211
+ {
212
+ branch,
213
+ head,
214
+ dirty,
215
+ changed_entries: porcelain ? porcelain.split(/\r?\n/u).length : 0,
216
+ },
217
+ collectedAt,
218
+ ),
219
+ };
220
+ } catch (error) {
221
+ return {
222
+ repo: { root: rootDir, branch: null, head: null, dirty: false },
223
+ source: missingSource(
224
+ "unavailable",
225
+ `git status unavailable: ${error?.message || "unknown error"}`,
226
+ collectedAt,
227
+ ),
228
+ };
229
+ }
230
+ }
231
+
232
+ function collectJsonArtifact(
233
+ id,
234
+ candidates,
235
+ rootDir,
236
+ collectedAt,
237
+ summarize = safeSummary,
238
+ ) {
239
+ const filePath = findFirstExisting(candidates);
240
+ if (!filePath) {
241
+ return missingSource(
242
+ "no_shell_readable_artifact",
243
+ "no durable artifact found",
244
+ collectedAt,
245
+ );
246
+ }
247
+ try {
248
+ const parsed = readJson(filePath);
249
+ return sourceState(
250
+ true,
251
+ "ok",
252
+ {
253
+ path: relPath(rootDir, filePath),
254
+ summary: summarize(parsed),
255
+ },
256
+ collectedAt,
257
+ );
258
+ } catch (error) {
259
+ return sourceState(
260
+ true,
261
+ "read_error",
262
+ {
263
+ path: relPath(rootDir, filePath),
264
+ error: error?.message || `${id} artifact read failed`,
265
+ },
266
+ collectedAt,
267
+ );
268
+ }
269
+ }
270
+
271
+ function normalizeGoals(parsed) {
272
+ const rawGoals = Array.isArray(parsed?.goals)
273
+ ? parsed.goals
274
+ : Array.isArray(parsed)
275
+ ? parsed
276
+ : [];
277
+ return rawGoals.map((goal) => ({
278
+ id: goal?.id || null,
279
+ title: goal?.title || goal?.objective || goal?.summary || null,
280
+ status: goal?.status || "unknown",
281
+ updated_at: goal?.updatedAt || goal?.updated_at || null,
282
+ }));
283
+ }
284
+
285
+ function collectUltragoal(dirPath, rootDir, collectedAt) {
286
+ const goalsPath = join(dirPath, "goals.json");
287
+ const ledgerPath = join(dirPath, "ledger.jsonl");
288
+ const briefPath = join(dirPath, "brief.md");
289
+ if (
290
+ !existsSync(goalsPath) &&
291
+ !existsSync(ledgerPath) &&
292
+ !existsSync(briefPath)
293
+ ) {
294
+ return missingSource(
295
+ "no_shell_readable_artifact",
296
+ "no ultragoal files found",
297
+ collectedAt,
298
+ );
299
+ }
300
+
301
+ const detail = {
302
+ paths: {},
303
+ goals_count: 0,
304
+ active_goals: [],
305
+ ledger_tail: [],
306
+ brief: null,
307
+ };
308
+
309
+ try {
310
+ if (existsSync(goalsPath)) {
311
+ detail.paths.goals = relPath(rootDir, goalsPath);
312
+ const parsed = readJson(goalsPath);
313
+ const goals = normalizeGoals(parsed);
314
+ detail.goals_count = goals.length;
315
+ detail.active_goals = goals
316
+ .filter(
317
+ (goal) =>
318
+ !["complete", "completed", "done"].includes(
319
+ String(goal.status).toLowerCase(),
320
+ ),
321
+ )
322
+ .slice(0, 5);
323
+ }
324
+ if (existsSync(ledgerPath)) {
325
+ detail.paths.ledger = relPath(rootDir, ledgerPath);
326
+ detail.ledger_tail = readJsonLines(ledgerPath, 5);
327
+ }
328
+ if (existsSync(briefPath)) {
329
+ detail.paths.brief = relPath(rootDir, briefPath);
330
+ detail.brief = readFileSync(briefPath, "utf8")
331
+ .replace(/\s+/g, " ")
332
+ .trim()
333
+ .slice(0, 300);
334
+ }
335
+ return sourceState(true, "ok", detail, collectedAt);
336
+ } catch (error) {
337
+ return sourceState(
338
+ true,
339
+ "read_error",
340
+ {
341
+ path: relPath(rootDir, dirPath),
342
+ error: error?.message || "ultragoal read failed",
343
+ },
344
+ collectedAt,
345
+ );
346
+ }
347
+ }
348
+
349
+ export function collectSwarm(rootDir, collectedAt) {
350
+ const locksPath = join(rootDir, ".triflux", "swarm-locks.json");
351
+ const logsDir = join(rootDir, ".triflux", "swarm-logs");
352
+ if (!existsSync(locksPath) && !existsSync(logsDir)) {
353
+ return missingSource(
354
+ "no_shell_readable_artifact",
355
+ "no swarm locks or logs found",
356
+ collectedAt,
357
+ );
358
+ }
359
+ try {
360
+ const detail = { locks_path: null, locks: null, shards: [], log_runs: [] };
361
+ if (existsSync(locksPath)) {
362
+ const locks = readJson(locksPath);
363
+ detail.locks_path = relPath(rootDir, locksPath);
364
+ detail.locks = safeSummary(locks);
365
+ if (Array.isArray(locks?.shards)) detail.shards = locks.shards;
366
+ else if (Array.isArray(locks)) detail.shards = locks;
367
+ else if (locks && typeof locks === "object") {
368
+ // 실제 producer(hub/team/swarm-locks.mjs persist)는
369
+ // { "<file>": { workerId, leaseType, sessionMeta } } 형태의 lock map 을 쓴다.
370
+ // workerId 가 곧 shard.name 이다 — swarm-hypervisor 가
371
+ // acquire(shard.name, shard.files) 로 잠그기 때문(swarm-hypervisor.mjs:753).
372
+ // workerId 별로 묶어 active shard row 로 환원한다. (expired lock 정밀 필터는
373
+ // hub snapshot() 의 책임이라, 여기서는 persist 된 보유 lock 전부를 active 로 본다.)
374
+ const byWorker = new Map();
375
+ for (const entry of Object.values(locks)) {
376
+ const workerId =
377
+ entry && typeof entry.workerId === "string" ? entry.workerId : null;
378
+ if (!workerId) continue;
379
+ let row = byWorker.get(workerId);
380
+ if (!row) {
381
+ row = { shard_name: workerId, phase: "active", members: [] };
382
+ byWorker.set(workerId, row);
383
+ }
384
+ const host = entry?.sessionMeta?.host;
385
+ if (typeof host === "string" && host && !row.members.includes(host)) {
386
+ row.members.push(host);
387
+ }
388
+ }
389
+ detail.shards = [...byWorker.values()];
390
+ }
391
+ }
392
+ if (existsSync(logsDir)) {
393
+ detail.log_runs = readdirSync(logsDir, { withFileTypes: true })
394
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith("run-"))
395
+ .map((entry) => {
396
+ const path = join(logsDir, entry.name);
397
+ return { id: entry.name, mtime: statSync(path).mtime.toISOString() };
398
+ })
399
+ .sort((a, b) => b.mtime.localeCompare(a.mtime))
400
+ .slice(0, 5);
401
+ }
402
+ return sourceState(true, "ok", detail, collectedAt);
403
+ } catch (error) {
404
+ return sourceState(
405
+ true,
406
+ "read_error",
407
+ error?.message || "swarm read failed",
408
+ collectedAt,
409
+ );
410
+ }
411
+ }
412
+
413
+ function collectHandoffs(rootDir, collectedAt) {
414
+ const dirPath = join(rootDir, ".omx", "handoffs");
415
+ const files = listDirFiles(dirPath, rootDir, 10);
416
+ if (files.length === 0) {
417
+ return missingSource(
418
+ "no_shell_readable_artifact",
419
+ "no .omx/handoffs files found",
420
+ collectedAt,
421
+ );
422
+ }
423
+ return sourceState(
424
+ true,
425
+ "ok",
426
+ { path: relPath(rootDir, dirPath), files },
427
+ collectedAt,
428
+ );
429
+ }
430
+
431
+ function collectDurableRefs(
432
+ id,
433
+ candidates,
434
+ rootDir,
435
+ collectedAt,
436
+ unavailableStatus = "no_shell_readable_artifact",
437
+ ) {
438
+ const filePath = findFirstExisting(candidates);
439
+ if (!filePath) {
440
+ return missingSource(
441
+ unavailableStatus,
442
+ `${id} exposes no shell-readable durable artifact`,
443
+ collectedAt,
444
+ );
445
+ }
446
+ try {
447
+ const stat = statSync(filePath);
448
+ if (stat.isDirectory()) {
449
+ return sourceState(
450
+ true,
451
+ "ok",
452
+ {
453
+ path: relPath(rootDir, filePath),
454
+ files: listDirFiles(filePath, rootDir, 10),
455
+ },
456
+ collectedAt,
457
+ );
458
+ }
459
+ const parsed = basename(filePath).endsWith(".json")
460
+ ? readJson(filePath)
461
+ : readFileSync(filePath, "utf8");
462
+ return sourceState(
463
+ true,
464
+ "ok",
465
+ {
466
+ path: relPath(rootDir, filePath),
467
+ summary: safeSummary(parsed),
468
+ },
469
+ collectedAt,
470
+ );
471
+ } catch (error) {
472
+ return sourceState(
473
+ true,
474
+ "read_error",
475
+ {
476
+ path: relPath(rootDir, filePath),
477
+ error: error?.message || `${id} read failed`,
478
+ },
479
+ collectedAt,
480
+ );
481
+ }
482
+ }
483
+
484
+ function buildSummary(current) {
485
+ const repo = current.repo;
486
+ const omxGoals = current.sources.ultragoal_omx.detail?.active_goals || [];
487
+ const omcGoals = current.sources.ultragoal_omc.detail?.active_goals || [];
488
+ const swarmShards = current.sources.tfx_swarm.detail?.shards || [];
489
+ const availableSources = Object.values(current.sources).filter(
490
+ (source) => source.available,
491
+ ).length;
492
+
493
+ return {
494
+ repo_state: `branch ${repo.branch || "unknown"} at ${repo.head || "unknown"} is ${repo.dirty ? "dirty" : "clean"}`,
495
+ active_goals: [...omxGoals, ...omcGoals].slice(0, 5),
496
+ swarm_shards: Array.isArray(swarmShards) ? swarmShards.slice(0, 5) : [],
497
+ hub_status: current.sources.tfx_hub.status,
498
+ available_sources: availableSources,
499
+ missing_sources: SOURCE_IDS.length - availableSources,
500
+ };
501
+ }
502
+
503
+ function isPlainObject(value) {
504
+ return value !== null && typeof value === "object" && !Array.isArray(value);
505
+ }
506
+
507
+ function assertPlainObject(name, value) {
508
+ if (!isPlainObject(value)) throw new Error(`${name} must be object`);
509
+ }
510
+
511
+ function assertOnlyKeys(name, value, allowedKeys) {
512
+ assertPlainObject(name, value);
513
+ const allowed = new Set(allowedKeys);
514
+ for (const key of Object.keys(value)) {
515
+ if (!allowed.has(key)) throw new Error(`${name} has unexpected ${key}`);
516
+ }
517
+ }
518
+
519
+ function assertString(name, value) {
520
+ if (typeof value !== "string") throw new Error(`${name} must be string`);
521
+ }
522
+
523
+ function assertStringOrNull(name, value) {
524
+ if (typeof value !== "string" && value !== null) {
525
+ throw new Error(`${name} must be string or null`);
526
+ }
527
+ }
528
+
529
+ function assertDateTimeStringOrNull(name, value) {
530
+ assertStringOrNull(name, value);
531
+ if (value !== null && Number.isNaN(Date.parse(value))) {
532
+ throw new Error(`${name} must be date-time`);
533
+ }
534
+ }
535
+
536
+ function assertBoolean(name, value) {
537
+ if (typeof value !== "boolean") throw new Error(`${name} must be boolean`);
538
+ }
539
+
540
+ function validateLedgerEntry(entry, prefix = "ledger_tail entry") {
541
+ assertOnlyKeys(prefix, entry, ["ts", "event", "source", "summary", "ref"]);
542
+ assertDateTimeStringOrNull(`${prefix}.ts`, entry.ts);
543
+ if (entry.ts === null) throw new Error(`${prefix}.ts must be string`);
544
+ assertString(`${prefix}.event`, entry.event);
545
+ assertString(`${prefix}.source`, entry.source);
546
+ assertString(`${prefix}.summary`, entry.summary);
547
+ if (
548
+ entry.ref !== null &&
549
+ typeof entry.ref !== "string" &&
550
+ !isPlainObject(entry.ref)
551
+ ) {
552
+ throw new Error(`${prefix}.ref must be string, object, or null`);
553
+ }
554
+ }
555
+
556
+ function isValidLedgerEntry(entry) {
557
+ try {
558
+ validateLedgerEntry(entry);
559
+ return true;
560
+ } catch {
561
+ return false;
562
+ }
563
+ }
564
+
565
+ function readLedgerTail(lakeRoot, limit = 5) {
566
+ return readJsonLines(join(lakeRoot, "ledger.jsonl"), limit * 4)
567
+ .filter(isValidLedgerEntry)
568
+ .slice(-limit);
569
+ }
570
+
571
+ function validateSourceState(sourceId, source) {
572
+ const prefix = `current.json source ${sourceId}`;
573
+ assertOnlyKeys(prefix, source, [
574
+ "available",
575
+ "status",
576
+ "detail",
577
+ "collected_at",
578
+ ]);
579
+ assertBoolean(`${prefix}.available`, source.available);
580
+ assertString(`${prefix}.status`, source.status);
581
+ if (
582
+ source.detail !== null &&
583
+ typeof source.detail !== "string" &&
584
+ typeof source.detail !== "object"
585
+ ) {
586
+ throw new Error(`${prefix}.detail must be string, object, array, or null`);
587
+ }
588
+ assertDateTimeStringOrNull(`${prefix}.collected_at`, source.collected_at);
589
+ }
590
+
591
+ function validateCurrent(current) {
592
+ const schema = readJson(SCHEMA_PATH);
593
+ assertOnlyKeys("current.json", current, schema.required);
594
+ for (const field of schema.required) {
595
+ if (!Object.hasOwn(current, field))
596
+ throw new Error(`current.json missing ${field}`);
597
+ }
598
+ if (current.schema_version !== SCHEMA_VERSION) {
599
+ throw new Error(`current.json schema_version must be ${SCHEMA_VERSION}`);
600
+ }
601
+ assertDateTimeStringOrNull("current.json.generated_at", current.generated_at);
602
+ if (current.generated_at === null)
603
+ throw new Error("current.json.generated_at must be string");
604
+
605
+ assertOnlyKeys("current.json.repo", current.repo, [
606
+ "root",
607
+ "branch",
608
+ "head",
609
+ "dirty",
610
+ ]);
611
+ assertString("current.json.repo.root", current.repo.root);
612
+ assertStringOrNull("current.json.repo.branch", current.repo.branch);
613
+ assertStringOrNull("current.json.repo.head", current.repo.head);
614
+ assertBoolean("current.json.repo.dirty", current.repo.dirty);
615
+
616
+ assertOnlyKeys(
617
+ "current.json.sources",
618
+ current.sources,
619
+ schema.properties.sources.required,
620
+ );
621
+ for (const sourceId of schema.properties.sources.required) {
622
+ const source = current.sources?.[sourceId];
623
+ if (!source) throw new Error(`current.json missing source ${sourceId}`);
624
+ validateSourceState(sourceId, source);
625
+ }
626
+
627
+ assertPlainObject("current.json.summary", current.summary);
628
+ if (!Array.isArray(current.ledger_tail))
629
+ throw new Error("current.json ledger_tail must be array");
630
+ for (const [index, entry] of current.ledger_tail.entries()) {
631
+ validateLedgerEntry(entry, `ledger_tail[${index}]`);
632
+ }
633
+ }
634
+
635
+ function writeAtomic(filePath, body) {
636
+ mkdirSync(dirname(filePath), { recursive: true });
637
+ const tmpPath = join(
638
+ dirname(filePath),
639
+ `.${basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
640
+ );
641
+ writeFileSync(tmpPath, body, "utf8");
642
+ renameSync(tmpPath, filePath);
643
+ }
644
+
645
+ async function appendLedgerEvent(lakeRoot, event, stderr) {
646
+ mkdirSync(lakeRoot, { recursive: true });
647
+ const ledgerPath = join(lakeRoot, "ledger.jsonl");
648
+ const lockPath = join(lakeRoot, "ledger.jsonl.lock");
649
+ let fd = null;
650
+ for (let attempt = 0; attempt < 3; attempt += 1) {
651
+ try {
652
+ fd = openSync(lockPath, "wx", 0o600);
653
+ break;
654
+ } catch (error) {
655
+ if (error?.code !== "EEXIST") throw error;
656
+ if (attempt < 2) await sleep(100);
657
+ }
658
+ }
659
+ if (fd === null) {
660
+ stderr.write(
661
+ "[tfx cto collect] warning: ledger lock timeout; skipped ledger append\n",
662
+ );
663
+ return false;
664
+ }
665
+
666
+ try {
667
+ appendFileSync(ledgerPath, `${JSON.stringify(event)}\n`, "utf8");
668
+ return true;
669
+ } finally {
670
+ closeSync(fd);
671
+ try {
672
+ unlinkSync(lockPath);
673
+ } catch {}
674
+ }
675
+ }
676
+
677
+ function collectSources(rootDir, collectedAt, execFileSyncFn, opts = {}) {
678
+ const home = homedir();
679
+ const hostArtifactCandidates =
680
+ opts.includeHostArtifacts === false
681
+ ? {
682
+ hub: [],
683
+ team: [],
684
+ synapse: [],
685
+ }
686
+ : {
687
+ hub: [
688
+ join(home, ".claude", "cache", "tfx-hub", "hub.pid"),
689
+ join(home, ".claude", "cache", "tfx-hub", "hub-state.json"),
690
+ ],
691
+ team: [
692
+ join(
693
+ home,
694
+ ".claude",
695
+ "cache",
696
+ "tfx-hub",
697
+ `team-state-${process.env.CLAUDE_SESSION_ID || ""}.json`,
698
+ ),
699
+ join(home, ".claude", "cache", "tfx-hub", "team-state.json"),
700
+ ],
701
+ synapse: [
702
+ join(home, ".claude", "cache", "tfx-hub", "synapse-registry.json"),
703
+ join(home, ".claude", "cache", "tfx-hub", "synapse-sessions.json"),
704
+ ],
705
+ };
706
+ const git = collectGit(rootDir, collectedAt, execFileSyncFn);
707
+ const sources = {
708
+ git: git.source,
709
+ tfx_hub: collectJsonArtifact(
710
+ "tfx_hub",
711
+ [
712
+ join(rootDir, ".triflux", "hub", "status.json"),
713
+ join(rootDir, ".triflux", "hub-state.json"),
714
+ ...hostArtifactCandidates.hub,
715
+ ],
716
+ rootDir,
717
+ collectedAt,
718
+ ),
719
+ tfx_swarm: collectSwarm(rootDir, collectedAt),
720
+ tfx_team: collectJsonArtifact(
721
+ "tfx_team",
722
+ [
723
+ join(rootDir, ".triflux", "team-state.json"),
724
+ ...hostArtifactCandidates.team,
725
+ ],
726
+ rootDir,
727
+ collectedAt,
728
+ ),
729
+ tfx_synapse: collectJsonArtifact(
730
+ "tfx_synapse",
731
+ [
732
+ join(rootDir, ".triflux", "synapse-registry.json"),
733
+ join(rootDir, ".triflux", "synapse", "registry.json"),
734
+ ...hostArtifactCandidates.synapse,
735
+ ],
736
+ rootDir,
737
+ collectedAt,
738
+ ),
739
+ ultragoal_omx: collectUltragoal(
740
+ join(rootDir, ".omx", "ultragoal"),
741
+ rootDir,
742
+ collectedAt,
743
+ ),
744
+ ultragoal_omc: collectUltragoal(
745
+ join(rootDir, ".omc", "ultragoal"),
746
+ rootDir,
747
+ collectedAt,
748
+ ),
749
+ handoffs: collectHandoffs(rootDir, collectedAt),
750
+ session_vault: collectDurableRefs(
751
+ "session_vault",
752
+ [
753
+ join(rootDir, ".triflux", "session-vault.json"),
754
+ join(rootDir, "sessions_v2.db"),
755
+ join(rootDir, ".session-vault"),
756
+ ],
757
+ rootDir,
758
+ collectedAt,
759
+ "not_shell_collectable",
760
+ ),
761
+ agy: collectDurableRefs(
762
+ "agy",
763
+ [join(rootDir, ".agy", "state.json"), join(rootDir, ".agy", "refs.json")],
764
+ rootDir,
765
+ collectedAt,
766
+ "not_shell_collectable",
767
+ ),
768
+ gbrain: collectDurableRefs(
769
+ "gbrain",
770
+ [
771
+ join(rootDir, ".gbrain", "refs.json"),
772
+ join(rootDir, ".gbrain", "config.json"),
773
+ ],
774
+ rootDir,
775
+ collectedAt,
776
+ "not_shell_collectable",
777
+ ),
778
+ };
779
+
780
+ return { repo: git.repo, sources };
781
+ }
782
+
783
+ function hasFlag(args, flag) {
784
+ return Array.isArray(args) && args.includes(flag);
785
+ }
786
+
787
+ export async function runCollect(args = [], opts = {}) {
788
+ const rootDir = opts.rootDir || process.cwd();
789
+ const lakeRoot = opts.lakeRoot || join(rootDir, ".triflux", "lake");
790
+ const stdout = opts.stdout || process.stdout;
791
+ const stderr = opts.stderr || process.stderr;
792
+ const execFileSyncFn = opts.execFileSyncFn || execFileSync;
793
+ const generatedAt = isoNow(opts);
794
+
795
+ mkdirSync(lakeRoot, { recursive: true });
796
+
797
+ const { repo, sources } = collectSources(
798
+ rootDir,
799
+ generatedAt,
800
+ execFileSyncFn,
801
+ opts,
802
+ );
803
+ const current = {
804
+ schema_version: SCHEMA_VERSION,
805
+ generated_at: generatedAt,
806
+ repo,
807
+ sources,
808
+ summary: {},
809
+ ledger_tail: readLedgerTail(lakeRoot),
810
+ };
811
+ current.summary = buildSummary(current);
812
+
813
+ const event = {
814
+ ts: generatedAt,
815
+ event: "collect",
816
+ source: "tfx_cto_collect",
817
+ summary: `collected ${current.summary.available_sources}/${SOURCE_IDS.length} durable sources`,
818
+ ref: {
819
+ current_json: "current.json",
820
+ current_md: "current.md",
821
+ sources_json: "sources.json",
822
+ },
823
+ };
824
+ const appended = await appendLedgerEvent(lakeRoot, event, stderr);
825
+ if (appended) current.ledger_tail = readLedgerTail(lakeRoot);
826
+
827
+ validateCurrent(current);
828
+ writeAtomic(
829
+ join(lakeRoot, "current.json"),
830
+ `${JSON.stringify(current, null, 2)}\n`,
831
+ );
832
+ writeAtomic(join(lakeRoot, "current.md"), renderBrief(current));
833
+ writeAtomic(
834
+ join(lakeRoot, "sources.json"),
835
+ `${JSON.stringify(SOURCE_REGISTRY, null, 2)}
836
+ `,
837
+ );
838
+
839
+ if (opts.json === true || hasFlag(args, "--json")) {
840
+ stdout.write(`${JSON.stringify(current, null, 2)}\n`);
841
+ } else {
842
+ stdout.write(`wrote ${join(lakeRoot, "current.json")} and current.md\n`);
843
+ }
844
+
845
+ return current;
846
+ }