@triflux/remote 10.42.0 → 10.44.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/cto/hygiene.mjs +1 -1
- package/hub/lib/cto-env.mjs +1 -0
- package/hub/pipe.mjs +9 -0
- package/hub/schema.sql +62 -2
- package/hub/server.mjs +131 -44
- package/hub/store.mjs +616 -19
- package/hub/team/claude-daemon-control.mjs +33 -12
- package/hub/team/cli/commands/start/parse-args.mjs +2 -2
- package/hub/team/conductor.mjs +23 -2
- package/hub/team/execution-mode.mjs +12 -7
- package/hub/team/headless.mjs +311 -24
- package/hub/team/intervention.mjs +530 -0
- package/hub/team/notify.mjs +31 -11
- package/hub/team/retry-state-machine.mjs +12 -3
- package/hub/team/swarm-hypervisor.mjs +21 -2
- package/hub/team/swarm-locks.mjs +17 -1
- package/hub/team/swarm-preflight.mjs +47 -0
- package/hub/team/uds-orchestrator.mjs +62 -9
- package/hub/team/worktree-lifecycle.mjs +56 -59
- package/hub/tools.mjs +16 -3
- package/hub/workers/codex-app-server-worker.mjs +54 -5
- package/hub/workers/codex-mcp.mjs +21 -5
- package/hub/workers/delegator-mcp.mjs +3 -49
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +307 -0
- package/scripts/lib/cli-agy.mjs +1 -0
- package/scripts/lib/cli-claude.mjs +1 -0
- package/scripts/lib/cli-codex.mjs +21 -160
- package/scripts/lib/codex-profile-config.mjs +31 -5
- package/scripts/lib/keyword-rules.mjs +12 -0
- package/cto/steward.mjs +0 -362
- package/hub/log-retention.mjs +0 -726
|
@@ -4,168 +4,21 @@
|
|
|
4
4
|
// 산출한다. 실제 spawn 은 Phase 1 에서 이관된다 (현재는 tfx-route.sh 가 수행).
|
|
5
5
|
//
|
|
6
6
|
// 진입점: tfx-route.mjs → selectAdapter("codex") → plan({...}).
|
|
7
|
-
// 출처:
|
|
7
|
+
// 출처: agent-route-policy.mjs 의 canonical Codex role policy.
|
|
8
|
+
// timeoutSec은 lane 예상 소요 시간(advisory)이다. hard ceiling 집행은 Bash route가 소유한다.
|
|
8
9
|
|
|
9
10
|
export const id = "codex";
|
|
10
11
|
export const cliType = "codex";
|
|
11
12
|
export const command = "codex";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
mcpHint: "implement",
|
|
20
|
-
},
|
|
21
|
-
codex: {
|
|
22
|
-
profile: "gpt55_high",
|
|
23
|
-
timeoutSec: 1080,
|
|
24
|
-
runMode: "fg",
|
|
25
|
-
opusOversight: false,
|
|
26
|
-
mcpHint: "implement",
|
|
27
|
-
},
|
|
28
|
-
"build-fixer": {
|
|
29
|
-
profile: "gpt55_low",
|
|
30
|
-
timeoutSec: 540,
|
|
31
|
-
runMode: "fg",
|
|
32
|
-
opusOversight: false,
|
|
33
|
-
mcpHint: "implement",
|
|
34
|
-
},
|
|
35
|
-
cleanup: {
|
|
36
|
-
profile: "gpt55_med",
|
|
37
|
-
timeoutSec: 540,
|
|
38
|
-
runMode: "fg",
|
|
39
|
-
opusOversight: false,
|
|
40
|
-
mcpHint: "implement",
|
|
41
|
-
},
|
|
42
|
-
deslop: {
|
|
43
|
-
profile: "gpt55_med",
|
|
44
|
-
timeoutSec: 540,
|
|
45
|
-
runMode: "fg",
|
|
46
|
-
opusOversight: false,
|
|
47
|
-
mcpHint: "implement",
|
|
48
|
-
},
|
|
49
|
-
debugger: {
|
|
50
|
-
profile: "gpt55_xhigh",
|
|
51
|
-
timeoutSec: 900,
|
|
52
|
-
runMode: "bg",
|
|
53
|
-
opusOversight: false,
|
|
54
|
-
mcpHint: "implement",
|
|
55
|
-
},
|
|
56
|
-
"deep-executor": {
|
|
57
|
-
profile: "gpt55_xhigh",
|
|
58
|
-
timeoutSec: 3600,
|
|
59
|
-
runMode: "bg",
|
|
60
|
-
opusOversight: true,
|
|
61
|
-
mcpHint: "implement",
|
|
62
|
-
},
|
|
63
|
-
architect: {
|
|
64
|
-
profile: "gpt55_xhigh",
|
|
65
|
-
timeoutSec: 3600,
|
|
66
|
-
runMode: "bg",
|
|
67
|
-
opusOversight: true,
|
|
68
|
-
mcpHint: "analyze",
|
|
69
|
-
},
|
|
70
|
-
critic: {
|
|
71
|
-
profile: "gpt55_xhigh",
|
|
72
|
-
timeoutSec: 3600,
|
|
73
|
-
runMode: "bg",
|
|
74
|
-
opusOversight: true,
|
|
75
|
-
mcpHint: "analyze",
|
|
76
|
-
},
|
|
77
|
-
planner: {
|
|
78
|
-
profile: "gpt55_xhigh",
|
|
79
|
-
timeoutSec: 3600,
|
|
80
|
-
runMode: "fg",
|
|
81
|
-
opusOversight: true,
|
|
82
|
-
mcpHint: "analyze",
|
|
83
|
-
},
|
|
84
|
-
analyst: {
|
|
85
|
-
profile: "gpt55_xhigh",
|
|
86
|
-
timeoutSec: 3600,
|
|
87
|
-
runMode: "fg",
|
|
88
|
-
opusOversight: true,
|
|
89
|
-
mcpHint: "analyze",
|
|
90
|
-
},
|
|
91
|
-
"code-reviewer": {
|
|
92
|
-
profile: "gpt55_high",
|
|
93
|
-
timeoutSec: 1800,
|
|
94
|
-
runMode: "bg",
|
|
95
|
-
opusOversight: false,
|
|
96
|
-
mcpHint: "review",
|
|
97
|
-
subcommand: "review",
|
|
98
|
-
},
|
|
99
|
-
"quality-reviewer": {
|
|
100
|
-
profile: "gpt55_high",
|
|
101
|
-
timeoutSec: 1800,
|
|
102
|
-
runMode: "bg",
|
|
103
|
-
opusOversight: false,
|
|
104
|
-
mcpHint: "review",
|
|
105
|
-
subcommand: "review",
|
|
106
|
-
},
|
|
107
|
-
"security-reviewer": {
|
|
108
|
-
profile: "gpt55_xhigh",
|
|
109
|
-
timeoutSec: 1800,
|
|
110
|
-
runMode: "bg",
|
|
111
|
-
opusOversight: true,
|
|
112
|
-
mcpHint: "review",
|
|
113
|
-
subcommand: "review",
|
|
114
|
-
},
|
|
115
|
-
scientist: {
|
|
116
|
-
profile: "gpt55_high",
|
|
117
|
-
timeoutSec: 1440,
|
|
118
|
-
runMode: "bg",
|
|
119
|
-
opusOversight: false,
|
|
120
|
-
mcpHint: "analyze",
|
|
121
|
-
},
|
|
122
|
-
"document-specialist": {
|
|
123
|
-
profile: "gpt55_high",
|
|
124
|
-
timeoutSec: 1440,
|
|
125
|
-
runMode: "bg",
|
|
126
|
-
opusOversight: false,
|
|
127
|
-
mcpHint: "analyze",
|
|
128
|
-
},
|
|
129
|
-
"scientist-deep": {
|
|
130
|
-
profile: "gpt55_xhigh",
|
|
131
|
-
timeoutSec: 3600,
|
|
132
|
-
runMode: "bg",
|
|
133
|
-
opusOversight: false,
|
|
134
|
-
mcpHint: "analyze",
|
|
135
|
-
},
|
|
136
|
-
verifier: {
|
|
137
|
-
profile: "gpt55_high",
|
|
138
|
-
timeoutSec: 1200,
|
|
139
|
-
runMode: "fg",
|
|
140
|
-
opusOversight: false,
|
|
141
|
-
mcpHint: "review",
|
|
142
|
-
subcommand: "review",
|
|
143
|
-
},
|
|
144
|
-
"test-engineer": {
|
|
145
|
-
profile: "gpt55_high",
|
|
146
|
-
timeoutSec: 1200,
|
|
147
|
-
runMode: "bg",
|
|
148
|
-
opusOversight: false,
|
|
149
|
-
mcpHint: "implement",
|
|
150
|
-
},
|
|
151
|
-
"qa-tester": {
|
|
152
|
-
profile: "gpt55_high",
|
|
153
|
-
timeoutSec: 1200,
|
|
154
|
-
runMode: "bg",
|
|
155
|
-
opusOversight: false,
|
|
156
|
-
mcpHint: "review",
|
|
157
|
-
subcommand: "review",
|
|
158
|
-
},
|
|
159
|
-
spark: {
|
|
160
|
-
profile: "gpt55_low",
|
|
161
|
-
timeoutSec: 180,
|
|
162
|
-
runMode: "fg",
|
|
163
|
-
opusOversight: false,
|
|
164
|
-
mcpHint: "implement",
|
|
165
|
-
},
|
|
166
|
-
};
|
|
14
|
+
import {
|
|
15
|
+
CODEX_AGENT_POLICY,
|
|
16
|
+
resolveCodexAgentPolicy,
|
|
17
|
+
resolveCodexAgentProfile,
|
|
18
|
+
resolveNestedCodexAgentProfile,
|
|
19
|
+
} from "./agent-route-policy.mjs";
|
|
167
20
|
|
|
168
|
-
|
|
21
|
+
export { resolveCodexAgentProfile, resolveNestedCodexAgentProfile };
|
|
169
22
|
|
|
170
23
|
export function plan({
|
|
171
24
|
agent,
|
|
@@ -173,18 +26,26 @@ export function plan({
|
|
|
173
26
|
mcpProfile = "auto",
|
|
174
27
|
timeoutSec,
|
|
175
28
|
contextFile,
|
|
29
|
+
profileOverride = "auto",
|
|
30
|
+
retryProfile = null,
|
|
31
|
+
nested = false,
|
|
176
32
|
} = {}) {
|
|
177
33
|
if (!agent) {
|
|
178
34
|
throw new Error("[cli-codex] agent required");
|
|
179
35
|
}
|
|
180
|
-
const cfg =
|
|
36
|
+
const cfg = resolveCodexAgentPolicy(agent);
|
|
37
|
+
const profile = resolveCodexAgentProfile(agent, {
|
|
38
|
+
profileOverride,
|
|
39
|
+
retryProfile,
|
|
40
|
+
nested,
|
|
41
|
+
});
|
|
181
42
|
const effectiveTimeoutSec =
|
|
182
43
|
Number.isFinite(timeoutSec) && timeoutSec > 0 ? timeoutSec : cfg.timeoutSec;
|
|
183
44
|
return {
|
|
184
45
|
command,
|
|
185
46
|
subcommand: cfg.subcommand ?? "exec",
|
|
186
|
-
profile
|
|
187
|
-
effort:
|
|
47
|
+
profile,
|
|
48
|
+
effort: profile,
|
|
188
49
|
timeoutMs: effectiveTimeoutSec * 1000,
|
|
189
50
|
runMode: cfg.runMode,
|
|
190
51
|
opusOversight: cfg.opusOversight,
|
|
@@ -195,5 +56,5 @@ export function plan({
|
|
|
195
56
|
}
|
|
196
57
|
|
|
197
58
|
export function describe() {
|
|
198
|
-
return { id, cliType, command, agents: Object.keys(
|
|
59
|
+
return { id, cliType, command, agents: Object.keys(CODEX_AGENT_POLICY) };
|
|
199
60
|
}
|
|
@@ -9,6 +9,8 @@ import { homedir } from "node:os";
|
|
|
9
9
|
import { dirname, join } from "node:path";
|
|
10
10
|
|
|
11
11
|
const EFFORT_BY_SUFFIX = {
|
|
12
|
+
ultra: "ultra",
|
|
13
|
+
max: "max",
|
|
12
14
|
xhigh: "xhigh",
|
|
13
15
|
high: "high",
|
|
14
16
|
med: "medium",
|
|
@@ -16,6 +18,15 @@ const EFFORT_BY_SUFFIX = {
|
|
|
16
18
|
low: "low",
|
|
17
19
|
};
|
|
18
20
|
|
|
21
|
+
const CANONICAL_PROFILE_VALUES = Object.freeze({
|
|
22
|
+
gpt56_luna_low: { model: "gpt-5.6-luna", effort: "low" },
|
|
23
|
+
gpt56_terra_med: { model: "gpt-5.6-terra", effort: "medium" },
|
|
24
|
+
gpt56_terra_high: { model: "gpt-5.6-terra", effort: "high" },
|
|
25
|
+
gpt56_sol_xhigh: { model: "gpt-5.6-sol", effort: "xhigh" },
|
|
26
|
+
gpt56_sol_max: { model: "gpt-5.6-sol", effort: "max" },
|
|
27
|
+
gpt56_sol_ultra: { model: "gpt-5.6-sol", effort: "ultra" },
|
|
28
|
+
});
|
|
29
|
+
|
|
19
30
|
function readProfileScalar(raw, key) {
|
|
20
31
|
const match = String(raw).match(
|
|
21
32
|
new RegExp(
|
|
@@ -29,7 +40,7 @@ function readProfileScalar(raw, key) {
|
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
/**
|
|
32
|
-
* Resolve a Codex effort profile name (e.g. "
|
|
43
|
+
* Resolve a Codex effort profile name (e.g. "gpt56_terra_high") to the `-c key=value`
|
|
33
44
|
* config overrides that `codex exec` accepts, so the headless CLI lanes can
|
|
34
45
|
* select a profile WITHOUT `codex exec --profile <name>`. codex 0.134+ rejects
|
|
35
46
|
* `--profile X` whenever config.toml still contains an inline `[profiles.X]`
|
|
@@ -47,10 +58,12 @@ function readProfileScalar(raw, key) {
|
|
|
47
58
|
*
|
|
48
59
|
* @param {string} profileName
|
|
49
60
|
* @param {{ codexHome?: string }} [opts]
|
|
50
|
-
* @returns {string[]} e.g. ['model="gpt-5.
|
|
61
|
+
* @returns {string[]} e.g. ['model="gpt-5.6-terra"', 'model_reasoning_effort="high"']
|
|
51
62
|
*/
|
|
52
|
-
export function
|
|
53
|
-
if (typeof profileName !== "string" || !profileName)
|
|
63
|
+
export function resolveCodexProfileConfigValues(profileName, opts = {}) {
|
|
64
|
+
if (typeof profileName !== "string" || !profileName) {
|
|
65
|
+
return { model: null, effort: null };
|
|
66
|
+
}
|
|
54
67
|
const codexHome =
|
|
55
68
|
opts.codexHome || process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
56
69
|
let model = null;
|
|
@@ -63,9 +76,22 @@ export function codexProfileConfigOverrides(profileName, opts = {}) {
|
|
|
63
76
|
model = readProfileScalar(raw, "model");
|
|
64
77
|
effort = readProfileScalar(raw, "model_reasoning_effort");
|
|
65
78
|
} catch {
|
|
66
|
-
const suffix = /_(xhigh|high|med|medium|low)$/.exec(profileName);
|
|
79
|
+
const suffix = /_(ultra|max|xhigh|high|med|medium|low)$/.exec(profileName);
|
|
67
80
|
if (suffix) effort = EFFORT_BY_SUFFIX[suffix[1]] || null;
|
|
68
81
|
}
|
|
82
|
+
return { model, effort };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function codexProfileConfigOverrides(profileName, opts = {}) {
|
|
86
|
+
let { model, effort } = resolveCodexProfileConfigValues(profileName, opts);
|
|
87
|
+
if (opts.enforceCanonicalProfile === true) {
|
|
88
|
+
const canonical = CANONICAL_PROFILE_VALUES[profileName];
|
|
89
|
+
if (canonical) ({ model, effort } = canonical);
|
|
90
|
+
}
|
|
91
|
+
if (opts.disallowUltra === true && effort?.toLowerCase() === "ultra") {
|
|
92
|
+
model = "gpt-5.6-sol";
|
|
93
|
+
effort = "max";
|
|
94
|
+
}
|
|
69
95
|
const overrides = [];
|
|
70
96
|
if (model) overrides.push(`model="${model}"`);
|
|
71
97
|
if (effort) overrides.push(`model_reasoning_effort="${effort}"`);
|
|
@@ -26,6 +26,7 @@ function normalizeState(state) {
|
|
|
26
26
|
|
|
27
27
|
function normalizeRule(rule) {
|
|
28
28
|
if (!rule || typeof rule !== "object") return null;
|
|
29
|
+
if (rule.disabled === true) return null;
|
|
29
30
|
if (typeof rule.id !== "string" || !rule.id.trim()) return null;
|
|
30
31
|
if (!Array.isArray(rule.patterns) || rule.patterns.length === 0) return null;
|
|
31
32
|
if (typeof rule.priority !== "number" || !Number.isFinite(rule.priority))
|
|
@@ -61,6 +62,13 @@ function normalizeRule(rule) {
|
|
|
61
62
|
const state = normalizeState(rule.state);
|
|
62
63
|
if (rule.state != null && state == null) return null;
|
|
63
64
|
|
|
65
|
+
// repo_scope: path segment 화이트리스트 (빈 배열 = 전역). explicit: 종결 라우팅 마커.
|
|
66
|
+
const repoScope = Array.isArray(rule.repo_scope)
|
|
67
|
+
? rule.repo_scope
|
|
68
|
+
.filter((s) => typeof s === "string" && s.trim())
|
|
69
|
+
.map((s) => s.trim())
|
|
70
|
+
: [];
|
|
71
|
+
|
|
64
72
|
return {
|
|
65
73
|
id: rule.id.trim(),
|
|
66
74
|
patterns,
|
|
@@ -72,6 +80,8 @@ function normalizeRule(rule) {
|
|
|
72
80
|
exclusive: rule.exclusive === true,
|
|
73
81
|
state,
|
|
74
82
|
mcp_route: mcpRoute,
|
|
83
|
+
repo_scope: repoScope,
|
|
84
|
+
explicit: rule.explicit === true,
|
|
75
85
|
};
|
|
76
86
|
}
|
|
77
87
|
|
|
@@ -149,6 +159,8 @@ export function matchRules(compiledRules, cleanText) {
|
|
|
149
159
|
exclusive: rule.exclusive === true,
|
|
150
160
|
state: rule.state || null,
|
|
151
161
|
mcp_route: rule.mcp_route || null,
|
|
162
|
+
repo_scope: rule.repo_scope || [],
|
|
163
|
+
explicit: rule.explicit === true,
|
|
152
164
|
});
|
|
153
165
|
}
|
|
154
166
|
|
package/cto/steward.mjs
DELETED
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
// cto/steward.mjs — collect + hygiene 주기 루프 (resident-cto-manager PRD T2 선행 슬라이스)
|
|
2
|
-
// 가드레일: TFX_CTO kill-switch 매 cycle 확인 + watch 루프는 lake당 단일 인스턴스 락.
|
|
3
|
-
import { utimesSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { isCtoDisabled } from "@triflux/core/hub/lib/cto-env.mjs";
|
|
7
|
-
import { runCollect } from "./collect.mjs";
|
|
8
|
-
import { acquireCtoHygieneStewardLock, runHygiene } from "./hygiene.mjs";
|
|
9
|
-
import { resolveLakeRootDir } from "./lake-root.mjs";
|
|
10
|
-
|
|
11
|
-
const SCHEMA_VERSION = "cto-steward.v1";
|
|
12
|
-
const DEFAULT_INTERVAL_MS = 60_000;
|
|
13
|
-
const LOOP_LOCK_STALE_MS = 10 * 60_000;
|
|
14
|
-
const KNOWN_FLAGS_WITH_VALUES = new Set(["--max-runs", "--interval-ms"]);
|
|
15
|
-
const KNOWN_BOOLEAN_FLAGS = new Set([
|
|
16
|
-
"--watch",
|
|
17
|
-
"--apply",
|
|
18
|
-
"--dry-run",
|
|
19
|
-
"--json",
|
|
20
|
-
"--no-collect",
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
|
-
function writeJson(stdout, payload) {
|
|
24
|
-
stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function parsePositiveInteger(name, value) {
|
|
28
|
-
if (value === undefined || String(value).startsWith("--")) {
|
|
29
|
-
throw new Error(`tfx cto steward ${name} requires a value`);
|
|
30
|
-
}
|
|
31
|
-
if (!/^\d+$/u.test(String(value))) {
|
|
32
|
-
throw new Error(`tfx cto steward ${name} must be a positive integer`);
|
|
33
|
-
}
|
|
34
|
-
const parsed = Number(value);
|
|
35
|
-
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
36
|
-
throw new Error(`tfx cto steward ${name} must be a positive integer`);
|
|
37
|
-
}
|
|
38
|
-
return parsed;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function parseNonNegativeInteger(name, value) {
|
|
42
|
-
if (value === undefined || String(value).startsWith("--")) {
|
|
43
|
-
throw new Error(`tfx cto steward ${name} requires a value`);
|
|
44
|
-
}
|
|
45
|
-
if (!/^\d+$/u.test(String(value))) {
|
|
46
|
-
throw new Error(`tfx cto steward ${name} must be a non-negative integer`);
|
|
47
|
-
}
|
|
48
|
-
const parsed = Number(value);
|
|
49
|
-
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
50
|
-
throw new Error(`tfx cto steward ${name} must be a non-negative integer`);
|
|
51
|
-
}
|
|
52
|
-
return parsed;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function parseStewardArgs(args = []) {
|
|
56
|
-
const parsed = {
|
|
57
|
-
watch: false,
|
|
58
|
-
maxRuns: null,
|
|
59
|
-
intervalMs: DEFAULT_INTERVAL_MS,
|
|
60
|
-
apply: false,
|
|
61
|
-
dryRun: false,
|
|
62
|
-
json: false,
|
|
63
|
-
collectEnabled: true,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
67
|
-
const arg = args[index];
|
|
68
|
-
if (KNOWN_FLAGS_WITH_VALUES.has(arg)) {
|
|
69
|
-
const value = args[index + 1];
|
|
70
|
-
if (arg === "--max-runs")
|
|
71
|
-
parsed.maxRuns = parsePositiveInteger(arg, value);
|
|
72
|
-
else parsed.intervalMs = parseNonNegativeInteger(arg, value);
|
|
73
|
-
index += 1;
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
if (KNOWN_BOOLEAN_FLAGS.has(arg)) {
|
|
77
|
-
if (arg === "--watch") parsed.watch = true;
|
|
78
|
-
else if (arg === "--apply") parsed.apply = true;
|
|
79
|
-
else if (arg === "--dry-run") parsed.dryRun = true;
|
|
80
|
-
else if (arg === "--json") parsed.json = true;
|
|
81
|
-
else if (arg === "--no-collect") parsed.collectEnabled = false;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
throw new Error(`unknown tfx cto steward flag: ${arg}`);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (parsed.apply && parsed.dryRun) {
|
|
88
|
-
throw new Error("tfx cto steward accepts only one of --apply or --dry-run");
|
|
89
|
-
}
|
|
90
|
-
return parsed;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function createSilentStdout() {
|
|
94
|
-
return {
|
|
95
|
-
write() {
|
|
96
|
-
return true;
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function compactRun(index, collectResult, hygieneResult) {
|
|
102
|
-
return {
|
|
103
|
-
index,
|
|
104
|
-
collected: collectResult !== null,
|
|
105
|
-
collect: collectResult,
|
|
106
|
-
hygiene: hygieneResult,
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function isAbortError(error) {
|
|
111
|
-
return error?.name === "AbortError" || error?.code === "ABORT_ERR";
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function stewardLoopLockPath(lakeRoot) {
|
|
115
|
-
return join(lakeRoot, "stewards", "steward-loop.lock");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export async function runSteward(args = [], opts = {}) {
|
|
119
|
-
const parsed = parseStewardArgs(args);
|
|
120
|
-
const jsonRequested = parsed.json || opts.json === true;
|
|
121
|
-
if (parsed.watch && parsed.maxRuns === null && jsonRequested) {
|
|
122
|
-
throw new Error(
|
|
123
|
-
"tfx cto steward --watch --json requires --max-runs for a finite JSON summary",
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
if (!parsed.watch && parsed.maxRuns !== null) {
|
|
127
|
-
(opts.stderr || process.stderr).write(
|
|
128
|
-
"tfx cto steward: --max-runs is ignored without --watch\n",
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// steward 전용 opts 는 여기서 걸러내고, 나머지(overlay, synapseReader 등)만
|
|
133
|
-
// collect/hygiene 로 전달한다.
|
|
134
|
-
const {
|
|
135
|
-
collectFn: injectedCollectFn,
|
|
136
|
-
hygieneFn: injectedHygieneFn,
|
|
137
|
-
sleepFn: injectedSleepFn,
|
|
138
|
-
onRun,
|
|
139
|
-
json: _jsonOpt,
|
|
140
|
-
stdout: injectedStdout,
|
|
141
|
-
stderr: _stderrOpt,
|
|
142
|
-
signal: externalSignal,
|
|
143
|
-
isCtoDisabledFn: injectedGate,
|
|
144
|
-
acquireLoopLockFn: injectedAcquireLoopLock,
|
|
145
|
-
installSignalHandler,
|
|
146
|
-
rootDir: _rootDirOpt,
|
|
147
|
-
lakeRoot: _lakeRootOpt,
|
|
148
|
-
...innerOpts
|
|
149
|
-
} = opts;
|
|
150
|
-
|
|
151
|
-
const rootDir = opts.rootDir || resolveLakeRootDir(process.cwd());
|
|
152
|
-
const lakeRoot = opts.lakeRoot || join(rootDir, ".triflux", "lake");
|
|
153
|
-
const stdout = injectedStdout || process.stdout;
|
|
154
|
-
const collectFn = injectedCollectFn || runCollect;
|
|
155
|
-
const hygieneFn = injectedHygieneFn || runHygiene;
|
|
156
|
-
const ctoDisabled = injectedGate || isCtoDisabled;
|
|
157
|
-
|
|
158
|
-
const controller = new AbortController();
|
|
159
|
-
const signal = controller.signal;
|
|
160
|
-
if (externalSignal) {
|
|
161
|
-
if (externalSignal.aborted) controller.abort();
|
|
162
|
-
else
|
|
163
|
-
externalSignal.addEventListener("abort", () => controller.abort(), {
|
|
164
|
-
once: true,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const sleepFn =
|
|
169
|
-
injectedSleepFn ||
|
|
170
|
-
((ms) =>
|
|
171
|
-
new Promise((resolve) => {
|
|
172
|
-
if (signal.aborted) {
|
|
173
|
-
resolve();
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
const timer = setTimeout(resolve, ms);
|
|
177
|
-
signal.addEventListener(
|
|
178
|
-
"abort",
|
|
179
|
-
() => {
|
|
180
|
-
clearTimeout(timer);
|
|
181
|
-
resolve();
|
|
182
|
-
},
|
|
183
|
-
{ once: true },
|
|
184
|
-
);
|
|
185
|
-
}));
|
|
186
|
-
|
|
187
|
-
const mode = parsed.apply ? "apply" : "dry-run";
|
|
188
|
-
const silentStdout = createSilentStdout();
|
|
189
|
-
const runLimit = parsed.watch ? parsed.maxRuns : 1;
|
|
190
|
-
const retainRuns = runLimit !== null;
|
|
191
|
-
const runs = [];
|
|
192
|
-
let latestRun = null;
|
|
193
|
-
let lastCollect = null;
|
|
194
|
-
let lastHygiene = null;
|
|
195
|
-
let runCount = 0;
|
|
196
|
-
let stoppedReason = null;
|
|
197
|
-
|
|
198
|
-
// 가드레일 1 (PRD kill-switch): 루프 진입 전에도, 매 cycle에도 확인한다.
|
|
199
|
-
if (ctoDisabled()) stoppedReason = "disabled";
|
|
200
|
-
|
|
201
|
-
// 가드레일 2 (PRD single-instance): watch 루프는 lake당 하나만 돈다.
|
|
202
|
-
const loopLockPath = stewardLoopLockPath(lakeRoot);
|
|
203
|
-
const loopLockStaleMs = Math.max(LOOP_LOCK_STALE_MS, parsed.intervalMs * 3);
|
|
204
|
-
let loopLock = null;
|
|
205
|
-
let loopLockHeartbeat = null;
|
|
206
|
-
if (!stoppedReason && parsed.watch) {
|
|
207
|
-
const acquireLock =
|
|
208
|
-
injectedAcquireLoopLock ||
|
|
209
|
-
((lockPath) =>
|
|
210
|
-
acquireCtoHygieneStewardLock(rootDir, lakeRoot, {
|
|
211
|
-
lockPath,
|
|
212
|
-
timeoutMs: 0,
|
|
213
|
-
staleMs: loopLockStaleMs,
|
|
214
|
-
}));
|
|
215
|
-
loopLock = await acquireLock(loopLockPath);
|
|
216
|
-
// 한 cycle이 staleMs보다 길어져도 다른 인스턴스가 락을 stale로 오판해
|
|
217
|
-
// 훔치지 못하도록, 작업 진행 중에도 주기적으로 mtime을 갱신한다.
|
|
218
|
-
if (loopLock?.path) {
|
|
219
|
-
const heartbeatMs = Math.max(30_000, Math.floor(loopLockStaleMs / 3));
|
|
220
|
-
loopLockHeartbeat = setInterval(() => {
|
|
221
|
-
try {
|
|
222
|
-
const touch = new Date();
|
|
223
|
-
utimesSync(loopLock.path, touch, touch);
|
|
224
|
-
} catch {}
|
|
225
|
-
}, heartbeatMs);
|
|
226
|
-
loopLockHeartbeat.unref?.();
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
let sigintHandler = null;
|
|
231
|
-
if (!stoppedReason && parsed.watch && installSignalHandler !== false) {
|
|
232
|
-
sigintHandler = () => controller.abort();
|
|
233
|
-
process.once("SIGINT", sigintHandler);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
while (!stoppedReason && (runLimit === null || runCount < runLimit)) {
|
|
238
|
-
if (ctoDisabled()) {
|
|
239
|
-
stoppedReason = "disabled";
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
if (signal.aborted) {
|
|
243
|
-
stoppedReason = "signal";
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const runIndex = runCount + 1;
|
|
248
|
-
let collectResult = null;
|
|
249
|
-
|
|
250
|
-
if (parsed.collectEnabled) {
|
|
251
|
-
collectResult = await collectFn([], {
|
|
252
|
-
...innerOpts,
|
|
253
|
-
rootDir,
|
|
254
|
-
lakeRoot,
|
|
255
|
-
stdout: silentStdout,
|
|
256
|
-
});
|
|
257
|
-
lastCollect = collectResult;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// collect 도중 중단/kill-switch가 들어왔으면 이번 cycle의 hygiene(특히
|
|
261
|
-
// apply)을 시작하지 않는다 — 중단 요청 이후의 fs-mutation 금지.
|
|
262
|
-
if (ctoDisabled()) {
|
|
263
|
-
stoppedReason = "disabled";
|
|
264
|
-
break;
|
|
265
|
-
}
|
|
266
|
-
if (signal.aborted) {
|
|
267
|
-
stoppedReason = "signal";
|
|
268
|
-
break;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const hygieneArgs = parsed.apply
|
|
272
|
-
? ["--apply", "--json"]
|
|
273
|
-
: ["--dry-run", "--json"];
|
|
274
|
-
const hygieneResult = await hygieneFn(hygieneArgs, {
|
|
275
|
-
...innerOpts,
|
|
276
|
-
rootDir,
|
|
277
|
-
lakeRoot,
|
|
278
|
-
stdout: silentStdout,
|
|
279
|
-
apply: parsed.apply,
|
|
280
|
-
dryRun: !parsed.apply,
|
|
281
|
-
json: true,
|
|
282
|
-
});
|
|
283
|
-
lastHygiene = hygieneResult;
|
|
284
|
-
|
|
285
|
-
const run = compactRun(runIndex, collectResult, hygieneResult);
|
|
286
|
-
latestRun = run;
|
|
287
|
-
if (retainRuns) runs.push(run);
|
|
288
|
-
runCount += 1;
|
|
289
|
-
|
|
290
|
-
if (parsed.watch && !jsonRequested) {
|
|
291
|
-
stdout.write(
|
|
292
|
-
`cto steward run ${runIndex} (${mode}): collect=${
|
|
293
|
-
collectResult !== null ? "ok" : "skipped"
|
|
294
|
-
} hygiene=ok\n`,
|
|
295
|
-
);
|
|
296
|
-
}
|
|
297
|
-
if (typeof onRun === "function") {
|
|
298
|
-
await onRun(run, {
|
|
299
|
-
run_count: runCount,
|
|
300
|
-
rootDir,
|
|
301
|
-
lakeRoot,
|
|
302
|
-
mode,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (!parsed.watch) break;
|
|
307
|
-
if (runLimit !== null && runCount >= runLimit) break;
|
|
308
|
-
try {
|
|
309
|
-
await sleepFn(parsed.intervalMs);
|
|
310
|
-
} catch (error) {
|
|
311
|
-
if (isAbortError(error)) {
|
|
312
|
-
stoppedReason = "signal";
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
throw error;
|
|
316
|
-
}
|
|
317
|
-
if (signal.aborted) {
|
|
318
|
-
stoppedReason = "signal";
|
|
319
|
-
break;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
} finally {
|
|
323
|
-
if (sigintHandler) process.off("SIGINT", sigintHandler);
|
|
324
|
-
if (loopLockHeartbeat) clearInterval(loopLockHeartbeat);
|
|
325
|
-
loopLock?.release?.();
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (!stoppedReason) stoppedReason = parsed.watch ? "max-runs" : "single-run";
|
|
329
|
-
|
|
330
|
-
const summary = {
|
|
331
|
-
schema_version: SCHEMA_VERSION,
|
|
332
|
-
mode,
|
|
333
|
-
watch: parsed.watch,
|
|
334
|
-
interval_ms: parsed.intervalMs,
|
|
335
|
-
max_runs: parsed.maxRuns,
|
|
336
|
-
collect_enabled: parsed.collectEnabled,
|
|
337
|
-
disabled: stoppedReason === "disabled",
|
|
338
|
-
stopped_reason: stoppedReason,
|
|
339
|
-
loop_lock_path: parsed.watch ? loopLockPath : null,
|
|
340
|
-
run_count: runCount,
|
|
341
|
-
retained_run_count: runs.length,
|
|
342
|
-
runs,
|
|
343
|
-
latest_run: latestRun,
|
|
344
|
-
last_collect: lastCollect,
|
|
345
|
-
last_hygiene: lastHygiene,
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
if (jsonRequested) writeJson(stdout, summary);
|
|
349
|
-
else if (stoppedReason === "disabled") {
|
|
350
|
-
stdout.write(
|
|
351
|
-
`cto steward disabled by TFX_CTO kill-switch after ${runCount} run(s)\n`,
|
|
352
|
-
);
|
|
353
|
-
} else {
|
|
354
|
-
stdout.write(
|
|
355
|
-
`cto steward ${mode}: ${runCount} run(s), collect ${
|
|
356
|
-
parsed.collectEnabled ? "enabled" : "disabled"
|
|
357
|
-
}, stopped=${stoppedReason}\n`,
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return summary;
|
|
362
|
-
}
|