@triflux/remote 10.1.0 → 10.26.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.
Files changed (162) hide show
  1. package/hub/pipe.mjs +239 -121
  2. package/hub/public/dashboard.html +61 -0
  3. package/hub/server.mjs +2179 -709
  4. package/hub/store-adapter.mjs +59 -26
  5. package/hub/store.mjs +356 -153
  6. package/hub/team/agent-map.json +26 -7
  7. package/hub/team/ansi.mjs +245 -146
  8. package/hub/team/backend.mjs +78 -17
  9. package/hub/team/build-worker-prompt.mjs +73 -0
  10. package/hub/team/check-mcp-hub.mjs +49 -0
  11. package/hub/team/claude-daemon-control.mjs +1130 -0
  12. package/hub/team/claude-native-bridge.mjs +1011 -0
  13. package/hub/team/claude-session-projection.mjs +74 -0
  14. package/hub/team/cli/commands/attach.mjs +57 -37
  15. package/hub/team/cli/commands/control.mjs +29 -8
  16. package/hub/team/cli/commands/debug.mjs +95 -74
  17. package/hub/team/cli/commands/focus.mjs +80 -53
  18. package/hub/team/cli/commands/interrupt.mjs +18 -6
  19. package/hub/team/cli/commands/kill.mjs +16 -5
  20. package/hub/team/cli/commands/list.mjs +31 -24
  21. package/hub/team/cli/commands/send.mjs +19 -6
  22. package/hub/team/cli/commands/start/index.mjs +151 -33
  23. package/hub/team/cli/commands/start/parse-args.mjs +97 -13
  24. package/hub/team/cli/commands/start/start-headless.mjs +155 -37
  25. package/hub/team/cli/commands/start/start-in-process.mjs +50 -40
  26. package/hub/team/cli/commands/start/start-mux.mjs +122 -73
  27. package/hub/team/cli/commands/start/start-wt.mjs +86 -69
  28. package/hub/team/cli/commands/status.mjs +43 -14
  29. package/hub/team/cli/commands/stop.mjs +11 -4
  30. package/hub/team/cli/commands/task.mjs +8 -3
  31. package/hub/team/cli/commands/tasks.mjs +13 -13
  32. package/hub/team/cli/help.mjs +1 -1
  33. package/hub/team/cli/index.mjs +2 -2
  34. package/hub/team/cli/manifest.mjs +38 -8
  35. package/hub/team/cli/render.mjs +52 -30
  36. package/hub/team/cli/services/attach-fallback.mjs +74 -54
  37. package/hub/team/cli/services/hub-client.mjs +44 -16
  38. package/hub/team/cli/services/member-selector.mjs +37 -30
  39. package/hub/team/cli/services/native-control.mjs +144 -116
  40. package/hub/team/cli/services/runtime-mode.mjs +61 -4
  41. package/hub/team/cli/services/state-store.mjs +66 -9
  42. package/hub/team/cli/services/task-model.mjs +40 -30
  43. package/hub/team/codex-review.mjs +521 -0
  44. package/hub/team/conductor-mesh-bridge.mjs +25 -24
  45. package/hub/team/conductor-registry.mjs +121 -0
  46. package/hub/team/conductor.mjs +1379 -671
  47. package/hub/team/consensus-meta.mjs +176 -0
  48. package/hub/team/dashboard-anchor.mjs +4 -5
  49. package/hub/team/dashboard-layout.mjs +3 -1
  50. package/hub/team/dashboard-open.mjs +20 -118
  51. package/hub/team/dashboard.mjs +76 -28
  52. package/hub/team/event-log.mjs +18 -10
  53. package/hub/team/execution-mode.mjs +256 -0
  54. package/hub/team/extract-completion-payload.mjs +110 -0
  55. package/hub/team/git-preflight.mjs +286 -0
  56. package/hub/team/handoff.mjs +49 -18
  57. package/hub/team/headless-bridge-session.mjs +17 -0
  58. package/hub/team/headless.mjs +2213 -1156
  59. package/hub/team/health-probe.mjs +232 -69
  60. package/hub/team/interactive-native-launcher.mjs +106 -0
  61. package/hub/team/interactive-native-worker.mjs +80 -0
  62. package/hub/team/interactive-tui-transport.mjs +465 -0
  63. package/hub/team/launcher-template.mjs +49 -17
  64. package/hub/team/lead-control.mjs +91 -4
  65. package/hub/team/mcp-selector.mjs +145 -0
  66. package/hub/team/native-supervisor.mjs +90 -26
  67. package/hub/team/native.mjs +120 -42
  68. package/hub/team/nativeProxy.mjs +184 -113
  69. package/hub/team/notify.mjs +147 -78
  70. package/hub/team/orchestrator.mjs +173 -161
  71. package/hub/team/pane.mjs +43 -20
  72. package/hub/team/process-cleanup.mjs +69 -25
  73. package/hub/team/psmux.mjs +1022 -281
  74. package/hub/team/recovery-store.mjs +149 -0
  75. package/hub/team/remote-probe.mjs +68 -52
  76. package/hub/team/remote-session.mjs +131 -59
  77. package/hub/team/remote-watcher.mjs +61 -33
  78. package/hub/team/retry-state-machine.mjs +336 -0
  79. package/hub/team/routing.mjs +51 -25
  80. package/hub/team/runtime-strategy.mjs +200 -0
  81. package/hub/team/schemas/worker-completion.json +39 -0
  82. package/hub/team/sentinel-capture.mjs +127 -0
  83. package/hub/team/session-sync.mjs +153 -6
  84. package/hub/team/session.mjs +574 -611
  85. package/hub/team/shared.mjs +13 -13
  86. package/hub/team/staleState.mjs +72 -30
  87. package/hub/team/swarm-cli.mjs +390 -0
  88. package/hub/team/swarm-hypervisor.mjs +1544 -193
  89. package/hub/team/swarm-intent.mjs +216 -0
  90. package/hub/team/swarm-locks.mjs +140 -25
  91. package/hub/team/swarm-planner.mjs +121 -21
  92. package/hub/team/swarm-preflight.mjs +319 -0
  93. package/hub/team/swarm-reconciler.mjs +128 -23
  94. package/hub/team/synapse-cli.mjs +458 -0
  95. package/hub/team/synapse-http.mjs +60 -0
  96. package/hub/team/synapse-registry.mjs +329 -0
  97. package/hub/team/terminal-opener.mjs +216 -0
  98. package/hub/team/tui-core.mjs +292 -0
  99. package/hub/team/tui-lite.mjs +237 -148
  100. package/hub/team/tui-remote-adapter.mjs +18 -34
  101. package/hub/team/tui-synapse.mjs +223 -0
  102. package/hub/team/tui-viewer.mjs +108 -48
  103. package/hub/team/tui-widgets.mjs +262 -0
  104. package/hub/team/tui.mjs +931 -439
  105. package/hub/team/worker-completion-validator.mjs +84 -0
  106. package/hub/team/worker-sandbox.mjs +97 -0
  107. package/hub/team/worker-signal.mjs +263 -0
  108. package/hub/team/worker-signal.types.d.ts +52 -0
  109. package/hub/team/worktree-lifecycle.mjs +517 -63
  110. package/hub/team/wt-manager.mjs +297 -26
  111. package/hub/tools.mjs +477 -252
  112. package/hub/tray.mjs +411 -368
  113. package/hub/workers/antigravity-route-worker.mjs +169 -0
  114. package/hub/workers/claude-worker.mjs +162 -118
  115. package/hub/workers/codex-app-server-worker.mjs +1015 -0
  116. package/hub/workers/codex-mcp.mjs +724 -507
  117. package/hub/workers/delegator-mcp.mjs +570 -499
  118. package/hub/workers/factory.mjs +103 -21
  119. package/hub/workers/gemini-worker.mjs +126 -284
  120. package/hub/workers/interface.mjs +24 -2
  121. package/hub/workers/lib/jsonrpc-stdio.mjs +463 -0
  122. package/hub/workers/worker-utils.mjs +23 -17
  123. package/package.json +4 -3
  124. package/scripts/lib/agent-json.mjs +27 -0
  125. package/scripts/lib/async.mjs +174 -0
  126. package/scripts/lib/claudemd-scanner.mjs +223 -0
  127. package/scripts/lib/cli-agy.mjs +62 -0
  128. package/scripts/lib/cli-claude.mjs +78 -0
  129. package/scripts/lib/cli-codex.mjs +199 -0
  130. package/scripts/lib/cli-gemini.mjs +37 -0
  131. package/scripts/lib/codex-recovery.sh +50 -0
  132. package/scripts/lib/context.mjs +67 -0
  133. package/scripts/lib/cross-review-utils.mjs +59 -0
  134. package/scripts/lib/doctor-env-checks.mjs +121 -0
  135. package/scripts/lib/dynamic-route-cli.mjs +107 -0
  136. package/scripts/lib/env-probe.mjs +269 -0
  137. package/scripts/lib/env.mjs +14 -0
  138. package/scripts/lib/gemini-profiles.mjs +119 -0
  139. package/scripts/lib/handoff.mjs +187 -0
  140. package/scripts/lib/hook-utils.mjs +16 -0
  141. package/scripts/lib/hub.mjs +48 -0
  142. package/scripts/lib/keyword-rules.mjs +195 -0
  143. package/scripts/lib/logger.mjs +105 -0
  144. package/scripts/lib/mcp-filter.mjs +883 -0
  145. package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
  146. package/scripts/lib/mcp-gateway-servers.mjs +70 -0
  147. package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
  148. package/scripts/lib/mcp-guard-engine.mjs +2120 -0
  149. package/scripts/lib/mcp-health.mjs +773 -0
  150. package/scripts/lib/mcp-manifest.mjs +89 -0
  151. package/scripts/lib/mcp-server-catalog.mjs +355 -0
  152. package/scripts/lib/pid.mjs +63 -0
  153. package/scripts/lib/process-utils.mjs +26 -0
  154. package/scripts/lib/psmux-info.mjs +224 -0
  155. package/scripts/lib/quota.mjs +47 -0
  156. package/scripts/lib/remote-spawn-transfer.mjs +226 -0
  157. package/scripts/lib/skill-state.mjs +220 -0
  158. package/scripts/lib/skill-template.mjs +375 -0
  159. package/scripts/lib/team.mjs +75 -0
  160. package/scripts/lib/timeout.mjs +66 -0
  161. package/scripts/lib/tmp.mjs +26 -0
  162. package/scripts/lib/toml.mjs +113 -0
@@ -0,0 +1,375 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import {
3
+ basename,
4
+ extname,
5
+ isAbsolute,
6
+ join,
7
+ relative,
8
+ resolve,
9
+ } from "node:path";
10
+
11
+ const IF_TAG_RE = /{{\s*(#if\s+([A-Za-z0-9_.-]+)|\/if)\s*}}/g;
12
+ const PARTIAL_RE = /{{>\s*([A-Za-z0-9_./-]+)\s*}}/g;
13
+ const FILE_INCLUDE_RE = /{{#include\s+([A-Za-z0-9_./-]+)\s*}}/g;
14
+ const VARIABLE_RE = /{{\s*([A-Za-z0-9_.-]+)\s*}}/g;
15
+ const FRONTMATTER_RE = /^---\r?\n(?:([\s\S]*?)\r?\n)?---\r?\n?/;
16
+
17
+ function isTruthy(value) {
18
+ if (typeof value === "string") return value.trim().length > 0;
19
+ return Boolean(value);
20
+ }
21
+
22
+ function normalizeVariable(context, key) {
23
+ if (Object.hasOwn(context, key)) return context[key];
24
+ const upper = key.toUpperCase();
25
+ if (Object.hasOwn(context, upper)) return context[upper];
26
+ return undefined;
27
+ }
28
+
29
+ function parseBoolean(raw) {
30
+ if (typeof raw !== "string") return undefined;
31
+ if (raw === "true") return true;
32
+ if (raw === "false") return false;
33
+ return undefined;
34
+ }
35
+
36
+ function parseScalar(raw) {
37
+ if (raw == null) return "";
38
+ const value = raw.trim();
39
+ if (!value) return "";
40
+ if (
41
+ (value.startsWith('"') && value.endsWith('"')) ||
42
+ (value.startsWith("'") && value.endsWith("'"))
43
+ ) {
44
+ return value.slice(1, -1);
45
+ }
46
+ const bool = parseBoolean(value);
47
+ return bool ?? value;
48
+ }
49
+
50
+ function parseMultilineValue(lines, startIndex, marker) {
51
+ const fold = marker.startsWith(">");
52
+ const chunks = [];
53
+ let index = startIndex;
54
+
55
+ while (index + 1 < lines.length && /^\s+/.test(lines[index + 1])) {
56
+ index += 1;
57
+ chunks.push(lines[index].replace(/^\s+/, ""));
58
+ }
59
+
60
+ return {
61
+ index,
62
+ value: fold ? chunks.join(" ").trim() : chunks.join("\n").trim(),
63
+ };
64
+ }
65
+
66
+ function parseListValue(lines, startIndex) {
67
+ const items = [];
68
+ let index = startIndex;
69
+
70
+ while (index + 1 < lines.length) {
71
+ const nextLine = lines[index + 1];
72
+ const match = nextLine.match(/^\s*-\s+(.*)$/);
73
+ if (match) {
74
+ index += 1;
75
+ items.push(parseScalar(match[1]));
76
+ continue;
77
+ }
78
+ if (/^\s*$/.test(nextLine)) {
79
+ index += 1;
80
+ continue;
81
+ }
82
+ break;
83
+ }
84
+
85
+ return { index, value: items };
86
+ }
87
+
88
+ function parseFrontmatterBlock(block) {
89
+ const lines = block.split(/\r?\n/);
90
+ const data = {};
91
+
92
+ for (let i = 0; i < lines.length; i += 1) {
93
+ const line = lines[i];
94
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
95
+ if (!match) continue;
96
+
97
+ const [, rawKey, rawValue] = match;
98
+ const key = rawKey.trim();
99
+ const value = rawValue.trim();
100
+
101
+ if (value === ">" || value === "|" || value === ">-" || value === "|-") {
102
+ const parsed = parseMultilineValue(lines, i, value);
103
+ data[key] = parsed.value;
104
+ i = parsed.index;
105
+ continue;
106
+ }
107
+
108
+ if (!value) {
109
+ const parsed = parseListValue(lines, i);
110
+ if (parsed.index !== i) {
111
+ data[key] = parsed.value;
112
+ i = parsed.index;
113
+ continue;
114
+ }
115
+ }
116
+
117
+ data[key] = parseScalar(value);
118
+ }
119
+
120
+ return data;
121
+ }
122
+
123
+ function evaluateConditionals(source, context) {
124
+ const matcher = new RegExp(IF_TAG_RE.source, "g");
125
+
126
+ function walk(startIndex, nested) {
127
+ let output = "";
128
+ let cursor = startIndex;
129
+
130
+ while (true) {
131
+ const found = matcher.exec(source);
132
+ if (!found) {
133
+ if (nested) throw new Error("Unclosed {{#if ...}} block in template");
134
+ output += source.slice(cursor);
135
+ return { output, index: source.length };
136
+ }
137
+
138
+ output += source.slice(cursor, found.index);
139
+ cursor = matcher.lastIndex;
140
+
141
+ const directive = found[1];
142
+ const flag = found[2];
143
+ if (directive.startsWith("#if")) {
144
+ const nestedResult = walk(cursor, true);
145
+ cursor = nestedResult.index;
146
+ matcher.lastIndex = cursor;
147
+ if (isTruthy(normalizeVariable(context, flag))) {
148
+ output += nestedResult.output;
149
+ }
150
+ continue;
151
+ }
152
+
153
+ if (!nested) {
154
+ throw new Error("Unexpected {{/if}} without matching {{#if ...}}");
155
+ }
156
+ return { output, index: cursor };
157
+ }
158
+ }
159
+
160
+ return walk(0, false).output;
161
+ }
162
+
163
+ function renderPartials(source, context, partials, options, includeStack) {
164
+ return source.replace(PARTIAL_RE, (_full, partialName) => {
165
+ const partial = partials[partialName];
166
+ if (partial == null) {
167
+ throw new Error(`Missing partial: ${partialName}`);
168
+ }
169
+ if (includeStack.includes(partialName)) {
170
+ const chain = [...includeStack, partialName].join(" -> ");
171
+ throw new Error(`Circular partial include: ${chain}`);
172
+ }
173
+ return renderWithContext(partial, context, partials, options, [
174
+ ...includeStack,
175
+ partialName,
176
+ ]);
177
+ });
178
+ }
179
+
180
+ function normalizeIncludeName(includeName) {
181
+ return includeName.replace(/\\/g, "/");
182
+ }
183
+
184
+ function resolveIncludeContent(includeName, options) {
185
+ const normalizedName = normalizeIncludeName(includeName);
186
+ if (Object.hasOwn(options.includes, normalizedName)) {
187
+ return options.includes[normalizedName];
188
+ }
189
+ if (!options.includeBaseDir) {
190
+ throw new Error(`Missing include: ${normalizedName}`);
191
+ }
192
+
193
+ const baseDir = resolve(options.includeBaseDir);
194
+ const fullPath = resolve(baseDir, normalizedName);
195
+ const relativePath = relative(baseDir, fullPath);
196
+ const isInsideBaseDir =
197
+ relativePath === "" ||
198
+ (!relativePath.startsWith("..") && !isAbsolute(relativePath));
199
+
200
+ if (!isInsideBaseDir || !existsSync(fullPath)) {
201
+ throw new Error(`Missing include: ${normalizedName}`);
202
+ }
203
+
204
+ const content = readFileSync(fullPath, "utf8");
205
+ options.includes[normalizedName] = content;
206
+ return content;
207
+ }
208
+
209
+ function renderFileIncludes(source, context, partials, options, includeStack) {
210
+ return source.replace(FILE_INCLUDE_RE, (_full, includeName) => {
211
+ const normalizedName = normalizeIncludeName(includeName);
212
+ const content = resolveIncludeContent(normalizedName, options);
213
+ if (includeStack.includes(normalizedName)) {
214
+ const chain = [...includeStack, normalizedName].join(" -> ");
215
+ throw new Error(`Circular template include: ${chain}`);
216
+ }
217
+ return renderWithContext(content, context, partials, options, [
218
+ ...includeStack,
219
+ normalizedName,
220
+ ]);
221
+ });
222
+ }
223
+
224
+ function renderVariables(source, context) {
225
+ return source.replace(VARIABLE_RE, (full, key) => {
226
+ const value = normalizeVariable(context, key);
227
+ if (value == null) {
228
+ throw new Error(`Missing template variable: ${key}`);
229
+ }
230
+ return String(value);
231
+ });
232
+ }
233
+
234
+ function renderWithContext(
235
+ source,
236
+ context,
237
+ partials,
238
+ options,
239
+ includeStack = [],
240
+ ) {
241
+ const afterIf = evaluateConditionals(source, context);
242
+ const afterPartials = renderPartials(
243
+ afterIf,
244
+ context,
245
+ partials,
246
+ options,
247
+ includeStack,
248
+ );
249
+ const afterFileIncludes = renderFileIncludes(
250
+ afterPartials,
251
+ context,
252
+ partials,
253
+ options,
254
+ includeStack,
255
+ );
256
+ return renderVariables(afterFileIncludes, context);
257
+ }
258
+
259
+ function readAllTemplateFiles(rootDir, currentDir = rootDir) {
260
+ if (!rootDir || !existsSync(rootDir)) return [];
261
+
262
+ const entries = readdirSync(currentDir, { withFileTypes: true }).sort(
263
+ (left, right) => left.name.localeCompare(right.name),
264
+ );
265
+ const files = [];
266
+
267
+ for (const entry of entries) {
268
+ const fullPath = join(currentDir, entry.name);
269
+ if (entry.isDirectory()) {
270
+ files.push(...readAllTemplateFiles(rootDir, fullPath));
271
+ continue;
272
+ }
273
+
274
+ if (!entry.isFile()) continue;
275
+ if (!entry.name.endsWith(".md") && !entry.name.endsWith(".tmpl")) continue;
276
+
277
+ const relativePath = relative(rootDir, fullPath).replace(/\\/g, "/");
278
+ const content = readFileSync(fullPath, "utf8");
279
+ files.push({ fullPath, relativePath, content });
280
+ }
281
+
282
+ return files;
283
+ }
284
+
285
+ function setPartial(partials, key, value) {
286
+ if (!key) return;
287
+ if (!Object.hasOwn(partials, key)) {
288
+ partials[key] = value;
289
+ }
290
+ }
291
+
292
+ export function parseFrontmatter(source) {
293
+ const match = source.match(FRONTMATTER_RE);
294
+ if (!match) return { data: {}, body: source };
295
+
296
+ const data = parseFrontmatterBlock(match[1] ?? "");
297
+ const body = source.slice(match[0].length);
298
+ return { data, body };
299
+ }
300
+
301
+ export function buildSkillTemplateContext({
302
+ frontmatter = {},
303
+ skillDirName = "",
304
+ } = {}) {
305
+ const context = { ...frontmatter };
306
+ const skillName = String(frontmatter.name || skillDirName || "").trim();
307
+ const skillDescription = String(frontmatter.description || "").trim();
308
+
309
+ let deep = frontmatter.DEEP ?? frontmatter.deep;
310
+ if (typeof deep === "string") {
311
+ const parsed = parseBoolean(deep.trim().toLowerCase());
312
+ deep = parsed ?? deep;
313
+ }
314
+ if (typeof deep !== "boolean") {
315
+ deep = /(^|[-_])deep($|[-_])/i.test(skillName);
316
+ }
317
+
318
+ context.SKILL_NAME = skillName;
319
+ context.SKILL_DESCRIPTION = skillDescription;
320
+ context.DEEP = deep;
321
+ return context;
322
+ }
323
+
324
+ export function loadTemplatePartials(partialsDir) {
325
+ const files = readAllTemplateFiles(partialsDir);
326
+ const partials = {};
327
+
328
+ for (const file of files) {
329
+ const extension = extname(file.relativePath);
330
+ const withoutExt = extension
331
+ ? file.relativePath.slice(0, -extension.length)
332
+ : file.relativePath;
333
+ const normalized = withoutExt.replace(/\\/g, "/");
334
+ const base = basename(normalized);
335
+
336
+ setPartial(partials, normalized, file.content);
337
+ setPartial(partials, base, file.content);
338
+ }
339
+
340
+ return partials;
341
+ }
342
+
343
+ export function loadSkillManifest(skillDir) {
344
+ const manifestPath = join(skillDir, "skill.json");
345
+ if (!existsSync(manifestPath)) return null;
346
+
347
+ const raw = readFileSync(manifestPath, "utf8");
348
+ return JSON.parse(raw);
349
+ }
350
+
351
+ export function parseFrontmatterWithManifest(source, skillDir) {
352
+ const manifest = skillDir ? loadSkillManifest(skillDir) : null;
353
+ const { data: yamlData, body } = parseFrontmatter(source);
354
+
355
+ if (!manifest) return { data: yamlData, body };
356
+
357
+ const merged = { ...yamlData };
358
+ if (manifest.name) merged.name = manifest.name;
359
+ if (manifest.description) merged.description = manifest.description;
360
+ if (manifest.triggers) merged.triggers = manifest.triggers;
361
+ if (manifest.argument_hint) merged["argument-hint"] = manifest.argument_hint;
362
+ if (manifest.internal != null) merged.internal = manifest.internal;
363
+ if (manifest.deprecated != null) merged.deprecated = manifest.deprecated;
364
+ if (manifest.superseded_by) merged["superseded-by"] = manifest.superseded_by;
365
+
366
+ return { data: merged, body };
367
+ }
368
+
369
+ export function renderSkillTemplate(template, context = {}, options = {}) {
370
+ const { partials = {}, includes = {}, includeBaseDir = "" } = options;
371
+ return renderWithContext(template, context, partials, {
372
+ includeBaseDir,
373
+ includes: { ...includes },
374
+ });
375
+ }
@@ -0,0 +1,75 @@
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ function teamContext(env) {
5
+ const team = env.TFX_TEAM_NAME || "";
6
+ const taskId = env.TFX_TEAM_TASK_ID || "";
7
+ if (!team || !taskId) return null;
8
+ return {
9
+ team,
10
+ taskId,
11
+ agent: env.TFX_TEAM_AGENT_NAME || "agent",
12
+ lead: env.TFX_TEAM_LEAD_NAME || "team-lead",
13
+ };
14
+ }
15
+
16
+ export async function claimTask({ env = process.env, bridge } = {}) {
17
+ const ctx = teamContext(env);
18
+ if (!ctx) return { skipped: true };
19
+ const response = await bridge.claim({
20
+ team: ctx.team,
21
+ taskId: ctx.taskId,
22
+ owner: ctx.agent,
23
+ status: "in_progress",
24
+ });
25
+ if (response?.ok) return { claimed: true };
26
+ const code = response?.error?.code;
27
+ const before = response?.error?.details?.task_before ?? {};
28
+ if (
29
+ code === "CLAIM_CONFLICT" &&
30
+ before.owner === ctx.agent &&
31
+ before.status === "in_progress"
32
+ ) {
33
+ return { claimed: true, alreadyClaimed: true };
34
+ }
35
+ if (code === "CLAIM_CONFLICT") {
36
+ return { claimed: false, conflict: true, owner: before.owner ?? null };
37
+ }
38
+ return { claimed: false, error: response?.error ?? null };
39
+ }
40
+
41
+ export async function completeTask({
42
+ env = process.env,
43
+ bridge,
44
+ result = "success",
45
+ summary = "작업 완료",
46
+ now = () => new Date().toISOString(),
47
+ } = {}) {
48
+ const ctx = teamContext(env);
49
+ if (!ctx) return { skipped: true };
50
+ const trimmed = String(summary).slice(0, 4096);
51
+ const response = await bridge.complete({
52
+ team: ctx.team,
53
+ taskId: ctx.taskId,
54
+ agent: ctx.agent,
55
+ result,
56
+ summary: trimmed,
57
+ });
58
+
59
+ const resultDir =
60
+ env.TFX_RESULT_DIR ||
61
+ join(process.env.HOME || ".", ".claude", "tfx-results", ctx.team);
62
+ mkdirSync(resultDir, { recursive: true });
63
+ writeFileSync(
64
+ join(resultDir, `${ctx.taskId}.json`),
65
+ `${JSON.stringify({
66
+ taskId: ctx.taskId,
67
+ agent: ctx.agent,
68
+ team: ctx.team,
69
+ result,
70
+ summary: trimmed,
71
+ timestamp: now(),
72
+ })}\n`,
73
+ );
74
+ return { completed: Boolean(response?.ok), response };
75
+ }
@@ -0,0 +1,66 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export function timeoutCodeForSignal(signal) {
4
+ return signal ? 124 : null;
5
+ }
6
+
7
+ export function runWithTimeout(command, args = [], options = {}) {
8
+ const {
9
+ cwd = process.cwd(),
10
+ env = process.env,
11
+ input,
12
+ timeoutMs = 0,
13
+ stdio = "pipe",
14
+ spawnFn = spawn,
15
+ } = options;
16
+
17
+ return new Promise((resolve, reject) => {
18
+ const controller = new AbortController();
19
+ let timedOut = false;
20
+ const child = spawnFn(command, args, {
21
+ cwd,
22
+ env,
23
+ stdio,
24
+ signal: controller.signal,
25
+ });
26
+ const stdout = [];
27
+ const stderr = [];
28
+ let timer = null;
29
+
30
+ if (child.stdout) child.stdout.on("data", (chunk) => stdout.push(chunk));
31
+ if (child.stderr) child.stderr.on("data", (chunk) => stderr.push(chunk));
32
+ child.on("error", (err) => {
33
+ if (timedOut && err.name === "AbortError") return;
34
+ reject(err);
35
+ });
36
+ child.on("exit", (code, signal) => {
37
+ if (timer) clearTimeout(timer);
38
+ resolve({
39
+ code: timedOut ? 124 : (code ?? timeoutCodeForSignal(signal) ?? 1),
40
+ signal,
41
+ timedOut,
42
+ stdout: Buffer.concat(stdout).toString("utf8"),
43
+ stderr: Buffer.concat(stderr).toString("utf8"),
44
+ });
45
+ });
46
+
47
+ if (timeoutMs > 0) {
48
+ timer = setTimeout(() => {
49
+ timedOut = true;
50
+ controller.abort();
51
+ if (child.pid) {
52
+ try {
53
+ process.kill(child.pid, "SIGTERM");
54
+ } catch {
55
+ // Process already exited.
56
+ }
57
+ }
58
+ }, timeoutMs);
59
+ timer.unref?.();
60
+ }
61
+
62
+ if (input !== undefined && child.stdin) {
63
+ child.stdin.end(input);
64
+ }
65
+ });
66
+ }
@@ -0,0 +1,26 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ export function resolveTmpDir({ env = process.env, cwd = process.cwd() } = {}) {
6
+ const candidate = env.TFX_TMP || tmpdir();
7
+ try {
8
+ mkdirSync(candidate, { recursive: true });
9
+ return candidate;
10
+ } catch {
11
+ const fallback = join(cwd, ".tfx-tmp");
12
+ try {
13
+ mkdirSync(fallback, { recursive: true });
14
+ } catch {
15
+ // Bash printed the fallback path even when mkdir failed.
16
+ }
17
+ return fallback;
18
+ }
19
+ }
20
+
21
+ export function resolveJobsDir({
22
+ env = process.env,
23
+ tmpDir = resolveTmpDir({ env }),
24
+ } = {}) {
25
+ return env.TFX_JOBS_DIR || join(tmpDir, "tfx-jobs");
26
+ }
@@ -0,0 +1,113 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+
4
+ const require = createRequire(import.meta.url);
5
+ let TOML = null;
6
+ try {
7
+ TOML = require("@iarna/toml");
8
+ } catch {
9
+ TOML = null;
10
+ }
11
+
12
+ function walk(value, visitor) {
13
+ if (!value || typeof value !== "object") return;
14
+ for (const [key, child] of Object.entries(value)) {
15
+ visitor(value, key, child);
16
+ walk(child, visitor);
17
+ }
18
+ }
19
+
20
+ export function parseToml(source) {
21
+ if (!TOML) return parseTomlFallback(source);
22
+ return TOML.parse(source || "");
23
+ }
24
+
25
+ export function stringifyToml(value) {
26
+ if (!TOML) return stringifyTomlFallback(value);
27
+ return TOML.stringify(value);
28
+ }
29
+
30
+ function parseTomlFallback(source = "") {
31
+ const root = {};
32
+ let current = root;
33
+ for (const rawLine of String(source).split(/\r?\n/)) {
34
+ const line = rawLine.trim();
35
+ if (!line || line.startsWith("#")) continue;
36
+ const section = line.match(/^\[([^\]]+)\]$/);
37
+ if (section) {
38
+ current = root;
39
+ for (const part of section[1].split(".")) {
40
+ current[part] ??= {};
41
+ current = current[part];
42
+ }
43
+ continue;
44
+ }
45
+ const assignment = line.match(/^([A-Za-z0-9_-]+)\s*=\s*"([^"]*)"$/);
46
+ if (assignment) {
47
+ current[assignment[1]] = assignment[2];
48
+ }
49
+ }
50
+ return root;
51
+ }
52
+
53
+ function stringifyTomlFallback(value) {
54
+ const lines = [];
55
+ for (const [key, child] of Object.entries(value)) {
56
+ if (child && typeof child === "object") continue;
57
+ lines.push(`${key} = "${String(child)}"`);
58
+ }
59
+ function writeSections(prefix, obj) {
60
+ for (const [key, child] of Object.entries(obj)) {
61
+ if (!child || typeof child !== "object") continue;
62
+ const section = [...prefix, key];
63
+ lines.push("", `[${section.join(".")}]`);
64
+ for (const [childKey, childValue] of Object.entries(child)) {
65
+ if (childValue && typeof childValue === "object") continue;
66
+ lines.push(`${childKey} = "${String(childValue)}"`);
67
+ }
68
+ writeSections(section, child);
69
+ }
70
+ }
71
+ writeSections([], value);
72
+ return `${lines.join("\n")}\n`;
73
+ }
74
+
75
+ export function detectTopLevelCodexConfig(source) {
76
+ const parsed = parseToml(source);
77
+ return {
78
+ hasTopLevelSandboxOrApproval:
79
+ Object.hasOwn(parsed, "sandbox") ||
80
+ Object.hasOwn(parsed, "approval_mode"),
81
+ };
82
+ }
83
+
84
+ export function patchMcpApprovalMode(source) {
85
+ const parsed = parseToml(source);
86
+ let count = 0;
87
+ walk(parsed.mcp_servers, (parent, key, value) => {
88
+ if (key === "approval_mode" && value === "approve") {
89
+ parent[key] = "full-auto";
90
+ count += 1;
91
+ }
92
+ });
93
+ return {
94
+ changed: count > 0,
95
+ count,
96
+ toml: count > 0 ? stringifyToml(parsed) : source,
97
+ };
98
+ }
99
+
100
+ export function patchCodexConfigFile(path, { now = () => new Date() } = {}) {
101
+ if (!path || !existsSync(path)) return { changed: false, count: 0 };
102
+ const source = readFileSync(path, "utf8");
103
+ const patched = patchMcpApprovalMode(source);
104
+ if (!patched.changed) return patched;
105
+ const stamp = now()
106
+ .toISOString()
107
+ .replace(/[-:]/g, "")
108
+ .replace(/\..*$/, "")
109
+ .replace("T", "-");
110
+ writeFileSync(`${path}.bak-${stamp}`, source);
111
+ writeFileSync(path, patched.toml);
112
+ return patched;
113
+ }