@oh-my-pi/pi-coding-agent 17.2.13 → 17.2.15

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 (239) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/{CHANGELOG-d8xh7keh.md → CHANGELOG-fk29zh06.md} +30 -0
  3. package/dist/cli.js +5114 -5417
  4. package/dist/types/cleanse/agent.d.ts +3 -0
  5. package/dist/types/cleanse/checkers.d.ts +21 -1
  6. package/dist/types/cleanse/index.d.ts +4 -0
  7. package/dist/types/cleanse/parsers.d.ts +3 -1
  8. package/dist/types/cli/args.d.ts +1 -0
  9. package/dist/types/cli/cleanse-picker.d.ts +17 -0
  10. package/dist/types/cli/command-help.d.ts +3 -0
  11. package/dist/types/cli/progress-reporter.d.ts +19 -0
  12. package/dist/types/commands/cleanse.d.ts +11 -0
  13. package/dist/types/commands/compress.d.ts +37 -0
  14. package/dist/types/commands/launch-help.d.ts +3 -0
  15. package/dist/types/commands/launch.d.ts +3 -0
  16. package/dist/types/compress/index.d.ts +27 -0
  17. package/dist/types/compress/protocol.d.ts +49 -0
  18. package/dist/types/compress/session.d.ts +15 -0
  19. package/dist/types/compress/types.d.ts +54 -0
  20. package/dist/types/config/settings-schema.d.ts +10 -0
  21. package/dist/types/internal-urls/local-protocol.d.ts +8 -0
  22. package/dist/types/mcp/transports/header-policy.d.ts +8 -0
  23. package/dist/types/mcp/transports/http.d.ts +2 -0
  24. package/dist/types/mcp/types.d.ts +19 -0
  25. package/dist/types/modes/components/late-diagnostics-message.d.ts +2 -0
  26. package/dist/types/modes/components/todo-reminder.d.ts +2 -0
  27. package/dist/types/modes/components/tool-activity.d.ts +17 -0
  28. package/dist/types/modes/components/transcript-container.d.ts +2 -0
  29. package/dist/types/modes/components/ttsr-notification.d.ts +2 -0
  30. package/dist/types/modes/interactive-mode.d.ts +3 -1
  31. package/dist/types/modes/types.d.ts +3 -1
  32. package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -1
  33. package/dist/types/modes/utils/ui-helpers.d.ts +3 -1
  34. package/dist/types/session/agent-session-types.d.ts +2 -0
  35. package/dist/types/session/agent-session.d.ts +2 -0
  36. package/dist/types/session/session-tools.d.ts +13 -0
  37. package/dist/types/tools/builtin-names.d.ts +1 -2
  38. package/dist/types/tools/index.d.ts +1 -0
  39. package/dist/types/tools/think.d.ts +41 -0
  40. package/dist/types/utils/zip.d.ts +19 -9
  41. package/package.json +13 -13
  42. package/src/cleanse/agent.ts +67 -2
  43. package/src/cleanse/checkers.ts +252 -21
  44. package/src/cleanse/index.ts +90 -36
  45. package/src/cleanse/parsers.ts +243 -20
  46. package/src/cleanse/prompts/discovery.md +74 -0
  47. package/src/cli/args.ts +3 -0
  48. package/src/cli/cleanse-picker.ts +86 -0
  49. package/src/cli/command-help.ts +4 -0
  50. package/src/cli/flag-tables.ts +1 -0
  51. package/src/cli/gallery-fixtures/agentic.ts +16 -0
  52. package/src/{cleanse/progress.ts → cli/progress-reporter.ts} +12 -7
  53. package/src/cli-commands.ts +5 -0
  54. package/src/commands/cleanse.ts +19 -4
  55. package/src/commands/compress.ts +45 -0
  56. package/src/commands/launch-help.ts +3 -0
  57. package/src/commit/agentic/prompts/analyze-file.md +6 -6
  58. package/src/commit/agentic/prompts/session-user.md +4 -4
  59. package/src/compress/index.ts +318 -0
  60. package/src/compress/prompts/request.md +11 -0
  61. package/src/compress/prompts/review.md +17 -0
  62. package/src/compress/prompts/system.md +81 -0
  63. package/src/compress/protocol.ts +210 -0
  64. package/src/compress/session.ts +72 -0
  65. package/src/compress/types.ts +59 -0
  66. package/src/config/settings-schema.ts +11 -0
  67. package/src/discovery/builtin-rules/go-add-cleanup.md +8 -8
  68. package/src/discovery/builtin-rules/go-exp-promoted.md +8 -8
  69. package/src/discovery/builtin-rules/go-ioutil.md +12 -12
  70. package/src/discovery/builtin-rules/go-new-expr.md +8 -8
  71. package/src/discovery/builtin-rules/go-range-int.md +5 -5
  72. package/src/discovery/builtin-rules/rs-box-leak.md +7 -7
  73. package/src/discovery/builtin-rules/rs-future-prelude.md +4 -4
  74. package/src/discovery/builtin-rules/rs-parking-lot.md +6 -6
  75. package/src/discovery/builtin-rules/ts-no-any.md +5 -5
  76. package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +8 -8
  77. package/src/discovery/builtin-rules/ts-no-inline-cast-access.md +13 -14
  78. package/src/discovery/builtin-rules/ts-no-local-is-record.md +7 -7
  79. package/src/discovery/builtin-rules/ts-no-test-timers.md +3 -9
  80. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +7 -7
  81. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +2 -2
  82. package/src/discovery/builtin-rules/ts-redundant-clear-guard.md +3 -9
  83. package/src/discovery/builtin-rules/ts-set-map.md +2 -4
  84. package/src/internal-urls/local-protocol.ts +42 -0
  85. package/src/live/prompts/live-instructions.md +10 -10
  86. package/src/main.ts +4 -0
  87. package/src/mcp/client.ts +7 -3
  88. package/src/mcp/transports/header-policy.ts +28 -0
  89. package/src/mcp/transports/http.ts +200 -35
  90. package/src/mcp/types.ts +21 -0
  91. package/src/modes/components/chat-transcript-builder.ts +17 -5
  92. package/src/modes/components/late-diagnostics-message.ts +12 -1
  93. package/src/modes/components/todo-reminder.ts +12 -0
  94. package/src/modes/components/tool-activity.ts +45 -0
  95. package/src/modes/components/transcript-container.ts +16 -0
  96. package/src/modes/components/ttsr-notification.ts +12 -0
  97. package/src/modes/controllers/event-controller.ts +3 -6
  98. package/src/modes/controllers/input-controller.ts +6 -6
  99. package/src/modes/controllers/selector-controller.ts +8 -6
  100. package/src/modes/interactive-mode.ts +5 -40
  101. package/src/modes/print-mode.ts +13 -54
  102. package/src/modes/types.ts +1 -1
  103. package/src/modes/utils/transcript-render-helpers.ts +3 -2
  104. package/src/modes/utils/ui-helpers.ts +17 -7
  105. package/src/prompts/advisor/active-repo-watchdog.md +3 -4
  106. package/src/prompts/advisor/advise-tool.md +3 -3
  107. package/src/prompts/advisor/context-files.md +1 -1
  108. package/src/prompts/advisor/system.md +45 -65
  109. package/src/prompts/agents/designer.md +44 -44
  110. package/src/prompts/agents/init.md +18 -18
  111. package/src/prompts/agents/librarian.md +31 -31
  112. package/src/prompts/agents/reviewer.md +34 -39
  113. package/src/prompts/agents/security-reviewer.md +3 -5
  114. package/src/prompts/agents/task.md +11 -12
  115. package/src/prompts/bench.md +2 -5
  116. package/src/prompts/ci-green-request.md +13 -15
  117. package/src/prompts/dry-balance-bench.md +5 -5
  118. package/src/prompts/goals/goal-budget-limit.md +4 -5
  119. package/src/prompts/goals/goal-continuation.md +11 -11
  120. package/src/prompts/goals/goal-mode-active.md +7 -7
  121. package/src/prompts/goals/goal-todo-context.md +2 -2
  122. package/src/prompts/goals/guided-goal-interview.md +18 -24
  123. package/src/prompts/memories/read-path.md +9 -9
  124. package/src/prompts/memories/stage_one_system.md +9 -11
  125. package/src/prompts/review-custom-request.md +8 -11
  126. package/src/prompts/review-headless-request.md +3 -10
  127. package/src/prompts/security/scan-coordinator.md +6 -5
  128. package/src/prompts/security/validate-request.md +4 -7
  129. package/src/prompts/skills/user-invocation.md +2 -2
  130. package/src/prompts/steering/parent-irc.md +1 -1
  131. package/src/prompts/steering/user-interjection.md +1 -3
  132. package/src/prompts/system/active-repo-context.md +4 -2
  133. package/src/prompts/system/agent-creation-architect.md +22 -37
  134. package/src/prompts/system/agent-creation-user.md +3 -3
  135. package/src/prompts/system/auto-continue.md +1 -1
  136. package/src/prompts/system/auto-thinking-difficulty-local.md +5 -7
  137. package/src/prompts/system/auto-thinking-difficulty.md +8 -10
  138. package/src/prompts/system/autolearn-guidance-learn.md +2 -1
  139. package/src/prompts/system/autolearn-guidance.md +5 -4
  140. package/src/prompts/system/autolearn-nudge-autocontinue.md +3 -3
  141. package/src/prompts/system/background-tan-dispatch.md +3 -3
  142. package/src/prompts/system/btw-user.md +2 -2
  143. package/src/prompts/system/commit-message-system.md +8 -6
  144. package/src/prompts/system/eager-task.md +3 -3
  145. package/src/prompts/system/empty-stop-retry.md +1 -1
  146. package/src/prompts/system/gemini-tool-call-reminder.md +5 -5
  147. package/src/prompts/system/interrupted-thinking.md +3 -3
  148. package/src/prompts/system/irc-autoreply.md +1 -1
  149. package/src/prompts/system/irc-incoming.md +3 -3
  150. package/src/prompts/system/manual-continue.md +3 -3
  151. package/src/prompts/system/mcp-xdev-guidance.md +2 -2
  152. package/src/prompts/system/memory-consolidation-system.md +2 -2
  153. package/src/prompts/system/mid-run-todo-nudge.md +1 -1
  154. package/src/prompts/system/orchestrate-notice.md +25 -25
  155. package/src/prompts/system/personalities/default.md +9 -9
  156. package/src/prompts/system/personalities/friendly.md +11 -11
  157. package/src/prompts/system/personalities/pragmatic.md +8 -8
  158. package/src/prompts/system/plan-mode-active.md +61 -64
  159. package/src/prompts/system/plan-mode-approved.md +10 -11
  160. package/src/prompts/system/plan-mode-compact-instructions.md +11 -11
  161. package/src/prompts/system/plan-mode-reference.md +5 -5
  162. package/src/prompts/system/plan-yolo-handoff.md +2 -2
  163. package/src/prompts/system/prewalk-checklist.md +5 -5
  164. package/src/prompts/system/prewalk-continue.md +1 -1
  165. package/src/prompts/system/prewalk-plan.md +8 -9
  166. package/src/prompts/system/project-prompt.md +11 -12
  167. package/src/prompts/system/recap-user.md +1 -1
  168. package/src/prompts/system/resolve-device-reminder.md +1 -1
  169. package/src/prompts/system/rewind-report.md +3 -3
  170. package/src/prompts/system/side-channel-no-tools.md +3 -1
  171. package/src/prompts/system/snapcompact-context-stub.md +1 -1
  172. package/src/prompts/system/snapcompact-system-frames-note.md +1 -1
  173. package/src/prompts/system/snapcompact-system-stub.md +1 -1
  174. package/src/prompts/system/snapcompact-toolresult-note.md +1 -1
  175. package/src/prompts/system/speech-rewrite.md +11 -13
  176. package/src/prompts/system/subagent-async-pending.md +5 -5
  177. package/src/prompts/system/subagent-system-prompt.md +5 -15
  178. package/src/prompts/system/subagent-user-prompt.md +1 -1
  179. package/src/prompts/system/subagent-yield-reminder.md +12 -12
  180. package/src/prompts/system/system-prompt.md +110 -136
  181. package/src/prompts/system/tan-context-switch.md +8 -14
  182. package/src/prompts/system/task-label.md +3 -3
  183. package/src/prompts/system/thinking-loop-redirect.md +6 -6
  184. package/src/prompts/system/title-marker-instruction.md +2 -1
  185. package/src/prompts/system/title-system.md +3 -5
  186. package/src/prompts/system/ttsr-interrupt.md +3 -3
  187. package/src/prompts/system/ttsr-tool-reminder.md +1 -1
  188. package/src/prompts/system/ultrathink-notice.md +1 -1
  189. package/src/prompts/system/unexpected-stop-classifier.md +3 -3
  190. package/src/prompts/system/vibe-mode-active.md +15 -15
  191. package/src/prompts/system/web-search.md +16 -16
  192. package/src/prompts/system/workflow-notice.md +35 -33
  193. package/src/prompts/system/xdev-mount-notice.md +4 -4
  194. package/src/prompts/tools/apply-patch.md +22 -23
  195. package/src/prompts/tools/approve.md +5 -0
  196. package/src/prompts/tools/ask.md +10 -10
  197. package/src/prompts/tools/checkpoint.md +7 -7
  198. package/src/prompts/tools/computer.md +16 -16
  199. package/src/prompts/tools/github.md +12 -12
  200. package/src/prompts/tools/goal.md +8 -9
  201. package/src/prompts/tools/grep.md +5 -5
  202. package/src/prompts/tools/image-attachment-describe-system.md +6 -6
  203. package/src/prompts/tools/image-attachment-describe.md +3 -8
  204. package/src/prompts/tools/image-gen.md +4 -4
  205. package/src/prompts/tools/inspect-image-system.md +10 -10
  206. package/src/prompts/tools/inspect-image.md +10 -13
  207. package/src/prompts/tools/learn.md +4 -4
  208. package/src/prompts/tools/manage-skill.md +9 -6
  209. package/src/prompts/tools/memory-edit.md +8 -8
  210. package/src/prompts/tools/recall.md +4 -4
  211. package/src/prompts/tools/reflect.md +2 -2
  212. package/src/prompts/tools/replace.md +13 -11
  213. package/src/prompts/tools/retain.md +3 -4
  214. package/src/prompts/tools/rewind.md +7 -8
  215. package/src/prompts/tools/rewrite.md +12 -0
  216. package/src/prompts/tools/security-publish.md +5 -1
  217. package/src/prompts/tools/security-scan.md +10 -1
  218. package/src/prompts/tools/task-async-contract.md +7 -1
  219. package/src/prompts/tools/todo.md +27 -25
  220. package/src/prompts/tools/vibe-kill.md +2 -2
  221. package/src/prompts/tools/vibe-list.md +2 -2
  222. package/src/prompts/tools/vibe-send.md +6 -7
  223. package/src/prompts/tools/vibe-spawn.md +8 -6
  224. package/src/prompts/tools/web-search.md +5 -5
  225. package/src/sdk.ts +10 -1
  226. package/src/session/agent-session-types.ts +2 -0
  227. package/src/session/agent-session.ts +28 -0
  228. package/src/session/session-handoff.ts +21 -0
  229. package/src/session/session-tools.ts +47 -0
  230. package/src/tools/builtin-names.ts +1 -2
  231. package/src/tools/index.ts +10 -0
  232. package/src/tools/read-format.ts +8 -3
  233. package/src/tools/read.ts +18 -7
  234. package/src/tools/renderers.ts +2 -0
  235. package/src/tools/think.ts +84 -0
  236. package/src/tools/write.ts +1 -1
  237. package/src/utils/file-mentions.ts +8 -2
  238. package/src/utils/zip.ts +858 -94
  239. package/dist/types/cleanse/progress.d.ts +0 -14
@@ -0,0 +1,41 @@
1
+ import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
2
+ import type { Model } from "@oh-my-pi/pi-ai";
3
+ import { type Component } from "@oh-my-pi/pi-tui";
4
+ import type { RenderResultOptions } from "../extensibility/custom-tools/types.js";
5
+ import { type Theme } from "../modes/theme/theme.js";
6
+ /** Whether a model transport can suppress native reasoning while private scratchpad thoughts are active. */
7
+ export declare function supportsExternalThinking(model: Model | null | undefined): boolean;
8
+ declare const thinkSchema: import("@oh-my-pi/omptype").FluentType<{
9
+ thoughts: string;
10
+ }, {
11
+ thoughts: string;
12
+ }>;
13
+ type ThinkParams = typeof thinkSchema.infer;
14
+ export type ThinkRenderArgs = {
15
+ thoughts?: string;
16
+ };
17
+ export declare const thinkToolRenderer: {
18
+ inline: boolean;
19
+ renderCall(args: ThinkRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component;
20
+ renderResult(): Component;
21
+ };
22
+ interface ThinkToolDetails {
23
+ recorded: true;
24
+ }
25
+ /** Records private scratchpad thoughts while native model reasoning is disabled. */
26
+ export declare class ThinkTool implements AgentTool<typeof thinkSchema, ThinkToolDetails> {
27
+ readonly name = "think";
28
+ readonly approval: "read";
29
+ readonly label = "Think";
30
+ readonly summary = "Record private scratchpad thoughts";
31
+ readonly description = "private scratchpad; not shown to user";
32
+ readonly parameters: import("@oh-my-pi/omptype").FluentType<{
33
+ thoughts: string;
34
+ }, {
35
+ thoughts: string;
36
+ }>;
37
+ readonly strict = true;
38
+ readonly intent: "omit";
39
+ execute(_toolCallId: string, _params: ThinkParams): Promise<AgentToolResult<ThinkToolDetails>>;
40
+ }
41
+ export {};
@@ -10,13 +10,16 @@ export declare function unzipText(entries: Unzipped, entryPath: string): string
10
10
  export declare function unzip(bytes: Uint8Array): Unzipped;
11
11
  export type ArchiveFormat = "zip" | "tar" | "tar.gz";
12
12
  /**
13
- * Where to read an archive from: a filesystem path (format inferred from the
14
- * extension; ZIP is read lazily via ranged central-directory access) or
15
- * in-memory bytes with an explicit format.
13
+ * Where to read an archive from: an extension-inferred filesystem path, a
14
+ * format-tagged filesystem path, or in-memory bytes with an explicit format.
15
+ * ZIP paths are read lazily via ranged central-directory access.
16
16
  */
17
17
  export type ArchiveSource = string | {
18
18
  bytes: Uint8Array;
19
19
  format: ArchiveFormat;
20
+ } | {
21
+ path: string;
22
+ format: ArchiveFormat;
20
23
  };
21
24
  /** Content for a member when packing or extracting an archive. */
22
25
  export type ArchiveMemberContent = string | Uint8Array | Blob;
@@ -43,7 +46,13 @@ interface ByteSource {
43
46
  }
44
47
  interface TarStorage {
45
48
  type: "tar";
46
- file: File;
49
+ buffer: Uint8Array;
50
+ dataOffset: number;
51
+ sparse: boolean;
52
+ }
53
+ interface TarLinkStorage {
54
+ type: "tar-link";
55
+ targetPath: string;
47
56
  }
48
57
  interface ZipStorage {
49
58
  type: "zip";
@@ -53,7 +62,7 @@ interface ZipStorage {
53
62
  flags: number;
54
63
  localHeaderOffset: number;
55
64
  }
56
- type EntryStorage = TarStorage | ZipStorage;
65
+ type EntryStorage = TarStorage | TarLinkStorage | ZipStorage;
57
66
  interface ArchiveIndexEntry extends ArchiveNode {
58
67
  storage?: EntryStorage;
59
68
  }
@@ -71,7 +80,8 @@ export declare function parseArchivePathCandidates(filePath: string): ArchivePat
71
80
  /**
72
81
  * An indexed, read-only view over a single archive. ZIP archives are indexed
73
82
  * from the central directory and members are inflated on demand; tar archives
74
- * are fully materialized by `Bun.Archive` up front.
83
+ * are parsed from one in-memory buffer, members are sliced on demand, and
84
+ * directory symlink aliases are traversed lazily.
75
85
  */
76
86
  export declare class ArchiveReader {
77
87
  #private;
@@ -93,9 +103,9 @@ export declare function listArchiveRoot(bytes: Uint8Array, format: ArchiveFormat
93
103
  }): Promise<string>;
94
104
  /**
95
105
  * Fully materialize every file member into a `path → content` map: ZIP members
96
- * are inflated in memory, tar members are returned as lazy `File`s. Use this
97
- * when you need every entry (rewrite, extract); for browsing or single-member
98
- * reads prefer `openArchive`, which is lazy for ZIP.
106
+ * are inflated in memory, tar members are sliced from the decoded archive
107
+ * buffer. Use this when you need every entry (rewrite, extract); for browsing
108
+ * or single-member reads prefer `openArchive`, which is lazy for ZIP.
99
109
  */
100
110
  export declare function readArchiveEntries(source: ArchiveSource): Promise<Map<string, ArchiveMemberContent>>;
101
111
  /**
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.2.13",
4
+ "version": "17.2.15",
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",
@@ -50,18 +50,18 @@
50
50
  },
51
51
  "dependencies": {
52
52
  "@babel/parser": "^7.29.7",
53
- "@oh-my-pi/hashline": "17.2.13",
54
- "@oh-my-pi/omp-stats": "17.2.13",
55
- "@oh-my-pi/omptype": "17.2.13",
56
- "@oh-my-pi/pi-agent-core": "17.2.13",
57
- "@oh-my-pi/pi-ai": "17.2.13",
58
- "@oh-my-pi/pi-catalog": "17.2.13",
59
- "@oh-my-pi/pi-mnemopi": "17.2.13",
60
- "@oh-my-pi/pi-natives": "17.2.13",
61
- "@oh-my-pi/pi-tui": "17.2.13",
62
- "@oh-my-pi/pi-utils": "17.2.13",
63
- "@oh-my-pi/pi-wire": "17.2.13",
64
- "@oh-my-pi/snapcompact": "17.2.13",
53
+ "@oh-my-pi/hashline": "17.2.15",
54
+ "@oh-my-pi/omp-stats": "17.2.15",
55
+ "@oh-my-pi/omptype": "17.2.15",
56
+ "@oh-my-pi/pi-agent-core": "17.2.15",
57
+ "@oh-my-pi/pi-ai": "17.2.15",
58
+ "@oh-my-pi/pi-catalog": "17.2.15",
59
+ "@oh-my-pi/pi-mnemopi": "17.2.15",
60
+ "@oh-my-pi/pi-natives": "17.2.15",
61
+ "@oh-my-pi/pi-tui": "17.2.15",
62
+ "@oh-my-pi/pi-utils": "17.2.15",
63
+ "@oh-my-pi/pi-wire": "17.2.15",
64
+ "@oh-my-pi/snapcompact": "17.2.15",
65
65
  "@opentelemetry/api": "^1.9.1",
66
66
  "@opentelemetry/api-logs": "^0.220.0",
67
67
  "@opentelemetry/context-async-hooks": "^2.9.0",
@@ -1,4 +1,4 @@
1
- import { getProjectDir, prompt } from "@oh-my-pi/pi-utils";
1
+ import { getProjectDir, isRecord, prompt } from "@oh-my-pi/pi-utils";
2
2
  import { ModelRegistry } from "../config/model-registry";
3
3
  import { formatModelString, resolveCliModel } from "../config/model-resolver";
4
4
  import { Settings } from "../config/settings";
@@ -9,7 +9,10 @@ import { mapWithConcurrencyLimitAllSettled } from "../task/parallel";
9
9
  import { runStructuredSubagent } from "../task/structured-subagent";
10
10
  import type { ToolSession } from "../tools";
11
11
  import { EventBus } from "../utils/event-bus";
12
+ import type { CustomCleanseCheckerSpec } from "./checkers";
13
+ import { CLEANSE_PARSER_KINDS } from "./parsers";
12
14
  import assignmentPrompt from "./prompts/assignment.md" with { type: "text" };
15
+ import discoveryPrompt from "./prompts/discovery.md" with { type: "text" };
13
16
  import type {
14
17
  CleanseAgentOutcome,
15
18
  CleanseAssignment,
@@ -21,6 +24,28 @@ import type {
21
24
 
22
25
  const MAX_DIAGNOSTIC_MESSAGE = 4_000;
23
26
 
27
+ /** Structured output contract for the prompted checker-discovery agent. */
28
+ const DISCOVERY_SCHEMA = {
29
+ type: "object",
30
+ required: ["checkers"],
31
+ properties: {
32
+ checkers: {
33
+ type: "array",
34
+ items: {
35
+ type: "object",
36
+ required: ["label", "command"],
37
+ properties: {
38
+ label: { type: "string" },
39
+ language: { type: "string" },
40
+ cwd: { type: "string" },
41
+ command: { type: "array", items: { type: "string" }, minItems: 1 },
42
+ parser: { type: "string", enum: [...CLEANSE_PARSER_KINDS] },
43
+ },
44
+ },
45
+ },
46
+ },
47
+ };
48
+
24
49
  /** Hooks used by the standalone command to render subagent lifecycle progress. */
25
50
  export interface CleanseAgentHooks {
26
51
  onStart?(name: string, assignment: CleanseAssignment): void;
@@ -31,6 +56,8 @@ export interface CleanseAgentHooks {
31
56
  export interface CleanseAgentRuntime {
32
57
  readonly model: string;
33
58
  readonly sessionFile: string;
59
+ /** Run one discovery subagent that translates a user request into runnable checker specs. */
60
+ discoverCheckers(request: string, signal?: AbortSignal): Promise<CustomCleanseCheckerSpec[]>;
34
61
  dispatch(
35
62
  assignments: CleanseAssignment[],
36
63
  wave: number,
@@ -80,7 +107,7 @@ export async function createCleanseAgentRuntime(options: {
80
107
  getArtifactsDir: () => sessionManager.getArtifactsDir(),
81
108
  getArtifactManager: () => sessionManager.getArtifactManager(),
82
109
  getAgentId: () => MAIN_AGENT_ID,
83
- getSessionSpawns: () => "sonic",
110
+ getSessionSpawns: () => "sonic,task",
84
111
  getModelString: () => modelSelector,
85
112
  getActiveModelString: () => modelSelector,
86
113
  getActiveModel: () => resolved.model,
@@ -94,6 +121,23 @@ export async function createCleanseAgentRuntime(options: {
94
121
  return {
95
122
  model: modelDisplay,
96
123
  sessionFile,
124
+ async discoverCheckers(request: string, signal?: AbortSignal): Promise<CustomCleanseCheckerSpec[]> {
125
+ sessionManager.appendCustomEntry("cleanse_discovery", { request });
126
+ const result = await runStructuredSubagent({
127
+ session: toolSession,
128
+ invocationKind: "task",
129
+ assignment: prompt.render(discoveryPrompt, { request }),
130
+ agent: "task",
131
+ model: modelSelector,
132
+ outputSchema: DISCOVERY_SCHEMA,
133
+ identity: { label: "CleanseDiscovery" },
134
+ enableLsp: true,
135
+ enableIrc: false,
136
+ signal,
137
+ });
138
+ if (result.result.error) throw new Error(`Checker discovery failed: ${result.result.error}`);
139
+ return parseDiscoverySpecs(result.result.structuredOutput?.data);
140
+ },
97
141
  async dispatch(
98
142
  assignments: CleanseAssignment[],
99
143
  wave: number,
@@ -166,6 +210,27 @@ export async function createCleanseAgentRuntime(options: {
166
210
  };
167
211
  }
168
212
 
213
+ /** Defensively validate discovery-agent output into runnable checker specs. */
214
+ function parseDiscoverySpecs(data: unknown): CustomCleanseCheckerSpec[] {
215
+ if (!isRecord(data) || !Array.isArray(data.checkers)) return [];
216
+ const specs: CustomCleanseCheckerSpec[] = [];
217
+ for (const value of data.checkers) {
218
+ if (!isRecord(value)) continue;
219
+ const command = Array.isArray(value.command)
220
+ ? value.command.filter((part): part is string => typeof part === "string" && part.length > 0)
221
+ : [];
222
+ if (typeof value.label !== "string" || !value.label.trim() || command.length === 0) continue;
223
+ specs.push({
224
+ label: value.label.trim(),
225
+ language: typeof value.language === "string" ? value.language : undefined,
226
+ cwd: typeof value.cwd === "string" ? value.cwd : undefined,
227
+ command,
228
+ parser: typeof value.parser === "string" ? value.parser : undefined,
229
+ });
230
+ }
231
+ return specs;
232
+ }
233
+
169
234
  function renderAssignment(
170
235
  assignment: CleanseAssignment,
171
236
  allAssignments: readonly CleanseAssignment[],
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import { $which, isRecord, ptree, sanitizeText } from "@oh-my-pi/pi-utils";
4
4
  import * as git from "../utils/git";
5
- import { type CleanseParserKind, parseCleanseDiagnostics } from "./parsers";
5
+ import { CLEANSE_PARSER_KINDS, type CleanseParserKind, parseCleanseDiagnostics } from "./parsers";
6
6
  import type { CleanseCheckResult, CleanseDiagnostic, CleanseDiagnosticReport, SkippedCleanseCheck } from "./types";
7
7
 
8
8
  const IGNORED_DIRECTORIES: Record<string, true> = {
@@ -43,6 +43,8 @@ interface PlanRequest {
43
43
  parser: CleanseParserKind;
44
44
  executable?: string;
45
45
  mutates?: boolean;
46
+ /** Skip silently instead of recording a skip when the binary is missing (alternative toolings). */
47
+ optional?: boolean;
46
48
  }
47
49
 
48
50
  interface DiscoveryState {
@@ -58,10 +60,21 @@ export interface CleanseDiagnosticSuiteOptions {
58
60
  includeTests?: boolean;
59
61
  }
60
62
 
63
+ /** Identity and display metadata for one runnable checker. */
64
+ export interface CleanseCheckerDescriptor {
65
+ id: string;
66
+ label: string;
67
+ language: string;
68
+ command: string;
69
+ }
70
+
61
71
  /** Re-runnable checker set discovered from one project snapshot. */
62
72
  export interface CleanseDiagnosticSuite {
63
- readonly checkCount: number;
73
+ /** Every discovered checker; unaffected by {@link CleanseDiagnosticSuite.select}. */
74
+ readonly checkers: readonly CleanseCheckerDescriptor[];
64
75
  readonly skipped: readonly SkippedCleanseCheck[];
76
+ /** Narrow subsequent {@link CleanseDiagnosticSuite.run} calls to the named checker ids. */
77
+ select(ids: readonly string[]): void;
65
78
  run(signal?: AbortSignal): Promise<CleanseDiagnosticReport>;
66
79
  }
67
80
 
@@ -81,7 +94,7 @@ export async function discoverCleanseDiagnosticSuite(
81
94
  };
82
95
  await discoverRust(state);
83
96
  discoverGo(state);
84
- discoverPython(state);
97
+ await discoverPython(state);
85
98
  await discoverJavaScript(state);
86
99
  discoverRuby(state);
87
100
  discoverPhp(state);
@@ -96,29 +109,109 @@ export async function discoverCleanseDiagnosticSuite(
96
109
  discoverDotnet(state);
97
110
  discoverZig(state);
98
111
  discoverJvm(state);
112
+ discoverGitHubActions(state);
99
113
 
100
- const allowedFiles = new Set(state.files);
114
+ return createSuite(resolvedCwd, state.plans, state.skipped, new Set(state.files));
115
+ }
116
+
117
+ function createSuite(
118
+ projectCwd: string,
119
+ plans: readonly CheckerPlan[],
120
+ skipped: SkippedCleanseCheck[],
121
+ allowedFiles: ReadonlySet<string>,
122
+ ): CleanseDiagnosticSuite {
123
+ let active = [...plans];
101
124
  return {
102
- checkCount: state.plans.length,
103
- skipped: state.skipped,
125
+ checkers: plans.map(plan => ({ id: plan.id, label: plan.label, language: plan.language, command: plan.command })),
126
+ skipped,
127
+ select(ids: readonly string[]): void {
128
+ const wanted = new Set(ids);
129
+ active = plans.filter(plan => wanted.has(plan.id));
130
+ },
104
131
  async run(signal?: AbortSignal): Promise<CleanseDiagnosticReport> {
105
132
  const mutatingChecks: CleanseCheckResult[] = [];
106
- for (const plan of state.plans) {
107
- if (plan.mutates) mutatingChecks.push(await runChecker(plan, resolvedCwd, allowedFiles, signal));
133
+ for (const plan of active) {
134
+ if (plan.mutates) mutatingChecks.push(await runChecker(plan, projectCwd, allowedFiles, signal));
108
135
  }
109
136
  const parallelChecks = await Promise.all(
110
- state.plans.filter(plan => !plan.mutates).map(plan => runChecker(plan, resolvedCwd, allowedFiles, signal)),
137
+ active.filter(plan => !plan.mutates).map(plan => runChecker(plan, projectCwd, allowedFiles, signal)),
111
138
  );
112
139
  const checks = [...mutatingChecks, ...parallelChecks];
113
140
  return {
114
141
  checks,
115
142
  diagnostics: deduplicateProjectDiagnostics(checks.flatMap(check => check.diagnostics)),
116
- skipped: [...state.skipped],
143
+ skipped: [...skipped],
117
144
  };
118
145
  },
119
146
  };
120
147
  }
121
148
 
149
+ /** One checker proposed by the prompted discovery agent. */
150
+ export interface CustomCleanseCheckerSpec {
151
+ label: string;
152
+ language?: string;
153
+ cwd?: string;
154
+ command: string[];
155
+ parser?: string;
156
+ }
157
+
158
+ /** Build a runnable suite from discovery-agent checker specs, dropping unrunnable entries into `skipped`. */
159
+ export async function buildCustomCleanseSuite(
160
+ projectCwd: string,
161
+ specs: readonly CustomCleanseCheckerSpec[],
162
+ ): Promise<CleanseDiagnosticSuite> {
163
+ const absoluteCwd = path.resolve(projectCwd);
164
+ const resolvedCwd = await fs.realpath(absoluteCwd).catch(() => absoluteCwd);
165
+ const files = await listProjectFiles(resolvedCwd);
166
+ const plans: CheckerPlan[] = [];
167
+ const skipped: SkippedCleanseCheck[] = [];
168
+ for (const [index, spec] of specs.entries()) {
169
+ const [binary, ...args] = spec.command;
170
+ const label = spec.label.trim() || `custom checker ${index + 1}`;
171
+ const language = spec.language?.trim() || "Custom";
172
+ if (!binary) {
173
+ skipped.push({ label, language, reason: "empty command" });
174
+ continue;
175
+ }
176
+ const root = normalizeCustomRoot(resolvedCwd, spec.cwd);
177
+ if (root === undefined) {
178
+ skipped.push({ label, language, reason: `working directory escapes the project: ${spec.cwd}` });
179
+ continue;
180
+ }
181
+ let executable: string | undefined;
182
+ if (/[\\/]/.test(binary)) {
183
+ const candidate = path.resolve(resolvedCwd, root, binary);
184
+ executable = (await Bun.file(candidate).exists()) ? candidate : undefined;
185
+ } else {
186
+ executable = resolveBinary(resolvedCwd, root, [binary]);
187
+ }
188
+ if (!executable) {
189
+ skipped.push({ label, language, reason: `executable not found: ${binary}` });
190
+ continue;
191
+ }
192
+ plans.push({
193
+ id: `custom-${index + 1}`,
194
+ label,
195
+ language,
196
+ cwd: path.resolve(resolvedCwd, root),
197
+ executable,
198
+ args,
199
+ parser: CLEANSE_PARSER_KINDS.find(kind => kind === spec.parser) ?? "generic",
200
+ command: formatCommand([path.basename(executable), ...args]),
201
+ mutates: false,
202
+ });
203
+ }
204
+ return createSuite(resolvedCwd, plans, skipped, new Set(files));
205
+ }
206
+
207
+ function normalizeCustomRoot(projectCwd: string, cwd: string | undefined): string | undefined {
208
+ const trimmed = cwd?.trim();
209
+ if (!trimmed || trimmed === ".") return ".";
210
+ const relative = path.relative(projectCwd, path.resolve(projectCwd, trimmed));
211
+ if (relative.startsWith("..") || path.isAbsolute(relative)) return undefined;
212
+ return relative.split(path.sep).join("/") || ".";
213
+ }
214
+
122
215
  async function listProjectFiles(cwd: string): Promise<string[]> {
123
216
  try {
124
217
  const [tracked, untracked] = await Promise.all([git.ls.files(cwd), git.ls.untracked(cwd)]);
@@ -187,8 +280,8 @@ function rootsForPattern(files: readonly string[], pattern: RegExp): string[] {
187
280
  return markerRoots(files, file => pattern.test(file));
188
281
  }
189
282
 
190
- function resolveBinary(state: DiscoveryState, root: string, names: readonly string[]): string | undefined {
191
- const cwd = path.resolve(state.projectCwd, root);
283
+ function resolveBinary(projectCwd: string, root: string, names: readonly string[]): string | undefined {
284
+ const cwd = path.resolve(projectCwd, root);
192
285
  const searchDirectories: string[] = [];
193
286
  let current = cwd;
194
287
  while (true) {
@@ -198,10 +291,9 @@ function resolveBinary(state: DiscoveryState, root: string, names: readonly stri
198
291
  path.join(current, "venv", process.platform === "win32" ? "Scripts" : "bin"),
199
292
  path.join(current, "vendor", "bin"),
200
293
  );
201
- if (current === state.projectCwd) break;
294
+ if (current === projectCwd) break;
202
295
  const parent = path.dirname(current);
203
- if (parent === current || (!parent.startsWith(`${state.projectCwd}${path.sep}`) && parent !== state.projectCwd))
204
- break;
296
+ if (parent === current || (!parent.startsWith(`${projectCwd}${path.sep}`) && parent !== projectCwd)) break;
205
297
  current = parent;
206
298
  }
207
299
  const searchPath = [...new Set(searchDirectories), Bun.env.PATH ?? ""].filter(Boolean).join(path.delimiter);
@@ -214,9 +306,10 @@ function resolveBinary(state: DiscoveryState, root: string, names: readonly stri
214
306
 
215
307
  function addPlan(state: DiscoveryState, request: PlanRequest): void {
216
308
  const cwd = path.resolve(state.projectCwd, request.root);
217
- const executable = request.executable ?? resolveBinary(state, request.root, request.binaries);
309
+ const executable = request.executable ?? resolveBinary(state.projectCwd, request.root, request.binaries);
218
310
  const rootLabel = request.root === "." ? "." : request.root;
219
311
  if (!executable) {
312
+ if (request.optional) return;
220
313
  state.skipped.push({
221
314
  label: `${request.label} (${rootLabel})`,
222
315
  language: request.language,
@@ -244,7 +337,7 @@ function formatCommand(argv: readonly string[]): string {
244
337
 
245
338
  async function discoverRust(state: DiscoveryState): Promise<void> {
246
339
  for (const root of rootsForBasenames(state.files, { "Cargo.toml": true })) {
247
- const cargo = resolveBinary(state, root, ["cargo"]);
340
+ const cargo = resolveBinary(state.projectCwd, root, ["cargo"]);
248
341
  if (!cargo) {
249
342
  addPlan(state, {
250
343
  id: "clippy",
@@ -363,6 +456,7 @@ function discoverGo(state: DiscoveryState): void {
363
456
  const workRoots = rootsForBasenames(state.files, { "go.work": true });
364
457
  const roots = workRoots.length > 0 ? workRoots : rootsForBasenames(state.files, { "go.mod": true });
365
458
  for (const root of roots) {
459
+ const prefix = root === "." ? "" : `${root}/`;
366
460
  addPlan(state, {
367
461
  id: "go-vet",
368
462
  label: "go vet",
@@ -372,6 +466,16 @@ function discoverGo(state: DiscoveryState): void {
372
466
  args: ["vet", "-json", "./..."],
373
467
  parser: "go",
374
468
  });
469
+ addPlan(state, {
470
+ id: "staticcheck",
471
+ label: "staticcheck",
472
+ language: "Go",
473
+ root,
474
+ binaries: ["staticcheck"],
475
+ args: ["-f", "json", "./..."],
476
+ parser: "staticcheck",
477
+ optional: !state.files.includes(`${prefix}staticcheck.conf`),
478
+ });
375
479
  if (state.includeTests) {
376
480
  addPlan(state, {
377
481
  id: "go-test",
@@ -384,9 +488,20 @@ function discoverGo(state: DiscoveryState): void {
384
488
  });
385
489
  }
386
490
  }
491
+ for (const root of rootsForPattern(state.files, /(?:^|\/)\.golangci\.(?:ya?ml|toml|json)$/)) {
492
+ addPlan(state, {
493
+ id: "golangci",
494
+ label: "golangci-lint",
495
+ language: "Go",
496
+ root,
497
+ binaries: ["golangci-lint"],
498
+ args: ["run"],
499
+ parser: "golangci",
500
+ });
501
+ }
387
502
  }
388
503
 
389
- function discoverPython(state: DiscoveryState): void {
504
+ async function discoverPython(state: DiscoveryState): Promise<void> {
390
505
  if (!containsExtension(state.files, [".py", ".pyi"])) return;
391
506
  const roots = rootsForBasenames(state.files, {
392
507
  ".ruff.toml": true,
@@ -423,11 +538,70 @@ function discoverPython(state: DiscoveryState): void {
423
538
  label: "pyright",
424
539
  language: "Python",
425
540
  root,
426
- binaries: ["pyright"],
541
+ binaries: ["pyright", "basedpyright"],
427
542
  args: ["--outputjson"],
428
543
  parser: "pyright",
429
544
  });
430
545
  }
546
+ const pyprojectRoots = rootsForBasenames(state.files, { "pyproject.toml": true });
547
+ const pyprojectContent = new Map<string, string>();
548
+ for (const root of pyprojectRoots) {
549
+ const file = root === "." ? "pyproject.toml" : `${root}/pyproject.toml`;
550
+ pyprojectContent.set(
551
+ root,
552
+ await Bun.file(path.resolve(state.projectCwd, file))
553
+ .text()
554
+ .catch(() => ""),
555
+ );
556
+ }
557
+ const withSection = (section: string): string[] =>
558
+ pyprojectRoots.filter(root => pyprojectContent.get(root)?.includes(section) === true);
559
+ const toolRoots = (basenames: Record<string, true>, section: string): string[] =>
560
+ topmostRoots([...rootsForBasenames(state.files, basenames), ...withSection(section)]);
561
+ for (const root of toolRoots({ "mypy.ini": true, ".mypy.ini": true }, "[tool.mypy")) {
562
+ addPlan(state, {
563
+ id: "mypy",
564
+ label: "mypy",
565
+ language: "Python",
566
+ root,
567
+ binaries: ["mypy"],
568
+ args: ["--no-error-summary", "--no-pretty", "."],
569
+ parser: "mypy",
570
+ });
571
+ }
572
+ for (const root of toolRoots({ ".pylintrc": true, pylintrc: true }, "[tool.pylint")) {
573
+ addPlan(state, {
574
+ id: "pylint",
575
+ label: "pylint",
576
+ language: "Python",
577
+ root,
578
+ binaries: ["pylint"],
579
+ args: ["--output-format=json", "--recursive=y", "."],
580
+ parser: "pylint",
581
+ });
582
+ }
583
+ for (const root of rootsForBasenames(state.files, { ".flake8": true })) {
584
+ addPlan(state, {
585
+ id: "flake8",
586
+ label: "flake8",
587
+ language: "Python",
588
+ root,
589
+ binaries: ["flake8"],
590
+ args: ["."],
591
+ parser: "flake8",
592
+ });
593
+ }
594
+ for (const root of toolRoots({ "ty.toml": true }, "[tool.ty")) {
595
+ addPlan(state, {
596
+ id: "ty",
597
+ label: "ty check",
598
+ language: "Python",
599
+ root,
600
+ binaries: ["ty"],
601
+ args: ["check", "--output-format", "concise"],
602
+ parser: "ty",
603
+ });
604
+ }
431
605
  }
432
606
 
433
607
  async function discoverJavaScript(state: DiscoveryState): Promise<void> {
@@ -457,16 +631,53 @@ async function discoverJavaScript(state: DiscoveryState): Promise<void> {
457
631
  });
458
632
  }
459
633
  for (const root of rootsForBasenames(state.files, { "tsconfig.json": true })) {
634
+ const prefix = root === "." ? "" : `${root}/`;
635
+ const hasVue = state.files.some(file => file.startsWith(prefix) && file.endsWith(".vue"));
460
636
  addPlan(state, {
461
637
  id: "typescript",
462
- label: "TypeScript",
638
+ label: hasVue ? "Vue TypeScript" : "TypeScript",
463
639
  language: "TypeScript",
464
640
  root,
465
- binaries: ["tsgo", "tsc"],
641
+ binaries: hasVue ? ["vue-tsc", "tsgo", "tsc"] : ["tsgo", "tsc"],
466
642
  args: ["--noEmit", "--pretty", "false"],
467
643
  parser: "generic",
468
644
  });
469
645
  }
646
+ for (const root of rootsForBasenames(state.files, { ".oxlintrc.json": true })) {
647
+ addPlan(state, {
648
+ id: "oxlint",
649
+ label: "oxlint",
650
+ language: "JavaScript/TypeScript",
651
+ root,
652
+ binaries: ["oxlint"],
653
+ args: ["--format=unix"],
654
+ parser: "oxlint",
655
+ });
656
+ }
657
+ for (const root of rootsForBasenames(state.files, { "deno.json": true, "deno.jsonc": true })) {
658
+ addPlan(state, {
659
+ id: "deno-lint",
660
+ label: "deno lint",
661
+ language: "JavaScript/TypeScript",
662
+ root,
663
+ binaries: ["deno"],
664
+ args: ["lint", "--json"],
665
+ parser: "deno-lint",
666
+ });
667
+ }
668
+ const stylelintConfig =
669
+ /(?:^|\/)(?:\.stylelintrc(?:\.(?:json|ya?ml|js|cjs|mjs))?|stylelint\.config\.(?:js|cjs|mjs|ts))$/;
670
+ for (const root of rootsForPattern(state.files, stylelintConfig)) {
671
+ addPlan(state, {
672
+ id: "stylelint",
673
+ label: "stylelint",
674
+ language: "CSS",
675
+ root,
676
+ binaries: ["stylelint"],
677
+ args: ["**/*.{css,scss,sass,less}", "--formatter", "json", "--allow-empty-input"],
678
+ parser: "stylelint",
679
+ });
680
+ }
470
681
  if (state.includeTests) await discoverPackageTests(state);
471
682
  }
472
683
 
@@ -905,6 +1116,26 @@ function discoverJvm(state: DiscoveryState): void {
905
1116
  }
906
1117
  }
907
1118
 
1119
+ function discoverGitHubActions(state: DiscoveryState): void {
1120
+ const roots = new Set<string>();
1121
+ for (const file of state.files) {
1122
+ const match = /^(?:(.*)\/)?\.github\/workflows\/[^/]+\.ya?ml$/.exec(file);
1123
+ if (match) roots.add(match[1] ?? ".");
1124
+ }
1125
+ for (const root of topmostRoots([...roots])) {
1126
+ addPlan(state, {
1127
+ id: "actionlint",
1128
+ label: "actionlint",
1129
+ language: "GitHub Actions",
1130
+ root,
1131
+ binaries: ["actionlint"],
1132
+ args: ["-format", "{{json .}}"],
1133
+ parser: "actionlint",
1134
+ optional: true,
1135
+ });
1136
+ }
1137
+ }
1138
+
908
1139
  async function runChecker(
909
1140
  plan: CheckerPlan,
910
1141
  projectCwd: string,