@oh-my-pi/pi-coding-agent 8.0.16 → 8.1.0

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 (166) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/package.json +14 -11
  3. package/scripts/generate-wasm-b64.ts +24 -0
  4. package/src/capability/context-file.ts +1 -1
  5. package/src/capability/extension-module.ts +1 -1
  6. package/src/capability/extension.ts +1 -1
  7. package/src/capability/hook.ts +1 -1
  8. package/src/capability/instruction.ts +1 -1
  9. package/src/capability/mcp.ts +1 -1
  10. package/src/capability/prompt.ts +1 -1
  11. package/src/capability/rule.ts +1 -1
  12. package/src/capability/settings.ts +1 -1
  13. package/src/capability/skill.ts +1 -1
  14. package/src/capability/slash-command.ts +1 -1
  15. package/src/capability/ssh.ts +1 -1
  16. package/src/capability/system-prompt.ts +1 -1
  17. package/src/capability/tool.ts +1 -1
  18. package/src/cli/args.ts +1 -1
  19. package/src/cli/plugin-cli.ts +1 -5
  20. package/src/commit/agentic/agent.ts +309 -0
  21. package/src/commit/agentic/fallback.ts +96 -0
  22. package/src/commit/agentic/index.ts +359 -0
  23. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  24. package/src/commit/agentic/prompts/session-user.md +26 -0
  25. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  26. package/src/commit/agentic/prompts/system.md +40 -0
  27. package/src/commit/agentic/state.ts +74 -0
  28. package/src/commit/agentic/tools/analyze-file.ts +131 -0
  29. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  30. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  31. package/src/commit/agentic/tools/git-overview.ts +84 -0
  32. package/src/commit/agentic/tools/index.ts +56 -0
  33. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  34. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  35. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  36. package/src/commit/agentic/tools/split-commit.ts +284 -0
  37. package/src/commit/agentic/topo-sort.ts +44 -0
  38. package/src/commit/agentic/trivial.ts +51 -0
  39. package/src/commit/agentic/validation.ts +200 -0
  40. package/src/commit/analysis/conventional.ts +169 -0
  41. package/src/commit/analysis/index.ts +4 -0
  42. package/src/commit/analysis/scope.ts +242 -0
  43. package/src/commit/analysis/summary.ts +114 -0
  44. package/src/commit/analysis/validation.ts +66 -0
  45. package/src/commit/changelog/detect.ts +36 -0
  46. package/src/commit/changelog/generate.ts +112 -0
  47. package/src/commit/changelog/index.ts +233 -0
  48. package/src/commit/changelog/parse.ts +44 -0
  49. package/src/commit/cli.ts +93 -0
  50. package/src/commit/git/diff.ts +148 -0
  51. package/src/commit/git/errors.ts +11 -0
  52. package/src/commit/git/index.ts +217 -0
  53. package/src/commit/git/operations.ts +53 -0
  54. package/src/commit/index.ts +5 -0
  55. package/src/commit/map-reduce/.map-phase.ts.kate-swp +0 -0
  56. package/src/commit/map-reduce/index.ts +63 -0
  57. package/src/commit/map-reduce/map-phase.ts +193 -0
  58. package/src/commit/map-reduce/reduce-phase.ts +147 -0
  59. package/src/commit/map-reduce/utils.ts +9 -0
  60. package/src/commit/message.ts +11 -0
  61. package/src/commit/model-selection.ts +84 -0
  62. package/src/commit/pipeline.ts +242 -0
  63. package/src/commit/prompts/analysis-system.md +155 -0
  64. package/src/commit/prompts/analysis-user.md +41 -0
  65. package/src/commit/prompts/changelog-system.md +56 -0
  66. package/src/commit/prompts/changelog-user.md +19 -0
  67. package/src/commit/prompts/file-observer-system.md +26 -0
  68. package/src/commit/prompts/file-observer-user.md +9 -0
  69. package/src/commit/prompts/reduce-system.md +60 -0
  70. package/src/commit/prompts/reduce-user.md +17 -0
  71. package/src/commit/prompts/summary-retry.md +4 -0
  72. package/src/commit/prompts/summary-system.md +52 -0
  73. package/src/commit/prompts/summary-user.md +13 -0
  74. package/src/commit/prompts/types-description.md +2 -0
  75. package/src/commit/types.ts +109 -0
  76. package/src/commit/utils/exclusions.ts +42 -0
  77. package/src/config/file-lock.ts +111 -0
  78. package/src/config/model-registry.ts +16 -7
  79. package/src/config/settings-manager.ts +115 -40
  80. package/src/config.ts +5 -5
  81. package/src/discovery/agents-md.ts +1 -1
  82. package/src/discovery/builtin.ts +1 -1
  83. package/src/discovery/claude.ts +1 -1
  84. package/src/discovery/cline.ts +1 -1
  85. package/src/discovery/codex.ts +1 -1
  86. package/src/discovery/cursor.ts +1 -1
  87. package/src/discovery/gemini.ts +1 -1
  88. package/src/discovery/github.ts +1 -1
  89. package/src/discovery/index.ts +11 -11
  90. package/src/discovery/mcp-json.ts +1 -1
  91. package/src/discovery/ssh.ts +1 -1
  92. package/src/discovery/vscode.ts +1 -1
  93. package/src/discovery/windsurf.ts +1 -1
  94. package/src/extensibility/custom-commands/loader.ts +1 -1
  95. package/src/extensibility/custom-commands/types.ts +1 -1
  96. package/src/extensibility/custom-tools/loader.ts +1 -1
  97. package/src/extensibility/custom-tools/types.ts +1 -1
  98. package/src/extensibility/extensions/loader.ts +1 -1
  99. package/src/extensibility/extensions/types.ts +1 -1
  100. package/src/extensibility/hooks/loader.ts +1 -1
  101. package/src/extensibility/hooks/types.ts +3 -3
  102. package/src/index.ts +10 -10
  103. package/src/ipy/executor.ts +97 -1
  104. package/src/lsp/index.ts +1 -1
  105. package/src/lsp/render.ts +90 -46
  106. package/src/main.ts +16 -3
  107. package/src/mcp/loader.ts +3 -3
  108. package/src/migrations.ts +3 -3
  109. package/src/modes/components/assistant-message.ts +29 -1
  110. package/src/modes/components/tool-execution.ts +5 -3
  111. package/src/modes/components/tree-selector.ts +1 -1
  112. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  113. package/src/modes/controllers/selector-controller.ts +1 -1
  114. package/src/modes/interactive-mode.ts +5 -3
  115. package/src/modes/rpc/rpc-client.ts +1 -1
  116. package/src/modes/rpc/rpc-mode.ts +1 -4
  117. package/src/modes/rpc/rpc-types.ts +1 -1
  118. package/src/modes/theme/mermaid-cache.ts +89 -0
  119. package/src/modes/theme/theme.ts +2 -0
  120. package/src/modes/types.ts +2 -2
  121. package/src/patch/index.ts +3 -9
  122. package/src/patch/shared.ts +33 -5
  123. package/src/prompts/tools/task.md +2 -0
  124. package/src/sdk.ts +60 -22
  125. package/src/session/agent-session.ts +3 -3
  126. package/src/session/agent-storage.ts +32 -28
  127. package/src/session/artifacts.ts +24 -1
  128. package/src/session/auth-storage.ts +25 -10
  129. package/src/session/storage-migration.ts +12 -53
  130. package/src/system-prompt.ts +2 -2
  131. package/src/task/.executor.ts.kate-swp +0 -0
  132. package/src/task/executor.ts +1 -1
  133. package/src/task/index.ts +10 -1
  134. package/src/task/output-manager.ts +94 -0
  135. package/src/task/render.ts +7 -12
  136. package/src/task/worker.ts +1 -1
  137. package/src/tools/ask.ts +35 -13
  138. package/src/tools/bash.ts +80 -87
  139. package/src/tools/calculator.ts +42 -40
  140. package/src/tools/complete.ts +1 -1
  141. package/src/tools/fetch.ts +67 -104
  142. package/src/tools/find.ts +83 -86
  143. package/src/tools/grep.ts +80 -96
  144. package/src/tools/index.ts +10 -7
  145. package/src/tools/ls.ts +39 -65
  146. package/src/tools/notebook.ts +48 -64
  147. package/src/tools/output-utils.ts +1 -1
  148. package/src/tools/python.ts +71 -183
  149. package/src/tools/read.ts +74 -15
  150. package/src/tools/render-utils.ts +1 -15
  151. package/src/tools/ssh.ts +43 -24
  152. package/src/tools/todo-write.ts +27 -15
  153. package/src/tools/write.ts +93 -64
  154. package/src/tui/code-cell.ts +115 -0
  155. package/src/tui/file-list.ts +48 -0
  156. package/src/tui/index.ts +11 -0
  157. package/src/tui/output-block.ts +73 -0
  158. package/src/tui/status-line.ts +40 -0
  159. package/src/tui/tree-list.ts +56 -0
  160. package/src/tui/types.ts +17 -0
  161. package/src/tui/utils.ts +49 -0
  162. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  163. package/src/web/search/auth.ts +1 -1
  164. package/src/web/search/index.ts +1 -1
  165. package/src/web/search/render.ts +119 -163
  166. package/tsconfig.json +0 -42
@@ -0,0 +1,96 @@
1
+ import { basename } from "node:path";
2
+ import type { CommitProposal } from "@oh-my-pi/pi-coding-agent/commit/agentic/state";
3
+ import type { CommitType, ConventionalAnalysis, NumstatEntry } from "@oh-my-pi/pi-coding-agent/commit/types";
4
+
5
+ const TEST_PATTERNS = ["/test/", "/tests/", "/__tests__/", "_test.", ".test.", ".spec.", "_spec."];
6
+ const DOC_EXTENSIONS = new Set([".md", ".txt", ".rst", ".adoc"]);
7
+ const CONFIG_EXTENSIONS = new Set([".json", ".yaml", ".yml", ".toml", ".xml", ".ini", ".cfg"]);
8
+ const STYLE_EXTENSIONS = new Set([".css", ".scss", ".less", ".sass"]);
9
+
10
+ function inferTypeFromFiles(numstat: NumstatEntry[]): CommitType {
11
+ if (numstat.length === 0) return "chore";
12
+
13
+ let hasTests = false;
14
+ let hasDocs = false;
15
+ let hasConfig = false;
16
+ let hasStyle = false;
17
+ let hasSource = false;
18
+
19
+ for (const entry of numstat) {
20
+ const lowerPath = entry.path.toLowerCase();
21
+ const ext = getExtension(entry.path);
22
+
23
+ if (TEST_PATTERNS.some((pattern) => lowerPath.includes(pattern))) {
24
+ hasTests = true;
25
+ } else if (DOC_EXTENSIONS.has(ext)) {
26
+ hasDocs = true;
27
+ } else if (CONFIG_EXTENSIONS.has(ext)) {
28
+ hasConfig = true;
29
+ } else if (STYLE_EXTENSIONS.has(ext)) {
30
+ hasStyle = true;
31
+ } else {
32
+ hasSource = true;
33
+ }
34
+ }
35
+
36
+ if (hasTests && !hasSource && !hasDocs) return "test";
37
+ if (hasDocs && !hasSource && !hasTests) return "docs";
38
+ if (hasStyle && !hasSource && !hasTests) return "style";
39
+ if (hasConfig && !hasSource && !hasTests && !hasDocs) return "chore";
40
+ return "refactor";
41
+ }
42
+
43
+ function getExtension(path: string): string {
44
+ const name = basename(path);
45
+ const dotIndex = name.lastIndexOf(".");
46
+ return dotIndex >= 0 ? name.slice(dotIndex).toLowerCase() : "";
47
+ }
48
+
49
+ export function generateFallbackAnalysis(numstat: NumstatEntry[]): ConventionalAnalysis {
50
+ const type = inferTypeFromFiles(numstat);
51
+ const details = numstat.slice(0, 3).map((e) => ({
52
+ text: `Updated ${basename(e.path)}`,
53
+ userVisible: false,
54
+ }));
55
+
56
+ return {
57
+ type,
58
+ scope: null,
59
+ details,
60
+ issueRefs: [],
61
+ };
62
+ }
63
+
64
+ export function generateFallbackSummary(type: CommitType, numstat: NumstatEntry[]): string {
65
+ const verbMap: Record<string, string> = {
66
+ test: "updated tests for",
67
+ docs: "updated documentation for",
68
+ refactor: "refactored",
69
+ style: "formatted",
70
+ chore: "updated",
71
+ feat: "updated",
72
+ fix: "updated",
73
+ perf: "updated",
74
+ build: "updated",
75
+ ci: "updated",
76
+ revert: "reverted changes in",
77
+ };
78
+ const verb = verbMap[type] ?? "updated";
79
+ const file = basename(numstat[0]?.path ?? "files");
80
+
81
+ if (numstat.length === 1) {
82
+ return `${verb} ${file}`;
83
+ }
84
+ return `${verb} ${file} and ${numstat.length - 1} other${numstat.length === 2 ? "" : "s"}`;
85
+ }
86
+
87
+ export function generateFallbackProposal(numstat: NumstatEntry[]): CommitProposal {
88
+ const analysis = generateFallbackAnalysis(numstat);
89
+ const summary = generateFallbackSummary(analysis.type, numstat);
90
+
91
+ return {
92
+ analysis,
93
+ summary,
94
+ warnings: ["Commit generated using fallback due to agent failure"],
95
+ };
96
+ }
@@ -0,0 +1,359 @@
1
+ import { relative } from "node:path";
2
+ import { createInterface } from "node:readline/promises";
3
+ import { type ExistingChangelogEntries, runCommitAgentSession } from "@oh-my-pi/pi-coding-agent/commit/agentic/agent";
4
+ import { generateFallbackProposal } from "@oh-my-pi/pi-coding-agent/commit/agentic/fallback";
5
+ import splitConfirmPrompt from "@oh-my-pi/pi-coding-agent/commit/agentic/prompts/split-confirm.md" with {
6
+ type: "text",
7
+ };
8
+ import type {
9
+ CommitAgentState,
10
+ CommitProposal,
11
+ HunkSelector,
12
+ SplitCommitPlan,
13
+ } from "@oh-my-pi/pi-coding-agent/commit/agentic/state";
14
+ import { computeDependencyOrder } from "@oh-my-pi/pi-coding-agent/commit/agentic/topo-sort";
15
+ import { detectTrivialChange } from "@oh-my-pi/pi-coding-agent/commit/agentic/trivial";
16
+ import { applyChangelogProposals } from "@oh-my-pi/pi-coding-agent/commit/changelog";
17
+ import { detectChangelogBoundaries } from "@oh-my-pi/pi-coding-agent/commit/changelog/detect";
18
+ import { parseUnreleasedSection } from "@oh-my-pi/pi-coding-agent/commit/changelog/parse";
19
+ import { ControlledGit } from "@oh-my-pi/pi-coding-agent/commit/git";
20
+ import { formatCommitMessage } from "@oh-my-pi/pi-coding-agent/commit/message";
21
+ import { resolvePrimaryModel, resolveSmolModel } from "@oh-my-pi/pi-coding-agent/commit/model-selection";
22
+ import type { CommitCommandArgs, ConventionalAnalysis } from "@oh-my-pi/pi-coding-agent/commit/types";
23
+ import { renderPromptTemplate } from "@oh-my-pi/pi-coding-agent/config/prompt-templates";
24
+ import { SettingsManager } from "@oh-my-pi/pi-coding-agent/config/settings-manager";
25
+ import { discoverAuthStorage, discoverContextFiles, discoverModels } from "@oh-my-pi/pi-coding-agent/sdk";
26
+
27
+ interface CommitExecutionContext {
28
+ git: ControlledGit;
29
+ dryRun: boolean;
30
+ push: boolean;
31
+ }
32
+
33
+ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
34
+ const cwd = process.cwd();
35
+ const git = new ControlledGit(cwd);
36
+ const [settingsManager, authStorage] = await Promise.all([SettingsManager.create(cwd), discoverAuthStorage()]);
37
+ const modelRegistryPromise = discoverModels(authStorage);
38
+
39
+ writeStdout("● Resolving model...");
40
+ const stagedFilesPromise = (async () => {
41
+ let stagedFiles = await git.getStagedFiles();
42
+ if (stagedFiles.length === 0) {
43
+ writeStdout("No staged changes detected, staging all changes...");
44
+ await git.stageAll();
45
+ stagedFiles = await git.getStagedFiles();
46
+ }
47
+ return stagedFiles;
48
+ })();
49
+
50
+ const modelRegistry = await modelRegistryPromise;
51
+ const primaryModelPromise = resolvePrimaryModel(args.model, settingsManager, modelRegistry);
52
+ const [primaryModelResult, stagedFiles] = await Promise.all([primaryModelPromise, stagedFilesPromise]);
53
+ const { model: primaryModel, apiKey: primaryApiKey } = primaryModelResult;
54
+ writeStdout(` └─ ${primaryModel.name}`);
55
+
56
+ const { model: agentModel } = await resolveSmolModel(settingsManager, modelRegistry, primaryModel, primaryApiKey);
57
+
58
+ if (stagedFiles.length === 0) {
59
+ writeStderr("No changes to commit.");
60
+ return;
61
+ }
62
+
63
+ if (!args.noChangelog) {
64
+ writeStdout("● Detecting changelog targets...");
65
+ }
66
+ const [changelogBoundaries, contextFiles, numstat, diff] = await Promise.all([
67
+ args.noChangelog ? [] : detectChangelogBoundaries(cwd, stagedFiles),
68
+ discoverContextFiles(cwd),
69
+ git.getNumstat(true),
70
+ git.getDiff(true),
71
+ ]);
72
+ const changelogTargets = changelogBoundaries.map((boundary) => boundary.changelogPath);
73
+ if (!args.noChangelog) {
74
+ if (changelogTargets.length > 0) {
75
+ for (const path of changelogTargets) {
76
+ writeStdout(` └─ ${path}`);
77
+ }
78
+ } else {
79
+ writeStdout(" └─ (none found)");
80
+ }
81
+ }
82
+
83
+ writeStdout("● Discovering context files...");
84
+ const agentsMdFiles = contextFiles.filter((file) => file.path.endsWith("AGENTS.md"));
85
+ if (agentsMdFiles.length > 0) {
86
+ for (const file of agentsMdFiles) {
87
+ writeStdout(` └─ ${file.path}`);
88
+ }
89
+ } else {
90
+ writeStdout(" └─ (none found)");
91
+ }
92
+ const forceFallback = process.env.OMP_COMMIT_TEST_FALLBACK?.toLowerCase() === "true";
93
+ if (forceFallback) {
94
+ writeStdout("● Forcing fallback commit generation...");
95
+ const fallbackProposal = generateFallbackProposal(numstat);
96
+ await runSingleCommit(fallbackProposal, { git, dryRun: args.dryRun, push: args.push });
97
+ return;
98
+ }
99
+
100
+ const trivialChange = detectTrivialChange(diff);
101
+ if (trivialChange) {
102
+ writeStdout(`● Detected trivial change: ${trivialChange.summary}`);
103
+ const trivialProposal: CommitProposal = {
104
+ analysis: {
105
+ type: trivialChange.type,
106
+ scope: null,
107
+ details: [],
108
+ issueRefs: [],
109
+ },
110
+ summary: trivialChange.summary,
111
+ warnings: [],
112
+ };
113
+ await runSingleCommit(trivialProposal, { git, dryRun: args.dryRun, push: args.push });
114
+ return;
115
+ }
116
+
117
+ let existingChangelogEntries: ExistingChangelogEntries[] | undefined;
118
+ if (!args.noChangelog && changelogTargets.length > 0) {
119
+ existingChangelogEntries = await loadExistingChangelogEntries(changelogTargets);
120
+ if (existingChangelogEntries.length === 0) {
121
+ existingChangelogEntries = undefined;
122
+ }
123
+ }
124
+
125
+ writeStdout("● Starting commit agent...");
126
+ let commitState: CommitAgentState;
127
+ let usedFallback = false;
128
+
129
+ try {
130
+ commitState = await runCommitAgentSession({
131
+ cwd,
132
+ git,
133
+ model: agentModel,
134
+ settingsManager,
135
+ modelRegistry,
136
+ authStorage,
137
+ userContext: args.context,
138
+ contextFiles,
139
+ changelogTargets,
140
+ requireChangelog: !args.noChangelog && changelogTargets.length > 0,
141
+ diffText: diff,
142
+ existingChangelogEntries,
143
+ });
144
+ } catch (error) {
145
+ writeStderr(`Agent error: ${error instanceof Error ? error.message : String(error)}`);
146
+ writeStdout("● Using fallback commit generation...");
147
+ commitState = { proposal: generateFallbackProposal(numstat) };
148
+ usedFallback = true;
149
+ }
150
+
151
+ if (!usedFallback && !commitState.proposal && !commitState.splitProposal) {
152
+ if (process.env.OMP_COMMIT_NO_FALLBACK?.toLowerCase() !== "true") {
153
+ writeStdout("● Agent did not provide proposal, using fallback...");
154
+ commitState.proposal = generateFallbackProposal(numstat);
155
+ usedFallback = true;
156
+ }
157
+ }
158
+
159
+ let updatedChangelogFiles: string[] = [];
160
+ if (!args.noChangelog && changelogTargets.length > 0 && !usedFallback) {
161
+ if (!commitState.changelogProposal) {
162
+ writeStderr("Commit agent did not provide changelog entries.");
163
+ return;
164
+ }
165
+ writeStdout("● Applying changelog entries...");
166
+ const updated = await applyChangelogProposals({
167
+ git,
168
+ cwd,
169
+ proposals: commitState.changelogProposal.entries,
170
+ dryRun: args.dryRun,
171
+ onProgress: (message) => {
172
+ writeStdout(` ├─ ${message}`);
173
+ },
174
+ });
175
+ updatedChangelogFiles = updated.map((path) => relative(cwd, path));
176
+ if (updated.length > 0) {
177
+ for (const path of updated) {
178
+ writeStdout(` └─ ${path}`);
179
+ }
180
+ } else {
181
+ writeStdout(" └─ (no changes)");
182
+ }
183
+ }
184
+
185
+ if (commitState.proposal) {
186
+ await runSingleCommit(commitState.proposal, { git, dryRun: args.dryRun, push: args.push });
187
+ return;
188
+ }
189
+
190
+ if (commitState.splitProposal) {
191
+ await runSplitCommit(commitState.splitProposal, {
192
+ git,
193
+ dryRun: args.dryRun,
194
+ push: args.push,
195
+ additionalFiles: updatedChangelogFiles,
196
+ });
197
+ return;
198
+ }
199
+
200
+ writeStderr("Commit agent did not provide a proposal.");
201
+ }
202
+
203
+ async function runSingleCommit(proposal: CommitProposal, ctx: CommitExecutionContext): Promise<void> {
204
+ if (proposal.warnings.length > 0) {
205
+ writeStdout(formatWarnings(proposal.warnings));
206
+ }
207
+ const commitMessage = formatCommitMessage(proposal.analysis, proposal.summary);
208
+ if (ctx.dryRun) {
209
+ writeStdout("\nGenerated commit message:\n");
210
+ writeStdout(commitMessage);
211
+ return;
212
+ }
213
+ await ctx.git.commit(commitMessage);
214
+ writeStdout("Commit created.");
215
+ if (ctx.push) {
216
+ await ctx.git.push();
217
+ writeStdout("Pushed to remote.");
218
+ }
219
+ }
220
+
221
+ async function runSplitCommit(
222
+ plan: SplitCommitPlan,
223
+ ctx: CommitExecutionContext & { additionalFiles?: string[] },
224
+ ): Promise<void> {
225
+ if (plan.warnings.length > 0) {
226
+ writeStdout(formatWarnings(plan.warnings));
227
+ }
228
+ if (ctx.additionalFiles && ctx.additionalFiles.length > 0) {
229
+ appendFilesToLastCommit(plan, ctx.additionalFiles);
230
+ }
231
+ const stagedFiles = await ctx.git.getStagedFiles();
232
+ const plannedFiles = new Set(plan.commits.flatMap((commit) => commit.changes.map((change) => change.path)));
233
+ const missingFiles = stagedFiles.filter((file) => !plannedFiles.has(file));
234
+ if (missingFiles.length > 0) {
235
+ writeStderr(`Split commit plan missing staged files: ${missingFiles.join(", ")}`);
236
+ return;
237
+ }
238
+
239
+ if (ctx.dryRun) {
240
+ writeStdout("\nSplit commit plan (dry run):\n");
241
+ for (const [index, commit] of plan.commits.entries()) {
242
+ const analysis: ConventionalAnalysis = {
243
+ type: commit.type,
244
+ scope: commit.scope,
245
+ details: commit.details,
246
+ issueRefs: commit.issueRefs,
247
+ };
248
+ const message = formatCommitMessage(analysis, commit.summary);
249
+ writeStdout(`Commit ${index + 1}:\n${message}\n`);
250
+ const changeSummary = commit.changes
251
+ .map((change) => formatFileChangeSummary(change.path, change.hunks))
252
+ .join(", ");
253
+ writeStdout(`Changes: ${changeSummary}\n`);
254
+ }
255
+ return;
256
+ }
257
+
258
+ if (!(await confirmSplitCommitPlan(plan))) {
259
+ writeStdout("Split commit aborted by user.");
260
+ return;
261
+ }
262
+
263
+ const order = computeDependencyOrder(plan.commits);
264
+ if ("error" in order) {
265
+ throw new Error(order.error);
266
+ }
267
+
268
+ await ctx.git.resetStaging();
269
+ for (const commitIndex of order) {
270
+ const commit = plan.commits[commitIndex];
271
+ await ctx.git.stageHunks(commit.changes);
272
+ const analysis: ConventionalAnalysis = {
273
+ type: commit.type,
274
+ scope: commit.scope,
275
+ details: commit.details,
276
+ issueRefs: commit.issueRefs,
277
+ };
278
+ const message = formatCommitMessage(analysis, commit.summary);
279
+ await ctx.git.commit(message);
280
+ await ctx.git.resetStaging();
281
+ }
282
+ writeStdout("Split commits created.");
283
+ if (ctx.push) {
284
+ await ctx.git.push();
285
+ writeStdout("Pushed to remote.");
286
+ }
287
+ }
288
+
289
+ function appendFilesToLastCommit(plan: SplitCommitPlan, files: string[]): void {
290
+ if (plan.commits.length === 0) return;
291
+ const planned = new Set(plan.commits.flatMap((commit) => commit.changes.map((change) => change.path)));
292
+ const targetCommit = plan.commits[plan.commits.length - 1];
293
+ for (const file of files) {
294
+ if (planned.has(file)) continue;
295
+ targetCommit.changes.push({ path: file, hunks: { type: "all" } });
296
+ planned.add(file);
297
+ }
298
+ }
299
+
300
+ async function confirmSplitCommitPlan(plan: SplitCommitPlan): Promise<boolean> {
301
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
302
+ return true;
303
+ }
304
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
305
+ try {
306
+ const prompt = renderPromptTemplate(splitConfirmPrompt, { count: plan.commits.length });
307
+ const answer = await rl.question(prompt);
308
+ return ["y", "yes"].includes(answer.trim().toLowerCase());
309
+ } finally {
310
+ rl.close();
311
+ }
312
+ }
313
+
314
+ function formatWarnings(warnings: string[]): string {
315
+ return `Warnings:\n${warnings.map((warning) => `- ${warning}`).join("\n")}`;
316
+ }
317
+
318
+ function writeStdout(message: string): void {
319
+ process.stdout.write(`${message}\n`);
320
+ }
321
+
322
+ function writeStderr(message: string): void {
323
+ process.stderr.write(`${message}\n`);
324
+ }
325
+
326
+ function formatFileChangeSummary(path: string, hunks: HunkSelector): string {
327
+ if (hunks.type === "all") {
328
+ return `${path} (all)`;
329
+ }
330
+ if (hunks.type === "indices") {
331
+ return `${path} (hunks ${hunks.indices.join(", ")})`;
332
+ }
333
+ return `${path} (lines ${hunks.start}-${hunks.end})`;
334
+ }
335
+
336
+ async function loadExistingChangelogEntries(paths: string[]): Promise<ExistingChangelogEntries[]> {
337
+ const entries = await Promise.all(
338
+ paths.map(async (path) => {
339
+ const file = Bun.file(path);
340
+ if (!(await file.exists())) {
341
+ return null;
342
+ }
343
+ const content = await file.text();
344
+ try {
345
+ const unreleased = parseUnreleasedSection(content);
346
+ const sections = Object.entries(unreleased.entries)
347
+ .filter(([, items]) => items.length > 0)
348
+ .map(([name, items]) => ({ name, items }));
349
+ if (sections.length > 0) {
350
+ return { path, sections };
351
+ }
352
+ } catch {
353
+ return null;
354
+ }
355
+ return null;
356
+ }),
357
+ );
358
+ return entries.filter((entry): entry is ExistingChangelogEntries => entry !== null);
359
+ }
@@ -0,0 +1,22 @@
1
+ Analyze the file at {{file}}.
2
+
3
+ Goal:
4
+ {{#if goal}}
5
+ {{goal}}
6
+ {{else}}
7
+ Summarize its purpose and the commit-relevant changes.
8
+ {{/if}}
9
+
10
+ Return a concise JSON object with:
11
+ - summary: one-sentence description of the file's role
12
+ - highlights: 2-5 bullet points about notable behaviors or changes
13
+ - risks: any edge cases or risks worth noting (empty array if none)
14
+
15
+ {{#if related_files}}
16
+ ## Other Files in This Change
17
+ {{related_files}}
18
+
19
+ Consider how this file's changes relate to the above files.
20
+ {{/if}}
21
+
22
+ Call the complete tool with the JSON payload.
@@ -0,0 +1,26 @@
1
+ Generate a conventional commit proposal for the current staged changes.
2
+
3
+ {{#if user_context}}
4
+ User context:
5
+ {{user_context}}
6
+ {{/if}}
7
+
8
+ {{#if changelog_targets}}
9
+ Changelog targets (you must call propose_changelog for these files):
10
+ {{changelog_targets}}
11
+ {{/if}}
12
+
13
+ {{#if existing_changelog_entries}}
14
+ ## Existing Unreleased Changelog Entries
15
+ You may include entries from this list in the propose_changelog `deletions` field if they should be removed.
16
+ {{#each existing_changelog_entries}}
17
+ ### {{path}}
18
+ {{#each sections}}
19
+ {{name}}:
20
+ {{#list items prefix="- " join="\n"}}{{this}}{{/list}}
21
+ {{/each}}
22
+
23
+ {{/each}}
24
+ {{/if}}
25
+
26
+ Use the git_* tools to inspect changes. Call analyze_files to spawn parallel file analysis if you need deeper per-file summaries. Finish by calling propose_commit or split_commit.
@@ -0,0 +1 @@
1
+ Split commit plan has {{count}} commits. Proceed? (y/N):
@@ -0,0 +1,40 @@
1
+ You are a conventional commit expert for the omp commit workflow.
2
+
3
+ Your job: decide what git information you need, gather it with tools, and finish by calling exactly one of:
4
+ - propose_commit (single commit)
5
+ - split_commit (multiple commits when changes are unrelated)
6
+
7
+ Workflow rules:
8
+ 1. Always call git_overview first.
9
+ 2. Keep tool calls minimal: prefer 1-2 git_file_diff calls covering key files (hard limit: 2).
10
+ 3. Use git_hunk only for very large diffs.
11
+ 4. Use recent_commits only if you need style context.
12
+ 5. Use analyze_files only when diffs are too large or unclear.
13
+ 6. Do not use read.
14
+ 7. When confident, submit the final proposal with propose_commit or split_commit.
15
+
16
+ Commit requirements:
17
+ - Summary line must start with a past-tense verb, be <= 72 chars, and not end with a period.
18
+ - Avoid filler words: comprehensive, various, several, improved, enhanced, better.
19
+ - Avoid meta phrases: "this commit", "this change", "updated code", "modified files".
20
+ - Scope is lowercase, max two segments, and uses only letters, digits, hyphens, or underscores.
21
+ - Detail lines are optional (0-6). Each must be a sentence ending in a period and <= 120 chars.
22
+ - Use the conventional commit type guidance below.
23
+
24
+ Conventional commit types:
25
+ {{types_description}}
26
+
27
+ Tool guidance:
28
+ - git_overview: staged file list, stat summary, numstat, scope candidates
29
+ - git_file_diff: diff for specific files
30
+ - git_hunk: pull specific hunks for large diffs
31
+ - recent_commits: recent commit subjects + style stats
32
+ - analyze_files: spawn quick_task subagents in parallel to analyze files
33
+ - propose_changelog: provide changelog entries for each changelog target
34
+ - propose_commit: submit final commit proposal and run validation
35
+ - split_commit: propose multiple commit groups (no overlapping files, all staged files covered)
36
+
37
+ ## Changelog Requirements
38
+
39
+ If changelog targets are provided, you MUST call `propose_changelog` before finishing.
40
+ If you propose a split commit plan, include changelog target files in the relevant commit changes.
@@ -0,0 +1,74 @@
1
+ import type {
2
+ CommitType,
3
+ ConventionalAnalysis,
4
+ ConventionalDetail,
5
+ NumstatEntry,
6
+ } from "@oh-my-pi/pi-coding-agent/commit/types";
7
+
8
+ export interface GitOverviewSnapshot {
9
+ files: string[];
10
+ stat: string;
11
+ numstat: NumstatEntry[];
12
+ scopeCandidates: string;
13
+ isWideScope: boolean;
14
+ untrackedFiles?: string[];
15
+ excludedFiles?: string[];
16
+ }
17
+
18
+ export interface CommitProposal {
19
+ analysis: ConventionalAnalysis;
20
+ summary: string;
21
+ warnings: string[];
22
+ }
23
+
24
+ export interface FileObservation {
25
+ file: string;
26
+ summary: string;
27
+ highlights: string[];
28
+ risks: string[];
29
+ additions: number;
30
+ deletions: number;
31
+ }
32
+
33
+ export type HunkSelector =
34
+ | { type: "all" }
35
+ | { type: "indices"; indices: number[] }
36
+ | { type: "lines"; start: number; end: number };
37
+
38
+ export interface FileChange {
39
+ path: string;
40
+ hunks: HunkSelector;
41
+ }
42
+
43
+ export interface SplitCommitGroup {
44
+ changes: FileChange[];
45
+ type: CommitType;
46
+ scope: string | null;
47
+ summary: string;
48
+ details: ConventionalDetail[];
49
+ issueRefs: string[];
50
+ rationale?: string;
51
+ dependencies: number[];
52
+ }
53
+
54
+ export interface SplitCommitPlan {
55
+ commits: SplitCommitGroup[];
56
+ warnings: string[];
57
+ }
58
+
59
+ export interface ChangelogProposal {
60
+ entries: Array<{
61
+ path: string;
62
+ entries: Record<string, string[]>;
63
+ deletions?: Record<string, string[]>;
64
+ }>;
65
+ }
66
+
67
+ export interface CommitAgentState {
68
+ overview?: GitOverviewSnapshot;
69
+ proposal?: CommitProposal;
70
+ splitProposal?: SplitCommitPlan;
71
+ changelogProposal?: ChangelogProposal;
72
+ diffCache?: Map<string, string>;
73
+ diffText?: string;
74
+ }