@oh-my-pi/pi-coding-agent 17.1.6 → 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.
- package/CHANGELOG.md +58 -0
- package/dist/{CHANGELOG-x9zt79k8.md → CHANGELOG-vt8ene9g.md} +58 -0
- package/dist/cli.js +4309 -7287
- package/dist/template-dys3vk5b.js +1671 -0
- package/dist/template-f8wx9vfn.css +1355 -0
- package/dist/template-qat058wr.html +55 -0
- package/dist/tool-views.generated-jdfmzwmn.js +35 -0
- package/dist/types/advisor/advise-tool.d.ts +0 -7
- package/dist/types/advisor/runtime.d.ts +0 -9
- package/dist/types/async/job-manager.d.ts +11 -0
- package/dist/types/cleanse/agent.d.ts +19 -0
- package/dist/types/cleanse/balance.d.ts +7 -0
- package/dist/types/cleanse/checkers.d.ts +13 -0
- package/dist/types/cleanse/index.d.ts +16 -0
- package/dist/types/cleanse/loop.d.ts +16 -0
- package/dist/types/cleanse/parsers.d.ts +13 -0
- package/dist/types/cleanse/progress.d.ts +14 -0
- package/dist/types/cleanse/types.d.ts +64 -0
- package/dist/types/commands/cleanse.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +16 -1
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/export/html/index.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +27 -1
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +4 -3
- package/dist/types/session/agent-session-types.d.ts +5 -5
- package/dist/types/session/agent-session.d.ts +26 -1
- package/dist/types/session/async-job-delivery.d.ts +8 -0
- package/dist/types/session/model-controls.d.ts +4 -4
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/turn-recovery.d.ts +1 -1
- package/dist/types/task/isolation-ownership.d.ts +34 -0
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +1 -2
- package/dist/types/tools/browser/tab-worker.d.ts +1 -4
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/tui/output-block.d.ts +5 -5
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/package.json +16 -12
- package/scripts/bundle-dist.ts +3 -1
- package/src/advisor/advise-tool.ts +0 -11
- package/src/advisor/runtime.ts +0 -12
- package/src/async/job-manager.ts +30 -7
- package/src/cleanse/agent.ts +226 -0
- package/src/cleanse/balance.ts +79 -0
- package/src/cleanse/checkers.ts +996 -0
- package/src/cleanse/index.ts +190 -0
- package/src/cleanse/loop.ts +51 -0
- package/src/cleanse/parsers.ts +726 -0
- package/src/cleanse/progress.ts +50 -0
- package/src/cleanse/prompts/assignment.md +47 -0
- package/src/cleanse/types.ts +72 -0
- package/src/cli/update-cli.ts +3 -0
- package/src/cli/usage-cli.ts +29 -4
- package/src/cli/worktree-cli.ts +28 -11
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +2 -1
- package/src/commands/cleanse.ts +45 -0
- package/src/config/settings-schema.ts +15 -1
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/discovery/claude-plugins.ts +144 -34
- package/src/export/html/index.ts +17 -10
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/legacy-pi-ai-shim.ts +46 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/launch/broker.ts +14 -4
- package/src/modes/acp/acp-agent.ts +12 -9
- package/src/modes/acp/acp-event-mapper.ts +38 -4
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/settings-selector.ts +9 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/extension-ui-controller.ts +4 -4
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/interactive-mode.ts +36 -47
- package/src/modes/prompt-action-autocomplete.ts +5 -3
- package/src/prompts/goals/guided-goal-interview.md +41 -6
- package/src/prompts/system/vibe-mode-active.md +4 -1
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/goal.md +1 -1
- package/src/sdk.ts +47 -50
- package/src/session/agent-session-types.ts +5 -5
- package/src/session/agent-session.ts +151 -11
- package/src/session/async-job-delivery.ts +8 -0
- package/src/session/model-controls.ts +9 -9
- package/src/session/session-advisors.ts +2 -7
- package/src/session/session-history-format.ts +31 -6
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +158 -52
- package/src/session/streaming-output.ts +18 -6
- package/src/session/turn-recovery.ts +4 -4
- package/src/slash-commands/builtin-registry.ts +74 -2
- package/src/task/isolation-ownership.ts +106 -0
- package/src/task/worktree.ts +8 -0
- package/src/tools/ask.ts +3 -3
- package/src/tools/ast-edit.ts +9 -2
- package/src/tools/bash.ts +16 -9
- package/src/tools/browser/cmux/cmux-tab.ts +9 -14
- package/src/tools/browser/tab-worker.ts +12 -35
- package/src/tools/browser.ts +2 -2
- package/src/tools/gh-renderer.ts +3 -3
- package/src/tools/index.ts +46 -17
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +87 -13
- package/src/tools/write.ts +51 -7
- package/src/tools/xdev.ts +198 -210
- package/src/tui/code-cell.ts +4 -4
- package/src/tui/output-block.ts +25 -8
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/shell-snapshot-fn-env.sh +5 -2
- package/src/web/search/render.ts +2 -2
- package/dist/types/goals/guided-setup.d.ts +0 -30
- package/src/goals/guided-setup.ts +0 -171
- package/src/prompts/goals/guided-goal-system.md +0 -33
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser and renderer for macOS `/usr/bin/sample` call-tree reports
|
|
3
|
+
* (conventionally saved as `*.sample.txt`).
|
|
4
|
+
*
|
|
5
|
+
* The raw report is a 10k+ line ASCII call tree with mangled symbols —
|
|
6
|
+
* expensive for an agent to digest. `renderSampleProfile` converts it into a
|
|
7
|
+
* compact bottleneck summary:
|
|
8
|
+
*
|
|
9
|
+
* - per-thread hot paths, pruned to frames with meaningful on-CPU samples,
|
|
10
|
+
* with pass-through chains collapsed and direct recursion flattened
|
|
11
|
+
* - blocked/idle threads reduced to a one-line classification
|
|
12
|
+
* - a process-wide "top functions by self samples" table
|
|
13
|
+
* - Rust v0 and legacy symbols demangled (best-effort, path extraction)
|
|
14
|
+
*
|
|
15
|
+
* Consumed by the read tool: `*.sample.txt` reads show the summary, `:raw`
|
|
16
|
+
* returns the original bytes.
|
|
17
|
+
*/
|
|
18
|
+
/** Matches paths the read tool should treat as macOS sample reports. */
|
|
19
|
+
export declare function isSampleProfilePath(filePath: string): boolean;
|
|
20
|
+
/** One frame in the sampled call tree. Counts are sample hits (subtree total). */
|
|
21
|
+
export interface SampleFrame {
|
|
22
|
+
count: number;
|
|
23
|
+
symbol: string;
|
|
24
|
+
module?: string;
|
|
25
|
+
children: SampleFrame[];
|
|
26
|
+
}
|
|
27
|
+
/** One sampled thread: `Thread_<id>` root plus its call tree. */
|
|
28
|
+
export interface SampleThread {
|
|
29
|
+
id: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
total: number;
|
|
32
|
+
roots: SampleFrame[];
|
|
33
|
+
}
|
|
34
|
+
/** Metadata from the report preamble (everything before `Call graph:`). */
|
|
35
|
+
export interface SampleProfileHeader {
|
|
36
|
+
process: string;
|
|
37
|
+
pid: number;
|
|
38
|
+
intervalMs: number;
|
|
39
|
+
path?: string;
|
|
40
|
+
codeType?: string;
|
|
41
|
+
osVersion?: string;
|
|
42
|
+
footprint?: string;
|
|
43
|
+
footprintPeak?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Parsed macOS sample report. */
|
|
46
|
+
export interface SampleProfile {
|
|
47
|
+
header: SampleProfileHeader;
|
|
48
|
+
threads: SampleThread[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse a macOS `sample` report. Returns null when the text does not look
|
|
52
|
+
* like sample output (missing analysis preamble or `Call graph:` section).
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseSampleProfile(text: string): SampleProfile | null;
|
|
55
|
+
/**
|
|
56
|
+
* Best-effort demangle of Rust symbols (v0 `_R…` and legacy `_ZN…E`).
|
|
57
|
+
* For v0 this is a path extractor, not a full demangler: identifiers are
|
|
58
|
+
* pulled out in order and joined with `::`, so generic arguments appear as
|
|
59
|
+
* extra path segments. Non-Rust symbols pass through unchanged.
|
|
60
|
+
*/
|
|
61
|
+
export declare function demangleSymbol(raw: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Render a macOS sample report as an agent-friendly bottleneck summary.
|
|
64
|
+
* Returns null when `text` is not sample output (caller falls back to the
|
|
65
|
+
* plain-text path).
|
|
66
|
+
*/
|
|
67
|
+
export declare function renderSampleProfile(text: string): string | null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "17.1.
|
|
4
|
+
"version": "17.1.8",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -52,17 +52,17 @@
|
|
|
52
52
|
"@agentclientprotocol/sdk": "1.2.1",
|
|
53
53
|
"@babel/parser": "^7.29.7",
|
|
54
54
|
"@mozilla/readability": "^0.6.0",
|
|
55
|
-
"@oh-my-pi/hashline": "17.1.
|
|
56
|
-
"@oh-my-pi/omp-stats": "17.1.
|
|
57
|
-
"@oh-my-pi/pi-agent-core": "17.1.
|
|
58
|
-
"@oh-my-pi/pi-ai": "17.1.
|
|
59
|
-
"@oh-my-pi/pi-catalog": "17.1.
|
|
60
|
-
"@oh-my-pi/pi-mnemopi": "17.1.
|
|
61
|
-
"@oh-my-pi/pi-natives": "17.1.
|
|
62
|
-
"@oh-my-pi/pi-tui": "17.1.
|
|
63
|
-
"@oh-my-pi/pi-utils": "17.1.
|
|
64
|
-
"@oh-my-pi/pi-wire": "17.1.
|
|
65
|
-
"@oh-my-pi/snapcompact": "17.1.
|
|
55
|
+
"@oh-my-pi/hashline": "17.1.8",
|
|
56
|
+
"@oh-my-pi/omp-stats": "17.1.8",
|
|
57
|
+
"@oh-my-pi/pi-agent-core": "17.1.8",
|
|
58
|
+
"@oh-my-pi/pi-ai": "17.1.8",
|
|
59
|
+
"@oh-my-pi/pi-catalog": "17.1.8",
|
|
60
|
+
"@oh-my-pi/pi-mnemopi": "17.1.8",
|
|
61
|
+
"@oh-my-pi/pi-natives": "17.1.8",
|
|
62
|
+
"@oh-my-pi/pi-tui": "17.1.8",
|
|
63
|
+
"@oh-my-pi/pi-utils": "17.1.8",
|
|
64
|
+
"@oh-my-pi/pi-wire": "17.1.8",
|
|
65
|
+
"@oh-my-pi/snapcompact": "17.1.8",
|
|
66
66
|
"@opentelemetry/api": "^1.9.1",
|
|
67
67
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
68
68
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -107,6 +107,10 @@
|
|
|
107
107
|
"dist/cli.js",
|
|
108
108
|
"dist/CHANGELOG-*.md",
|
|
109
109
|
"dist/*.node",
|
|
110
|
+
"dist/template-*.css",
|
|
111
|
+
"dist/template-*.html",
|
|
112
|
+
"dist/template-*.js",
|
|
113
|
+
"dist/tool-views.generated-*.js",
|
|
110
114
|
"scripts",
|
|
111
115
|
"examples",
|
|
112
116
|
"README.md",
|
package/scripts/bundle-dist.ts
CHANGED
|
@@ -9,6 +9,7 @@ const packageDir = path.join(import.meta.dir, "..");
|
|
|
9
9
|
const outDir = path.join(packageDir, "dist");
|
|
10
10
|
const cliPath = path.join(outDir, "cli.js");
|
|
11
11
|
const shebang = "#!/usr/bin/env bun\n";
|
|
12
|
+
const legacyHtmlExportAssetPattern = /^(?:template-[^.]+\.(?:css|html|js)|tool-views\.generated-[^.]+\.js)$/;
|
|
12
13
|
|
|
13
14
|
// Native / optional / platform-specific deps are loaded from installed files.
|
|
14
15
|
// `omp-legacy-pi-modules` exists only in compiled binaries via the build plugin;
|
|
@@ -80,7 +81,8 @@ async function cleanBundleOutputs(): Promise<void> {
|
|
|
80
81
|
entry === "cli.js" ||
|
|
81
82
|
entry.endsWith(".node") ||
|
|
82
83
|
entry.endsWith(".js.map") ||
|
|
83
|
-
(entry.startsWith("CHANGELOG-") && entry.endsWith(".md"))
|
|
84
|
+
(entry.startsWith("CHANGELOG-") && entry.endsWith(".md")) ||
|
|
85
|
+
legacyHtmlExportAssetPattern.test(entry),
|
|
84
86
|
)
|
|
85
87
|
.map(entry => fs.rm(path.join(outDir, entry), { force: true })),
|
|
86
88
|
);
|
|
@@ -75,17 +75,6 @@ export function isInterruptingSeverity(severity: AdvisorSeverity | undefined): b
|
|
|
75
75
|
return severity === "concern" || severity === "blocker";
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/**
|
|
79
|
-
* Append a staleness caveat to an advisor note when newer primary turns arrived
|
|
80
|
-
* after the reviewed transcript window (i.e. `hasFreshBacklog` is true on the
|
|
81
|
-
* advisor runtime at delivery time). Pure function — no session coupling — so it
|
|
82
|
-
* can be unit-tested in isolation and called from `AgentSession#routeAdvice`.
|
|
83
|
-
*/
|
|
84
|
-
export function annotateForStaleness(note: string, hasFreshBacklog: boolean): string {
|
|
85
|
-
if (!hasFreshBacklog) return note;
|
|
86
|
-
return `${note}\n\n_(Note: newer primary turns arrived after this reviewed window — verify this still applies.)_`;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
78
|
/** How an advisor note is routed to the primary. */
|
|
90
79
|
export type AdvisorDeliveryChannel = "aside" | "steer" | "preserve";
|
|
91
80
|
/** Half-open turn-count fence for the post-interrupt cooldown. */
|
package/src/advisor/runtime.ts
CHANGED
|
@@ -340,18 +340,6 @@ export class AdvisorRuntime {
|
|
|
340
340
|
return this.#halted;
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
/**
|
|
344
|
-
* True when `#pending` is non-empty while the drain loop is busy — i.e., newer
|
|
345
|
-
* primary turns arrived after the current batch's transcript window was fixed
|
|
346
|
-
* but before the advisor model finished processing it. The delivery path uses
|
|
347
|
-
* this to annotate advice that was generated without seeing those newer turns.
|
|
348
|
-
* Can be true during `agent.prompt()`, a `maintainContext` await, or a retry
|
|
349
|
-
* sleep — any time `#drain` is busy and a concurrent `onTurnEnd` pushed.
|
|
350
|
-
*/
|
|
351
|
-
get hasFreshBacklog(): boolean {
|
|
352
|
-
return this.#pending.length > 0;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
343
|
/**
|
|
356
344
|
* Called after each primary turn ends. Renders the incremental delta and
|
|
357
345
|
* queues it for the advisor model.
|
package/src/async/job-manager.ts
CHANGED
|
@@ -417,6 +417,26 @@ export class AsyncJobManager {
|
|
|
417
417
|
}
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Immediately evict completed and failed jobs matching the filter instead of
|
|
422
|
+
* waiting for retention expiry, dropping every queued delivery so a prior
|
|
423
|
+
* session's result can never be injected into a later transcript. Returns the
|
|
424
|
+
* number of jobs evicted.
|
|
425
|
+
*
|
|
426
|
+
* A delivery whose sink call is already in flight (or drained onto a caller's
|
|
427
|
+
* yield queue) is guarded by the owner's delivery generation, not the per-id
|
|
428
|
+
* suppression marker — that marker is cleared when the id is reused.
|
|
429
|
+
*/
|
|
430
|
+
evictCompletedJobs(filter?: AsyncJobFilter): number {
|
|
431
|
+
let evicted = 0;
|
|
432
|
+
for (const job of this.#filterJobs(this.#jobs.values(), filter)) {
|
|
433
|
+
if (job.status !== "completed" && job.status !== "failed") continue;
|
|
434
|
+
this.acknowledgeDeliveries([job.id]);
|
|
435
|
+
if (this.#evictJob(job.id)) evicted += 1;
|
|
436
|
+
}
|
|
437
|
+
return evicted;
|
|
438
|
+
}
|
|
439
|
+
|
|
420
440
|
async waitForAll(): Promise<void> {
|
|
421
441
|
await Promise.all(Array.from(this.#jobs.values()).map(job => job.promise));
|
|
422
442
|
}
|
|
@@ -579,12 +599,18 @@ export class AsyncJobManager {
|
|
|
579
599
|
return candidate;
|
|
580
600
|
}
|
|
581
601
|
|
|
602
|
+
#evictJob(jobId: string): boolean {
|
|
603
|
+
clearTimeout(this.#evictionTimers.get(jobId));
|
|
604
|
+
this.#evictionTimers.delete(jobId);
|
|
605
|
+
this.#suppressedDeliveries.delete(jobId);
|
|
606
|
+
this.#watchedJobs.delete(jobId);
|
|
607
|
+
return this.#jobs.delete(jobId);
|
|
608
|
+
}
|
|
609
|
+
|
|
582
610
|
#scheduleEviction(jobId: string): void {
|
|
583
611
|
if (this.#disposed) return;
|
|
584
612
|
if (this.#retentionMs <= 0) {
|
|
585
|
-
this.#
|
|
586
|
-
this.#suppressedDeliveries.delete(jobId);
|
|
587
|
-
this.#watchedJobs.delete(jobId);
|
|
613
|
+
this.#evictJob(jobId);
|
|
588
614
|
return;
|
|
589
615
|
}
|
|
590
616
|
const existing = this.#evictionTimers.get(jobId);
|
|
@@ -592,10 +618,7 @@ export class AsyncJobManager {
|
|
|
592
618
|
clearTimeout(existing);
|
|
593
619
|
}
|
|
594
620
|
const timer = setTimeout(() => {
|
|
595
|
-
this.#
|
|
596
|
-
this.#jobs.delete(jobId);
|
|
597
|
-
this.#suppressedDeliveries.delete(jobId);
|
|
598
|
-
this.#watchedJobs.delete(jobId);
|
|
621
|
+
this.#evictJob(jobId);
|
|
599
622
|
}, this.#retentionMs);
|
|
600
623
|
timer.unref();
|
|
601
624
|
this.#evictionTimers.set(jobId, timer);
|
|
@@ -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
|
+
}
|