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.
@@ -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
- process.argv[1],
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
- const isDirectRun =
2372
- process.argv[1] &&
2373
- fileURLToPath(import.meta.url) === pathResolve(process.argv[1]);
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 (isDirectRun) {
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
- process.argv[1],
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "triflux",
3
- "version": "10.40.0",
3
+ "version": "10.41.1",
4
4
  "description": "CLI-first multi-model orchestrator for Claude Code — route tasks to Codex, Antigravity, and Claude",
5
5
  "type": "module",
6
6
  "bin": {
@@ -165,7 +165,13 @@ describe("release governance scripts", () => {
165
165
  );
166
166
  assert.deepEqual(
167
167
  tagOnlyPublish.steps.map((step) => step.label),
168
- ["git tag", "git push", "gh release create"],
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 --tags",
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,
@@ -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
@@ -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. **비용**: Codex 우선 Antigravity Claude 최후 수단. `claude` 선택 "Codex로 가능한가?" 재확인.
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 native + headless Codex/Antigravity 동시 dispatch
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 / build-fixer / spark / debugger / deep-executor | Codex | implement |
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