@oh-my-pi/pi-coding-agent 13.0.0 → 13.0.1
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 +7 -0
- package/package.json +7 -7
- package/src/patch/index.ts +101 -151
- package/src/patch/shared.ts +16 -24
- package/src/prompts/compaction/compaction-short-summary.md +1 -1
- package/src/prompts/system/agent-creation-architect.md +4 -4
- package/src/prompts/tools/hashline.md +20 -19
- package/src/prompts/tools/patch.md +1 -1
- package/src/prompts/tools/python.md +2 -2
- package/src/prompts/tools/replace.md +1 -1
- package/src/prompts/tools/task.md +15 -16
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.0.1] - 2026-02-22
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
- Simplified hashline edit schema to use unified `first`/`last` anchor fields instead of operation-specific field names (`tag`, `before`, `after`)
|
|
9
|
+
- Improved resilience of anchor resolution to degrade gracefully when anchors are missing or invalid, allowing edits to proceed with available anchors
|
|
10
|
+
- Updated hashline tool documentation to reflect new unified anchor syntax across all operations (replace, append, prepend, insert)
|
|
11
|
+
|
|
5
12
|
## [13.0.0] - 2026-02-22
|
|
6
13
|
### Added
|
|
7
14
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.0.
|
|
4
|
+
"version": "13.0.1",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.0.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.0.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.0.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.0.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.0.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.0.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.0.1",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.0.1",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.0.1",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.0.1",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.0.1",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.0.1",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
package/src/patch/index.ts
CHANGED
|
@@ -168,24 +168,6 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
|
168
168
|
});
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
-
const hashlineReplaceContentFormat = (kind: string) =>
|
|
172
|
-
Type.Union([
|
|
173
|
-
Type.Null(),
|
|
174
|
-
Type.Array(Type.String(), { description: `${kind} lines` }),
|
|
175
|
-
Type.String({ description: `${kind} line` }),
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
|
-
const hashlineInsertContentFormat = (kind: string) =>
|
|
179
|
-
Type.Union([
|
|
180
|
-
Type.Array(Type.String(), { description: `${kind} lines`, minItems: 1 }),
|
|
181
|
-
Type.String({ description: `${kind} line`, minLength: 1 }),
|
|
182
|
-
]);
|
|
183
|
-
|
|
184
|
-
const hashlineTagFormat = (what: string) =>
|
|
185
|
-
Type.String({
|
|
186
|
-
description: `Tag identifying the ${what} in "LINE#ID" format`,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
171
|
export function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
190
172
|
if (edit === null) return [];
|
|
191
173
|
if (Array.isArray(edit)) return edit;
|
|
@@ -194,61 +176,23 @@ export function hashlineParseContent(edit: string | string[] | null): string[] {
|
|
|
194
176
|
if (lines[lines.length - 1].trim() === "") return lines.slice(0, -1);
|
|
195
177
|
return lines;
|
|
196
178
|
}
|
|
197
|
-
const hashlineReplaceTagEditSchema = Type.Object(
|
|
198
|
-
{
|
|
199
|
-
op: Type.Literal("replace"),
|
|
200
|
-
tag: hashlineTagFormat("line being replaced"),
|
|
201
|
-
content: hashlineReplaceContentFormat("Replacement"),
|
|
202
|
-
},
|
|
203
|
-
{ additionalProperties: false },
|
|
204
|
-
);
|
|
205
179
|
|
|
206
|
-
const
|
|
180
|
+
const hashlineEditSpecSchema = Type.Object(
|
|
207
181
|
{
|
|
208
|
-
op:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
before: Type.Optional(hashlineTagFormat("line before which to prepend")),
|
|
219
|
-
content: hashlineInsertContentFormat("Prepended"),
|
|
220
|
-
},
|
|
221
|
-
{ additionalProperties: false },
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
const hashlineReplaceRangeEditSchema = Type.Object(
|
|
225
|
-
{
|
|
226
|
-
op: Type.Literal("replace"),
|
|
227
|
-
first: hashlineTagFormat("first line"),
|
|
228
|
-
last: hashlineTagFormat("last line"),
|
|
229
|
-
content: hashlineReplaceContentFormat("Replacement"),
|
|
230
|
-
},
|
|
231
|
-
{ additionalProperties: false },
|
|
232
|
-
);
|
|
233
|
-
|
|
234
|
-
const hashlineInsertEditSchema = Type.Object(
|
|
235
|
-
{
|
|
236
|
-
op: Type.Literal("insert"),
|
|
237
|
-
before: Type.Optional(hashlineTagFormat("line before which to insert")),
|
|
238
|
-
after: Type.Optional(hashlineTagFormat("line after which to insert")),
|
|
239
|
-
content: hashlineInsertContentFormat("Inserted"),
|
|
182
|
+
op: StringEnum(["replace", "append", "prepend", "insert"], {
|
|
183
|
+
description: "Operation type",
|
|
184
|
+
}),
|
|
185
|
+
first: Type.Optional(Type.String({ description: 'First/start anchor tag in "LINE#ID" format' })),
|
|
186
|
+
last: Type.Optional(Type.String({ description: 'Last/end anchor tag in "LINE#ID" format' })),
|
|
187
|
+
content: Type.Union([
|
|
188
|
+
Type.Null(),
|
|
189
|
+
Type.Array(Type.String(), { description: "Content lines" }),
|
|
190
|
+
Type.String({ description: "Content line" }),
|
|
191
|
+
]),
|
|
240
192
|
},
|
|
241
193
|
{ additionalProperties: false },
|
|
242
194
|
);
|
|
243
195
|
|
|
244
|
-
const hashlineEditSpecSchema = Type.Union([
|
|
245
|
-
hashlineReplaceTagEditSchema,
|
|
246
|
-
hashlineReplaceRangeEditSchema,
|
|
247
|
-
hashlineAppendEditSchema,
|
|
248
|
-
hashlinePrependEditSchema,
|
|
249
|
-
hashlineInsertEditSchema,
|
|
250
|
-
]);
|
|
251
|
-
|
|
252
196
|
const hashlineEditSchema = Type.Object(
|
|
253
197
|
{
|
|
254
198
|
path: Type.String({ description: "File path (relative or absolute)" }),
|
|
@@ -265,6 +209,88 @@ const hashlineEditSchema = Type.Object(
|
|
|
265
209
|
export type HashlineToolEdit = Static<typeof hashlineEditSpecSchema>;
|
|
266
210
|
export type HashlineParams = Static<typeof hashlineEditSchema>;
|
|
267
211
|
|
|
212
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
213
|
+
// Resilient anchor resolution
|
|
214
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Map flat tool-schema edits (first/last) into typed HashlineEdit objects.
|
|
218
|
+
*
|
|
219
|
+
* Resilient: as long as at least one anchor exists, we execute.
|
|
220
|
+
* - replace + first only → single-line replace (tag = first)
|
|
221
|
+
* - replace + first + last → range replace
|
|
222
|
+
* - append + first or last → append after that anchor
|
|
223
|
+
* - prepend + first or last → prepend before that anchor
|
|
224
|
+
* - insert + first + last → insert between them
|
|
225
|
+
* - insert + one anchor → degrade to append/prepend
|
|
226
|
+
* - no anchors → file-level append/prepend (only for those ops)
|
|
227
|
+
*
|
|
228
|
+
* Unknown ops default to "replace".
|
|
229
|
+
*/
|
|
230
|
+
function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
|
|
231
|
+
const result: HashlineEdit[] = [];
|
|
232
|
+
for (const edit of edits) {
|
|
233
|
+
const content = hashlineParseContent(edit.content);
|
|
234
|
+
const first = edit.first ? tryParseTag(edit.first) : undefined;
|
|
235
|
+
const last = edit.last ? tryParseTag(edit.last) : undefined;
|
|
236
|
+
|
|
237
|
+
// Normalize op — default unknown values to "replace"
|
|
238
|
+
const op = edit.op === "append" || edit.op === "prepend" || edit.op === "insert" ? edit.op : "replace";
|
|
239
|
+
|
|
240
|
+
switch (op) {
|
|
241
|
+
case "replace": {
|
|
242
|
+
if (first && last) {
|
|
243
|
+
result.push({ op: "replace", first, last, content });
|
|
244
|
+
} else if (first) {
|
|
245
|
+
result.push({ op: "replace", tag: first, content });
|
|
246
|
+
} else if (last) {
|
|
247
|
+
result.push({ op: "replace", tag: last, content });
|
|
248
|
+
} else {
|
|
249
|
+
throw new Error("Replace requires at least one anchor (first or last).");
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case "append": {
|
|
254
|
+
// Prefer first as the "after" anchor; fall back to last
|
|
255
|
+
const anchor = first ?? last;
|
|
256
|
+
result.push({ op: "append", ...(anchor ? { after: anchor } : {}), content });
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
case "prepend": {
|
|
260
|
+
// Prefer last as the "before" anchor; fall back to first
|
|
261
|
+
const anchor = last ?? first;
|
|
262
|
+
result.push({ op: "prepend", ...(anchor ? { before: anchor } : {}), content });
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
case "insert": {
|
|
266
|
+
if (first && last) {
|
|
267
|
+
result.push({ op: "insert", after: first, before: last, content });
|
|
268
|
+
} else if (first) {
|
|
269
|
+
// Degrade: insert after first
|
|
270
|
+
result.push({ op: "append", after: first, content });
|
|
271
|
+
} else if (last) {
|
|
272
|
+
// Degrade: insert before last
|
|
273
|
+
result.push({ op: "prepend", before: last, content });
|
|
274
|
+
} else {
|
|
275
|
+
// No anchors — append to end
|
|
276
|
+
result.push({ op: "append", content });
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Parse a tag, returning undefined instead of throwing on garbage. */
|
|
286
|
+
function tryParseTag(raw: string): LineTag | undefined {
|
|
287
|
+
try {
|
|
288
|
+
return parseTag(raw);
|
|
289
|
+
} catch {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
268
294
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
269
295
|
// LSP FileSystem for patch mode
|
|
270
296
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -522,24 +548,15 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
522
548
|
if (!(await file.exists())) {
|
|
523
549
|
const content: string[] = [];
|
|
524
550
|
for (const edit of edits) {
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
throw new Error(`File not found: ${path}`);
|
|
529
|
-
}
|
|
530
|
-
content.push(...hashlineParseContent(edit.content));
|
|
531
|
-
break;
|
|
532
|
-
}
|
|
533
|
-
case "prepend": {
|
|
534
|
-
if (edit.before) {
|
|
535
|
-
throw new Error(`File not found: ${path}`);
|
|
536
|
-
}
|
|
551
|
+
// For file creation, only anchorless appends/prepends are valid
|
|
552
|
+
if ((edit.op === "append" || edit.op === "prepend") && !edit.first && !edit.last) {
|
|
553
|
+
if (edit.op === "prepend") {
|
|
537
554
|
content.unshift(...hashlineParseContent(edit.content));
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
default: {
|
|
541
|
-
throw new Error(`File not found: ${path}`);
|
|
555
|
+
} else {
|
|
556
|
+
content.push(...hashlineParseContent(edit.content));
|
|
542
557
|
}
|
|
558
|
+
} else {
|
|
559
|
+
throw new Error(`File not found: ${path}`);
|
|
543
560
|
}
|
|
544
561
|
}
|
|
545
562
|
await file.write(content.join("\n"));
|
|
@@ -553,74 +570,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
553
570
|
};
|
|
554
571
|
}
|
|
555
572
|
|
|
556
|
-
const anchorEdits
|
|
557
|
-
for (const edit of edits) {
|
|
558
|
-
switch (edit.op) {
|
|
559
|
-
case "replace": {
|
|
560
|
-
if ("tag" in edit) {
|
|
561
|
-
anchorEdits.push({
|
|
562
|
-
op: "replace",
|
|
563
|
-
tag: parseTag(edit.tag),
|
|
564
|
-
content: hashlineParseContent(edit.content),
|
|
565
|
-
});
|
|
566
|
-
} else {
|
|
567
|
-
anchorEdits.push({
|
|
568
|
-
op: "replace",
|
|
569
|
-
first: parseTag(edit.first),
|
|
570
|
-
last: parseTag(edit.last),
|
|
571
|
-
content: hashlineParseContent(edit.content),
|
|
572
|
-
});
|
|
573
|
-
}
|
|
574
|
-
break;
|
|
575
|
-
}
|
|
576
|
-
case "append": {
|
|
577
|
-
const { after, content } = edit;
|
|
578
|
-
anchorEdits.push({
|
|
579
|
-
op: "append",
|
|
580
|
-
...(after ? { after: parseTag(after) } : {}),
|
|
581
|
-
content: hashlineParseContent(content),
|
|
582
|
-
});
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
585
|
-
case "prepend": {
|
|
586
|
-
const { before, content } = edit;
|
|
587
|
-
anchorEdits.push({
|
|
588
|
-
op: "prepend",
|
|
589
|
-
...(before ? { before: parseTag(before) } : {}),
|
|
590
|
-
content: hashlineParseContent(content),
|
|
591
|
-
});
|
|
592
|
-
break;
|
|
593
|
-
}
|
|
594
|
-
case "insert": {
|
|
595
|
-
const { before, after, content } = edit;
|
|
596
|
-
if (before && !after) {
|
|
597
|
-
anchorEdits.push({
|
|
598
|
-
op: "prepend",
|
|
599
|
-
before: parseTag(before),
|
|
600
|
-
content: hashlineParseContent(content),
|
|
601
|
-
});
|
|
602
|
-
} else if (after && !before) {
|
|
603
|
-
anchorEdits.push({
|
|
604
|
-
op: "append",
|
|
605
|
-
after: parseTag(after),
|
|
606
|
-
content: hashlineParseContent(content),
|
|
607
|
-
});
|
|
608
|
-
} else if (before && after) {
|
|
609
|
-
anchorEdits.push({
|
|
610
|
-
op: "insert",
|
|
611
|
-
before: parseTag(before),
|
|
612
|
-
after: parseTag(after),
|
|
613
|
-
content: hashlineParseContent(content),
|
|
614
|
-
});
|
|
615
|
-
} else {
|
|
616
|
-
throw new Error(`Insert must have both before and after tags.`);
|
|
617
|
-
}
|
|
618
|
-
break;
|
|
619
|
-
}
|
|
620
|
-
default:
|
|
621
|
-
throw new Error(`Invalid edit operation: ${JSON.stringify(edit)}`);
|
|
622
|
-
}
|
|
623
|
-
}
|
|
573
|
+
const anchorEdits = resolveEditAnchors(edits);
|
|
624
574
|
|
|
625
575
|
const rawContent = await file.text();
|
|
626
576
|
const { bom, text: content } = stripBom(rawContent);
|
package/src/patch/shared.ts
CHANGED
|
@@ -86,12 +86,12 @@ interface EditRenderArgs {
|
|
|
86
86
|
edits?: HashlineEditPreview[];
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
type HashlineEditPreview =
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
89
|
+
type HashlineEditPreview = {
|
|
90
|
+
op: string;
|
|
91
|
+
first?: string;
|
|
92
|
+
last?: string;
|
|
93
|
+
content: string | string[] | null;
|
|
94
|
+
};
|
|
95
95
|
|
|
96
96
|
/** Extended context for edit tool rendering */
|
|
97
97
|
export interface EditRenderContext {
|
|
@@ -166,26 +166,18 @@ function formatStreamingHashlineEdits(edits: unknown[], uiTheme: Theme): string
|
|
|
166
166
|
|
|
167
167
|
const contentLines = Array.isArray(editRecord.content) ? (editRecord.content as string[]).join("\n") : "";
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if ("first" in editRecord || "last" in editRecord) {
|
|
176
|
-
const first = typeof editRecord.first === "string" ? editRecord.first : "…";
|
|
177
|
-
const last = typeof editRecord.last === "string" ? editRecord.last : "…";
|
|
178
|
-
return { srcLabel: `• range ${first}..${last}`, dst: contentLines };
|
|
169
|
+
const op = typeof editRecord.op === "string" ? editRecord.op : "?";
|
|
170
|
+
const first = typeof editRecord.first === "string" ? editRecord.first : undefined;
|
|
171
|
+
const last = typeof editRecord.last === "string" ? editRecord.last : undefined;
|
|
172
|
+
|
|
173
|
+
if (first && last && first !== last) {
|
|
174
|
+
return { srcLabel: `\u2022 ${op} ${first}..${last}`, dst: contentLines };
|
|
179
175
|
}
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
const before = typeof editRecord.before === "string" ? editRecord.before : undefined;
|
|
184
|
-
const refs = [after, before].filter(Boolean).join("..") || "…";
|
|
185
|
-
return { srcLabel: `• insert ${refs}`, dst: contentLines };
|
|
176
|
+
const anchor = first ?? last;
|
|
177
|
+
if (anchor) {
|
|
178
|
+
return { srcLabel: `\u2022 ${op} ${anchor}`, dst: contentLines };
|
|
186
179
|
}
|
|
187
|
-
|
|
188
|
-
return { srcLabel: "• (incomplete edit)", dst: "" };
|
|
180
|
+
return { srcLabel: `\u2022 ${op} (file-level)`, dst: contentLines };
|
|
189
181
|
}
|
|
190
182
|
}
|
|
191
183
|
function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
|
|
@@ -5,5 +5,5 @@ Rules:
|
|
|
5
5
|
- MUST describe the changes made, not the process
|
|
6
6
|
- MUST NOT mention running tests, builds, or other validation steps
|
|
7
7
|
- MUST NOT explain what the user asked for
|
|
8
|
-
- MUST write in first person (I added
|
|
8
|
+
- MUST write in first person (I added…, I fixed…)
|
|
9
9
|
- MUST NOT ask questions
|
|
@@ -33,17 +33,17 @@ When a user describes what they want an agent to do, you will:
|
|
|
33
33
|
<function call omitted for brevity only for this example>
|
|
34
34
|
<commentary>
|
|
35
35
|
Since a significant piece of code was written, use the {{TASK_TOOL_NAME}} tool to launch the test-runner agent to run the tests.
|
|
36
|
-
</commentary>
|
|
36
|
+
</commentary>
|
|
37
37
|
assistant: "Now let me use the test-runner agent to run the tests"
|
|
38
|
-
</example>
|
|
38
|
+
</example>
|
|
39
39
|
- <example>
|
|
40
40
|
Context: User is creating an agent to respond to the word "hello" with a friendly jok.
|
|
41
41
|
user: "Hello"
|
|
42
42
|
assistant: "I'm going to use the {{TASK_TOOL_NAME}} tool to launch the greeting-responder agent to respond with a friendly joke"
|
|
43
43
|
<commentary>
|
|
44
44
|
Since the user is greeting, use the greeting-responder agent to respond with a friendly joke.
|
|
45
|
-
</commentary>
|
|
46
|
-
</example>
|
|
45
|
+
</commentary>
|
|
46
|
+
</example>
|
|
47
47
|
- If the user mentioned or implied that the agent should be used proactively, you SHOULD include examples of this.
|
|
48
48
|
- NOTE: You MUST ensure that in the examples, you are making the assistant use the Agent tool and MUST NOT simply respond directly to the task.
|
|
49
49
|
|
|
@@ -12,14 +12,15 @@ Apply precise file edits using `LINE#ID` tags, anchoring to the file content.
|
|
|
12
12
|
</workflow>
|
|
13
13
|
|
|
14
14
|
<operations>
|
|
15
|
+
Every edit has `op`, `first` (start anchor), `last` (end anchor), and `content`. All anchors use `"LINE#ID"` format.
|
|
15
16
|
- **Line or range replace/delete**
|
|
16
|
-
- `{ op: "replace",
|
|
17
|
-
- `{ op: "replace", first: "N#ID", last: "N#ID", content: […] }`
|
|
17
|
+
- `{ op: "replace", first: "N#ID", content: […] }` (single line)
|
|
18
|
+
- `{ op: "replace", first: "N#ID", last: "N#ID", content: […] }` (range)
|
|
18
19
|
- Use for swaps, block rewrites, or deleting a full span (`content: null`).
|
|
19
20
|
- **Insert** (new content)
|
|
20
|
-
- `{ op: "prepend",
|
|
21
|
-
- `{ op: "append",
|
|
22
|
-
- `{ op: "insert",
|
|
21
|
+
- `{ op: "prepend", last: "N#ID", content: […] }` or `{ op: "prepend", content: […] }` (no anchor = insert at beginning of file)
|
|
22
|
+
- `{ op: "append", first: "N#ID", content: […] }` or `{ op: "append", content: […] }` (no anchor = insert at end of file)
|
|
23
|
+
- `{ op: "insert", first: "N#ID", last: "N#ID", content: […] }` (between adjacent anchors; safest for blocks)
|
|
23
24
|
- **File-level controls**
|
|
24
25
|
- `{ delete: true, edits: [] }` deletes the file (cannot be combined with `rename`).
|
|
25
26
|
- `{ rename: "new/path.ts", edits: […] }` writes result to new path and removes old path.
|
|
@@ -62,7 +63,7 @@ Apply precise file edits using `LINE#ID` tags, anchoring to the file content.
|
|
|
62
63
|
```
|
|
63
64
|
```
|
|
64
65
|
op: "replace"
|
|
65
|
-
|
|
66
|
+
first: "{{hlineref 23 " const timeout: number = 5000;"}}"
|
|
66
67
|
content: [" const timeout: number = 30_000;"]
|
|
67
68
|
```
|
|
68
69
|
</example>
|
|
@@ -74,7 +75,7 @@ content: [" const timeout: number = 30_000;"]
|
|
|
74
75
|
```
|
|
75
76
|
```
|
|
76
77
|
op: "replace"
|
|
77
|
-
|
|
78
|
+
first: "{{hlineref 7 "// @ts-ignore"}}"
|
|
78
79
|
content: null
|
|
79
80
|
```
|
|
80
81
|
</example>
|
|
@@ -85,7 +86,7 @@ content: null
|
|
|
85
86
|
```
|
|
86
87
|
```
|
|
87
88
|
op: "replace"
|
|
88
|
-
|
|
89
|
+
first: "{{hlineref 14 " placeholder: \"DO NOT SHIP\","}}"
|
|
89
90
|
content: [""]
|
|
90
91
|
```
|
|
91
92
|
</example>
|
|
@@ -127,10 +128,10 @@ content: null
|
|
|
127
128
|
```
|
|
128
129
|
```
|
|
129
130
|
op: "prepend"
|
|
130
|
-
|
|
131
|
+
last: "{{hlineref 1 "import * as fs from \"node:fs/promises\";"}}"
|
|
131
132
|
content: ["import * as os from \"node:os\";"]
|
|
132
133
|
```
|
|
133
|
-
Use `
|
|
134
|
+
Use `last` for anchored insertion before a specific line. Omit anchor to prepend at BOF.
|
|
134
135
|
</example>
|
|
135
136
|
|
|
136
137
|
<example name="append at end of file">
|
|
@@ -139,10 +140,10 @@ Use `before` for anchored insertion before a specific line. Omit `before` to pre
|
|
|
139
140
|
```
|
|
140
141
|
```
|
|
141
142
|
op: "append"
|
|
142
|
-
|
|
143
|
+
first: "{{hlineref 260 "export { serialize, deserialize };"}}"
|
|
143
144
|
content: ["export { validate };"]
|
|
144
145
|
```
|
|
145
|
-
Use `
|
|
146
|
+
Use `first` for anchored insertion after a specific line. Omit anchor to append at EOF.
|
|
146
147
|
</example>
|
|
147
148
|
|
|
148
149
|
<example name="add an entry between known siblings">
|
|
@@ -152,8 +153,8 @@ Use `after` for anchored insertion after a specific line. Omit `after` to append
|
|
|
152
153
|
```
|
|
153
154
|
```
|
|
154
155
|
op: "insert"
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
first: "{{hlineref 44 " \"build\": \"bun run compile\","}}"
|
|
157
|
+
last: "{{hlineref 45 " \"test\": \"bun test\""}}"
|
|
157
158
|
content: [" \"lint\": \"biome check\","]
|
|
158
159
|
```
|
|
159
160
|
Dual anchors pin the insert to exactly one gap, preventing drift from edits elsewhere in the file. **Always prefer dual anchors when both boundaries are content lines.**
|
|
@@ -168,10 +169,10 @@ Dual anchors pin the insert to exactly one gap, preventing drift from edits else
|
|
|
168
169
|
```
|
|
169
170
|
```
|
|
170
171
|
op: "insert"
|
|
171
|
-
|
|
172
|
+
last: "{{hlineref 103 "export function serialize(data: unknown): string {"}}"
|
|
172
173
|
content: ["function validate(data: unknown): boolean {", " return data != null && typeof data === \"object\";", "}", ""]
|
|
173
174
|
```
|
|
174
|
-
The trailing `""` in `content` preserves the blank-line separator. **Anchor to the structural line (`export function
|
|
175
|
+
The trailing `""` in `content` preserves the blank-line separator. **Anchor to the structural line (`export function …`), not the blank line above it** — blank lines are ambiguous and may be added or removed by other edits.
|
|
175
176
|
</example>
|
|
176
177
|
|
|
177
178
|
<example name="file delete">
|
|
@@ -192,14 +193,14 @@ edits: […]
|
|
|
192
193
|
<example name="anti-pattern: anchoring to whitespace">
|
|
193
194
|
Bad — tags to a blank line; fragile if blank lines shift:
|
|
194
195
|
```
|
|
195
|
-
|
|
196
|
+
first: "{{hlineref 102 ""}}"
|
|
196
197
|
content: ["function validate() {", …, "}"]
|
|
197
198
|
```
|
|
198
199
|
|
|
199
200
|
Good — anchors to the structural target:
|
|
200
201
|
|
|
201
202
|
```
|
|
202
|
-
|
|
203
|
+
last: "{{hlineref 103 "export function serialize(data: unknown): string {"}}"
|
|
203
204
|
content: ["function validate() {", …, "}"]
|
|
204
205
|
```
|
|
205
206
|
</example>
|
|
@@ -207,7 +208,7 @@ content: ["function validate() {", …, "}"]
|
|
|
207
208
|
<critical>
|
|
208
209
|
You MUST ensure:
|
|
209
210
|
- Payload shape is `{ "path": string, "edits": [operation, …], "delete"?: boolean, "rename"?: string }`
|
|
210
|
-
-
|
|
211
|
+
- Each edit has `op`, optional `first`/`last` anchors, and `content`
|
|
211
212
|
- Every tag MUST be copied EXACTLY from a tool result as `N#ID`
|
|
212
213
|
- Scope MUST be minimal and formatting MUST be preserved except targeted token changes
|
|
213
214
|
</critical>
|
|
@@ -60,7 +60,7 @@ edit {"path":"src/app.py","op":"update","diff":"@@ def greet():\n def greet():\n
|
|
|
60
60
|
</example>
|
|
61
61
|
|
|
62
62
|
<example name="rename">
|
|
63
|
-
edit {"path":"src/app.py","op":"update","rename":"src/main.py","diff":"@@\n
|
|
63
|
+
edit {"path":"src/app.py","op":"update","rename":"src/main.py","diff":"@@\n …\n"}
|
|
64
64
|
</example>
|
|
65
65
|
|
|
66
66
|
<example name="delete">
|
|
@@ -36,8 +36,8 @@ All helpers auto-print results and return values for chaining.
|
|
|
36
36
|
<output>
|
|
37
37
|
User sees output like Jupyter notebook; rich displays render fully:
|
|
38
38
|
- `display(JSON(data))` → interactive JSON tree
|
|
39
|
-
- `display(HTML(
|
|
40
|
-
- `display(Markdown(
|
|
39
|
+
- `display(HTML(…))` → rendered HTML
|
|
40
|
+
- `display(Markdown(…))` → formatted markdown
|
|
41
41
|
- `plt.show()` → inline figures
|
|
42
42
|
**You will see object repr** (e.g., `<IPython.core.display.JSON object>`). Trust `display()`; you MUST NOT assume user sees only repr.
|
|
43
43
|
</output>
|
|
@@ -24,7 +24,7 @@ For position-addressed or pattern-addressed changes, bash more efficient:
|
|
|
24
24
|
|
|
25
25
|
|Operation|Command|
|
|
26
26
|
|---|---|
|
|
27
|
-
|Append to file|`cat >> file <<'EOF'
|
|
27
|
+
|Append to file|`cat >> file <<'EOF'`…`EOF`|
|
|
28
28
|
|Prepend to file|`{ cat - file; } <<'EOF' > tmp && mv tmp file`|
|
|
29
29
|
|Delete lines N-M|`sed -i 'N,Md' file`|
|
|
30
30
|
|Insert after line N|`sed -i 'Na\text' file`|
|
|
@@ -19,7 +19,6 @@ Subagents have no access to your conversation history. They don't know:
|
|
|
19
19
|
Subagents CAN grep the parent conversation file for supplementary details.
|
|
20
20
|
|
|
21
21
|
For large intermediate outputs (long traces, JSON payloads, temporary analysis snapshots), you SHOULD write them to `local://<path>` and pass the path in task context instead of inlining bulky text.
|
|
22
|
-
---
|
|
23
22
|
|
|
24
23
|
## Parameters
|
|
25
24
|
|
|
@@ -128,9 +127,9 @@ Use structure every assignment:
|
|
|
128
127
|
- Patterns/APIs to use; reference files if applicable
|
|
129
128
|
|
|
130
129
|
## Edge Cases / Don't Break
|
|
131
|
-
- Tricky case 1:
|
|
132
|
-
- Tricky case 2:
|
|
133
|
-
- Existing behavior must survive:
|
|
130
|
+
- Tricky case 1: …
|
|
131
|
+
- Tricky case 2: …
|
|
132
|
+
- Existing behavior must survive: …
|
|
134
133
|
|
|
135
134
|
## Acceptance (task-local)
|
|
136
135
|
- Expected behavior or observable result
|
|
@@ -145,11 +144,11 @@ Use structure every assignment:
|
|
|
145
144
|
- "Migrate to N-API."
|
|
146
145
|
- "Fix the bug in streaming."
|
|
147
146
|
- "Update all constructors in `src/**/*.ts`."
|
|
148
|
-
**Vague context** — forces agent invent conventions:
|
|
147
|
+
**Vague context** — forces agent invent conventions:
|
|
149
148
|
- "Use existing patterns."
|
|
150
149
|
- "Follow conventions."
|
|
151
150
|
- "No WASM."
|
|
152
|
-
**Redundant context** — wastes tokens repeating what subagents already have:
|
|
151
|
+
**Redundant context** — wastes tokens repeating what subagents already have:
|
|
153
152
|
- Restating AGENTS.md rules (coding style, import conventions, formatting commands, logger usage, etc.)
|
|
154
153
|
- Repeating project constraints from context files
|
|
155
154
|
- Listing tool/framework preferences already documented in the repo
|
|
@@ -192,9 +191,9 @@ First style wastes your time, brittle if code shifts. Second gives agent room to
|
|
|
192
191
|
<example type="bad" label="Duplicated context inflates tokens">
|
|
193
192
|
<tasks>
|
|
194
193
|
<task name="Grep">
|
|
195
|
-
<description>Port grep module from WASM to N-API
|
|
196
|
-
<assignment>Port grep module from WASM to N-API
|
|
197
|
-
</task>
|
|
194
|
+
<description>Port grep module from WASM to N-API…</description>
|
|
195
|
+
<assignment>Port grep module from WASM to N-API… (same blob repeated)</assignment>
|
|
196
|
+
</task>
|
|
198
197
|
</tasks>
|
|
199
198
|
</example>
|
|
200
199
|
|
|
@@ -210,7 +209,7 @@ Do not touch TS bindings or downstream consumers — separate phase.
|
|
|
210
209
|
- MUST use `#[napi]` attribute macro on all exports
|
|
211
210
|
- MUST return `napi::Result<T>` for fallible ops; never panic
|
|
212
211
|
- MUST use `spawn_blocking` for filesystem I/O or >1ms work
|
|
213
|
-
|
|
212
|
+
…
|
|
214
213
|
|
|
215
214
|
## Acceptance (global)
|
|
216
215
|
- Caller verifies after all tasks: `cargo test -p pi-natives` and `cargo build -p pi-natives` with no warnings
|
|
@@ -232,17 +231,17 @@ Do not touch TS bindings or downstream consumers — separate phase.
|
|
|
232
231
|
|
|
233
232
|
## Acceptance (task-local)
|
|
234
233
|
- Three functions exported with correct signatures (caller verifies build after all tasks)
|
|
235
|
-
</assignment>
|
|
236
|
-
</task>
|
|
234
|
+
</assignment>
|
|
235
|
+
</task>
|
|
237
236
|
|
|
238
237
|
<task name="PortHighlight">
|
|
239
238
|
<description>Port highlight module to N-API</description>
|
|
240
239
|
<assignment>
|
|
241
240
|
## Target
|
|
242
241
|
- Files: `src/highlight.rs`, `src/lib.rs` (registration only)
|
|
243
|
-
|
|
244
|
-
</assignment>
|
|
245
|
-
</task>
|
|
242
|
+
…
|
|
243
|
+
</assignment>
|
|
244
|
+
</task>
|
|
246
245
|
</tasks>
|
|
247
246
|
</example>
|
|
248
247
|
---
|
|
@@ -254,7 +253,7 @@ Each task MUST have small, well-defined scope — **at most 3–5 files**.
|
|
|
254
253
|
- File paths use globs (`src/**/*.ts`) instead of explicit names
|
|
255
254
|
- Assignment says "update all" / "migrate everything" / "refactor across"
|
|
256
255
|
- Scope covers entire package or directory tree
|
|
257
|
-
**Fix:** You MUST enumerate files first (grep/glob discovery), then fan out one task per file or small cluster.
|
|
256
|
+
**Fix:** You MUST enumerate files first (grep/glob discovery), then fan out one task per file or small cluster.
|
|
258
257
|
---
|
|
259
258
|
|
|
260
259
|
## Parallelization
|