@oh-my-pi/pi-coding-agent 14.6.2 → 14.6.4

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 (62) hide show
  1. package/CHANGELOG.md +95 -2
  2. package/README.md +21 -0
  3. package/package.json +23 -7
  4. package/src/cli/grievances-cli.ts +89 -4
  5. package/src/commands/grievances.ts +33 -7
  6. package/src/config/prompt-templates.ts +14 -7
  7. package/src/config/settings-schema.ts +610 -100
  8. package/src/config/settings.ts +42 -0
  9. package/src/discovery/helpers.ts +13 -6
  10. package/src/edit/index.ts +3 -3
  11. package/src/edit/line-hash.ts +73 -25
  12. package/src/edit/modes/hashline.lark +10 -3
  13. package/src/edit/modes/hashline.ts +295 -40
  14. package/src/edit/renderer.ts +3 -3
  15. package/src/hindsight/backend.ts +205 -0
  16. package/src/hindsight/bank.ts +131 -0
  17. package/src/hindsight/client.ts +598 -0
  18. package/src/hindsight/config.ts +175 -0
  19. package/src/hindsight/content.ts +210 -0
  20. package/src/hindsight/index.ts +8 -0
  21. package/src/hindsight/mental-models.ts +382 -0
  22. package/src/hindsight/seeds.json +32 -0
  23. package/src/hindsight/state.ts +469 -0
  24. package/src/hindsight/transcript.ts +71 -0
  25. package/src/main.ts +7 -10
  26. package/src/memories/index.ts +1 -1
  27. package/src/memory-backend/index.ts +4 -0
  28. package/src/memory-backend/local-backend.ts +30 -0
  29. package/src/memory-backend/off-backend.ts +16 -0
  30. package/src/memory-backend/resolve.ts +24 -0
  31. package/src/memory-backend/types.ts +79 -0
  32. package/src/modes/components/settings-defs.ts +50 -451
  33. package/src/modes/components/settings-selector.ts +2 -2
  34. package/src/modes/components/status-line/presets.ts +1 -1
  35. package/src/modes/controllers/command-controller.ts +266 -6
  36. package/src/modes/controllers/event-controller.ts +12 -0
  37. package/src/modes/controllers/selector-controller.ts +3 -12
  38. package/src/modes/theme/theme.ts +4 -0
  39. package/src/prompts/tools/github.md +3 -0
  40. package/src/prompts/tools/hashline.md +21 -16
  41. package/src/prompts/tools/read.md +10 -6
  42. package/src/prompts/tools/recall.md +5 -0
  43. package/src/prompts/tools/reflect.md +5 -0
  44. package/src/prompts/tools/retain.md +5 -0
  45. package/src/prompts/tools/search.md +1 -1
  46. package/src/sdk.ts +21 -9
  47. package/src/session/agent-session.ts +118 -3
  48. package/src/slash-commands/builtin-registry.ts +12 -12
  49. package/src/task/executor.ts +3 -0
  50. package/src/task/index.ts +2 -0
  51. package/src/tools/ast-edit.ts +14 -5
  52. package/src/tools/ast-grep.ts +12 -3
  53. package/src/tools/find.ts +47 -7
  54. package/src/tools/gh-renderer.ts +10 -1
  55. package/src/tools/gh.ts +233 -5
  56. package/src/tools/hindsight-recall.ts +68 -0
  57. package/src/tools/hindsight-reflect.ts +55 -0
  58. package/src/tools/hindsight-retain.ts +60 -0
  59. package/src/tools/index.ts +20 -0
  60. package/src/tools/path-utils.ts +55 -0
  61. package/src/tools/read.ts +1 -1
  62. package/src/tools/search.ts +45 -8
@@ -568,6 +568,48 @@ export class Settings {
568
568
  }
569
569
  }
570
570
 
571
+ // Map legacy `memories.enabled` boolean to the explicit `memory.backend`
572
+ // enum if the latter hasn't been set yet. Idempotent: subsequent
573
+ // migrations are no-ops once memory.backend is materialised.
574
+ const memoryBackendObj = raw.memory as Record<string, unknown> | undefined;
575
+ const memoryBackendSet = memoryBackendObj && typeof memoryBackendObj.backend === "string";
576
+ const memoriesObj = raw.memories as Record<string, unknown> | undefined;
577
+ if (!memoryBackendSet && memoriesObj && typeof memoriesObj.enabled === "boolean") {
578
+ const next = memoriesObj.enabled ? "local" : "off";
579
+ const memoryRoot = (memoryBackendObj ?? {}) as Record<string, unknown>;
580
+ memoryRoot.backend = next;
581
+ raw.memory = memoryRoot;
582
+ }
583
+
584
+ // hindsight: dynamicBankId/agentName -> scoping enum + bankId
585
+ // - dynamicBankId=true → scoping="per-project" (closest semantic match;
586
+ // the legacy `agent::project::channel::user` tuple was per-project in
587
+ // practice — the channel/user env vars were rarely set).
588
+ // - hindsight.agentName was only used as the agent slot in the legacy
589
+ // dynamic tuple; if the user customised it we surface it as the new
590
+ // bankId base when no explicit bankId is set.
591
+ const hindsightObj = raw.hindsight as Record<string, unknown> | undefined;
592
+ if (hindsightObj) {
593
+ if ("dynamicBankId" in hindsightObj) {
594
+ if (!("scoping" in hindsightObj) && hindsightObj.dynamicBankId === true) {
595
+ hindsightObj.scoping = "per-project";
596
+ }
597
+ delete hindsightObj.dynamicBankId;
598
+ }
599
+ if ("agentName" in hindsightObj) {
600
+ const agentName = hindsightObj.agentName;
601
+ if (
602
+ !("bankId" in hindsightObj) &&
603
+ typeof agentName === "string" &&
604
+ agentName.trim().length > 0 &&
605
+ agentName !== "omp"
606
+ ) {
607
+ hindsightObj.bankId = agentName;
608
+ }
609
+ delete hindsightObj.agentName;
610
+ }
611
+ }
612
+
571
613
  return raw;
572
614
  }
573
615
 
@@ -3,7 +3,14 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
5
5
  import { FileType, glob } from "@oh-my-pi/pi-natives";
6
- import { CONFIG_DIR_NAME, getConfigDirName, getProjectDir, parseFrontmatter, tryParseJson } from "@oh-my-pi/pi-utils";
6
+ import {
7
+ CONFIG_DIR_NAME,
8
+ getConfigDirName,
9
+ getPluginsDir,
10
+ getProjectDir,
11
+ parseFrontmatter,
12
+ tryParseJson,
13
+ } from "@oh-my-pi/pi-utils";
7
14
  import type { ExtensionModule } from "../capability/extension-module";
8
15
  import { invalidate as invalidateFsCache, readDirEntries, readFile } from "../capability/fs";
9
16
  import { parseRuleConditionAndScope, type Rule, type RuleFrontmatter } from "../capability/rule";
@@ -804,8 +811,9 @@ export async function listClaudePluginRoots(
804
811
 
805
812
  // ── OMP installed plugins registry ───────────────────────────────────────
806
813
  // OMP registry is authoritative: its entries replace Claude's entries for the same plugin ID.
807
- // Path derived from `home` (not os.homedir()) so test isolation works when home is overridden.
808
- const ompRegistryPath = path.join(home, getConfigDirName(), "plugins", "installed_plugins.json");
814
+ // getPluginsDir() resolves to the same path the marketplace writer uses
815
+ // (XDG-aware via the dir resolver), so reads and writes always agree.
816
+ const ompRegistryPath = path.join(getPluginsDir(), "installed_plugins.json");
809
817
  const ompContent = await readFile(ompRegistryPath);
810
818
  if (ompContent) {
811
819
  const ompRegistry = parseClaudePluginsRegistry(ompContent);
@@ -928,9 +936,8 @@ export function clearClaudePluginRootsCache(): void {
928
936
  * installing/uninstalling/enabling/disabling plugins.
929
937
  */
930
938
  export function clearPluginRootsAndCaches(extraPaths?: readonly string[]): void {
931
- const home = os.homedir();
932
- invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
933
- invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
939
+ invalidateFsCache(path.join(os.homedir(), ".claude", "plugins", "installed_plugins.json"));
940
+ invalidateFsCache(path.join(getPluginsDir(), "installed_plugins.json"));
934
941
  for (const p of extraPaths ?? []) invalidateFsCache(p);
935
942
  clearClaudePluginRootsCache();
936
943
  }
package/src/edit/index.ts CHANGED
@@ -16,7 +16,7 @@ import type { ToolSession } from "../tools";
16
16
  import { VimTool, vimSchema } from "../tools/vim";
17
17
  import { type EditMode, normalizeEditMode, resolveEditMode } from "../utils/edit-mode";
18
18
  import type { VimToolDetails } from "../vim/types";
19
- import { resolveLarkLidPlaceholders } from "./line-hash";
19
+ import { resolveHashlineGrammarPlaceholders } from "./line-hash";
20
20
  import { type ApplyPatchParams, applyPatchSchema, expandApplyPatchToEntries } from "./modes/apply-patch";
21
21
  import applyPatchGrammar from "./modes/apply-patch.lark" with { type: "text" };
22
22
  import {
@@ -35,8 +35,8 @@ export * from "./apply-patch";
35
35
  export * from "./diff";
36
36
  export * from "./line-hash";
37
37
 
38
- // Resolve the `$HASHFMT$` placeholder in the hashline Lark grammar.
39
- const hashlineGrammar = resolveLarkLidPlaceholders(hashlineGrammarTemplate);
38
+ // Resolve the `$HFMT$` and `$HSEP$` placeholders in the hashline Lark grammar.
39
+ const hashlineGrammar = resolveHashlineGrammarPlaceholders(hashlineGrammarTemplate);
40
40
 
41
41
  export * from "./modes/apply-patch";
42
42
  export * from "./modes/hashline";
@@ -15,7 +15,7 @@
15
15
  * Order is stable forever — changing it would invalidate every saved
16
16
  * `LINE+ID` reference in transcripts and prompts.
17
17
  */
18
- export const HASHLINE_BIGRAMS = [
18
+ export const HL_BIGRAMS = [
19
19
  "aa",
20
20
  "ab",
21
21
  "ac",
@@ -665,7 +665,7 @@ export const HASHLINE_BIGRAMS = [
665
665
  "zz",
666
666
  ] as const;
667
667
 
668
- export const HASHLINE_BIGRAMS_COUNT = HASHLINE_BIGRAMS.length;
668
+ export const HL_BIGRAMS_COUNT = HL_BIGRAMS.length;
669
669
 
670
670
  /**
671
671
  * Decoration prefix that may precede a `LINE+HASH` anchor in tool output:
@@ -675,7 +675,7 @@ export const HASHLINE_BIGRAMS_COUNT = HASHLINE_BIGRAMS.length;
675
675
  * regex stays liberal because anchor-ref parsers accept whatever the model
676
676
  * echoes back.
677
677
  */
678
- export const HASHLINE_ANCHOR_DECORATION_RE_SRC = `\\s*[>+\\-*]*\\s*`;
678
+ export const HL_ANCHOR_DECORATION_RE_RAW = `\\s*[>+\\-*]*\\s*`;
679
679
 
680
680
  /**
681
681
  * Capture-group regex source for a decorated `LINE+HASH` anchor. Group 1
@@ -683,62 +683,110 @@ export const HASHLINE_ANCHOR_DECORATION_RE_SRC = `\\s*[>+\\-*]*\\s*`;
683
683
  * source is intentionally unanchored — anchoring with `^` (or composing into a
684
684
  * larger pattern) is the caller's responsibility.
685
685
  */
686
- export const HASHLINE_ANCHOR_RE_SRC = `${HASHLINE_ANCHOR_DECORATION_RE_SRC}(\\d+)([a-z]{2})`;
686
+ export const HL_ANCHOR_RE_RAW = `${HL_ANCHOR_DECORATION_RE_RAW}(\\d+)([a-z]{2})`;
687
687
 
688
688
  /**
689
689
  * Bare `LINE+HASH` Lid (no decorations, no captures, no anchors). Use for
690
690
  * embedding inside larger patterns where the line+hash unit appears as a
691
691
  * literal (e.g. range bounds, alternation arms, op-line heuristics).
692
692
  */
693
- export const HASHLINE_LID_RE_SRC = `[1-9]\\d*[a-z]{2}`;
693
+ export const HL_HASH_RE_RAW = `[1-9]\\d*[a-z]{2}`;
694
694
 
695
695
  /**
696
- * Capture-group form of {@link HASHLINE_LID_RE_SRC}: group 1 captures the
696
+ * Capture-group form of {@link HL_HASH_RE_RAW}: group 1 captures the
697
697
  * line number, group 2 captures the hash.
698
698
  */
699
- export const HASHLINE_LID_CAPTURE_RE_SRC = `([1-9]\\d*)([a-z]{2})`;
699
+ export const HL_HASH_CAPTURE_RE_RAW = `([1-9]\\d*)([a-z]{2})`;
700
700
 
701
701
  /** Width of a hash in display characters. */
702
- export const HASHLINE_HASH_WIDTH = 2;
702
+ export const HL_HASH_WIDTH = 2;
703
703
 
704
704
  /**
705
705
  * Representative hash suffixes for use in user-facing error messages and
706
706
  * prompt examples.
707
707
  */
708
- export const HASHLINE_HASH_EXAMPLES = ["sr", "ab", "th"] as const;
708
+ export const HL_HASH_EXAMPLES = ["sr", "ab", "th"] as const;
709
709
 
710
710
  /**
711
711
  * Format a comma-separated list of example anchors with an optional line-number
712
712
  * prefix, quoted for inclusion in error messages: `"160sr", "160ab", "160th"`.
713
713
  */
714
714
  export function describeAnchorExamples(linePrefix = ""): string {
715
- return HASHLINE_HASH_EXAMPLES.map(e => `"${linePrefix}${e}"`).join(", ");
715
+ return HL_HASH_EXAMPLES.map(e => `"${linePrefix}${e}"`).join(", ");
716
716
  }
717
717
 
718
718
  /**
719
- * Sentinel token that the hashline Lark grammar uses for the hash
720
- * regex source. Replaced at module-load time by {@link resolveLarkLidPlaceholders}
721
- * so the grammar is re-derived from a single source of truth alongside its
722
- * TypeScript consumers. Update the placeholder name here and in the grammar together.
719
+ * Substitute every grammar placeholder with the value derived from its
720
+ * TypeScript counterpart. Grammars that don't reference these placeholders
721
+ * pass through unchanged.
723
722
  */
724
- export const LARK_LID_HASH_PLACEHOLDER = "$HASHFMT$";
723
+ export function resolveHashlineGrammarPlaceholders(grammar: string): string {
724
+ return grammar.replaceAll("$HFMT$", "[a-z]{2}").replaceAll("$HSEP$", JSON.stringify(HL_EDIT_SEP));
725
+ }
726
+
727
+ /** @deprecated Use {@link resolveHashlineGrammarPlaceholders}. */
728
+ export const resolveLarkLidPlaceholders = resolveHashlineGrammarPlaceholders;
729
+
730
+ const regexEscape = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
725
731
 
726
732
  /**
727
- * Substitute the LID hash placeholder in a Lark grammar text with the
728
- * `[a-z]{2}` hash regex source. Grammars that don't reference Lids pass
729
- * through unchanged.
733
+ * Single source of truth for the hashline edit payload separator. This is the
734
+ * configured separator that starts inserted/replacement payload lines in
735
+ * hashline edit input (`<separator>TEXT`) and separates inline modify ops from
736
+ * their appended/prepended text.
737
+ *
738
+ * Override at runtime with the `PI_HL_SEP` env var (e.g.
739
+ * `PI_HL_SEP=">"`, `PI_HL_SEP="\\"`). The value is read once at module load;
740
+ * the edit grammar, prompt helper, and edit parser derive from it.
741
+ *
742
+ * Default is `~`, chosen empirically. Benchmark across 8 candidate separators
743
+ * x 3 models (glm-4.7:nitro, gpt-5.4-nano, claude-sonnet-4-6), 24-48 runs per
744
+ * cell, hashline variant, 12 sampled tasks per run:
745
+ *
746
+ * sep | task ✓ | edit ✓ | patch fail | tok/run
747
+ * ----|--------|--------|-----------------|--------
748
+ * + | 70.8% | 78.0% | 27/125 (21.6%) | 32,127
749
+ * ÷ | 70.7% | 90.6% | 22/211 (10.4%) | 31,666
750
+ * ~ | 69.4% | 94.9% | 6/107 ( 5.6%) | 30,529 <-- default
751
+ * > | 69.2% | 91.5% | 21/219 ( 9.6%) | 30,777
752
+ * : | 66.7% | 86.4% | 20/126 (15.9%) | 33,900
753
+ * | | 65.9% | 86.9% | 20/127 (15.7%) | 34,589
754
+ * \ | 65.5% | 89.8% | 16/124 (12.9%) | 36,010
755
+ * % | 63.9% | 92.8% | 11/125 ( 8.8%) | 36,530
756
+ *
757
+ * `~` wins because:
758
+ * - highest edit-tool success rate (94.9%) of any tested separator
759
+ * - lowest patch-failure rate (5.6%) — model rarely emits a malformed payload
760
+ * - cheapest in tokens alongside `>` (no retry overhead from format collisions)
761
+ * - no line-leading role in any mainstream language, markdown, diff, regex,
762
+ * or shell, so payload lines are unambiguous to both the parser and models
763
+ * - task-success is statistically tied with `>` and `÷` (within run-to-run
764
+ * noise), so the edit-reliability win is free
765
+ *
766
+ * `+` and `÷` lead on raw task-success but at the cost of ~2-4x more patch
767
+ * failures (the model retries until it lands a valid edit). `:`, `|`, `\`
768
+ * collide with line-leading syntax (label/object-key, body separator, escape)
769
+ * and degrade both edit reliability and intent-match.
730
770
  */
731
- export function resolveLarkLidPlaceholders(grammar: string): string {
732
- return grammar.replaceAll(LARK_LID_HASH_PLACEHOLDER, "[a-z]{2}");
733
- }
771
+ export const HL_EDIT_SEP = (() => {
772
+ const sep = process.env.PI_HL_SEP?.trim();
773
+ return sep?.length === 1 ? sep : "~";
774
+ })();
775
+
776
+ /** Regex-escaped form of {@link HL_EDIT_SEP}, safe for regexes. */
777
+ export const HL_EDIT_SEP_RE_RAW = regexEscape(HL_EDIT_SEP);
778
+
779
+ /** Stable separator for read/search/hashline display output. Intentionally not configurable. */
780
+ export const HL_BODY_SEP = "|";
734
781
 
735
- export const HASHLINE_CONTENT_SEPARATOR = "|";
782
+ /** Regex-escaped form of {@link HL_BODY_SEP}, safe for embedding inside a regex. */
783
+ export const HL_BODY_SEP_RE_RAW = regexEscape(HL_BODY_SEP);
736
784
 
737
785
  const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
738
786
 
739
787
  /**
740
788
  * Compute a 2-character hash of a single line via xxHash32 mod 647 over
741
- * {@link HASHLINE_BIGRAMS}. Lines with no letter or digit (e.g. bare `}`,
789
+ * {@link HL_BIGRAMS}. Lines with no letter or digit (e.g. bare `}`,
742
790
  * bare `{`) mix the line number into the seed so adjacent identical
743
791
  * brace-only lines get distinct hashes; lines with significant content stay
744
792
  * line-number-independent so a line is identifiable across small shifts.
@@ -748,7 +796,7 @@ const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
748
796
  export function computeLineHash(idx: number, line: string): string {
749
797
  line = line.replace(/\r/g, "").trimEnd();
750
798
  const seed = RE_SIGNIFICANT.test(line) ? 0 : idx;
751
- return HASHLINE_BIGRAMS[Bun.hash.xxHash32(line, seed) % HASHLINE_BIGRAMS_COUNT];
799
+ return HL_BIGRAMS[Bun.hash.xxHash32(line, seed) % HL_BIGRAMS_COUNT];
752
800
  }
753
801
 
754
802
  /**
@@ -765,7 +813,7 @@ export function formatLineHash(line: number, lines: string): string {
765
813
  * Returns `LINE+ID|TEXT` (e.g., `42sr|function hi() {`, `3ab|}`).
766
814
  */
767
815
  export function formatHashLine(lineNumber: number, line: string): string {
768
- return `${lineNumber}${computeLineHash(lineNumber, line)}${HASHLINE_CONTENT_SEPARATOR}${line}`;
816
+ return `${lineNumber}${computeLineHash(lineNumber, line)}${HL_BODY_SEP}${line}`;
769
817
  }
770
818
 
771
819
  /**
@@ -1,4 +1,5 @@
1
1
  %import common.LF
2
+ %import common.WS_INLINE
2
3
 
3
4
  start: section+
4
5
 
@@ -6,21 +7,27 @@ section: file_header line_op*
6
7
 
7
8
  file_header: "@" path LF
8
9
 
9
- line_op: insert_before_op payload+
10
+ line_op: inline_before_op payload*
11
+ | inline_after_op payload*
12
+ | insert_before_op payload+
10
13
  | insert_after_op payload+
11
14
  | replace_op payload*
12
15
  | delete_op
13
16
  | blank
14
17
 
18
+ inline_before_op: "<" LID $HSEP$ line_text? LF
19
+ inline_after_op: "+" LID $HSEP$ line_text? LF
15
20
  insert_before_op: "<" insert_target LF
16
21
  insert_after_op: "+" insert_target LF
17
22
  replace_op: "=" range LF
18
23
  delete_op: "-" range LF
19
- payload: "|" /[^\r\n]*/ LF
24
+ payload: $HSEP$ line_text? LF
25
+
26
+ line_text: /[^\r\n]+/
20
27
 
21
28
  insert_target: LID | "EOF" | "BOF"
22
29
  range: LID (".." LID)?
23
30
 
24
31
  path: /(?:[^\s\r\n]+|"[^"\r\n]+"|'[^'\r\n]+')/
25
- LID: /[1-9][0-9]*$HASHFMT$/
32
+ LID: /[1-9][0-9]*$HFMT$/
26
33
  blank: LF