triflux 10.30.0 → 10.31.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 +11 -0
- package/hub/team/synapse-registry.mjs +83 -1
- package/hub/workers/codex-mcp.mjs +87 -3
- package/package.json +1 -1
- package/scripts/tfx-route.sh +104 -1
- package/skills/tfx-auto/SKILL.md +33 -1
- package/skills/tfx-goal-clarify/SKILL.md +382 -0
package/hub/server.mjs
CHANGED
|
@@ -2054,6 +2054,16 @@ export async function startHub({
|
|
|
2054
2054
|
}, 60000);
|
|
2055
2055
|
sessionTimer.unref();
|
|
2056
2056
|
|
|
2057
|
+
const synapsePruneTimer = setInterval(() => {
|
|
2058
|
+
try {
|
|
2059
|
+
const { count } = synapseRegistry.pruneExpired();
|
|
2060
|
+
if (count > 0) hubLog.info({ count }, "synapse.prune");
|
|
2061
|
+
} catch (err) {
|
|
2062
|
+
hubLog.warn({ err }, "synapse.prune_failed");
|
|
2063
|
+
}
|
|
2064
|
+
}, 60000);
|
|
2065
|
+
synapsePruneTimer.unref();
|
|
2066
|
+
|
|
2057
2067
|
// 고아 node.exe 프로세스 + stale spawn 세션 주기적 정리 (5분마다)
|
|
2058
2068
|
// TFX_DISABLE_ORPHAN_CLEANUP=1 로 비활성 (active SSH/swarm 세션 보호 회피용 hotfix gate)
|
|
2059
2069
|
const orphanCleanupTimer = setInterval(
|
|
@@ -2288,6 +2298,7 @@ export async function startHub({
|
|
|
2288
2298
|
router.stopSweeper();
|
|
2289
2299
|
clearInterval(hitlTimer);
|
|
2290
2300
|
clearInterval(sessionTimer);
|
|
2301
|
+
clearInterval(synapsePruneTimer);
|
|
2291
2302
|
clearInterval(rateLimitTimer);
|
|
2292
2303
|
clearInterval(orphanCleanupTimer);
|
|
2293
2304
|
if (idleTimer) {
|
|
@@ -5,6 +5,7 @@ const DEFAULT_LOCAL_HEARTBEAT_INTERVAL_MS = 5_000;
|
|
|
5
5
|
const DEFAULT_LOCAL_TIMEOUT_MS = 30_000;
|
|
6
6
|
const DEFAULT_REMOTE_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
7
7
|
const DEFAULT_REMOTE_TIMEOUT_MS = 90_000;
|
|
8
|
+
const DEFAULT_EXPIRE_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
8
9
|
// Interactive (Claude/Codex) sessions idle for long stretches; a 30s local
|
|
9
10
|
// timeout produces stale false-positives. 5-minute TTL + an `idle` state
|
|
10
11
|
// distinguishes "alive but inactive" from "presumed dead".
|
|
@@ -133,6 +134,7 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
133
134
|
remoteTimeoutMs = DEFAULT_REMOTE_TIMEOUT_MS,
|
|
134
135
|
interactiveHeartbeatIntervalMs = DEFAULT_INTERACTIVE_HEARTBEAT_INTERVAL_MS,
|
|
135
136
|
interactiveTimeoutMs = DEFAULT_INTERACTIVE_TIMEOUT_MS,
|
|
137
|
+
expireTimeoutMs = DEFAULT_EXPIRE_TIMEOUT_MS,
|
|
136
138
|
} = opts;
|
|
137
139
|
|
|
138
140
|
const sessions = new Map();
|
|
@@ -355,10 +357,89 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
355
357
|
return true;
|
|
356
358
|
}
|
|
357
359
|
|
|
360
|
+
// stale/expired 세션이 expireTimeoutMs 넘게 누적되면 Map에서 제거.
|
|
361
|
+
// live(active/idle)는 lastHeartbeat가 오래돼도 보존 — git-preflight dirty-file 가드가 의존.
|
|
362
|
+
function pruneExpired(opts2 = {}) {
|
|
363
|
+
if (destroyed) return { removed: [], count: 0 };
|
|
364
|
+
|
|
365
|
+
const cutoff =
|
|
366
|
+
typeof opts2.olderThanMs === "number"
|
|
367
|
+
? opts2.olderThanMs
|
|
368
|
+
: expireTimeoutMs;
|
|
369
|
+
const removed = [];
|
|
370
|
+
const currentTime = now();
|
|
371
|
+
|
|
372
|
+
for (const [sessionId, session] of sessions) {
|
|
373
|
+
if (
|
|
374
|
+
isLiveStatus(session.status) ||
|
|
375
|
+
currentTime - session.lastHeartbeat <= cutoff
|
|
376
|
+
) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
stopMonitor(sessionId);
|
|
381
|
+
sessions.delete(sessionId);
|
|
382
|
+
removed.push(sessionId);
|
|
383
|
+
notifyRemoved(session);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (removed.length > 0) persist();
|
|
387
|
+
return { removed, count: removed.length };
|
|
388
|
+
}
|
|
389
|
+
|
|
358
390
|
function heartbeat(sessionId, partialMeta = null) {
|
|
359
391
|
const normalized = normalizeSessionId(sessionId);
|
|
360
392
|
const session = sessions.get(normalized);
|
|
361
|
-
if (!session)
|
|
393
|
+
if (!session) {
|
|
394
|
+
// self-heal은 위치 정보(worktreePath 또는 cwd)가 있는 heartbeat에서만
|
|
395
|
+
// 발동한다. UserPromptSubmit heartbeat는 항상 worktreePath를 보내므로
|
|
396
|
+
// SessionStart register POST drop 복구 경로는 커버되고, taskSummary만 담은
|
|
397
|
+
// 빈약한 heartbeat로 위치 없는 가비지 세션이 register 되는 것은 막는다
|
|
398
|
+
// (그 경우는 기존대로 false → 400/heartbeat failed).
|
|
399
|
+
const hasLocator =
|
|
400
|
+
partialMeta &&
|
|
401
|
+
typeof partialMeta === "object" &&
|
|
402
|
+
!Array.isArray(partialMeta) &&
|
|
403
|
+
((typeof partialMeta.worktreePath === "string" &&
|
|
404
|
+
partialMeta.worktreePath.length > 0) ||
|
|
405
|
+
(typeof partialMeta.cwd === "string" && partialMeta.cwd.length > 0));
|
|
406
|
+
const canSelfHeal = Boolean(normalized) && hasLocator;
|
|
407
|
+
if (!canSelfHeal) return false;
|
|
408
|
+
|
|
409
|
+
// 미등록 세션 heartbeat = SessionStart register POST가 drop된 경우.
|
|
410
|
+
// self-heal로 register 복구(영구 미등록 방지).
|
|
411
|
+
//
|
|
412
|
+
// sessionKind는 명시값이 없으면 "interactive"로 본다. 실제 복구 경로인
|
|
413
|
+
// heartbeatInteractiveSession은 worktreePath/host/taskSummary만 보내고
|
|
414
|
+
// sessionKind는 생략하는데, sanitizeSession 기본값("headless")으로 떨어지면
|
|
415
|
+
// 복구된 Claude 세션이 30초 local timeout을 받아 idle/5분 TTL을 잃고 곧장
|
|
416
|
+
// stale로 전이돼 getActive()/peer discovery에서 사라진다(이 PRD가 고치려던
|
|
417
|
+
// 원래 증상의 재현). heartbeat 기반 liveness로 살아나는 세션은 interactive다.
|
|
418
|
+
const sanitizedKind =
|
|
419
|
+
typeof partialMeta.sessionKind === "string"
|
|
420
|
+
? partialMeta.sessionKind
|
|
421
|
+
: "interactive";
|
|
422
|
+
const healed = sanitizeSession(
|
|
423
|
+
{
|
|
424
|
+
...partialMeta,
|
|
425
|
+
sessionId: normalized,
|
|
426
|
+
sessionKind: sanitizedKind,
|
|
427
|
+
status: "active",
|
|
428
|
+
lastHeartbeat: now(),
|
|
429
|
+
},
|
|
430
|
+
normalized,
|
|
431
|
+
);
|
|
432
|
+
if (!healed) return false;
|
|
433
|
+
|
|
434
|
+
sessions.set(normalized, healed);
|
|
435
|
+
startMonitor(normalized);
|
|
436
|
+
persist();
|
|
437
|
+
emitter?.emit("synapse.session.started", {
|
|
438
|
+
sessionId: normalized,
|
|
439
|
+
session: cloneSession(healed),
|
|
440
|
+
});
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
362
443
|
|
|
363
444
|
const wasRemote = session.isRemote;
|
|
364
445
|
const updated = { ...session, lastHeartbeat: now(), status: "active" };
|
|
@@ -492,6 +573,7 @@ export function createSynapseRegistry(opts = {}) {
|
|
|
492
573
|
return Object.freeze({
|
|
493
574
|
register,
|
|
494
575
|
unregister,
|
|
576
|
+
pruneExpired,
|
|
495
577
|
heartbeat,
|
|
496
578
|
getActive,
|
|
497
579
|
getAll,
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
// hub/workers/codex-mcp.mjs — Codex MCP 서버 래퍼
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import process from "node:process";
|
|
3
6
|
import { fileURLToPath } from "node:url";
|
|
4
7
|
|
|
@@ -94,13 +97,78 @@ function normalizeStructuredContent(structuredContent, fallbackText = "") {
|
|
|
94
97
|
return { threadId, content };
|
|
95
98
|
}
|
|
96
99
|
|
|
97
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Read a top-level TOML scalar (`key = "value"` / `key = 'value'` /
|
|
102
|
+
* `key = value`), tolerating an inline `# comment` and surrounding whitespace.
|
|
103
|
+
* Returns the trimmed value, or null when the key is absent/empty. Intentionally
|
|
104
|
+
* tiny — the per-profile files only carry `model` + `model_reasoning_effort`.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} raw
|
|
107
|
+
* @param {string} key
|
|
108
|
+
* @returns {string|null}
|
|
109
|
+
*/
|
|
110
|
+
function readTomlScalar(raw, key) {
|
|
111
|
+
const re = new RegExp(
|
|
112
|
+
`^\\s*${key}\\s*=\\s*(?:"([^"\\n]*)"|'([^'\\n]*)'|([^#\\n]*?))\\s*(?:#.*)?$`,
|
|
113
|
+
"m",
|
|
114
|
+
);
|
|
115
|
+
const match = raw.match(re);
|
|
116
|
+
if (!match) return null;
|
|
117
|
+
const value = (match[1] ?? match[2] ?? match[3] ?? "").trim();
|
|
118
|
+
return value || null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve a Codex effort profile name (e.g. "gpt55_high") to its concrete
|
|
123
|
+
* `model` + reasoning effort. SSOT = the per-profile file written by
|
|
124
|
+
* `tfx setup` at `~/.codex/<profile>.config.toml`. Falls back to deriving the
|
|
125
|
+
* effort from the `<...>_<effort>` naming convention when the file is absent.
|
|
126
|
+
*
|
|
127
|
+
* codex 0.137 removed `profile` from the Codex MCP tool schema, so we translate
|
|
128
|
+
* the profile into the still-accepted `model` + `config` fields instead of
|
|
129
|
+
* forwarding `profile` (which now hard-fails with "unknown field `profile`").
|
|
130
|
+
*
|
|
131
|
+
* @param {string} profileName
|
|
132
|
+
* @returns {{ model: string|null, reasoningEffort: string|null }}
|
|
133
|
+
*/
|
|
134
|
+
export function resolveCodexProfileConfig(profileName) {
|
|
135
|
+
const result = { model: null, reasoningEffort: null };
|
|
136
|
+
if (typeof profileName !== "string" || !profileName) return result;
|
|
137
|
+
|
|
138
|
+
const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
139
|
+
try {
|
|
140
|
+
const raw = readFileSync(
|
|
141
|
+
join(codexHome, `${profileName}.config.toml`),
|
|
142
|
+
"utf8",
|
|
143
|
+
);
|
|
144
|
+
result.model = readTomlScalar(raw, "model");
|
|
145
|
+
result.reasoningEffort = readTomlScalar(raw, "model_reasoning_effort");
|
|
146
|
+
} catch {
|
|
147
|
+
// Profile file absent (non-file alias). Derive effort from the naming
|
|
148
|
+
// convention; leave model unset so codex uses its config.toml default.
|
|
149
|
+
const suffix = /_(xhigh|high|med|medium|low)$/.exec(profileName);
|
|
150
|
+
if (suffix) {
|
|
151
|
+
const map = {
|
|
152
|
+
xhigh: "xhigh",
|
|
153
|
+
high: "high",
|
|
154
|
+
med: "medium",
|
|
155
|
+
medium: "medium",
|
|
156
|
+
low: "low",
|
|
157
|
+
};
|
|
158
|
+
result.reasoningEffort = map[suffix[1]] || null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function buildCodexArguments(prompt, opts = {}) {
|
|
98
165
|
const args = { prompt };
|
|
99
166
|
|
|
100
167
|
if (typeof opts.cwd === "string" && opts.cwd) args.cwd = opts.cwd;
|
|
101
168
|
if (typeof opts.model === "string" && opts.model) args.model = opts.model;
|
|
102
|
-
|
|
103
|
-
|
|
169
|
+
// NOTE: `profile` is intentionally NOT forwarded as a Codex tool argument.
|
|
170
|
+
// codex 0.137 dropped it from the tool schema; it is resolved into
|
|
171
|
+
// `model` + `config.model_reasoning_effort` just before return (see below).
|
|
104
172
|
if (typeof opts.approvalPolicy === "string" && opts.approvalPolicy) {
|
|
105
173
|
args["approval-policy"] = opts.approvalPolicy;
|
|
106
174
|
}
|
|
@@ -120,6 +188,22 @@ function buildCodexArguments(prompt, opts = {}) {
|
|
|
120
188
|
args["compact-prompt"] = opts.compactPrompt;
|
|
121
189
|
}
|
|
122
190
|
|
|
191
|
+
// Map the effort profile to model + reasoning effort instead of forwarding the
|
|
192
|
+
// (0.137-rejected) `profile` field. Explicit opts.model wins over the profile;
|
|
193
|
+
// profile effort merges on top of any opts.config.
|
|
194
|
+
if (typeof opts.profile === "string" && opts.profile) {
|
|
195
|
+
const resolved = resolveCodexProfileConfig(opts.profile);
|
|
196
|
+
if (resolved.model && typeof args.model !== "string") {
|
|
197
|
+
args.model = resolved.model;
|
|
198
|
+
}
|
|
199
|
+
if (resolved.reasoningEffort) {
|
|
200
|
+
args.config = {
|
|
201
|
+
...(args.config || {}),
|
|
202
|
+
model_reasoning_effort: resolved.reasoningEffort,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
123
207
|
return args;
|
|
124
208
|
}
|
|
125
209
|
|
package/package.json
CHANGED
package/scripts/tfx-route.sh
CHANGED
|
@@ -123,7 +123,11 @@ _preflight_check_gh_auth
|
|
|
123
123
|
# top-level sandbox/approval_mode와 의미가 다르다. 이 값이 "approve"이면
|
|
124
124
|
# codex exec이 non-TTY subprocess에서 승인 대기로 stall하므로 감지 대상에서 제외.
|
|
125
125
|
# (refs: tellang/triflux#66, Yeachan-Heo/oh-my-codex#1478)
|
|
126
|
-
|
|
126
|
+
# TFX_CODEX_CONFIG override: integration tests that run the full route script
|
|
127
|
+
# point this at an isolated tmpdir config so the MCP config-swap never mutates
|
|
128
|
+
# the real ~/.codex/config.toml (non-hermetic corruption guard). Defaults to the
|
|
129
|
+
# real path for normal runs.
|
|
130
|
+
_CODEX_CONFIG="${TFX_CODEX_CONFIG:-${HOME}/.codex/config.toml}"
|
|
127
131
|
_CODEX_HAS_SANDBOX=""
|
|
128
132
|
if [[ -f "$_CODEX_CONFIG" ]] && awk '
|
|
129
133
|
/^\[{1,2}mcp_servers\..*\.tools\./ { in_mcp_tool=1; next }
|
|
@@ -460,6 +464,85 @@ prepend_codex_north_star() {
|
|
|
460
464
|
rm -f "$tmp_file" 2>/dev/null || true
|
|
461
465
|
}
|
|
462
466
|
|
|
467
|
+
# prepend_skill: opt-in 으로 등록된 스킬의 SKILL.md 본문을 프롬프트 앞에 주입한다.
|
|
468
|
+
# 활성 조건은 TFX_INJECT_SKILL env (tfx-auto 가 --skill <name> 으로 set). 미설정이면
|
|
469
|
+
# 현행 동작 그대로 (no-op). CLI-agnostic — codex/agy 레인에서 동일하게 재사용한다.
|
|
470
|
+
# 전달은 prepend_codex_north_star 와 동일하게 printf/cat → temp file 만 사용하므로
|
|
471
|
+
# $ / 백슬래시(₩) 같은 특수문자를 셸 재확장 없이 리터럴로 보존한다 (parse-safe).
|
|
472
|
+
# 스킬 경로: ${TFX_SKILLS_DIR:-<repo>/skills}/<name>/SKILL.md.
|
|
473
|
+
prepend_skill() {
|
|
474
|
+
local prompt="${1:-}"
|
|
475
|
+
local skill_name="${TFX_INJECT_SKILL:-}"
|
|
476
|
+
if [[ -z "$skill_name" ]]; then
|
|
477
|
+
printf '%s' "$prompt"
|
|
478
|
+
return 0
|
|
479
|
+
fi
|
|
480
|
+
# 경로 탈출 방지: skill_name 은 skills/ 하위 단일 디렉토리 이름만 허용한다.
|
|
481
|
+
if [[ "$skill_name" == *"/"* || "$skill_name" == *".."* ]]; then
|
|
482
|
+
echo "[tfx-route] WARNING: TFX_INJECT_SKILL='${skill_name}' 에 경로 구분자/.. 포함 — 주입 거부" >&2
|
|
483
|
+
printf '%s' "$prompt"
|
|
484
|
+
return 0
|
|
485
|
+
fi
|
|
486
|
+
|
|
487
|
+
local skills_dir="${TFX_SKILLS_DIR:-}"
|
|
488
|
+
if [[ -z "$skills_dir" ]]; then
|
|
489
|
+
skills_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." 2>/dev/null && pwd)/skills"
|
|
490
|
+
fi
|
|
491
|
+
local skill_file="${skills_dir}/${skill_name}/SKILL.md"
|
|
492
|
+
if [[ ! -r "$skill_file" ]]; then
|
|
493
|
+
echo "[tfx-route] WARNING: TFX_INJECT_SKILL='${skill_name}' 스킬을 찾지 못함 ($skill_file) — 주입 생략" >&2
|
|
494
|
+
printf '%s' "$prompt"
|
|
495
|
+
return 0
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
local tmp_file
|
|
499
|
+
tmp_file="$(mktemp "${TFX_TMP:-${TMPDIR:-/tmp}}/tfx-skill-prompt.XXXXXX" 2>/dev/null)" || {
|
|
500
|
+
printf '%s' "$prompt"
|
|
501
|
+
return 0
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if ! {
|
|
505
|
+
printf '%s\n' "--- SKILL: ${skill_name} (apply this methodology to the task below) ---"
|
|
506
|
+
cat "$skill_file"
|
|
507
|
+
if [[ -s "$skill_file" ]]; then
|
|
508
|
+
local last_byte
|
|
509
|
+
last_byte="$(tail -c 1 "$skill_file" 2>/dev/null || true)"
|
|
510
|
+
[[ -n "$last_byte" ]] && printf '\n'
|
|
511
|
+
fi
|
|
512
|
+
printf '%s\n' "--- END SKILL ---"
|
|
513
|
+
printf '%s' "$prompt"
|
|
514
|
+
} > "$tmp_file"; then
|
|
515
|
+
rm -f "$tmp_file" 2>/dev/null || true
|
|
516
|
+
printf '%s' "$prompt"
|
|
517
|
+
return 0
|
|
518
|
+
fi
|
|
519
|
+
|
|
520
|
+
cat "$tmp_file" 2>/dev/null || printf '%s' "$prompt"
|
|
521
|
+
rm -f "$tmp_file" 2>/dev/null || true
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
# append_agy_anti_overclaim: agy(Gemini 3.x) 레인 전용 완료/grounding 규율 블록을
|
|
525
|
+
# 프롬프트 *뒤*에 붙인다. Gemini 3.x 는 과신(overconfidence) outlier 라 거짓 완료
|
|
526
|
+
# 주장·환각이 잦다 (AA-Omniscience 실증). 부정 제약은 Gemini 3 공식 가이드 권고대로
|
|
527
|
+
# 프롬프트 END 에 배치하고, blanket "do not guess" 대신 "주어진 context 로 추론 +
|
|
528
|
+
# 정확한 abstention 우선" 형태로 적는다 (open-ended negative 는 역효과). TFX_AGY_ANTI_OVERCLAIM=0 으로 opt-out.
|
|
529
|
+
append_agy_anti_overclaim() {
|
|
530
|
+
local prompt="${1:-}"
|
|
531
|
+
case "${TFX_AGY_ANTI_OVERCLAIM:-1}" in
|
|
532
|
+
0|false|FALSE|off|OFF|no|NO)
|
|
533
|
+
printf '%s' "$prompt"
|
|
534
|
+
return 0
|
|
535
|
+
;;
|
|
536
|
+
esac
|
|
537
|
+
|
|
538
|
+
printf '%s' "$prompt"
|
|
539
|
+
printf '\n\n%s\n' "--- COMPLETION & GROUNDING DISCIPLINE (apply strictly) ---"
|
|
540
|
+
printf '%s\n' "- Claim done/fixed/passing ONLY after running the check in this turn and seeing its output. With no fresh evidence, report what is still unverified instead of asserting success."
|
|
541
|
+
printf '%s\n' "- Ground every answer in the provided context and the actual repository. If something is not verifiable from what you can see, say 'No Info / 확인 불가' and stop rather than inventing plausible-but-unconfirmed details."
|
|
542
|
+
printf '%s\n' "- Use the provided context for deductions and prefer accurate abstention over confident guessing."
|
|
543
|
+
printf '%s' "--- END DISCIPLINE ---"
|
|
544
|
+
}
|
|
545
|
+
|
|
463
546
|
read_probe_state() {
|
|
464
547
|
local pid="$1"
|
|
465
548
|
local state_file="${TFX_PROBE_STATE_FILE:-${TFX_PROBE_DIR}/${pid}.json}"
|
|
@@ -2674,6 +2757,14 @@ FALLBACK_EOF
|
|
|
2674
2757
|
_codex_full_prompt_with_sentinel="$(prepend_codex_north_star "$FULL_PROMPT"; printf '%s' "$_codex_prompt_sentinel")"
|
|
2675
2758
|
FULL_PROMPT="${_codex_full_prompt_with_sentinel%"$_codex_prompt_sentinel"}"
|
|
2676
2759
|
fi
|
|
2760
|
+
# Opt-in 스킬 주입 (TFX_INJECT_SKILL). north-star 와 동일한 sentinel 패턴으로
|
|
2761
|
+
# trailing newline 을 보존한다. 미설정이면 prepend_skill 이 no-op.
|
|
2762
|
+
if [[ -n "${TFX_INJECT_SKILL:-}" ]]; then
|
|
2763
|
+
local _codex_skill_sentinel="__TFX_CODEX_SKILL_END_${$}_${RANDOM}__"
|
|
2764
|
+
local _codex_skill_prompt
|
|
2765
|
+
_codex_skill_prompt="$(prepend_skill "$FULL_PROMPT"; printf '%s' "$_codex_skill_sentinel")"
|
|
2766
|
+
FULL_PROMPT="${_codex_skill_prompt%"$_codex_skill_sentinel"}"
|
|
2767
|
+
fi
|
|
2677
2768
|
codex_transport_effective="exec"
|
|
2678
2769
|
if [[ "$TFX_CODEX_TRANSPORT" != "exec" ]]; then
|
|
2679
2770
|
run_codex_mcp "$FULL_PROMPT" "$use_tee" || exit_code=$?
|
|
@@ -2778,6 +2869,18 @@ EOF
|
|
|
2778
2869
|
_agy_full_prompt_with_sentinel="$(prepend_codex_north_star "$FULL_PROMPT"; printf '%s' "$_agy_prompt_sentinel")"
|
|
2779
2870
|
FULL_PROMPT="${_agy_full_prompt_with_sentinel%"$_agy_prompt_sentinel"}"
|
|
2780
2871
|
fi
|
|
2872
|
+
# Opt-in 스킬 주입 (TFX_INJECT_SKILL) — codex 레인과 동일.
|
|
2873
|
+
if [[ -n "${TFX_INJECT_SKILL:-}" ]]; then
|
|
2874
|
+
local _agy_skill_sentinel="__TFX_AGY_SKILL_END_${$}_${RANDOM}__"
|
|
2875
|
+
local _agy_skill_prompt
|
|
2876
|
+
_agy_skill_prompt="$(prepend_skill "$FULL_PROMPT"; printf '%s' "$_agy_skill_sentinel")"
|
|
2877
|
+
FULL_PROMPT="${_agy_skill_prompt%"$_agy_skill_sentinel"}"
|
|
2878
|
+
fi
|
|
2879
|
+
# agy(Gemini 3.x) anti-overclaim 규율 블록을 프롬프트 END 에 append. opt-out: TFX_AGY_ANTI_OVERCLAIM=0.
|
|
2880
|
+
local _agy_aoc_sentinel="__TFX_AGY_AOC_END_${$}_${RANDOM}__"
|
|
2881
|
+
local _agy_aoc_prompt
|
|
2882
|
+
_agy_aoc_prompt="$(append_agy_anti_overclaim "$FULL_PROMPT"; printf '%s' "$_agy_aoc_sentinel")"
|
|
2883
|
+
FULL_PROMPT="${_agy_aoc_prompt%"$_agy_aoc_sentinel"}"
|
|
2781
2884
|
run_antigravity_exec "$FULL_PROMPT" "$use_tee" || exit_code=$?
|
|
2782
2885
|
if [[ "$exit_code" -ne 0 && "$exit_code" -ne 124 ]]; then
|
|
2783
2886
|
local agy_stderr_bytes=0
|
package/skills/tfx-auto/SKILL.md
CHANGED
|
@@ -28,7 +28,7 @@ triggers:
|
|
|
28
28
|
- spec-panel
|
|
29
29
|
- business-panel
|
|
30
30
|
- index-repo
|
|
31
|
-
argument-hint: "<command|task> [args...] [--cli auto|codex|antigravity|claude] [--mode quick|deep|consensus] [--risk-tier auto|low|medium|high] [--shape consensus|debate|panel] [--cli-set triad|no-antigravity|custom] [--parallel 1|N|swarm] [--retry 0|1|ralph] [--isolation none|worktree] [--remote <host>|none]"
|
|
31
|
+
argument-hint: "<command|task> [args...] [--cli auto|codex|antigravity|claude] [--mode quick|deep|consensus] [--risk-tier auto|low|medium|high] [--shape consensus|debate|panel] [--cli-set triad|no-antigravity|custom] [--parallel 1|N|swarm] [--retry 0|1|ralph] [--isolation none|worktree] [--remote <host>|none] [--skill <name>]"
|
|
32
32
|
---
|
|
33
33
|
|
|
34
34
|
# tfx-auto — 통합 CLI 오케스트레이터
|
|
@@ -169,6 +169,7 @@ ARGUMENTS 에 아래 플래그가 있으면 Step 0 스마트 라우팅의 내부
|
|
|
169
169
|
| `--no-claude-native` | false (기본) | Claude native sub-agent 경로 유지 | — |
|
|
170
170
|
| `--no-claude-native` | true | Claude native 경로 disable, CLI 기반 worker 강제 | tfx-route.sh |
|
|
171
171
|
| `--max-iterations` | `0` (기본, unlimited) | `--retry ralph`/`auto-escalate` 상한 | retry-state-machine.mjs |
|
|
172
|
+
| `--skill` | `<name>` | `skills/<name>/SKILL.md` 본문을 codex/agy 프롬프트 앞에 주입 (`TFX_INJECT_SKILL`). 미지정 시 no-op | tfx-route.sh |
|
|
172
173
|
|
|
173
174
|
### `--risk-tier` 계약
|
|
174
175
|
|
|
@@ -204,6 +205,37 @@ ARGUMENTS 에 아래 플래그가 있으면 Step 0 스마트 라우팅의 내부
|
|
|
204
205
|
- `--retry ralph` → true ralph state machine (Phase 3, retry-state-machine.mjs)
|
|
205
206
|
- `--retry auto-escalate` → CLI 승격 체인 (Phase 3)
|
|
206
207
|
- `--max-iterations N` (N>0) → ralph/auto-escalate 에 상한 부여
|
|
208
|
+
- `--skill <name>` → `TFX_INJECT_SKILL=<name>` 로 tfx-route.sh 에 전달. 스킬 파일 부재 시 warning 후 주입 생략 (fail-open, 작업은 계속)
|
|
209
|
+
|
|
210
|
+
### 스킬 주입 (`--skill <name>`)
|
|
211
|
+
|
|
212
|
+
codex/agy 워커 프롬프트 앞에 등록된 스킬의 방법론을 주입하는 **opt-in** 프레임워크. 설계 근거는 `decision-skill-passing` (omc 레퍼런스 + codex/agy 네이티브 스킬 조사):
|
|
213
|
+
|
|
214
|
+
- **메커니즘**: `--skill <name>` → tfx-route.sh `TFX_INJECT_SKILL=<name>`. `skills/<name>/SKILL.md` 본문을 `--- SKILL: <name> (apply this methodology...) ---` delimited block 으로 프롬프트 앞에 prepend. codex·agy 레인 공통 (CLI-agnostic, `prepend_skill`).
|
|
215
|
+
- **기본 off**: 미지정이면 no-op → 현행 동작 그대로. 회귀 위험 없음.
|
|
216
|
+
- **파싱 안전**: prepend 는 `printf`/`cat` → temp file 만 사용. codex 는 argv `--` 뒤, agy 는 stdin `printf %s` 로 전달되므로 `$`(codex)·`₩`/백슬래시(agy) 같은 특수문자를 셸 재확장 없이 **리터럴 보존**. 스킬 본문을 그대로 넘겨도 깨지지 않음.
|
|
217
|
+
- **네이티브 스킬을 안 쓰는 이유**: codex `$skill-name`/`/name` 슬래시와 agy `/name` 은 둘 다 **인터랙티브 TUI 전용** → headless one-shot 에서 named-skill 강제 호출 불가 (미문서). 그래서 prose 주입을 채택 (레퍼런스 omc 도 role .md 를 raw prepend, 네이티브 회피).
|
|
218
|
+
|
|
219
|
+
#### 커맨드→스킬 매핑 (tfx-auto convention, opt-in)
|
|
220
|
+
|
|
221
|
+
tfx-auto 가 커맨드/agent 별로 주입할 스킬을 고를 때 쓰는 **권고 테이블**. 기본은 매핑 없음 (명시 `--skill` 만 작동) — 부적절한 스킬이 모든 프롬프트에 새는 것을 막는다. 프로젝트가 명시적으로 활성화할 때만 아래를 적용해 `--skill` 을 자동 부여한다.
|
|
222
|
+
|
|
223
|
+
| 커맨드/agent | 권고 스킬 | 비고 |
|
|
224
|
+
|--------------|-----------|------|
|
|
225
|
+
| (기본) | 없음 | explicit `--skill` 우선 |
|
|
226
|
+
| 프로젝트 정의 | 프로젝트가 지정 | 활성화 시 tfx-auto 가 해당 `--skill` set |
|
|
227
|
+
|
|
228
|
+
> 매핑된 스킬 파일이 없으면 `prepend_skill` 이 warning 후 주입을 생략한다 (fail-open). 존재하지 않는 매핑을 "주입됨"으로 가정하지 않는다.
|
|
229
|
+
|
|
230
|
+
#### agy anti-overclaim (자동, agy 레인 전용)
|
|
231
|
+
|
|
232
|
+
agy(Gemini 3.x) 레인은 `TFX_AGY_ANTI_OVERCLAIM`(기본 on) 으로 완료/grounding 규율 블록을 프롬프트 **END** 에 자동 append (`append_agy_anti_overclaim`). Gemini 3.x 과신(AA-Omniscience 실측: 정확도 53-56% 대비 환각률 88-91%) 대응:
|
|
233
|
+
|
|
234
|
+
- fresh 증거 없이 done/fixed/passing 주장 금지.
|
|
235
|
+
- 검증 불가 시 `No Info / 확인 불가` 후 중단 (날조 금지).
|
|
236
|
+
- 추론은 주어진 context 로, confident guessing 보다 accurate abstention 우선.
|
|
237
|
+
|
|
238
|
+
부정 제약은 Gemini 3 공식 가이드대로 **END 배치**하고 blanket "do not guess" 는 역효과라 피한다. opt-out: `TFX_AGY_ANTI_OVERCLAIM=0`.
|
|
207
239
|
|
|
208
240
|
### Retry state machine 계약 (legacy autoroute / persist 이관)
|
|
209
241
|
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tfx-goal-clarify
|
|
3
|
+
description: "자연어 아이디어를 Claude Code `/goal` 명령어 한 블록으로 변환하는 clarifier. 모호한 작업 설명을 받아 공식 3축(End state / Stated check / Constraints) + stop bound로 압축해 복붙 가능한 /goal 문자열을 출력. '/goal로 만들어', 'goal 프롬프트', 'goal 변환', 'clarify goal', 'goal 양식', '자율 작업 시작' 같은 요청에 사용."
|
|
4
|
+
triggers:
|
|
5
|
+
- goal 프롬프트
|
|
6
|
+
- goal 변환
|
|
7
|
+
- clarify goal
|
|
8
|
+
- goal 양식
|
|
9
|
+
- /goal 만들어
|
|
10
|
+
- goal-clarify
|
|
11
|
+
- tfx-goal-clarify
|
|
12
|
+
- 자율 작업 만들어
|
|
13
|
+
argument-hint: "<목표 자연어> [--tier 1|2|3]"
|
|
14
|
+
allowed-tools:
|
|
15
|
+
- Bash
|
|
16
|
+
- Read
|
|
17
|
+
- Glob
|
|
18
|
+
- Grep
|
|
19
|
+
- AskUserQuestion
|
|
20
|
+
- Write
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# tfx-goal-clarify — 자연어 → `/goal` 블록 변환기
|
|
24
|
+
|
|
25
|
+
> tfx-interview의 슬림 fork. **인터뷰 산출물을 Action Plan 대신 `/goal` 블록 한 덩어리로만** 출력한다.
|
|
26
|
+
> 공식 `/goal` 3축(End state / Stated check / Constraints) + `or stop after N turns`를 강제한다.
|
|
27
|
+
|
|
28
|
+
## 용도
|
|
29
|
+
|
|
30
|
+
Claude Code 2.1.139+의 `/goal` 명령어에 넣을 프롬프트를 자연어 아이디어로부터 구조화한다. `/goal`은 평가자(Haiku)가 transcript만 보고 yes/no 판정하므로, **측정 불가능한 조건은 무한 루프 또는 환각 yes**를 유발한다. 이 스킬은 그 함정을 회피한다.
|
|
31
|
+
|
|
32
|
+
**비교**:
|
|
33
|
+
- `tfx-interview`: 일반 요구사항 인터뷰 → 전체 Action Plan 산출
|
|
34
|
+
- `tfx-goal-clarify`: **/goal 블록 한 덩어리만** 산출 (5분 내 완료 목표)
|
|
35
|
+
- `gstack-office-hours`: 제품 아이디어 단계 (코드 이전)
|
|
36
|
+
|
|
37
|
+
## 입력 처리
|
|
38
|
+
|
|
39
|
+
`ARGUMENTS: <자연어 목표>`가 들어오면 첫 입력으로 사용. 없으면 사용자에게 요청.
|
|
40
|
+
|
|
41
|
+
`--tier N` 플래그:
|
|
42
|
+
- `1` = one-liner (3턴 이내 작업)
|
|
43
|
+
- `2` = standard 3-part (기본값, 10턴 이내)
|
|
44
|
+
- `3` = 9-section (다중 모듈/다일 작업)
|
|
45
|
+
|
|
46
|
+
## 워크플로우
|
|
47
|
+
|
|
48
|
+
### Step 1: 분류 + 초기 ambiguity 측정
|
|
49
|
+
|
|
50
|
+
코드베이스 컨텍스트 살펴보기 (선택적):
|
|
51
|
+
```bash
|
|
52
|
+
# 사용자가 어떤 repo에서 호출했는지 확인
|
|
53
|
+
git rev-parse --show-toplevel 2>/dev/null
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
자연어 입력을 3축으로 분해:
|
|
57
|
+
| 축 | 질문 | 점수 (0~1) |
|
|
58
|
+
|----|------|-----------|
|
|
59
|
+
| End state | 완료를 무엇으로 증명? | goal |
|
|
60
|
+
| Check | Claude가 어떤 명령/출력으로 보여줄 수 있나? | constraints |
|
|
61
|
+
| Constraints | 절대 건드리면 안 될 것은? | criteria |
|
|
62
|
+
|
|
63
|
+
`ambiguity = 1 - (end_state×0.4 + check×0.3 + constraints×0.3)`
|
|
64
|
+
|
|
65
|
+
Gemini 위임 (tfx-interview와 동일):
|
|
66
|
+
```bash
|
|
67
|
+
Bash("bash ~/.claude/scripts/tfx-route.sh gemini '<analysis prompt>'")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Gemini 미사용 환경(m5 등 SSH 원격)에서는 Claude가 직접 채점.
|
|
71
|
+
|
|
72
|
+
### Step 2: 적응형 인터뷰 (AskUserQuestion 사용, Tier별 질문 수 가변)
|
|
73
|
+
|
|
74
|
+
**반드시 Claude Code 네이티브 `AskUserQuestion` 도구를 사용**한다. 자유 텍스트 질의 금지 — 사용자가 옵션 중에서 선택하도록 구조화한다.
|
|
75
|
+
|
|
76
|
+
**Tier별 필수 축**:
|
|
77
|
+
| Tier | 핵심 축 (필수) | 보조 축 (조건부) | 질문 수 |
|
|
78
|
+
|------|---------------|----------------|---------|
|
|
79
|
+
| 1 | End state, Bound | — | 1~2 |
|
|
80
|
+
| 2 | End state, Check, Constraints, Bound | Scope | 3~5 |
|
|
81
|
+
| 3 | End state, Check, Constraints, Scope, Priority, Plan, Bound | Rollback, Output artifact | 5~8 |
|
|
82
|
+
|
|
83
|
+
**ambiguity < 20% 도달 시 즉시 종료** (남은 질문 스킵).
|
|
84
|
+
|
|
85
|
+
#### AskUserQuestion 호출 패턴 (예시)
|
|
86
|
+
|
|
87
|
+
각 축마다 다음 형태로 호출. **options는 사용자 입력 기반으로 동적 생성** + 사용자가 "Other"로 직접 입력 가능.
|
|
88
|
+
|
|
89
|
+
**Q1. End state (모든 Tier 필수)**
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"questions": [{
|
|
93
|
+
"question": "이 작업이 끝났다는 걸 어떤 단일 측정 가능한 결과로 증명하나요?",
|
|
94
|
+
"header": "End state",
|
|
95
|
+
"multiSelect": false,
|
|
96
|
+
"options": [
|
|
97
|
+
{"label": "테스트 exit code", "description": "예: pnpm test <path> exits 0"},
|
|
98
|
+
{"label": "패턴 카운트 0", "description": "예: rg \"legacy\" src returns 0 hits"},
|
|
99
|
+
{"label": "타입체크 통과", "description": "예: pnpm typecheck exits 0"},
|
|
100
|
+
{"label": "빌드 성공", "description": "예: pnpm build exits 0"}
|
|
101
|
+
]
|
|
102
|
+
}]
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Q2. Check 명령 (Tier 2/3)**
|
|
107
|
+
사용자 End state 선택 후 그 결과를 어떻게 transcript에 노출시킬지 — 보통 자동 추론(End state가 이미 명령형이면 그대로 사용). 모호하면 명령 후보 3-4개 옵션.
|
|
108
|
+
|
|
109
|
+
**Q3. Scope (Tier 2/3, 코드베이스 컨텍스트 있을 때만)**
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"questions": [{
|
|
113
|
+
"question": "변경 허용 범위는 어디까지인가요?",
|
|
114
|
+
"header": "Scope",
|
|
115
|
+
"multiSelect": true,
|
|
116
|
+
"options": [
|
|
117
|
+
{"label": "src/<module>/만", "description": "단일 모듈 한정"},
|
|
118
|
+
{"label": "src/ 전체", "description": "소스 전체"},
|
|
119
|
+
{"label": "tests/ 포함", "description": "테스트도 수정 가능"},
|
|
120
|
+
{"label": "package.json 가능", "description": "의존성 추가 허용"}
|
|
121
|
+
]
|
|
122
|
+
}]
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Q4. Constraints (Tier 2/3 필수)**
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"questions": [{
|
|
130
|
+
"question": "절대 변경하면 안 되는 것은 무엇인가요?",
|
|
131
|
+
"header": "Constraints",
|
|
132
|
+
"multiSelect": true,
|
|
133
|
+
"options": [
|
|
134
|
+
{"label": "공개 API 시그니처", "description": "props/exports 유지"},
|
|
135
|
+
{"label": "기존 테스트", "description": "tests/legacy/ 등 보호"},
|
|
136
|
+
{"label": "DB 스키마", "description": "migration 금지"},
|
|
137
|
+
{"label": "없음 (None)", "description": "제약 없이 진행"}
|
|
138
|
+
]
|
|
139
|
+
}]
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Q5. Priority (Tier 3만)**
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"questions": [{
|
|
147
|
+
"question": "충돌 시 어느 쪽을 우선하나요?",
|
|
148
|
+
"header": "Priority",
|
|
149
|
+
"multiSelect": false,
|
|
150
|
+
"options": [
|
|
151
|
+
{"label": "타입 안전 > 테스트 > 시각", "description": "타입체크 우선"},
|
|
152
|
+
{"label": "테스트 > 타입 > 시각", "description": "기존 동작 보존"},
|
|
153
|
+
{"label": "사용자 정의", "description": "직접 입력"}
|
|
154
|
+
]
|
|
155
|
+
}]
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Q6. Bound (모든 Tier 필수, 마지막)**
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"questions": [{
|
|
163
|
+
"question": "최대 얼마나 돌릴까요? (Stop 조건)",
|
|
164
|
+
"header": "Stop bound",
|
|
165
|
+
"multiSelect": false,
|
|
166
|
+
"options": [
|
|
167
|
+
{"label": "10 turns", "description": "단순 수정 작업"},
|
|
168
|
+
{"label": "20 turns", "description": "중간 규모 (기본 권장)"},
|
|
169
|
+
{"label": "30 turns", "description": "마이그레이션"},
|
|
170
|
+
{"label": "60 turns", "description": "다중 모듈 (Tier 3)"}
|
|
171
|
+
]
|
|
172
|
+
}]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Q7. Rollback (Tier 3 선택)**
|
|
177
|
+
실패 시 어떻게 복구할지 — git revert, checkpoint commit 빈도 등.
|
|
178
|
+
|
|
179
|
+
**Q8. Output artifact (Tier 3 선택)**
|
|
180
|
+
산출물 형태 — PR description, 보고서 파일, 단순 코드 변경 등.
|
|
181
|
+
|
|
182
|
+
#### 적응형 종료 조건
|
|
183
|
+
|
|
184
|
+
각 응답 후 ambiguity 재계산:
|
|
185
|
+
```
|
|
186
|
+
if ambiguity < 0.20:
|
|
187
|
+
→ 즉시 Step 3 (남은 질문 스킵)
|
|
188
|
+
elif 모든 필수 축 답변 완료:
|
|
189
|
+
→ Step 3
|
|
190
|
+
elif 질문 수 >= Tier별 상한:
|
|
191
|
+
→ Step 3 (남은 ambiguity는 사용자가 직접 편집할 영역으로 표시)
|
|
192
|
+
else:
|
|
193
|
+
→ 다음 질문
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### Fallback: AskUserQuestion 미사용 환경 (Codex/Gemini CLI 등)
|
|
197
|
+
|
|
198
|
+
자유 텍스트로 한 질문씩 던지되, 위 옵션 리스트를 보기로 제시.
|
|
199
|
+
|
|
200
|
+
### Step 3: `/goal` 블록 조립
|
|
201
|
+
|
|
202
|
+
**Tier 1 (one-liner)** — 단순 명령:
|
|
203
|
+
```
|
|
204
|
+
/goal <end_state> [or stop after N turns]
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Tier 2 (standard, 기본)**:
|
|
208
|
+
```
|
|
209
|
+
/goal
|
|
210
|
+
End state: {end_state}
|
|
211
|
+
Check: {check_command}
|
|
212
|
+
Constraints: {constraints}
|
|
213
|
+
Bound: or stop after {N} turns
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Tier 3 (9-section)** — 다중 모듈/긴 작업:
|
|
217
|
+
```
|
|
218
|
+
/goal
|
|
219
|
+
GOAL: {goal}
|
|
220
|
+
CONTEXT: {context}
|
|
221
|
+
CONSTRAINTS: {constraints}
|
|
222
|
+
PRIORITY: {priority}
|
|
223
|
+
PLAN: {plan_steps}
|
|
224
|
+
DONE WHEN: {done_when}
|
|
225
|
+
VERIFY: {verify_commands}
|
|
226
|
+
OUTPUT: {output_artifacts}
|
|
227
|
+
STOP RULES: or stop after {N} turns
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Step 4: 사용자 표시 + 검증
|
|
231
|
+
|
|
232
|
+
출력 형식 (반드시 이 순서):
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
📊 Ambiguity: {initial}% → {final}% (목표 < 20%)
|
|
236
|
+
- End state: {pct}%
|
|
237
|
+
- Check: {pct}%
|
|
238
|
+
- Constraints: {pct}%
|
|
239
|
+
|
|
240
|
+
📋 4000자 한도: {used}/{4000} chars
|
|
241
|
+
|
|
242
|
+
📦 /goal 블록 (Tier {N}):
|
|
243
|
+
|
|
244
|
+
────────── 복붙 영역 ──────────
|
|
245
|
+
{the assembled /goal block}
|
|
246
|
+
─────────────────────────────────
|
|
247
|
+
|
|
248
|
+
✅ 검증:
|
|
249
|
+
- 종료 상태가 측정 가능한가? {Y/N}
|
|
250
|
+
- Check 명령이 transcript에 결과를 남기는가? {Y/N}
|
|
251
|
+
- Stop bound가 있는가? {Y/N}
|
|
252
|
+
- 4000자 이내인가? {Y/N}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Step 5: 저장 (선택)
|
|
256
|
+
|
|
257
|
+
기본은 출력만. 사용자가 원하면 저장:
|
|
258
|
+
- 위치: `.tfx/goals/goal-{timestamp}.txt`
|
|
259
|
+
- 형식: 원본 자연어 + 인터뷰 응답 + 최종 /goal 블록
|
|
260
|
+
|
|
261
|
+
## 출력 후 옵션 (AskUserQuestion)
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
질문: 이 /goal 블록을 어떻게 처리할까요?
|
|
265
|
+
옵션:
|
|
266
|
+
- 클립보드 복사: 블록을 클립보드로 복사 (입력창에 붙여넣기만 하면 됨)
|
|
267
|
+
- 출력만: 화면 출력만 하고 종료
|
|
268
|
+
- 수정: 특정 축(End state/Check/Constraints) 재질문
|
|
269
|
+
- Tier 변경: 1↔2↔3 재조립
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
> `/goal`은 Claude Code 슬래시 명령어라 **에이전트가 세션 입력창에 대신 전송할 수 없다.**
|
|
273
|
+
> 그래서 "바로 실행" 옵션은 두지 않는다. 사용자가 클립보드에서 입력창에 직접 붙여넣어 실행한다.
|
|
274
|
+
|
|
275
|
+
### 클립보드 복사 실행 방법
|
|
276
|
+
|
|
277
|
+
"클립보드 복사" 선택 시 `Bash`로 OS별 클립보드 명령에 블록을 파이프한다.
|
|
278
|
+
블록에 백틱·따옴표가 섞이므로 **임시 파일을 거쳐** 복사한다 (인용 깨짐 방지).
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
# 1) 조립된 /goal 블록을 임시 파일로 기록 (Write 도구 또는 heredoc)
|
|
282
|
+
cat > "${TMPDIR:-/tmp}/tfx-goal-block.txt" <<'GOAL_EOF'
|
|
283
|
+
<the assembled /goal block>
|
|
284
|
+
GOAL_EOF
|
|
285
|
+
|
|
286
|
+
# 2) OS별 클립보드로 복사 (darwin 우선, fallback chain)
|
|
287
|
+
f="${TMPDIR:-/tmp}/tfx-goal-block.txt"
|
|
288
|
+
if command -v pbcopy >/dev/null 2>&1; then pbcopy < "$f" # macOS
|
|
289
|
+
elif command -v wl-copy >/dev/null 2>&1; then wl-copy < "$f" # Wayland
|
|
290
|
+
elif command -v xclip >/dev/null 2>&1; then xclip -selection clipboard < "$f" # X11
|
|
291
|
+
elif command -v clip.exe >/dev/null 2>&1; then clip.exe < "$f" # Windows/WSL
|
|
292
|
+
else echo "클립보드 명령 없음 — 위 출력에서 수동 복사"; fi
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
복사 성공 시 `✅ 클립보드에 복사됨 — 입력창에 붙여넣어 실행하세요.` 를 출력한다.
|
|
296
|
+
|
|
297
|
+
## 동작 규칙
|
|
298
|
+
|
|
299
|
+
1. **출력 우선**: 최종 산출물은 항상 `/goal` 블록 한 덩어리. 부수 설명은 위/아래에만.
|
|
300
|
+
2. **4000자 강제**: 한도 초과 시 자동으로 PLAN/CONTEXT를 압축.
|
|
301
|
+
3. **Stop bound 강제**: 사용자가 명시 안 해도 `or stop after 20 turns`를 기본 삽입 (Tier 2/3).
|
|
302
|
+
4. **Check 강제**: 평가자가 transcript만 보므로 Check 없는 블록은 거부.
|
|
303
|
+
5. **Constraints 비어있어도 OK**: 'none' 명시.
|
|
304
|
+
6. **언어**: 사용자가 한국어로 입력하면 인터뷰는 한국어, /goal 블록은 영어 (Claude Code가 영어 명령에 더 안정적).
|
|
305
|
+
|
|
306
|
+
## 안티패턴 (자동 거부)
|
|
307
|
+
|
|
308
|
+
다음 End state는 거부:
|
|
309
|
+
- "production-ready", "better", "modern", "clean" — 주관적
|
|
310
|
+
- "all tests pass" without 명시된 테스트 위치 — 범위 폭발
|
|
311
|
+
- "refactor everything" — 범위 무제한
|
|
312
|
+
|
|
313
|
+
거부 시 사용자에게:
|
|
314
|
+
```
|
|
315
|
+
⚠️ End state "{...}"는 평가자가 측정할 수 없습니다.
|
|
316
|
+
다음 중 어느 형태로 바꿀까요?
|
|
317
|
+
1) 명령어 exit code (예: pnpm test exits 0)
|
|
318
|
+
2) 파일/문자열 카운트 (예: rg X returns 0 hits)
|
|
319
|
+
3) 파일 변경 여부 (예: git diff --stat shows ≤3 files changed)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Quick Examples
|
|
323
|
+
|
|
324
|
+
### Tier 1 입력/출력
|
|
325
|
+
```
|
|
326
|
+
입력: "auth 테스트 다 통과시켜"
|
|
327
|
+
출력: /goal pnpm test test/auth exits 0, or stop after 15 turns
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Tier 2 입력/출력
|
|
331
|
+
```
|
|
332
|
+
입력: "레거시 auth API 호출들을 v2로 마이그레이션"
|
|
333
|
+
출력:
|
|
334
|
+
/goal
|
|
335
|
+
End state: src/legacy/auth/*.ts 의 모든 호출이 src/auth/v2 로 마이그레이션됨
|
|
336
|
+
Check: `pnpm test src/auth` exits 0 AND `rg "legacy/auth" src` returns 0 hits
|
|
337
|
+
Constraints: tests/legacy/는 수정 금지, package.json 의존성 추가 금지
|
|
338
|
+
Bound: or stop after 30 turns
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Tier 3 입력/출력
|
|
342
|
+
```
|
|
343
|
+
입력: "users 모듈 전체를 새 design system으로 갈아엎고 PR 만들어"
|
|
344
|
+
출력:
|
|
345
|
+
/goal
|
|
346
|
+
GOAL: src/users/** 의 모든 React 컴포넌트를 @company/ds-v3로 마이그레이션
|
|
347
|
+
CONTEXT: 기존은 @company/ds-v2, 디자인 토큰은 docs/design/v3.md, 18개 컴포넌트
|
|
348
|
+
CONSTRAINTS:
|
|
349
|
+
- tests/users/ snapshot 갱신 외 변경 금지
|
|
350
|
+
- props 시그니처 유지 (caller 영향 없음)
|
|
351
|
+
- i18n key 변경 금지
|
|
352
|
+
PRIORITY: 시각적 회귀 < 타입 안전성 < 테스트 통과
|
|
353
|
+
PLAN:
|
|
354
|
+
1. 컴포넌트 인벤토리 추출
|
|
355
|
+
2. v2→v3 mapping 표 생성
|
|
356
|
+
3. 각 컴포넌트 변환 + snapshot 갱신
|
|
357
|
+
4. PR description 작성
|
|
358
|
+
DONE WHEN: 18/18 컴포넌트 마이그레이션 + 모든 snapshot 통과 + PR 본문 생성
|
|
359
|
+
VERIFY: `pnpm typecheck` exits 0 AND `pnpm test src/users` exits 0 AND `rg "ds-v2" src/users` returns 0 hits
|
|
360
|
+
OUTPUT: PR description in .claude/scratch/pr-users-ds-v3.md
|
|
361
|
+
STOP RULES: or stop after 60 turns, or if token spend exceeds 500K
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## 토큰 예산
|
|
365
|
+
|
|
366
|
+
| 단계 | Claude | Gemini |
|
|
367
|
+
|------|--------|--------|
|
|
368
|
+
| 초기 분류 | ~0.5K | ~1K |
|
|
369
|
+
| 3축 인터뷰 (Q1~Q3) | ~1K | ~3K |
|
|
370
|
+
| /goal 블록 조립 | ~0.5K | ~1K |
|
|
371
|
+
| **총합** | **~2K** | **~5K** |
|
|
372
|
+
|
|
373
|
+
Gemini 없으면 Claude 단독 ~5K.
|
|
374
|
+
|
|
375
|
+
## 사용 예
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
/tfx-goal-clarify "auth 마이그레이션"
|
|
379
|
+
/tfx-goal-clarify "테스트 다 고쳐" --tier 1
|
|
380
|
+
/tfx-goal-clarify "users 모듈 design system 갈아엎기" --tier 3
|
|
381
|
+
/goal 변환 "스냅샷 갱신해서 CI 그린"
|
|
382
|
+
```
|