triflux 10.40.0 → 10.41.1
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/bin/tfx-doctor.mjs +6 -1
- package/bin/tfx-live.mjs +13 -4
- package/bin/tfx-setup.mjs +6 -1
- package/hub/router.mjs +13 -0
- package/hub/workers/claude-worker.mjs +3 -0
- package/package.json +1 -1
- package/scripts/__tests__/release-governance.test.mjs +49 -2
- package/scripts/release/publish.mjs +90 -5
- package/scripts/tfx-route-worker.mjs +5 -0
- package/scripts/tfx-route.sh +27 -1
- package/skills/tfx-auto/SKILL.md +12 -8
- package/skills/tfx-consensus/SKILL.md +1 -0
- package/skills/tfx-debate/SKILL.md +2 -2
- package/skills/tfx-panel/SKILL.md +1 -1
package/bin/tfx-doctor.mjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// tfx-doctor — triflux doctor 바로가기
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
3
5
|
process.argv = [
|
|
4
6
|
process.argv[0],
|
|
5
|
-
|
|
7
|
+
// triflux.mjs's own isMainModule() guard compares this against its real
|
|
8
|
+
// path, not this shim's — point argv[1] at triflux.mjs so it recognizes
|
|
9
|
+
// itself as the entry point instead of silently no-op'ing.
|
|
10
|
+
fileURLToPath(new URL("./triflux.mjs", import.meta.url)),
|
|
6
11
|
"doctor",
|
|
7
12
|
...process.argv.slice(2),
|
|
8
13
|
];
|
package/bin/tfx-live.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
+
import { realpathSync } from "node:fs";
|
|
3
4
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
5
|
import { homedir, tmpdir } from "node:os";
|
|
5
6
|
import { join as pathJoin, resolve as pathResolve } from "node:path";
|
|
@@ -2368,11 +2369,19 @@ export {
|
|
|
2368
2369
|
resolveAskTransport,
|
|
2369
2370
|
};
|
|
2370
2371
|
|
|
2371
|
-
|
|
2372
|
-
process.argv[1]
|
|
2373
|
-
fileURLToPath(import.meta.url)
|
|
2372
|
+
function isMainModule() {
|
|
2373
|
+
if (!process.argv[1]) return false;
|
|
2374
|
+
const modulePath = fileURLToPath(import.meta.url);
|
|
2375
|
+
try {
|
|
2376
|
+
return realpathSync(process.argv[1]) === modulePath;
|
|
2377
|
+
} catch {
|
|
2378
|
+
// process.argv[1] doesn't resolve on disk (e.g. `node -e`) — fall back
|
|
2379
|
+
// to a non-symlink-aware comparison instead of treating it as not-main.
|
|
2380
|
+
return pathResolve(process.argv[1]) === modulePath;
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2374
2383
|
|
|
2375
|
-
if (
|
|
2384
|
+
if (isMainModule()) {
|
|
2376
2385
|
main().catch((error) => {
|
|
2377
2386
|
printJson({
|
|
2378
2387
|
ok: false,
|
package/bin/tfx-setup.mjs
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// tfx-setup — triflux setup 바로가기
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
3
5
|
process.argv = [
|
|
4
6
|
process.argv[0],
|
|
5
|
-
|
|
7
|
+
// triflux.mjs's own isMainModule() guard compares this against its real
|
|
8
|
+
// path, not this shim's — point argv[1] at triflux.mjs so it recognizes
|
|
9
|
+
// itself as the entry point instead of silently no-op'ing.
|
|
10
|
+
fileURLToPath(new URL("./triflux.mjs", import.meta.url)),
|
|
6
11
|
"setup",
|
|
7
12
|
...process.argv.slice(2),
|
|
8
13
|
];
|
package/hub/router.mjs
CHANGED
|
@@ -1343,6 +1343,7 @@ export function createRouter(store) {
|
|
|
1343
1343
|
staleTimer = setInterval(() => {
|
|
1344
1344
|
try {
|
|
1345
1345
|
store.sweepStaleAgents();
|
|
1346
|
+
router.reelectStaleRoles();
|
|
1346
1347
|
} catch {}
|
|
1347
1348
|
}, 120000);
|
|
1348
1349
|
sweepTimer.unref();
|
|
@@ -1360,6 +1361,18 @@ export function createRouter(store) {
|
|
|
1360
1361
|
}
|
|
1361
1362
|
},
|
|
1362
1363
|
|
|
1364
|
+
reelectStaleRoles({ reason = "sweep" } = {}) {
|
|
1365
|
+
for (const roleName of ROLE_TOPICS) {
|
|
1366
|
+
const role = roleStates.get(roleName);
|
|
1367
|
+
const leader = role?.leaderAgentId
|
|
1368
|
+
? buildRoleCandidate(role.leaderAgentId, roleName)
|
|
1369
|
+
: null;
|
|
1370
|
+
if (!isCandidateLive(leader)) {
|
|
1371
|
+
ensureRoleLeader(roleName, { reason });
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
},
|
|
1375
|
+
|
|
1363
1376
|
getQueueDepths() {
|
|
1364
1377
|
const counts = { urgent: 0, normal: 0, dlq: store.getAuditStats().dlq };
|
|
1365
1378
|
for (const record of liveMessages.values()) {
|
|
@@ -113,6 +113,7 @@ function buildClaudeArgs(worker, options) {
|
|
|
113
113
|
if (options.includePartialMessages) args.push("--include-partial-messages");
|
|
114
114
|
if (options.replayUserMessages) args.push("--replay-user-messages");
|
|
115
115
|
if (options.model) args.push("--model", options.model);
|
|
116
|
+
if (options.effort) args.push("--effort", options.effort);
|
|
116
117
|
if (options.allowDangerouslySkipPermissions)
|
|
117
118
|
args.push("--dangerously-skip-permissions");
|
|
118
119
|
if (options.permissionMode)
|
|
@@ -143,6 +144,7 @@ export class ClaudeWorker {
|
|
|
143
144
|
this.cwd = options.cwd || process.cwd();
|
|
144
145
|
this.env = { ...process.env, ...(options.env || {}) };
|
|
145
146
|
this.model = options.model || null;
|
|
147
|
+
this.effort = options.effort || null;
|
|
146
148
|
this.permissionMode = options.permissionMode || null;
|
|
147
149
|
this.allowDangerouslySkipPermissions =
|
|
148
150
|
options.allowDangerouslySkipPermissions !== false;
|
|
@@ -323,6 +325,7 @@ export class ClaudeWorker {
|
|
|
323
325
|
|
|
324
326
|
const args = buildClaudeArgs(this, {
|
|
325
327
|
model: this.model,
|
|
328
|
+
effort: this.effort,
|
|
326
329
|
permissionMode: this.permissionMode,
|
|
327
330
|
allowDangerouslySkipPermissions: this.allowDangerouslySkipPermissions,
|
|
328
331
|
includePartialMessages: this.includePartialMessages,
|
package/package.json
CHANGED
|
@@ -165,7 +165,13 @@ describe("release governance scripts", () => {
|
|
|
165
165
|
);
|
|
166
166
|
assert.deepEqual(
|
|
167
167
|
tagOnlyPublish.steps.map((step) => step.label),
|
|
168
|
-
[
|
|
168
|
+
[
|
|
169
|
+
"git tag",
|
|
170
|
+
"git push branch",
|
|
171
|
+
"git push tag",
|
|
172
|
+
"wait for tag",
|
|
173
|
+
"gh release create",
|
|
174
|
+
],
|
|
169
175
|
);
|
|
170
176
|
|
|
171
177
|
const executed = [];
|
|
@@ -178,6 +184,9 @@ describe("release governance scripts", () => {
|
|
|
178
184
|
execFileSyncFn: (command, args) => {
|
|
179
185
|
executed.push([command, ...args].join(" "));
|
|
180
186
|
if (command === "git" && args[0] === "rev-parse") return "tag\n";
|
|
187
|
+
if (command === "git" && args[0] === "ls-remote") {
|
|
188
|
+
return "abc123\trefs/tags/v1.2.3\n";
|
|
189
|
+
}
|
|
181
190
|
if (command === "gh" && args[0] === "release") {
|
|
182
191
|
return '{"tagName":"v1.2.3"}\n';
|
|
183
192
|
}
|
|
@@ -187,7 +196,9 @@ describe("release governance scripts", () => {
|
|
|
187
196
|
assert.equal(resumedPublish.allowExistingArtifacts, true);
|
|
188
197
|
assert.deepEqual(executed, [
|
|
189
198
|
"git rev-parse --verify v1.2.3",
|
|
190
|
-
"git push origin HEAD
|
|
199
|
+
"git push origin HEAD",
|
|
200
|
+
"git push origin v1.2.3",
|
|
201
|
+
"git ls-remote --tags origin v1.2.3",
|
|
191
202
|
"gh release view v1.2.3 --json tagName",
|
|
192
203
|
]);
|
|
193
204
|
|
|
@@ -222,6 +233,41 @@ describe("release governance scripts", () => {
|
|
|
222
233
|
}
|
|
223
234
|
});
|
|
224
235
|
|
|
236
|
+
it("tag-only(CI) 모드는 preflight로 HEAD==origin/main을 검증하고 branch push를 생략한다", async () => {
|
|
237
|
+
const root = makeRepo();
|
|
238
|
+
const prevRef = process.env.GITHUB_REF_NAME;
|
|
239
|
+
process.env.GITHUB_REF_NAME = "main";
|
|
240
|
+
try {
|
|
241
|
+
assertVersionSync({ rootDir: root, fix: true });
|
|
242
|
+
const executed = [];
|
|
243
|
+
await publishRelease({
|
|
244
|
+
rootDir: root,
|
|
245
|
+
version: "1.2.3",
|
|
246
|
+
dryRun: false,
|
|
247
|
+
publishNpm: false,
|
|
248
|
+
pushBranch: false,
|
|
249
|
+
createGithubRelease: false,
|
|
250
|
+
tagPoll: { attempts: 1, sleepFn: async () => {} },
|
|
251
|
+
execFileSyncFn: (command, args) => {
|
|
252
|
+
executed.push([command, ...args].join(" "));
|
|
253
|
+
if (command === "git" && args[0] === "rev-parse") {
|
|
254
|
+
return "deadbeef\n";
|
|
255
|
+
}
|
|
256
|
+
return "";
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
assert.ok(executed.includes("git fetch origin main"));
|
|
260
|
+
assert.ok(executed.includes("git rev-parse HEAD"));
|
|
261
|
+
assert.ok(executed.includes("git rev-parse origin/main"));
|
|
262
|
+
assert.ok(executed.includes("git push origin v1.2.3"));
|
|
263
|
+
assert.ok(!executed.includes("git push origin HEAD"));
|
|
264
|
+
} finally {
|
|
265
|
+
if (prevRef === undefined) delete process.env.GITHUB_REF_NAME;
|
|
266
|
+
else process.env.GITHUB_REF_NAME = prevRef;
|
|
267
|
+
rmSync(root, { recursive: true, force: true });
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
225
271
|
it("release workflows use trusted publishing and skip duplicate tag publishes", () => {
|
|
226
272
|
const releaseWorkflow = readFileSync(
|
|
227
273
|
new URL("../../.github/workflows/release.yml", import.meta.url),
|
|
@@ -230,6 +276,7 @@ describe("release governance scripts", () => {
|
|
|
230
276
|
assert.match(releaseWorkflow, /actions:\s*write/);
|
|
231
277
|
assert.match(releaseWorkflow, /node-version:\s*24/);
|
|
232
278
|
assert.match(releaseWorkflow, /publish\.mjs.*--skip-npm.*--allow-existing/);
|
|
279
|
+
assert.match(releaseWorkflow, /publish\.mjs.*--tag-only/);
|
|
233
280
|
assert.match(
|
|
234
281
|
releaseWorkflow,
|
|
235
282
|
/gh workflow run npm-publish\.yml --ref \$\{\{ github\.ref_name \}\}/,
|
|
@@ -3,6 +3,40 @@ import { join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { assertVersionSync, parseArgs, ROOT, runCommand } from "./lib.mjs";
|
|
5
5
|
|
|
6
|
+
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
|
|
8
|
+
export async function waitForRemoteTag(
|
|
9
|
+
tagName,
|
|
10
|
+
{
|
|
11
|
+
rootDir = ROOT,
|
|
12
|
+
execFileSyncFn,
|
|
13
|
+
attempts = 30,
|
|
14
|
+
intervalMs = 2000,
|
|
15
|
+
sleepFn = defaultSleep,
|
|
16
|
+
} = {},
|
|
17
|
+
) {
|
|
18
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
19
|
+
try {
|
|
20
|
+
const out = runCommand(
|
|
21
|
+
"git",
|
|
22
|
+
["ls-remote", "--tags", "origin", tagName],
|
|
23
|
+
{
|
|
24
|
+
cwd: rootDir,
|
|
25
|
+
execFileSyncFn,
|
|
26
|
+
stdio: "pipe",
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
if (String(out).includes(`refs/tags/${tagName}`)) return true;
|
|
30
|
+
} catch {
|
|
31
|
+
// Transient network/auth blips are retried below.
|
|
32
|
+
}
|
|
33
|
+
if (attempt < attempts - 1) await sleepFn(intervalMs);
|
|
34
|
+
}
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Tag ${tagName} not visible on origin after ${attempts} attempts`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
6
40
|
export async function publishRelease({
|
|
7
41
|
version,
|
|
8
42
|
rootDir = ROOT,
|
|
@@ -12,6 +46,8 @@ export async function publishRelease({
|
|
|
12
46
|
publishNpm = true,
|
|
13
47
|
provenance = false,
|
|
14
48
|
allowExistingArtifacts = false,
|
|
49
|
+
pushBranch = true,
|
|
50
|
+
tagPoll = {},
|
|
15
51
|
execFileSyncFn,
|
|
16
52
|
} = {}) {
|
|
17
53
|
const sync = assertVersionSync({ rootDir });
|
|
@@ -73,14 +109,27 @@ export async function publishRelease({
|
|
|
73
109
|
}))
|
|
74
110
|
: []),
|
|
75
111
|
{ label: "git tag", command: "git", args: ["tag", `v${releaseVersion}`] },
|
|
76
|
-
{
|
|
77
|
-
label: "git push",
|
|
78
|
-
command: "git",
|
|
79
|
-
args: ["push", "origin", "HEAD", "--tags"],
|
|
80
|
-
},
|
|
81
112
|
];
|
|
113
|
+
if (pushBranch) {
|
|
114
|
+
steps.push({
|
|
115
|
+
label: "git push branch",
|
|
116
|
+
command: "git",
|
|
117
|
+
args: ["push", "origin", "HEAD"],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
steps.push({
|
|
121
|
+
label: "git push tag",
|
|
122
|
+
command: "git",
|
|
123
|
+
args: ["push", "origin", `v${releaseVersion}`],
|
|
124
|
+
});
|
|
82
125
|
|
|
83
126
|
if (createGithubRelease) {
|
|
127
|
+
steps.push({
|
|
128
|
+
label: "wait for tag",
|
|
129
|
+
kind: "wait-tag",
|
|
130
|
+
command: "git",
|
|
131
|
+
args: ["ls-remote", "--tags", "origin", `v${releaseVersion}`],
|
|
132
|
+
});
|
|
84
133
|
steps.push({
|
|
85
134
|
label: "gh release create",
|
|
86
135
|
command: "gh",
|
|
@@ -125,11 +174,45 @@ export async function publishRelease({
|
|
|
125
174
|
};
|
|
126
175
|
|
|
127
176
|
if (!dryRun) {
|
|
177
|
+
if (!pushBranch) {
|
|
178
|
+
const branch = process.env.GITHUB_REF_NAME || "main";
|
|
179
|
+
runCommand("git", ["fetch", "origin", branch], {
|
|
180
|
+
cwd: rootDir,
|
|
181
|
+
execFileSyncFn,
|
|
182
|
+
});
|
|
183
|
+
const head = String(
|
|
184
|
+
runCommand("git", ["rev-parse", "HEAD"], {
|
|
185
|
+
cwd: rootDir,
|
|
186
|
+
execFileSyncFn,
|
|
187
|
+
stdio: "pipe",
|
|
188
|
+
}),
|
|
189
|
+
).trim();
|
|
190
|
+
const remote = String(
|
|
191
|
+
runCommand("git", ["rev-parse", `origin/${branch}`], {
|
|
192
|
+
cwd: rootDir,
|
|
193
|
+
execFileSyncFn,
|
|
194
|
+
stdio: "pipe",
|
|
195
|
+
}),
|
|
196
|
+
).trim();
|
|
197
|
+
if (head !== remote) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`Refusing tag-only publish: HEAD ${head} != origin/${branch} ${remote} (stale checkout or unpushed ${branch})`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
128
203
|
for (const step of steps) {
|
|
129
204
|
const tagName = `v${releaseVersion}`;
|
|
130
205
|
if (step.label === "git tag" && shouldSkipExistingTag(tagName)) {
|
|
131
206
|
continue;
|
|
132
207
|
}
|
|
208
|
+
if (step.kind === "wait-tag") {
|
|
209
|
+
await waitForRemoteTag(tagName, {
|
|
210
|
+
rootDir,
|
|
211
|
+
execFileSyncFn,
|
|
212
|
+
...tagPoll,
|
|
213
|
+
});
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
133
216
|
if (
|
|
134
217
|
step.label === "gh release create" &&
|
|
135
218
|
shouldSkipExistingGithubRelease(tagName)
|
|
@@ -151,6 +234,7 @@ export async function publishRelease({
|
|
|
151
234
|
publishNpm,
|
|
152
235
|
provenance,
|
|
153
236
|
allowExistingArtifacts,
|
|
237
|
+
pushBranch,
|
|
154
238
|
dryRun,
|
|
155
239
|
notesPath,
|
|
156
240
|
steps: steps.map((step) => ({
|
|
@@ -175,6 +259,7 @@ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
|
175
259
|
publishNpm: !args["skip-npm"],
|
|
176
260
|
provenance: Boolean(args.provenance || envProvenance),
|
|
177
261
|
allowExistingArtifacts: Boolean(args["allow-existing"]),
|
|
262
|
+
pushBranch: !args["tag-only"],
|
|
178
263
|
});
|
|
179
264
|
console.log(JSON.stringify(result, null, 2));
|
|
180
265
|
}
|
|
@@ -77,6 +77,10 @@ function parseArgs(argv) {
|
|
|
77
77
|
args.model = next;
|
|
78
78
|
index += 1;
|
|
79
79
|
break;
|
|
80
|
+
case "--effort":
|
|
81
|
+
args.effort = next;
|
|
82
|
+
index += 1;
|
|
83
|
+
break;
|
|
80
84
|
case "--timeout-ms":
|
|
81
85
|
args.timeoutMs = Number(next);
|
|
82
86
|
index += 1;
|
|
@@ -219,6 +223,7 @@ const worker = await createWorker(args.type, {
|
|
|
219
223
|
command: args.command,
|
|
220
224
|
commandArgs: parseJsonArray(args.commandArgsJson, "--command-args-json"),
|
|
221
225
|
model: args.model,
|
|
226
|
+
effort: args.effort,
|
|
222
227
|
timeoutMs: args.timeoutMs,
|
|
223
228
|
approvalMode: args.approvalMode,
|
|
224
229
|
permissionMode: args.permissionMode,
|
package/scripts/tfx-route.sh
CHANGED
|
@@ -1873,12 +1873,36 @@ get_claude_model() {
|
|
|
1873
1873
|
esac
|
|
1874
1874
|
}
|
|
1875
1875
|
|
|
1876
|
+
get_claude_effort() {
|
|
1877
|
+
# Claude Code exposes a separate --effort flag. Preserve high-end effort
|
|
1878
|
+
# levels instead of collapsing them into "high". `claude --effort` accepts
|
|
1879
|
+
# exactly low/medium/high/xhigh/max (an unknown value warns and falls back to
|
|
1880
|
+
# the default).
|
|
1881
|
+
case "$CLI_EFFORT" in
|
|
1882
|
+
low) echo "low" ;;
|
|
1883
|
+
medium) echo "medium" ;;
|
|
1884
|
+
high) echo "high" ;;
|
|
1885
|
+
xhigh) echo "xhigh" ;;
|
|
1886
|
+
max) echo "max" ;;
|
|
1887
|
+
*max*) echo "max" ;;
|
|
1888
|
+
*xhigh*) echo "xhigh" ;;
|
|
1889
|
+
*high*) echo "high" ;;
|
|
1890
|
+
*med*) echo "medium" ;;
|
|
1891
|
+
n/a|agy_v1|"") echo "" ;;
|
|
1892
|
+
*low*) echo "low" ;;
|
|
1893
|
+
*) echo "" ;;
|
|
1894
|
+
esac
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1876
1897
|
emit_claude_native_metadata() {
|
|
1877
1898
|
local model
|
|
1878
1899
|
model=$(get_claude_model)
|
|
1900
|
+
local effort
|
|
1901
|
+
effort=$(get_claude_effort)
|
|
1879
1902
|
echo "ROUTE_TYPE=claude-native"
|
|
1880
1903
|
echo "AGENT=$AGENT_TYPE"
|
|
1881
1904
|
echo "MODEL=$model"
|
|
1905
|
+
echo "EFFORT=$effort"
|
|
1882
1906
|
echo "RUN_MODE=$RUN_MODE"
|
|
1883
1907
|
echo "OPUS_OVERSIGHT=$OPUS_OVERSIGHT"
|
|
1884
1908
|
echo "TIMEOUT=$TIMEOUT_SEC"
|
|
@@ -3022,8 +3046,9 @@ EOF
|
|
|
3022
3046
|
fi
|
|
3023
3047
|
|
|
3024
3048
|
elif [[ "$CLI_TYPE" == "claude" ]]; then
|
|
3025
|
-
local claude_model
|
|
3049
|
+
local claude_model claude_effort
|
|
3026
3050
|
claude_model=$(get_claude_model)
|
|
3051
|
+
claude_effort=$(get_claude_effort)
|
|
3027
3052
|
local -a claude_worker_args=(
|
|
3028
3053
|
"--command" "$CLI_CMD"
|
|
3029
3054
|
"--command-args-json" "$CLAUDE_BIN_ARGS_JSON"
|
|
@@ -3031,6 +3056,7 @@ EOF
|
|
|
3031
3056
|
"--permission-mode" "bypassPermissions"
|
|
3032
3057
|
"--allow-dangerously-skip-permissions"
|
|
3033
3058
|
)
|
|
3059
|
+
[ -n "$claude_effort" ] && claude_worker_args+=("--effort" "$claude_effort")
|
|
3034
3060
|
|
|
3035
3061
|
run_stream_worker "claude" "$FULL_PROMPT" "$use_tee" "${claude_worker_args[@]}" || exit_code=$?
|
|
3036
3062
|
if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
|
package/skills/tfx-auto/SKILL.md
CHANGED
|
@@ -72,7 +72,9 @@ echo "USER_PREFERRED_MODE: ${USER_MODE:-none}"
|
|
|
72
72
|
> **MANDATORY RULES**
|
|
73
73
|
>
|
|
74
74
|
> 1. **실행**: CLI 에이전트는 반드시 `Bash("bash ~/.claude/scripts/tfx-route.sh ...")`. Claude 네이티브(explore/verifier/test-engineer/qa-tester)만 `Agent()`.
|
|
75
|
-
> 2.
|
|
75
|
+
> 2. **비용/편향 분리**: 실행 워커 비용은 Codex/Antigravity 우선으로 낮추되, consensus/debate/panel에서는 Claude/Opus를 최후수단이 아니라 counter-bias outvoice로 둔다. Claude가 실행을 맡는 경우는 명시적 `--cli claude`, Claude-native host role, 또는 fallback뿐이다.
|
|
76
|
+
> 2-a. **Anti-bias triad**: Triflux의 본가 목적은 Codex↔Claude 균형으로 단일 모델 편향을 줄이는 것이다. `triad`는 Claude/Opus(counter-bias outvoice) + Codex(implementation/reality check) + Antigravity(product/UX auxiliary)를 보존하고, disputed/minority view를 삭제하지 않는다.
|
|
77
|
+
> 2-b. **Claude effort**: Claude lane은 Claude Code CLI의 `--model`과 `--effort`를 사용한다. Triflux의 `CLI_EFFORT`/profile 값은 `low|medium|high|xhigh|max` Claude effort로 매핑한다(`ultracode`는 `claude --effort` 값이 아니라 하니스 모드이므로 기본 effort로 매핑된다). skill 문서가 모델 ID를 하드코딩하지 않는다.
|
|
76
78
|
> 3. **DAG**: SEQUENTIAL/DAG이면 레벨 기반 순차 실행. `.omc/context/{sid}/` 생성, context_output 저장, 실패 시 후속 SKIP.
|
|
77
79
|
> 4. **트리아지**: Codex `exec --full-auto` 분류 + Opus 인라인 분해. Agent 스폰 금지.
|
|
78
80
|
> 5. **thorough**: `-t`/`--thorough` 시 파이프라인 init 필수. 커맨드 숏컷은 항상 quick.
|
|
@@ -114,7 +116,7 @@ ARGUMENTS 에 아래 플래그가 있으면 Step 0 스마트 라우팅의 내부
|
|
|
114
116
|
| `--shape` | `debate` | 옵션 비교 + 점수화 + 최종 추천 | debate renderer |
|
|
115
117
|
| `--shape` | `panel` | 전문가 roster 기반 시뮬레이션 | panel renderer |
|
|
116
118
|
| `--cli-set` | `triad` (기본) | Claude + Codex + Antigravity | consensus participants |
|
|
117
|
-
| `--cli-set` | `no-antigravity` | Claude + Codex partial degrade | consensus participants |
|
|
119
|
+
| `--cli-set` | `no-antigravity` | Claude/Opus outvoice + Codex partial degrade | consensus participants |
|
|
118
120
|
| `--cli-set` | `custom` | 기존 3 CLI 내부 subset/repetition 만 허용 | consensus participants |
|
|
119
121
|
| `--options` | `"A|B|C"` | debate 비교 대상 | debate normalizer |
|
|
120
122
|
| `--criteria` | `"latency|complexity|operability"` | debate 평가 기준 | debate normalizer |
|
|
@@ -284,8 +286,8 @@ shape 의미:
|
|
|
284
286
|
|
|
285
287
|
| 값 | 의미 | 비고 |
|
|
286
288
|
|----|------|------|
|
|
287
|
-
| `triad` | Claude + Codex + Antigravity | 기본값 |
|
|
288
|
-
| `no-antigravity` | Claude + Codex | Antigravity 미가용 degrade |
|
|
289
|
+
| `triad` | Claude/Opus outvoice + Codex + Antigravity | 기본값 |
|
|
290
|
+
| `no-antigravity` | Claude/Opus outvoice + Codex | Antigravity 미가용 degrade |
|
|
289
291
|
| `custom` | 기존 3 CLI 내부 subset/repetition 만 허용 | 신규 provider 추가 금지 |
|
|
290
292
|
|
|
291
293
|
shape 입력 정규화:
|
|
@@ -331,7 +333,7 @@ shape 별 `shape_input`:
|
|
|
331
333
|
2. `--mode consensus` 확인
|
|
332
334
|
3. `--shape` 기본값 보정 (`consensus`)
|
|
333
335
|
4. shape 별 payload 정규화
|
|
334
|
-
5. Claude
|
|
336
|
+
5. Claude/Opus outvoice + headless Codex/Antigravity 동시 dispatch
|
|
335
337
|
6. 결과 수집
|
|
336
338
|
7. 공통 `meta_judgment` 생성
|
|
337
339
|
8. shape renderer 로 markdown/json 출력
|
|
@@ -386,7 +388,7 @@ shape 별 orchestration 정책:
|
|
|
386
388
|
|
|
387
389
|
- 목적: 각 participant 의 findings 를 합의/충돌 항목으로 압축하고 `FIX_FIRST` / `merge` / `defer` 같은 실행 결정을 빠르게 내린다.
|
|
388
390
|
- 수집 단위: 옵션 비교가 아니라 finding/assertion 단위다. 동일 결론이라도 근거가 다르면 separate evidence 로 보존한다.
|
|
389
|
-
- 합의 판정: 3자 중 2자 이상이 같은 remediation 또는 risk assessment 를 지지하면 provisional agreement 로 분류하고, Claude 가 최종 `resolved_items` 승격 여부를 결정한다.
|
|
391
|
+
- 합의 판정: 3자 중 2자 이상이 같은 remediation 또는 risk assessment 를 지지하면 provisional agreement 로 분류하고, Claude/Opus outvoice가 반론을 제공하고 lead가 최종 `resolved_items` 승격 여부를 결정한다.
|
|
390
392
|
- 충돌 승격: P1/P2 급 충돌은 score 와 무관하게 `user_decision_needed` 또는 `FIX_FIRST` 로 승격한다. score 가 높아도 안전 이슈를 묻지 않는다.
|
|
391
393
|
- degrade: `no-antigravity` 또는 partial timeout 시 2자 합의를 허용하되 root meta 의 `status=partial` 과 누락 participant 이유를 반드시 남긴다.
|
|
392
394
|
|
|
@@ -559,7 +561,7 @@ shape 별 orchestration 정책:
|
|
|
559
561
|
- roster 규칙: `--experts` 미지정 시 기본 roster 를 채우되 각 CLI 가 서로 다른 전문성을 대표하도록 배분한다. 동일 전문가를 중복 배정하지 않는다.
|
|
560
562
|
- 발언 구조: participant raw answer 를 그대로 이어붙이지 말고 `expert -> thesis -> supporting evidence -> concern -> recommendation` 구조로 정리한다.
|
|
561
563
|
- 합의 규칙: panel 은 unanimity 보다 "majority view + minority view + open questions" 보존이 중요하다. minority 가 P1/P2 를 제기하면 별도 `open_questions` 로 승격한다.
|
|
562
|
-
- moderator 역할: Claude 는 moderator 로서 panel synthesis 를 담당하지만, 자기 의견을 추가 participant 처럼 중복 집계하지 않는다.
|
|
564
|
+
- moderator 역할: Claude/Opus outvoice 는 moderator 로서 panel synthesis 를 담당하지만, 자기 의견을 추가 participant 처럼 중복 집계하지 않는다.
|
|
563
565
|
|
|
564
566
|
출력 schema 예시:
|
|
565
567
|
|
|
@@ -853,7 +855,9 @@ Agent(subagent_type="oh-my-claudecode:{agent}", model="{model}", prompt="{prompt
|
|
|
853
855
|
|
|
854
856
|
| 입력 | CLI | MCP |
|
|
855
857
|
|------|-----|-----|
|
|
856
|
-
| codex / executor /
|
|
858
|
+
| codex / executor / spark | Codex (high; 복잡 구현은 deep-executor/xhigh) | implement |
|
|
859
|
+
| debugger / deep-executor | Codex (xhigh) | implement |
|
|
860
|
+
| build-fixer | Codex (low) | implement |
|
|
857
861
|
| architect / planner / critic / analyst | Codex (xhigh) | analyze |
|
|
858
862
|
| scientist / document-specialist | Codex | analyze |
|
|
859
863
|
| code-reviewer / security-reviewer / quality-reviewer | Codex (review) | review |
|
|
@@ -43,6 +43,7 @@ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) tfx-consensus -> tfx-auto --mode consensus"
|
|
|
43
43
|
tfx-consensus 는 여전히 consensus family 의 canonical semantics 를 대표하지만, 별도 엔진으로 유지하지 않는다. Phase 4a 부터 공통 orchestration, participant 상태, artifact 경로, `meta_judgment` 스키마를 `tfx-auto --mode consensus` 가 소유한다.
|
|
44
44
|
|
|
45
45
|
공통 계약:
|
|
46
|
+
- Triflux anti-bias contract: `triad`는 Claude/Opus counter-bias outvoice + Codex implementation/reality check + Antigravity product/UX auxiliary 관점을 보존한다. Claude/Opus는 합의에서 minority/dispute를 지우는 최종 심판이 아니라 반론/위험/필요 증거를 명시하는 outvoice다.
|
|
46
47
|
- artifact 경로: `.omc/artifacts/consensus/<session-id>/consensus.{md,json}`
|
|
47
48
|
- 공통 메타 유틸: `hub/team/consensus-meta.mjs`
|
|
48
49
|
- 공통 root 메타: `mode`, `shape`, `topic`, `cli_set`, `participants`, `status`
|
|
@@ -48,8 +48,8 @@ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) tfx-debate -> tfx-auto --mode consensus --s
|
|
|
48
48
|
tfx-debate 의 본질은 "3-CLI 토론 엔진"이 아니라 "옵션 비교와 최종 추천을 내는 보고서 shape" 였다. Phase 4a 부터 orchestration root 는 `--mode consensus` 로 통합되고, debate 의미는 `--shape debate` 로 보존된다.
|
|
49
49
|
|
|
50
50
|
공통 규약:
|
|
51
|
-
- participants 기본값은 `triad` (Claude + Codex + Antigravity)
|
|
52
|
-
- `--cli-set no-antigravity` 시 partial consensus 로 degrade 가능
|
|
51
|
+
- participants 기본값은 `triad` (Claude/Opus counter-bias outvoice + Codex + Antigravity)
|
|
52
|
+
- `--cli-set no-antigravity` 시 Claude/Opus + Codex partial consensus 로 degrade 가능
|
|
53
53
|
- 공통 `meta_judgment` 는 `hub/team/consensus-meta.mjs` 스키마를 따른다
|
|
54
54
|
|
|
55
55
|
## 마이그레이션 가이드
|
|
@@ -48,7 +48,7 @@ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) tfx-panel -> tfx-auto --mode consensus --sh
|
|
|
48
48
|
tfx-panel 의 본질은 "패널 전용 orchestration" 이 아니라 "전문가 roster 를 주입한 뒤 panel 보고서로 렌더링하는 shape" 였다. Phase 4a 부터 공통 합의 루프는 `--mode consensus` 아래에 남기고, 전문가 분배와 panel renderer 만 `--shape panel` 이 책임진다.
|
|
49
49
|
|
|
50
50
|
공통 규약:
|
|
51
|
-
- participants 기본값은 `triad`
|
|
51
|
+
- participants 기본값은 `triad` (Claude/Opus counter-bias outvoice + Codex + Antigravity)
|
|
52
52
|
- 공통 `meta_judgment` 는 `mode_specific_meta.panel_size`, `mode_specific_meta.expert_distribution` 만 shape 확장으로 추가한다
|
|
53
53
|
- artifact 경로는 `.omc/artifacts/consensus/<session-id>/panel.{md,json}` 로 통일한다
|
|
54
54
|
|