@triflux/remote 10.46.0 → 10.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hub/server.mjs +2 -51
- package/hub/team/codex-review.mjs +2 -3
- package/hub/team/cto-auto-collect.mjs +1 -1
- package/hub/team/cto-pull.mjs +1 -1
- package/hub/team/launcher-template.mjs +2 -1
- package/hub/team/recovery-store.mjs +1 -1
- package/hub/team/synapse-registry.mjs +14 -11
- package/hub/team/worktree-lifecycle.mjs +8 -48
- package/hub/tray-lifecycle.mjs +1 -1
- package/hub/workers/codex-mcp.mjs +27 -3
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +13 -13
- package/scripts/lib/codex-profile-config.mjs +10 -7
- package/hub/team/git-preflight.mjs +0 -286
package/hub/server.mjs
CHANGED
|
@@ -72,9 +72,7 @@ import {
|
|
|
72
72
|
} from "@triflux/core/hub/state.mjs";
|
|
73
73
|
import { createStoreAdapter } from "./store-adapter.mjs";
|
|
74
74
|
import { createCtoAutoCollector } from "./team/cto-auto-collect.mjs";
|
|
75
|
-
import { createGitPreflight } from "./team/git-preflight.mjs";
|
|
76
75
|
import { nativeProxy } from "./team/nativeProxy.mjs";
|
|
77
|
-
import { createSwarmLocks } from "./team/swarm-locks.mjs";
|
|
78
76
|
import {
|
|
79
77
|
createSynapseRegistry,
|
|
80
78
|
projectPeer,
|
|
@@ -119,14 +117,6 @@ const AIMD_WINDOW_MS = 30 * 60 * 1000;
|
|
|
119
117
|
const AIMD_INITIAL_BATCH_SIZE = 3;
|
|
120
118
|
const AIMD_MIN_BATCH_SIZE = 1;
|
|
121
119
|
const AIMD_MAX_BATCH_SIZE = 10;
|
|
122
|
-
const SYNAPSE_VALID_OPS = new Set([
|
|
123
|
-
"checkout",
|
|
124
|
-
"rebase",
|
|
125
|
-
"cherry-pick",
|
|
126
|
-
"reset",
|
|
127
|
-
"stash-pop",
|
|
128
|
-
"worktree-remove",
|
|
129
|
-
]);
|
|
130
120
|
const HUB_IDLE_TIMEOUT_DEFAULT_MS = 30 * 60 * 1000;
|
|
131
121
|
const HUB_IDLE_SWEEP_DEFAULT_MS = 60 * 1000;
|
|
132
122
|
const STATIC_CONTENT_TYPES = Object.freeze({
|
|
@@ -1124,21 +1114,13 @@ export async function startHub({
|
|
|
1124
1114
|
}
|
|
1125
1115
|
const delegatorService = new DelegatorService({ worker: delegatorWorker });
|
|
1126
1116
|
|
|
1127
|
-
// Synapse Layer 4: session registry
|
|
1117
|
+
// Synapse Layer 4: session registry
|
|
1128
1118
|
const synapseEmitter = new EventEmitter();
|
|
1129
1119
|
synapseEmitter.setMaxListeners(50);
|
|
1130
1120
|
const synapseRegistry = createSynapseRegistry({
|
|
1131
1121
|
persistPath: join(CACHE_DIR, "tfx-hub", "synapse-sessions.json"),
|
|
1132
1122
|
emitter: synapseEmitter,
|
|
1133
1123
|
});
|
|
1134
|
-
const swarmLocks = createSwarmLocks({
|
|
1135
|
-
repoRoot: PROJECT_ROOT,
|
|
1136
|
-
persistPath: join(CACHE_DIR, "tfx-hub", "swarm-locks.json"),
|
|
1137
|
-
});
|
|
1138
|
-
const gitPreflight = createGitPreflight({
|
|
1139
|
-
registry: synapseRegistry,
|
|
1140
|
-
locks: swarmLocks,
|
|
1141
|
-
});
|
|
1142
1124
|
const ctoAutoCollector = createCtoAutoCollector({
|
|
1143
1125
|
registry: synapseRegistry,
|
|
1144
1126
|
runCollect,
|
|
@@ -1437,7 +1419,7 @@ export async function startHub({
|
|
|
1437
1419
|
});
|
|
1438
1420
|
}
|
|
1439
1421
|
|
|
1440
|
-
// ── Synapse Layer 5: session registry
|
|
1422
|
+
// ── Synapse Layer 5: session registry routes ──
|
|
1441
1423
|
// Admin/raw snapshot (loopback-only). Returns raw cwd/pid for local
|
|
1442
1424
|
// admin/HUD use; the redacted peer surface is GET /synapse/peers.
|
|
1443
1425
|
if (path === "/synapse/sessions" && req.method === "GET") {
|
|
@@ -1642,37 +1624,6 @@ export async function startHub({
|
|
|
1642
1624
|
}
|
|
1643
1625
|
}
|
|
1644
1626
|
|
|
1645
|
-
if (path === "/synapse/locks" && req.method === "GET") {
|
|
1646
|
-
return writeJson(res, 200, {
|
|
1647
|
-
ok: true,
|
|
1648
|
-
locks: swarmLocks.snapshot(),
|
|
1649
|
-
ts: Date.now(),
|
|
1650
|
-
});
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
if (path === "/synapse/preflight" && req.method === "POST") {
|
|
1654
|
-
try {
|
|
1655
|
-
const body = await parseBody(req);
|
|
1656
|
-
const { op, args = {}, sessionContext = {} } = body;
|
|
1657
|
-
if (!op || typeof op !== "string") {
|
|
1658
|
-
return writeJson(res, 400, { ok: false, error: "op 필수" });
|
|
1659
|
-
}
|
|
1660
|
-
if (!SYNAPSE_VALID_OPS.has(op)) {
|
|
1661
|
-
return writeJson(res, 400, {
|
|
1662
|
-
ok: false,
|
|
1663
|
-
error: `invalid op: ${op}`,
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
const result = gitPreflight.check(op, args, sessionContext);
|
|
1667
|
-
return writeJson(res, 200, { ok: true, ...result });
|
|
1668
|
-
} catch (err) {
|
|
1669
|
-
return writeJson(res, 400, {
|
|
1670
|
-
ok: false,
|
|
1671
|
-
error: String(err?.message || err),
|
|
1672
|
-
});
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
1627
|
if (path.startsWith("/bridge")) {
|
|
1677
1628
|
const isBridgeStatusGet =
|
|
1678
1629
|
path === "/bridge/status" && req.method === "GET";
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Codex headless review helper. Runs a cross-model review of a git diff
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// intentionally because `tfx` is the sanctioned entry point).
|
|
2
|
+
// through node `spawn` rather than the Bash tool, because `tfx` is the
|
|
3
|
+
// sanctioned entry point for Codex.
|
|
5
4
|
//
|
|
6
5
|
// Contract:
|
|
7
6
|
// runCodexReview({ ref = "HEAD", base?: string, timeoutMs = 180_000 })
|
|
@@ -2,7 +2,7 @@ import { existsSync, statSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { resolveLakeRootDir } from "../../cto/lake-root.mjs";
|
|
5
|
-
import { resolveRoleControlSnapshot } from "
|
|
5
|
+
import { resolveRoleControlSnapshot } from "../lib/cto-env.mjs";
|
|
6
6
|
|
|
7
7
|
const DEFAULT_DEBOUNCE_MS = 120_000;
|
|
8
8
|
|
package/hub/team/cto-pull.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
import { resolveRoleControlSnapshot } from "
|
|
4
|
+
import { resolveRoleControlSnapshot } from "../lib/cto-env.mjs";
|
|
5
5
|
|
|
6
6
|
function defaultLakeRoot() {
|
|
7
7
|
return join(process.cwd(), ".triflux", "lake");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// hub/team/launcher-template.mjs — 결정론적 런처 생성
|
|
2
|
-
// 기존 codex-adapter와 Antigravity
|
|
2
|
+
// 기존 codex-adapter와 Antigravity print-value 계약을 소비하여
|
|
3
3
|
// 동일 입력 → 동일 args 배열을 보장한다.
|
|
4
4
|
// F1 해결: codex adapter가 --dangerously-bypass-approvals-and-sandbox 자동 추가
|
|
5
5
|
// F4 해결: codex exec "prompt" 인라인 (파이프/리다이렉트 아님)
|
|
@@ -18,6 +18,7 @@ function shellSingleQuote(value) {
|
|
|
18
18
|
|
|
19
19
|
function buildAntigravityArgs(opts = {}) {
|
|
20
20
|
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
21
|
+
// agy 1.1.27: --print 는 값(프롬프트) 필수, stdin 프롬프트 불가.
|
|
21
22
|
const command = `agy --dangerously-skip-permissions --print ${shellSingleQuote(prompt)}`;
|
|
22
23
|
if (opts.resultFile) {
|
|
23
24
|
return `${command} > ${shellSingleQuote(opts.resultFile)} 2>${shellSingleQuote(`${opts.resultFile}.err`)}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// hub/team/recovery-store.mjs — Best-effort preservation of worker changes
|
|
2
2
|
// before a dirty worktree is removed. Uses file-based `.patch` + JSON manifest
|
|
3
|
-
// instead of git stash
|
|
3
|
+
// instead of git stash, which is risky to pop under concurrent worktree use.
|
|
4
4
|
|
|
5
5
|
import { execFile } from "node:child_process";
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -48,9 +48,9 @@ function hasDirtyFiles(session) {
|
|
|
48
48
|
// A session is "live" while active OR idle. Idle is an interactive session that
|
|
49
49
|
// missed its heartbeat interval but is still under the TTL — alive but inactive,
|
|
50
50
|
// not presumed dead. getActive() and querySessions() share this single predicate
|
|
51
|
-
// so the liveness contract can't drift: getActive() feeds
|
|
52
|
-
//
|
|
53
|
-
// interactive session
|
|
51
|
+
// so the liveness contract can't drift: getActive() feeds peer discovery,
|
|
52
|
+
// CTO status/hygiene and log retention, so dropping idle there would hide a
|
|
53
|
+
// still-live interactive session from every one of those consumers.
|
|
54
54
|
function isLiveStatus(status) {
|
|
55
55
|
return status === "active" || status === "idle";
|
|
56
56
|
}
|
|
@@ -337,7 +337,7 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
337
337
|
// session resuming: Claude Code re-fires SessionStart with the same
|
|
338
338
|
// session_id on resume/clear/compact, so fall through and re-register
|
|
339
339
|
// (revive) it. Otherwise the resumed-but-live session stays stale forever
|
|
340
|
-
// and vanishes from peer-discovery
|
|
340
|
+
// and vanishes from peer-discovery.
|
|
341
341
|
const existing = sessions.get(sessionId);
|
|
342
342
|
if (existing && isLiveStatus(existing.status)) {
|
|
343
343
|
console.warn(
|
|
@@ -381,10 +381,13 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
381
381
|
}
|
|
382
382
|
|
|
383
383
|
// stale/expired 세션이 cutoff 넘게 누적되면 Map에서 제거.
|
|
384
|
-
// live(active/idle)
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
//
|
|
384
|
+
// live(active/idle) rows survive a long-stale lastHeartbeat because peer
|
|
385
|
+
// discovery and CTO status/hygiene still read them.
|
|
386
|
+
// Dirty stale rows keep the historical 24h window. Its original consumer,
|
|
387
|
+
// the git-preflight dirty-file guard, is gone and nothing populates
|
|
388
|
+
// dirtyFiles today, so this split is a candidate for collapsing into one
|
|
389
|
+
// window. Clean stale rows expire quickly to avoid daily dummy rows after
|
|
390
|
+
// overnight operator gaps.
|
|
388
391
|
function pruneExpired(opts2 = {}) {
|
|
389
392
|
if (destroyed) return { removed: [], count: 0 };
|
|
390
393
|
|
|
@@ -510,9 +513,9 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
510
513
|
}
|
|
511
514
|
|
|
512
515
|
function getActive() {
|
|
513
|
-
// Live = active or idle.
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
+
// Live = active or idle. Peer discovery, CTO status/hygiene and log
|
|
517
|
+
// retention read this list; an idle interactive session is still live
|
|
518
|
+
// (process alive) and must stay visible to them.
|
|
516
519
|
return [...sessions.values()]
|
|
517
520
|
.filter((session) => isLiveStatus(session.status))
|
|
518
521
|
.map((session) => cloneSession(session));
|
|
@@ -273,6 +273,8 @@ export async function ensureWorktree({
|
|
|
273
273
|
* @param {string} opts.baseBranch
|
|
274
274
|
* @param {string} [opts.rootDir=process.cwd()]
|
|
275
275
|
* @returns {Promise<{ integrationBranch: string, baseCommit: string }>}
|
|
276
|
+
* @throws {Error} when the integration branch is checked out by another
|
|
277
|
+
* worktree; this is an intentional NO-GO and leaves that worktree untouched.
|
|
276
278
|
*/
|
|
277
279
|
export async function prepareIntegrationBranch({
|
|
278
280
|
runId,
|
|
@@ -299,42 +301,17 @@ export async function prepareIntegrationBranch({
|
|
|
299
301
|
* Rebase a shard branch onto the integration branch.
|
|
300
302
|
* Uses rebase + fast-forward only (no merge commits).
|
|
301
303
|
*
|
|
302
|
-
* Synapse v1: when a `preflight` checker + `sessionContext` are provided,
|
|
303
|
-
* the rebase is pre-flighted against active sessions' dirty files/leases.
|
|
304
|
-
* Blocked rebases return `{ ok: false, preflight: decision }` without touching
|
|
305
|
-
* any branch. If preflight is omitted, behavior is unchanged.
|
|
306
|
-
*
|
|
307
304
|
* @param {object} opts
|
|
308
305
|
* @param {string} opts.shardBranch — the shard's branch name
|
|
309
306
|
* @param {string} opts.integrationBranch — target integration branch
|
|
310
307
|
* @param {string} [opts.rootDir=process.cwd()]
|
|
311
|
-
* @
|
|
312
|
-
* @param {{ sessionId: string, workerId?: string } | null} [opts.sessionContext=null]
|
|
313
|
-
* @returns {Promise<{ ok: boolean, headCommit?: string, strategy?: string, commits?: number, notes?: string, fallbackReason?: string, error?: string, preflight?: object }>}
|
|
308
|
+
* @returns {Promise<{ ok: boolean, headCommit?: string, strategy?: string, commits?: number, notes?: string, fallbackReason?: string, error?: string }>}
|
|
314
309
|
*/
|
|
315
310
|
export async function rebaseShardOntoIntegration({
|
|
316
311
|
shardBranch,
|
|
317
312
|
integrationBranch,
|
|
318
313
|
rootDir = process.cwd(),
|
|
319
|
-
preflight = null,
|
|
320
|
-
sessionContext = null,
|
|
321
314
|
}) {
|
|
322
|
-
// Synapse v1 pre-flight: bail out before mutating branches when another
|
|
323
|
-
// session claims overlapping files.
|
|
324
|
-
if (preflight && sessionContext) {
|
|
325
|
-
const decision = preflight.checkRebase(
|
|
326
|
-
{ branch: integrationBranch },
|
|
327
|
-
sessionContext,
|
|
328
|
-
);
|
|
329
|
-
if (!decision.allowed) {
|
|
330
|
-
return {
|
|
331
|
-
ok: false,
|
|
332
|
-
error: `git-preflight blocked rebase: ${decision.reason || "overlap"}`,
|
|
333
|
-
preflight: decision,
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
315
|
// #127 BUG-E accident history: this function used to checkout the
|
|
339
316
|
// integration branch in rootDir, leak rootDir HEAD, and redirect later user
|
|
340
317
|
// edits/commits into swarm/.../merge. Integration now happens exclusively in
|
|
@@ -458,6 +435,11 @@ export async function rebaseShardOntoIntegration({
|
|
|
458
435
|
}
|
|
459
436
|
} else {
|
|
460
437
|
await rm(integrationWorktree, { recursive: true, force: true });
|
|
438
|
+
try {
|
|
439
|
+
await git(["worktree", "prune"], rootDir);
|
|
440
|
+
} catch {
|
|
441
|
+
/* best-effort metadata cleanup after a partial worktree add */
|
|
442
|
+
}
|
|
461
443
|
}
|
|
462
444
|
}
|
|
463
445
|
}
|
|
@@ -524,40 +506,18 @@ export async function cleanupWorktree({
|
|
|
524
506
|
* Remove a worktree and its branch.
|
|
525
507
|
* Follows WT race-guard: sleep between operations.
|
|
526
508
|
*
|
|
527
|
-
* Synapse v1: when `preflight` + `sessionContext` are provided, the removal
|
|
528
|
-
* is pre-flighted against active sessions — blocked if another session is
|
|
529
|
-
* still registered to this worktree path.
|
|
530
|
-
*
|
|
531
509
|
* @param {object} opts
|
|
532
510
|
* @param {string} opts.worktreePath
|
|
533
511
|
* @param {string} [opts.branchName] — optional branch to delete
|
|
534
512
|
* @param {string} [opts.rootDir=process.cwd()]
|
|
535
513
|
* @param {boolean} [opts.force=false]
|
|
536
|
-
* @param {{ checkWorktreeRemove: Function } | null} [opts.preflight=null]
|
|
537
|
-
* @param {{ sessionId: string, workerId?: string } | null} [opts.sessionContext=null]
|
|
538
514
|
*/
|
|
539
515
|
export async function pruneWorktree({
|
|
540
516
|
worktreePath,
|
|
541
517
|
branchName,
|
|
542
518
|
rootDir = process.cwd(),
|
|
543
519
|
force = false,
|
|
544
|
-
preflight = null,
|
|
545
|
-
sessionContext = null,
|
|
546
520
|
}) {
|
|
547
|
-
if (preflight && sessionContext) {
|
|
548
|
-
const decision = preflight.checkWorktreeRemove(
|
|
549
|
-
{ worktreePath },
|
|
550
|
-
sessionContext,
|
|
551
|
-
);
|
|
552
|
-
if (!decision.allowed) {
|
|
553
|
-
return {
|
|
554
|
-
ok: false,
|
|
555
|
-
error: `git-preflight blocked worktree-remove: ${decision.reason || "active_worktree"}`,
|
|
556
|
-
preflight: decision,
|
|
557
|
-
};
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
521
|
const cleanupResult = await cleanupWorktree({
|
|
562
522
|
worktreePath,
|
|
563
523
|
branchName,
|
package/hub/tray-lifecycle.mjs
CHANGED
|
@@ -120,7 +120,7 @@ export function spawnTrayForHub({
|
|
|
120
120
|
cwd = process.cwd(),
|
|
121
121
|
spawnFn = spawn,
|
|
122
122
|
} = {}) {
|
|
123
|
-
if (env?.TFX_HUB_AUTO_TRAY
|
|
123
|
+
if (env?.TFX_HUB_AUTO_TRAY !== "1") return { status: "disabled" };
|
|
124
124
|
if (isWorktreeOrEphemeralHubContext({ cwd, env })) {
|
|
125
125
|
return { status: "disabled", reason: "ephemeral-or-worktree-context" };
|
|
126
126
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// hub/workers/codex-mcp.mjs — Codex MCP 서버 래퍼
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
@@ -120,7 +120,7 @@ function readTomlScalar(raw, key) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* Resolve a Codex effort profile name (e.g. "
|
|
123
|
+
* Resolve a Codex effort profile name (e.g. "gpt61_sol_high") to its concrete
|
|
124
124
|
* `model` + reasoning effort. SSOT = the per-profile file written by
|
|
125
125
|
* `tfx setup` at `~/.codex/<profile>.config.toml`. Falls back to deriving the
|
|
126
126
|
* effort from the `<...>_<effort>` naming convention when the file is absent.
|
|
@@ -263,6 +263,21 @@ function isNoiseOnlyOutput(text) {
|
|
|
263
263
|
return NOISE_ONLY_OUTPUT_PATTERNS.some((pattern) => pattern.test(trimmed));
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
+
// MCP tool responses are completion-buffered, so stdout cannot distinguish an
|
|
267
|
+
// active long-running call from a stuck one. The SDK delivers only genuine
|
|
268
|
+
// `notifications/progress` callbacks here; append one record per event for
|
|
269
|
+
// tfx-route's per-run heartbeat sidecar. This is intentionally best-effort:
|
|
270
|
+
// inability to write observability data must never change tool execution.
|
|
271
|
+
function recordMcpProgress(activityFile) {
|
|
272
|
+
if (typeof activityFile !== "string" || !activityFile) return;
|
|
273
|
+
try {
|
|
274
|
+
appendFileSync(activityFile, "progress\n");
|
|
275
|
+
} catch {
|
|
276
|
+
// The route's liveness file is diagnostic only. A missing/readonly file
|
|
277
|
+
// must leave the normal MCP request path untouched.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
266
281
|
function watchBootstrapClose(transport) {
|
|
267
282
|
let active = true;
|
|
268
283
|
const promise = new Promise((_, reject) => {
|
|
@@ -429,6 +444,10 @@ export class CodexMcpWorker {
|
|
|
429
444
|
? options.bootstrapTimeoutMs
|
|
430
445
|
: DEFAULT_CODEX_MCP_BOOTSTRAP_TIMEOUT_MS;
|
|
431
446
|
this.retryOptions = normalizeRetryOptions(options.retryOptions);
|
|
447
|
+
this.activityFile =
|
|
448
|
+
typeof options.activityFile === "string"
|
|
449
|
+
? options.activityFile
|
|
450
|
+
: this.env.TFX_CODEX_MCP_ACTIVITY_FILE || "";
|
|
432
451
|
|
|
433
452
|
this.client = null;
|
|
434
453
|
this.transport = null;
|
|
@@ -614,7 +633,12 @@ export class CodexMcpWorker {
|
|
|
614
633
|
const nextRawResult = await this.client.callTool(
|
|
615
634
|
{ name: toolName, arguments: toolArguments },
|
|
616
635
|
undefined,
|
|
617
|
-
{
|
|
636
|
+
{
|
|
637
|
+
timeout: timeoutMs,
|
|
638
|
+
...(this.activityFile
|
|
639
|
+
? { onprogress: () => recordMcpProgress(this.activityFile) }
|
|
640
|
+
: {}),
|
|
641
|
+
},
|
|
618
642
|
);
|
|
619
643
|
|
|
620
644
|
const textContent = collectTextContent(nextRawResult.content);
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
const policy = {
|
|
9
9
|
executor: {
|
|
10
|
-
profile: "
|
|
10
|
+
profile: "gpt61_sol_high",
|
|
11
11
|
timeoutSec: 1080,
|
|
12
12
|
runMode: "fg",
|
|
13
13
|
opusOversight: false,
|
|
@@ -15,7 +15,7 @@ const policy = {
|
|
|
15
15
|
subcommand: "exec",
|
|
16
16
|
},
|
|
17
17
|
codex: {
|
|
18
|
-
profile: "
|
|
18
|
+
profile: "gpt61_sol_high",
|
|
19
19
|
timeoutSec: 1080,
|
|
20
20
|
runMode: "fg",
|
|
21
21
|
opusOversight: false,
|
|
@@ -31,7 +31,7 @@ const policy = {
|
|
|
31
31
|
subcommand: "exec",
|
|
32
32
|
},
|
|
33
33
|
cleanup: {
|
|
34
|
-
profile: "
|
|
34
|
+
profile: "gpt61_sol_med",
|
|
35
35
|
timeoutSec: 540,
|
|
36
36
|
runMode: "fg",
|
|
37
37
|
opusOversight: false,
|
|
@@ -39,7 +39,7 @@ const policy = {
|
|
|
39
39
|
subcommand: "exec",
|
|
40
40
|
},
|
|
41
41
|
deslop: {
|
|
42
|
-
profile: "
|
|
42
|
+
profile: "gpt61_sol_med",
|
|
43
43
|
timeoutSec: 540,
|
|
44
44
|
runMode: "fg",
|
|
45
45
|
opusOversight: false,
|
|
@@ -95,7 +95,7 @@ const policy = {
|
|
|
95
95
|
subcommand: "exec",
|
|
96
96
|
},
|
|
97
97
|
"code-reviewer": {
|
|
98
|
-
profile: "
|
|
98
|
+
profile: "gpt61_sol_high",
|
|
99
99
|
timeoutSec: 1800,
|
|
100
100
|
runMode: "bg",
|
|
101
101
|
opusOversight: false,
|
|
@@ -103,7 +103,7 @@ const policy = {
|
|
|
103
103
|
subcommand: "review",
|
|
104
104
|
},
|
|
105
105
|
"quality-reviewer": {
|
|
106
|
-
profile: "
|
|
106
|
+
profile: "gpt61_sol_high",
|
|
107
107
|
timeoutSec: 1800,
|
|
108
108
|
runMode: "bg",
|
|
109
109
|
opusOversight: false,
|
|
@@ -119,7 +119,7 @@ const policy = {
|
|
|
119
119
|
subcommand: "review",
|
|
120
120
|
},
|
|
121
121
|
scientist: {
|
|
122
|
-
profile: "
|
|
122
|
+
profile: "gpt61_sol_high",
|
|
123
123
|
timeoutSec: 1440,
|
|
124
124
|
runMode: "bg",
|
|
125
125
|
opusOversight: false,
|
|
@@ -127,7 +127,7 @@ const policy = {
|
|
|
127
127
|
subcommand: "exec",
|
|
128
128
|
},
|
|
129
129
|
"document-specialist": {
|
|
130
|
-
profile: "
|
|
130
|
+
profile: "gpt61_sol_high",
|
|
131
131
|
timeoutSec: 1440,
|
|
132
132
|
runMode: "bg",
|
|
133
133
|
opusOversight: false,
|
|
@@ -143,7 +143,7 @@ const policy = {
|
|
|
143
143
|
subcommand: "exec",
|
|
144
144
|
},
|
|
145
145
|
verifier: {
|
|
146
|
-
profile: "
|
|
146
|
+
profile: "gpt61_sol_high",
|
|
147
147
|
timeoutSec: 1200,
|
|
148
148
|
runMode: "fg",
|
|
149
149
|
opusOversight: false,
|
|
@@ -151,7 +151,7 @@ const policy = {
|
|
|
151
151
|
subcommand: "review",
|
|
152
152
|
},
|
|
153
153
|
"test-engineer": {
|
|
154
|
-
profile: "
|
|
154
|
+
profile: "gpt61_sol_high",
|
|
155
155
|
timeoutSec: 1200,
|
|
156
156
|
runMode: "bg",
|
|
157
157
|
opusOversight: false,
|
|
@@ -159,7 +159,7 @@ const policy = {
|
|
|
159
159
|
subcommand: "exec",
|
|
160
160
|
},
|
|
161
161
|
"qa-tester": {
|
|
162
|
-
profile: "
|
|
162
|
+
profile: "gpt61_sol_high",
|
|
163
163
|
timeoutSec: 1200,
|
|
164
164
|
runMode: "bg",
|
|
165
165
|
opusOversight: false,
|
|
@@ -212,8 +212,8 @@ export const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
|
|
|
212
212
|
new Set([
|
|
213
213
|
"gpt6_luna_low",
|
|
214
214
|
"gpt6_luna_high",
|
|
215
|
-
"
|
|
216
|
-
"
|
|
215
|
+
"gpt61_sol_med",
|
|
216
|
+
"gpt61_sol_high",
|
|
217
217
|
"gpt6_astra_xhigh",
|
|
218
218
|
"gpt6_astra_max",
|
|
219
219
|
"gpt6_astra_ultra",
|
|
@@ -21,22 +21,25 @@ const EFFORT_BY_SUFFIX = {
|
|
|
21
21
|
const CANONICAL_PROFILE_VALUES = Object.freeze({
|
|
22
22
|
gpt6_luna_low: { model: "gpt-6-luna", effort: "low" },
|
|
23
23
|
gpt6_luna_high: { model: "gpt-6-luna", effort: "high" },
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
gpt61_sol_med: { model: "gpt-6.1-sol", effort: "medium" },
|
|
25
|
+
gpt61_sol_high: { model: "gpt-6.1-sol", effort: "high" },
|
|
26
26
|
gpt6_astra_xhigh: { model: "gpt-6-astra", effort: "xhigh" },
|
|
27
27
|
gpt6_astra_max: { model: "gpt-6-astra", effort: "max" },
|
|
28
28
|
gpt6_astra_ultra: { model: "gpt-6-astra", effort: "ultra" },
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
// GPT-5.6 lane names map to the GPT-6 lane with the same effort. GPT-6 has no
|
|
32
|
-
// Terra tier, so Terra lanes move to Sol.
|
|
32
|
+
// Terra tier, so Terra lanes move to Sol. GPT-6 Sol lane names map to GPT-6.1
|
|
33
|
+
// Sol with the same effort.
|
|
33
34
|
const LEGACY_CANONICAL_PROFILE_ALIASES = Object.freeze({
|
|
34
35
|
gpt56_sol_xhigh: "gpt6_astra_xhigh",
|
|
35
36
|
gpt56_sol_max: "gpt6_astra_max",
|
|
36
37
|
gpt56_sol_ultra: "gpt6_astra_ultra",
|
|
37
|
-
gpt56_terra_high: "
|
|
38
|
-
gpt56_terra_med: "
|
|
38
|
+
gpt56_terra_high: "gpt61_sol_high",
|
|
39
|
+
gpt56_terra_med: "gpt61_sol_med",
|
|
39
40
|
gpt56_luna_low: "gpt6_luna_low",
|
|
41
|
+
gpt6_sol_high: "gpt61_sol_high",
|
|
42
|
+
gpt6_sol_med: "gpt61_sol_med",
|
|
40
43
|
});
|
|
41
44
|
|
|
42
45
|
export function normalizeCodexProfileName(profileName) {
|
|
@@ -65,7 +68,7 @@ function readProfileScalar(raw, key) {
|
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
/**
|
|
68
|
-
* Resolve a Codex effort profile name (e.g. "
|
|
71
|
+
* Resolve a Codex effort profile name (e.g. "gpt61_sol_high") to the `-c key=value`
|
|
69
72
|
* config overrides that `codex exec` accepts, so the headless CLI lanes can
|
|
70
73
|
* select a profile WITHOUT `codex exec --profile <name>`. codex 0.134+ rejects
|
|
71
74
|
* `--profile X` whenever config.toml still contains an inline `[profiles.X]`
|
|
@@ -83,7 +86,7 @@ function readProfileScalar(raw, key) {
|
|
|
83
86
|
*
|
|
84
87
|
* @param {string} profileName
|
|
85
88
|
* @param {{ codexHome?: string }} [opts]
|
|
86
|
-
* @returns {string[]} e.g. ['model="gpt-6-sol"', 'model_reasoning_effort="high"']
|
|
89
|
+
* @returns {string[]} e.g. ['model="gpt-6.1-sol"', 'model_reasoning_effort="high"']
|
|
87
90
|
*/
|
|
88
91
|
export function resolveCodexProfileConfigValues(profileName, opts = {}) {
|
|
89
92
|
if (typeof profileName !== "string" || !profileName) {
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
// hub/team/git-preflight.mjs — Pre-flight safety check for dangerous git ops.
|
|
2
|
-
// Blocks checkout/rebase/cherry-pick/reset/stash-pop/worktree-remove when they
|
|
3
|
-
// would conflict with other active Synapse sessions' dirty files or claimed
|
|
4
|
-
// leases. Fail-open by default: if registry/locks are unavailable, allow the op
|
|
5
|
-
// with a warning (worktree isolation is the baseline safety net).
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Factory for git pre-flight check.
|
|
9
|
-
*
|
|
10
|
-
* @param {object} opts
|
|
11
|
-
* @param {{ getActive: () => Array<object> }} opts.registry — synapse-registry duck type
|
|
12
|
-
* @param {{ snapshot: () => Array<object> }} opts.locks — swarm-locks duck type
|
|
13
|
-
* @param {boolean} [opts.failOpen=true] — allow op when registry/locks throw or are missing
|
|
14
|
-
* @param {(level: string, msg: string, data?: object) => void} [opts.logger] — optional logger
|
|
15
|
-
* @returns {{
|
|
16
|
-
* check: Function,
|
|
17
|
-
* checkRebase: Function,
|
|
18
|
-
* checkCheckout: Function,
|
|
19
|
-
* checkCherryPick: Function,
|
|
20
|
-
* checkReset: Function,
|
|
21
|
-
* checkStashPop: Function,
|
|
22
|
-
* checkWorktreeRemove: Function,
|
|
23
|
-
* }}
|
|
24
|
-
*/
|
|
25
|
-
export function createGitPreflight(opts = {}) {
|
|
26
|
-
const { registry, locks, failOpen = true, logger = null } = opts;
|
|
27
|
-
|
|
28
|
-
function log(level, msg, data) {
|
|
29
|
-
if (logger) {
|
|
30
|
-
try {
|
|
31
|
-
logger(level, msg, data);
|
|
32
|
-
} catch {
|
|
33
|
-
/* logger failure is non-fatal */
|
|
34
|
-
}
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (level === "warn") {
|
|
38
|
-
console.warn(`[git-preflight] ${msg}`, data ?? "");
|
|
39
|
-
} else if (level === "error") {
|
|
40
|
-
console.error(`[git-preflight] ${msg}`, data ?? "");
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// ── helpers ──────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
function safeGetActive() {
|
|
47
|
-
try {
|
|
48
|
-
const value = registry?.getActive?.();
|
|
49
|
-
if (!Array.isArray(value)) return null;
|
|
50
|
-
return value;
|
|
51
|
-
} catch (err) {
|
|
52
|
-
log("warn", "registry.getActive threw", { error: err?.message });
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function safeSnapshot() {
|
|
58
|
-
try {
|
|
59
|
-
const value = locks?.snapshot?.();
|
|
60
|
-
if (!Array.isArray(value)) return null;
|
|
61
|
-
return value;
|
|
62
|
-
} catch (err) {
|
|
63
|
-
log("warn", "locks.snapshot threw", { error: err?.message });
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function failOpenDecision(reason) {
|
|
69
|
-
log("warn", "hub unavailable, failing open", { reason });
|
|
70
|
-
return { allowed: true, reason: "hub_unavailable_fail_open" };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function normalizeFiles(list) {
|
|
74
|
-
if (!Array.isArray(list)) return new Set();
|
|
75
|
-
return new Set(
|
|
76
|
-
list
|
|
77
|
-
.filter((f) => typeof f === "string" && f.length > 0)
|
|
78
|
-
.map((f) => f.replace(/\\/g, "/")),
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function otherActiveSessions(active, sessionId) {
|
|
83
|
-
return active.filter((s) => s?.sessionId && s.sessionId !== sessionId);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function otherLeases(snapshot, workerId) {
|
|
87
|
-
return snapshot.filter(
|
|
88
|
-
(entry) => entry?.workerId && entry.workerId !== workerId,
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function buildConflict(file, session, holder) {
|
|
93
|
-
return {
|
|
94
|
-
file,
|
|
95
|
-
activeSession: session?.sessionId ?? holder ?? "unknown",
|
|
96
|
-
activeTask: session?.taskSummary ?? "",
|
|
97
|
-
...(holder ? { leaseHolder: holder } : {}),
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function recommend(conflicts) {
|
|
102
|
-
if (!conflicts.length) return "";
|
|
103
|
-
const first = conflicts[0];
|
|
104
|
-
const owner = first.leaseHolder || first.activeSession;
|
|
105
|
-
const task = first.activeTask ? ` Active task: '${first.activeTask}'` : "";
|
|
106
|
-
return `Wait for '${owner}' to finish, or coordinate via HITL.${task}`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function blockedDecision(op, conflicts, extraReason) {
|
|
110
|
-
return {
|
|
111
|
-
allowed: false,
|
|
112
|
-
reason: extraReason || "overlap_with_active_session",
|
|
113
|
-
conflicts,
|
|
114
|
-
recommendation: recommend(conflicts),
|
|
115
|
-
op,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function allowedDecision() {
|
|
120
|
-
return { allowed: true };
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ── shared scanners ─────────────────────────────────────
|
|
124
|
-
|
|
125
|
-
function findDirtyFileConflicts(sessionId, workerId) {
|
|
126
|
-
const active = safeGetActive();
|
|
127
|
-
const snapshot = safeSnapshot();
|
|
128
|
-
|
|
129
|
-
if (active == null || snapshot == null) {
|
|
130
|
-
return failOpen ? { fallOpen: true } : { fallOpen: false, conflicts: [] };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const conflicts = [];
|
|
134
|
-
const seen = new Set();
|
|
135
|
-
|
|
136
|
-
for (const session of otherActiveSessions(active, sessionId)) {
|
|
137
|
-
const dirty = normalizeFiles(session.dirtyFiles);
|
|
138
|
-
for (const file of dirty) {
|
|
139
|
-
if (seen.has(file)) continue;
|
|
140
|
-
seen.add(file);
|
|
141
|
-
conflicts.push(buildConflict(file, session));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
for (const lease of otherLeases(snapshot, workerId)) {
|
|
146
|
-
if (lease.leaseType && lease.leaseType !== "exclusive") continue;
|
|
147
|
-
const file = typeof lease.file === "string" ? lease.file : null;
|
|
148
|
-
if (!file || seen.has(file)) continue;
|
|
149
|
-
seen.add(file);
|
|
150
|
-
const sessionMeta = lease.sessionMeta || null;
|
|
151
|
-
conflicts.push(
|
|
152
|
-
buildConflict(
|
|
153
|
-
file,
|
|
154
|
-
sessionMeta
|
|
155
|
-
? {
|
|
156
|
-
sessionId: sessionMeta.sessionId || lease.workerId,
|
|
157
|
-
taskSummary: sessionMeta.taskSummary || "",
|
|
158
|
-
}
|
|
159
|
-
: null,
|
|
160
|
-
lease.workerId,
|
|
161
|
-
),
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return { conflicts };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function findTargetFileConflicts(targetFiles, sessionId, workerId) {
|
|
169
|
-
const active = safeGetActive();
|
|
170
|
-
const snapshot = safeSnapshot();
|
|
171
|
-
|
|
172
|
-
if (active == null || snapshot == null) {
|
|
173
|
-
return failOpen ? { fallOpen: true } : { fallOpen: false, conflicts: [] };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const targets = normalizeFiles(targetFiles);
|
|
177
|
-
if (targets.size === 0) return { conflicts: [] };
|
|
178
|
-
|
|
179
|
-
const conflicts = [];
|
|
180
|
-
const seen = new Set();
|
|
181
|
-
|
|
182
|
-
for (const session of otherActiveSessions(active, sessionId)) {
|
|
183
|
-
const dirty = normalizeFiles(session.dirtyFiles);
|
|
184
|
-
for (const file of dirty) {
|
|
185
|
-
if (!targets.has(file) || seen.has(file)) continue;
|
|
186
|
-
seen.add(file);
|
|
187
|
-
conflicts.push(buildConflict(file, session));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
for (const lease of otherLeases(snapshot, workerId)) {
|
|
192
|
-
if (lease.leaseType && lease.leaseType !== "exclusive") continue;
|
|
193
|
-
const file = typeof lease.file === "string" ? lease.file : null;
|
|
194
|
-
if (!file || !targets.has(file) || seen.has(file)) continue;
|
|
195
|
-
seen.add(file);
|
|
196
|
-
const sessionMeta = lease.sessionMeta || null;
|
|
197
|
-
conflicts.push(
|
|
198
|
-
buildConflict(
|
|
199
|
-
file,
|
|
200
|
-
sessionMeta
|
|
201
|
-
? {
|
|
202
|
-
sessionId: sessionMeta.sessionId || lease.workerId,
|
|
203
|
-
taskSummary: sessionMeta.taskSummary || "",
|
|
204
|
-
}
|
|
205
|
-
: null,
|
|
206
|
-
lease.workerId,
|
|
207
|
-
),
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return { conflicts };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// ── public API ──────────────────────────────────────────
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Core pre-flight check.
|
|
218
|
-
*
|
|
219
|
-
* @param {"checkout"|"rebase"|"cherry-pick"|"reset"|"stash-pop"|"worktree-remove"} op
|
|
220
|
-
* @param {{ targetFiles?: string[], branch?: string, ref?: string, worktreePath?: string }} args
|
|
221
|
-
* @param {{ sessionId: string, workerId?: string }} sessionContext
|
|
222
|
-
* @returns {{ allowed: boolean, reason?: string, conflicts?: object[], recommendation?: string, op?: string }}
|
|
223
|
-
*/
|
|
224
|
-
function check(op, args = {}, sessionContext = {}) {
|
|
225
|
-
const sessionId = sessionContext.sessionId || "";
|
|
226
|
-
const workerId = sessionContext.workerId || sessionId;
|
|
227
|
-
|
|
228
|
-
switch (op) {
|
|
229
|
-
case "checkout":
|
|
230
|
-
case "rebase":
|
|
231
|
-
case "reset":
|
|
232
|
-
case "stash-pop": {
|
|
233
|
-
const res = findDirtyFileConflicts(sessionId, workerId);
|
|
234
|
-
if (res.fallOpen) return failOpenDecision(op);
|
|
235
|
-
if (res.conflicts.length > 0) return blockedDecision(op, res.conflicts);
|
|
236
|
-
return allowedDecision();
|
|
237
|
-
}
|
|
238
|
-
case "cherry-pick": {
|
|
239
|
-
const targets = Array.isArray(args.targetFiles) ? args.targetFiles : [];
|
|
240
|
-
const res = findTargetFileConflicts(targets, sessionId, workerId);
|
|
241
|
-
if (res.fallOpen) return failOpenDecision(op);
|
|
242
|
-
if (res.conflicts.length > 0) return blockedDecision(op, res.conflicts);
|
|
243
|
-
return allowedDecision();
|
|
244
|
-
}
|
|
245
|
-
case "worktree-remove": {
|
|
246
|
-
const active = safeGetActive();
|
|
247
|
-
if (active == null) return failOpenDecision(op);
|
|
248
|
-
const target =
|
|
249
|
-
typeof args.worktreePath === "string"
|
|
250
|
-
? args.worktreePath.replace(/\\/g, "/")
|
|
251
|
-
: "";
|
|
252
|
-
if (!target) return allowedDecision();
|
|
253
|
-
const conflicts = [];
|
|
254
|
-
for (const session of otherActiveSessions(active, sessionId)) {
|
|
255
|
-
const sessionPath =
|
|
256
|
-
typeof session.worktreePath === "string"
|
|
257
|
-
? session.worktreePath.replace(/\\/g, "/")
|
|
258
|
-
: "";
|
|
259
|
-
if (sessionPath && sessionPath === target) {
|
|
260
|
-
conflicts.push({
|
|
261
|
-
file: target,
|
|
262
|
-
activeSession: session.sessionId,
|
|
263
|
-
activeTask: session.taskSummary || "",
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
if (conflicts.length > 0) {
|
|
268
|
-
return blockedDecision(op, conflicts, "active_worktree");
|
|
269
|
-
}
|
|
270
|
-
return allowedDecision();
|
|
271
|
-
}
|
|
272
|
-
default:
|
|
273
|
-
return allowedDecision();
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
return Object.freeze({
|
|
278
|
-
check,
|
|
279
|
-
checkRebase: (args, ctx) => check("rebase", args, ctx),
|
|
280
|
-
checkCheckout: (args, ctx) => check("checkout", args, ctx),
|
|
281
|
-
checkCherryPick: (args, ctx) => check("cherry-pick", args, ctx),
|
|
282
|
-
checkReset: (args, ctx) => check("reset", args, ctx),
|
|
283
|
-
checkStashPop: (args, ctx) => check("stash-pop", args, ctx),
|
|
284
|
-
checkWorktreeRemove: (args, ctx) => check("worktree-remove", args, ctx),
|
|
285
|
-
});
|
|
286
|
-
}
|