@oh-my-pi/pi-coding-agent 14.9.1 → 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 (59) hide show
  1. package/CHANGELOG.md +60 -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/extensibility/extensions/runner.ts +173 -177
  14. package/src/hashline/apply.ts +8 -24
  15. package/src/hashline/constants.ts +20 -0
  16. package/src/hashline/execute.ts +0 -1
  17. package/src/hashline/grammar.lark +16 -27
  18. package/src/hashline/hash.ts +4 -34
  19. package/src/hashline/input.ts +16 -2
  20. package/src/hashline/parser.ts +12 -40
  21. package/src/hashline/types.ts +1 -2
  22. package/src/internal-urls/agent-protocol.ts +1 -0
  23. package/src/internal-urls/artifact-protocol.ts +1 -0
  24. package/src/internal-urls/docs-index.generated.ts +2 -1
  25. package/src/internal-urls/jobs-protocol.ts +1 -0
  26. package/src/internal-urls/local-protocol.ts +1 -0
  27. package/src/internal-urls/mcp-protocol.ts +1 -0
  28. package/src/internal-urls/memory-protocol.ts +1 -0
  29. package/src/internal-urls/pi-protocol.ts +1 -0
  30. package/src/internal-urls/router.ts +2 -1
  31. package/src/internal-urls/rule-protocol.ts +1 -0
  32. package/src/internal-urls/skill-protocol.ts +1 -0
  33. package/src/internal-urls/types.ts +18 -2
  34. package/src/mcp/transports/http.ts +49 -47
  35. package/src/prompts/system/custom-system-prompt.md +0 -2
  36. package/src/prompts/system/now-prompt.md +7 -0
  37. package/src/prompts/system/project-prompt.md +2 -0
  38. package/src/prompts/system/subagent-system-prompt.md +18 -9
  39. package/src/prompts/system/subagent-user-prompt.md +1 -10
  40. package/src/prompts/system/system-prompt.md +154 -233
  41. package/src/prompts/tools/bash.md +0 -24
  42. package/src/prompts/tools/eval.md +26 -13
  43. package/src/prompts/tools/hashline.md +1 -4
  44. package/src/sdk.ts +12 -22
  45. package/src/session/agent-session.ts +49 -17
  46. package/src/system-prompt.ts +38 -104
  47. package/src/task/executor.ts +15 -9
  48. package/src/task/index.ts +38 -33
  49. package/src/task/render.ts +4 -2
  50. package/src/tools/bash.ts +15 -41
  51. package/src/tools/eval.ts +13 -36
  52. package/src/tools/index.ts +0 -3
  53. package/src/tools/path-utils.ts +21 -1
  54. package/src/tools/read.ts +71 -49
  55. package/src/tools/search.ts +13 -1
  56. package/src/utils/file-display-mode.ts +11 -5
  57. package/src/workspace-tree.ts +210 -410
  58. package/src/task/template.ts +0 -47
  59. package/src/tools/bash-normalize.ts +0 -107
@@ -60,6 +60,15 @@ interface BeforeAgentStartCombinedResult {
60
60
 
61
61
  export type ExtensionErrorListener = (error: ExtensionError) => void;
62
62
 
63
+ export const EXTENSION_HANDLER_TIMEOUT_MS = 30_000;
64
+ let extensionHandlerTimeoutMs = EXTENSION_HANDLER_TIMEOUT_MS;
65
+
66
+ export function __test_setExtensionHandlerTimeoutMs(timeoutMs: number): void {
67
+ extensionHandlerTimeoutMs = timeoutMs;
68
+ }
69
+
70
+ const EXTENSION_HANDLER_TIMEOUT = Symbol("extensionHandlerTimeout");
71
+
63
72
  /**
64
73
  * Events handled by the generic emit() method.
65
74
  * Events with dedicated emitXxx() methods are excluded for stronger type safety.
@@ -434,6 +443,46 @@ export class ExtensionRunner {
434
443
  );
435
444
  }
436
445
 
446
+ async #runHandlerWithTimeout<TEvent extends { type: string }, TResult>(
447
+ handler: (event: TEvent, ctx: ExtensionContext) => Promise<TResult | undefined> | TResult | undefined,
448
+ event: TEvent,
449
+ ctx: ExtensionContext,
450
+ ext: Extension,
451
+ timeoutMs: number,
452
+ ): Promise<TResult | undefined> {
453
+ try {
454
+ const handlerResult = await Promise.race([
455
+ Promise.resolve(handler(event, ctx)),
456
+ Bun.sleep(timeoutMs).then(() => EXTENSION_HANDLER_TIMEOUT),
457
+ ]);
458
+ if (handlerResult === EXTENSION_HANDLER_TIMEOUT) {
459
+ const error = `handler timed out after ${timeoutMs}ms`;
460
+ logger.warn("Extension handler timed out", {
461
+ extensionPath: ext.path,
462
+ event: event.type,
463
+ timeoutMs,
464
+ });
465
+ this.emitError({
466
+ extensionPath: ext.path,
467
+ event: event.type,
468
+ error,
469
+ });
470
+ return undefined;
471
+ }
472
+ return handlerResult as TResult | undefined;
473
+ } catch (err) {
474
+ const message = err instanceof Error ? err.message : String(err);
475
+ const stack = err instanceof Error ? err.stack : undefined;
476
+ this.emitError({
477
+ extensionPath: ext.path,
478
+ event: event.type,
479
+ error: message,
480
+ stack,
481
+ });
482
+ return undefined;
483
+ }
484
+ }
485
+
437
486
  async emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>> {
438
487
  const ctx = this.createContext();
439
488
  let result: SessionBeforeEventResult | SessionCompactingResult | undefined;
@@ -443,28 +492,23 @@ export class ExtensionRunner {
443
492
  if (!handlers || handlers.length === 0) continue;
444
493
 
445
494
  for (const handler of handlers) {
446
- try {
447
- const handlerResult = await handler(event, ctx);
448
-
449
- if (this.#isSessionBeforeEvent(event) && handlerResult) {
450
- result = handlerResult as SessionBeforeEventResult;
451
- if (result.cancel) {
452
- return result as RunnerEmitResult<TEvent>;
453
- }
495
+ const handlerResult = await this.#runHandlerWithTimeout(
496
+ handler,
497
+ event,
498
+ ctx,
499
+ ext,
500
+ extensionHandlerTimeoutMs,
501
+ );
502
+
503
+ if (this.#isSessionBeforeEvent(event) && handlerResult) {
504
+ result = handlerResult as SessionBeforeEventResult;
505
+ if (result.cancel) {
506
+ return result as RunnerEmitResult<TEvent>;
454
507
  }
508
+ }
455
509
 
456
- if (event.type === "session.compacting" && handlerResult) {
457
- result = handlerResult as SessionCompactingResult;
458
- }
459
- } catch (err) {
460
- const message = err instanceof Error ? err.message : String(err);
461
- const stack = err instanceof Error ? err.stack : undefined;
462
- this.emitError({
463
- extensionPath: ext.path,
464
- event: event.type,
465
- error: message,
466
- stack,
467
- });
510
+ if (event.type === "session.compacting" && handlerResult) {
511
+ result = handlerResult as SessionCompactingResult;
468
512
  }
469
513
  }
470
514
  }
@@ -482,31 +526,26 @@ export class ExtensionRunner {
482
526
  if (!handlers || handlers.length === 0) continue;
483
527
 
484
528
  for (const handler of handlers) {
485
- try {
486
- const handlerResult = (await handler(currentEvent, ctx)) as ToolResultEventResult | undefined;
487
- if (!handlerResult) continue;
488
-
489
- if (handlerResult.content !== undefined) {
490
- currentEvent.content = handlerResult.content;
491
- modified = true;
492
- }
493
- if (handlerResult.details !== undefined) {
494
- currentEvent.details = handlerResult.details;
495
- modified = true;
496
- }
497
- if (handlerResult.isError !== undefined) {
498
- currentEvent.isError = handlerResult.isError;
499
- modified = true;
500
- }
501
- } catch (err) {
502
- const message = err instanceof Error ? err.message : String(err);
503
- const stack = err instanceof Error ? err.stack : undefined;
504
- this.emitError({
505
- extensionPath: ext.path,
506
- event: "tool_result",
507
- error: message,
508
- stack,
509
- });
529
+ const handlerResult = (await this.#runHandlerWithTimeout(
530
+ handler,
531
+ currentEvent,
532
+ ctx,
533
+ ext,
534
+ extensionHandlerTimeoutMs,
535
+ )) as ToolResultEventResult | undefined;
536
+ if (!handlerResult) continue;
537
+
538
+ if (handlerResult.content !== undefined) {
539
+ currentEvent.content = handlerResult.content;
540
+ modified = true;
541
+ }
542
+ if (handlerResult.details !== undefined) {
543
+ currentEvent.details = handlerResult.details;
544
+ modified = true;
545
+ }
546
+ if (handlerResult.isError !== undefined) {
547
+ currentEvent.isError = handlerResult.isError;
548
+ modified = true;
510
549
  }
511
550
  }
512
551
  }
@@ -574,20 +613,15 @@ export class ExtensionRunner {
574
613
  if (!handlers || handlers.length === 0) continue;
575
614
 
576
615
  for (const handler of handlers) {
577
- try {
578
- const handlerResult = await handler(event, ctx);
579
- if (handlerResult) {
580
- return handlerResult as R;
581
- }
582
- } catch (err) {
583
- const message = err instanceof Error ? err.message : String(err);
584
- const stack = err instanceof Error ? err.stack : undefined;
585
- this.emitError({
586
- extensionPath: ext.path,
587
- event: eventName,
588
- error: message,
589
- stack,
590
- });
616
+ const handlerResult = await this.#runHandlerWithTimeout(
617
+ handler,
618
+ event,
619
+ ctx,
620
+ ext,
621
+ extensionHandlerTimeoutMs,
622
+ );
623
+ if (handlerResult) {
624
+ return handlerResult as R;
591
625
  }
592
626
  }
593
627
  }
@@ -613,29 +647,24 @@ export class ExtensionRunner {
613
647
  if (!handlers || handlers.length === 0) continue;
614
648
 
615
649
  for (const handler of handlers) {
616
- try {
617
- const event: ResourcesDiscoverEvent = { type: "resources_discover", cwd, reason };
618
- const handlerResult = await handler(event, ctx);
619
- const result = handlerResult as ResourcesDiscoverResult | undefined;
620
-
621
- if (result?.skillPaths?.length) {
622
- skillPaths.push(...result.skillPaths.map(path => ({ path, extensionPath: ext.path })));
623
- }
624
- if (result?.promptPaths?.length) {
625
- promptPaths.push(...result.promptPaths.map(path => ({ path, extensionPath: ext.path })));
626
- }
627
- if (result?.themePaths?.length) {
628
- themePaths.push(...result.themePaths.map(path => ({ path, extensionPath: ext.path })));
629
- }
630
- } catch (err) {
631
- const message = err instanceof Error ? err.message : String(err);
632
- const stack = err instanceof Error ? err.stack : undefined;
633
- this.emitError({
634
- extensionPath: ext.path,
635
- event: "resources_discover",
636
- error: message,
637
- stack,
638
- });
650
+ const event: ResourcesDiscoverEvent = { type: "resources_discover", cwd, reason };
651
+ const handlerResult = await this.#runHandlerWithTimeout(
652
+ handler,
653
+ event,
654
+ ctx,
655
+ ext,
656
+ extensionHandlerTimeoutMs,
657
+ );
658
+ const result = handlerResult as ResourcesDiscoverResult | undefined;
659
+
660
+ if (result?.skillPaths?.length) {
661
+ skillPaths.push(...result.skillPaths.map(path => ({ path, extensionPath: ext.path })));
662
+ }
663
+ if (result?.promptPaths?.length) {
664
+ promptPaths.push(...result.promptPaths.map(path => ({ path, extensionPath: ext.path })));
665
+ }
666
+ if (result?.themePaths?.length) {
667
+ themePaths.push(...result.themePaths.map(path => ({ path, extensionPath: ext.path })));
639
668
  }
640
669
  }
641
670
  }
@@ -655,21 +684,14 @@ export class ExtensionRunner {
655
684
 
656
685
  for (const ext of this.extensions) {
657
686
  for (const handler of ext.handlers.get("input") ?? []) {
658
- try {
659
- const event: InputEvent = { type: "input", text: currentText, images: currentImages, source };
660
- const result = (await handler(event, ctx)) as InputEventResult | undefined;
661
- if (result?.handled) return result;
662
- if (result?.text !== undefined) {
663
- currentText = result.text;
664
- currentImages = result.images ?? currentImages;
665
- }
666
- } catch (err) {
667
- this.emitError({
668
- extensionPath: ext.path,
669
- event: "input",
670
- error: err instanceof Error ? err.message : String(err),
671
- stack: err instanceof Error ? err.stack : undefined,
672
- });
687
+ const event: InputEvent = { type: "input", text: currentText, images: currentImages, source };
688
+ const result = (await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs)) as
689
+ | InputEventResult
690
+ | undefined;
691
+ if (result?.handled) return result;
692
+ if (result?.text !== undefined) {
693
+ currentText = result.text;
694
+ currentImages = result.images ?? currentImages;
673
695
  }
674
696
  }
675
697
  }
@@ -704,22 +726,17 @@ export class ExtensionRunner {
704
726
  if (!handlers || handlers.length === 0) continue;
705
727
 
706
728
  for (const handler of handlers) {
707
- try {
708
- const event: ContextEvent = { type: "context", messages: currentMessages };
709
- const handlerResult = await handler(event, ctx);
710
-
711
- if (handlerResult && (handlerResult as ContextEventResult).messages) {
712
- currentMessages = (handlerResult as ContextEventResult).messages!;
713
- }
714
- } catch (err) {
715
- const message = err instanceof Error ? err.message : String(err);
716
- const stack = err instanceof Error ? err.stack : undefined;
717
- this.emitError({
718
- extensionPath: ext.path,
719
- event: "context",
720
- error: message,
721
- stack,
722
- });
729
+ const event: ContextEvent = { type: "context", messages: currentMessages };
730
+ const handlerResult = await this.#runHandlerWithTimeout(
731
+ handler,
732
+ event,
733
+ ctx,
734
+ ext,
735
+ extensionHandlerTimeoutMs,
736
+ );
737
+
738
+ if (handlerResult && (handlerResult as ContextEventResult).messages) {
739
+ currentMessages = (handlerResult as ContextEventResult).messages!;
723
740
  }
724
741
  }
725
742
  }
@@ -736,24 +753,19 @@ export class ExtensionRunner {
736
753
  if (!handlers || handlers.length === 0) continue;
737
754
 
738
755
  for (const handler of handlers) {
739
- try {
740
- const event: BeforeProviderRequestEvent = {
741
- type: "before_provider_request",
742
- payload: currentPayload,
743
- };
744
- const handlerResult = await handler(event, ctx);
745
- if (handlerResult !== undefined) {
746
- currentPayload = handlerResult;
747
- }
748
- } catch (err) {
749
- const message = err instanceof Error ? err.message : String(err);
750
- const stack = err instanceof Error ? err.stack : undefined;
751
- this.emitError({
752
- extensionPath: ext.path,
753
- event: "before_provider_request",
754
- error: message,
755
- stack,
756
- });
756
+ const event: BeforeProviderRequestEvent = {
757
+ type: "before_provider_request",
758
+ payload: currentPayload,
759
+ };
760
+ const handlerResult = await this.#runHandlerWithTimeout(
761
+ handler,
762
+ event,
763
+ ctx,
764
+ ext,
765
+ extensionHandlerTimeoutMs,
766
+ );
767
+ if (handlerResult !== undefined) {
768
+ currentPayload = handlerResult;
757
769
  }
758
770
  }
759
771
  }
@@ -769,25 +781,14 @@ export class ExtensionRunner {
769
781
  if (!handlers || handlers.length === 0) continue;
770
782
 
771
783
  for (const handler of handlers) {
772
- try {
773
- const event: AfterProviderResponseEvent = {
774
- type: "after_provider_response",
775
- status: response.status,
776
- headers: response.headers,
777
- requestId: response.requestId,
778
- metadata: response.metadata,
779
- };
780
- await handler(event, ctx);
781
- } catch (err) {
782
- const message = err instanceof Error ? err.message : String(err);
783
- const stack = err instanceof Error ? err.stack : undefined;
784
- this.emitError({
785
- extensionPath: ext.path,
786
- event: "after_provider_response",
787
- error: message,
788
- stack,
789
- });
790
- }
784
+ const event: AfterProviderResponseEvent = {
785
+ type: "after_provider_response",
786
+ status: response.status,
787
+ headers: response.headers,
788
+ requestId: response.requestId,
789
+ metadata: response.metadata,
790
+ };
791
+ await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
791
792
  }
792
793
  }
793
794
  }
@@ -807,34 +808,29 @@ export class ExtensionRunner {
807
808
  if (!handlers || handlers.length === 0) continue;
808
809
 
809
810
  for (const handler of handlers) {
810
- try {
811
- const event: BeforeAgentStartEvent = {
812
- type: "before_agent_start",
813
- prompt,
814
- images,
815
- systemPrompt: currentSystemPrompt,
816
- };
817
- const handlerResult = await handler(event, ctx);
818
-
819
- if (handlerResult) {
820
- const result = handlerResult as BeforeAgentStartEventResult;
821
- if (result.message) {
822
- messages.push(result.message);
823
- }
824
- if (result.systemPrompt !== undefined) {
825
- currentSystemPrompt = result.systemPrompt;
826
- systemPromptModified = true;
827
- }
811
+ const event: BeforeAgentStartEvent = {
812
+ type: "before_agent_start",
813
+ prompt,
814
+ images,
815
+ systemPrompt: currentSystemPrompt,
816
+ };
817
+ const handlerResult = await this.#runHandlerWithTimeout(
818
+ handler,
819
+ event,
820
+ ctx,
821
+ ext,
822
+ extensionHandlerTimeoutMs,
823
+ );
824
+
825
+ if (handlerResult) {
826
+ const result = handlerResult as BeforeAgentStartEventResult;
827
+ if (result.message) {
828
+ messages.push(result.message);
829
+ }
830
+ if (result.systemPrompt !== undefined) {
831
+ currentSystemPrompt = result.systemPrompt;
832
+ systemPromptModified = true;
828
833
  }
829
- } catch (err) {
830
- const message = err instanceof Error ? err.message : String(err);
831
- const stack = err instanceof Error ? err.stack : undefined;
832
- this.emitError({
833
- extensionPath: ext.path,
834
- event: "before_agent_start",
835
- error: message,
836
- stack,
837
- });
838
834
  }
839
835
  }
840
836
  }
@@ -1,6 +1,6 @@
1
1
  import { HashlineMismatchError } from "./anchors";
2
2
  import { RANGE_INTERIOR_HASH } from "./constants";
3
- import { computeLineHash, HL_EDIT_SEP } from "./hash";
3
+ import { computeLineHash } from "./hash";
4
4
  import { cloneCursor } from "./parser";
5
5
  import type { Anchor, HashlineApplyOptions, HashlineCursor, HashlineEdit, HashMismatch } from "./types";
6
6
 
@@ -37,7 +37,6 @@ interface HashlineReplacementGroup {
37
37
 
38
38
  function getHashlineEditAnchors(edit: HashlineEdit): Anchor[] {
39
39
  if (edit.kind === "delete") return [edit.anchor];
40
- if (edit.kind === "modify") return [edit.anchor];
41
40
  if (edit.cursor.kind === "before_anchor") return [edit.cursor.anchor];
42
41
  if (edit.cursor.kind === "after_anchor") return [edit.cursor.anchor];
43
42
  return [];
@@ -96,7 +95,7 @@ function insertAtEnd(fileLines: string[], lineOrigins: HashlineLineOrigin[], lin
96
95
  /** Bucket edits by the line they target so we can apply each line's group in one splice. */
97
96
 
98
97
  function getAnchorTargetLine(edit: HashlineEdit): number | undefined {
99
- if (edit.kind === "delete" || edit.kind === "modify") return edit.anchor.line;
98
+ if (edit.kind === "delete") return edit.anchor.line;
100
99
  if (edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor") return edit.cursor.anchor.line;
101
100
  return undefined;
102
101
  }
@@ -607,11 +606,9 @@ function bucketAnchorEditsByLine(edits: IndexedEdit[]): Map<number, IndexedEdit[
607
606
  const line =
608
607
  entry.edit.kind === "delete"
609
608
  ? entry.edit.anchor.line
610
- : entry.edit.kind === "modify"
611
- ? entry.edit.anchor.line
612
- : entry.edit.cursor.kind === "before_anchor"
613
- ? entry.edit.cursor.anchor.line
614
- : 0;
609
+ : entry.edit.cursor.kind === "before_anchor"
610
+ ? entry.edit.cursor.anchor.line
611
+ : 0;
615
612
  const bucket = byLine.get(line);
616
613
  if (bucket) bucket.push(entry);
617
614
  else byLine.set(line, [entry]);
@@ -683,33 +680,20 @@ export function applyHashlineEdits(
683
680
  const currentLine = fileLines[idx] ?? "";
684
681
  const beforeLines: string[] = [];
685
682
  let deleteLine = false;
686
- let prefix = "";
687
- let suffix = "";
688
- let modified = false;
689
683
 
690
684
  for (const { edit } of bucket) {
691
685
  if (edit.kind === "insert") {
692
686
  beforeLines.push(edit.text);
693
687
  } else if (edit.kind === "delete") {
694
688
  deleteLine = true;
695
- } else if (edit.kind === "modify") {
696
- prefix = edit.prefix + prefix;
697
- suffix = suffix + edit.suffix;
698
- modified = true;
699
689
  }
700
690
  }
701
- if (beforeLines.length === 0 && !deleteLine && !modified) continue;
702
- if (deleteLine && modified) {
703
- throw new Error(
704
- `line ${line}: cannot combine inline modify ("< ${line}${HL_EDIT_SEP}…" or "+ ${line}${HL_EDIT_SEP}…") with a delete or replace targeting the same line.`,
705
- );
706
- }
691
+ if (beforeLines.length === 0 && !deleteLine) continue;
707
692
 
708
- const effectiveLine = modified ? prefix + currentLine + suffix : currentLine;
709
- const replacement = deleteLine ? beforeLines : [...beforeLines, effectiveLine];
693
+ const replacement = deleteLine ? beforeLines : [...beforeLines, currentLine];
710
694
  const origins = replacement.map((): HashlineLineOrigin => (deleteLine ? "replacement" : "insert"));
711
695
  if (!deleteLine) {
712
- origins[origins.length - 1] = modified ? "replacement" : (lineOrigins[idx] ?? "original");
696
+ origins[origins.length - 1] = lineOrigins[idx] ?? "original";
713
697
  }
714
698
 
715
699
  fileLines.splice(idx, 1, ...replacement);
@@ -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.";
@@ -42,7 +42,6 @@ async function readHashlineFile(absolutePath: string, pathText: string): Promise
42
42
  function hasAnchorScopedEdit(edits: HashlineEdit[]): boolean {
43
43
  return edits.some(edit => {
44
44
  if (edit.kind === "delete") return true;
45
- if (edit.kind === "modify") return true;
46
45
  return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor";
47
46
  });
48
47
  }
@@ -1,33 +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: inline_before_op payload*
11
- | inline_after_op payload*
12
- | insert_before_op payload+
13
- | insert_after_op payload+
14
- | replace_op payload*
15
- | delete_op
16
- | blank
5
+ hunk: update_hunk
6
+ update_hunk: "@" filename LF line_op*
17
7
 
18
- inline_before_op: "<" LID $HSEP$ line_text? LF
19
- inline_after_op: "+" LID $HSEP$ line_text? LF
20
- insert_before_op: "<" insert_target LF
21
- insert_after_op: "+" insert_target LF
22
- replace_op: "=" range LF
23
- delete_op: "-" range LF
24
- payload: $HSEP$ line_text? LF
8
+ filename: /(.+)/
25
9
 
26
- 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
27
17
 
28
- insert_target: LID | "EOF" | "BOF"
18
+ anchor: LID | "EOF" | "BOF"
29
19
  range: LID ".." LID
20
+ LID: /[1-9]\d*$HFMT$/
30
21
 
31
- path: /(?:[^\s\r\n]+|"[^"\r\n]+"|'[^'\r\n]+')/
32
- LID: /[1-9][0-9]*$HFMT$/
33
- 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
  }