@triflux/remote 10.53.0 → 10.55.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.
- package/hub/team/backend.mjs +10 -3
- package/hub/team/cli/commands/start/index.mjs +0 -2
- package/hub/team/handoff.mjs +1 -1
- package/hub/team/headless.mjs +41 -57
- package/hub/team/intervention.mjs +6 -4
- package/hub/team/nativeProxy.mjs +2 -48
- package/hub/team/pane.mjs +31 -18
- package/hub/team/psmux.mjs +36 -20
- package/hub/team/remote-session.mjs +66 -56
- package/hub/team/retry-state-machine.mjs +1 -1
- package/hub/team/session.mjs +80 -59
- package/hub/team/terminal-opener.mjs +5 -19
- package/hub/team/tui-viewer.mjs +2 -2
- package/hub/team/tui.mjs +0 -6
- package/hub/workers/factory.mjs +1 -10
- package/hub/workers/interface.mjs +0 -7
- package/package.json +2 -6
- package/scripts/lib/backup-rotation.mjs +60 -0
- package/scripts/lib/codex-profile-config.mjs +20 -7
- package/scripts/lib/codex-recovery.sh +2 -3
- package/scripts/lib/install-blob-history.json +3196 -0
- package/scripts/lib/install-retire.mjs +203 -0
- package/scripts/lib/legacy-hook-cleanup.mjs +148 -13
- package/scripts/lib/legacy-mcp-cleanup.mjs +405 -104
- package/scripts/lib/machine-profile.mjs +2 -2
- package/scripts/lib/mcp-filter.mjs +1 -42
- package/scripts/lib/mcp-guard-engine.mjs +198 -19
- package/scripts/lib/mcp-manifest.mjs +0 -40
- package/scripts/lib/process-utils.mjs +19 -0
- package/scripts/lib/stable-node.mjs +35 -0
- package/scripts/lib/test-env.mjs +20 -0
- package/cto/brief.mjs +0 -73
- package/cto/collect.mjs +0 -490
- package/cto/current.schema.json +0 -148
- package/cto/events.mjs +0 -208
- package/cto/hygiene.mjs +0 -320
- package/cto/lake-root.mjs +0 -127
- package/cto/status.mjs +0 -124
- package/hub/lib/cto-env.mjs +0 -1
- package/hub/team/notify.mjs +0 -385
- package/hub/team/process-cleanup.mjs +0 -386
- package/hub/team/routing.mjs +0 -249
- package/hub/team/staleState.mjs +0 -403
- package/hub/team/worker-signal.mjs +0 -263
- package/hub/team/worker-signal.types.d.ts +0 -52
- package/hub/workers/codex-mcp.mjs +0 -853
- package/scripts/lib/remote-spawn-transfer.mjs +0 -226
package/cto/events.mjs
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
appendFileSync,
|
|
3
|
-
closeSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
openSync,
|
|
6
|
-
unlinkSync,
|
|
7
|
-
} from "node:fs";
|
|
8
|
-
import { basename, join } from "node:path";
|
|
9
|
-
import { pathLabel, shortHash, toIsoTime } from "./lake-root.mjs";
|
|
10
|
-
|
|
11
|
-
export const CTO_EVENT_SCHEMA_VERSION = "cto-event.v1";
|
|
12
|
-
export const DEFAULT_CTO_EVENT_SOURCE = "tfx_cto_event";
|
|
13
|
-
|
|
14
|
-
export const CTO_EVENT_TYPES = Object.freeze([
|
|
15
|
-
"collect",
|
|
16
|
-
"session_started",
|
|
17
|
-
"session_heartbeat",
|
|
18
|
-
"session_stale",
|
|
19
|
-
"checkpoint_saved",
|
|
20
|
-
"checkpoint_restored",
|
|
21
|
-
"task_claimed",
|
|
22
|
-
"task_completed",
|
|
23
|
-
"pr_created",
|
|
24
|
-
"pr_merged_or_closed",
|
|
25
|
-
"worktree_created",
|
|
26
|
-
"worktree_removed",
|
|
27
|
-
"hygiene_applied",
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
export const CTO_HYGIENE_STATUSES = Object.freeze([
|
|
31
|
-
"active",
|
|
32
|
-
"idle",
|
|
33
|
-
"stale",
|
|
34
|
-
"superseded",
|
|
35
|
-
"orphaned",
|
|
36
|
-
"completed",
|
|
37
|
-
"blocked",
|
|
38
|
-
"unknown_owner",
|
|
39
|
-
"hidden",
|
|
40
|
-
"needs_attention",
|
|
41
|
-
]);
|
|
42
|
-
|
|
43
|
-
const EVENT_TYPE_SET = new Set(CTO_EVENT_TYPES);
|
|
44
|
-
const STATUS_SET = new Set(CTO_HYGIENE_STATUSES);
|
|
45
|
-
|
|
46
|
-
function sleep(ms) {
|
|
47
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function maybeString(value) {
|
|
51
|
-
if (typeof value !== "string") return null;
|
|
52
|
-
const trimmed = value.trim();
|
|
53
|
-
return trimmed || null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function normalizeNumericRefs(values) {
|
|
57
|
-
const rawValues = Array.isArray(values)
|
|
58
|
-
? values
|
|
59
|
-
: values == null
|
|
60
|
-
? []
|
|
61
|
-
: [values];
|
|
62
|
-
const refs = [];
|
|
63
|
-
for (const value of rawValues) {
|
|
64
|
-
const parsed =
|
|
65
|
-
typeof value === "number" && Number.isInteger(value)
|
|
66
|
-
? value
|
|
67
|
-
: typeof value === "string"
|
|
68
|
-
? Number(value.trim().replace(/^#/u, ""))
|
|
69
|
-
: NaN;
|
|
70
|
-
if (Number.isInteger(parsed) && parsed > 0 && !refs.includes(parsed)) {
|
|
71
|
-
refs.push(parsed);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return refs;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function normalizeActor(actor) {
|
|
78
|
-
if (!actor || typeof actor !== "object" || Array.isArray(actor)) return null;
|
|
79
|
-
const normalized = {};
|
|
80
|
-
for (const key of ["cli", "session_id", "agent_id", "host"]) {
|
|
81
|
-
const value = maybeString(actor[key]);
|
|
82
|
-
if (value) normalized[key] = value;
|
|
83
|
-
}
|
|
84
|
-
return Object.keys(normalized).length > 0 ? normalized : null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function putString(target, key, value) {
|
|
88
|
-
const normalized = maybeString(value);
|
|
89
|
-
if (normalized) target[key] = normalized;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function defaultSummary(eventType, ref) {
|
|
93
|
-
if (ref.task_id) return `${eventType} ${ref.task_id}`;
|
|
94
|
-
if (ref.checkpoint_id) return `${eventType} ${ref.checkpoint_id}`;
|
|
95
|
-
if (ref.session_id) return `${eventType} ${ref.session_id}`;
|
|
96
|
-
return eventType;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function normalizeCtoEvent(input = {}) {
|
|
100
|
-
const eventType = maybeString(input.event ?? input.event_type);
|
|
101
|
-
if (!eventType || !EVENT_TYPE_SET.has(eventType)) {
|
|
102
|
-
throw new Error(`unknown CTO event: ${eventType || "<empty>"}`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const status = maybeString(input.status);
|
|
106
|
-
if (status && !STATUS_SET.has(status)) {
|
|
107
|
-
throw new Error(`invalid CTO status: ${status}`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const ts = toIsoTime(input.now ?? input.ts) || new Date().toISOString();
|
|
111
|
-
if (eventType === "collect") {
|
|
112
|
-
return {
|
|
113
|
-
ts,
|
|
114
|
-
event: eventType,
|
|
115
|
-
source: maybeString(input.source) || "tfx_cto_collect",
|
|
116
|
-
summary: maybeString(input.summary) || eventType,
|
|
117
|
-
ref: input.ref ?? null,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
const ref = {
|
|
121
|
-
schema_version: CTO_EVENT_SCHEMA_VERSION,
|
|
122
|
-
event_type: eventType,
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
putString(ref, "session_id", input.session_id);
|
|
126
|
-
putString(ref, "parent_session_id", input.parent_session_id);
|
|
127
|
-
putString(ref, "restored_from_session_id", input.restored_from_session_id);
|
|
128
|
-
putString(ref, "checkpoint_id", input.checkpoint_id);
|
|
129
|
-
putString(ref, "artifact_path", input.artifact_path);
|
|
130
|
-
putString(ref, "task_id", input.task_id);
|
|
131
|
-
putString(ref, "branch", input.branch);
|
|
132
|
-
putString(ref, "last_seen_at", input.last_seen_at);
|
|
133
|
-
putString(ref, "stale_reason", input.stale_reason);
|
|
134
|
-
putString(ref, "hygiene_key", input.hygiene_key);
|
|
135
|
-
putString(ref, "hygiene_kind", input.hygiene_kind);
|
|
136
|
-
putString(ref, "hygiene_id", input.hygiene_id);
|
|
137
|
-
putString(ref, "hygiene_action", input.hygiene_action);
|
|
138
|
-
if (status) ref.status = status;
|
|
139
|
-
|
|
140
|
-
const projectRoot = maybeString(input.project_root);
|
|
141
|
-
if (projectRoot) {
|
|
142
|
-
ref.project_root_hash = shortHash(projectRoot);
|
|
143
|
-
ref.project_root_label = pathLabel(projectRoot) || basename(projectRoot);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const worktreePath = maybeString(input.worktree_path ?? input.worktreePath);
|
|
147
|
-
if (worktreePath) {
|
|
148
|
-
ref.worktree_path_hash = shortHash(worktreePath);
|
|
149
|
-
ref.worktree_label = pathLabel(worktreePath) || basename(worktreePath);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const issueRefs = normalizeNumericRefs(input.issue_refs ?? input.issueRefs);
|
|
153
|
-
if (issueRefs.length > 0) ref.issue_refs = issueRefs;
|
|
154
|
-
const prRefs = normalizeNumericRefs(input.pr_refs ?? input.prRefs);
|
|
155
|
-
if (prRefs.length > 0) ref.pr_refs = prRefs;
|
|
156
|
-
|
|
157
|
-
const actor = normalizeActor(input.actor);
|
|
158
|
-
if (actor) ref.actor = actor;
|
|
159
|
-
|
|
160
|
-
return {
|
|
161
|
-
ts,
|
|
162
|
-
event: eventType,
|
|
163
|
-
source: maybeString(input.source) || DEFAULT_CTO_EVENT_SOURCE,
|
|
164
|
-
summary: maybeString(input.summary) || defaultSummary(eventType, ref),
|
|
165
|
-
ref,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export async function appendCtoEvent(lakeRoot, input, opts = {}) {
|
|
170
|
-
const event = normalizeCtoEvent(input);
|
|
171
|
-
const stderr = opts.stderr || process.stderr;
|
|
172
|
-
const lockRetries = Number.isInteger(opts.lockRetries) ? opts.lockRetries : 3;
|
|
173
|
-
const lockRetryDelayMs = Number.isFinite(opts.lockRetryDelayMs)
|
|
174
|
-
? opts.lockRetryDelayMs
|
|
175
|
-
: 100;
|
|
176
|
-
|
|
177
|
-
mkdirSync(lakeRoot, { recursive: true });
|
|
178
|
-
const ledgerPath = join(lakeRoot, "ledger.jsonl");
|
|
179
|
-
const lockPath = join(lakeRoot, "ledger.jsonl.lock");
|
|
180
|
-
let fd = null;
|
|
181
|
-
|
|
182
|
-
for (let attempt = 0; attempt < lockRetries; attempt += 1) {
|
|
183
|
-
try {
|
|
184
|
-
fd = openSync(lockPath, "wx", 0o600);
|
|
185
|
-
break;
|
|
186
|
-
} catch (error) {
|
|
187
|
-
if (error?.code !== "EEXIST") throw error;
|
|
188
|
-
if (attempt < lockRetries - 1) await sleep(lockRetryDelayMs);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (fd === null) {
|
|
193
|
-
stderr.write(
|
|
194
|
-
"[tfx cto] warning: ledger lock timeout; skipped ledger append\n",
|
|
195
|
-
);
|
|
196
|
-
return { appended: false, event };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
try {
|
|
200
|
-
appendFileSync(ledgerPath, `${JSON.stringify(event)}\n`, "utf8");
|
|
201
|
-
return { appended: true, event };
|
|
202
|
-
} finally {
|
|
203
|
-
closeSync(fd);
|
|
204
|
-
try {
|
|
205
|
-
unlinkSync(lockPath);
|
|
206
|
-
} catch {}
|
|
207
|
-
}
|
|
208
|
-
}
|
package/cto/hygiene.mjs
DELETED
|
@@ -1,320 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { readJsonLines, resolveLakeRootDir } from "./lake-root.mjs";
|
|
4
|
-
|
|
5
|
-
const COUNT_KEYS = [
|
|
6
|
-
"active_tasks",
|
|
7
|
-
"completed_tasks",
|
|
8
|
-
"stale_sessions",
|
|
9
|
-
"orphan_worktrees",
|
|
10
|
-
"superseded_checkpoints",
|
|
11
|
-
"unknown_owner",
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
function hasFlag(args, flag) {
|
|
15
|
-
return Array.isArray(args) && args.includes(flag);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function writeJson(stdout, payload) {
|
|
19
|
-
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function readJson(filePath) {
|
|
23
|
-
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function eventTime(entry) {
|
|
27
|
-
return typeof entry?.ts === "string" ? entry.ts : "";
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function eventRef(entry) {
|
|
31
|
-
return entry?.ref &&
|
|
32
|
-
typeof entry.ref === "object" &&
|
|
33
|
-
!Array.isArray(entry.ref)
|
|
34
|
-
? entry.ref
|
|
35
|
-
: {};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function hasKnownOwner(ref) {
|
|
39
|
-
const actor = ref?.actor;
|
|
40
|
-
if (actor && typeof actor === "object" && !Array.isArray(actor)) {
|
|
41
|
-
for (const key of ["cli", "session_id", "agent_id", "host"]) {
|
|
42
|
-
if (typeof actor[key] === "string" && actor[key].trim()) return true;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
for (const key of ["owner", "owner_id", "agent_id"]) {
|
|
46
|
-
if (typeof ref?.[key] === "string" && ref[key].trim()) return true;
|
|
47
|
-
}
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function statusOf(ref, fallback) {
|
|
52
|
-
return typeof ref?.status === "string" && ref.status.trim()
|
|
53
|
-
? ref.status.trim()
|
|
54
|
-
: fallback;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function rowFrom(kind, id, status, entry, ref, action) {
|
|
58
|
-
const row = {
|
|
59
|
-
kind,
|
|
60
|
-
id,
|
|
61
|
-
status,
|
|
62
|
-
last_event_at: eventTime(entry) || null,
|
|
63
|
-
};
|
|
64
|
-
if (entry?.summary) row.summary = String(entry.summary);
|
|
65
|
-
if (action) row.action = action;
|
|
66
|
-
if (action && !hasKnownOwner(ref)) row.owner = "unknown";
|
|
67
|
-
if (typeof ref?.artifact_path === "string" && ref.artifact_path.trim()) {
|
|
68
|
-
row.artifact_path = ref.artifact_path.trim();
|
|
69
|
-
}
|
|
70
|
-
if (
|
|
71
|
-
typeof ref?.project_root_hash === "string" &&
|
|
72
|
-
ref.project_root_hash.trim()
|
|
73
|
-
) {
|
|
74
|
-
row.project_root_hash = ref.project_root_hash.trim();
|
|
75
|
-
}
|
|
76
|
-
if (typeof ref?.session_id === "string" && ref.session_id.trim()) {
|
|
77
|
-
row.session_id = ref.session_id.trim();
|
|
78
|
-
}
|
|
79
|
-
return row;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function rowKey(kind, id) {
|
|
83
|
-
return `${kind}:${id}`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function hygieneKeyForRow(row) {
|
|
87
|
-
return rowKey(row.kind, row.id);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function upsertLatest(map, key, value) {
|
|
91
|
-
const existing = map.get(key);
|
|
92
|
-
if (!existing || eventTime(value.entry) >= eventTime(existing.entry)) {
|
|
93
|
-
map.set(key, value);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function emptyCounts() {
|
|
98
|
-
return Object.fromEntries(COUNT_KEYS.map((key) => [key, 0]));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function countsFromRows(rows) {
|
|
102
|
-
const counts = emptyCounts();
|
|
103
|
-
for (const row of rows) {
|
|
104
|
-
if (row.kind === "task") {
|
|
105
|
-
if (row.status === "completed") counts.completed_tasks += 1;
|
|
106
|
-
else counts.active_tasks += 1;
|
|
107
|
-
} else if (row.kind === "session" && row.status === "stale") {
|
|
108
|
-
counts.stale_sessions += 1;
|
|
109
|
-
} else if (row.kind === "worktree" && row.status === "orphaned") {
|
|
110
|
-
counts.orphan_worktrees += 1;
|
|
111
|
-
} else if (row.kind === "checkpoint" && row.status === "superseded") {
|
|
112
|
-
counts.superseded_checkpoints += 1;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
counts.unknown_owner = rows.filter((row) => row.owner === "unknown").length;
|
|
116
|
-
return counts;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function compactHygieneCounts(projection) {
|
|
120
|
-
const counts = projection?.counts || {};
|
|
121
|
-
return Object.fromEntries(COUNT_KEYS.map((key) => [key, counts[key] || 0]));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export function projectCtoHygiene({ current = {}, ledger = null } = {}) {
|
|
125
|
-
const events = Array.isArray(ledger)
|
|
126
|
-
? ledger
|
|
127
|
-
: Array.isArray(current?.ledger_tail)
|
|
128
|
-
? current.ledger_tail
|
|
129
|
-
: [];
|
|
130
|
-
const tasks = new Map();
|
|
131
|
-
const staleSessions = new Map();
|
|
132
|
-
const worktrees = new Map();
|
|
133
|
-
const checkpointsBySession = new Map();
|
|
134
|
-
const explicitSupersededCheckpoints = new Map();
|
|
135
|
-
const appliedByKey = new Map();
|
|
136
|
-
|
|
137
|
-
for (const entry of events) {
|
|
138
|
-
const ref = eventRef(entry);
|
|
139
|
-
switch (entry?.event) {
|
|
140
|
-
case "hygiene_applied": {
|
|
141
|
-
const key =
|
|
142
|
-
typeof ref.hygiene_key === "string" && ref.hygiene_key.trim()
|
|
143
|
-
? ref.hygiene_key.trim()
|
|
144
|
-
: null;
|
|
145
|
-
if (!key) break;
|
|
146
|
-
upsertLatest(appliedByKey, key, { entry, ref });
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
case "task_claimed":
|
|
150
|
-
case "task_completed": {
|
|
151
|
-
const taskId = typeof ref.task_id === "string" ? ref.task_id : null;
|
|
152
|
-
if (!taskId) break;
|
|
153
|
-
upsertLatest(tasks, taskId, { entry, ref });
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
case "session_stale": {
|
|
157
|
-
const sessionId =
|
|
158
|
-
typeof ref.session_id === "string" ? ref.session_id : null;
|
|
159
|
-
if (!sessionId) break;
|
|
160
|
-
upsertLatest(staleSessions, sessionId, { entry, ref });
|
|
161
|
-
break;
|
|
162
|
-
}
|
|
163
|
-
case "worktree_created":
|
|
164
|
-
case "worktree_removed": {
|
|
165
|
-
const key =
|
|
166
|
-
(typeof ref.worktree_path_hash === "string" &&
|
|
167
|
-
ref.worktree_path_hash) ||
|
|
168
|
-
(typeof ref.worktree_label === "string" && ref.worktree_label) ||
|
|
169
|
-
null;
|
|
170
|
-
if (!key) break;
|
|
171
|
-
upsertLatest(worktrees, key, { entry, ref });
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
case "checkpoint_saved":
|
|
175
|
-
case "checkpoint_restored": {
|
|
176
|
-
const checkpointId =
|
|
177
|
-
typeof ref.checkpoint_id === "string" ? ref.checkpoint_id : null;
|
|
178
|
-
if (!checkpointId) break;
|
|
179
|
-
if (ref.status === "superseded") {
|
|
180
|
-
upsertLatest(explicitSupersededCheckpoints, checkpointId, {
|
|
181
|
-
entry,
|
|
182
|
-
ref,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
const sessionId =
|
|
186
|
-
(typeof ref.session_id === "string" && ref.session_id) ||
|
|
187
|
-
(typeof ref.restored_from_session_id === "string" &&
|
|
188
|
-
ref.restored_from_session_id) ||
|
|
189
|
-
null;
|
|
190
|
-
if (!sessionId) break;
|
|
191
|
-
if (!checkpointsBySession.has(sessionId))
|
|
192
|
-
checkpointsBySession.set(sessionId, []);
|
|
193
|
-
checkpointsBySession.get(sessionId).push({ entry, ref, checkpointId });
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
default:
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const rows = [];
|
|
202
|
-
|
|
203
|
-
for (const [taskId, item] of tasks) {
|
|
204
|
-
const completed = item.entry?.event === "task_completed";
|
|
205
|
-
const status = statusOf(item.ref, completed ? "completed" : "active");
|
|
206
|
-
rows.push(
|
|
207
|
-
rowFrom(
|
|
208
|
-
"task",
|
|
209
|
-
taskId,
|
|
210
|
-
completed || status === "completed" ? "completed" : status,
|
|
211
|
-
item.entry,
|
|
212
|
-
item.ref,
|
|
213
|
-
completed || status === "completed"
|
|
214
|
-
? null
|
|
215
|
-
: "complete_or_reassign_task",
|
|
216
|
-
),
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
for (const [sessionId, item] of staleSessions) {
|
|
221
|
-
rows.push(
|
|
222
|
-
rowFrom(
|
|
223
|
-
"session",
|
|
224
|
-
sessionId,
|
|
225
|
-
"stale",
|
|
226
|
-
item.entry,
|
|
227
|
-
item.ref,
|
|
228
|
-
"archive_or_resume_session",
|
|
229
|
-
),
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
for (const [key, item] of worktrees) {
|
|
234
|
-
if (item.entry?.event === "worktree_removed") continue;
|
|
235
|
-
const status = statusOf(item.ref, "active");
|
|
236
|
-
if (status !== "orphaned") continue;
|
|
237
|
-
rows.push(
|
|
238
|
-
rowFrom(
|
|
239
|
-
"worktree",
|
|
240
|
-
item.ref.worktree_label || key,
|
|
241
|
-
"orphaned",
|
|
242
|
-
item.entry,
|
|
243
|
-
item.ref,
|
|
244
|
-
"remove_or_reassign_worktree",
|
|
245
|
-
),
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const superseded = new Map(explicitSupersededCheckpoints);
|
|
250
|
-
for (const checkpoints of checkpointsBySession.values()) {
|
|
251
|
-
const ordered = [...checkpoints].sort((a, b) =>
|
|
252
|
-
eventTime(a.entry).localeCompare(eventTime(b.entry)),
|
|
253
|
-
);
|
|
254
|
-
for (const item of ordered.slice(0, -1)) {
|
|
255
|
-
if (!superseded.has(item.checkpointId)) {
|
|
256
|
-
superseded.set(item.checkpointId, item);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
for (const [checkpointId, item] of superseded) {
|
|
261
|
-
rows.push(
|
|
262
|
-
rowFrom(
|
|
263
|
-
"checkpoint",
|
|
264
|
-
checkpointId,
|
|
265
|
-
"superseded",
|
|
266
|
-
item.entry,
|
|
267
|
-
item.ref,
|
|
268
|
-
"prune_superseded_checkpoint",
|
|
269
|
-
),
|
|
270
|
-
);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const sortedRows = rows
|
|
274
|
-
.filter((row) => {
|
|
275
|
-
const applied = appliedByKey.get(hygieneKeyForRow(row));
|
|
276
|
-
if (!applied) return true;
|
|
277
|
-
return eventTime(applied.entry) < String(row.last_event_at || "");
|
|
278
|
-
})
|
|
279
|
-
.map((row) => {
|
|
280
|
-
if (row.owner === undefined) {
|
|
281
|
-
const { owner: _owner, ...rest } = row;
|
|
282
|
-
return rest;
|
|
283
|
-
}
|
|
284
|
-
return row;
|
|
285
|
-
});
|
|
286
|
-
const counts = countsFromRows(sortedRows);
|
|
287
|
-
|
|
288
|
-
return {
|
|
289
|
-
schema_version: "cto-hygiene.v1",
|
|
290
|
-
dry_run: true,
|
|
291
|
-
counts,
|
|
292
|
-
rows: sortedRows,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export async function runHygiene(args = [], opts = {}) {
|
|
297
|
-
if (args.some((arg) => !["--dry-run", "--json"].includes(arg))) {
|
|
298
|
-
throw new Error("tfx cto hygiene supports only --dry-run and --json");
|
|
299
|
-
}
|
|
300
|
-
if (opts.dryRun !== true && !hasFlag(args, "--dry-run")) {
|
|
301
|
-
throw new Error("tfx cto hygiene requires --dry-run");
|
|
302
|
-
}
|
|
303
|
-
const rootDir = opts.rootDir || resolveLakeRootDir(process.cwd());
|
|
304
|
-
const lakeRoot = opts.lakeRoot || join(rootDir, ".triflux", "lake");
|
|
305
|
-
const stdout = opts.stdout || process.stdout;
|
|
306
|
-
const currentPath = join(lakeRoot, "current.json");
|
|
307
|
-
const current = existsSync(currentPath) ? readJson(currentPath) : {};
|
|
308
|
-
const ledger = readJsonLines(join(lakeRoot, "ledger.jsonl"));
|
|
309
|
-
const projection = projectCtoHygiene({
|
|
310
|
-
current,
|
|
311
|
-
ledger: ledger.length ? ledger : null,
|
|
312
|
-
});
|
|
313
|
-
if (opts.json === true || hasFlag(args, "--json"))
|
|
314
|
-
writeJson(stdout, projection);
|
|
315
|
-
else
|
|
316
|
-
stdout.write(
|
|
317
|
-
`cto hygiene dry-run: ${projection.counts.active_tasks} active tasks, ${projection.counts.stale_sessions} stale sessions, ${projection.counts.orphan_worktrees} orphan worktrees\n`,
|
|
318
|
-
);
|
|
319
|
-
return projection;
|
|
320
|
-
}
|
package/cto/lake-root.mjs
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { execFileSync as defaultExecFileSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
existsSync as defaultExistsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
readFileSync,
|
|
6
|
-
renameSync,
|
|
7
|
-
writeFileSync,
|
|
8
|
-
} from "node:fs";
|
|
9
|
-
import { basename, dirname, join, parse } from "node:path";
|
|
10
|
-
|
|
11
|
-
const MAX_DEPTH = 64;
|
|
12
|
-
|
|
13
|
-
function execGit(cwd, args, execFileSync) {
|
|
14
|
-
return execFileSync("git", ["-C", cwd, ...args], {
|
|
15
|
-
encoding: "utf8",
|
|
16
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
17
|
-
windowsHide: true,
|
|
18
|
-
}).trim();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function resolveViaGitCommonDir(cwd, execFileSync) {
|
|
22
|
-
try {
|
|
23
|
-
const commonDir = execGit(
|
|
24
|
-
cwd,
|
|
25
|
-
["rev-parse", "--path-format=absolute", "--git-common-dir"],
|
|
26
|
-
execFileSync,
|
|
27
|
-
);
|
|
28
|
-
if (commonDir && basename(commonDir) === ".git") {
|
|
29
|
-
return dirname(commonDir);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Bare repos and submodules can have non-`.git` common dirs. In that case,
|
|
33
|
-
// `--show-toplevel` is the safest project root signal.
|
|
34
|
-
const topLevel = execGit(
|
|
35
|
-
cwd,
|
|
36
|
-
["rev-parse", "--path-format=absolute", "--show-toplevel"],
|
|
37
|
-
execFileSync,
|
|
38
|
-
);
|
|
39
|
-
return topLevel || null;
|
|
40
|
-
} catch {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* cwd 에서 가장 가까운 git toplevel(`.git` 디렉토리 또는 파일이 있는 폴더)을
|
|
47
|
-
* 찾아 반환한다. linked worktree 에서는 worktree 루트가 아니라 git common dir 의
|
|
48
|
-
* project root 를 반환해 프로젝트당 CTO lake 하나를 유지한다. 못 찾으면 cwd 를
|
|
49
|
-
* 그대로 돌려준다(기존 동작 보존).
|
|
50
|
-
*
|
|
51
|
-
* @param {string} cwd 시작 디렉토리(보통 process.cwd())
|
|
52
|
-
* @param {object} [opts]
|
|
53
|
-
* @param {(path: string) => boolean} [opts.existsSync] 테스트용 주입 seam
|
|
54
|
-
* @param {typeof defaultExecFileSync} [opts.execFileSync] 테스트용 git seam
|
|
55
|
-
* @returns {string}
|
|
56
|
-
*/
|
|
57
|
-
export function resolveLakeRootDir(cwd, opts = {}) {
|
|
58
|
-
const exists = opts?.existsSync || defaultExistsSync;
|
|
59
|
-
const execFileSync = opts?.execFileSync || defaultExecFileSync;
|
|
60
|
-
// 비문자열(객체/숫자 등 truthy 포함) 또는 빈 문자열이면 항상 "" 를 반환해
|
|
61
|
-
// @returns {string} 계약을 지킨다. truthy 비문자열을 그대로 누설하지 않는다.
|
|
62
|
-
if (typeof cwd !== "string" || !cwd) return "";
|
|
63
|
-
|
|
64
|
-
const gitRoot = resolveViaGitCommonDir(cwd, execFileSync);
|
|
65
|
-
if (gitRoot) return gitRoot;
|
|
66
|
-
|
|
67
|
-
let dir = cwd;
|
|
68
|
-
const { root } = parse(dir);
|
|
69
|
-
for (let depth = 0; depth < MAX_DEPTH; depth++) {
|
|
70
|
-
if (exists(join(dir, ".git"))) return dir;
|
|
71
|
-
if (dir === root) break;
|
|
72
|
-
const parent = dirname(dir);
|
|
73
|
-
if (parent === dir) break;
|
|
74
|
-
dir = parent;
|
|
75
|
-
}
|
|
76
|
-
return cwd;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function toIsoTime(value) {
|
|
80
|
-
if (typeof value === "string" && value.trim()) return value;
|
|
81
|
-
if (value instanceof Date) return value.toISOString();
|
|
82
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
83
|
-
return new Date(value).toISOString();
|
|
84
|
-
}
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function shortHash(value) {
|
|
89
|
-
const str = String(value ?? "");
|
|
90
|
-
let h = 5381;
|
|
91
|
-
for (let i = 0; i < str.length; i++) {
|
|
92
|
-
h = (h * 33) ^ str.charCodeAt(i);
|
|
93
|
-
}
|
|
94
|
-
return (h >>> 0).toString(36);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function pathLabel(value) {
|
|
98
|
-
const str = String(value ?? "").replace(/[/\\]+$/u, "");
|
|
99
|
-
if (!str) return "";
|
|
100
|
-
const segments = str.split(/[/\\]+/u);
|
|
101
|
-
return segments[segments.length - 1] || "";
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function readJsonLines(filePath, limit = Infinity) {
|
|
105
|
-
if (!defaultExistsSync(filePath)) return [];
|
|
106
|
-
const lines = readFileSync(filePath, "utf8")
|
|
107
|
-
.split(/\r?\n/u)
|
|
108
|
-
.map((line) => line.trim())
|
|
109
|
-
.filter(Boolean);
|
|
110
|
-
return lines.slice(-limit).flatMap((line) => {
|
|
111
|
-
try {
|
|
112
|
-
return [JSON.parse(line)];
|
|
113
|
-
} catch {
|
|
114
|
-
return [];
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function writeAtomic(filePath, body) {
|
|
120
|
-
mkdirSync(dirname(filePath), { recursive: true });
|
|
121
|
-
const tmpPath = join(
|
|
122
|
-
dirname(filePath),
|
|
123
|
-
`.${basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
|
|
124
|
-
);
|
|
125
|
-
writeFileSync(tmpPath, body, "utf8");
|
|
126
|
-
renameSync(tmpPath, filePath);
|
|
127
|
-
}
|