@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/CHANGELOG.md +81 -0
- package/package.json +7 -7
- package/src/cli/update-cli.ts +2 -7
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +5 -1
- package/src/commit/changelog/detect.ts +2 -1
- package/src/commit/changelog/index.ts +2 -1
- package/src/config/prompt-templates.ts +2 -1
- package/src/config/settings-manager.ts +39 -3
- package/src/exec/bash-executor.ts +53 -3
- package/src/exec/shell-session.ts +593 -0
- package/src/ipy/gateway-coordinator.ts +5 -5
- package/src/ipy/kernel.ts +8 -7
- package/src/lsp/config.ts +13 -16
- package/src/lsp/index.ts +5 -5
- package/src/modes/components/settings-defs.ts +9 -0
- package/src/modes/components/status-line.ts +28 -32
- package/src/patch/applicator.ts +5 -4
- package/src/prompts/system/system-prompt.md +42 -58
- package/src/sdk.ts +3 -0
- package/src/session/auth-storage.ts +26 -5
- package/src/system-prompt.ts +5 -0
- package/src/utils/shell-snapshot.ts +27 -13
- package/src/utils/tools-manager.ts +9 -9
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
|
-
|
|
88
|
+
function readConfigFile(filePath: string): NormalizedConfig | null {
|
|
88
89
|
try {
|
|
89
|
-
const
|
|
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
|
|
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 (
|
|
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
|
|
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 (
|
|
210
|
+
if (hasRootMarkers(cwd, markers)) {
|
|
214
211
|
const localPath = path.join(cwd, binDir, command);
|
|
215
|
-
if (
|
|
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
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
201
|
+
function getConfig(cwd: string): LspConfig {
|
|
202
202
|
let config = configCache.get(cwd);
|
|
203
203
|
if (!config) {
|
|
204
|
-
config =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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()
|
|
107
|
-
|
|
106
|
+
const gitHeadPath = findGitHeadPath();
|
|
107
|
+
if (!gitHeadPath) return;
|
|
108
108
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
}
|
package/src/patch/applicator.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
1300
|
+
error: errorMsg,
|
|
1301
|
+
isDefinitiveFailure,
|
|
1288
1302
|
});
|
|
1289
|
-
|
|
1290
|
-
if (
|
|
1291
|
-
|
|
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
|
|
package/src/system-prompt.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
32
|
-
const hasRcFile =
|
|
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 |
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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 =
|
|
152
|
+
const script = generateSnapshotScript(shell, snapshotPath, rcFile);
|
|
141
153
|
|
|
142
154
|
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
168
|
-
fs.unlinkSync(
|
|
180
|
+
for (const snapshotPath of cachedSnapshotPaths.values()) {
|
|
181
|
+
fs.unlinkSync(snapshotPath);
|
|
169
182
|
}
|
|
183
|
+
cachedSnapshotPaths.clear();
|
|
170
184
|
});
|