u-foo 2.3.12 → 2.3.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u-foo",
3
- "version": "2.3.12",
3
+ "version": "2.3.14",
4
4
  "description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://ufoo.dev",
@@ -23,7 +23,7 @@ function hasArg(args = [], names = []) {
23
23
  }
24
24
 
25
25
  function hasMetaCommandArgs(args = []) {
26
- return hasArg(args, ["-h", "--help", "-v", "--version"]);
26
+ return hasArg(args, ["-h", "--help", "-v", "--version", "resume"]);
27
27
  }
28
28
 
29
29
  function readOptionalFile(filePath) {
@@ -54,7 +54,7 @@ function buildDefaultStartupBootstrapPrompt({ agentType = "", projectRoot = "" }
54
54
  const normalizedAgent = asTrimmedString(agentType).toLowerCase();
55
55
  const displayAgent = normalizedAgent === "claude-code"
56
56
  ? "Claude"
57
- : (normalizedAgent === "codex" ? "Codex" : "agent");
57
+ : (normalizedAgent === "codex" ? "Codex" : (normalizedAgent === "ufoo-code" ? "ucode" : "agent"));
58
58
 
59
59
  const segments = [
60
60
  `Session bootstrap for ${displayAgent}.`,
@@ -102,6 +102,28 @@ function mergePromptSegments(...segments) {
102
102
  .join("\n\n");
103
103
  }
104
104
 
105
+ const CODEX_OPTIONS_WITH_VALUE = new Set([
106
+ "-m",
107
+ "--model",
108
+ "-c",
109
+ "--config",
110
+ "--profile",
111
+ "--sandbox",
112
+ "-s",
113
+ "--approval-mode",
114
+ "--ask-for-approval",
115
+ "--cd",
116
+ "--cwd",
117
+ "--color",
118
+ "--output-schema",
119
+ ]);
120
+
121
+ function isValueForCodexOption(args = [], index = -1) {
122
+ if (!Array.isArray(args) || index <= 0) return false;
123
+ const previous = asTrimmedString(args[index - 1]);
124
+ return CODEX_OPTIONS_WITH_VALUE.has(previous);
125
+ }
126
+
105
127
  function mergeClaudePromptArgs({
106
128
  projectRoot,
107
129
  agentType = "claude-code",
@@ -160,7 +182,9 @@ function mergeCodexPromptArgs({ args = [], bootstrapText = "" } = {}) {
160
182
  const lastIndex = currentArgs.length - 1;
161
183
  if (lastIndex < 0) return null;
162
184
  const lastItem = asTrimmedString(currentArgs[lastIndex]);
163
- const promptIndex = lastItem && !lastItem.startsWith("-") ? lastIndex : -1;
185
+ const promptIndex = lastItem && !lastItem.startsWith("-") && !isValueForCodexOption(currentArgs, lastIndex)
186
+ ? lastIndex
187
+ : -1;
164
188
 
165
189
  if (promptIndex < 0) return null;
166
190
 
@@ -262,6 +286,7 @@ function resolveDefaultManualBootstrap({
262
286
  module.exports = {
263
287
  hasArg,
264
288
  hasMetaCommandArgs,
289
+ isValueForCodexOption,
265
290
  buildDefaultStartupBootstrapPrompt,
266
291
  defaultBootstrapFile,
267
292
  prepareDefaultBootstrapFile,
@@ -4,8 +4,6 @@ const { getUfooPaths } = require("../ufoo/paths");
4
4
  const { spawnSync } = require("child_process");
5
5
  const EventBus = require("../bus");
6
6
  const { readJSON, writeJSON } = require("../bus/utils");
7
- const { runCliAgent } = require("./cliRunner");
8
- const { normalizeCliOutput } = require("./normalizeOutput");
9
7
  const { createActivityStatePublisher } = require("./activityStatePublisher");
10
8
  const { loadConfig, normalizeCodexInternalThreadMode } = require("../config");
11
9
  const { createCodexThreadProvider } = require("./codexThreadProvider");
@@ -17,6 +15,10 @@ const { redactToolCallPayload, redactSecrets } = require("../providerapi/redacto
17
15
  const { buildCachedMemoryPrefix } = require("../memory");
18
16
  const { shouldForwardStreamToPublisher } = require("./publisherRouting");
19
17
  const { appendAgentRegistryDiagnostic } = require("../ufoo/agentRegistryDiagnostics");
18
+ const {
19
+ buildDefaultStartupBootstrapPrompt,
20
+ isValueForCodexOption,
21
+ } = require("./defaultBootstrap");
20
22
 
21
23
  function sleep(ms) {
22
24
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -58,6 +60,98 @@ function safeSubscriber(subscriber) {
58
60
  return subscriber.replace(/:/g, "_");
59
61
  }
60
62
 
63
+ function readFileSafe(filePath = "") {
64
+ const target = String(filePath || "").trim();
65
+ if (!target) return "";
66
+ try {
67
+ return fs.readFileSync(target, "utf8");
68
+ } catch {
69
+ return "";
70
+ }
71
+ }
72
+
73
+ function hasUfooProtocolPrompt(promptText = "") {
74
+ const text = String(promptText || "");
75
+ return text.includes("ufoo protocol:") && text.includes("ufoo ctx decisions -l");
76
+ }
77
+
78
+ function hasPromptArg(args = []) {
79
+ if (!Array.isArray(args) || args.length === 0) return false;
80
+ const lastIndex = args.length - 1;
81
+ const lastItem = String(args[lastIndex] || "").trim();
82
+ if (!lastItem || lastItem.startsWith("-")) return false;
83
+ return !isValueForCodexOption(args, lastIndex);
84
+ }
85
+
86
+ function consumeClaudeAppendSystemPrompt(args = []) {
87
+ const nextArgs = [];
88
+ const promptSegments = [];
89
+ for (let index = 0; index < args.length; index += 1) {
90
+ const item = String(args[index] || "");
91
+ if (item === "--append-system-prompt") {
92
+ const filePath = String(args[index + 1] || "");
93
+ const content = readFileSafe(filePath);
94
+ if (content.trim()) promptSegments.push(content.trim());
95
+ index += 1;
96
+ continue;
97
+ }
98
+ if (item.startsWith("--append-system-prompt=")) {
99
+ const filePath = item.slice("--append-system-prompt=".length);
100
+ const content = readFileSafe(filePath);
101
+ if (content.trim()) promptSegments.push(content.trim());
102
+ continue;
103
+ }
104
+ nextArgs.push(item);
105
+ }
106
+ return {
107
+ args: nextArgs,
108
+ promptText: promptSegments.filter(Boolean).join("\n\n"),
109
+ };
110
+ }
111
+
112
+ function resolveInternalBootstrap({
113
+ projectRoot = process.cwd(),
114
+ agentType = "codex",
115
+ extraArgs = [],
116
+ env = process.env,
117
+ } = {}) {
118
+ const normalizedAgent = String(agentType || "").trim().toLowerCase();
119
+ const bootstrapAgentType = normalizedAgent === "claude" || normalizedAgent === "claude-code"
120
+ ? "claude-code"
121
+ : (normalizedAgent === "codex" ? "codex" : "");
122
+ const args = Array.isArray(extraArgs) ? extraArgs.slice() : [];
123
+ if (!bootstrapAgentType) {
124
+ return { promptText: "", extraArgs: args };
125
+ }
126
+
127
+ let promptText = "";
128
+ let nextArgs = args;
129
+
130
+ if (bootstrapAgentType === "claude-code") {
131
+ const consumed = consumeClaudeAppendSystemPrompt(args);
132
+ promptText = consumed.promptText;
133
+ nextArgs = consumed.args;
134
+ } else if (hasPromptArg(args)) {
135
+ promptText = String(args[args.length - 1] || "").trim();
136
+ nextArgs = args.slice(0, -1);
137
+ } else {
138
+ promptText = String(env.UFOO_STARTUP_BOOTSTRAP_TEXT || "").trim();
139
+ }
140
+
141
+ if (!hasUfooProtocolPrompt(promptText)) {
142
+ const defaultPrompt = buildDefaultStartupBootstrapPrompt({
143
+ agentType: bootstrapAgentType,
144
+ projectRoot,
145
+ }).trim();
146
+ promptText = [defaultPrompt, promptText.trim()].filter(Boolean).join("\n\n");
147
+ }
148
+
149
+ return {
150
+ promptText,
151
+ extraArgs: nextArgs,
152
+ };
153
+ }
154
+
61
155
  function buildMemoryPrefix(projectRoot, limit = 50) {
62
156
  try {
63
157
  return buildCachedMemoryPrefix(projectRoot, { limit }).prefix.trim();
@@ -90,10 +184,6 @@ function createBusSender(projectRoot, subscriber) {
90
184
  return { enqueue, flush };
91
185
  }
92
186
 
93
- function shouldFallbackToLegacyThreadProvider() {
94
- return false;
95
- }
96
-
97
187
  function drainQueue(queueFile) {
98
188
  if (!fs.existsSync(queueFile)) return [];
99
189
  const processingFile = `${queueFile}.processing.${process.pid}.${Date.now()}`;
@@ -202,32 +292,29 @@ async function handleEvent(
202
292
  subscriber,
203
293
  nickname,
204
294
  evt,
205
- cliSessionState,
206
295
  busSender,
207
296
  extraArgs = [],
208
- threadRuntime = null
297
+ threadRuntime = null,
298
+ bootstrapText = ""
209
299
  ) {
210
300
  if (!evt || !evt.data || !evt.data.message) return;
211
301
  const memoryPrefix = buildMemoryPrefix(projectRoot);
212
- const prompt = memoryPrefix
213
- ? `${memoryPrefix}\n\n${evt.data.message}`
214
- : evt.data.message;
302
+ const prompt = [bootstrapText, memoryPrefix, evt.data.message]
303
+ .map((item) => String(item || "").trim())
304
+ .filter(Boolean)
305
+ .join("\n\n");
215
306
  const publisher = evt.publisher || "unknown";
216
- const sandbox = "workspace-write";
217
- const streamState = { emitted: false, lastChar: "" };
218
307
  const streamToPublisher = shouldForwardStreamToPublisher(projectRoot, publisher);
219
308
 
220
309
  const emitStreamDelta = (delta) => {
221
310
  const text = String(delta || "");
222
311
  if (!text) return;
223
312
  if (!streamToPublisher) return;
224
- streamState.emitted = true;
225
- streamState.lastChar = text.slice(-1);
226
313
  busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: text }));
227
314
  };
228
315
 
229
316
  if (threadRuntime && threadRuntime.enabled && threadRuntime.thread) {
230
- const threadedResult = await handleThreadedEvent({
317
+ await handleThreadedEvent({
231
318
  agentType,
232
319
  provider,
233
320
  publisher,
@@ -237,74 +324,18 @@ async function handleEvent(
237
324
  streamToPublisher,
238
325
  threadRuntime,
239
326
  });
240
- if (!threadedResult || !threadedResult.fallbackToLegacy) {
241
- return;
242
- }
243
- }
244
-
245
- let res = await runCliAgent({
246
- provider,
247
- model,
248
- prompt,
249
- sessionId: cliSessionState.cliSessionId,
250
- sandbox,
251
- cwd: projectRoot,
252
- extraArgs,
253
- onStreamDelta: emitStreamDelta,
254
- });
255
-
256
- // Handle session errors with immediate retry (only for claude)
257
- if (!res.ok && provider === "claude-cli") {
258
- const errMsg = (res.error || "").toLowerCase();
259
- if (errMsg.includes("session") || errMsg.includes("already in use")) {
260
- // Clear session and retry immediately with new session
261
- cliSessionState.cliSessionId = null;
262
- cliSessionState.needsSave = true;
263
-
264
- res = await runCliAgent({
265
- provider,
266
- model,
267
- prompt,
268
- sessionId: null, // Let runCliAgent generate new session
269
- sandbox,
270
- cwd: projectRoot,
271
- extraArgs,
272
- onStreamDelta: emitStreamDelta,
273
- });
274
- }
275
- }
276
-
277
- // Update CLI session ID for continuity (only for claude)
278
- if (res.ok && res.sessionId && provider === "claude-cli") {
279
- cliSessionState.cliSessionId = res.sessionId;
280
- cliSessionState.needsSave = true;
327
+ return;
281
328
  }
282
329
 
283
- let reply = "";
284
- if (res.ok) {
285
- reply = normalizeCliOutput(res.output) || "";
330
+ const errorText = `[internal:${agentType}] error: no thread runtime available for provider ${provider}; cliRunner fallback has been removed`;
331
+ // eslint-disable-next-line no-console
332
+ console.error(errorText);
333
+ if (streamToPublisher) {
334
+ busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: errorText }));
335
+ busSender.enqueue(publisher, JSON.stringify({ stream: true, done: true, reason: "error" }));
286
336
  } else {
287
- reply = `[internal:${agentType}] error: ${res.error || "unknown error"}`;
337
+ busSender.enqueue(publisher, errorText);
288
338
  }
289
-
290
- if (streamState.emitted) {
291
- if (!res.ok) {
292
- if (streamState.lastChar !== "\n") {
293
- busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: "\n" }));
294
- }
295
- busSender.enqueue(publisher, JSON.stringify({ stream: true, delta: reply }));
296
- }
297
- busSender.enqueue(
298
- publisher,
299
- JSON.stringify({ stream: true, done: true, reason: res.ok ? "complete" : "error" })
300
- );
301
- await busSender.flush();
302
- return;
303
- }
304
-
305
- if (!reply) return;
306
-
307
- busSender.enqueue(publisher, reply);
308
339
  await busSender.flush();
309
340
  }
310
341
 
@@ -350,13 +381,12 @@ async function handleThreadedEvent({
350
381
  }
351
382
  await busSender.flush();
352
383
  } catch (err) {
353
- if (shouldFallbackToLegacyThreadProvider(err, provider)) {
354
- return { fallbackToLegacy: true };
355
- }
356
384
  if (threadRuntime && typeof threadRuntime.rebuildThread === "function") {
357
385
  await threadRuntime.rebuildThread();
358
386
  }
359
387
  const errorText = `[internal:${agentType}] error: ${err && err.message ? err.message : "unknown error"}`;
388
+ // eslint-disable-next-line no-console
389
+ console.error(errorText);
360
390
  if (streamToPublisher) {
361
391
  busSender.enqueue(
362
392
  publisher,
@@ -370,7 +400,6 @@ async function handleThreadedEvent({
370
400
  busSender.enqueue(publisher, errorText);
371
401
  }
372
402
  await busSender.flush();
373
- return { fallbackToLegacy: false };
374
403
  }
375
404
  }
376
405
 
@@ -473,8 +502,9 @@ function buildWorkerThreadToolRuntime({ projectRoot, subscriber, observer }) {
473
502
  function getClaudeThreadMode() {
474
503
  const envValue = process.env.UFOO_CLAUDE_INTERNAL_THREAD_MODE;
475
504
  const raw = String(envValue || "").trim().toLowerCase();
505
+ if (raw === "legacy" || raw === "off" || raw === "disabled" || raw === "0") return "legacy";
476
506
  if (raw === "api") return "api";
477
- return "legacy";
507
+ return "api";
478
508
  }
479
509
 
480
510
  function buildClaudeAuthProvider(projectRoot) {
@@ -658,35 +688,23 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
658
688
  }
659
689
  const provider = normalizedAgentType === "codex" ? "codex-cli" : "claude-cli";
660
690
  const model = process.env.UFOO_AGENT_MODEL || "";
691
+ const bootstrap = resolveInternalBootstrap({
692
+ projectRoot,
693
+ agentType: normalizedAgentType,
694
+ extraArgs,
695
+ env: process.env,
696
+ });
661
697
  const busSender = createBusSender(projectRoot, subscriber);
662
698
  const interactiveSessions = new Map();
663
699
  const threadRuntime = createThreadRuntime({
664
700
  projectRoot,
665
701
  provider,
666
702
  model,
667
- extraArgs,
703
+ extraArgs: bootstrap.extraArgs,
668
704
  subscriber,
669
705
  providerSessionId: process.env.UFOO_PROVIDER_SESSION_ID || "",
670
706
  });
671
707
 
672
- // Session state management for CLI continuity
673
- // Use stable path based on nickname (if exists) or agent type, NOT subscriber ID
674
- const stableKey = nickname || `${agentType}-default`;
675
- const sessionDir = path.join(getUfooPaths(projectRoot).agentDir, "sessions");
676
- fs.mkdirSync(sessionDir, { recursive: true });
677
- const stateFile = path.join(sessionDir, `${stableKey}.json`);
678
-
679
- let cliSessionId = null;
680
- // Only load session for claude (codex doesn't support sessions)
681
- if (provider === "claude-cli") {
682
- try {
683
- const state = JSON.parse(fs.readFileSync(stateFile, "utf8"));
684
- cliSessionId = state.cliSessionId;
685
- } catch {
686
- // No previous session
687
- }
688
- }
689
-
690
708
  let running = true;
691
709
  let processing = false;
692
710
  let lastHeartbeat = 0;
@@ -699,7 +717,6 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
699
717
  process.on("SIGTERM", stop);
700
718
  process.on("SIGINT", stop);
701
719
 
702
- const cliSessionState = { cliSessionId, needsSave: false };
703
720
  const agentsFile = getUfooPaths(projectRoot).agentsFile;
704
721
  const activityPublisher = createActivityStatePublisher({
705
722
  agentsFile, subscriber, projectRoot,
@@ -795,30 +812,15 @@ async function runInternalRunner({ projectRoot, agentType = "codex", extraArgs =
795
812
  subscriber,
796
813
  nickname,
797
814
  evt,
798
- cliSessionState,
799
815
  busSender,
800
- extraArgs,
801
- threadRuntime
816
+ bootstrap.extraArgs,
817
+ threadRuntime,
818
+ bootstrap.promptText
802
819
  );
803
820
  if (evt.__agentViewRaw) {
804
821
  getInteractiveSession(evt.publisher || "unknown").writeResponsePrompt();
805
822
  }
806
823
  }
807
-
808
- // Persist CLI session state after processing (only if changed and for claude)
809
- if (cliSessionState.needsSave && provider === "claude-cli") {
810
- try {
811
- fs.writeFileSync(stateFile, JSON.stringify({
812
- cliSessionId: cliSessionState.cliSessionId,
813
- nickname: nickname || "",
814
- updated_at: new Date().toISOString(),
815
- }));
816
- cliSessionState.needsSave = false;
817
- } catch {
818
- // ignore save errors
819
- }
820
- }
821
-
822
824
  // 处理消息后更新心跳
823
825
  updateHeartbeat();
824
826
  lastHeartbeat = now;
@@ -849,8 +851,8 @@ module.exports = {
849
851
  normalizeWorkerThreadToolMode,
850
852
  getClaudeThreadMode,
851
853
  buildClaudeAuthProvider,
852
- shouldFallbackToLegacyThreadProvider,
853
854
  parseAgentViewRawInput,
854
855
  createInteractiveInputSession,
856
+ resolveInternalBootstrap,
855
857
  persistProviderSessionId,
856
858
  };
@@ -12,6 +12,10 @@ const {
12
12
  shouldAutoReplyFromPtyToPublisher,
13
13
  shouldForwardStreamToPublisher,
14
14
  } = require("./publisherRouting");
15
+ const {
16
+ isValueForCodexOption,
17
+ resolveDefaultManualBootstrap,
18
+ } = require("./defaultBootstrap");
15
19
 
16
20
  function sleep(ms) {
17
21
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -116,22 +120,70 @@ function computeInjectedSubmitDelayMs(agentType, text) {
116
120
  return delayMs;
117
121
  }
118
122
 
119
- function resolveCommand(agentType, extraArgs = []) {
123
+ function hasPromptArg(args = []) {
124
+ if (!Array.isArray(args) || args.length === 0) return false;
125
+ const lastIndex = args.length - 1;
126
+ const lastItem = String(args[lastIndex] || "").trim();
127
+ if (!lastItem || lastItem.startsWith("-")) return false;
128
+ return !isValueForCodexOption(args, lastIndex);
129
+ }
130
+
131
+ function appendStartupBootstrapArg(agentType, extraArgs = [], env = process.env) {
132
+ const normalizedAgent = String(agentType || "").trim().toLowerCase();
133
+ const args = Array.isArray(extraArgs) ? extraArgs.slice() : [];
134
+ if (normalizedAgent !== "codex") return args;
135
+ if (hasPromptArg(args)) return args;
136
+ const startupBootstrapText = String(env.UFOO_STARTUP_BOOTSTRAP_TEXT || "").trim();
137
+ if (!startupBootstrapText) return args;
138
+ return [...args, startupBootstrapText];
139
+ }
140
+
141
+ function resolvePtyBootstrapArgs(agentType, extraArgs = [], {
142
+ projectRoot = process.cwd(),
143
+ env = process.env,
144
+ } = {}) {
145
+ const normalizedAgent = String(agentType || "").trim().toLowerCase();
146
+ const args = Array.isArray(extraArgs) ? extraArgs.slice() : [];
147
+ if (normalizedAgent !== "codex" && normalizedAgent !== "claude" && normalizedAgent !== "claude-code") {
148
+ return { args, env: {} };
149
+ }
150
+
151
+ const bootstrapAgentType = normalizedAgent === "codex" ? "codex" : "claude-code";
152
+ const resolved = resolveDefaultManualBootstrap({
153
+ projectRoot,
154
+ agentType: bootstrapAgentType,
155
+ args,
156
+ env,
157
+ });
158
+ const resolvedArgs = resolved && resolved.mode !== "skip" && Array.isArray(resolved.args)
159
+ ? resolved.args
160
+ : args;
161
+ const resolvedEnv = resolved && resolved.mode !== "skip" && resolved.env && typeof resolved.env === "object"
162
+ ? resolved.env
163
+ : {};
164
+ return {
165
+ args: appendStartupBootstrapArg(normalizedAgent, resolvedArgs, { ...env, ...resolvedEnv }),
166
+ env: resolvedEnv,
167
+ };
168
+ }
169
+
170
+ function resolveCommand(agentType, extraArgs = [], options = {}) {
120
171
  const normalizedAgent = String(agentType || "").trim().toLowerCase();
121
- const extra = Array.isArray(extraArgs) ? extraArgs : [];
172
+ const bootstrap = resolvePtyBootstrapArgs(normalizedAgent, extraArgs, options);
173
+ const extra = bootstrap.args;
122
174
  const rawCmd = String(process.env.UFOO_PTY_CMD || "").trim();
123
175
  if (rawCmd) {
124
176
  const rawArgs = String(process.env.UFOO_PTY_ARGS || "").trim();
125
177
  const args = rawArgs ? rawArgs.split(/\s+/).filter(Boolean) : [];
126
- return { command: rawCmd, args: [...args, ...extra] };
178
+ return { command: rawCmd, args: [...args, ...extra], env: bootstrap.env };
127
179
  }
128
180
  if (normalizedAgent === "claude" || normalizedAgent === "claude-code") {
129
- return { command: "claude", args: [...extra] };
181
+ return { command: "claude", args: [...extra], env: bootstrap.env };
130
182
  }
131
183
  if (normalizedAgent === "ufoo" || normalizedAgent === "ucode" || normalizedAgent === "ufoo-code") {
132
- return { command: "ucode", args: [...extra] };
184
+ return { command: "ucode", args: [...extra], env: bootstrap.env };
133
185
  }
134
- return { command: "codex", args: ["--no-alt-screen", "--sandbox", "workspace-write", ...extra] };
186
+ return { command: "codex", args: ["--no-alt-screen", "--sandbox", "workspace-write", ...extra], env: bootstrap.env };
135
187
  }
136
188
 
137
189
  async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }) {
@@ -160,9 +212,13 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
160
212
  const logFile = path.join(runDir, "pty-runner.log");
161
213
  const injectSockPath = path.join(queueDir, "inject.sock");
162
214
 
163
- const { command, args } = resolveCommand(agentType, extraArgs);
215
+ const { command, args, env: commandEnv } = resolveCommand(agentType, extraArgs, {
216
+ projectRoot,
217
+ env: process.env,
218
+ });
164
219
  const env = {
165
220
  ...process.env,
221
+ ...(commandEnv && typeof commandEnv === "object" ? commandEnv : {}),
166
222
  UFOO_LAUNCH_MODE: "internal-pty",
167
223
  UFOO_INTERNAL_PTY: "1",
168
224
  };
@@ -987,4 +1043,10 @@ async function runPtyRunner({ projectRoot, agentType = "codex", extraArgs = [] }
987
1043
  }
988
1044
  }
989
1045
 
990
- module.exports = { parseInputMessage, runPtyRunner };
1046
+ module.exports = {
1047
+ appendStartupBootstrapArg,
1048
+ parseInputMessage,
1049
+ resolvePtyBootstrapArgs,
1050
+ resolveCommand,
1051
+ runPtyRunner,
1052
+ };
@@ -1,6 +1,7 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { getUfooPaths } = require("../ufoo/paths");
4
+ const { buildDefaultStartupBootstrapPrompt } = require("./defaultBootstrap");
4
5
 
5
6
  function readFileSafe(filePath = "") {
6
7
  if (!filePath) return "";
@@ -74,11 +75,27 @@ function buildBootstrapContent({
74
75
  return `${lines.join("\n")}\n`;
75
76
  }
76
77
 
78
+ function hasUfooProtocolPrompt(promptText = "") {
79
+ const text = String(promptText || "");
80
+ return text.includes("ufoo protocol:") && text.includes("ufoo ctx decisions -l");
81
+ }
82
+
83
+ function mergeDefaultUfooProtocolPrompt(projectRoot = "", promptText = "") {
84
+ const currentPrompt = String(promptText || "").trim();
85
+ if (hasUfooProtocolPrompt(currentPrompt)) return currentPrompt;
86
+ const defaultPrompt = buildDefaultStartupBootstrapPrompt({
87
+ agentType: "ufoo-code",
88
+ projectRoot,
89
+ }).trim();
90
+ return [defaultPrompt, currentPrompt].filter(Boolean).join("\n\n");
91
+ }
92
+
77
93
  function prepareUcodeBootstrap({
78
94
  projectRoot = process.cwd(),
79
95
  promptFile = "",
80
96
  promptText = "",
81
97
  targetFile = "",
98
+ includeDefaultProtocol = true,
82
99
  } = {}) {
83
100
  const resolvedProjectRoot = path.resolve(projectRoot);
84
101
  const resolvedPrompt = String(promptFile || "").trim();
@@ -86,11 +103,14 @@ function prepareUcodeBootstrap({
86
103
 
87
104
  const inlinePromptText = String(promptText || "").trim();
88
105
  const resolvedPromptText = inlinePromptText || readFileSafe(resolvedPrompt);
106
+ const finalPromptText = includeDefaultProtocol
107
+ ? mergeDefaultUfooProtocolPrompt(resolvedProjectRoot, resolvedPromptText)
108
+ : resolvedPromptText;
89
109
  const rules = resolveProjectRules(resolvedProjectRoot);
90
110
  const content = buildBootstrapContent({
91
111
  projectRoot: resolvedProjectRoot,
92
112
  promptFile: resolvedPrompt,
93
- promptText: resolvedPromptText,
113
+ promptText: finalPromptText,
94
114
  rules,
95
115
  });
96
116
 
@@ -107,6 +127,8 @@ function prepareUcodeBootstrap({
107
127
  }
108
128
 
109
129
  module.exports = {
130
+ hasUfooProtocolPrompt,
131
+ mergeDefaultUfooProtocolPrompt,
110
132
  readFileSafe,
111
133
  resolveProjectRules,
112
134
  defaultBootstrapPath,