@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
|
@@ -143,7 +143,14 @@ function envWarning(server, home, env, warnings) {
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
function patchJson(
|
|
146
|
+
function patchJson(
|
|
147
|
+
original,
|
|
148
|
+
file,
|
|
149
|
+
home,
|
|
150
|
+
env,
|
|
151
|
+
warnings,
|
|
152
|
+
{ withEnv = true } = {},
|
|
153
|
+
) {
|
|
147
154
|
const data = JSON.parse(original);
|
|
148
155
|
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
149
156
|
throw new Error(`${file}: JSON 객체가 아닙니다`);
|
|
@@ -190,7 +197,7 @@ function patchJson(original, file, home, env, warnings) {
|
|
|
190
197
|
...kept,
|
|
191
198
|
command: server.command,
|
|
192
199
|
args: server.args,
|
|
193
|
-
...(server.envVars.length
|
|
200
|
+
...(withEnv && server.envVars.length
|
|
194
201
|
? {
|
|
195
202
|
env: {
|
|
196
203
|
...kept.env,
|
|
@@ -710,6 +717,8 @@ export function cleanupLegacyMcp({
|
|
|
710
717
|
[join(repoRoot, ".mcp.json"), "json"],
|
|
711
718
|
[join(home, ".codex/config.toml"), "toml"],
|
|
712
719
|
[join(home, ".gemini/settings.json"), "json"],
|
|
720
|
+
// agy 는 \${VAR} 참조를 풀지 않으므로 env 없이 바꾸고 셸 환경을 물려받게 한다.
|
|
721
|
+
[join(home, ".gemini/config/mcp_config.json"), "agy"],
|
|
713
722
|
];
|
|
714
723
|
const plans = [];
|
|
715
724
|
let blocked = false;
|
|
@@ -721,7 +730,9 @@ export function cleanupLegacyMcp({
|
|
|
721
730
|
const plan =
|
|
722
731
|
kind === "toml"
|
|
723
732
|
? patchToml(original, file, home, env, result.warnings)
|
|
724
|
-
: patchJson(original, file, home, env, result.warnings
|
|
733
|
+
: patchJson(original, file, home, env, result.warnings, {
|
|
734
|
+
withEnv: kind !== "agy",
|
|
735
|
+
});
|
|
725
736
|
if (plan.blocked) blocked = true;
|
|
726
737
|
if (plan.count) plans.push({ file, target, original, ...plan });
|
|
727
738
|
} catch {
|
|
@@ -10,7 +10,7 @@ export const PSMUX_REQUIRED_COMMANDS = [
|
|
|
10
10
|
|
|
11
11
|
export const PSMUX_OPTIONAL_COMMANDS = ["detach-client"];
|
|
12
12
|
|
|
13
|
-
// Windows
|
|
13
|
+
// Windows 전용 설치 명령
|
|
14
14
|
export const PSMUX_INSTALL_COMMANDS = [
|
|
15
15
|
"winget install psmux",
|
|
16
16
|
"scoop install psmux",
|
|
@@ -25,50 +25,18 @@ export const PSMUX_UPDATE_COMMANDS = [
|
|
|
25
25
|
"cargo install psmux --force",
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
-
// macOS 전용 — brew 가 표준, cargo 는 cross-platform fallback
|
|
29
|
-
export const PSMUX_INSTALL_COMMANDS_DARWIN = [
|
|
30
|
-
"brew install psmux",
|
|
31
|
-
"cargo install psmux",
|
|
32
|
-
];
|
|
33
|
-
|
|
34
|
-
export const PSMUX_UPDATE_COMMANDS_DARWIN = [
|
|
35
|
-
"brew upgrade psmux",
|
|
36
|
-
"cargo install psmux --force",
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
// Linux 전용 — cargo 만 cross-platform 지원, distro 패키지는 미제공
|
|
40
|
-
export const PSMUX_INSTALL_COMMANDS_LINUX = ["cargo install psmux"];
|
|
41
|
-
|
|
42
|
-
export const PSMUX_UPDATE_COMMANDS_LINUX = ["cargo install psmux --force"];
|
|
43
|
-
|
|
44
|
-
// PR #258 OS-primary 분기 정책: macOS/Linux 는 tmux 가 POSIX 표준 primary,
|
|
45
|
-
// psmux 는 선택형이다. 따라서 macOS/Linux 에서 "psmux 미설치"는 fallback 사유가
|
|
46
|
-
// 아니며, tmux 까지 없을 때만 native/in-process fallback 을 고려한다.
|
|
47
|
-
export const PSMUX_FALLBACK_NOTE_DARWIN =
|
|
48
|
-
"macOS 에서는 tmux 가 표준 멀티플렉서이며, " +
|
|
49
|
-
"psmux 미설치는 fallback 사유가 아닙니다. tmux 도 없을 때만 in-process/native fallback 을 사용합니다.";
|
|
50
|
-
|
|
51
|
-
export const PSMUX_FALLBACK_NOTE_LINUX =
|
|
52
|
-
"Linux 에서는 tmux 가 표준 멀티플렉서이며, " +
|
|
53
|
-
"psmux 미설치는 fallback 사유가 아닙니다. tmux 도 없을 때만 in-process/native fallback 을 사용합니다.";
|
|
54
|
-
|
|
55
28
|
export function getPsmuxInstallCommandsFor(platform = process.platform) {
|
|
56
|
-
if (platform === "
|
|
57
|
-
|
|
58
|
-
// win32 또는 기타 (BSD 등) → Windows 풀 리스트로 폴백 (변경 전 동작 유지)
|
|
59
|
-
return PSMUX_INSTALL_COMMANDS;
|
|
29
|
+
if (platform === "win32") return PSMUX_INSTALL_COMMANDS;
|
|
30
|
+
return [platform === "darwin" ? "brew install tmux" : "apt install tmux"];
|
|
60
31
|
}
|
|
61
32
|
|
|
62
33
|
export function getPsmuxUpdateCommandsFor(platform = process.platform) {
|
|
63
|
-
if (platform === "
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (platform === "darwin") return PSMUX_FALLBACK_NOTE_DARWIN;
|
|
70
|
-
if (platform === "linux") return PSMUX_FALLBACK_NOTE_LINUX;
|
|
71
|
-
return null;
|
|
34
|
+
if (platform === "win32") return PSMUX_UPDATE_COMMANDS;
|
|
35
|
+
return [
|
|
36
|
+
platform === "darwin"
|
|
37
|
+
? "brew upgrade tmux"
|
|
38
|
+
: "apt install --only-upgrade tmux",
|
|
39
|
+
];
|
|
72
40
|
}
|
|
73
41
|
|
|
74
42
|
export function formatPsmuxCommandList(
|
|
@@ -82,10 +50,7 @@ export function formatPsmuxInstallGuidance(
|
|
|
82
50
|
indent = "",
|
|
83
51
|
platform = process.platform,
|
|
84
52
|
) {
|
|
85
|
-
|
|
86
|
-
const list = formatPsmuxCommandList(commands, indent);
|
|
87
|
-
const note = getPsmuxFallbackNoteFor(platform);
|
|
88
|
-
return note ? `${list}\n${indent}(${note})` : list;
|
|
53
|
+
return formatPsmuxCommandList(getPsmuxInstallCommandsFor(platform), indent);
|
|
89
54
|
}
|
|
90
55
|
|
|
91
56
|
export function formatPsmuxUpdateGuidance(
|
|
@@ -160,8 +125,8 @@ export function probePsmuxSupport(options = {}) {
|
|
|
160
125
|
missingCommands,
|
|
161
126
|
missingOptionalCommands,
|
|
162
127
|
hasHelp: helpOutput.length > 0,
|
|
163
|
-
installHint: formatPsmuxInstallGuidance(" "),
|
|
164
|
-
updateHint: formatPsmuxUpdateGuidance(" "),
|
|
128
|
+
installHint: formatPsmuxInstallGuidance(" ", "win32"),
|
|
129
|
+
updateHint: formatPsmuxUpdateGuidance(" ", "win32"),
|
|
165
130
|
};
|
|
166
131
|
} catch {
|
|
167
132
|
return {
|
|
@@ -173,8 +138,8 @@ export function probePsmuxSupport(options = {}) {
|
|
|
173
138
|
missingCommands: [...PSMUX_REQUIRED_COMMANDS],
|
|
174
139
|
missingOptionalCommands: [...PSMUX_OPTIONAL_COMMANDS],
|
|
175
140
|
hasHelp: false,
|
|
176
|
-
installHint: formatPsmuxInstallGuidance(" "),
|
|
177
|
-
updateHint: formatPsmuxUpdateGuidance(" "),
|
|
141
|
+
installHint: formatPsmuxInstallGuidance(" ", "win32"),
|
|
142
|
+
updateHint: formatPsmuxUpdateGuidance(" ", "win32"),
|
|
178
143
|
};
|
|
179
144
|
}
|
|
180
145
|
}
|
|
@@ -207,6 +172,8 @@ export function probePrimaryMultiplexerSupport(options = {}) {
|
|
|
207
172
|
recommended: true,
|
|
208
173
|
missingCommands: [],
|
|
209
174
|
missingOptionalCommands: [],
|
|
175
|
+
installHint: formatPsmuxInstallGuidance(" ", platform),
|
|
176
|
+
updateHint: formatPsmuxUpdateGuidance(" ", platform),
|
|
210
177
|
};
|
|
211
178
|
} catch (error) {
|
|
212
179
|
return {
|
|
@@ -218,6 +185,8 @@ export function probePrimaryMultiplexerSupport(options = {}) {
|
|
|
218
185
|
recommended: false,
|
|
219
186
|
missingCommands: ["tmux"],
|
|
220
187
|
missingOptionalCommands: [],
|
|
188
|
+
installHint: formatPsmuxInstallGuidance(" ", platform),
|
|
189
|
+
updateHint: formatPsmuxUpdateGuidance(" ", platform),
|
|
221
190
|
error: error?.message || String(error),
|
|
222
191
|
};
|
|
223
192
|
}
|
|
@@ -340,32 +340,6 @@ export function loadTemplatePartials(partialsDir) {
|
|
|
340
340
|
return partials;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
export function loadSkillManifest(skillDir) {
|
|
344
|
-
const manifestPath = join(skillDir, "skill.json");
|
|
345
|
-
if (!existsSync(manifestPath)) return null;
|
|
346
|
-
|
|
347
|
-
const raw = readFileSync(manifestPath, "utf8");
|
|
348
|
-
return JSON.parse(raw);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
export function parseFrontmatterWithManifest(source, skillDir) {
|
|
352
|
-
const manifest = skillDir ? loadSkillManifest(skillDir) : null;
|
|
353
|
-
const { data: yamlData, body } = parseFrontmatter(source);
|
|
354
|
-
|
|
355
|
-
if (!manifest) return { data: yamlData, body };
|
|
356
|
-
|
|
357
|
-
const merged = { ...yamlData };
|
|
358
|
-
if (manifest.name) merged.name = manifest.name;
|
|
359
|
-
if (manifest.description) merged.description = manifest.description;
|
|
360
|
-
if (manifest.triggers) merged.triggers = manifest.triggers;
|
|
361
|
-
if (manifest.argument_hint) merged["argument-hint"] = manifest.argument_hint;
|
|
362
|
-
if (manifest.internal != null) merged.internal = manifest.internal;
|
|
363
|
-
if (manifest.deprecated != null) merged.deprecated = manifest.deprecated;
|
|
364
|
-
if (manifest.superseded_by) merged["superseded-by"] = manifest.superseded_by;
|
|
365
|
-
|
|
366
|
-
return { data: merged, body };
|
|
367
|
-
}
|
|
368
|
-
|
|
369
343
|
export function renderSkillTemplate(template, context = {}, options = {}) {
|
|
370
344
|
const { partials = {}, includes = {}, includeBaseDir = "" } = options;
|
|
371
345
|
return renderWithContext(template, context, partials, {
|
package/cto/hygiene-actions.mjs
DELETED
|
@@ -1,355 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
copyFileSync,
|
|
4
|
-
cpSync,
|
|
5
|
-
existsSync,
|
|
6
|
-
lstatSync,
|
|
7
|
-
mkdirSync,
|
|
8
|
-
renameSync,
|
|
9
|
-
rmSync,
|
|
10
|
-
statSync,
|
|
11
|
-
unlinkSync,
|
|
12
|
-
} from "node:fs";
|
|
13
|
-
import {
|
|
14
|
-
basename,
|
|
15
|
-
dirname,
|
|
16
|
-
isAbsolute,
|
|
17
|
-
join,
|
|
18
|
-
relative,
|
|
19
|
-
resolve,
|
|
20
|
-
} from "node:path";
|
|
21
|
-
|
|
22
|
-
const ARCHIVABLE_ACTIONS = new Set([
|
|
23
|
-
"archive_or_resume_session",
|
|
24
|
-
"prune_superseded_checkpoint",
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
function shortHash(value) {
|
|
28
|
-
const str = String(value ?? "");
|
|
29
|
-
let h = 5381;
|
|
30
|
-
for (let i = 0; i < str.length; i += 1) {
|
|
31
|
-
h = (h * 33) ^ str.charCodeAt(i);
|
|
32
|
-
}
|
|
33
|
-
return (h >>> 0).toString(36);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function stewardRootHash(rootDir) {
|
|
37
|
-
return createHash("sha256")
|
|
38
|
-
.update(String(rootDir || ""))
|
|
39
|
-
.digest("hex")
|
|
40
|
-
.slice(0, 12);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function hygieneKeyForRow(row) {
|
|
44
|
-
return `${row.kind}:${row.id}`;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function yyyymmdd(value) {
|
|
48
|
-
const date = value ? new Date(value) : new Date();
|
|
49
|
-
if (Number.isNaN(date.getTime())) return yyyymmdd(new Date().toISOString());
|
|
50
|
-
return date.toISOString().slice(0, 10).replace(/-/gu, "");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function safeLabel(value) {
|
|
54
|
-
return (
|
|
55
|
-
String(value || "item")
|
|
56
|
-
.toLowerCase()
|
|
57
|
-
.replace(/[^a-z0-9._-]+/gu, "-")
|
|
58
|
-
.replace(/^-+|-+$/gu, "")
|
|
59
|
-
.slice(0, 80) || "item"
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function isInside(parent, child) {
|
|
64
|
-
const rel = relative(resolve(parent), resolve(child));
|
|
65
|
-
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function normalizeActiveSessions(active) {
|
|
69
|
-
const sessions = Array.isArray(active)
|
|
70
|
-
? active
|
|
71
|
-
: Array.isArray(active?.sessions)
|
|
72
|
-
? active.sessions
|
|
73
|
-
: Array.isArray(active?.live_sessions)
|
|
74
|
-
? active.live_sessions
|
|
75
|
-
: [];
|
|
76
|
-
return sessions
|
|
77
|
-
.map((session) => ({
|
|
78
|
-
sessionId: String(session?.sessionId || session?.session_id || "").trim(),
|
|
79
|
-
phase: String(
|
|
80
|
-
session?.phase || session?.status || "active",
|
|
81
|
-
).toLowerCase(),
|
|
82
|
-
}))
|
|
83
|
-
.filter((session) => session.sessionId);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function isLiveOwned(row, activeSessions) {
|
|
87
|
-
const rowSession = String(row?.session_id || row?.id || "").trim();
|
|
88
|
-
if (!rowSession) return false;
|
|
89
|
-
return activeSessions.some(
|
|
90
|
-
(session) => session.sessionId === rowSession && session.phase !== "stale",
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function assertProjectHash(row, rootDir) {
|
|
95
|
-
if (!row?.project_root_hash) return;
|
|
96
|
-
const allowed = new Set([shortHash(rootDir), stewardRootHash(rootDir)]);
|
|
97
|
-
if (!allowed.has(String(row.project_root_hash))) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`project_root_hash mismatch for ${hygieneKeyForRow(row)}: ${row.project_root_hash}`,
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function assertLakePath(lakeRoot, path, label) {
|
|
105
|
-
if (!isInside(lakeRoot, path)) {
|
|
106
|
-
throw new Error(`${label} must be under lakeRoot: ${path}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function sourceBytes(path) {
|
|
111
|
-
const info = statSync(path);
|
|
112
|
-
if (info.isFile()) return info.size;
|
|
113
|
-
if (!info.isDirectory()) return info.size || 0;
|
|
114
|
-
return 0;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function targetPathFor({ lakeRoot, row, sourcePath, now }) {
|
|
118
|
-
const archiveDir = join(lakeRoot, "archive", yyyymmdd(now));
|
|
119
|
-
const label = safeLabel(hygieneKeyForRow(row));
|
|
120
|
-
return join(archiveDir, `${label}-${basename(sourcePath)}`);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function hasSecondActorApproval(opts = {}) {
|
|
124
|
-
if (opts.humanAck === true || opts.humanGate === true) return true;
|
|
125
|
-
const actorSessionId = String(opts.actorSessionId || "").trim();
|
|
126
|
-
if (!actorSessionId) return false;
|
|
127
|
-
const hygieneKey = opts.hygieneKey;
|
|
128
|
-
const approvals = Array.isArray(opts.approvals) ? opts.approvals : [];
|
|
129
|
-
return approvals.some((entry) => {
|
|
130
|
-
const ref = entry?.ref && typeof entry.ref === "object" ? entry.ref : {};
|
|
131
|
-
if (ref.hygiene_key !== hygieneKey) return false;
|
|
132
|
-
const approverSession = String(ref?.actor?.session_id || "").trim();
|
|
133
|
-
return Boolean(approverSession && approverSession !== actorSessionId);
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function movePath(sourcePath, targetPath, fsOps = {}) {
|
|
138
|
-
const ops = {
|
|
139
|
-
copyFileSync,
|
|
140
|
-
cpSync,
|
|
141
|
-
existsSync,
|
|
142
|
-
lstatSync,
|
|
143
|
-
mkdirSync,
|
|
144
|
-
renameSync,
|
|
145
|
-
rmSync,
|
|
146
|
-
unlinkSync,
|
|
147
|
-
...fsOps,
|
|
148
|
-
};
|
|
149
|
-
ops.mkdirSync(dirname(targetPath), { recursive: true });
|
|
150
|
-
try {
|
|
151
|
-
ops.renameSync(sourcePath, targetPath);
|
|
152
|
-
return "rename";
|
|
153
|
-
} catch (error) {
|
|
154
|
-
if (error?.code !== "EXDEV") throw error;
|
|
155
|
-
const info = ops.lstatSync(sourcePath);
|
|
156
|
-
if (info.isDirectory()) {
|
|
157
|
-
ops.cpSync(sourcePath, targetPath, { recursive: true, force: false });
|
|
158
|
-
ops.rmSync(sourcePath, { recursive: true, force: false });
|
|
159
|
-
} else {
|
|
160
|
-
ops.copyFileSync(sourcePath, targetPath);
|
|
161
|
-
ops.unlinkSync(sourcePath);
|
|
162
|
-
}
|
|
163
|
-
return "copy_unlink";
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function digestOperations(operations) {
|
|
168
|
-
const payload = operations.map((operation) => ({
|
|
169
|
-
hygiene_key: operation.hygiene_key,
|
|
170
|
-
status: operation.status,
|
|
171
|
-
source_path: operation.source_path,
|
|
172
|
-
target_path: operation.target_path,
|
|
173
|
-
bytes: operation.bytes,
|
|
174
|
-
reason: operation.reason,
|
|
175
|
-
}));
|
|
176
|
-
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function rowsFromProjection(projection) {
|
|
180
|
-
return Array.isArray(projection?.rows) ? projection.rows : [];
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export async function applyHygieneArchiveActions({
|
|
184
|
-
rootDir,
|
|
185
|
-
lakeRoot,
|
|
186
|
-
projection,
|
|
187
|
-
dryRun = true,
|
|
188
|
-
now = new Date().toISOString(),
|
|
189
|
-
applyMode = "off",
|
|
190
|
-
active = {},
|
|
191
|
-
humanAck = false,
|
|
192
|
-
humanGate = false,
|
|
193
|
-
actorSessionId = null,
|
|
194
|
-
approvals = [],
|
|
195
|
-
fsOps = {},
|
|
196
|
-
} = {}) {
|
|
197
|
-
const archiveRoot = join(lakeRoot, "archive", yyyymmdd(now));
|
|
198
|
-
assertLakePath(lakeRoot, archiveRoot, "archive target");
|
|
199
|
-
|
|
200
|
-
const activeSessions = normalizeActiveSessions(active);
|
|
201
|
-
const operations = [];
|
|
202
|
-
|
|
203
|
-
for (const row of rowsFromProjection(projection)) {
|
|
204
|
-
const hygieneKey = hygieneKeyForRow(row);
|
|
205
|
-
if (row?.kind === "worktree" && row?.status === "orphaned") {
|
|
206
|
-
operations.push({
|
|
207
|
-
hygiene_key: hygieneKey,
|
|
208
|
-
kind: row.kind,
|
|
209
|
-
id: row.id,
|
|
210
|
-
action: row.action,
|
|
211
|
-
status: "blocked",
|
|
212
|
-
reason: "orphan_worktree_requires_human",
|
|
213
|
-
bytes: 0,
|
|
214
|
-
});
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (!ARCHIVABLE_ACTIONS.has(row?.action)) continue;
|
|
219
|
-
|
|
220
|
-
assertProjectHash(row, rootDir);
|
|
221
|
-
|
|
222
|
-
if (isLiveOwned(row, activeSessions)) {
|
|
223
|
-
operations.push({
|
|
224
|
-
hygiene_key: hygieneKey,
|
|
225
|
-
kind: row.kind,
|
|
226
|
-
id: row.id,
|
|
227
|
-
action: row.action,
|
|
228
|
-
status: "skipped",
|
|
229
|
-
reason: "live_session_owned",
|
|
230
|
-
bytes: 0,
|
|
231
|
-
});
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const sourcePath = row.artifact_path ? resolve(row.artifact_path) : null;
|
|
236
|
-
if (!sourcePath) {
|
|
237
|
-
operations.push({
|
|
238
|
-
hygiene_key: hygieneKey,
|
|
239
|
-
kind: row.kind,
|
|
240
|
-
id: row.id,
|
|
241
|
-
action: row.action,
|
|
242
|
-
status: "skipped",
|
|
243
|
-
reason: "missing_artifact_path",
|
|
244
|
-
bytes: 0,
|
|
245
|
-
});
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
// lake 밖 산출물은 자동 이동 대상이 아니다 — 크래시 대신 skip으로 보고한다.
|
|
249
|
-
// (checkpoint 등 실제 산출물은 대부분 lake 밖에 있으므로 throw 하면 apply 전체가 죽는다.)
|
|
250
|
-
if (!isInside(lakeRoot, sourcePath)) {
|
|
251
|
-
operations.push({
|
|
252
|
-
hygiene_key: hygieneKey,
|
|
253
|
-
kind: row.kind,
|
|
254
|
-
id: row.id,
|
|
255
|
-
action: row.action,
|
|
256
|
-
source_path: sourcePath,
|
|
257
|
-
status: "skipped",
|
|
258
|
-
reason: "source_outside_lake",
|
|
259
|
-
bytes: 0,
|
|
260
|
-
});
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const targetPath = targetPathFor({ lakeRoot, row, sourcePath, now });
|
|
265
|
-
assertLakePath(lakeRoot, targetPath, "archive target");
|
|
266
|
-
|
|
267
|
-
if (!existsSync(sourcePath)) {
|
|
268
|
-
operations.push({
|
|
269
|
-
hygiene_key: hygieneKey,
|
|
270
|
-
kind: row.kind,
|
|
271
|
-
id: row.id,
|
|
272
|
-
action: row.action,
|
|
273
|
-
source_path: sourcePath,
|
|
274
|
-
target_path: targetPath,
|
|
275
|
-
status: existsSync(targetPath) ? "already_archived" : "skipped",
|
|
276
|
-
reason: existsSync(targetPath) ? undefined : "source_missing",
|
|
277
|
-
bytes: 0,
|
|
278
|
-
});
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
operations.push({
|
|
283
|
-
hygiene_key: hygieneKey,
|
|
284
|
-
kind: row.kind,
|
|
285
|
-
id: row.id,
|
|
286
|
-
action: row.action,
|
|
287
|
-
source_path: sourcePath,
|
|
288
|
-
target_path: targetPath,
|
|
289
|
-
status: "planned",
|
|
290
|
-
bytes: sourceBytes(sourcePath),
|
|
291
|
-
});
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (!dryRun) {
|
|
295
|
-
for (const operation of operations) {
|
|
296
|
-
if (operation.status !== "planned") continue;
|
|
297
|
-
if (applyMode !== "archive") {
|
|
298
|
-
operation.status = "blocked";
|
|
299
|
-
operation.reason = "apply_mode_off";
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (
|
|
303
|
-
!hasSecondActorApproval({
|
|
304
|
-
humanAck,
|
|
305
|
-
humanGate,
|
|
306
|
-
actorSessionId,
|
|
307
|
-
approvals,
|
|
308
|
-
hygieneKey: operation.hygiene_key,
|
|
309
|
-
})
|
|
310
|
-
) {
|
|
311
|
-
operation.status = "blocked";
|
|
312
|
-
operation.reason = "second_actor_ack_required";
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
operation.move_method = movePath(
|
|
316
|
-
operation.source_path,
|
|
317
|
-
operation.target_path,
|
|
318
|
-
fsOps,
|
|
319
|
-
);
|
|
320
|
-
operation.status = "archived";
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const plannedCount = operations.filter((operation) =>
|
|
325
|
-
["planned", "archived", "already_archived"].includes(operation.status),
|
|
326
|
-
).length;
|
|
327
|
-
const appliedCount = operations.filter(
|
|
328
|
-
(operation) => operation.status === "archived",
|
|
329
|
-
).length;
|
|
330
|
-
const blockedCount = operations.filter(
|
|
331
|
-
(operation) => operation.status === "blocked",
|
|
332
|
-
).length;
|
|
333
|
-
|
|
334
|
-
return {
|
|
335
|
-
schema_version: "cto-hygiene-actions.v1",
|
|
336
|
-
dry_run: Boolean(dryRun),
|
|
337
|
-
apply_mode: applyMode,
|
|
338
|
-
archive_root: archiveRoot,
|
|
339
|
-
planned_count: plannedCount,
|
|
340
|
-
applied_count: appliedCount,
|
|
341
|
-
blocked_count: blockedCount,
|
|
342
|
-
total_bytes: operations.reduce(
|
|
343
|
-
(sum, operation) => sum + Number(operation.bytes || 0),
|
|
344
|
-
0,
|
|
345
|
-
),
|
|
346
|
-
digest: digestOperations(operations),
|
|
347
|
-
live_safety: {
|
|
348
|
-
active_session_count: activeSessions.length,
|
|
349
|
-
skipped_live_count: operations.filter(
|
|
350
|
-
(operation) => operation.reason === "live_session_owned",
|
|
351
|
-
).length,
|
|
352
|
-
},
|
|
353
|
-
operations,
|
|
354
|
-
};
|
|
355
|
-
}
|