@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
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "@triflux/core/hub/dynamic-routing-engine.mjs";
|
|
22
22
|
import { cleanupShardProcesses } from "@triflux/core/hub/lib/process-utils.mjs";
|
|
23
23
|
import { getHostConfig } from "@triflux/core/hub/lib/ssh-command.mjs";
|
|
24
|
+
import { DEFAULT_SWARM_LOCK_TTL_MS } from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
24
25
|
import { buildWorkerPrompt } from "./build-worker-prompt.mjs";
|
|
25
26
|
import { registerSwarmShard } from "./claude-native-bridge.mjs";
|
|
26
27
|
import { createConductor, STATES } from "./conductor.mjs";
|
|
@@ -189,7 +190,6 @@ function isInteractiveNativeBridgeShard(shard) {
|
|
|
189
190
|
* @param {number} [opts.maxRestarts=2] — per-shard max restarts
|
|
190
191
|
* @param {boolean} [opts.nativeBridge=false] — expose local shards in Claude agents
|
|
191
192
|
* @param {number} [opts.graceMs=10000] — conductor shutdown grace period
|
|
192
|
-
* @param {number} [opts.integrationTimeoutMs=60000] — max time for integration phase
|
|
193
193
|
* @param {boolean} [opts.keepFailedWorktrees=false] — preserve failed shard worktrees for debugging
|
|
194
194
|
* @param {object} [opts.probeOpts] — health probe overrides
|
|
195
195
|
* @param {object} [opts.deps] — dependency injection for testing
|
|
@@ -205,7 +205,6 @@ export function createSwarmHypervisor(opts) {
|
|
|
205
205
|
nativeBridge = false,
|
|
206
206
|
graceMs = 10_000,
|
|
207
207
|
keepFailedWorktrees = false,
|
|
208
|
-
_integrationTimeoutMs = 60_000,
|
|
209
208
|
probeOpts = {},
|
|
210
209
|
_deps = {},
|
|
211
210
|
} = opts;
|
|
@@ -679,6 +678,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
679
678
|
const config = {
|
|
680
679
|
id: buildSwarmSessionId(shard),
|
|
681
680
|
agent: shard.agent,
|
|
681
|
+
role: shard.role,
|
|
682
682
|
// #125: append Completion Protocol appendix so workers emit a
|
|
683
683
|
// sentinel-framed JSON payload that conductor can reliably capture.
|
|
684
684
|
prompt: buildWorkerPrompt(shard.prompt, {
|
|
@@ -1911,6 +1911,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
1911
1911
|
*/
|
|
1912
1912
|
/** Hub keepalive — Hub crash recovery (idle timeout은 기본 비활성이므로 crash 복구 용도) */
|
|
1913
1913
|
let hubKeepaliveTimer = null;
|
|
1914
|
+
let lockRenewTimer = null;
|
|
1914
1915
|
function startHubKeepalive() {
|
|
1915
1916
|
// 5분마다 Hub /status 핑 — crash 감지 시 ensureHubAlive로 재시작
|
|
1916
1917
|
hubKeepaliveTimer = setInterval(
|
|
@@ -1960,6 +1961,22 @@ export function createSwarmHypervisor(opts) {
|
|
|
1960
1961
|
}
|
|
1961
1962
|
}
|
|
1962
1963
|
|
|
1964
|
+
function startLockRenewal() {
|
|
1965
|
+
lockRenewTimer = setInterval(() => {
|
|
1966
|
+
if (!lockManager) return;
|
|
1967
|
+
for (const shardName of workers.keys()) {
|
|
1968
|
+
if (!completedShards.has(shardName) && !failures.has(shardName))
|
|
1969
|
+
lockManager.renew(shardName);
|
|
1970
|
+
}
|
|
1971
|
+
}, DEFAULT_SWARM_LOCK_TTL_MS / 2);
|
|
1972
|
+
lockRenewTimer.unref?.();
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function stopLockRenewal() {
|
|
1976
|
+
if (lockRenewTimer) clearInterval(lockRenewTimer);
|
|
1977
|
+
lockRenewTimer = null;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1963
1980
|
async function launch(swarmPlan) {
|
|
1964
1981
|
if (state !== SWARM_STATES.PLANNING) {
|
|
1965
1982
|
throw new Error(`Cannot launch in state "${state}"`);
|
|
@@ -2053,6 +2070,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
2053
2070
|
persistPath: join(workdir, ".triflux", "swarm-locks.json"),
|
|
2054
2071
|
});
|
|
2055
2072
|
|
|
2073
|
+
startLockRenewal();
|
|
2056
2074
|
// Hub keepalive 시작
|
|
2057
2075
|
startHubKeepalive();
|
|
2058
2076
|
|
|
@@ -2108,6 +2126,7 @@ export function createSwarmHypervisor(opts) {
|
|
|
2108
2126
|
*/
|
|
2109
2127
|
async function shutdown(reason = "shutdown") {
|
|
2110
2128
|
stopHubKeepalive();
|
|
2129
|
+
stopLockRenewal();
|
|
2111
2130
|
eventLog.append("swarm_shutdown", { reason, state });
|
|
2112
2131
|
|
|
2113
2132
|
const shutdowns = [];
|
package/hub/team/swarm-locks.mjs
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname, relative, resolve } from "node:path";
|
|
8
|
+
import { DEFAULT_SWARM_LOCK_TTL_MS } from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
8
9
|
|
|
9
|
-
const LOCK_TTL_MS =
|
|
10
|
+
const LOCK_TTL_MS = DEFAULT_SWARM_LOCK_TTL_MS;
|
|
10
11
|
|
|
11
12
|
// Distribution-critical paths. A swarm worker that modifies these without
|
|
12
13
|
// having them explicitly listed in its shard lease is almost certainly a
|
|
@@ -208,6 +209,20 @@ export function createSwarmLocks(opts = {}) {
|
|
|
208
209
|
return count;
|
|
209
210
|
}
|
|
210
211
|
|
|
212
|
+
function renew(workerId) {
|
|
213
|
+
pruneExpired();
|
|
214
|
+
const ts = now();
|
|
215
|
+
let count = 0;
|
|
216
|
+
for (const entry of locks.values()) {
|
|
217
|
+
if (entry.workerId === workerId) {
|
|
218
|
+
entry.acquiredAt = ts;
|
|
219
|
+
count += 1;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (count > 0) persist();
|
|
223
|
+
return count;
|
|
224
|
+
}
|
|
225
|
+
|
|
211
226
|
/**
|
|
212
227
|
* Check if a file write would violate any lease.
|
|
213
228
|
* @param {string} workerId — the worker attempting the write
|
|
@@ -308,6 +323,7 @@ export function createSwarmLocks(opts = {}) {
|
|
|
308
323
|
return Object.freeze({
|
|
309
324
|
acquire,
|
|
310
325
|
release,
|
|
326
|
+
renew,
|
|
311
327
|
check,
|
|
312
328
|
validateChanges,
|
|
313
329
|
snapshot,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// hub/team/swarm-preflight.mjs — PRD swarm preflight report/gate (#188)
|
|
2
2
|
// Predicts launch-time blockers before any worker session or worktree spawn.
|
|
3
3
|
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
4
5
|
import { resolve } from "node:path";
|
|
5
6
|
import { readHosts, resolveHost } from "@triflux/core/hub/lib/hosts-compat.mjs";
|
|
6
7
|
import { whichCommand } from "@triflux/core/hub/platform.mjs";
|
|
@@ -83,6 +84,44 @@ function checkSensitive(plan, sensitiveDeny) {
|
|
|
83
84
|
return makeCheck("sensitiveDeny", true, { matches });
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
function parseTrackedStatus(rawStatus) {
|
|
88
|
+
const files = [];
|
|
89
|
+
const records = String(rawStatus || "").split("\0");
|
|
90
|
+
for (let index = 0; index < records.length; index++) {
|
|
91
|
+
const record = records[index];
|
|
92
|
+
if (record.length < 4) continue;
|
|
93
|
+
const status = record.slice(0, 2);
|
|
94
|
+
files.push(normalizePath(record.slice(3)));
|
|
95
|
+
if (/[RC]/u.test(status) && records[index + 1]) {
|
|
96
|
+
files.push(normalizePath(records[++index]));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return unique(files);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function checkRootDirtyLease(plan, repoRoot, deps = {}) {
|
|
103
|
+
const gitStatus =
|
|
104
|
+
deps.gitStatus ||
|
|
105
|
+
((cwd) =>
|
|
106
|
+
execFileSync(
|
|
107
|
+
"git",
|
|
108
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=no"],
|
|
109
|
+
{ cwd, encoding: "utf8", windowsHide: true },
|
|
110
|
+
));
|
|
111
|
+
const dirtyFiles = parseTrackedStatus(gitStatus(repoRoot));
|
|
112
|
+
const leasedFiles = unique(
|
|
113
|
+
[...plan.leaseMap.values()].flat().map(normalizePath),
|
|
114
|
+
);
|
|
115
|
+
const leaseSet = new Set(leasedFiles);
|
|
116
|
+
const overlappingFiles = dirtyFiles.filter((file) => leaseSet.has(file));
|
|
117
|
+
|
|
118
|
+
return makeCheck("rootDirtyLease", overlappingFiles.length === 0, {
|
|
119
|
+
dirtyFiles,
|
|
120
|
+
leasedFiles,
|
|
121
|
+
overlappingFiles,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
86
125
|
function checkHosts(plan, repoRoot) {
|
|
87
126
|
const registry = readHosts(repoRoot);
|
|
88
127
|
const missing = [];
|
|
@@ -216,6 +255,13 @@ function collectMessages(checks) {
|
|
|
216
255
|
);
|
|
217
256
|
}
|
|
218
257
|
|
|
258
|
+
const rootDirtyLease = checks.rootDirtyLease;
|
|
259
|
+
if (rootDirtyLease && !rootDirtyLease.ok) {
|
|
260
|
+
errors.push(
|
|
261
|
+
`rootDir tracked changes overlap shard leases: ${rootDirtyLease.overlappingFiles.join(", ")}`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
219
265
|
return { errors: unique(errors), warnings: unique(warnings) };
|
|
220
266
|
}
|
|
221
267
|
|
|
@@ -258,6 +304,7 @@ export function runSwarmPreflight(prdPath, opts = {}) {
|
|
|
258
304
|
leases: makeCheck("leases", plan.conflicts.length === 0, {
|
|
259
305
|
conflicts: [...plan.conflicts],
|
|
260
306
|
}),
|
|
307
|
+
rootDirtyLease: checkRootDirtyLease(plan, repoRoot, opts.deps),
|
|
261
308
|
sensitiveDeny: checkSensitive(plan, sensitiveDeny),
|
|
262
309
|
hosts: checkHosts(plan, repoRoot),
|
|
263
310
|
workerCli: checkLocalWorkerClis(plan, opts.deps),
|
|
@@ -6,6 +6,11 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import { join as pathJoin } from "node:path";
|
|
7
7
|
|
|
8
8
|
import { execute as defaultExecuteCodex } from "@triflux/core/hub/codex-adapter.mjs";
|
|
9
|
+
import {
|
|
10
|
+
createActivityLifecycle,
|
|
11
|
+
isActivityLifecycleEnabled,
|
|
12
|
+
resolveHardCeilingMs,
|
|
13
|
+
} from "@triflux/core/hub/lib/worker-lifecycle.mjs";
|
|
9
14
|
import { JsonRpcWsUdsClient } from "../workers/lib/jsonrpc-ws-uds.mjs";
|
|
10
15
|
import {
|
|
11
16
|
buildClaudePromptDispatchPayload,
|
|
@@ -141,8 +146,11 @@ export async function runUdsOrchestration({ mode, task, claude, codex } = {}) {
|
|
|
141
146
|
throw new Error(`Unsupported UDS orchestration mode: ${selectedMode}`);
|
|
142
147
|
}
|
|
143
148
|
|
|
144
|
-
function scanMessage(message, marker, state) {
|
|
149
|
+
function scanMessage(message, marker, state, onActivity = () => {}) {
|
|
145
150
|
const scan = (text) => {
|
|
151
|
+
// Only stream/snapshot content advances liveness. "state" heartbeats do
|
|
152
|
+
// not keep an otherwise silent turn alive forever.
|
|
153
|
+
onActivity();
|
|
146
154
|
state.streamBytes += Buffer.byteLength(text);
|
|
147
155
|
state.stream.push(text);
|
|
148
156
|
if (/⏺/.test(stripAnsi(text))) state.assistantStarted = true;
|
|
@@ -167,7 +175,10 @@ export function subscribeClaudeUntilMarker(
|
|
|
167
175
|
controlSock,
|
|
168
176
|
short,
|
|
169
177
|
marker,
|
|
170
|
-
{
|
|
178
|
+
{
|
|
179
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
180
|
+
maxTurnMs = Math.min(resolveHardCeilingMs(), 15 * 60_000),
|
|
181
|
+
} = {},
|
|
171
182
|
) {
|
|
172
183
|
return new Promise((resolve) => {
|
|
173
184
|
const socket = net.connect(controlSock);
|
|
@@ -185,7 +196,23 @@ export function subscribeClaudeUntilMarker(
|
|
|
185
196
|
stream: [],
|
|
186
197
|
assistantStarted: false,
|
|
187
198
|
};
|
|
188
|
-
const
|
|
199
|
+
const activityEnabled = isActivityLifecycleEnabled();
|
|
200
|
+
const lifecycle = createActivityLifecycle({
|
|
201
|
+
enabled: activityEnabled,
|
|
202
|
+
interventionMs: timeoutMs,
|
|
203
|
+
hardCeilingMs: maxTurnMs,
|
|
204
|
+
});
|
|
205
|
+
let timer = null;
|
|
206
|
+
|
|
207
|
+
const armTimer = () => {
|
|
208
|
+
timer = setTimeout(async () => {
|
|
209
|
+
if (!activityEnabled) return finish({ timedOut: true });
|
|
210
|
+
const reason = await lifecycle.check();
|
|
211
|
+
if (reason) return finish({ timedOut: true, timeoutReason: reason });
|
|
212
|
+
armTimer();
|
|
213
|
+
}, timeoutMs);
|
|
214
|
+
};
|
|
215
|
+
armTimer();
|
|
189
216
|
|
|
190
217
|
function finish(extra = {}) {
|
|
191
218
|
if (finished) return;
|
|
@@ -209,7 +236,9 @@ export function subscribeClaudeUntilMarker(
|
|
|
209
236
|
data = data.slice(index + 1);
|
|
210
237
|
if (!line) continue;
|
|
211
238
|
try {
|
|
212
|
-
scanMessage(JSON.parse(line), marker, state)
|
|
239
|
+
scanMessage(JSON.parse(line), marker, state, () =>
|
|
240
|
+
lifecycle.observe(),
|
|
241
|
+
);
|
|
213
242
|
} catch (_error) {
|
|
214
243
|
state.messageCount += 1;
|
|
215
244
|
}
|
|
@@ -263,6 +292,7 @@ export function createClaudeUdsEndpoint({
|
|
|
263
292
|
daemonPaths = deriveClaudeDaemonPaths(),
|
|
264
293
|
cwd = process.cwd(),
|
|
265
294
|
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
295
|
+
maxTurnMs = Math.min(resolveHardCeilingMs(), 15 * 60_000),
|
|
266
296
|
markerFactory = () =>
|
|
267
297
|
`TFX_UDS_ORCH_DONE_${crypto.randomBytes(6).toString("hex")}`,
|
|
268
298
|
shortFactory,
|
|
@@ -299,7 +329,7 @@ export function createClaudeUdsEndpoint({
|
|
|
299
329
|
sock,
|
|
300
330
|
payload.short,
|
|
301
331
|
marker,
|
|
302
|
-
{ timeoutMs },
|
|
332
|
+
{ timeoutMs, maxTurnMs },
|
|
303
333
|
);
|
|
304
334
|
await teardownDaemonJob({
|
|
305
335
|
controlSock: sock,
|
|
@@ -441,6 +471,7 @@ export function createCodexAppServerUdsEndpoint({
|
|
|
441
471
|
cwd = process.cwd(),
|
|
442
472
|
model,
|
|
443
473
|
timeoutMs = DEFAULT_CODEX_APP_SERVER_UDS_TIMEOUT_MS,
|
|
474
|
+
maxTurnMs = Math.min(resolveHardCeilingMs(), 15 * 60_000),
|
|
444
475
|
bootstrapTimeoutMs = DEFAULT_CODEX_APP_SERVER_UDS_BOOTSTRAP_MS,
|
|
445
476
|
clientFactory = (opts) => new JsonRpcWsUdsClient(opts),
|
|
446
477
|
spawnFn = spawn,
|
|
@@ -536,10 +567,12 @@ export function createCodexAppServerUdsEndpoint({
|
|
|
536
567
|
let offDelta = () => {};
|
|
537
568
|
let offError = () => {};
|
|
538
569
|
let offDone = () => {};
|
|
570
|
+
let offActivity = () => {};
|
|
539
571
|
const cleanup = () => {
|
|
540
572
|
offDelta();
|
|
541
573
|
offError();
|
|
542
574
|
offDone();
|
|
575
|
+
offActivity();
|
|
543
576
|
};
|
|
544
577
|
const completion = new Promise((resolve) => {
|
|
545
578
|
offDelta = client.onNotification(
|
|
@@ -566,12 +599,32 @@ export function createCodexAppServerUdsEndpoint({
|
|
|
566
599
|
{ threadId, input: [{ type: "text", text: prompt }] },
|
|
567
600
|
timeoutMs,
|
|
568
601
|
);
|
|
602
|
+
const activityEnabled = isActivityLifecycleEnabled();
|
|
603
|
+
const lifecycle = createActivityLifecycle({
|
|
604
|
+
enabled: activityEnabled,
|
|
605
|
+
interventionMs: timeoutMs,
|
|
606
|
+
hardCeilingMs: maxTurnMs,
|
|
607
|
+
});
|
|
608
|
+
offActivity = client.onNotification("*", () => lifecycle.observe());
|
|
569
609
|
let timeoutHandle = null;
|
|
570
610
|
const timeoutPromise = new Promise((resolve) => {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
611
|
+
const arm = () => {
|
|
612
|
+
timeoutHandle = setTimeout(async () => {
|
|
613
|
+
if (!activityEnabled) {
|
|
614
|
+
cleanup();
|
|
615
|
+
resolve({ status: "timeout" });
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const reason = await lifecycle.check();
|
|
619
|
+
if (reason) {
|
|
620
|
+
cleanup();
|
|
621
|
+
resolve({ status: "timeout", reason });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
arm();
|
|
625
|
+
}, timeoutMs);
|
|
626
|
+
};
|
|
627
|
+
arm();
|
|
575
628
|
});
|
|
576
629
|
let settled;
|
|
577
630
|
try {
|
|
@@ -4,8 +4,16 @@
|
|
|
4
4
|
// Remote support: host option → SSH-based git operations via remote-session.mjs.
|
|
5
5
|
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
access,
|
|
9
|
+
mkdir,
|
|
10
|
+
mkdtemp,
|
|
11
|
+
readdir,
|
|
12
|
+
realpath,
|
|
13
|
+
rm,
|
|
14
|
+
} from "node:fs/promises";
|
|
15
|
+
import { tmpdir } from "node:os";
|
|
16
|
+
import { join, normalize, relative, resolve } from "node:path";
|
|
9
17
|
import { remoteGit, validateHost } from "./remote-session.mjs";
|
|
10
18
|
|
|
11
19
|
const SWARM_ROOT = ".codex-swarm";
|
|
@@ -327,24 +335,23 @@ export async function rebaseShardOntoIntegration({
|
|
|
327
335
|
}
|
|
328
336
|
}
|
|
329
337
|
|
|
330
|
-
// #127 BUG-E:
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
} catch {
|
|
340
|
-
/* detached HEAD or unknown — skip restore */
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Backup integration HEAD for rollback
|
|
338
|
+
// #127 BUG-E accident history: this function used to checkout the
|
|
339
|
+
// integration branch in rootDir, leak rootDir HEAD, and redirect later user
|
|
340
|
+
// edits/commits into swarm/.../merge. Integration now happens exclusively in
|
|
341
|
+
// a disposable worktree, so rootDir HEAD never moves and needs no restore.
|
|
342
|
+
//
|
|
343
|
+
// #129 BUG-J accident history: checkout failure was followed by a blind
|
|
344
|
+
// reset --hard (or branch -f rollback) against rootDir, which rewound the
|
|
345
|
+
// caller branch. Rollback now resets only the disposable worktree; there is
|
|
346
|
+
// no rootDir reset path and therefore no stash-create last-resort path.
|
|
344
347
|
const backupCommit = await git(["rev-parse", integrationBranch], rootDir);
|
|
345
348
|
|
|
346
349
|
let shaList = [];
|
|
347
350
|
let ffError = null;
|
|
351
|
+
let integrationWorktreeAdded = false;
|
|
352
|
+
const integrationWorktree = await mkdtemp(
|
|
353
|
+
join(tmpdir(), "tfx-swarm-integration-"),
|
|
354
|
+
);
|
|
348
355
|
|
|
349
356
|
try {
|
|
350
357
|
const log = await git(
|
|
@@ -361,11 +368,15 @@ export async function rebaseShardOntoIntegration({
|
|
|
361
368
|
.map((s) => s.trim())
|
|
362
369
|
.filter(Boolean);
|
|
363
370
|
|
|
364
|
-
await git(
|
|
371
|
+
await git(
|
|
372
|
+
["worktree", "add", "--quiet", integrationWorktree, integrationBranch],
|
|
373
|
+
rootDir,
|
|
374
|
+
);
|
|
375
|
+
integrationWorktreeAdded = true;
|
|
365
376
|
|
|
366
377
|
try {
|
|
367
|
-
await git(["merge", "--ff-only", shardBranch],
|
|
368
|
-
const headCommit = await git(["rev-parse", "HEAD"],
|
|
378
|
+
await git(["merge", "--ff-only", shardBranch], integrationWorktree);
|
|
379
|
+
const headCommit = await git(["rev-parse", "HEAD"], integrationWorktree);
|
|
369
380
|
return {
|
|
370
381
|
ok: true,
|
|
371
382
|
headCommit,
|
|
@@ -376,12 +387,15 @@ export async function rebaseShardOntoIntegration({
|
|
|
376
387
|
} catch (err) {
|
|
377
388
|
ffError = err;
|
|
378
389
|
try {
|
|
379
|
-
await git(["merge", "--abort"],
|
|
390
|
+
await git(["merge", "--abort"], integrationWorktree);
|
|
380
391
|
} catch {
|
|
381
392
|
/* ff-only usually leaves no merge state */
|
|
382
393
|
}
|
|
383
394
|
try {
|
|
384
|
-
|
|
395
|
+
// Safe destructive operation: this cwd is the disposable integration
|
|
396
|
+
// worktree, never rootDir. It restores the integration ref before the
|
|
397
|
+
// cherry-pick fallback without touching user files.
|
|
398
|
+
await git(["reset", "--hard", backupCommit], integrationWorktree);
|
|
385
399
|
} catch {
|
|
386
400
|
/* fallback below will report if cherry-pick also fails */
|
|
387
401
|
}
|
|
@@ -391,10 +405,10 @@ export async function rebaseShardOntoIntegration({
|
|
|
391
405
|
// fail when histories diverge or a shard branch is checked out by a swarm
|
|
392
406
|
// worktree. Cherry-pick reads commits without moving the shard branch ref.
|
|
393
407
|
for (const sha of shaList) {
|
|
394
|
-
await git(["cherry-pick", sha],
|
|
408
|
+
await git(["cherry-pick", sha], integrationWorktree);
|
|
395
409
|
}
|
|
396
410
|
|
|
397
|
-
const headCommit = await git(["rev-parse", "HEAD"],
|
|
411
|
+
const headCommit = await git(["rev-parse", "HEAD"], integrationWorktree);
|
|
398
412
|
return {
|
|
399
413
|
ok: true,
|
|
400
414
|
headCommit,
|
|
@@ -406,42 +420,17 @@ export async function rebaseShardOntoIntegration({
|
|
|
406
420
|
: "cherry-pick fallback",
|
|
407
421
|
};
|
|
408
422
|
} catch (err) {
|
|
409
|
-
|
|
410
|
-
await git(["cherry-pick", "--abort"], rootDir);
|
|
411
|
-
} catch {
|
|
412
|
-
/* already clean */
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// #129 BUG-J: Roll back integrationBranch without mutating whatever
|
|
416
|
-
// branch HEAD currently points at. The previous implementation did a
|
|
417
|
-
// blind `reset --hard backupCommit` on current HEAD — if the earlier
|
|
418
|
-
// `git checkout integrationBranch` inside the try block failed (e.g.
|
|
419
|
-
// integrationBranch was already checked out by a swarm worktree) the
|
|
420
|
-
// caller's branch silently rewound to integrationBranch's backup commit.
|
|
421
|
-
// Observed 2026-04-20: fix branch tree lost to main HEAD during swarm
|
|
422
|
-
// probe, requiring `git merge --ff-only origin/<branch>` recovery.
|
|
423
|
-
let currentBranch = null;
|
|
424
|
-
try {
|
|
425
|
-
const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], rootDir);
|
|
426
|
-
if (head && head !== "HEAD") currentBranch = head;
|
|
427
|
-
} catch {
|
|
428
|
-
/* detached — fall through to ref-only update */
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (currentBranch === integrationBranch) {
|
|
432
|
-
// HEAD is on integrationBranch — reset advances this branch only.
|
|
423
|
+
if (integrationWorktreeAdded) {
|
|
433
424
|
try {
|
|
434
|
-
await git(["
|
|
425
|
+
await git(["cherry-pick", "--abort"], integrationWorktree);
|
|
435
426
|
} catch {
|
|
436
|
-
/*
|
|
427
|
+
/* already clean */
|
|
437
428
|
}
|
|
438
|
-
} else {
|
|
439
|
-
// HEAD is elsewhere (originalBranch, detached, or another branch).
|
|
440
|
-
// Update the integrationBranch ref directly; never touch current HEAD.
|
|
441
429
|
try {
|
|
442
|
-
|
|
430
|
+
// #129 rollback is now local to the disposable integration worktree.
|
|
431
|
+
await git(["reset", "--hard", backupCommit], integrationWorktree);
|
|
443
432
|
} catch {
|
|
444
|
-
/* best-effort
|
|
433
|
+
/* best-effort */
|
|
445
434
|
}
|
|
446
435
|
}
|
|
447
436
|
|
|
@@ -453,14 +442,22 @@ export async function rebaseShardOntoIntegration({
|
|
|
453
442
|
fallbackReason: ffError?.message || null,
|
|
454
443
|
};
|
|
455
444
|
} finally {
|
|
456
|
-
|
|
457
|
-
// (detached HEAD case) or already on target.
|
|
458
|
-
if (originalBranch) {
|
|
445
|
+
if (integrationWorktreeAdded) {
|
|
459
446
|
try {
|
|
460
|
-
await git(
|
|
447
|
+
await git(
|
|
448
|
+
["worktree", "remove", "--force", integrationWorktree],
|
|
449
|
+
rootDir,
|
|
450
|
+
);
|
|
461
451
|
} catch {
|
|
462
|
-
|
|
452
|
+
await rm(integrationWorktree, { recursive: true, force: true });
|
|
453
|
+
try {
|
|
454
|
+
await git(["worktree", "prune"], rootDir);
|
|
455
|
+
} catch {
|
|
456
|
+
/* best-effort metadata cleanup */
|
|
457
|
+
}
|
|
463
458
|
}
|
|
459
|
+
} else {
|
|
460
|
+
await rm(integrationWorktree, { recursive: true, force: true });
|
|
464
461
|
}
|
|
465
462
|
}
|
|
466
463
|
}
|
package/hub/tools.mjs
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// register/status/publish/ask/poll/handoff/HITL + team proxy
|
|
3
3
|
// 모든 도구 응답: { ok: boolean, error?: { code, message }, data?: ... }
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
7
|
+
DEFAULT_ASSIGN_TTL_MS,
|
|
8
|
+
DEFAULT_HANDOFF_TTL_MS,
|
|
9
|
+
} from "@triflux/core/hub/lib/timeout-defaults.mjs";
|
|
5
10
|
import { createPipeline, ensurePipelineTable } from "@triflux/core/hub/pipeline/index.mjs";
|
|
6
11
|
import {
|
|
7
12
|
initPipelineState,
|
|
@@ -148,6 +153,14 @@ export function createTools(store, router, hitl, pipe = null) {
|
|
|
148
153
|
},
|
|
149
154
|
topics: { type: "array", items: { type: "string" }, maxItems: 64 },
|
|
150
155
|
metadata: { type: "object" },
|
|
156
|
+
project_id: { type: "string", pattern: "^prj_[A-Za-z0-9_-]{22}$" },
|
|
157
|
+
session_id: { type: "string", minLength: 1, maxLength: 256 },
|
|
158
|
+
host_id: { type: "string", pattern: "^hst_[A-Za-z0-9_-]+$" },
|
|
159
|
+
transport_locators_json: {
|
|
160
|
+
description:
|
|
161
|
+
"tfx.transport-locator.v1 객체 배열 또는 그 JSON 문자열",
|
|
162
|
+
oneOf: [{ type: "array", items: { type: "object" } }, { type: "string" }],
|
|
163
|
+
},
|
|
151
164
|
heartbeat_ttl_ms: {
|
|
152
165
|
type: "integer",
|
|
153
166
|
minimum: 10000,
|
|
@@ -359,7 +372,7 @@ export function createTools(store, router, hitl, pipe = null) {
|
|
|
359
372
|
type: "integer",
|
|
360
373
|
minimum: 1000,
|
|
361
374
|
maximum: 86400000,
|
|
362
|
-
default:
|
|
375
|
+
default: DEFAULT_HANDOFF_TTL_MS,
|
|
363
376
|
},
|
|
364
377
|
trace_id: { type: "string" },
|
|
365
378
|
correlation_id: { type: "string" },
|
|
@@ -396,13 +409,13 @@ export function createTools(store, router, hitl, pipe = null) {
|
|
|
396
409
|
type: "integer",
|
|
397
410
|
minimum: 1000,
|
|
398
411
|
maximum: 86400000,
|
|
399
|
-
default:
|
|
412
|
+
default: DEFAULT_ASSIGN_TTL_MS,
|
|
400
413
|
},
|
|
401
414
|
timeout_ms: {
|
|
402
415
|
type: "integer",
|
|
403
416
|
minimum: 1000,
|
|
404
417
|
maximum: 86400000,
|
|
405
|
-
default:
|
|
418
|
+
default: DEFAULT_ASSIGN_TIMEOUT_MS,
|
|
406
419
|
},
|
|
407
420
|
max_retries: { type: "integer", minimum: 0, maximum: 20, default: 0 },
|
|
408
421
|
trace_id: { type: "string" },
|