@oh-my-pi/pi-coding-agent 17.1.7 → 17.1.8

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 (84) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/{CHANGELOG-5k4dq4g6.md → CHANGELOG-vt8ene9g.md} +35 -0
  3. package/dist/cli.js +4087 -7122
  4. package/dist/template-dys3vk5b.js +1671 -0
  5. package/dist/template-f8wx9vfn.css +1355 -0
  6. package/dist/template-qat058wr.html +55 -0
  7. package/dist/tool-views.generated-jdfmzwmn.js +35 -0
  8. package/dist/types/advisor/advise-tool.d.ts +0 -7
  9. package/dist/types/advisor/runtime.d.ts +0 -9
  10. package/dist/types/async/job-manager.d.ts +11 -0
  11. package/dist/types/cleanse/agent.d.ts +19 -0
  12. package/dist/types/cleanse/balance.d.ts +7 -0
  13. package/dist/types/cleanse/checkers.d.ts +13 -0
  14. package/dist/types/cleanse/index.d.ts +16 -0
  15. package/dist/types/cleanse/loop.d.ts +16 -0
  16. package/dist/types/cleanse/parsers.d.ts +13 -0
  17. package/dist/types/cleanse/progress.d.ts +14 -0
  18. package/dist/types/cleanse/types.d.ts +64 -0
  19. package/dist/types/commands/cleanse.d.ts +23 -0
  20. package/dist/types/export/html/index.d.ts +2 -0
  21. package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
  22. package/dist/types/modes/interactive-mode.d.ts +4 -3
  23. package/dist/types/session/async-job-delivery.d.ts +8 -0
  24. package/dist/types/session/model-controls.d.ts +3 -3
  25. package/dist/types/task/isolation-ownership.d.ts +34 -0
  26. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
  27. package/dist/types/tools/browser/tab-worker.d.ts +1 -4
  28. package/dist/types/tui/output-block.d.ts +5 -5
  29. package/package.json +16 -12
  30. package/scripts/bundle-dist.ts +3 -1
  31. package/src/advisor/advise-tool.ts +0 -11
  32. package/src/advisor/runtime.ts +0 -12
  33. package/src/async/job-manager.ts +30 -7
  34. package/src/cleanse/agent.ts +226 -0
  35. package/src/cleanse/balance.ts +79 -0
  36. package/src/cleanse/checkers.ts +996 -0
  37. package/src/cleanse/index.ts +190 -0
  38. package/src/cleanse/loop.ts +51 -0
  39. package/src/cleanse/parsers.ts +726 -0
  40. package/src/cleanse/progress.ts +50 -0
  41. package/src/cleanse/prompts/assignment.md +47 -0
  42. package/src/cleanse/types.ts +72 -0
  43. package/src/cli/update-cli.ts +3 -0
  44. package/src/cli/usage-cli.ts +29 -4
  45. package/src/cli/worktree-cli.ts +28 -11
  46. package/src/cli-commands.ts +1 -0
  47. package/src/cli.ts +2 -1
  48. package/src/commands/cleanse.ts +45 -0
  49. package/src/discovery/claude-plugins.ts +144 -34
  50. package/src/export/html/index.ts +17 -10
  51. package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
  52. package/src/launch/broker.ts +14 -4
  53. package/src/modes/acp/acp-agent.ts +12 -9
  54. package/src/modes/acp/acp-event-mapper.ts +38 -4
  55. package/src/modes/components/settings-selector.ts +9 -1
  56. package/src/modes/interactive-mode.ts +36 -47
  57. package/src/modes/prompt-action-autocomplete.ts +5 -3
  58. package/src/prompts/goals/guided-goal-interview.md +41 -6
  59. package/src/prompts/system/vibe-mode-active.md +4 -1
  60. package/src/prompts/tools/browser.md +1 -0
  61. package/src/prompts/tools/goal.md +1 -1
  62. package/src/session/agent-session.ts +28 -4
  63. package/src/session/async-job-delivery.ts +8 -0
  64. package/src/session/model-controls.ts +4 -4
  65. package/src/session/session-advisors.ts +2 -7
  66. package/src/session/session-history-format.ts +31 -6
  67. package/src/slash-commands/builtin-registry.ts +5 -2
  68. package/src/task/isolation-ownership.ts +106 -0
  69. package/src/task/worktree.ts +8 -0
  70. package/src/tools/ask.ts +3 -3
  71. package/src/tools/ast-edit.ts +9 -2
  72. package/src/tools/browser/cmux/cmux-tab.ts +9 -14
  73. package/src/tools/browser/tab-worker.ts +12 -35
  74. package/src/tools/browser.ts +2 -2
  75. package/src/tools/gh-renderer.ts +3 -3
  76. package/src/tools/index.ts +10 -1
  77. package/src/tools/write.ts +35 -0
  78. package/src/tui/code-cell.ts +4 -4
  79. package/src/tui/output-block.ts +25 -8
  80. package/src/utils/shell-snapshot-fn-env.sh +5 -2
  81. package/src/web/search/render.ts +2 -2
  82. package/dist/types/goals/guided-setup.d.ts +0 -30
  83. package/src/goals/guided-setup.ts +0 -171
  84. package/src/prompts/goals/guided-goal-system.md +0 -33
@@ -0,0 +1,226 @@
1
+ import { getProjectDir, prompt } from "@oh-my-pi/pi-utils";
2
+ import { ModelRegistry } from "../config/model-registry";
3
+ import { formatModelString, resolveCliModel } from "../config/model-resolver";
4
+ import { Settings } from "../config/settings";
5
+ import { MAIN_AGENT_ID } from "../registry/agent-registry";
6
+ import { discoverAuthStorage } from "../sdk";
7
+ import { SessionManager } from "../session/session-manager";
8
+ import { mapWithConcurrencyLimitAllSettled } from "../task/parallel";
9
+ import { runStructuredSubagent } from "../task/structured-subagent";
10
+ import type { ToolSession } from "../tools";
11
+ import { EventBus } from "../utils/event-bus";
12
+ import assignmentPrompt from "./prompts/assignment.md" with { type: "text" };
13
+ import type {
14
+ CleanseAgentOutcome,
15
+ CleanseAssignment,
16
+ CleanseCheckResult,
17
+ CleanseDiagnostic,
18
+ CleanseDiagnosticReport,
19
+ CleanseLoopResult,
20
+ } from "./types";
21
+
22
+ const MAX_DIAGNOSTIC_MESSAGE = 4_000;
23
+
24
+ /** Hooks used by the standalone command to render subagent lifecycle progress. */
25
+ export interface CleanseAgentHooks {
26
+ onStart?(name: string, assignment: CleanseAssignment): void;
27
+ onFinish?(outcome: CleanseAgentOutcome, assignment: CleanseAssignment): void;
28
+ }
29
+
30
+ /** Persisted parent session that dispatches file-disjoint cleanse workers. */
31
+ export interface CleanseAgentRuntime {
32
+ readonly model: string;
33
+ readonly sessionFile: string;
34
+ dispatch(
35
+ assignments: CleanseAssignment[],
36
+ wave: number,
37
+ report: CleanseDiagnosticReport,
38
+ signal?: AbortSignal,
39
+ ): Promise<CleanseAgentOutcome[]>;
40
+ close(result?: CleanseLoopResult): Promise<void>;
41
+ }
42
+
43
+ /** Resolve the requested model and create a fresh persisted cleanse session. */
44
+ export async function createCleanseAgentRuntime(options: {
45
+ cwd?: string;
46
+ model: string;
47
+ hooks?: CleanseAgentHooks;
48
+ }): Promise<CleanseAgentRuntime> {
49
+ const cwd = options.cwd ?? getProjectDir();
50
+ const [settings, authStorage] = await Promise.all([Settings.init({ cwd }), discoverAuthStorage()]);
51
+ const modelRegistry = new ModelRegistry(authStorage);
52
+ await modelRegistry.refresh();
53
+ const resolved = resolveCliModel({ cliModel: options.model, modelRegistry, settings });
54
+ if (resolved.error || !resolved.model) {
55
+ throw new Error(resolved.error ?? `Model "${options.model}" not found`);
56
+ }
57
+ const modelSelector = resolved.selector ?? formatModelString(resolved.model);
58
+ const modelDisplay = formatModelString(resolved.model);
59
+ const sessionManager = SessionManager.create(cwd);
60
+ await sessionManager.setSessionName("Cleanse", "auto");
61
+ sessionManager.appendCustomEntry("cleanse", {
62
+ status: "running",
63
+ model: modelDisplay,
64
+ selector: options.model,
65
+ });
66
+ await sessionManager.ensureOnDisk();
67
+ const sessionFile = sessionManager.getSessionFile();
68
+ if (!sessionFile) throw new Error("Cleanse session could not be persisted");
69
+ const eventBus = new EventBus();
70
+ const toolSession: ToolSession = {
71
+ cwd,
72
+ hasUI: false,
73
+ suppressSpawnAdvisory: true,
74
+ enableLsp: true,
75
+ enableIrc: true,
76
+ enableMCP: false,
77
+ eventBus,
78
+ getSessionFile: () => sessionFile,
79
+ getSessionId: () => sessionManager.getSessionId(),
80
+ getArtifactsDir: () => sessionManager.getArtifactsDir(),
81
+ getArtifactManager: () => sessionManager.getArtifactManager(),
82
+ getAgentId: () => MAIN_AGENT_ID,
83
+ getSessionSpawns: () => "sonic",
84
+ getModelString: () => modelSelector,
85
+ getActiveModelString: () => modelSelector,
86
+ getActiveModel: () => resolved.model,
87
+ sessionManager,
88
+ settings,
89
+ authStorage,
90
+ modelRegistry,
91
+ };
92
+ let closed = false;
93
+
94
+ return {
95
+ model: modelDisplay,
96
+ sessionFile,
97
+ async dispatch(
98
+ assignments: CleanseAssignment[],
99
+ wave: number,
100
+ report: CleanseDiagnosticReport,
101
+ signal?: AbortSignal,
102
+ ): Promise<CleanseAgentOutcome[]> {
103
+ sessionManager.appendCustomEntry("cleanse_wave", {
104
+ wave,
105
+ assignments: assignments.map(assignment => ({
106
+ weight: assignment.weight,
107
+ files: assignment.groups.map(group => group.file ?? "<project>"),
108
+ })),
109
+ });
110
+ const settled = await mapWithConcurrencyLimitAllSettled(
111
+ assignments,
112
+ assignments.length,
113
+ async (assignment, index, workerSignal) => {
114
+ const name = `CleanseW${wave}A${index + 1}`;
115
+ options.hooks?.onStart?.(name, assignment);
116
+ const result = await runStructuredSubagent({
117
+ session: toolSession,
118
+ invocationKind: "task",
119
+ assignment: renderAssignment(assignment, assignments, wave, index + 1, report.checks),
120
+ agent: "sonic",
121
+ model: modelSelector,
122
+ identity: { label: name },
123
+ index,
124
+ enableLsp: true,
125
+ enableIrc: true,
126
+ signal: workerSignal,
127
+ });
128
+ const outcome: CleanseAgentOutcome = {
129
+ name,
130
+ success: result.result.exitCode === 0 && !result.result.error && result.result.aborted !== true,
131
+ output: result.result.output,
132
+ error: result.result.error ?? (result.result.stderr || undefined),
133
+ resolvedModel: result.result.resolvedModel,
134
+ };
135
+ options.hooks?.onFinish?.(outcome, assignment);
136
+ return outcome;
137
+ },
138
+ signal,
139
+ );
140
+ const outcomes: CleanseAgentOutcome[] = [];
141
+ for (let index = 0; index < settled.results.length; index += 1) {
142
+ const result = settled.results[index];
143
+ if (!result) {
144
+ outcomes.push({ name: `CleanseW${wave}A${index + 1}`, success: false, output: "", error: "Cancelled" });
145
+ } else if (result.status === "fulfilled") {
146
+ outcomes.push(result.value);
147
+ } else {
148
+ const error = result.reason instanceof Error ? result.reason.message : String(result.reason);
149
+ const outcome = { name: `CleanseW${wave}A${index + 1}`, success: false, output: "", error };
150
+ options.hooks?.onFinish?.(outcome, assignments[index]);
151
+ outcomes.push(outcome);
152
+ }
153
+ }
154
+ return outcomes;
155
+ },
156
+ async close(result?: CleanseLoopResult): Promise<void> {
157
+ if (closed) return;
158
+ closed = true;
159
+ sessionManager.appendCustomEntry("cleanse", {
160
+ status: result?.status ?? "interrupted",
161
+ waves: result?.waves ?? 0,
162
+ remaining: result?.report.diagnostics.length,
163
+ });
164
+ await sessionManager.close();
165
+ },
166
+ };
167
+ }
168
+
169
+ function renderAssignment(
170
+ assignment: CleanseAssignment,
171
+ allAssignments: readonly CleanseAssignment[],
172
+ wave: number,
173
+ worker: number,
174
+ checks: readonly CleanseCheckResult[],
175
+ ): string {
176
+ const hasProjectIssues = assignment.groups.some(group => group.file === undefined);
177
+ const files = assignment.groups.flatMap(group => (group.file ? [group.file] : []));
178
+ const writeScope = [
179
+ ...(files.length > 0 ? files.map(file => `- ${file}`) : ["- No file is named by the project-level diagnostic."]),
180
+ ...(hasProjectIssues ? ["- Minimal additional files strictly required by project-level diagnostics."] : []),
181
+ ].join("\n");
182
+ return prompt.render(assignmentPrompt, {
183
+ wave,
184
+ worker,
185
+ write_scope: writeScope,
186
+ diagnostics: formatDiagnostics(assignment.groups.flatMap(group => group.diagnostics)),
187
+ checker_commands: formatCheckerCommands(checks),
188
+ peer_assignments: formatPeerAssignments(assignment, allAssignments),
189
+ });
190
+ }
191
+
192
+ function formatDiagnostics(diagnostics: readonly CleanseDiagnostic[]): string {
193
+ return diagnostics
194
+ .map(diagnostic => {
195
+ const location = diagnostic.file
196
+ ? `${diagnostic.file}${diagnostic.line ? `:${diagnostic.line}${diagnostic.column ? `:${diagnostic.column}` : ""}` : ""}`
197
+ : "<project>";
198
+ const code = diagnostic.code ? ` ${diagnostic.code}` : "";
199
+ const message = diagnostic.message.slice(0, MAX_DIAGNOSTIC_MESSAGE);
200
+ const suggestion = diagnostic.suggestion ? `\n Suggested fix: ${diagnostic.suggestion}` : "";
201
+ return `- [${diagnostic.severity}] ${location} — ${diagnostic.checker}${code}: ${message}${suggestion}`;
202
+ })
203
+ .join("\n");
204
+ }
205
+
206
+ function formatCheckerCommands(checks: readonly CleanseCheckResult[]): string {
207
+ const seen = new Set<string>();
208
+ const lines: string[] = [];
209
+ for (const check of checks) {
210
+ const key = `${check.cwd}\u0000${check.command}`;
211
+ if (seen.has(key)) continue;
212
+ seen.add(key);
213
+ lines.push(`- [${check.cwd}] ${check.command}`);
214
+ }
215
+ return lines.join("\n") || "- No command metadata available.";
216
+ }
217
+
218
+ function formatPeerAssignments(current: CleanseAssignment, assignments: readonly CleanseAssignment[]): string {
219
+ const lines: string[] = [];
220
+ for (const assignment of assignments) {
221
+ if (assignment.index === current.index) continue;
222
+ const files = assignment.groups.map(group => group.file ?? "<project-level>").join(", ");
223
+ lines.push(`- Worker ${assignment.index + 1}: ${files}`);
224
+ }
225
+ return lines.join("\n") || "- None.";
226
+ }
@@ -0,0 +1,79 @@
1
+ import type { CleanseAssignment, CleanseDiagnostic, CleanseFileIssues, CleanseSeverity } from "./types";
2
+
3
+ const SEVERITY_WEIGHT: Record<CleanseSeverity, number> = {
4
+ error: 5,
5
+ warning: 3,
6
+ info: 1,
7
+ };
8
+
9
+ /** Estimate repair burden from severity and available location/fix evidence. */
10
+ export function diagnosticWeight(diagnostic: CleanseDiagnostic): number {
11
+ let weight = SEVERITY_WEIGHT[diagnostic.severity];
12
+ if (diagnostic.line === undefined) weight += 2;
13
+ if (!diagnostic.code) weight += 1;
14
+ if (!diagnostic.suggestion) weight += 1;
15
+ return weight;
16
+ }
17
+
18
+ /** Group diagnostics by file while keeping project-level failures together. */
19
+ export function groupDiagnosticsByFile(diagnostics: readonly CleanseDiagnostic[]): CleanseFileIssues[] {
20
+ const groups = new Map<string, CleanseDiagnostic[]>();
21
+ for (const diagnostic of diagnostics) {
22
+ const key = diagnostic.file ?? "";
23
+ const existing = groups.get(key);
24
+ if (existing) existing.push(diagnostic);
25
+ else groups.set(key, [diagnostic]);
26
+ }
27
+ return [...groups.entries()]
28
+ .map(([file, entries]) => ({
29
+ file: file || undefined,
30
+ diagnostics: entries.sort(compareDiagnostics),
31
+ weight: entries.reduce((sum, diagnostic) => sum + diagnosticWeight(diagnostic), 0),
32
+ }))
33
+ .sort((left, right) => right.weight - left.weight || (left.file ?? "").localeCompare(right.file ?? ""));
34
+ }
35
+
36
+ /** Balance whole-file workloads with longest-processing-time bin packing. */
37
+ export function balanceDiagnostics(diagnostics: readonly CleanseDiagnostic[], maxAgents: number): CleanseAssignment[] {
38
+ if (!Number.isInteger(maxAgents) || maxAgents <= 0) {
39
+ throw new Error("maxAgents must be a positive integer");
40
+ }
41
+ const groups = groupDiagnosticsByFile(diagnostics);
42
+ if (groups.length === 0) return [];
43
+ const count = Math.min(maxAgents, groups.length);
44
+ const assignments: CleanseAssignment[] = Array.from({ length: count }, (_, index) => ({
45
+ index,
46
+ groups: [],
47
+ weight: 0,
48
+ }));
49
+ for (const group of groups) {
50
+ let lightest = assignments[0];
51
+ for (let index = 1; index < assignments.length; index += 1) {
52
+ const candidate = assignments[index];
53
+ if (
54
+ candidate.weight < lightest.weight ||
55
+ (candidate.weight === lightest.weight && candidate.groups.length < lightest.groups.length) ||
56
+ (candidate.weight === lightest.weight &&
57
+ candidate.groups.length === lightest.groups.length &&
58
+ candidate.index < lightest.index)
59
+ ) {
60
+ lightest = candidate;
61
+ }
62
+ }
63
+ lightest.groups.push(group);
64
+ lightest.weight += group.weight;
65
+ }
66
+ for (const assignment of assignments) {
67
+ assignment.groups.sort((left, right) => (left.file ?? "").localeCompare(right.file ?? ""));
68
+ }
69
+ return assignments;
70
+ }
71
+
72
+ function compareDiagnostics(left: CleanseDiagnostic, right: CleanseDiagnostic): number {
73
+ return (
74
+ (left.line ?? 0) - (right.line ?? 0) ||
75
+ (left.column ?? 0) - (right.column ?? 0) ||
76
+ left.checker.localeCompare(right.checker) ||
77
+ left.message.localeCompare(right.message)
78
+ );
79
+ }