triflux 10.34.0 → 10.35.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/bin/tfx-live.mjs +62 -19
- package/cto/status.mjs +77 -1
- package/hooks/agy-session-hook.mjs +140 -0
- package/hub/bridge.mjs +130 -70
- package/hub/server.mjs +4 -2
- package/hub/team/build-worker-prompt.mjs +23 -4
- package/hub/team/claude-daemon-control.mjs +348 -4
- package/hub/team/claude-native-bridge.mjs +6 -8
- package/hub/team/conductor.mjs +1 -0
- package/hub/team/handoff.mjs +8 -4
- package/hub/team/headless.mjs +33 -11
- package/hub/team/native-supervisor.mjs +4 -0
- package/hub/team/orchestrator.mjs +7 -2
- package/hub/team/swarm-hypervisor.mjs +230 -44
- package/hub/team/worker-completion-validator.mjs +11 -16
- package/hub/team/worker-sandbox.mjs +29 -1
- package/hub/tray-lifecycle.mjs +2 -1
- package/hub/workers/delegator-mcp.mjs +1 -0
- package/hud/constants.mjs +2 -0
- package/hud/providers/gemini.mjs +135 -3
- package/hud/renderers.mjs +37 -35
- package/package.json +1 -1
- package/scripts/ensure-agy-hooks.mjs +134 -0
- package/scripts/ensure-codex-hooks.mjs +8 -0
- package/scripts/lib/mcp-manifest.mjs +2 -2
- package/scripts/mcp-gateway-start.ps1 +0 -1
- package/scripts/preflight-cache.mjs +230 -55
- package/scripts/setup.mjs +19 -0
- package/scripts/test-lock.mjs +15 -1
- package/scripts/tfx-route.sh +43 -9
- package/skills/tfx-setup/SKILL.md +6 -6
- package/config/mcp-registry.json.bak-pre-serena-removal +0 -89
package/hub/server.mjs
CHANGED
|
@@ -246,7 +246,9 @@ async function parseBody(req) {
|
|
|
246
246
|
return JSON.parse(Buffer.concat(chunks).toString());
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
const PID_DIR =
|
|
249
|
+
const PID_DIR =
|
|
250
|
+
process.env.TFX_HUB_PID_DIR?.trim() ||
|
|
251
|
+
join(homedir(), ".claude", "cache", "tfx-hub");
|
|
250
252
|
const PID_FILE = join(PID_DIR, "hub.pid");
|
|
251
253
|
const TOKEN_FILE = join(homedir(), ".claude", ".tfx-hub-token");
|
|
252
254
|
|
|
@@ -1004,7 +1006,7 @@ export async function startHub({
|
|
|
1004
1006
|
// DB를 npm 패키지 밖에 저장하여 npm update 시 EBUSY 방지
|
|
1005
1007
|
// 기존: PROJECT_ROOT/.tfx/state/state.db (패키지 내부 → 락 충돌)
|
|
1006
1008
|
// 변경: ~/.claude/cache/tfx-hub/state.db (패키지 외부 → 안전)
|
|
1007
|
-
const hubCacheDir =
|
|
1009
|
+
const hubCacheDir = PID_DIR;
|
|
1008
1010
|
mkdirSync(hubCacheDir, { recursive: true });
|
|
1009
1011
|
dbPath = join(hubCacheDir, "state.db");
|
|
1010
1012
|
}
|
|
@@ -22,10 +22,23 @@ function formatLeaseFiles(leaseFiles) {
|
|
|
22
22
|
return normalized.map((file) => `- ${file}`).join("\n");
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
function normalizeWorktreePath(worktreePath) {
|
|
26
|
+
return typeof worktreePath === "string" ? worktreePath.trim() : "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function buildLeaseScopedAcceptanceAppendix({
|
|
30
|
+
leaseFiles = [],
|
|
31
|
+
worktreePath,
|
|
32
|
+
} = {}) {
|
|
33
|
+
const normalizedWorktreePath = normalizeWorktreePath(worktreePath);
|
|
34
|
+
const rootLine = normalizedWorktreePath
|
|
35
|
+
? `작업 루트(절대경로): ${normalizedWorktreePath} — 아래 lease 경로와 모든 상대경로는 이 루트 기준으로만 해석한다.\n`
|
|
36
|
+
: "";
|
|
37
|
+
|
|
26
38
|
return `
|
|
27
39
|
|
|
28
40
|
## Lease-scoped Acceptance / Lint Guard (자동 삽입됨)
|
|
41
|
+
${rootLine}- 위 PRD 본문의 모든 제약과 acceptance 기준은 이 appendix 이후에도 그대로 유효하다.
|
|
29
42
|
- 이 shard 의 파일 lease:
|
|
30
43
|
${formatLeaseFiles(leaseFiles)}
|
|
31
44
|
- Acceptance, lint, and format checks are scoped to files changed by this shard; if that set is unclear, use only the lease list above.
|
|
@@ -51,7 +64,10 @@ ${SENTINEL_END}
|
|
|
51
64
|
규약:
|
|
52
65
|
- 두 sentinel 마커는 각자 자기 줄에 단독으로 출력 (앞뒤 newline)
|
|
53
66
|
- 마커 사이 본문은 단일 JSON object (배열/primitive 금지)
|
|
54
|
-
-
|
|
67
|
+
- status 값은 ok | failed | blocked 중 하나
|
|
68
|
+
- payload 출력 직전 \`git log -1 --format=%H\` 로 보고할 sha 가 실재하는지 검증하라
|
|
69
|
+
- 코드 변경이 기대되는 shard 에서 변경/커밋을 못 했으면 status:failed 와 함께 reason 필드로 사유를 보고하라 — 그 경우 빈 commits_made 에 status:ok 는 금지
|
|
70
|
+
- no-op shard 는 status:ok + 빈 commits_made 배열 허용
|
|
55
71
|
- 마커 쌍은 stdout 에 정확히 한 번만 출력해야 함. 재emit 시 conductor 는 첫 BEGIN..END 한 쌍만 채택하며, 이후 stdout 은 무시한다.
|
|
56
72
|
- ${SENTINEL_BEGIN} 만 출력하고 ${SENTINEL_END} 누락 시 conductor 가 truncation 으로 명확히 reject
|
|
57
73
|
`;
|
|
@@ -60,14 +76,17 @@ ${SENTINEL_END}
|
|
|
60
76
|
* Append the Completion Protocol section to a PRD prompt.
|
|
61
77
|
*
|
|
62
78
|
* @param {string|null|undefined} prdPrompt — original PRD body
|
|
63
|
-
* @param {{ leaseFiles?: string[] }} [opts] — shard file lease for scoped acceptance
|
|
79
|
+
* @param {{ leaseFiles?: string[], worktreePath?: string }} [opts] — shard file lease and absolute worktree root for scoped acceptance
|
|
64
80
|
* @returns {string} prompt with appendix
|
|
65
81
|
*/
|
|
66
82
|
export function buildWorkerPrompt(prdPrompt, opts = {}) {
|
|
67
83
|
const body = typeof prdPrompt === "string" ? prdPrompt : "";
|
|
68
84
|
return (
|
|
69
85
|
body +
|
|
70
|
-
buildLeaseScopedAcceptanceAppendix({
|
|
86
|
+
buildLeaseScopedAcceptanceAppendix({
|
|
87
|
+
leaseFiles: opts.leaseFiles,
|
|
88
|
+
worktreePath: opts.worktreePath,
|
|
89
|
+
}) +
|
|
71
90
|
COMPLETION_PROTOCOL_APPENDIX
|
|
72
91
|
);
|
|
73
92
|
}
|
|
@@ -16,7 +16,24 @@ import {
|
|
|
16
16
|
|
|
17
17
|
export function resolveClaudeConfigDir(env = process.env) {
|
|
18
18
|
if (env.CLAUDE_CONFIG_DIR) return path.resolve(env.CLAUDE_CONFIG_DIR);
|
|
19
|
-
return path.join(
|
|
19
|
+
return path.join(resolveClaudeHomeDir(env), ".claude");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveClaudeHomeDir(
|
|
23
|
+
env = process.env,
|
|
24
|
+
platform = process.platform,
|
|
25
|
+
) {
|
|
26
|
+
// win32: claude daemon launcher 의 os.homedir() 는 USERPROFILE 기반이고
|
|
27
|
+
// HOME 을 보지 않는다. git-bash 가 HOME 을 따로 잡아도 launcher 폴백을
|
|
28
|
+
// 그대로 미러해야 socket hash 가 daemon 과 일치한다 (HOME 수용 금지).
|
|
29
|
+
if (platform === "win32") {
|
|
30
|
+
const userProfile = nullableEnv(env.USERPROFILE);
|
|
31
|
+
return userProfile ? path.resolve(userProfile) : os.homedir();
|
|
32
|
+
}
|
|
33
|
+
// POSIX 는 HOME 우선 — sandbox HOME 오버라이드를 추적하는 provenance
|
|
34
|
+
// 해석(#382)과 일치 (os.homedir() 도 POSIX 에서는 HOME 을 우선한다).
|
|
35
|
+
const home = nullableEnv(env.HOME);
|
|
36
|
+
return home ? path.resolve(home) : os.homedir();
|
|
20
37
|
}
|
|
21
38
|
|
|
22
39
|
export function deriveClaudeDaemonPaths({
|
|
@@ -44,6 +61,316 @@ export function deriveClaudeDaemonPaths({
|
|
|
44
61
|
};
|
|
45
62
|
}
|
|
46
63
|
|
|
64
|
+
function nullableEnv(value) {
|
|
65
|
+
const text = typeof value === "string" ? value.trim() : "";
|
|
66
|
+
return text.length > 0 ? text : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function defaultClaudeConfigDir(env = process.env) {
|
|
70
|
+
return path.join(resolveClaudeHomeDir(env), ".claude");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function detectCallerProvenance(env = process.env) {
|
|
74
|
+
const claudeConfigDir = nullableEnv(env.CLAUDE_CONFIG_DIR);
|
|
75
|
+
const omxSessionId = nullableEnv(env.OMX_SESSION_ID);
|
|
76
|
+
const omxEntryPath = nullableEnv(env.OMX_ENTRY_PATH);
|
|
77
|
+
const codexThreadId = nullableEnv(env.CODEX_THREAD_ID);
|
|
78
|
+
const codexLauncher =
|
|
79
|
+
omxSessionId || omxEntryPath ? "omx" : codexThreadId ? "codex" : "unknown";
|
|
80
|
+
return {
|
|
81
|
+
claudeLauncher: "unknown",
|
|
82
|
+
codexLauncher,
|
|
83
|
+
signals: {
|
|
84
|
+
claudeConfigDir,
|
|
85
|
+
omxSessionId,
|
|
86
|
+
omxEntryPath,
|
|
87
|
+
codexThreadId,
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function readOmcLaunchProfile(configDir) {
|
|
93
|
+
if (!configDir) return null;
|
|
94
|
+
try {
|
|
95
|
+
// Provenance hint only: OMC writes this profile in its runtime config dir.
|
|
96
|
+
// It is not an auth/security boundary and must not override explicit/env
|
|
97
|
+
// config-dir authority.
|
|
98
|
+
const parsed = JSON.parse(
|
|
99
|
+
await fs.readFile(
|
|
100
|
+
path.join(path.resolve(configDir), ".omc-launch-profile.json"),
|
|
101
|
+
"utf8",
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
const sourceConfigDir = nullableEnv(parsed?.sourceConfigDir);
|
|
105
|
+
if (!sourceConfigDir) return null;
|
|
106
|
+
const sourceClaudeMd = nullableEnv(parsed?.sourceClaudeMd);
|
|
107
|
+
return {
|
|
108
|
+
sourceConfigDir: path.resolve(sourceConfigDir),
|
|
109
|
+
sourceClaudeMd: sourceClaudeMd ? path.resolve(sourceClaudeMd) : null,
|
|
110
|
+
};
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error?.code === "ENOENT" || error instanceof SyntaxError) return null;
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function buildClaudeDaemonCandidate({
|
|
118
|
+
configDir,
|
|
119
|
+
configDirSource,
|
|
120
|
+
selectionMode,
|
|
121
|
+
env,
|
|
122
|
+
uid,
|
|
123
|
+
tmpRoot,
|
|
124
|
+
}) {
|
|
125
|
+
const paths = deriveClaudeDaemonPaths({ configDir, uid, tmpRoot });
|
|
126
|
+
const defaultConfig = path.resolve(defaultClaudeConfigDir(env));
|
|
127
|
+
const omcProfile = await readOmcLaunchProfile(paths.configDir);
|
|
128
|
+
const claudeLauncher = omcProfile
|
|
129
|
+
? "omc"
|
|
130
|
+
: paths.configDir === defaultConfig
|
|
131
|
+
? "claude"
|
|
132
|
+
: "unknown";
|
|
133
|
+
return {
|
|
134
|
+
...paths,
|
|
135
|
+
configDirSource,
|
|
136
|
+
selectionMode,
|
|
137
|
+
claudeLauncher,
|
|
138
|
+
sourceConfigDir: omcProfile?.sourceConfigDir,
|
|
139
|
+
sourceClaudeMd: omcProfile?.sourceClaudeMd ?? null,
|
|
140
|
+
callerProvenance: detectCallerProvenance(env),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function buildClaudeDaemonDiscoveryCandidates({
|
|
145
|
+
configDir,
|
|
146
|
+
env = process.env,
|
|
147
|
+
uid = typeof process.getuid === "function" ? process.getuid() : 0,
|
|
148
|
+
tmpRoot = "/tmp",
|
|
149
|
+
} = {}) {
|
|
150
|
+
const explicitConfigDir = nullableEnv(configDir);
|
|
151
|
+
if (explicitConfigDir) {
|
|
152
|
+
return [
|
|
153
|
+
await buildClaudeDaemonCandidate({
|
|
154
|
+
configDir: explicitConfigDir,
|
|
155
|
+
configDirSource: "explicit",
|
|
156
|
+
selectionMode: "exact",
|
|
157
|
+
env,
|
|
158
|
+
uid,
|
|
159
|
+
tmpRoot,
|
|
160
|
+
}),
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const envConfigDir = nullableEnv(env.CLAUDE_CONFIG_DIR);
|
|
165
|
+
if (envConfigDir) {
|
|
166
|
+
return [
|
|
167
|
+
await buildClaudeDaemonCandidate({
|
|
168
|
+
configDir: envConfigDir,
|
|
169
|
+
configDirSource: "env",
|
|
170
|
+
selectionMode: "env-strict",
|
|
171
|
+
env,
|
|
172
|
+
uid,
|
|
173
|
+
tmpRoot,
|
|
174
|
+
}),
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const candidates = [];
|
|
179
|
+
const defaultConfig = defaultClaudeConfigDir(env);
|
|
180
|
+
const omcRuntime = path.join(defaultConfig, ".omc-launch");
|
|
181
|
+
if (await readOmcLaunchProfile(omcRuntime)) {
|
|
182
|
+
candidates.push(
|
|
183
|
+
await buildClaudeDaemonCandidate({
|
|
184
|
+
configDir: omcRuntime,
|
|
185
|
+
configDirSource: "omc-runtime",
|
|
186
|
+
selectionMode: "ambient",
|
|
187
|
+
env,
|
|
188
|
+
uid,
|
|
189
|
+
tmpRoot,
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
candidates.push(
|
|
194
|
+
await buildClaudeDaemonCandidate({
|
|
195
|
+
configDir: defaultConfig,
|
|
196
|
+
configDirSource: "default",
|
|
197
|
+
selectionMode: "ambient",
|
|
198
|
+
env,
|
|
199
|
+
uid,
|
|
200
|
+
tmpRoot,
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
return candidates;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function publicClaudeDaemonCandidate(candidate) {
|
|
207
|
+
if (!candidate) return null;
|
|
208
|
+
return {
|
|
209
|
+
configDirSource: candidate.configDirSource,
|
|
210
|
+
selectionMode: candidate.selectionMode,
|
|
211
|
+
claudeLauncher: candidate.claudeLauncher,
|
|
212
|
+
configDir: candidate.configDir,
|
|
213
|
+
sourceConfigDir: candidate.sourceConfigDir ?? null,
|
|
214
|
+
sourceClaudeMd: candidate.sourceClaudeMd ?? null,
|
|
215
|
+
hash: candidate.hash,
|
|
216
|
+
controlSock: candidate.controlSock,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function sessionsFromDaemonList(listResponse) {
|
|
221
|
+
// probe 경로도 list/find 경로와 같은 정규화 row 를 내보내야 한다
|
|
222
|
+
// (jobs/sessions, snake_case, dispatch 중첩 변형 흡수 — 8016b724).
|
|
223
|
+
return extractClaudeAgentSessions(listResponse);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function errorMessage(error) {
|
|
227
|
+
return error?.message || String(error);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function findDaemonTargetInSessions(sessions, { short, sessionId } = {}) {
|
|
231
|
+
if (sessionId) {
|
|
232
|
+
return findDaemonJobBySessionId({ jobs: sessions }, sessionId);
|
|
233
|
+
}
|
|
234
|
+
if (short) {
|
|
235
|
+
return findDaemonJobByShort({ jobs: sessions }, short);
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function candidateResultSummary(result) {
|
|
241
|
+
const summary = publicClaudeDaemonCandidate(result.candidate);
|
|
242
|
+
return {
|
|
243
|
+
...summary,
|
|
244
|
+
ok: result.ok === true,
|
|
245
|
+
error: result.error ?? null,
|
|
246
|
+
sessionCount: Array.isArray(result.sessions) ? result.sessions.length : 0,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildProbeResponse({
|
|
251
|
+
candidates,
|
|
252
|
+
results,
|
|
253
|
+
selected,
|
|
254
|
+
matches,
|
|
255
|
+
targetRequested,
|
|
256
|
+
callerProvenance,
|
|
257
|
+
}) {
|
|
258
|
+
const reachable = results.filter((result) => result.ok === true);
|
|
259
|
+
let ok = false;
|
|
260
|
+
let reason = "daemon-unavailable";
|
|
261
|
+
let error = null;
|
|
262
|
+
|
|
263
|
+
if (targetRequested && matches.length > 1) {
|
|
264
|
+
reason = "ambiguous-target";
|
|
265
|
+
error = `Claude daemon target ambiguous across ${matches.length} candidates`;
|
|
266
|
+
} else if (selected) {
|
|
267
|
+
ok = true;
|
|
268
|
+
reason = selected.target ? "target-found" : "daemon-available";
|
|
269
|
+
} else if (targetRequested && reachable.length > 0) {
|
|
270
|
+
reason = "target-not-found";
|
|
271
|
+
error = "Claude daemon target not found in reachable candidates";
|
|
272
|
+
} else if (results.length > 0) {
|
|
273
|
+
error =
|
|
274
|
+
results
|
|
275
|
+
.map((result) => result.error)
|
|
276
|
+
.filter(Boolean)
|
|
277
|
+
.join("; ") || "Claude daemon unavailable";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const selectedSessions = selected
|
|
281
|
+
? selected.sessions
|
|
282
|
+
: targetRequested
|
|
283
|
+
? []
|
|
284
|
+
: reachable.flatMap((result) => result.sessions);
|
|
285
|
+
const selectedDaemon = publicClaudeDaemonCandidate(selected?.candidate);
|
|
286
|
+
const reachableDaemons = reachable.map((result) =>
|
|
287
|
+
publicClaudeDaemonCandidate(result.candidate),
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
ok,
|
|
292
|
+
reason,
|
|
293
|
+
controlSock: selected?.candidate?.controlSock,
|
|
294
|
+
daemon: selectedDaemon,
|
|
295
|
+
daemons: reachableDaemons,
|
|
296
|
+
sessions: selectedSessions,
|
|
297
|
+
target: selected?.target || undefined,
|
|
298
|
+
matches: matches.map((match) => ({
|
|
299
|
+
daemon: publicClaudeDaemonCandidate(match.candidate),
|
|
300
|
+
target: match.target,
|
|
301
|
+
})),
|
|
302
|
+
candidateResults: results.map(candidateResultSummary),
|
|
303
|
+
callerProvenance,
|
|
304
|
+
candidates: candidates.map(publicClaudeDaemonCandidate),
|
|
305
|
+
error: ok ? undefined : error,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function probeClaudeDaemonCandidates({
|
|
310
|
+
configDir,
|
|
311
|
+
env = process.env,
|
|
312
|
+
short,
|
|
313
|
+
sessionId,
|
|
314
|
+
timeoutMs = 6000,
|
|
315
|
+
} = {}) {
|
|
316
|
+
const candidates = await buildClaudeDaemonDiscoveryCandidates({
|
|
317
|
+
configDir,
|
|
318
|
+
env,
|
|
319
|
+
});
|
|
320
|
+
const callerProvenance = detectCallerProvenance(env);
|
|
321
|
+
const targetRequested = Boolean(short || sessionId);
|
|
322
|
+
const results = [];
|
|
323
|
+
|
|
324
|
+
for (const candidate of candidates) {
|
|
325
|
+
try {
|
|
326
|
+
const list = await sendClaudeControlRequest(
|
|
327
|
+
candidate.controlSock,
|
|
328
|
+
{ proto: 1, op: "list" },
|
|
329
|
+
{ timeoutMs },
|
|
330
|
+
);
|
|
331
|
+
const sessions = sessionsFromDaemonList(list);
|
|
332
|
+
const ok = list?.ok !== false;
|
|
333
|
+
results.push({
|
|
334
|
+
ok,
|
|
335
|
+
candidate,
|
|
336
|
+
list,
|
|
337
|
+
sessions: ok ? sessions : [],
|
|
338
|
+
target: ok
|
|
339
|
+
? findDaemonTargetInSessions(sessions, { short, sessionId })
|
|
340
|
+
: null,
|
|
341
|
+
error: ok ? null : list?.error || "Claude daemon list failed",
|
|
342
|
+
});
|
|
343
|
+
} catch (error) {
|
|
344
|
+
results.push({
|
|
345
|
+
ok: false,
|
|
346
|
+
candidate,
|
|
347
|
+
list: null,
|
|
348
|
+
sessions: [],
|
|
349
|
+
target: null,
|
|
350
|
+
error: errorMessage(error),
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const matches = targetRequested
|
|
356
|
+
? results.filter((result) => result.ok === true && result.target)
|
|
357
|
+
: [];
|
|
358
|
+
const selected = targetRequested
|
|
359
|
+
? matches.length === 1
|
|
360
|
+
? matches[0]
|
|
361
|
+
: null
|
|
362
|
+
: results.find((result) => result.ok === true) || null;
|
|
363
|
+
|
|
364
|
+
return buildProbeResponse({
|
|
365
|
+
candidates,
|
|
366
|
+
results,
|
|
367
|
+
selected,
|
|
368
|
+
matches,
|
|
369
|
+
targetRequested,
|
|
370
|
+
callerProvenance,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
47
374
|
export function getProcStart(pid = process.pid) {
|
|
48
375
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
49
376
|
throw new Error(`invalid pid: ${pid}`);
|
|
@@ -141,6 +468,7 @@ export function buildDaemonAttachRequest({
|
|
|
141
468
|
cols = DEFAULT_DAEMON_ATTACH_COLS,
|
|
142
469
|
rows = 40,
|
|
143
470
|
caps = { terminal: null, mux: null, ssh: false },
|
|
471
|
+
auth,
|
|
144
472
|
} = {}) {
|
|
145
473
|
if (!short) throw new Error("short is required");
|
|
146
474
|
return {
|
|
@@ -150,6 +478,7 @@ export function buildDaemonAttachRequest({
|
|
|
150
478
|
cols,
|
|
151
479
|
rows,
|
|
152
480
|
caps,
|
|
481
|
+
...(auth ? { auth } : {}),
|
|
153
482
|
};
|
|
154
483
|
}
|
|
155
484
|
|
|
@@ -683,6 +1012,7 @@ export function attachClaudeDaemonSession({
|
|
|
683
1012
|
controlSock,
|
|
684
1013
|
short,
|
|
685
1014
|
input,
|
|
1015
|
+
auth,
|
|
686
1016
|
cols = DEFAULT_DAEMON_ATTACH_COLS,
|
|
687
1017
|
rows = 40,
|
|
688
1018
|
caps,
|
|
@@ -790,7 +1120,9 @@ export function attachClaudeDaemonSession({
|
|
|
790
1120
|
});
|
|
791
1121
|
socket.on("connect", () => {
|
|
792
1122
|
socket.write(
|
|
793
|
-
`${JSON.stringify(
|
|
1123
|
+
`${JSON.stringify(
|
|
1124
|
+
buildDaemonAttachRequest({ short, cols, rows, caps, auth }),
|
|
1125
|
+
)}\n`,
|
|
794
1126
|
);
|
|
795
1127
|
});
|
|
796
1128
|
socket.on("data", (chunk) => {
|
|
@@ -906,6 +1238,7 @@ export function attachClaudeDaemonSession({
|
|
|
906
1238
|
export function interruptClaudeDaemonSession({
|
|
907
1239
|
controlSock,
|
|
908
1240
|
short,
|
|
1241
|
+
auth,
|
|
909
1242
|
cols = DEFAULT_DAEMON_ATTACH_COLS,
|
|
910
1243
|
rows = 40,
|
|
911
1244
|
caps,
|
|
@@ -956,7 +1289,9 @@ export function interruptClaudeDaemonSession({
|
|
|
956
1289
|
socket.on("error", fail);
|
|
957
1290
|
socket.on("connect", () => {
|
|
958
1291
|
socket.write(
|
|
959
|
-
`${JSON.stringify(
|
|
1292
|
+
`${JSON.stringify(
|
|
1293
|
+
buildDaemonAttachRequest({ short, cols, rows, caps, auth }),
|
|
1294
|
+
)}\n`,
|
|
960
1295
|
);
|
|
961
1296
|
});
|
|
962
1297
|
socket.on("data", (chunk) => {
|
|
@@ -1182,6 +1517,7 @@ export async function sendKillBySessionId({
|
|
|
1182
1517
|
daemonPaths,
|
|
1183
1518
|
sessionId,
|
|
1184
1519
|
timeoutMs = 6000,
|
|
1520
|
+
auth,
|
|
1185
1521
|
} = {}) {
|
|
1186
1522
|
const controlSock = daemonPaths?.controlSock;
|
|
1187
1523
|
if (!controlSock || !sessionId) {
|
|
@@ -1201,12 +1537,16 @@ export async function sendKillBySessionId({
|
|
|
1201
1537
|
if (!job?.short) {
|
|
1202
1538
|
return { ok: true, killed: false, reason: "not_found" };
|
|
1203
1539
|
}
|
|
1540
|
+
const controlAuth = auth
|
|
1541
|
+
? { auth }
|
|
1542
|
+
: await buildDaemonControlAuth(daemonPaths?.configDir);
|
|
1204
1543
|
return await sendClaudeControlRequest(
|
|
1205
1544
|
controlSock,
|
|
1206
1545
|
{
|
|
1207
1546
|
proto: 1,
|
|
1208
1547
|
op: "kill",
|
|
1209
1548
|
short: job.short,
|
|
1549
|
+
...controlAuth,
|
|
1210
1550
|
},
|
|
1211
1551
|
{ timeoutMs },
|
|
1212
1552
|
);
|
|
@@ -1356,6 +1696,7 @@ export async function teardownClaudeDaemonJob({
|
|
|
1356
1696
|
_deps.removeClaudeSessionProjection || removeClaudeSessionProjection;
|
|
1357
1697
|
const killJob = _deps.killDaemonJob || killDaemonJob;
|
|
1358
1698
|
const removeJobStateImpl = _deps.removeClaudeJobState;
|
|
1699
|
+
const buildAuth = _deps.buildDaemonControlAuth || buildDaemonControlAuth;
|
|
1359
1700
|
const resolvedControlSock = controlSock || paths?.controlSock;
|
|
1360
1701
|
const resolvedJobsDir =
|
|
1361
1702
|
jobsDir ||
|
|
@@ -1371,7 +1712,10 @@ export async function teardownClaudeDaemonJob({
|
|
|
1371
1712
|
steps.push(sendKillBySessionId({ daemonPaths, sessionId }).catch(() => {}));
|
|
1372
1713
|
}
|
|
1373
1714
|
if (resolvedControlSock && short) {
|
|
1374
|
-
|
|
1715
|
+
const controlAuth = await buildAuth(paths?.configDir);
|
|
1716
|
+
steps.push(
|
|
1717
|
+
killJob(resolvedControlSock, short, controlAuth).catch(() => {}),
|
|
1718
|
+
);
|
|
1375
1719
|
}
|
|
1376
1720
|
if (removeJobState && removeJobStateImpl && resolvedJobsDir && short) {
|
|
1377
1721
|
steps.push(removeJobStateImpl(resolvedJobsDir, short).catch(() => {}));
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
dispatchClaudeDaemonJob,
|
|
11
11
|
getProcStart,
|
|
12
12
|
killDaemonJob,
|
|
13
|
+
resolveClaudeConfigDir,
|
|
13
14
|
resolveDaemonBridgeSessionId,
|
|
14
15
|
sendClaudeControlRequest,
|
|
15
16
|
teardownClaudeDaemonJob,
|
|
@@ -22,9 +23,11 @@ import {
|
|
|
22
23
|
} from "./claude-session-projection.mjs";
|
|
23
24
|
import { createInteractiveTuiTransport } from "./interactive-tui-transport.mjs";
|
|
24
25
|
|
|
25
|
-
// daemon-control 이 deriveClaudeDaemonPaths / getProcStart
|
|
26
|
-
//
|
|
27
|
-
|
|
26
|
+
// daemon-control 이 deriveClaudeDaemonPaths / getProcStart /
|
|
27
|
+
// resolveClaudeConfigDir 의 단일 owner 다. configDir 해석이 갈리면 양쪽이
|
|
28
|
+
// 서로 다른 control.sock hash 를 보게 되므로 (HOME 오버라이드 split-brain)
|
|
29
|
+
// 로컬 복제본을 두지 않는다. 기존 import 경로 호환을 위해 re-export 한다.
|
|
30
|
+
export { deriveClaudeDaemonPaths, getProcStart, resolveClaudeConfigDir };
|
|
28
31
|
|
|
29
32
|
const DEFAULT_ROWS = 40;
|
|
30
33
|
const DEFAULT_COLS = 120;
|
|
@@ -33,11 +36,6 @@ const ROSTER_LOCK_STALE_MS = 30_000;
|
|
|
33
36
|
const ROSTER_LOCK_TIMEOUT_MS = 5_000;
|
|
34
37
|
const ROSTER_LOCK_RETRY_MS = 10;
|
|
35
38
|
|
|
36
|
-
export function resolveClaudeConfigDir(env = process.env) {
|
|
37
|
-
if (env.CLAUDE_CONFIG_DIR) return path.resolve(env.CLAUDE_CONFIG_DIR);
|
|
38
|
-
return path.join(os.homedir(), ".claude");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
39
|
export function buildPtyDataFrame(value) {
|
|
42
40
|
const payload = Buffer.isBuffer(value) ? value : Buffer.from(value, "utf8");
|
|
43
41
|
const frame = Buffer.allocUnsafe(5 + payload.length);
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -1140,6 +1140,7 @@ export function createConductor(opts = {}) {
|
|
|
1140
1140
|
const sandbox = buildWorkerSandboxEnv({
|
|
1141
1141
|
cwd: resolvedConfig.workdir || process.cwd(),
|
|
1142
1142
|
sessionId: resolvedConfig.id,
|
|
1143
|
+
agent: resolvedConfig.agent,
|
|
1143
1144
|
env: { ...process.env, ...(resolvedConfig.env || {}) },
|
|
1144
1145
|
});
|
|
1145
1146
|
resolvedConfig = {
|
package/hub/team/handoff.mjs
CHANGED
|
@@ -23,7 +23,7 @@ After completing the task, you MUST output a HANDOFF block in exactly this forma
|
|
|
23
23
|
status: ok | partial | failed
|
|
24
24
|
lead_action: accept | needs_read | retry | reassign
|
|
25
25
|
task: <1-3 word task type>
|
|
26
|
-
files_changed: <comma-separated file paths, or "none">
|
|
26
|
+
files_changed: <comma-separated repo-root relative file paths, or "none">
|
|
27
27
|
verdict: <one sentence conclusion>
|
|
28
28
|
confidence: high | medium | low
|
|
29
29
|
risk: low | med | high
|
|
@@ -36,21 +36,25 @@ partial_output: yes | no
|
|
|
36
36
|
|
|
37
37
|
Rules:
|
|
38
38
|
- The HANDOFF block must start with exactly "--- HANDOFF ---"
|
|
39
|
+
- Set status: ok only after you have verified the work (file/diff/test evidence) — unverified claims must use partial or failed.
|
|
39
40
|
- Each field must be on its own line as "key: value"
|
|
41
|
+
- Report files_changed as repo-root relative paths
|
|
40
42
|
- verdict must be a single concise sentence
|
|
41
43
|
- Do not skip any required field
|
|
44
|
+
- This block owns the final output position; this block must be the last thing you output
|
|
42
45
|
`.trim();
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* CLI 프롬프트 길이 제한을 고려한 축약 HANDOFF 지시
|
|
46
49
|
*/
|
|
47
|
-
export const HANDOFF_INSTRUCTION_SHORT = `After completing, output this block at the end:
|
|
50
|
+
export const HANDOFF_INSTRUCTION_SHORT = `After completing, output this block at the end; this block must be the last thing you output:
|
|
48
51
|
--- HANDOFF ---
|
|
49
52
|
status: ok | partial | failed
|
|
50
53
|
lead_action: accept | needs_read | retry | reassign
|
|
51
54
|
verdict: <one sentence>
|
|
52
|
-
files_changed: <comma-separated paths or "none">
|
|
53
|
-
confidence: high | medium | low
|
|
55
|
+
files_changed: <comma-separated repo-root relative paths or "none">
|
|
56
|
+
confidence: high | medium | low
|
|
57
|
+
Set status: ok only after you have verified the work (file/diff/test evidence) — unverified claims must use partial or failed.`;
|
|
54
58
|
|
|
55
59
|
/**
|
|
56
60
|
* raw 텍스트에서 HANDOFF 블록을 파싱한다.
|
package/hub/team/headless.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
import { createRequire } from "node:module";
|
|
19
19
|
import net from "node:net";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
|
-
import { dirname, join } from "node:path";
|
|
21
|
+
import { dirname, join, resolve } from "node:path";
|
|
22
22
|
import { fileURLToPath } from "node:url";
|
|
23
23
|
import { requestJson } from "../bridge.mjs";
|
|
24
24
|
import { escapePwshSingleQuoted } from "../cli-adapter-base.mjs";
|
|
@@ -26,6 +26,7 @@ import { getMaxSpawnPerSec } from "../lib/spawn-trace.mjs";
|
|
|
26
26
|
import { IS_WINDOWS } from "../platform.mjs";
|
|
27
27
|
import { getBackend } from "./backend.mjs";
|
|
28
28
|
import {
|
|
29
|
+
buildDaemonControlAuth,
|
|
29
30
|
buildDaemonExecDispatchPayload,
|
|
30
31
|
deriveClaudeDaemonPaths as deriveClaudeControlPaths,
|
|
31
32
|
dispatchClaudeDaemonJob,
|
|
@@ -255,7 +256,7 @@ function unregisterHeadlessSynapseWorker(workerId) {
|
|
|
255
256
|
/** MCP 프로필별 프롬프트 힌트 (tfx-route.sh resolve_mcp_policy의 경량 미러) */
|
|
256
257
|
const MCP_PROFILE_HINTS = {
|
|
257
258
|
implement:
|
|
258
|
-
"You have full filesystem read/write access. Implement changes directly.",
|
|
259
|
+
"You have full filesystem read/write access. Implement changes directly. After changes, run the narrowest relevant test/lint for the files you touched; if none can run, state why and the next-best check.",
|
|
259
260
|
analyze:
|
|
260
261
|
"Focus on reading and analyzing the codebase. Prefer analysis over modification.",
|
|
261
262
|
review: "Review the code for quality, security, and correctness.",
|
|
@@ -301,22 +302,31 @@ export function buildHeadlessCommand(cli, prompt, resultFile, opts = {}) {
|
|
|
301
302
|
// contextFile 처리: 32KB(32768 bytes) 초과 시 UTF-8 안전 절단
|
|
302
303
|
let contextPrefix = "";
|
|
303
304
|
if (contextFile && existsSync(contextFile)) {
|
|
305
|
+
const contextSource = resolve(contextFile);
|
|
304
306
|
let ctx = readFileSync(contextFile, "utf8");
|
|
307
|
+
let truncated = false;
|
|
305
308
|
if (Buffer.byteLength(ctx, "utf8") > 32768) {
|
|
306
309
|
ctx = Buffer.from(ctx).subarray(0, 32768).toString("utf8");
|
|
310
|
+
truncated = true;
|
|
311
|
+
}
|
|
312
|
+
if (truncated) {
|
|
313
|
+
ctx = `${ctx}\n[... truncated at 32KB]`;
|
|
307
314
|
}
|
|
308
315
|
if (ctx.length > 0) {
|
|
309
|
-
contextPrefix = `<prior_context>\n${ctx}\n</prior_context>\n\n`;
|
|
316
|
+
contextPrefix = `<prior_context source="${contextSource}" truncated="${truncated ? "true" : "false"}">\n${ctx}\n</prior_context>\nBase your work on the prior_context above; if it conflicts with the task below, the task wins.\n\n`;
|
|
310
317
|
}
|
|
311
318
|
}
|
|
312
319
|
|
|
313
320
|
const mcpHint =
|
|
314
321
|
mcp && MCP_PROFILE_HINTS[mcp]
|
|
315
|
-
?
|
|
322
|
+
? `\n\n[MCP: ${mcp}]\n${MCP_PROFILE_HINTS[mcp]}`
|
|
316
323
|
: "";
|
|
324
|
+
const workspaceHint = cwd
|
|
325
|
+
? `\n\n[workspace] root(절대경로): ${resolve(cwd)}. 모든 상대경로는 이 루트 기준. 파일 쓰기/커밋 전 \`git rev-parse --show-toplevel\` 이 이 root 와 일치하는지 확인하고 불일치 시 중단·보고.`
|
|
326
|
+
: "";
|
|
317
327
|
// P2: HANDOFF 지시를 프롬프트에 삽입 (워커가 구조화된 handoff 블록을 출력하도록)
|
|
318
328
|
const handoffHint = handoff ? `\n\n${HANDOFF_INSTRUCTION_SHORT}` : "";
|
|
319
|
-
const fullPrompt = `${contextPrefix}${prompt}${mcpHint}${handoffHint}`;
|
|
329
|
+
const fullPrompt = `${contextPrefix}${prompt}${mcpHint}${workspaceHint}${handoffHint}`;
|
|
320
330
|
|
|
321
331
|
// 보안: 프롬프트를 임시 파일에 쓰고 파일 참조로 전달 (셸 주입 방지).
|
|
322
332
|
// 이전 구현은 `"$(cat 'PATH')"` shell-expansion expression 을 만들어
|
|
@@ -1064,9 +1074,14 @@ export async function cleanupDaemonDispatches(dispatches) {
|
|
|
1064
1074
|
}).catch(() => {});
|
|
1065
1075
|
}
|
|
1066
1076
|
if (dispatch.controlSock && dispatch.daemonShort) {
|
|
1067
|
-
await
|
|
1068
|
-
|
|
1069
|
-
);
|
|
1077
|
+
const controlAuth = await buildDaemonControlAuth(
|
|
1078
|
+
dispatch.daemonPaths?.configDir,
|
|
1079
|
+
).catch(() => ({}));
|
|
1080
|
+
await killDaemonJob(
|
|
1081
|
+
dispatch.controlSock,
|
|
1082
|
+
dispatch.daemonShort,
|
|
1083
|
+
controlAuth,
|
|
1084
|
+
).catch(() => {});
|
|
1070
1085
|
}
|
|
1071
1086
|
}),
|
|
1072
1087
|
);
|
|
@@ -1244,9 +1259,16 @@ async function waitForDaemonCompletion(
|
|
|
1244
1259
|
dispatch.daemonCompletionMatched = true;
|
|
1245
1260
|
cleanupDaemonDispatches([dispatch]).catch(() => {});
|
|
1246
1261
|
} else {
|
|
1247
|
-
|
|
1248
|
-
() => {}
|
|
1249
|
-
|
|
1262
|
+
buildDaemonControlAuth(dispatch.daemonPaths?.configDir)
|
|
1263
|
+
.catch(() => ({}))
|
|
1264
|
+
.then((controlAuth) =>
|
|
1265
|
+
killDaemonJob(
|
|
1266
|
+
dispatch.controlSock,
|
|
1267
|
+
dispatch.daemonShort,
|
|
1268
|
+
controlAuth,
|
|
1269
|
+
),
|
|
1270
|
+
)
|
|
1271
|
+
.catch(() => {});
|
|
1250
1272
|
}
|
|
1251
1273
|
resolve(finalCompletion);
|
|
1252
1274
|
};
|