@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.
- package/CHANGELOG.md +45 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +3 -3
- package/src/config/prompt-templates.ts +0 -5
- package/src/config/settings-schema.ts +38 -0
- package/src/eval/eval.lark +10 -31
- package/src/eval/index.ts +1 -0
- package/src/eval/parse.ts +156 -255
- package/src/eval/sniff.ts +28 -0
- package/src/export/html/template.css +38 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +209 -15
- package/src/hashline/constants.ts +20 -0
- package/src/hashline/grammar.lark +16 -23
- package/src/hashline/hash.ts +4 -34
- package/src/hashline/input.ts +16 -2
- package/src/hashline/parser.ts +12 -1
- package/src/internal-urls/agent-protocol.ts +1 -0
- package/src/internal-urls/artifact-protocol.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +2 -1
- package/src/internal-urls/jobs-protocol.ts +1 -0
- package/src/internal-urls/local-protocol.ts +1 -0
- package/src/internal-urls/mcp-protocol.ts +1 -0
- package/src/internal-urls/memory-protocol.ts +1 -0
- package/src/internal-urls/pi-protocol.ts +1 -0
- package/src/internal-urls/router.ts +2 -1
- package/src/internal-urls/rule-protocol.ts +1 -0
- package/src/internal-urls/skill-protocol.ts +1 -0
- package/src/internal-urls/types.ts +18 -2
- package/src/prompts/system/custom-system-prompt.md +0 -2
- package/src/prompts/system/now-prompt.md +7 -0
- package/src/prompts/system/project-prompt.md +2 -0
- package/src/prompts/system/subagent-system-prompt.md +18 -9
- package/src/prompts/system/subagent-user-prompt.md +1 -10
- package/src/prompts/system/system-prompt.md +154 -233
- package/src/prompts/tools/bash.md +0 -24
- package/src/prompts/tools/eval.md +26 -13
- package/src/session/agent-session.ts +49 -17
- package/src/system-prompt.ts +8 -9
- package/src/task/executor.ts +9 -5
- package/src/task/index.ts +38 -31
- package/src/tools/bash.ts +15 -41
- package/src/tools/eval.ts +13 -36
- package/src/tools/path-utils.ts +21 -1
- package/src/tools/read.ts +69 -27
- package/src/tools/search.ts +13 -1
- package/src/utils/file-display-mode.ts +11 -5
- package/src/task/template.ts +0 -47
- 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
|
|
842
|
+
const pathHtml = filePath ? escapeHtml(shortenPath(filePath)) : '';
|
|
843
843
|
let html = toolHead('edit', pathHtml);
|
|
844
|
-
if (
|
|
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
|
|
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
|
|
935
|
-
const patHtml =
|
|
936
|
-
const badges =
|
|
937
|
-
|
|
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
|
|
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
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
| replace_op payload*
|
|
13
|
-
| delete_op
|
|
14
|
-
| blank
|
|
5
|
+
hunk: update_hunk
|
|
6
|
+
update_hunk: "@" filename LF line_op*
|
|
15
7
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
anchor: LID | "EOF" | "BOF"
|
|
25
19
|
range: LID ".." LID
|
|
20
|
+
LID: /[1-9]\d*$HFMT$/
|
|
26
21
|
|
|
27
|
-
|
|
28
|
-
LID: /[1-9][0-9]*$HFMT$/
|
|
29
|
-
blank: LF
|
|
22
|
+
%import common.LF
|
package/src/hashline/hash.ts
CHANGED
|
@@ -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
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
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
|
}
|
package/src/hashline/input.ts
CHANGED
|
@@ -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
|
|
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();
|
package/src/hashline/parser.ts
CHANGED
|
@@ -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
|
|