triflux 10.20.0 → 10.20.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +34 -42
- package/README.md +30 -36
- package/bin/triflux.mjs +13 -4
- package/hooks/hook-orchestrator.mjs +1 -1
- package/hooks/hook-registry.json +76 -2
- package/hooks/hooks.json +49 -1
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +355 -16
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/session-end-cleanup.mjs +201 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +4 -0
- package/hub/account-broker.mjs +297 -8
- package/hub/codex-adapter.mjs +62 -2
- package/hub/server.mjs +51 -4
- package/hub/team/cli/services/native-control.mjs +1 -0
- package/hub/team/conductor.mjs +94 -29
- package/hub/team/native-supervisor.mjs +20 -7
- package/hub/team/swarm-cli.mjs +98 -4
- package/hub/team/swarm-hypervisor.mjs +112 -10
- package/hub/team/swarm-locks.mjs +7 -2
- package/hub/team/swarm-preflight.mjs +317 -0
- package/hub/team/synapse-cli.mjs +243 -6
- package/hub/team/worker-sandbox.mjs +93 -0
- package/hub/team/worktree-lifecycle.mjs +49 -8
- package/hub/workers/delegator-mcp.mjs +14 -1
- package/package.json +1 -1
- package/scripts/__tests__/gen-skill-docs.test.mjs +4 -0
- package/scripts/__tests__/skill-surface.test.mjs +61 -0
- package/scripts/gen-skill-manifest.mjs +3 -0
- package/scripts/lib/skill-template.mjs +2 -0
- package/scripts/session-stale-cleanup.mjs +111 -2
- package/skills/star-prompt/skill.json +1 -1
- package/skills/tfx-analysis/skill.json +11 -3
- package/skills/tfx-auto/skill.json +2 -2
- package/skills/tfx-autopilot/skill.json +9 -4
- package/skills/tfx-consensus/skill.json +4 -3
- package/skills/tfx-debate/skill.json +11 -4
- package/skills/tfx-doctor/skill.json +3 -1
- package/skills/tfx-fullcycle/skill.json +10 -4
- package/skills/tfx-hooks/skill.json +3 -1
- package/skills/tfx-hub/skill.json +5 -3
- package/skills/tfx-index/skill.json +6 -1
- package/skills/tfx-interview/skill.json +6 -1
- package/skills/tfx-multi/skill.json +7 -3
- package/skills/tfx-panel/skill.json +11 -4
- package/skills/tfx-persist/skill.json +11 -4
- package/skills/tfx-plan/skill.json +13 -3
- package/skills/tfx-profile/skill.json +3 -1
- package/skills/tfx-prune/skill.json +7 -1
- package/skills/tfx-psmux-rules/SKILL.md +2 -0
- package/skills/tfx-psmux-rules/SKILL.md.tmpl +22 -307
- package/skills/tfx-psmux-rules/skill.json +7 -2
- package/skills/tfx-qa/skill.json +12 -3
- package/skills/tfx-ralph/skill.json +4 -1
- package/skills/tfx-remote/skill.json +8 -0
- package/skills/tfx-remote-setup/SKILL.md +2 -0
- package/skills/tfx-remote-setup/SKILL.md.tmpl +23 -570
- package/skills/tfx-remote-setup/skill.json +7 -3
- package/skills/tfx-remote-spawn/SKILL.md +2 -0
- package/skills/tfx-remote-spawn/SKILL.md.tmpl +31 -242
- package/skills/tfx-remote-spawn/skill.json +7 -3
- package/skills/tfx-research/skill.json +11 -4
- package/skills/tfx-review/skill.json +12 -3
- package/skills/tfx-setup/skill.json +3 -1
- package/skills/tfx-ship/skill.json +13 -0
- package/skills/tfx-swarm/skill.json +13 -0
- package/skills/tfx-wt/skill.json +12 -0
package/hub/team/conductor.mjs
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
registerSynapseSession,
|
|
46
46
|
unregisterSynapseSession,
|
|
47
47
|
} from "./synapse-http.mjs";
|
|
48
|
+
import { buildWorkerSandboxEnv } from "./worker-sandbox.mjs";
|
|
48
49
|
|
|
49
50
|
/** 세션 상태 */
|
|
50
51
|
export const STATES = Object.freeze({
|
|
@@ -87,16 +88,24 @@ const DEFAULT_GRACE_MS = 10_000;
|
|
|
87
88
|
* @param {'codex'|'gemini'} agent
|
|
88
89
|
* @param {string} sessionId — 로깅용
|
|
89
90
|
* @param {object} eventLog — createEventLog 인스턴스
|
|
91
|
+
* @param {object} brokerInstance — AccountBroker 인스턴스
|
|
90
92
|
*/
|
|
91
|
-
function swapAuthFile(
|
|
93
|
+
function swapAuthFile(
|
|
94
|
+
lease,
|
|
95
|
+
agent,
|
|
96
|
+
sessionId,
|
|
97
|
+
eventLog,
|
|
98
|
+
brokerInstance,
|
|
99
|
+
targetEnv = {},
|
|
100
|
+
) {
|
|
92
101
|
if (lease?.mode !== "auth" || !lease.authFile) return true;
|
|
93
102
|
if (
|
|
94
103
|
agent === "codex" &&
|
|
95
104
|
lease.id &&
|
|
96
|
-
|
|
97
|
-
typeof
|
|
105
|
+
brokerInstance &&
|
|
106
|
+
typeof brokerInstance.syncAuthToSource === "function"
|
|
98
107
|
) {
|
|
99
|
-
const result =
|
|
108
|
+
const result = brokerInstance.syncAuthToSource(lease.id);
|
|
100
109
|
if (result?.copied || result?.reason === "up_to_date") {
|
|
101
110
|
eventLog.append("auth_copy", {
|
|
102
111
|
session: sessionId,
|
|
@@ -113,12 +122,14 @@ function swapAuthFile(lease, agent, sessionId, eventLog) {
|
|
|
113
122
|
});
|
|
114
123
|
return false;
|
|
115
124
|
}
|
|
125
|
+
const targetHome = targetEnv.HOME || homedir();
|
|
126
|
+
const codexHome = targetEnv.CODEX_HOME || join(targetHome, ".codex");
|
|
116
127
|
const dests =
|
|
117
128
|
agent === "codex"
|
|
118
|
-
? [join(
|
|
129
|
+
? [join(codexHome, "auth.json")]
|
|
119
130
|
: [
|
|
120
|
-
join(
|
|
121
|
-
join(
|
|
131
|
+
join(targetHome, ".gemini", "oauth_creds.json"),
|
|
132
|
+
join(targetHome, ".gemini", "gemini-credentials.json"),
|
|
122
133
|
];
|
|
123
134
|
let allOk = true;
|
|
124
135
|
for (const dest of dests) {
|
|
@@ -182,6 +193,7 @@ export function createConductor(opts = {}) {
|
|
|
182
193
|
baseUrl: opts.synapseBaseUrl,
|
|
183
194
|
fetchImpl: opts.synapseFetch,
|
|
184
195
|
};
|
|
196
|
+
const getActiveBroker = () => optsBroker ?? broker;
|
|
185
197
|
|
|
186
198
|
function buildSynapseMeta(session, state = session.state, reason = "") {
|
|
187
199
|
return {
|
|
@@ -486,13 +498,14 @@ export function createConductor(opts = {}) {
|
|
|
486
498
|
emitter.emit("dead", { sessionId: session.id, reason });
|
|
487
499
|
|
|
488
500
|
// broker release on final death
|
|
489
|
-
|
|
490
|
-
|
|
501
|
+
const activeBroker = getActiveBroker();
|
|
502
|
+
if (activeBroker && session.config.accountId) {
|
|
503
|
+
activeBroker.release(session.config.accountId, {
|
|
491
504
|
ok: false,
|
|
492
505
|
failureMode: session.lastFailureMode,
|
|
493
506
|
});
|
|
494
507
|
if (session.lastFailureMode === "rate_limited") {
|
|
495
|
-
|
|
508
|
+
activeBroker.markRateLimited(session.config.accountId, 5 * 60 * 1000);
|
|
496
509
|
}
|
|
497
510
|
}
|
|
498
511
|
}
|
|
@@ -568,19 +581,26 @@ export function createConductor(opts = {}) {
|
|
|
568
581
|
}
|
|
569
582
|
}
|
|
570
583
|
|
|
584
|
+
const spawnEnv = {
|
|
585
|
+
...process.env,
|
|
586
|
+
...launcher.env,
|
|
587
|
+
...(session.config.env || {}),
|
|
588
|
+
...(session.config.branchGuard ? { TRIFLUX_GIT_BRANCH_GUARD: "1" } : {}),
|
|
589
|
+
};
|
|
590
|
+
if (session.config.workerSandbox?.root) {
|
|
591
|
+
eventLog.append("worker_sandbox_env", {
|
|
592
|
+
session: session.id,
|
|
593
|
+
root: session.config.workerSandbox.root,
|
|
594
|
+
codeHomePreserved: Boolean(spawnEnv.CODEX_HOME),
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
571
598
|
let child;
|
|
572
599
|
try {
|
|
573
600
|
child = spawnFn(spawnSpec.command, spawnSpec.args, {
|
|
574
601
|
shell: false,
|
|
575
602
|
cwd: spawnCwd,
|
|
576
|
-
env:
|
|
577
|
-
...process.env,
|
|
578
|
-
...launcher.env,
|
|
579
|
-
...(session.config.env || {}),
|
|
580
|
-
...(session.config.branchGuard
|
|
581
|
-
? { TRIFLUX_GIT_BRANCH_GUARD: "1" }
|
|
582
|
-
: {}),
|
|
583
|
-
},
|
|
603
|
+
env: spawnEnv,
|
|
584
604
|
reason: `conductor:respawnSession:${session.id}`,
|
|
585
605
|
stdio: ["pipe", "pipe", "pipe"],
|
|
586
606
|
windowsHide: true,
|
|
@@ -699,8 +719,9 @@ export function createConductor(opts = {}) {
|
|
|
699
719
|
completionPayload,
|
|
700
720
|
});
|
|
701
721
|
}
|
|
702
|
-
|
|
703
|
-
|
|
722
|
+
const activeBroker = getActiveBroker();
|
|
723
|
+
if (activeBroker && session.config.accountId) {
|
|
724
|
+
activeBroker.release(session.config.accountId, { ok: true });
|
|
704
725
|
}
|
|
705
726
|
} else {
|
|
706
727
|
// detect rate_limited from recent output before handleFailure
|
|
@@ -1001,10 +1022,11 @@ export function createConductor(opts = {}) {
|
|
|
1001
1022
|
|
|
1002
1023
|
// broker lease (graceful — broker null if accounts.json absent)
|
|
1003
1024
|
let lease = null;
|
|
1004
|
-
|
|
1005
|
-
|
|
1025
|
+
const activeBroker = getActiveBroker();
|
|
1026
|
+
if (activeBroker && config.agent && !config.remote) {
|
|
1027
|
+
lease = activeBroker.lease({ provider: config.agent });
|
|
1006
1028
|
if (lease === null) {
|
|
1007
|
-
const eta =
|
|
1029
|
+
const eta = activeBroker.nextAvailableEta(config.agent);
|
|
1008
1030
|
eventLog.append("broker_no_lease", {
|
|
1009
1031
|
session: config.id,
|
|
1010
1032
|
agent: config.agent,
|
|
@@ -1016,17 +1038,47 @@ export function createConductor(opts = {}) {
|
|
|
1016
1038
|
}
|
|
1017
1039
|
|
|
1018
1040
|
// apply lease profile/env/auth to config (immutable — new object)
|
|
1019
|
-
const
|
|
1041
|
+
const leaseEnv =
|
|
1042
|
+
lease?.authFile && config.agent === "codex"
|
|
1043
|
+
? { ...(lease.env || {}), CODEX_HOME: dirname(lease.authFile) }
|
|
1044
|
+
: lease?.env || {};
|
|
1045
|
+
|
|
1046
|
+
let resolvedConfig = lease
|
|
1020
1047
|
? {
|
|
1021
1048
|
...config,
|
|
1022
1049
|
profile: lease.profile ?? config.profile,
|
|
1023
|
-
env: { ...(config.env || {}), ...
|
|
1050
|
+
env: { ...(config.env || {}), ...leaseEnv },
|
|
1024
1051
|
accountId: lease.id,
|
|
1025
1052
|
}
|
|
1026
|
-
:
|
|
1053
|
+
: {
|
|
1054
|
+
...config,
|
|
1055
|
+
env: { ...(config.env || {}) },
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
if (!resolvedConfig.remote) {
|
|
1059
|
+
const sandbox = buildWorkerSandboxEnv({
|
|
1060
|
+
cwd: resolvedConfig.workdir || process.cwd(),
|
|
1061
|
+
sessionId: resolvedConfig.id,
|
|
1062
|
+
env: { ...process.env, ...(resolvedConfig.env || {}) },
|
|
1063
|
+
});
|
|
1064
|
+
resolvedConfig = {
|
|
1065
|
+
...resolvedConfig,
|
|
1066
|
+
env: { ...(resolvedConfig.env || {}), ...sandbox.env },
|
|
1067
|
+
workerSandbox: sandbox.disabled
|
|
1068
|
+
? { disabled: true }
|
|
1069
|
+
: { root: sandbox.root, home: sandbox.home },
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1027
1072
|
|
|
1028
1073
|
// auth file copy — broker resolved absolute path, conductor does the actual copy
|
|
1029
|
-
swapAuthFile(
|
|
1074
|
+
swapAuthFile(
|
|
1075
|
+
lease,
|
|
1076
|
+
config.agent,
|
|
1077
|
+
config.id,
|
|
1078
|
+
eventLog,
|
|
1079
|
+
activeBroker,
|
|
1080
|
+
resolvedConfig.env,
|
|
1081
|
+
);
|
|
1030
1082
|
|
|
1031
1083
|
// 원격 세션은 launcher 불필요 (이미 원격에서 실행 중)
|
|
1032
1084
|
const launcher = resolvedConfig.remote
|
|
@@ -1188,8 +1240,21 @@ export function createConductor(opts = {}) {
|
|
|
1188
1240
|
|
|
1189
1241
|
const newLease = brokerInstance.lease({ provider });
|
|
1190
1242
|
if (!newLease) continue;
|
|
1191
|
-
|
|
1192
|
-
|
|
1243
|
+
if (newLease.authFile && provider === "codex") {
|
|
1244
|
+
session.config.env = {
|
|
1245
|
+
...(session.config.env || {}),
|
|
1246
|
+
CODEX_HOME: dirname(newLease.authFile),
|
|
1247
|
+
};
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
swapAuthFile(
|
|
1251
|
+
newLease,
|
|
1252
|
+
provider,
|
|
1253
|
+
session.id,
|
|
1254
|
+
eventLog,
|
|
1255
|
+
brokerInstance,
|
|
1256
|
+
session.config.env,
|
|
1257
|
+
);
|
|
1193
1258
|
eventLog.append("tier_fallback_swap", {
|
|
1194
1259
|
session: session.id,
|
|
1195
1260
|
agent: provider,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../lib/bash-path.mjs";
|
|
16
16
|
import { verifySlimWrapperRouteExecution } from "./native.mjs";
|
|
17
17
|
import { forceCleanupTeam } from "./nativeProxy.mjs";
|
|
18
|
+
import { buildWorkerSandboxEnv } from "./worker-sandbox.mjs";
|
|
18
19
|
|
|
19
20
|
const ROUTE_LOG_TAIL_BYTES = 65536;
|
|
20
21
|
|
|
@@ -116,6 +117,7 @@ function memberStateSnapshot() {
|
|
|
116
117
|
routeVerification: state?.routeVerification || null,
|
|
117
118
|
logFile: state?.logFile || null,
|
|
118
119
|
errFile: state?.errFile || null,
|
|
120
|
+
workerSandboxRoot: state?.workerSandboxRoot || null,
|
|
119
121
|
});
|
|
120
122
|
}
|
|
121
123
|
return states;
|
|
@@ -164,15 +166,25 @@ function spawnMember(member) {
|
|
|
164
166
|
const outWs = createWriteStream(outPath, { flags: "a" });
|
|
165
167
|
const errWs = createWriteStream(errPath, { flags: "a" });
|
|
166
168
|
|
|
169
|
+
const baseEnv = {
|
|
170
|
+
...process.env,
|
|
171
|
+
TERM:
|
|
172
|
+
process.env.TERM && process.env.TERM !== "dumb"
|
|
173
|
+
? process.env.TERM
|
|
174
|
+
: "xterm-256color",
|
|
175
|
+
};
|
|
176
|
+
const sandbox =
|
|
177
|
+
member.role === "worker"
|
|
178
|
+
? buildWorkerSandboxEnv({
|
|
179
|
+
cwd: config.workdir || process.cwd(),
|
|
180
|
+
sessionId: `${sessionName}-${member.name}`,
|
|
181
|
+
env: baseEnv,
|
|
182
|
+
})
|
|
183
|
+
: { env: {}, root: null };
|
|
184
|
+
|
|
167
185
|
const child = spawn(command, {
|
|
168
186
|
shell: true,
|
|
169
|
-
env: {
|
|
170
|
-
...process.env,
|
|
171
|
-
TERM:
|
|
172
|
-
process.env.TERM && process.env.TERM !== "dumb"
|
|
173
|
-
? process.env.TERM
|
|
174
|
-
: "xterm-256color",
|
|
175
|
-
},
|
|
187
|
+
env: { ...baseEnv, ...sandbox.env },
|
|
176
188
|
stdio: ["pipe", "pipe", "pipe"],
|
|
177
189
|
windowsHide: true,
|
|
178
190
|
});
|
|
@@ -187,6 +199,7 @@ function spawnMember(member) {
|
|
|
187
199
|
status: "running",
|
|
188
200
|
exitCode: null,
|
|
189
201
|
lastPreview: "",
|
|
202
|
+
workerSandboxRoot: sandbox.root || null,
|
|
190
203
|
};
|
|
191
204
|
|
|
192
205
|
child.stdout.on("data", (buf) => {
|
package/hub/team/swarm-cli.mjs
CHANGED
|
@@ -5,6 +5,10 @@ import { existsSync } from "node:fs";
|
|
|
5
5
|
import { join, resolve } from "node:path";
|
|
6
6
|
import { createSwarmHypervisor } from "./swarm-hypervisor.mjs";
|
|
7
7
|
import { planSwarm } from "./swarm-planner.mjs";
|
|
8
|
+
import {
|
|
9
|
+
formatPreflightReport,
|
|
10
|
+
runSwarmPreflight,
|
|
11
|
+
} from "./swarm-preflight.mjs";
|
|
8
12
|
|
|
9
13
|
const RESET = "\u001b[0m";
|
|
10
14
|
const BOLD = "\u001b[1m";
|
|
@@ -150,10 +154,44 @@ function printPlan(plan) {
|
|
|
150
154
|
}
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
function formatCell(value, width) {
|
|
158
|
+
const text = value == null || value === "" ? "-" : String(value);
|
|
159
|
+
return text.padEnd(width).slice(0, width);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function formatIntegrationReport(report = []) {
|
|
163
|
+
const rows = Array.isArray(report) ? report : [];
|
|
164
|
+
const lines = [
|
|
165
|
+
"=== INTEGRATION REPORT ===",
|
|
166
|
+
"SHARD STRATEGY COMMITS FINAL_SHA NOTES",
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
let integrated = 0;
|
|
170
|
+
let recoveryPatches = 0;
|
|
171
|
+
for (const row of rows) {
|
|
172
|
+
if (row.strategy !== "failed") integrated += 1;
|
|
173
|
+
if (row.recoveryPatch) recoveryPatches += 1;
|
|
174
|
+
const finalSha = row.finalSha ? String(row.finalSha).slice(0, 7) : "-";
|
|
175
|
+
const notes = row.recoveryPatch
|
|
176
|
+
? `${row.notes || "failed"}; recovery: ${row.recoveryPatch}`
|
|
177
|
+
: row.notes || "-";
|
|
178
|
+
lines.push(
|
|
179
|
+
`${formatCell(row.shard, 9)} ${formatCell(row.strategy, 14)} ${formatCell(row.commits, 8)} ${formatCell(finalSha, 10)} ${notes}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const total = rows.length;
|
|
184
|
+
lines.push("");
|
|
185
|
+
lines.push(
|
|
186
|
+
`TOTAL: ${integrated}/${total} integrated, ${recoveryPatches} recovery patch${recoveryPatches === 1 ? "" : "es"} saved.`,
|
|
187
|
+
);
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
153
191
|
/**
|
|
154
192
|
* tfx swarm <prd-path> — PRD 실행
|
|
155
193
|
*/
|
|
156
|
-
export async function cmdSwarmRun(args, { json = false } = {}) {
|
|
194
|
+
export async function cmdSwarmRun(args, { json = false, deps = {} } = {}) {
|
|
157
195
|
const { flags, positional } = parseFlags(args);
|
|
158
196
|
flags.json = flags.json || json;
|
|
159
197
|
|
|
@@ -168,9 +206,10 @@ export async function cmdSwarmRun(args, { json = false } = {}) {
|
|
|
168
206
|
throw new Error(`PRD file not found: ${absPrd}`);
|
|
169
207
|
}
|
|
170
208
|
|
|
171
|
-
const plan = planSwarm(absPrd, { repoRoot: process.cwd() });
|
|
172
|
-
|
|
173
209
|
if (flags.dryRun) {
|
|
210
|
+
const plan = (deps.planSwarm || planSwarm)(absPrd, {
|
|
211
|
+
repoRoot: process.cwd(),
|
|
212
|
+
});
|
|
174
213
|
if (flags.json) {
|
|
175
214
|
process.stdout.write(JSON.stringify(planToJson(plan), null, 2) + "\n");
|
|
176
215
|
} else {
|
|
@@ -179,6 +218,24 @@ export async function cmdSwarmRun(args, { json = false } = {}) {
|
|
|
179
218
|
return;
|
|
180
219
|
}
|
|
181
220
|
|
|
221
|
+
const preflight = (deps.runSwarmPreflight || runSwarmPreflight)(absPrd, {
|
|
222
|
+
repoRoot: process.cwd(),
|
|
223
|
+
deps,
|
|
224
|
+
});
|
|
225
|
+
if (!preflight.ok) {
|
|
226
|
+
if (flags.json) {
|
|
227
|
+
process.stdout.write(JSON.stringify(preflight, null, 2) + "\n");
|
|
228
|
+
} else {
|
|
229
|
+
process.stderr.write(formatPreflightReport(preflight));
|
|
230
|
+
}
|
|
231
|
+
process.exitCode = 1;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const plan = (deps.planSwarm || planSwarm)(absPrd, {
|
|
236
|
+
repoRoot: process.cwd(),
|
|
237
|
+
});
|
|
238
|
+
|
|
182
239
|
const ttyGate = assertTtyForSwarm();
|
|
183
240
|
for (const w of ttyGate.warnings) {
|
|
184
241
|
console.error(` ${YELLOW}⚠${RESET} ${w}`);
|
|
@@ -190,7 +247,7 @@ export async function cmdSwarmRun(args, { json = false } = {}) {
|
|
|
190
247
|
const logsDir =
|
|
191
248
|
flags.logsDir ||
|
|
192
249
|
join(process.cwd(), ".triflux", "swarm-logs", `run-${Date.now()}`);
|
|
193
|
-
const hyper = createSwarmHypervisor({
|
|
250
|
+
const hyper = (deps.createSwarmHypervisor || createSwarmHypervisor)({
|
|
194
251
|
workdir: process.cwd(),
|
|
195
252
|
logsDir,
|
|
196
253
|
maxRestarts: flags.maxRestarts,
|
|
@@ -257,6 +314,10 @@ export async function cmdSwarmRun(args, { json = false } = {}) {
|
|
|
257
314
|
);
|
|
258
315
|
}
|
|
259
316
|
|
|
317
|
+
if (Array.isArray(ip.report) && ip.report.length > 0) {
|
|
318
|
+
console.log(`\n${formatIntegrationReport(ip.report)}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
260
321
|
if (flags.json) {
|
|
261
322
|
process.stdout.write(JSON.stringify(status, null, 2) + "\n");
|
|
262
323
|
}
|
|
@@ -272,6 +333,39 @@ export async function cmdSwarmPlan(args, { json = false } = {}) {
|
|
|
272
333
|
return cmdSwarmRun(["--dry-run", ...args], { json });
|
|
273
334
|
}
|
|
274
335
|
|
|
336
|
+
/**
|
|
337
|
+
* tfx swarm preflight <prd-path> — PRD 실행 전 no-go 리포트 출력
|
|
338
|
+
*/
|
|
339
|
+
export async function cmdSwarmPreflight(
|
|
340
|
+
args,
|
|
341
|
+
{ json = false, deps = {} } = {},
|
|
342
|
+
) {
|
|
343
|
+
const { flags, positional } = parseFlags(args);
|
|
344
|
+
flags.json = flags.json || json;
|
|
345
|
+
|
|
346
|
+
const prdPath = positional[0];
|
|
347
|
+
if (!prdPath) {
|
|
348
|
+
throw new Error(
|
|
349
|
+
"PRD path required. Usage: tfx swarm preflight <prd-path> [--json]",
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
const absPrd = resolve(prdPath);
|
|
353
|
+
if (!existsSync(absPrd)) {
|
|
354
|
+
throw new Error(`PRD file not found: ${absPrd}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const report = (deps.runSwarmPreflight || runSwarmPreflight)(absPrd, {
|
|
358
|
+
repoRoot: process.cwd(),
|
|
359
|
+
deps,
|
|
360
|
+
});
|
|
361
|
+
if (flags.json) {
|
|
362
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
363
|
+
} else {
|
|
364
|
+
process.stdout.write(formatPreflightReport(report));
|
|
365
|
+
}
|
|
366
|
+
if (!report.ok) process.exitCode = 1;
|
|
367
|
+
}
|
|
368
|
+
|
|
275
369
|
/**
|
|
276
370
|
* tfx swarm list — synapse status로 위임 (swarm 세션은 synapse registry에 등록)
|
|
277
371
|
*/
|