@oh-my-pi/pi-coding-agent 14.9.2 → 14.9.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +3 -3
  4. package/src/config/prompt-templates.ts +0 -5
  5. package/src/config/settings-schema.ts +38 -0
  6. package/src/eval/eval.lark +10 -31
  7. package/src/eval/index.ts +1 -0
  8. package/src/eval/parse.ts +156 -255
  9. package/src/eval/sniff.ts +28 -0
  10. package/src/export/html/template.css +38 -0
  11. package/src/export/html/template.generated.ts +1 -1
  12. package/src/export/html/template.js +209 -15
  13. package/src/hashline/constants.ts +20 -0
  14. package/src/hashline/grammar.lark +16 -23
  15. package/src/hashline/hash.ts +4 -34
  16. package/src/hashline/input.ts +16 -2
  17. package/src/hashline/parser.ts +12 -1
  18. package/src/internal-urls/agent-protocol.ts +1 -0
  19. package/src/internal-urls/artifact-protocol.ts +1 -0
  20. package/src/internal-urls/docs-index.generated.ts +2 -1
  21. package/src/internal-urls/jobs-protocol.ts +1 -0
  22. package/src/internal-urls/local-protocol.ts +1 -0
  23. package/src/internal-urls/mcp-protocol.ts +1 -0
  24. package/src/internal-urls/memory-protocol.ts +1 -0
  25. package/src/internal-urls/pi-protocol.ts +1 -0
  26. package/src/internal-urls/router.ts +2 -1
  27. package/src/internal-urls/rule-protocol.ts +1 -0
  28. package/src/internal-urls/skill-protocol.ts +1 -0
  29. package/src/internal-urls/types.ts +18 -2
  30. package/src/prompts/system/custom-system-prompt.md +0 -2
  31. package/src/prompts/system/now-prompt.md +7 -0
  32. package/src/prompts/system/project-prompt.md +2 -0
  33. package/src/prompts/system/subagent-system-prompt.md +18 -9
  34. package/src/prompts/system/subagent-user-prompt.md +1 -10
  35. package/src/prompts/system/system-prompt.md +154 -233
  36. package/src/prompts/tools/bash.md +0 -24
  37. package/src/prompts/tools/eval.md +26 -13
  38. package/src/session/agent-session.ts +49 -17
  39. package/src/system-prompt.ts +8 -9
  40. package/src/task/executor.ts +9 -5
  41. package/src/task/index.ts +38 -31
  42. package/src/tools/bash.ts +15 -41
  43. package/src/tools/eval.ts +13 -36
  44. package/src/tools/path-utils.ts +21 -1
  45. package/src/tools/read.ts +69 -27
  46. package/src/tools/search.ts +13 -1
  47. package/src/utils/file-display-mode.ts +11 -5
  48. package/src/task/template.ts +0 -47
  49. package/src/tools/bash-normalize.ts +0 -107
@@ -839,9 +839,11 @@
839
839
 
840
840
  function renderEdit(name, args, result, ctx) {
841
841
  const filePath = str(args.file_path == null ? args.path : args.file_path);
842
- const pathHtml = filePath === null ? invalidArgHtml() : escapeHtml(shortenPath(filePath || ''));
842
+ const pathHtml = filePath ? escapeHtml(shortenPath(filePath)) : '';
843
843
  let html = toolHead('edit', pathHtml);
844
- if (Array.isArray(args.edits)) {
844
+ if (typeof args.input === 'string' && args.input.length) {
845
+ html += codeBlock(args.input, null);
846
+ } else if (Array.isArray(args.edits)) {
845
847
  html += '<div class="tool-args">';
846
848
  for (const e of args.edits) {
847
849
  const op = e && typeof e.op === 'string' ? e.op : '?';
@@ -867,7 +869,8 @@
867
869
 
868
870
  function renderAstEdit(name, args, result, ctx) {
869
871
  const lang = args.lang || null;
870
- const pathHtml = args.path ? escapeHtml(shortenPath(String(args.path))) : '';
872
+ const paths = Array.isArray(args.paths) ? args.paths.map(p => shortenPath(String(p))).join(', ') : (args.path ? shortenPath(String(args.path)) : '');
873
+ const pathHtml = paths ? escapeHtml(paths) : '';
871
874
  const badges = [];
872
875
  if (lang) badges.push(lang);
873
876
  if (args.glob) badges.push('glob=' + args.glob);
@@ -931,10 +934,12 @@
931
934
  }
932
935
 
933
936
  function renderFind(name, args, result, ctx) {
934
- const pattern = str(args.pattern);
935
- const patHtml = pattern === null ? invalidArgHtml() : escapeHtml(pattern);
936
- const badges = args.limit ? ['limit=' + args.limit] : null;
937
- let html = toolHead('find', '<span class="tool-pattern">' + patHtml + '</span>', badges);
937
+ const paths = Array.isArray(args.paths) ? args.paths.map(p => shortenPath(String(p))).join(', ') : (str(args.pattern) || '.');
938
+ const patHtml = paths ? escapeHtml(paths) : invalidArgHtml();
939
+ const badges = [];
940
+ if (args.limit) badges.push('limit=' + args.limit);
941
+ if (args.hidden === false) badges.push('no-hidden');
942
+ let html = toolHead('find', '<span class="tool-pattern">' + patHtml + '</span>', badges.length ? badges : null);
938
943
  if (result) {
939
944
  const output = ctx.getResultText();
940
945
  if (output) html += formatExpandableOutput(output, 10);
@@ -1168,14 +1173,18 @@
1168
1173
  }
1169
1174
 
1170
1175
  function renderGh(name, args, result, ctx) {
1176
+ const op = str(args.op);
1171
1177
  const badges = [];
1178
+ if (op) badges.push(op);
1172
1179
  if (args.repo) badges.push(String(args.repo));
1173
1180
  if (args.issue) badges.push('#' + args.issue);
1174
- if (args.pr) badges.push('PR ' + args.pr);
1181
+ if (args.pr) badges.push(Array.isArray(args.pr) ? 'PRs ' + args.pr.join(',') : 'PR ' + args.pr);
1175
1182
  if (args.branch) badges.push('branch=' + args.branch);
1176
- if (args.query) badges.push('query=' + args.query);
1183
+ if (args.query) badges.push('query=' + truncate(String(args.query), 60));
1177
1184
  if (args.run) badges.push('run=' + args.run);
1185
+ if (args.title) badges.push('title=' + truncate(String(args.title), 40));
1178
1186
  let html = toolHead(name, '', badges);
1187
+ if (args.body) html += '<div class="tool-output"><div>' + escapeHtml(truncate(String(args.body), 400)) + '</div></div>';
1179
1188
  if (result) {
1180
1189
  const output = ctx.getResultText();
1181
1190
  if (output) html += formatExpandableOutput(output, 12, 'markdown');
@@ -1249,6 +1258,178 @@
1249
1258
  return html;
1250
1259
  }
1251
1260
 
1261
+ // Parse `*** Begin <LANG>` cell headers (canonical) and the legacy
1262
+ // `===== <info> =====` headers used by older transcripts. Cells emitted
1263
+ // before the format cutover still need to render in HTML exports.
1264
+ function parseEvalCells(input) {
1265
+ const text = String(input);
1266
+ if (/^[*]{2,}\s*Begin\b/im.test(text)) return parseEvalCellsNew(text);
1267
+ return parseEvalCellsLegacy(text);
1268
+ }
1269
+
1270
+ function evalLangAlias(token) {
1271
+ const t = String(token || '').toUpperCase();
1272
+ if (t === 'PY' || t === 'PYTHON' || t === 'IPY' || t === 'IPYTHON') return 'py';
1273
+ if (t === 'JS' || t === 'JAVASCRIPT') return 'js';
1274
+ if (t === 'TS' || t === 'TYPESCRIPT') return 'ts';
1275
+ return null;
1276
+ }
1277
+
1278
+ function parseEvalCellsNew(text) {
1279
+ const STARS = '\\*{2,}';
1280
+ const BEGIN = new RegExp('^' + STARS + '\\s*Begin\\b\\s*(\\S+)?\\s*$', 'i');
1281
+ const END = new RegExp('^' + STARS + '\\s*End\\b.*$', 'i');
1282
+ const TITLE = new RegExp('^' + STARS + '\\s*Title\\s*:\\s*(.+?)\\s*$', 'i');
1283
+ const TIMEOUT = new RegExp('^' + STARS + '\\s*Timeout\\s*:\\s*(\\S+)\\s*$', 'i');
1284
+ const RESET = new RegExp('^' + STARS + '\\s*Reset\\s*$', 'i');
1285
+ const lines = text.split('\n');
1286
+ if (lines.length && lines[lines.length - 1] === '') lines.pop();
1287
+ const cells = [];
1288
+ let i = 0;
1289
+ while (i < lines.length && lines[i].trim() === '') i++;
1290
+ while (i < lines.length) {
1291
+ const beginMatch = BEGIN.exec(lines[i]);
1292
+ if (!beginMatch) { i++; continue; }
1293
+ const lang = evalLangAlias(beginMatch[1]) || 'py';
1294
+ i++;
1295
+ let title = '';
1296
+ const attrs = [];
1297
+ while (i < lines.length) {
1298
+ const tm = TITLE.exec(lines[i]);
1299
+ if (tm) { if (!title) title = tm[1]; i++; continue; }
1300
+ const to = TIMEOUT.exec(lines[i]);
1301
+ if (to) { attrs.push('t=' + to[1]); i++; continue; }
1302
+ if (RESET.test(lines[i])) { attrs.push('rst'); i++; continue; }
1303
+ break;
1304
+ }
1305
+ const codeLines = [];
1306
+ while (i < lines.length) {
1307
+ if (END.test(lines[i])) { i++; break; }
1308
+ if (BEGIN.test(lines[i])) break;
1309
+ codeLines.push(lines[i]);
1310
+ i++;
1311
+ }
1312
+ while (codeLines.length && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
1313
+ cells.push({ lang, title, attrs, code: codeLines.join('\n') });
1314
+ while (i < lines.length && lines[i].trim() === '') i++;
1315
+ }
1316
+ return cells;
1317
+ }
1318
+
1319
+ function parseEvalCellsLegacy(input) {
1320
+ const HEADER = /^={5,}\s*(.*?)\s*={5,}\s*$/;
1321
+ const lines = String(input).split('\n');
1322
+ const cells = [];
1323
+ let inheritedLang = 'py';
1324
+ let current = null;
1325
+ for (const line of lines) {
1326
+ const m = line.match(HEADER);
1327
+ if (m) {
1328
+ if (current) cells.push(current);
1329
+ const info = m[1] || '';
1330
+ let lang = inheritedLang;
1331
+ let title = '';
1332
+ const langMatch = info.match(/^(py|js|ts)(?::"([^"]*)")?/);
1333
+ if (langMatch) {
1334
+ lang = langMatch[1];
1335
+ if (langMatch[2]) title = langMatch[2];
1336
+ }
1337
+ if (!title) {
1338
+ const idMatch = info.match(/id:"([^"]*)"/);
1339
+ if (idMatch) title = idMatch[1];
1340
+ }
1341
+ inheritedLang = lang;
1342
+ const attrs = [];
1343
+ const tMatch = info.match(/(?:^|\s)t:(\S+)/);
1344
+ if (tMatch) attrs.push('t=' + tMatch[1]);
1345
+ if (/(?:^|\s)rst(?:\s|$)/.test(info)) attrs.push('rst');
1346
+ current = { lang, title, attrs, code: '' };
1347
+ } else {
1348
+ if (!current) current = { lang: inheritedLang, title: '', attrs: [], code: '' };
1349
+ current.code += (current.code ? '\n' : '') + line;
1350
+ }
1351
+ }
1352
+ if (current) cells.push(current);
1353
+ return cells.map(c => ({ ...c, code: c.code.replace(/\s+$/, '') }));
1354
+ }
1355
+
1356
+ function evalLangToHljs(lang) {
1357
+ return lang === 'py' ? 'python' : lang === 'js' ? 'javascript' : lang === 'ts' ? 'typescript' : null;
1358
+ }
1359
+
1360
+ function renderEval(name, args, result, ctx) {
1361
+ let html = toolHead('eval');
1362
+ if (typeof args.input !== 'string') {
1363
+ html += '<div class="tool-error">[missing input]</div>';
1364
+ } else {
1365
+ const cells = parseEvalCells(args.input);
1366
+ if (cells.length === 0) {
1367
+ html += codeBlock(args.input, 'python');
1368
+ } else {
1369
+ for (const cell of cells) {
1370
+ html += '<div class="tool-cell">';
1371
+ const titleParts = [];
1372
+ if (cell.title) titleParts.push(cell.title);
1373
+ titleParts.push(cell.lang);
1374
+ if (cell.attrs && cell.attrs.length) titleParts.push(...cell.attrs);
1375
+ html += '<div class="tool-cell-title">' + escapeHtml(titleParts.join(' · ')) + '</div>';
1376
+ html += codeBlock(cell.code, evalLangToHljs(cell.lang));
1377
+ html += '</div>';
1378
+ }
1379
+ }
1380
+ }
1381
+ if (result) {
1382
+ html += ctx.renderResultImages();
1383
+ const output = ctx.getResultText();
1384
+ if (output) html += formatExpandableOutput(output, 12);
1385
+ }
1386
+ return html;
1387
+ }
1388
+
1389
+ function renderSearch(name, args, result, ctx) {
1390
+ const pattern = str(args.pattern);
1391
+ const paths = Array.isArray(args.paths) ? args.paths.map(p => shortenPath(String(p))).join(', ') : (args.path ? shortenPath(String(args.path)) : '.');
1392
+ const patHtml = pattern === null ? invalidArgHtml() : escapeHtml(pattern);
1393
+ let head = '<span class="tool-name">search</span> <span class="tool-pattern">/' + patHtml + '/</span>';
1394
+ head += ' <span class="tool-arg-key">in</span> <span class="tool-path">' + escapeHtml(paths) + '</span>';
1395
+ const badges = [];
1396
+ if (args.i) badges.push('i');
1397
+ if (args.skip) badges.push('skip=' + args.skip);
1398
+ if (args.gitignore === false) badges.push('no-gitignore');
1399
+ for (const b of badges) head += ' <span class="tool-badge">' + escapeHtml(b) + '</span>';
1400
+ let html = '<div class="tool-header">' + head + '</div>';
1401
+ if (result) {
1402
+ const output = ctx.getResultText();
1403
+ if (output) html += formatExpandableOutput(output, 12);
1404
+ }
1405
+ return html;
1406
+ }
1407
+
1408
+ function renderRecipe(name, args, result, ctx) {
1409
+ const op = str(args.op) || '?';
1410
+ let html = toolHead('recipe', '<span class="tool-arg-val">' + escapeHtml(op) + '</span>');
1411
+ if (result) {
1412
+ html += ctx.renderResultImages();
1413
+ const output = ctx.getResultText();
1414
+ if (output) html += formatExpandableOutput(output, 10);
1415
+ }
1416
+ return html;
1417
+ }
1418
+
1419
+ function renderIrc(name, args, result, ctx) {
1420
+ const op = str(args.op) || '?';
1421
+ const badges = [op];
1422
+ if (args.to) badges.push('to=' + args.to);
1423
+ if (args.awaitReply === false) badges.push('no-reply');
1424
+ let html = toolHead('irc', '', badges);
1425
+ if (args.message) html += '<div class="tool-output"><div>' + escapeHtml(String(args.message)) + '</div></div>';
1426
+ if (result) {
1427
+ const output = ctx.getResultText();
1428
+ if (output) html += formatExpandableOutput(output, 8);
1429
+ }
1430
+ return html;
1431
+ }
1432
+
1252
1433
 
1253
1434
  function renderGenericTool(name, args, result, ctx) {
1254
1435
  let html = toolHead(name);
@@ -1266,6 +1447,7 @@
1266
1447
 
1267
1448
  const TOOL_RENDERERS = {
1268
1449
  bash: renderBash,
1450
+ eval: renderEval,
1269
1451
  js: renderJsLike,
1270
1452
  python: renderJsLike,
1271
1453
  notebook: renderJsLike,
@@ -1275,6 +1457,7 @@
1275
1457
  ast_edit: renderAstEdit,
1276
1458
  ast_grep: renderAstGrep,
1277
1459
  grep: renderGrep,
1460
+ search: renderSearch,
1278
1461
  find: renderFind,
1279
1462
  lsp: renderLsp,
1280
1463
  todo_write: renderTodoWrite,
@@ -1300,16 +1483,25 @@
1300
1483
  poll: renderJob,
1301
1484
  cancel_job: renderJob,
1302
1485
  job: renderJob,
1486
+ recipe: renderRecipe,
1487
+ irc: renderIrc,
1303
1488
  };
1304
1489
 
1305
1490
  function renderToolCall(call) {
1306
1491
  const result = findToolResult(call.id);
1307
1492
  const isError = result?.isError || false;
1308
1493
  const statusClass = result ? (isError ? 'error' : 'success') : 'pending';
1309
- const args = call.arguments || {};
1494
+ const rawArgs = call.arguments || {};
1495
+ const intent = typeof rawArgs._i === 'string' ? rawArgs._i.trim() : '';
1496
+ // Strip internal _i intent so renderers don't dump it as JSON.
1497
+ const args = {};
1498
+ for (const k of Object.keys(rawArgs)) {
1499
+ if (k !== '_i') args[k] = rawArgs[k];
1500
+ }
1310
1501
  const name = call.name;
1311
1502
 
1312
1503
  const ctx = {
1504
+ intent,
1313
1505
  getResultText: () => {
1314
1506
  if (!result) return '';
1315
1507
  const textBlocks = result.content.filter(c => c.type === 'text');
@@ -1331,6 +1523,7 @@
1331
1523
 
1332
1524
  const renderer = TOOL_RENDERERS[name] || renderGenericTool;
1333
1525
  let html = '<div class="tool-execution ' + statusClass + '">';
1526
+ if (intent) html += '<div class="tool-intent">' + escapeHtml(intent) + '</div>';
1334
1527
  try {
1335
1528
  html += renderer(name, args, result, ctx);
1336
1529
  } catch (err) {
@@ -1638,11 +1831,12 @@
1638
1831
  }
1639
1832
 
1640
1833
  if (tools && tools.length > 0) {
1641
- html += `<div class="tools-list">
1642
- <div class="tools-header">Available Tools</div>
1643
- <div class="tools-content">
1644
- ${tools.map(t => `<div class="tool-item"><span class="tool-item-name">${escapeHtml(t.name)}</span> - <span class="tool-item-desc">${escapeHtml(t.description)}</span></div>`).join('')}
1645
- </div>
1834
+ const namesHtml = tools.map(t => `<span class="tool-name-chip">${escapeHtml(t.name)}</span>`).join('');
1835
+ const fullHtml = tools.map(t => `<div class="tool-item"><span class="tool-item-name">${escapeHtml(t.name)}</span> - <span class="tool-item-desc">${escapeHtml(t.description)}</span></div>`).join('');
1836
+ html += `<div class="tools-list collapsed" onclick="this.classList.toggle('collapsed')">
1837
+ <div class="tools-header">Available Tools (${tools.length})</div>
1838
+ <div class="tools-collapsed">${namesHtml}</div>
1839
+ <div class="tools-content">${fullHtml}</div>
1646
1840
  </div>`;
1647
1841
  }
1648
1842
 
@@ -6,3 +6,23 @@ export const RANGE_INTERIOR_HASH = "**";
6
6
 
7
7
  /** Header marker introducing a new file section in multi-section input. */
8
8
  export const FILE_HEADER_PREFIX = "@";
9
+
10
+ /** Optional patch envelope start marker; silently consumed when present. */
11
+ export const BEGIN_PATCH_MARKER = "*** Begin Patch";
12
+
13
+ /** Optional patch envelope end marker; terminates parsing when encountered. */
14
+ export const END_PATCH_MARKER = "*** End Patch";
15
+
16
+ /**
17
+ * Recovery sentinel emitted by the agent loop when a contaminated
18
+ * `to=functions.edit` stream is truncated mid-call (see
19
+ * `docs/ERRATA-GPT5-HARMONY.md`). Behaves like `END_PATCH_MARKER` for
20
+ * parsing — terminates the line loop — and additionally surfaces a
21
+ * warning in the tool result so the model knows to re-issue any
22
+ * remaining edits.
23
+ */
24
+ export const ABORT_MARKER = "*** Abort";
25
+
26
+ /** Warning text appended to the tool result when ABORT_MARKER terminates parsing. */
27
+ export const ABORT_WARNING =
28
+ "Tool stream truncated mid-call due to detected output corruption. Applied ops above are valid. Re-issue any remaining edits.";
@@ -1,29 +1,22 @@
1
- %import common.LF
2
- %import common.WS_INLINE
3
-
4
- start: section+
5
-
6
- section: file_header line_op*
7
-
8
- file_header: "@" path LF
1
+ start: begin_patch hunk+ end_patch
2
+ begin_patch: "*** Begin Patch" LF
3
+ end_patch: "*** End Patch" LF?
9
4
 
10
- line_op: insert_before_op payload+
11
- | insert_after_op payload+
12
- | replace_op payload*
13
- | delete_op
14
- | blank
5
+ hunk: update_hunk
6
+ update_hunk: "@" filename LF line_op*
15
7
 
16
- insert_before_op: "<" insert_target LF
17
- insert_after_op: "+" insert_target LF
18
- replace_op: "=" range LF
19
- delete_op: "-" range LF
20
- payload: $HSEP$ line_text? LF
8
+ filename: /(.+)/
21
9
 
22
- line_text: /[^\r\n]+/
10
+ line_op: insert_before | insert_after | replace | delete | blank
11
+ insert_before: ("<" | "< ") anchor LF payload+
12
+ insert_after: ("+" | "+ ") anchor LF payload+
13
+ replace: ("=" | "= ") range LF payload*
14
+ delete: ("-" | "- ") range LF
15
+ payload: $HSEP$ /(.*)/ LF
16
+ blank: LF
23
17
 
24
- insert_target: LID | "EOF" | "BOF"
18
+ anchor: LID | "EOF" | "BOF"
25
19
  range: LID ".." LID
20
+ LID: /[1-9]\d*$HFMT$/
26
21
 
27
- path: /(?:[^\s\r\n]+|"[^"\r\n]+"|'[^'\r\n]+')/
28
- LID: /[1-9][0-9]*$HFMT$/
29
- blank: LF
22
+ %import common.LF
@@ -137,48 +137,18 @@ export const HL_BODY_SEP = "|";
137
137
  export const HL_BODY_SEP_RE_RAW = regexEscape(HL_BODY_SEP);
138
138
 
139
139
  const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
140
- const RE_STRUCTURAL_STRIP = /[\s{}]/g;
141
-
142
- /**
143
- * Bigram returned for lines that contain only whitespace and `{`/`}`.
144
- * Picks the English ordinal suffix for the line number (`1` → `st`,
145
- * `2` → `nd`, `3` → `rd`, `11`/`12`/`13` → `th`, else `th`) so the
146
- * line digits + bigram BPE-merge into a single ordinal token (`1st`, `42nd`,
147
- * `100th`, …). Brace-only lines therefore cost one token for the whole
148
- * `LINE+ID` anchor instead of two.
149
- */
150
- function structuralBigram(line: number): string {
151
- const mod100 = line % 100;
152
- if (mod100 >= 11 && mod100 <= 13) return "th";
153
- switch (line % 10) {
154
- case 1:
155
- return "st";
156
- case 2:
157
- return "nd";
158
- case 3:
159
- return "rd";
160
- default:
161
- return "th";
162
- }
163
- }
164
140
 
165
141
  /**
166
142
  * Compute a 2-character hash of a single line via xxHash32 mod 647 over
167
- * {@link HL_BIGRAMS}. Lines that contain only whitespace and `{`/`}` collapse
168
- * to an ordinal-suffix bigram (see {@link structuralBigram}) so brace-only
169
- * structure shares one merged ordinal token (`1st`, `42nd`, `100th`, …).
170
- * Other lines with no letter or digit mix the line number into the seed so
171
- * adjacent identical punctuation-only lines get distinct hashes; lines with
172
- * significant content stay line-number-independent so a line is identifiable
173
- * across small shifts.
143
+ * {@link HL_BIGRAMS}. Lines with no letter or digit mix the line number
144
+ * into the seed so adjacent identical punctuation-only lines (e.g. brace-only
145
+ * lines) get distinct hashes; lines with significant content stay
146
+ * line-number-independent so a line is identifiable across small shifts.
174
147
  *
175
148
  * The line input should not include a trailing newline.
176
149
  */
177
150
  export function computeLineHash(idx: number, line: string): string {
178
151
  line = line.replace(/\r/g, "").trimEnd();
179
- if (line.replace(RE_STRUCTURAL_STRIP, "").length === 0) {
180
- return structuralBigram(idx);
181
- }
182
152
  const seed = RE_SIGNIFICANT.test(line) ? 0 : idx;
183
153
  return HL_BIGRAMS[Bun.hash.xxHash32(line, seed) % HL_BIGRAMS_COUNT];
184
154
  }
@@ -1,5 +1,5 @@
1
1
  import * as path from "node:path";
2
- import { FILE_HEADER_PREFIX } from "./constants";
2
+ import { ABORT_MARKER, BEGIN_PATCH_MARKER, END_PATCH_MARKER, FILE_HEADER_PREFIX } from "./constants";
3
3
  import { HL_EDIT_SEP } from "./hash";
4
4
  import type { SplitHashlineOptions } from "./types";
5
5
  import { stripTrailingCarriageReturn } from "./utils";
@@ -38,10 +38,22 @@ function parseHashlineHeaderLine(line: string, cwd?: string): HashlineInputSecti
38
38
  return { path: parsedPath, diff: "" };
39
39
  }
40
40
 
41
+ function isPatchEnvelopeMarker(line: string): boolean {
42
+ const trimmed = line.trimEnd();
43
+ return trimmed === BEGIN_PATCH_MARKER || trimmed === END_PATCH_MARKER;
44
+ }
45
+
41
46
  function stripLeadingBlankLines(input: string): string {
42
47
  const stripped = input.startsWith("\uFEFF") ? input.slice(1) : input;
43
48
  const lines = stripped.split("\n");
44
- while (lines.length > 0 && lines[0].replace(/\r$/, "").trim().length === 0) lines.shift();
49
+ while (lines.length > 0) {
50
+ const head = lines[0].replace(/\r$/, "");
51
+ if (head.trim().length === 0 || head.trimEnd() === BEGIN_PATCH_MARKER) {
52
+ lines.shift();
53
+ continue;
54
+ }
55
+ break;
56
+ }
45
57
  return lines.join("\n");
46
58
  }
47
59
 
@@ -96,6 +108,8 @@ export function splitHashlineInputs(input: string, options: SplitHashlineOptions
96
108
 
97
109
  for (const rawLine of lines) {
98
110
  const line = stripTrailingCarriageReturn(rawLine);
111
+ if (line.trimEnd() === END_PATCH_MARKER || line.trimEnd() === ABORT_MARKER) break;
112
+ if (isPatchEnvelopeMarker(line)) continue;
99
113
  const header = parseHashlineHeaderLine(line, options.cwd);
100
114
  if (header !== null) {
101
115
  flush();
@@ -1,4 +1,4 @@
1
- import { RANGE_INTERIOR_HASH } from "./constants";
1
+ import { ABORT_MARKER, ABORT_WARNING, BEGIN_PATCH_MARKER, END_PATCH_MARKER, RANGE_INTERIOR_HASH } from "./constants";
2
2
  import { describeAnchorExamples, HL_EDIT_SEP, HL_HASH_CAPTURE_RE_RAW } from "./hash";
3
3
  import type { Anchor, HashlineCursor, HashlineEdit } from "./types";
4
4
  import { stripTrailingCarriageReturn } from "./utils";
@@ -118,6 +118,17 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
118
118
  i++;
119
119
  continue;
120
120
  }
121
+ if (line === END_PATCH_MARKER) {
122
+ break;
123
+ }
124
+ if (line === ABORT_MARKER) {
125
+ warnings.push(ABORT_WARNING);
126
+ break;
127
+ }
128
+ if (line === BEGIN_PATCH_MARKER) {
129
+ i++;
130
+ continue;
131
+ }
121
132
  if (line.startsWith(HL_EDIT_SEP)) {
122
133
  throw new Error(`line ${lineNum}: payload line has no preceding +, <, or = operation.`);
123
134
  }
@@ -42,6 +42,7 @@ async function listAvailableOutputs(artifactsDir: string): Promise<string[]> {
42
42
  */
43
43
  export class AgentProtocolHandler implements ProtocolHandler {
44
44
  readonly scheme = "agent";
45
+ readonly immutable = true;
45
46
 
46
47
  constructor(private readonly options: AgentProtocolOptions) {}
47
48
 
@@ -44,6 +44,7 @@ async function listAvailableArtifacts(artifactsDir: string): Promise<string[]> {
44
44
  */
45
45
  export class ArtifactProtocolHandler implements ProtocolHandler {
46
46
  readonly scheme = "artifact";
47
+ readonly immutable = true;
47
48
 
48
49
  constructor(private readonly options: ArtifactProtocolOptions) {}
49
50