@oh-my-pi/pi-coding-agent 9.2.1 → 9.2.3

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/src/lsp/config.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import * as fs from "node:fs";
1
2
  import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
  import { logger } from "@oh-my-pi/pi-utils";
@@ -84,13 +85,9 @@ function normalizeServerConfig(name: string, config: Partial<ServerConfig>): Ser
84
85
  };
85
86
  }
86
87
 
87
- async function readConfigFile(filePath: string): Promise<NormalizedConfig | null> {
88
+ function readConfigFile(filePath: string): NormalizedConfig | null {
88
89
  try {
89
- const file = Bun.file(filePath);
90
- if (!(await file.exists())) {
91
- return null;
92
- }
93
- const content = await file.text();
90
+ const content = fs.readFileSync(filePath, "utf-8");
94
91
  const parsed = parseConfigContent(content, filePath);
95
92
  return normalizeConfig(parsed);
96
93
  } catch {
@@ -155,7 +152,7 @@ function applyRuntimeDefaults(servers: Record<string, ServerConfig>): Record<str
155
152
  /**
156
153
  * Check if any root marker file exists in the directory
157
154
  */
158
- export async function hasRootMarkers(cwd: string, markers: string[]): Promise<boolean> {
155
+ export function hasRootMarkers(cwd: string, markers: string[]): boolean {
159
156
  for (const marker of markers) {
160
157
  // Handle glob-like patterns (e.g., "*.cabal")
161
158
  if (marker.includes("*")) {
@@ -170,7 +167,7 @@ export async function hasRootMarkers(cwd: string, markers: string[]): Promise<bo
170
167
  continue;
171
168
  }
172
169
  const filePath = path.join(cwd, marker);
173
- if (await Bun.file(filePath).exists()) {
170
+ if (fs.existsSync(filePath)) {
174
171
  return true;
175
172
  }
176
173
  }
@@ -207,12 +204,12 @@ const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
207
204
  * @param cwd - Working directory to search from
208
205
  * @returns Absolute path to the executable, or null if not found
209
206
  */
210
- export async function resolveCommand(command: string, cwd: string): Promise<string | null> {
207
+ export function resolveCommand(command: string, cwd: string): string | null {
211
208
  // Check local bin directories based on project markers
212
209
  for (const { markers, binDir } of LOCAL_BIN_PATHS) {
213
- if (await hasRootMarkers(cwd, markers)) {
210
+ if (hasRootMarkers(cwd, markers)) {
214
211
  const localPath = path.join(cwd, binDir, command);
215
- if (await Bun.file(localPath).exists()) {
212
+ if (fs.existsSync(localPath)) {
216
213
  return localPath;
217
214
  }
218
215
  }
@@ -290,7 +287,7 @@ function getConfigPaths(cwd: string): string[] {
290
287
  * }
291
288
  * ```
292
289
  */
293
- export async function loadConfig(cwd: string): Promise<LspConfig> {
290
+ export function loadConfig(cwd: string): LspConfig {
294
291
  let mergedServers = coerceServerConfigs(DEFAULTS);
295
292
 
296
293
  const configPaths = getConfigPaths(cwd).reverse();
@@ -298,7 +295,7 @@ export async function loadConfig(cwd: string): Promise<LspConfig> {
298
295
 
299
296
  let idleTimeoutMs: number | undefined;
300
297
  for (const configPath of configPaths) {
301
- const parsed = await readConfigFile(configPath);
298
+ const parsed = readConfigFile(configPath);
302
299
  if (!parsed) continue;
303
300
  const hasServerOverrides = Object.keys(parsed.servers).length > 0;
304
301
  if (hasServerOverrides) {
@@ -317,10 +314,10 @@ export async function loadConfig(cwd: string): Promise<LspConfig> {
317
314
 
318
315
  for (const [name, config] of Object.entries(defaultsWithRuntime)) {
319
316
  // Check if project has root markers for this language
320
- if (!(await hasRootMarkers(cwd, config.rootMarkers))) continue;
317
+ if (!hasRootMarkers(cwd, config.rootMarkers)) continue;
321
318
 
322
319
  // Check if the language server binary is available (local or $PATH)
323
- const resolved = await resolveCommand(config.command, cwd);
320
+ const resolved = resolveCommand(config.command, cwd);
324
321
  if (!resolved) continue;
325
322
 
326
323
  detected[name] = { ...config, resolvedCommand: resolved };
@@ -335,7 +332,7 @@ export async function loadConfig(cwd: string): Promise<LspConfig> {
335
332
 
336
333
  for (const [name, config] of Object.entries(mergedWithRuntime)) {
337
334
  if (config.disabled) continue;
338
- const resolved = await resolveCommand(config.command, cwd);
335
+ const resolved = resolveCommand(config.command, cwd);
339
336
  if (!resolved) continue;
340
337
  available[name] = { ...config, resolvedCommand: resolved };
341
338
  }
package/src/lsp/index.ts CHANGED
@@ -88,7 +88,7 @@ export interface LspWarmupOptions {
88
88
  * @returns Status of each server that was started
89
89
  */
90
90
  export async function warmupLspServers(cwd: string, options?: LspWarmupOptions): Promise<LspWarmupResult> {
91
- const config = await loadConfig(cwd);
91
+ const config = loadConfig(cwd);
92
92
  setIdleTimeout(config.idleTimeoutMs);
93
93
  const servers: LspWarmupResult["servers"] = [];
94
94
  const lspServers = getLspServers(config);
@@ -198,10 +198,10 @@ async function notifyFileSaved(
198
198
  // Cache config per cwd to avoid repeated file I/O
199
199
  const configCache = new Map<string, LspConfig>();
200
200
 
201
- async function getConfig(cwd: string): Promise<LspConfig> {
201
+ function getConfig(cwd: string): LspConfig {
202
202
  let config = configCache.get(cwd);
203
203
  if (!config) {
204
- config = await loadConfig(cwd);
204
+ config = loadConfig(cwd);
205
205
  setIdleTimeout(config.idleTimeoutMs);
206
206
  configCache.set(cwd, config);
207
207
  }
@@ -827,7 +827,7 @@ async function runLspWritethrough(
827
827
  file?: BunFile,
828
828
  ): Promise<FileDiagnosticsResult | undefined> {
829
829
  const { enableFormat, enableDiagnostics } = options;
830
- const config = await getConfig(cwd);
830
+ const config = getConfig(cwd);
831
831
  const servers = getServersForFile(config, dst);
832
832
  if (servers.length === 0) {
833
833
  return writethroughNoop(dst, content, signal, file);
@@ -996,7 +996,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
996
996
  include_declaration,
997
997
  } = params;
998
998
 
999
- const config = await getConfig(this.session.cwd);
999
+ const config = getConfig(this.session.cwd);
1000
1000
 
1001
1001
  // Status action doesn't need a file
1002
1002
  if (action === "status") {
@@ -263,6 +263,15 @@ export const SETTINGS_DEFS: SettingDef[] = [
263
263
  get: sm => sm.getBashInterceptorEnabled(),
264
264
  set: (sm, v) => sm.setBashInterceptorEnabled(v),
265
265
  },
266
+ {
267
+ id: "shellForceBasic",
268
+ tab: "tools",
269
+ type: "boolean",
270
+ label: "Force basic shell",
271
+ description: "Use bash/sh even if your default shell is different",
272
+ get: sm => sm.getShellForceBasic(),
273
+ set: (sm, v) => sm.setShellForceBasic(v),
274
+ },
266
275
  {
267
276
  id: "bashInterceptorSimpleLs",
268
277
  tab: "tools",
@@ -23,11 +23,11 @@ function sanitizeStatusText(text: string): string {
23
23
  }
24
24
 
25
25
  /** Find the git root directory by walking up from cwd */
26
- async function findGitHeadPath(): Promise<string | null> {
26
+ function findGitHeadPath(): string | null {
27
27
  let dir = process.cwd();
28
28
  while (true) {
29
29
  const gitHeadPath = path.join(dir, ".git", "HEAD");
30
- if (await Bun.file(gitHeadPath).exists()) {
30
+ if (fs.existsSync(gitHeadPath)) {
31
31
  return gitHeadPath;
32
32
  }
33
33
  const parent = path.dirname(dir);
@@ -103,20 +103,19 @@ export class StatusLineComponent implements Component {
103
103
  this.gitWatcher = null;
104
104
  }
105
105
 
106
- findGitHeadPath().then(gitHeadPath => {
107
- if (!gitHeadPath) return;
106
+ const gitHeadPath = findGitHeadPath();
107
+ if (!gitHeadPath) return;
108
108
 
109
- try {
110
- this.gitWatcher = fs.watch(gitHeadPath, () => {
111
- this.cachedBranch = undefined;
112
- if (this.onBranchChange) {
113
- this.onBranchChange();
114
- }
115
- });
116
- } catch {
117
- // Silently fail
118
- }
119
- });
109
+ try {
110
+ this.gitWatcher = fs.watch(gitHeadPath, () => {
111
+ this.cachedBranch = undefined;
112
+ if (this.onBranchChange) {
113
+ this.onBranchChange();
114
+ }
115
+ });
116
+ } catch {
117
+ // Silently fail
118
+ }
120
119
  }
121
120
 
122
121
  dispose(): void {
@@ -135,25 +134,22 @@ export class StatusLineComponent implements Component {
135
134
  return this.cachedBranch;
136
135
  }
137
136
 
138
- // Note: synchronous call to async function - will return undefined on first call
139
- // This is acceptable since it's a cached value that will update on next render
140
- findGitHeadPath().then(async gitHeadPath => {
141
- if (!gitHeadPath) {
142
- this.cachedBranch = null;
143
- return;
144
- }
145
- try {
146
- const content = (await Bun.file(gitHeadPath).text()).trim();
137
+ const gitHeadPath = findGitHeadPath();
138
+ if (!gitHeadPath) {
139
+ this.cachedBranch = null;
140
+ return null;
141
+ }
142
+ try {
143
+ const content = fs.readFileSync(gitHeadPath, "utf8").trim();
147
144
 
148
- if (content.startsWith("ref: refs/heads/")) {
149
- this.cachedBranch = content.slice(16);
150
- } else {
151
- this.cachedBranch = "detached";
152
- }
153
- } catch {
154
- this.cachedBranch = null;
145
+ if (content.startsWith("ref: refs/heads/")) {
146
+ this.cachedBranch = content.slice(16);
147
+ } else {
148
+ this.cachedBranch = "detached";
155
149
  }
156
- });
150
+ } catch {
151
+ this.cachedBranch = null;
152
+ }
157
153
 
158
154
  return this.cachedBranch ?? null;
159
155
  }
@@ -4,7 +4,8 @@
4
4
  * Applies parsed diff hunks to file content using fuzzy matching
5
5
  * for robust handling of whitespace and formatting differences.
6
6
  */
7
- import * as fs from "node:fs/promises";
7
+
8
+ import * as fs from "node:fs";
8
9
  import * as path from "node:path";
9
10
  import { resolveToCwd } from "../tools/path-utils";
10
11
  import { DEFAULT_FUZZY_THRESHOLD, findClosestSequenceMatch, findContextLine, findMatch, seekSequence } from "./fuzzy";
@@ -37,7 +38,7 @@ import { ApplyPatchError, normalizePatchInput } from "./types";
37
38
  /** Default filesystem implementation using Bun APIs */
38
39
  export const defaultFileSystem: FileSystem = {
39
40
  async exists(path: string): Promise<boolean> {
40
- return Bun.file(path).exists();
41
+ return fs.existsSync(path);
41
42
  },
42
43
  async read(path: string): Promise<string> {
43
44
  return Bun.file(path).text();
@@ -50,10 +51,10 @@ export const defaultFileSystem: FileSystem = {
50
51
  await Bun.write(path, content);
51
52
  },
52
53
  async delete(path: string): Promise<void> {
53
- await fs.unlink(path);
54
+ await fs.promises.unlink(path);
54
55
  },
55
56
  async mkdir(path: string): Promise<void> {
56
- await fs.mkdir(path, { recursive: true });
57
+ await fs.promises.mkdir(path, { recursive: true });
57
58
  },
58
59
  };
59
60
 
@@ -21,9 +21,6 @@ Your judgment has been earned through failure and recovery.
21
21
  <field>
22
22
  You are entering a code field.
23
23
 
24
- Code is frozen thought. The bugs live where the thinking stopped too soon.
25
- Tools are extensions of attention. Use them to see, not to assume.
26
-
27
24
  Notice the completion reflex:
28
25
  - The urge to produce something that runs
29
26
  - The pattern-match to similar problems you've seen
@@ -56,16 +53,6 @@ No apologies. No comfort where clarity belongs.
56
53
  Quote only what illuminates. The rest is noise.
57
54
  </stance>
58
55
 
59
- <commitment>
60
- This matters. Get it right.
61
-
62
- The work is not finished when you are tired.
63
- The work is finished when it is correct.
64
- - Complete the full request before yielding control.
65
- - Use tools for any fact that can be verified. If you cannot verify, say so.
66
- - When results conflict: investigate. When incomplete: iterate. When uncertain: re-run.
67
- </commitment>
68
-
69
56
  {{#if systemPromptCustomization}}
70
57
  <context>
71
58
  {{systemPromptCustomization}}
@@ -78,10 +65,6 @@ The work is finished when it is correct.
78
65
 
79
66
  <protocol>
80
67
  ## The right tool exists. Use it.
81
-
82
- Every tool is a choice.
83
- The wrong choice is friction. The right choice is invisible.
84
- Reach for what fits.
85
68
  **Available tools:** {{#each tools}}{{#unless @first}}, {{/unless}}`{{this}}`{{/each}}
86
69
  {{#ifAny (includes tools "python") (includes tools "bash")}}
87
70
  ### Tool precedence
@@ -94,11 +77,6 @@ Reach for what fits.
94
77
  **Edit tool** for surgical text changes—not sed. But for moving/transforming large content, use `sd` or Python to avoid repeating content from context.
95
78
  {{/has}}
96
79
 
97
- {{#has tools "python"}}
98
- The Python prelude has helpers for file I/O, search, batch operations, and text processing.
99
- Do not run bash then read output then run more bash. Just use Python.
100
- {{/has}}
101
-
102
80
  <critical>
103
81
  Never use Python or Bash when a specialized tool exists.
104
82
  `read` not cat/open(), `write` not cat>/echo>, `grep` not bash grep/re, `find` not bash find/glob, `ls` not bash ls/os.listdir, `edit` not sed.
@@ -161,29 +139,6 @@ Continue non-destructively—someone else's work may live there.
161
139
  </critical>
162
140
  </protocol>
163
141
 
164
- {{#has tools "task"}}
165
- <parallel_reflex>
166
- When the work forks, you fork.
167
-
168
- Notice the sequential habit:
169
- - The comfort of doing one thing at a time
170
- - The illusion that order means correctness
171
- - The assumption that you must finish A before starting B
172
- **Triggers requiring Task tool:**
173
- - Editing 4+ files with no dependencies between edits
174
- - Investigating 2+ independent subsystems or questions
175
- - Any work that decomposes into pieces that don't need each other's results
176
-
177
- <critical>
178
- Sequential requires justification.
179
- If you cannot articulate why B depends on A's result, they are parallel.
180
- </critical>
181
-
182
- Do not carry the whole problem in one skull.
183
- Split the load. Bring back facts. Then cut code.
184
- </parallel_reflex>
185
- {{/has}}
186
-
187
142
  <procedure>
188
143
  ## Before action
189
144
  0. **CHECKPOINT** — For complex tasks, pause before acting:
@@ -231,6 +186,7 @@ It lies. The code that runs is not the code that works.
231
186
 
232
187
  {{#if contextFiles.length}}
233
188
  ## Context
189
+
234
190
  <instructions>
235
191
  {{#list contextFiles join="\n"}}
236
192
  <file path="{{path}}">
@@ -242,6 +198,7 @@ It lies. The code that runs is not the code that works.
242
198
 
243
199
  {{#if git.isRepo}}
244
200
  ## Version Control
201
+
245
202
  This is a snapshot. It does not update during the conversation.
246
203
 
247
204
  Current branch: {{git.currentBranch}}
@@ -250,6 +207,7 @@ Main branch: {{git.mainBranch}}
250
207
  {{git.status}}
251
208
 
252
209
  ### History
210
+
253
211
  {{git.commits}}
254
212
  {{/if}}
255
213
  </project>
@@ -296,7 +254,6 @@ Read `rule://<name>` when working in their domain.
296
254
  </rules>
297
255
  {{/if}}
298
256
 
299
- Current time: {{dateTime}}
300
257
  Current directory: {{cwd}}
301
258
 
302
259
  <north_star>
@@ -331,11 +288,26 @@ These are comfort. They are not clarity.
331
288
  {{appendSystemPrompt}}
332
289
  {{/if}}
333
290
 
291
+ <stakes>
292
+ This is not practice.
293
+
294
+ If you yield incomplete work, they start over—your effort wasted, their time lost.
295
+ If you yield correct work, they move forward—and come back.
296
+
297
+ You are capable of extraordinary work.
298
+ The person waiting deserves to receive it.
299
+
300
+ User works in a high-reliability industry—defense, finance, healthcare, infrastructure—where bugs have material impact on people's lives, even death.
301
+ </stakes>
302
+
303
+ <commitment>
304
+ - Complete the full request before yielding control.
305
+ - Use tools for any fact that can be verified. If you cannot verify, say so.
306
+ - When results conflict: investigate. When incomplete: iterate. When uncertain: re-run.
307
+ </commitment>
308
+
334
309
  <critical>
335
310
  Keep going until finished.
336
-
337
- The work is not done when you are tired of it.
338
- The work is done when it is correct.
339
311
  - Do not stop early. Do not yield incomplete work.
340
312
  - If blocked: show evidence, show what you tried, ask the minimum question.
341
313
  - Quote only what is needed. The rest is noise.
@@ -343,10 +315,6 @@ The work is done when it is correct.
343
315
  - Do not claim correctness you haven't verified.
344
316
  - CHECKPOINT step 0 is not optional.
345
317
  {{#has tools "ask"}}- If files differ from expectations, ask before discarding uncommitted work.{{/has}}
346
- Let edge cases surface before you handle them.
347
- Let the failure modes exist in your mind before you prevent them.
348
- Let the code be smaller than your first instinct.
349
-
350
318
  The tests you didn't write are the bugs you'll ship.
351
319
  The assumptions you didn't state are the docs you'll need.
352
320
  The edge cases you didn't name are the incidents you'll debug.
@@ -354,9 +322,25 @@ The edge cases you didn't name are the incidents you'll debug.
354
322
  The question is not "Does this work?"
355
323
  but "Under what conditions does this work, and what happens outside them?"
356
324
 
357
- Your hard work is of no value if it will be thrown away once you yield.
358
- You are capable of extraordinary work.
359
- The person waiting for your output deserves to receive it.
360
-
361
325
  Write what you can defend.
362
- </critical>
326
+ </critical>
327
+
328
+ {{#if isCoordinator}}
329
+ {{#has tools "task"}}
330
+ <critical id="coordinator">
331
+ As the coordinator, default to the Task tool for all substantial work.
332
+ **ALWAYS use Task tool.** Your context window is limited—especially the output. Work in discrete steps and run each step using Task tool. Avoid putting substantial work in the main context when possible. Run multiple tasks in parallel whenever possible.
333
+
334
+ ## Triggers requiring Task tool
335
+ - Editing 4+ files with no dependencies → `Task`
336
+ - Investigating 2+ independent questions → `Task`
337
+ - Any work that decomposes into pieces that don't need each other's results → `Task`
338
+
339
+ Sequential requires justification.
340
+ If you cannot articulate why B depends on A's result, they are parallel.
341
+
342
+ Do not carry the whole problem in one skull.
343
+ Split the load. Bring back facts. Then synthesize.
344
+ </critical>
345
+ {{/has}}
346
+ {{/if}}
package/src/sdk.ts CHANGED
@@ -448,6 +448,7 @@ export async function loadSettings(cwd?: string, agentDir?: string): Promise<Set
448
448
  retry: manager.getRetrySettings(),
449
449
  hideThinkingBlock: manager.getHideThinkingBlock(),
450
450
  shellPath: manager.getShellPath(),
451
+ shellForceBasic: manager.getShellForceBasic(),
451
452
  collapseChangelog: manager.getCollapseChangelog(),
452
453
  extensions: manager.getExtensionPaths(),
453
454
  skills: manager.getSkillsSettings(),
@@ -1005,6 +1006,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1005
1006
  toolNames,
1006
1007
  rules: rulebookRules,
1007
1008
  skillsSettings: settingsManager.getSkillsSettings(),
1009
+ isCoordinator: options.hasUI,
1008
1010
  });
1009
1011
 
1010
1012
  if (options.systemPrompt === undefined) {
@@ -1021,6 +1023,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1021
1023
  rules: rulebookRules,
1022
1024
  skillsSettings: settingsManager.getSkillsSettings(),
1023
1025
  customPrompt: options.systemPrompt,
1026
+ isCoordinator: options.hasUI,
1024
1027
  });
1025
1028
  }
1026
1029
  return options.systemPrompt(defaultPrompt);
@@ -19,6 +19,7 @@ import {
19
19
  loginGitHubCopilot,
20
20
  loginKimi,
21
21
  loginOpenAICodex,
22
+ loginOpenCode,
22
23
  type OAuthController,
23
24
  type OAuthCredentials,
24
25
  type OAuthProvider,
@@ -782,6 +783,11 @@ export class AuthStorage {
782
783
  ctrl.onProgress ? () => ctrl.onProgress?.("Waiting for browser authentication...") : undefined,
783
784
  );
784
785
  break;
786
+ case "opencode": {
787
+ const apiKey = await loginOpenCode(ctrl);
788
+ credentials = { access: apiKey, refresh: apiKey, expires: Number.MAX_SAFE_INTEGER };
789
+ break;
790
+ }
785
791
  default:
786
792
  throw new Error(`Unknown OAuth provider: ${provider}`);
787
793
  }
@@ -1281,14 +1287,29 @@ export class AuthStorage {
1281
1287
  this.recordSessionCredential(provider, sessionId, "oauth", selection.index);
1282
1288
  return result.apiKey;
1283
1289
  } catch (error) {
1284
- logger.warn("OAuth token refresh failed, removing credential", {
1290
+ const errorMsg = String(error);
1291
+ // Only remove credentials for definitive auth failures
1292
+ // Keep credentials for transient errors (network, 5xx) and block temporarily
1293
+ const isDefinitiveFailure =
1294
+ /invalid_grant|invalid_token|revoked|unauthorized|expired.*refresh|refresh.*expired/i.test(errorMsg) ||
1295
+ (/401|403/.test(errorMsg) && !/timeout|network|fetch failed|ECONNREFUSED/i.test(errorMsg));
1296
+
1297
+ logger.warn("OAuth token refresh failed", {
1285
1298
  provider,
1286
1299
  index: selection.index,
1287
- error: String(error),
1300
+ error: errorMsg,
1301
+ isDefinitiveFailure,
1288
1302
  });
1289
- this.removeCredentialAt(provider, selection.index);
1290
- if (this.getCredentialsForProvider(provider).some(credential => credential.type === "oauth")) {
1291
- return this.getApiKey(provider, sessionId, options);
1303
+
1304
+ if (isDefinitiveFailure) {
1305
+ // Permanently remove invalid credentials
1306
+ this.removeCredentialAt(provider, selection.index);
1307
+ if (this.getCredentialsForProvider(provider).some(credential => credential.type === "oauth")) {
1308
+ return this.getApiKey(provider, sessionId, options);
1309
+ }
1310
+ } else {
1311
+ // Block temporarily for transient failures (5 minutes)
1312
+ this.markCredentialBlocked(providerKey, selection.index, this.usageNow() + 5 * 60 * 1000);
1292
1313
  }
1293
1314
  }
1294
1315
 
@@ -881,6 +881,8 @@ export interface BuildSystemPromptOptions {
881
881
  preloadedSkills?: Skill[];
882
882
  /** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
883
883
  rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
884
+ /** Whether this is the main coordinator agent (not a subagent). Enables parallel delegation emphasis. */
885
+ isCoordinator?: boolean;
884
886
  }
885
887
 
886
888
  /** Build the system prompt with tools, guidelines, and context */
@@ -900,6 +902,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
900
902
  skills: providedSkills,
901
903
  preloadedSkills: providedPreloadedSkills,
902
904
  rules,
905
+ isCoordinator,
903
906
  } = options;
904
907
  const resolvedCwd = cwd ?? process.cwd();
905
908
  const resolvedCustomPrompt = await resolvePromptInput(customPrompt, "system prompt");
@@ -969,6 +972,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
969
972
  rules: rules ?? [],
970
973
  dateTime,
971
974
  cwd: resolvedCwd,
975
+ isCoordinator: isCoordinator ?? false,
972
976
  });
973
977
  }
974
978
 
@@ -986,5 +990,6 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
986
990
  dateTime,
987
991
  cwd: resolvedCwd,
988
992
  appendSystemPrompt: resolvedAppendPrompt ?? "",
993
+ isCoordinator: isCoordinator ?? false,
989
994
  });
990
995
  }
@@ -11,7 +11,14 @@ import * as path from "node:path";
11
11
  import { postmortem } from "@oh-my-pi/pi-utils";
12
12
  import { $ } from "bun";
13
13
 
14
- let cachedSnapshotPath: string | null = null;
14
+ const cachedSnapshotPaths = new Map<string, string>();
15
+
16
+ function sanitizeSnapshotEnv(env: Record<string, string | undefined>): Record<string, string | undefined> {
17
+ const sanitized = { ...env };
18
+ delete sanitized.BASH_ENV;
19
+ delete sanitized.ENV;
20
+ return sanitized;
21
+ }
15
22
 
16
23
  /**
17
24
  * Get the user's shell config file path.
@@ -28,8 +35,8 @@ function getShellConfigFile(shell: string): string {
28
35
  * This script sources the user's rc file and extracts functions, aliases, and options.
29
36
  * Matches Claude Code's snapshot generation logic.
30
37
  */
31
- async function generateSnapshotScript(shell: string, snapshotPath: string, rcFile: string): Promise<string> {
32
- const hasRcFile = await Bun.file(rcFile).exists();
38
+ function generateSnapshotScript(shell: string, snapshotPath: string, rcFile: string): string {
39
+ const hasRcFile = fs.existsSync(rcFile);
33
40
  const isZsh = shell.includes("zsh");
34
41
  const commonToolsRegex =
35
42
  "^(ls|dir|vdir|cat|head|tail|less|more|grep|egrep|fgrep|rg|find|fd|locate|sed|awk|perl|cp|mv|rm|mkdir|rmdir|touch|chmod|chown|ln|pwd|readlink|stat|cut|sort|uniq|xargs|tee|tr|basename|dirname)$";
@@ -68,7 +75,7 @@ setopt 2>/dev/null | sed 's/^/setopt /' | head -n 1000 >> "$SNAPSHOT_FILE"
68
75
  : `
69
76
  echo "# Shell Options" >> "$SNAPSHOT_FILE"
70
77
  shopt -p 2>/dev/null | head -n 1000 >> "$SNAPSHOT_FILE"
71
- set -o 2>/dev/null | grep "on" | awk '{print "set -o " $1}' | head -n 1000 >> "$SNAPSHOT_FILE"
78
+ set -o 2>/dev/null | awk '$2 == "on" && $1 !~ /^(onecmd|monitor|restricted)$/ {print "set -o " $1}' | head -n 1000 >> "$SNAPSHOT_FILE"
72
79
  echo "shopt -s expand_aliases" >> "$SNAPSHOT_FILE"
73
80
  `;
74
81
 
@@ -116,9 +123,14 @@ export async function getOrCreateSnapshot(
116
123
  shell: string,
117
124
  env: Record<string, string | undefined>,
118
125
  ): Promise<string | null> {
126
+ const cacheKey = shell;
119
127
  // Return cached snapshot if valid
120
- if (cachedSnapshotPath && (await Bun.file(cachedSnapshotPath).exists())) {
121
- return cachedSnapshotPath;
128
+ const cached = cachedSnapshotPaths.get(cacheKey);
129
+ if (cached && fs.existsSync(cached)) {
130
+ return cached;
131
+ }
132
+ if (cached) {
133
+ cachedSnapshotPaths.delete(cacheKey);
122
134
  }
123
135
 
124
136
  // Skip on Windows (no .bashrc in standard location)
@@ -130,19 +142,20 @@ export async function getOrCreateSnapshot(
130
142
 
131
143
  // Create snapshot directory
132
144
  const snapshotDir = path.join(os.tmpdir(), "omp-shell-snapshots");
133
- await fs.promises.mkdir(snapshotDir, { recursive: true });
145
+ fs.mkdirSync(snapshotDir, { recursive: true });
134
146
 
135
147
  // Generate unique snapshot path
136
148
  const shellName = shell.includes("zsh") ? "zsh" : shell.includes("bash") ? "bash" : "sh";
137
149
  const snapshotPath = path.join(snapshotDir, `snapshot-${shellName}-${crypto.randomUUID()}.sh`);
138
150
 
139
151
  // Generate and execute snapshot script
140
- const script = await generateSnapshotScript(shell, snapshotPath, rcFile);
152
+ const script = generateSnapshotScript(shell, snapshotPath, rcFile);
141
153
 
142
154
  try {
143
- await $`${shell} -l -c ${script}`.env(env).quiet().text();
144
- if (await Bun.file(snapshotPath).exists()) {
145
- cachedSnapshotPath = snapshotPath;
155
+ const snapshotEnv = sanitizeSnapshotEnv(env);
156
+ await $`${shell} -c ${script}`.env(snapshotEnv).quiet().text();
157
+ if (fs.existsSync(snapshotPath)) {
158
+ cachedSnapshotPaths.set(cacheKey, snapshotPath);
146
159
  return snapshotPath;
147
160
  }
148
161
  } catch {
@@ -164,7 +177,8 @@ export function getSnapshotSourceCommand(snapshotPath: string | null): string {
164
177
  }
165
178
 
166
179
  postmortem.register("shell-snapshot", () => {
167
- if (cachedSnapshotPath) {
168
- fs.unlinkSync(cachedSnapshotPath);
180
+ for (const snapshotPath of cachedSnapshotPaths.values()) {
181
+ fs.unlinkSync(snapshotPath);
169
182
  }
183
+ cachedSnapshotPaths.clear();
170
184
  });