@oh-my-pi/pi-coding-agent 13.18.0 → 14.0.2

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 (235) hide show
  1. package/CHANGELOG.md +316 -1
  2. package/package.json +86 -24
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +116 -30
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +123 -178
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -8
  28. package/src/commit/agentic/index.ts +22 -26
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/tools/git-file-diff.ts +3 -6
  31. package/src/commit/agentic/tools/git-hunk.ts +3 -3
  32. package/src/commit/agentic/tools/git-overview.ts +6 -9
  33. package/src/commit/agentic/tools/index.ts +6 -8
  34. package/src/commit/agentic/tools/propose-commit.ts +4 -7
  35. package/src/commit/agentic/tools/recent-commits.ts +3 -3
  36. package/src/commit/agentic/tools/split-commit.ts +4 -4
  37. package/src/commit/agentic/validation.ts +1 -1
  38. package/src/commit/analysis/conventional.ts +4 -4
  39. package/src/commit/analysis/summary.ts +3 -3
  40. package/src/commit/changelog/generate.ts +4 -4
  41. package/src/commit/changelog/index.ts +5 -9
  42. package/src/commit/map-reduce/map-phase.ts +4 -4
  43. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  44. package/src/commit/pipeline.ts +13 -16
  45. package/src/config/keybindings.ts +7 -6
  46. package/src/config/prompt-templates.ts +44 -226
  47. package/src/config/resolve-config-value.ts +4 -2
  48. package/src/config/settings-schema.ts +98 -2
  49. package/src/config/settings.ts +25 -26
  50. package/src/dap/client.ts +674 -0
  51. package/src/dap/config.ts +150 -0
  52. package/src/dap/defaults.json +211 -0
  53. package/src/dap/index.ts +4 -0
  54. package/src/dap/session.ts +1255 -0
  55. package/src/dap/types.ts +600 -0
  56. package/src/debug/log-viewer.ts +3 -2
  57. package/src/discovery/builtin.ts +1 -2
  58. package/src/discovery/codex.ts +2 -2
  59. package/src/discovery/github.ts +2 -1
  60. package/src/discovery/helpers.ts +2 -2
  61. package/src/discovery/opencode.ts +2 -2
  62. package/src/edit/diff.ts +818 -0
  63. package/src/edit/index.ts +309 -0
  64. package/src/edit/line-hash.ts +67 -0
  65. package/src/edit/modes/chunk.ts +454 -0
  66. package/src/{patch → edit/modes}/hashline.ts +741 -361
  67. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  68. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  69. package/src/{patch → edit}/normalize.ts +97 -76
  70. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  71. package/src/exec/bash-executor.ts +4 -2
  72. package/src/exec/idle-timeout-watchdog.ts +126 -0
  73. package/src/exec/non-interactive-env.ts +5 -0
  74. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +6 -18
  75. package/src/extensibility/custom-commands/bundled/review/index.ts +45 -43
  76. package/src/extensibility/custom-commands/loader.ts +1 -2
  77. package/src/extensibility/custom-tools/loader.ts +34 -11
  78. package/src/extensibility/custom-tools/types.ts +1 -1
  79. package/src/extensibility/extensions/loader.ts +9 -4
  80. package/src/extensibility/extensions/runner.ts +24 -1
  81. package/src/extensibility/extensions/types.ts +4 -2
  82. package/src/extensibility/hooks/loader.ts +5 -6
  83. package/src/extensibility/hooks/types.ts +2 -2
  84. package/src/extensibility/plugins/doctor.ts +2 -1
  85. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -57
  86. package/src/extensibility/plugins/marketplace/source-resolver.ts +4 -4
  87. package/src/extensibility/slash-commands.ts +3 -7
  88. package/src/index.ts +3 -1
  89. package/src/internal-urls/docs-index.generated.ts +11 -11
  90. package/src/ipy/executor.ts +58 -17
  91. package/src/ipy/gateway-coordinator.ts +6 -4
  92. package/src/ipy/kernel.ts +45 -22
  93. package/src/ipy/runtime.ts +2 -2
  94. package/src/lsp/client.ts +7 -4
  95. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  96. package/src/lsp/config.ts +2 -2
  97. package/src/lsp/defaults.json +688 -154
  98. package/src/lsp/index.ts +234 -45
  99. package/src/lsp/lspmux.ts +2 -2
  100. package/src/lsp/startup-events.ts +13 -0
  101. package/src/lsp/types.ts +12 -1
  102. package/src/lsp/utils.ts +8 -1
  103. package/src/main.ts +125 -47
  104. package/src/memories/index.ts +4 -5
  105. package/src/modes/acp/acp-agent.ts +563 -163
  106. package/src/modes/acp/acp-event-mapper.ts +9 -1
  107. package/src/modes/acp/acp-mode.ts +4 -2
  108. package/src/modes/components/agent-dashboard.ts +3 -4
  109. package/src/modes/components/diff.ts +6 -7
  110. package/src/modes/components/footer.ts +9 -29
  111. package/src/modes/components/hook-editor.ts +3 -3
  112. package/src/modes/components/hook-selector.ts +6 -1
  113. package/src/modes/components/read-tool-group.ts +6 -12
  114. package/src/modes/components/session-observer-overlay.ts +472 -0
  115. package/src/modes/components/settings-defs.ts +24 -0
  116. package/src/modes/components/status-line.ts +15 -61
  117. package/src/modes/components/tool-execution.ts +1 -1
  118. package/src/modes/components/welcome.ts +1 -1
  119. package/src/modes/controllers/btw-controller.ts +2 -2
  120. package/src/modes/controllers/command-controller.ts +4 -2
  121. package/src/modes/controllers/event-controller.ts +59 -2
  122. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  123. package/src/modes/controllers/input-controller.ts +15 -8
  124. package/src/modes/controllers/selector-controller.ts +26 -0
  125. package/src/modes/index.ts +20 -2
  126. package/src/modes/interactive-mode.ts +278 -69
  127. package/src/modes/rpc/host-tools.ts +186 -0
  128. package/src/modes/rpc/rpc-client.ts +178 -13
  129. package/src/modes/rpc/rpc-mode.ts +73 -3
  130. package/src/modes/rpc/rpc-types.ts +53 -1
  131. package/src/modes/session-observer-registry.ts +146 -0
  132. package/src/modes/shared.ts +0 -42
  133. package/src/modes/theme/theme.ts +80 -8
  134. package/src/modes/types.ts +4 -2
  135. package/src/modes/utils/keybinding-matchers.ts +9 -0
  136. package/src/prompts/system/custom-system-prompt.md +5 -0
  137. package/src/prompts/system/system-prompt.md +8 -1
  138. package/src/prompts/tools/chunk-edit.md +219 -0
  139. package/src/prompts/tools/debug.md +43 -0
  140. package/src/prompts/tools/grep.md +3 -0
  141. package/src/prompts/tools/lsp.md +5 -5
  142. package/src/prompts/tools/read-chunk.md +17 -0
  143. package/src/prompts/tools/read.md +19 -5
  144. package/src/sdk.ts +216 -165
  145. package/src/secrets/index.ts +1 -1
  146. package/src/secrets/obfuscator.ts +25 -17
  147. package/src/session/agent-session.ts +381 -286
  148. package/src/session/agent-storage.ts +12 -12
  149. package/src/session/compaction/branch-summarization.ts +3 -3
  150. package/src/session/compaction/compaction.ts +5 -6
  151. package/src/session/compaction/utils.ts +3 -3
  152. package/src/session/history-storage.ts +62 -19
  153. package/src/session/messages.ts +3 -3
  154. package/src/session/session-dump-format.ts +203 -0
  155. package/src/session/session-manager.ts +15 -5
  156. package/src/session/session-storage.ts +4 -2
  157. package/src/session/streaming-output.ts +1 -1
  158. package/src/session/tool-choice-queue.ts +213 -0
  159. package/src/slash-commands/builtin-registry.ts +56 -8
  160. package/src/ssh/connection-manager.ts +2 -2
  161. package/src/ssh/sshfs-mount.ts +5 -5
  162. package/src/stt/downloader.ts +4 -4
  163. package/src/stt/recorder.ts +4 -4
  164. package/src/stt/transcriber.ts +2 -2
  165. package/src/system-prompt.ts +25 -13
  166. package/src/task/agents.ts +5 -6
  167. package/src/task/commands.ts +2 -5
  168. package/src/task/executor.ts +32 -4
  169. package/src/task/index.ts +91 -82
  170. package/src/task/template.ts +2 -2
  171. package/src/task/types.ts +25 -0
  172. package/src/task/worktree.ts +131 -149
  173. package/src/tools/ask.ts +2 -3
  174. package/src/tools/ast-edit.ts +7 -7
  175. package/src/tools/ast-grep.ts +7 -7
  176. package/src/tools/auto-generated-guard.ts +36 -41
  177. package/src/tools/await-tool.ts +2 -2
  178. package/src/tools/bash.ts +5 -23
  179. package/src/tools/browser.ts +4 -5
  180. package/src/tools/calculator.ts +2 -3
  181. package/src/tools/cancel-job.ts +2 -2
  182. package/src/tools/checkpoint.ts +3 -3
  183. package/src/tools/debug.ts +1007 -0
  184. package/src/tools/exit-plan-mode.ts +3 -3
  185. package/src/tools/fetch.ts +67 -3
  186. package/src/tools/find.ts +4 -5
  187. package/src/tools/fs-cache-invalidation.ts +5 -0
  188. package/src/tools/gemini-image.ts +13 -5
  189. package/src/tools/gh.ts +130 -308
  190. package/src/tools/grep.ts +57 -9
  191. package/src/tools/index.ts +44 -22
  192. package/src/tools/inspect-image.ts +4 -4
  193. package/src/tools/output-meta.ts +1 -1
  194. package/src/tools/python.ts +19 -6
  195. package/src/tools/read.ts +211 -146
  196. package/src/tools/render-mermaid.ts +2 -3
  197. package/src/tools/render-utils.ts +20 -6
  198. package/src/tools/renderers.ts +3 -1
  199. package/src/tools/report-tool-issue.ts +80 -0
  200. package/src/tools/resolve.ts +70 -39
  201. package/src/tools/search-tool-bm25.ts +2 -2
  202. package/src/tools/ssh.ts +2 -2
  203. package/src/tools/todo-write.ts +2 -2
  204. package/src/tools/tool-timeouts.ts +1 -0
  205. package/src/tools/write.ts +5 -6
  206. package/src/tui/tree-list.ts +3 -1
  207. package/src/utils/clipboard.ts +80 -0
  208. package/src/utils/commit-message-generator.ts +2 -3
  209. package/src/utils/edit-mode.ts +49 -0
  210. package/src/utils/external-editor.ts +11 -5
  211. package/src/utils/file-display-mode.ts +6 -5
  212. package/src/utils/file-mentions.ts +8 -7
  213. package/src/utils/git.ts +1400 -0
  214. package/src/utils/image-loading.ts +98 -0
  215. package/src/utils/title-generator.ts +2 -3
  216. package/src/utils/tools-manager.ts +6 -6
  217. package/src/web/scrapers/choosealicense.ts +1 -1
  218. package/src/web/search/index.ts +3 -3
  219. package/src/web/search/render.ts +6 -4
  220. package/src/autoresearch/command-initialize.md +0 -34
  221. package/src/commit/git/errors.ts +0 -9
  222. package/src/commit/git/index.ts +0 -210
  223. package/src/commit/git/operations.ts +0 -54
  224. package/src/patch/diff.ts +0 -433
  225. package/src/patch/index.ts +0 -888
  226. package/src/patch/parser.ts +0 -532
  227. package/src/patch/types.ts +0 -292
  228. package/src/prompts/agents/oracle.md +0 -77
  229. package/src/tools/gh-cli.ts +0 -125
  230. package/src/tools/pending-action.ts +0 -49
  231. package/src/utils/child-process.ts +0 -88
  232. package/src/utils/frontmatter.ts +0 -117
  233. package/src/utils/image-input.ts +0 -274
  234. package/src/utils/mime.ts +0 -53
  235. package/src/utils/prompt-format.ts +0 -170
@@ -1,8 +1,8 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import type { CommitAgentState, GitOverviewSnapshot } from "../../../commit/agentic/state";
3
3
  import { extractScopeCandidates } from "../../../commit/analysis/scope";
4
- import type { ControlledGit } from "../../../commit/git";
5
4
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
5
+ import * as git from "../../../utils/git";
6
6
 
7
7
  const EXCLUDED_LOCK_FILES = new Set([
8
8
  "Cargo.lock",
@@ -47,10 +47,7 @@ const gitOverviewSchema = Type.Object({
47
47
  include_untracked: Type.Optional(Type.Boolean({ description: "Include untracked files when staged=false" })),
48
48
  });
49
49
 
50
- export function createGitOverviewTool(
51
- git: ControlledGit,
52
- state: CommitAgentState,
53
- ): CustomTool<typeof gitOverviewSchema> {
50
+ export function createGitOverviewTool(cwd: string, state: CommitAgentState): CustomTool<typeof gitOverviewSchema> {
54
51
  return {
55
52
  name: "git_overview",
56
53
  label: "Git Overview",
@@ -58,13 +55,13 @@ export function createGitOverviewTool(
58
55
  parameters: gitOverviewSchema,
59
56
  async execute(_toolCallId, params) {
60
57
  const staged = params.staged ?? true;
61
- const allFiles = staged ? await git.getStagedFiles() : await git.getChangedFiles(false);
58
+ const allFiles = await git.diff.changedFiles(cwd, { cached: staged });
62
59
  const { filtered: files, excluded } = filterExcludedFiles(allFiles);
63
- const stat = await git.getStat(staged);
64
- const allNumstat = await git.getNumstat(staged);
60
+ const stat = await git.diff(cwd, { stat: true, cached: staged });
61
+ const allNumstat = await git.diff.numstat(cwd, { cached: staged });
65
62
  const numstat = allNumstat.filter(entry => !isExcludedFile(entry.path));
66
63
  const scopeResult = extractScopeCandidates(numstat);
67
- const untrackedFiles = !staged && params.include_untracked ? await git.getUntrackedFiles() : undefined;
64
+ const untrackedFiles = !staged && params.include_untracked ? await git.ls.untracked(cwd) : undefined;
68
65
  const snapshot: GitOverviewSnapshot = {
69
66
  files,
70
67
  stat,
@@ -1,5 +1,4 @@
1
1
  import type { CommitAgentState } from "../../../commit/agentic/state";
2
- import type { ControlledGit } from "../../../commit/git";
3
2
  import type { ModelRegistry } from "../../../config/model-registry";
4
3
  import type { Settings } from "../../../config/settings";
5
4
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
@@ -15,7 +14,6 @@ import { createSplitCommitTool } from "./split-commit";
15
14
 
16
15
  export interface CommitToolOptions {
17
16
  cwd: string;
18
- git: ControlledGit;
19
17
  authStorage: AuthStorage;
20
18
  modelRegistry: ModelRegistry;
21
19
  settings: Settings;
@@ -27,10 +25,10 @@ export interface CommitToolOptions {
27
25
 
28
26
  export function createCommitTools(options: CommitToolOptions): Array<CustomTool<any, any>> {
29
27
  const tools: Array<CustomTool<any, any>> = [
30
- createGitOverviewTool(options.git, options.state),
31
- createGitFileDiffTool(options.git, options.state),
32
- createGitHunkTool(options.git),
33
- createRecentCommitsTool(options.git),
28
+ createGitOverviewTool(options.cwd, options.state),
29
+ createGitFileDiffTool(options.cwd, options.state),
30
+ createGitHunkTool(options.cwd),
31
+ createRecentCommitsTool(options.cwd),
34
32
  ];
35
33
 
36
34
  if (options.enableAnalyzeFiles ?? true) {
@@ -48,8 +46,8 @@ export function createCommitTools(options: CommitToolOptions): Array<CustomTool<
48
46
 
49
47
  tools.push(
50
48
  createProposeChangelogTool(options.state, options.changelogTargets),
51
- createProposeCommitTool(options.git, options.state),
52
- createSplitCommitTool(options.git, options.state, options.changelogTargets),
49
+ createProposeCommitTool(options.cwd, options.state),
50
+ createSplitCommitTool(options.cwd, options.state, options.changelogTargets),
53
51
  );
54
52
 
55
53
  return tools;
@@ -9,9 +9,9 @@ import {
9
9
  validateTypeConsistency,
10
10
  } from "../../../commit/agentic/validation";
11
11
  import { validateAnalysis } from "../../../commit/analysis/validation";
12
- import type { ControlledGit } from "../../../commit/git";
13
12
  import type { CommitType, ConventionalAnalysis, ConventionalDetail } from "../../../commit/types";
14
13
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
14
+ import * as git from "../../../utils/git";
15
15
  import { commitTypeSchema, detailSchema } from "./schemas.js";
16
16
 
17
17
  const proposeCommitSchema = Type.Object({
@@ -49,10 +49,7 @@ function normalizeDetails(
49
49
  }));
50
50
  }
51
51
 
52
- export function createProposeCommitTool(
53
- git: ControlledGit,
54
- state: CommitAgentState,
55
- ): CustomTool<typeof proposeCommitSchema> {
52
+ export function createProposeCommitTool(cwd: string, state: CommitAgentState): CustomTool<typeof proposeCommitSchema> {
56
53
  return {
57
54
  name: "propose_commit",
58
55
  label: "Propose Commit",
@@ -72,8 +69,8 @@ export function createProposeCommitTool(
72
69
 
73
70
  const summaryValidation = validateSummaryRules(summary);
74
71
  const analysisValidation = validateAnalysis(analysis);
75
- const stagedFiles = state.overview?.files ?? (await git.getStagedFiles());
76
- const diffText = state.diffText ?? (await git.getDiff(true));
72
+ const stagedFiles = state.overview?.files ?? (await git.diff.changedFiles(cwd, { cached: true }));
73
+ const diffText = state.diffText ?? (await git.diff(cwd, { cached: true }));
77
74
  const typeValidation = validateTypeConsistency(params.type, stagedFiles, {
78
75
  diffText,
79
76
  summary,
@@ -1,6 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
- import type { ControlledGit } from "../../../commit/git";
3
2
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
3
+ import * as git from "../../../utils/git";
4
4
 
5
5
  const recentCommitsSchema = Type.Object({
6
6
  count: Type.Optional(Type.Number({ description: "Number of commits to fetch", minimum: 1, maximum: 50 })),
@@ -25,7 +25,7 @@ function extractScope(subject: string): string | null {
25
25
  return match?.[1]?.trim() ?? null;
26
26
  }
27
27
 
28
- export function createRecentCommitsTool(git: ControlledGit): CustomTool<typeof recentCommitsSchema> {
28
+ export function createRecentCommitsTool(cwd: string): CustomTool<typeof recentCommitsSchema> {
29
29
  return {
30
30
  name: "recent_commits",
31
31
  label: "Recent Commits",
@@ -33,7 +33,7 @@ export function createRecentCommitsTool(git: ControlledGit): CustomTool<typeof r
33
33
  parameters: recentCommitsSchema,
34
34
  async execute(_toolCallId, params) {
35
35
  const count = params.count ?? 8;
36
- const commits = await git.getRecentCommits(count);
36
+ const commits = await git.log.subjects(cwd, count);
37
37
  const verbs: Record<string, number> = {};
38
38
  const scopes: Record<string, number> = {};
39
39
  const lengths: number[] = [];
@@ -10,9 +10,9 @@ import {
10
10
  validateTypeConsistency,
11
11
  } from "../../../commit/agentic/validation";
12
12
  import { validateScope } from "../../../commit/analysis/validation";
13
- import type { ControlledGit } from "../../../commit/git";
14
13
  import type { ConventionalDetail } from "../../../commit/types";
15
14
  import type { CustomTool } from "../../../extensibility/custom-tools/types";
15
+ import * as git from "../../../utils/git";
16
16
  import { commitTypeSchema, detailSchema } from "./schemas.js";
17
17
 
18
18
  const hunkSelectorSchema = Type.Union([
@@ -64,7 +64,7 @@ function normalizeDetails(
64
64
  }
65
65
 
66
66
  export function createSplitCommitTool(
67
- git: ControlledGit,
67
+ cwd: string,
68
68
  state: CommitAgentState,
69
69
  changelogTargets: string[],
70
70
  ): CustomTool<typeof splitCommitSchema> {
@@ -74,13 +74,13 @@ export function createSplitCommitTool(
74
74
  description: "Propose multiple atomic commits for unrelated changes.",
75
75
  parameters: splitCommitSchema,
76
76
  async execute(_toolCallId, params) {
77
- const stagedFiles = state.overview?.files ?? (await git.getStagedFiles());
77
+ const stagedFiles = state.overview?.files ?? (await git.diff.changedFiles(cwd, { cached: true }));
78
78
  const stagedSet = new Set(stagedFiles);
79
79
  const changelogSet = new Set(changelogTargets);
80
80
  const usedFiles = new Set<string>();
81
81
  const errors: string[] = [];
82
82
  const warnings: string[] = [];
83
- const diffText = await git.getDiff(true);
83
+ const diffText = await git.diff(cwd, { cached: true });
84
84
 
85
85
  const commits: SplitCommitGroup[] = params.commits.map((commit, index) => {
86
86
  const scope = commit.scope?.trim() || null;
@@ -1,7 +1,7 @@
1
1
  import { stripTypePrefix } from "../../commit/analysis/summary";
2
2
  import { validateSummary } from "../../commit/analysis/validation";
3
3
  import type { CommitType, ConventionalDetail } from "../../commit/types";
4
- import { normalizeUnicode } from "../../patch/normalize";
4
+ import { normalizeUnicode } from "../../edit/normalize";
5
5
 
6
6
  export const SUMMARY_MAX_CHARS = 72;
7
7
  export const MAX_DETAIL_ITEMS = 6;
@@ -1,11 +1,11 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
+ import { prompt } from "@oh-my-pi/pi-utils";
4
5
  import { Type } from "@sinclair/typebox";
5
6
  import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with { type: "text" };
6
7
  import analysisUserPrompt from "../../commit/prompts/analysis-user.md" with { type: "text" };
7
8
  import type { ChangelogCategory, ConventionalAnalysis } from "../../commit/types";
8
- import { renderPromptTemplate } from "../../config/prompt-templates";
9
9
  import { toReasoningEffort } from "../../thinking";
10
10
  import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
11
11
 
@@ -76,7 +76,7 @@ export async function generateConventionalAnalysis({
76
76
  stat,
77
77
  diff,
78
78
  }: ConventionalAnalysisInput): Promise<ConventionalAnalysis> {
79
- const prompt = renderPromptTemplate(analysisUserPrompt, {
79
+ const userContent = prompt.render(analysisUserPrompt, {
80
80
  context_files: contextFiles && contextFiles.length > 0 ? contextFiles : undefined,
81
81
  user_context: userContext,
82
82
  types_description: typesDescription,
@@ -89,8 +89,8 @@ export async function generateConventionalAnalysis({
89
89
  const response = await completeSimple(
90
90
  model,
91
91
  {
92
- systemPrompt: renderPromptTemplate(analysisSystemPrompt),
93
- messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
92
+ systemPrompt: prompt.render(analysisSystemPrompt),
93
+ messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
94
94
  tools: [ConventionalAnalysisTool],
95
95
  },
96
96
  { apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
@@ -1,11 +1,11 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
+ import { prompt } from "@oh-my-pi/pi-utils";
4
5
  import { Type } from "@sinclair/typebox";
5
6
  import summarySystemPrompt from "../../commit/prompts/summary-system.md" with { type: "text" };
6
7
  import summaryUserPrompt from "../../commit/prompts/summary-user.md" with { type: "text" };
7
8
  import type { CommitSummary } from "../../commit/types";
8
- import { renderPromptTemplate } from "../../config/prompt-templates";
9
9
  import { toReasoningEffort } from "../../thinking";
10
10
  import { extractTextContent, extractToolCall } from "../utils";
11
11
 
@@ -44,7 +44,7 @@ export async function generateSummary({
44
44
  userContext,
45
45
  }: SummaryInput): Promise<CommitSummary> {
46
46
  const systemPrompt = renderSummaryPrompt({ commitType, scope, maxChars });
47
- const userPrompt = renderPromptTemplate(summaryUserPrompt, {
47
+ const userPrompt = prompt.render(summaryUserPrompt, {
48
48
  user_context: userContext,
49
49
  details: details.join("\n"),
50
50
  stat,
@@ -73,7 +73,7 @@ function renderSummaryPrompt({
73
73
  maxChars: number;
74
74
  }): string {
75
75
  const scopePrefix = scope ? `(${scope})` : "";
76
- return renderPromptTemplate(summarySystemPrompt, {
76
+ return prompt.render(summarySystemPrompt, {
77
77
  commit_type: commitType,
78
78
  scope_prefix: scopePrefix,
79
79
  chars: String(maxChars),
@@ -1,11 +1,11 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
+ import { prompt } from "@oh-my-pi/pi-utils";
4
5
  import { type TSchema, Type } from "@sinclair/typebox";
5
6
  import changelogSystemPrompt from "../../commit/prompts/changelog-system.md" with { type: "text" };
6
7
  import changelogUserPrompt from "../../commit/prompts/changelog-user.md" with { type: "text" };
7
8
  import { CHANGELOG_CATEGORIES, type ChangelogCategory, type ChangelogGenerationResult } from "../../commit/types";
8
- import { renderPromptTemplate } from "../../config/prompt-templates";
9
9
  import { toReasoningEffort } from "../../thinking";
10
10
  import { extractTextContent, extractToolCall, parseJsonPayload } from "../utils";
11
11
 
@@ -48,7 +48,7 @@ export async function generateChangelogEntries({
48
48
  stat,
49
49
  diff,
50
50
  }: ChangelogPromptInput): Promise<ChangelogGenerationResult> {
51
- const prompt = renderPromptTemplate(changelogUserPrompt, {
51
+ const userContent = prompt.render(changelogUserPrompt, {
52
52
  changelog_path: changelogPath,
53
53
  is_package_changelog: isPackageChangelog,
54
54
  existing_entries: existingEntries,
@@ -58,8 +58,8 @@ export async function generateChangelogEntries({
58
58
  const response = await completeSimple(
59
59
  model,
60
60
  {
61
- systemPrompt: renderPromptTemplate(changelogSystemPrompt),
62
- messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
61
+ systemPrompt: prompt.render(changelogSystemPrompt),
62
+ messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
63
63
  tools: [changelogTool],
64
64
  },
65
65
  { apiKey, maxTokens: 1200, reasoning: toReasoningEffort(thinkingLevel) },
@@ -2,8 +2,8 @@ import * as path from "node:path";
2
2
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
3
  import type { Api, Model } from "@oh-my-pi/pi-ai";
4
4
  import { logger } from "@oh-my-pi/pi-utils";
5
- import type { ControlledGit } from "../../commit/git";
6
5
  import { CHANGELOG_CATEGORIES } from "../../commit/types";
6
+ import * as git from "../../utils/git";
7
7
  import { detectChangelogBoundaries } from "./detect";
8
8
  import { generateChangelogEntries } from "./generate";
9
9
  import { parseUnreleasedSection } from "./parse";
@@ -13,7 +13,6 @@ const CHANGELOG_SECTIONS = CHANGELOG_CATEGORIES;
13
13
  const DEFAULT_MAX_DIFF_CHARS = 120_000;
14
14
 
15
15
  export interface ChangelogFlowInput {
16
- git: ControlledGit;
17
16
  cwd: string;
18
17
  model: Model<Api>;
19
18
  apiKey: string;
@@ -25,7 +24,6 @@ export interface ChangelogFlowInput {
25
24
  }
26
25
 
27
26
  export interface ChangelogProposalInput {
28
- git: ControlledGit;
29
27
  cwd: string;
30
28
  proposals: Array<{
31
29
  path: string;
@@ -40,7 +38,6 @@ export interface ChangelogProposalInput {
40
38
  * Update CHANGELOG.md entries for staged changes.
41
39
  */
42
40
  export async function runChangelogFlow({
43
- git,
44
41
  cwd,
45
42
  model,
46
43
  apiKey,
@@ -58,9 +55,9 @@ export async function runChangelogFlow({
58
55
  const updated: string[] = [];
59
56
  for (const boundary of boundaries) {
60
57
  onProgress?.(`Generating entries for ${boundary.changelogPath}…`);
61
- const diff = await git.getDiffForFiles(boundary.files, true);
58
+ const diff = await git.diff(cwd, { cached: true, files: boundary.files });
62
59
  if (!diff.trim()) continue;
63
- const stat = await git.getStatForFiles(boundary.files, true);
60
+ const stat = await git.diff(cwd, { stat: true, cached: true, files: boundary.files });
64
61
  const diffForPrompt = truncateDiff(diff, maxDiffChars ?? DEFAULT_MAX_DIFF_CHARS);
65
62
  const changelogContent = await Bun.file(boundary.changelogPath).text();
66
63
  let unreleased: { startLine: number; endLine: number; entries: Record<string, string[]> };
@@ -87,7 +84,7 @@ export async function runChangelogFlow({
87
84
  const updatedContent = applyChangelogEntries(changelogContent, unreleased, generated.entries);
88
85
  if (!dryRun) {
89
86
  await Bun.write(boundary.changelogPath, updatedContent);
90
- await git.stageFiles([path.relative(cwd, boundary.changelogPath)]);
87
+ await git.stage.files(cwd, [path.relative(cwd, boundary.changelogPath)]);
91
88
  }
92
89
  updated.push(boundary.changelogPath);
93
90
  }
@@ -99,7 +96,6 @@ export async function runChangelogFlow({
99
96
  * Apply changelog entries provided by the commit agent.
100
97
  */
101
98
  export async function applyChangelogProposals({
102
- git,
103
99
  cwd,
104
100
  proposals,
105
101
  dryRun,
@@ -132,7 +128,7 @@ export async function applyChangelogProposals({
132
128
  const updatedContent = applyChangelogEntries(changelogContent, unreleased, normalized, normalizedDeletions);
133
129
  if (!dryRun) {
134
130
  await Bun.write(proposal.path, updatedContent);
135
- await git.stageFiles([path.relative(cwd, proposal.path)]);
131
+ await git.stage.files(cwd, [path.relative(cwd, proposal.path)]);
136
132
  }
137
133
  updated.push(proposal.path);
138
134
  }
@@ -1,11 +1,11 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple } from "@oh-my-pi/pi-ai";
4
+ import { prompt } from "@oh-my-pi/pi-utils";
4
5
  import fileObserverSystemPrompt from "../../commit/prompts/file-observer-system.md" with { type: "text" };
5
6
  import fileObserverUserPrompt from "../../commit/prompts/file-observer-user.md" with { type: "text" };
6
7
  import type { FileDiff, FileObservation } from "../../commit/types";
7
8
  import { isExcludedFile } from "../../commit/utils/exclusions";
8
- import { renderPromptTemplate } from "../../config/prompt-templates";
9
9
  import { toReasoningEffort } from "../../thinking";
10
10
  import { truncateToTokenLimit } from "./utils";
11
11
 
@@ -38,7 +38,7 @@ export async function runMapPhase({
38
38
  config,
39
39
  }: MapPhaseInput): Promise<FileObservation[]> {
40
40
  const filtered = files.filter(file => !isExcludedFile(file.filename));
41
- const systemPrompt = renderPromptTemplate(fileObserverSystemPrompt);
41
+ const systemPrompt = prompt.render(fileObserverSystemPrompt);
42
42
  const maxFileTokens = config?.maxFileTokens ?? MAX_FILE_TOKENS;
43
43
  const maxConcurrency = config?.maxConcurrency ?? MAX_CONCURRENCY;
44
44
  const timeoutMs = config?.timeoutMs ?? MAP_PHASE_TIMEOUT_MS;
@@ -56,14 +56,14 @@ export async function runMapPhase({
56
56
 
57
57
  const contextHeader = generateContextHeader(filtered, file.filename);
58
58
  const truncated = truncateToTokenLimit(file.content, maxFileTokens);
59
- const prompt = renderPromptTemplate(fileObserverUserPrompt, {
59
+ const userContent = prompt.render(fileObserverUserPrompt, {
60
60
  filename: file.filename,
61
61
  diff: truncated,
62
62
  context_header: contextHeader,
63
63
  });
64
64
  const request = {
65
65
  systemPrompt,
66
- messages: [{ role: "user", content: prompt, timestamp: Date.now() }] as Message[],
66
+ messages: [{ role: "user", content: userContent, timestamp: Date.now() }] as Message[],
67
67
  };
68
68
 
69
69
  const response = await withRetry(
@@ -1,11 +1,11 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
3
  import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
4
+ import { prompt } from "@oh-my-pi/pi-utils";
4
5
  import { Type } from "@sinclair/typebox";
5
6
  import reduceSystemPrompt from "../../commit/prompts/reduce-system.md" with { type: "text" };
6
7
  import reduceUserPrompt from "../../commit/prompts/reduce-user.md" with { type: "text" };
7
8
  import type { ChangelogCategory, ConventionalAnalysis, FileObservation } from "../../commit/types";
8
- import { renderPromptTemplate } from "../../config/prompt-templates";
9
9
  import { toReasoningEffort } from "../../thinking";
10
10
  import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
11
11
 
@@ -67,7 +67,7 @@ export async function runReducePhase({
67
67
  scopeCandidates,
68
68
  typesDescription,
69
69
  }: ReducePhaseInput): Promise<ConventionalAnalysis> {
70
- const prompt = renderPromptTemplate(reduceUserPrompt, {
70
+ const userContent = prompt.render(reduceUserPrompt, {
71
71
  types_description: typesDescription,
72
72
  observations: observations.flatMap(obs => obs.observations.map(line => `- ${obs.file}: ${line}`)).join("\n"),
73
73
  stat,
@@ -76,8 +76,8 @@ export async function runReducePhase({
76
76
  const response = await completeSimple(
77
77
  model,
78
78
  {
79
- systemPrompt: renderPromptTemplate(reduceSystemPrompt),
80
- messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
79
+ systemPrompt: prompt.render(reduceSystemPrompt),
80
+ messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
81
81
  tools: [ReduceTool],
82
82
  },
83
83
  { apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
@@ -1,12 +1,12 @@
1
1
  import * as path from "node:path";
2
2
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
3
3
  import type { Api, Model } from "@oh-my-pi/pi-ai";
4
- import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
4
+ import { getProjectDir, logger, prompt } from "@oh-my-pi/pi-utils";
5
5
  import { ModelRegistry } from "../config/model-registry";
6
- import { renderPromptTemplate } from "../config/prompt-templates";
7
6
  import { Settings } from "../config/settings";
8
7
  import { discoverAuthStorage } from "../sdk";
9
8
  import { loadProjectContextFiles } from "../system-prompt";
9
+ import * as git from "../utils/git";
10
10
  import { runAgenticCommit } from "./agentic";
11
11
  import {
12
12
  extractScopeCandidates,
@@ -16,7 +16,6 @@ import {
16
16
  validateSummary,
17
17
  } from "./analysis";
18
18
  import { runChangelogFlow } from "./changelog";
19
- import { ControlledGit } from "./git";
20
19
  import { runMapReduceAnalysis, shouldUseMapReduce } from "./map-reduce";
21
20
  import { formatCommitMessage } from "./message";
22
21
  import { resolvePrimaryModel, resolveSmolModel } from "./model-selection";
@@ -26,7 +25,7 @@ import type { CommitCommandArgs, ConventionalAnalysis } from "./types";
26
25
 
27
26
  const SUMMARY_MAX_CHARS = 72;
28
27
  const RECENT_COMMITS_COUNT = 8;
29
- const TYPES_DESCRIPTION = renderPromptTemplate(typesDescriptionPrompt);
28
+ const TYPES_DESCRIPTION = prompt.render(typesDescriptionPrompt);
30
29
 
31
30
  /**
32
31
  * Execute the omp commit pipeline for staged changes.
@@ -57,12 +56,11 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
57
56
  thinkingLevel: smolThinkingLevel,
58
57
  } = await resolveSmolModel(settings, modelRegistry, primaryModel, primaryApiKey);
59
58
 
60
- const git = new ControlledGit(cwd);
61
- let stagedFiles = await git.getStagedFiles();
59
+ let stagedFiles = await git.diff.changedFiles(cwd, { cached: true });
62
60
  if (stagedFiles.length === 0) {
63
61
  process.stdout.write("No staged changes detected, staging all changes...\n");
64
- await git.stageAll();
65
- stagedFiles = await git.getStagedFiles();
62
+ await git.stage.files(cwd);
63
+ stagedFiles = await git.diff.changedFiles(cwd, { cached: true });
66
64
  }
67
65
  if (stagedFiles.length === 0) {
68
66
  process.stderr.write("No changes to commit.\n");
@@ -71,7 +69,6 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
71
69
 
72
70
  if (!args.noChangelog) {
73
71
  await runChangelogFlow({
74
- git,
75
72
  cwd,
76
73
  model: primaryModel,
77
74
  apiKey: primaryApiKey,
@@ -82,11 +79,11 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
82
79
  });
83
80
  }
84
81
 
85
- const diff = await git.getDiff(true);
86
- const stat = await git.getStat(true);
87
- const numstat = await git.getNumstat(true);
82
+ const diff = await git.diff(cwd, { cached: true });
83
+ const stat = await git.diff(cwd, { stat: true, cached: true });
84
+ const numstat = await git.diff.numstat(cwd, { cached: true });
88
85
  const scopeCandidates = extractScopeCandidates(numstat).scopeCandidates;
89
- const recentCommits = await git.getRecentCommits(RECENT_COMMITS_COUNT);
86
+ const recentCommits = await git.log.subjects(cwd, RECENT_COMMITS_COUNT);
90
87
  const contextFiles = await loadProjectContextFiles({ cwd });
91
88
  const formattedContextFiles = contextFiles.map(file => ({
92
89
  path: path.relative(cwd, file.path),
@@ -131,10 +128,10 @@ async function runLegacyCommitCommand(args: CommitCommandArgs): Promise<void> {
131
128
  return;
132
129
  }
133
130
 
134
- await git.commit(commitMessage);
131
+ await git.commit(cwd, commitMessage);
135
132
  process.stdout.write("Commit created.\n");
136
133
  if (args.push) {
137
- await git.push();
134
+ await git.push(cwd);
138
135
  process.stdout.write("Pushed to remote.\n");
139
136
  }
140
137
  }
@@ -238,7 +235,7 @@ async function generateSummaryWithRetry(input: {
238
235
  }
239
236
 
240
237
  function buildRetryContext(base: string | undefined, errors: string[]): string {
241
- return renderPromptTemplate(summaryRetryPrompt, {
238
+ return prompt.render(summaryRetryPrompt, {
242
239
  base_context: base,
243
240
  errors: errors.join("; "),
244
241
  });
@@ -37,6 +37,7 @@ interface AppKeybindings {
37
37
  "app.session.tree": true;
38
38
  "app.session.fork": true;
39
39
  "app.session.resume": true;
40
+ "app.session.observe": true;
40
41
  "app.session.togglePath": true;
41
42
  "app.session.toggleSort": true;
42
43
  "app.session.rename": true;
@@ -144,6 +145,10 @@ export const KEYBINDINGS = {
144
145
  defaultKeys: [],
145
146
  description: "Resume session",
146
147
  },
148
+ "app.session.observe": {
149
+ defaultKeys: "ctrl+s",
150
+ description: "Observe subagent sessions",
151
+ },
147
152
  "app.session.togglePath": {
148
153
  defaultKeys: "ctrl+p",
149
154
  description: "Toggle session path display",
@@ -214,6 +219,7 @@ const KEYBINDING_NAME_MIGRATIONS = {
214
219
  tree: "app.session.tree",
215
220
  fork: "app.session.fork",
216
221
  resume: "app.session.resume",
222
+ observeSessions: "app.session.observe",
217
223
  toggleSTT: "app.stt.toggle",
218
224
  // TUI editor (old names for backward compatibility)
219
225
  cursorUp: "tui.editor.cursorUp",
@@ -260,9 +266,6 @@ function isLegacyKeybindingName(key: string): key is keyof typeof KEYBINDING_NAM
260
266
  return key in KEYBINDING_NAME_MIGRATIONS;
261
267
  }
262
268
 
263
- /**
264
- * Normalize input to KeybindingsConfig, validating types.
265
- */
266
269
  function toKeybindingsConfig(value: unknown): KeybindingsConfig {
267
270
  if (typeof value !== "object" || value === null) {
268
271
  return {};
@@ -270,15 +273,13 @@ function toKeybindingsConfig(value: unknown): KeybindingsConfig {
270
273
 
271
274
  const config: KeybindingsConfig = {};
272
275
  for (const [key, val] of Object.entries(value)) {
273
- // Allow undefined, string (KeyId), or array of strings
274
276
  if (val === undefined) {
275
277
  config[key] = undefined;
276
278
  } else if (typeof val === "string") {
277
279
  config[key] = val as KeyId;
278
280
  } else if (Array.isArray(val) && val.every(v => typeof v === "string")) {
279
- config[key] = val as string[] as KeyId[];
281
+ config[key] = val as KeyId[];
280
282
  }
281
- // Silently skip invalid entries
282
283
  }
283
284
  return config;
284
285
  }