@gavin-hu/my-pi-agent 0.14.0 → 0.15.1

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 (229) hide show
  1. package/README.md +54 -34
  2. package/README.zh-CN.md +44 -28
  3. package/extensions/bytefly/README.md +193 -140
  4. package/extensions/bytefly/block/component.ts +7 -15
  5. package/extensions/bytefly/block/register.ts +6 -1
  6. package/extensions/bytefly/doctor/register.ts +24 -5
  7. package/extensions/bytefly/doctor/tools.ts +4 -0
  8. package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
  9. package/extensions/bytefly/footer/config.ts +10 -6
  10. package/extensions/bytefly/footer/price/price.ts +5 -3
  11. package/extensions/bytefly/footer/price/rates.ts +63 -26
  12. package/extensions/bytefly/footer/render/status.ts +3 -3
  13. package/extensions/bytefly/header/component.ts +3 -2
  14. package/extensions/bytefly/index.ts +3 -3
  15. package/extensions/bytefly/notice/component.ts +8 -3
  16. package/extensions/bytefly/notify/config.ts +6 -11
  17. package/extensions/bytefly/notify/dispatch.ts +13 -5
  18. package/extensions/bytefly/panel/commands.ts +21 -1
  19. package/extensions/bytefly/panel/format.ts +5 -2
  20. package/extensions/bytefly/panel/settings-runtime.ts +26 -6
  21. package/extensions/bytefly/panel/settings.ts +41 -4
  22. package/extensions/bytefly/panel/themes.ts +32 -0
  23. package/extensions/bytefly/panel/tui/editing.ts +77 -0
  24. package/extensions/bytefly/panel/tui/entries.ts +24 -0
  25. package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
  26. package/extensions/bytefly/panel/tui/frame.ts +121 -4
  27. package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
  28. package/extensions/bytefly/panel/tui.ts +94 -152
  29. package/extensions/bytefly/picker/discovery.ts +1 -0
  30. package/extensions/bytefly/picker/types.ts +2 -0
  31. package/extensions/bytefly/separator/component.ts +14 -4
  32. package/extensions/bytefly/update/cache.ts +58 -19
  33. package/extensions/bytefly/update/check.ts +35 -0
  34. package/extensions/bytefly/update/format.ts +76 -35
  35. package/extensions/bytefly/update/register.ts +144 -55
  36. package/extensions/bytefly/update/state.ts +4 -2
  37. package/extensions/bytefly/update/suppress.ts +66 -0
  38. package/extensions/bytefly/update/types.ts +15 -0
  39. package/extensions/check/README.md +253 -0
  40. package/extensions/check/commands.ts +128 -0
  41. package/extensions/check/config.ts +39 -0
  42. package/extensions/check/detect.ts +269 -0
  43. package/extensions/check/failures.ts +230 -0
  44. package/extensions/check/format.ts +135 -0
  45. package/extensions/check/index.ts +26 -0
  46. package/extensions/check/render.ts +185 -0
  47. package/extensions/check/runner.ts +176 -0
  48. package/extensions/check/runtime.ts +244 -0
  49. package/extensions/check/source.ts +80 -0
  50. package/extensions/check/tools.ts +173 -0
  51. package/extensions/check/tui.ts +195 -0
  52. package/extensions/check/types.ts +99 -0
  53. package/extensions/document/README.md +1 -1
  54. package/extensions/document/extract/pdf.ts +8 -16
  55. package/extensions/document/formats.ts +2 -2
  56. package/extensions/file-browse/README.md +126 -37
  57. package/extensions/file-browse/client.ts +98 -33
  58. package/extensions/file-browse/config.ts +24 -9
  59. package/extensions/file-browse/files.ts +183 -48
  60. package/extensions/file-browse/fuzzy.ts +65 -11
  61. package/extensions/file-browse/git.ts +37 -47
  62. package/extensions/file-browse/html.ts +97 -171
  63. package/extensions/file-browse/live-client.ts +158 -0
  64. package/extensions/file-browse/live.ts +222 -0
  65. package/extensions/file-browse/markdown.ts +110 -92
  66. package/extensions/file-browse/raw-html.ts +478 -0
  67. package/extensions/file-browse/router.ts +65 -78
  68. package/extensions/file-browse/server.ts +75 -59
  69. package/extensions/file-browse/styles.ts +124 -0
  70. package/extensions/file-browse/urls.ts +80 -0
  71. package/extensions/goal/README.md +103 -47
  72. package/extensions/goal/clarify-parse.ts +4 -9
  73. package/extensions/goal/clarify.ts +27 -4
  74. package/extensions/goal/commands.ts +24 -11
  75. package/extensions/goal/format.ts +41 -10
  76. package/extensions/goal/index.ts +39 -16
  77. package/extensions/goal/plan-route.ts +51 -0
  78. package/extensions/goal/schema.ts +6 -23
  79. package/extensions/goal/state.ts +39 -57
  80. package/extensions/goal/tools.ts +26 -20
  81. package/extensions/goal/types.ts +11 -1
  82. package/extensions/job/README.md +20 -6
  83. package/extensions/job/format.ts +18 -68
  84. package/extensions/job/index.ts +26 -41
  85. package/extensions/job/log-writer.ts +208 -0
  86. package/extensions/job/logs.ts +3 -2
  87. package/extensions/job/process.ts +6 -99
  88. package/extensions/job/runtime.ts +42 -181
  89. package/extensions/job/schema.ts +1 -4
  90. package/extensions/job/tools.ts +1 -15
  91. package/extensions/job/tui.ts +117 -307
  92. package/extensions/job/ui.ts +7 -33
  93. package/extensions/memory/README.md +1 -1
  94. package/extensions/memory/errors.ts +5 -6
  95. package/extensions/memory/tui.ts +14 -24
  96. package/extensions/plan/README.md +94 -18
  97. package/extensions/plan/commands.ts +9 -5
  98. package/extensions/plan/format.ts +87 -0
  99. package/extensions/plan/index.ts +31 -15
  100. package/extensions/plan/plans.ts +2 -3
  101. package/extensions/plan/policy.ts +0 -6
  102. package/extensions/plan/runtime.ts +4 -2
  103. package/extensions/plan/steps.ts +37 -8
  104. package/extensions/plan/todos.ts +44 -3
  105. package/extensions/plan/tools/enter.ts +94 -0
  106. package/extensions/plan/tools/exit.ts +296 -0
  107. package/extensions/plan/tools/nested.ts +21 -0
  108. package/extensions/plan/tools/steps.ts +136 -0
  109. package/extensions/plan/tools/todo-seed.ts +76 -0
  110. package/extensions/plan/tools/write.ts +127 -0
  111. package/extensions/plan/tools.ts +18 -703
  112. package/extensions/plan/web-client.ts +6 -22
  113. package/extensions/plan/web-page.ts +19 -30
  114. package/extensions/plan/web-router.ts +32 -79
  115. package/extensions/plan/web.ts +21 -42
  116. package/extensions/question/answers.ts +3 -8
  117. package/extensions/question/tui.ts +6 -6
  118. package/extensions/rewind/README.md +95 -22
  119. package/extensions/rewind/checklist.ts +192 -0
  120. package/extensions/rewind/commands.ts +8 -0
  121. package/extensions/rewind/config.ts +3 -11
  122. package/extensions/rewind/format.ts +220 -2
  123. package/extensions/rewind/git.ts +101 -21
  124. package/extensions/rewind/hunk-picker.ts +114 -0
  125. package/extensions/rewind/patch.ts +88 -0
  126. package/extensions/rewind/path-picker.ts +67 -100
  127. package/extensions/rewind/paths.ts +93 -19
  128. package/extensions/rewind/restore.ts +53 -10
  129. package/extensions/rewind/review-tui.ts +320 -0
  130. package/extensions/rewind/review.ts +397 -0
  131. package/extensions/rewind/rewind-tui.ts +4 -3
  132. package/extensions/rewind/runtime.ts +94 -17
  133. package/extensions/rewind/snapshot.ts +13 -4
  134. package/extensions/subagent/README.md +61 -6
  135. package/extensions/subagent/background-tui.ts +208 -0
  136. package/extensions/subagent/background.ts +470 -0
  137. package/extensions/subagent/commands.ts +80 -0
  138. package/extensions/subagent/config.ts +4 -0
  139. package/extensions/subagent/format.ts +24 -1
  140. package/extensions/subagent/index.ts +91 -2
  141. package/extensions/subagent/render.ts +13 -0
  142. package/extensions/subagent/run.ts +17 -16
  143. package/extensions/subagent/schema.ts +12 -0
  144. package/extensions/subagent/types.ts +11 -0
  145. package/extensions/subagent/ui.ts +50 -0
  146. package/extensions/todo/README.md +18 -13
  147. package/extensions/todo/format.ts +3 -2
  148. package/extensions/todo/runtime.ts +3 -2
  149. package/extensions/todo/schema.ts +8 -84
  150. package/extensions/todo/state.ts +5 -46
  151. package/extensions/todo/tools.ts +2 -1
  152. package/extensions/todo/types.ts +6 -15
  153. package/extensions/watch/README.md +14 -6
  154. package/extensions/watch/child.ts +150 -0
  155. package/extensions/watch/format.ts +8 -48
  156. package/extensions/watch/index.ts +42 -58
  157. package/extensions/watch/runtime.ts +49 -128
  158. package/extensions/watch/schema.ts +1 -4
  159. package/extensions/watch/tools.ts +1 -11
  160. package/extensions/watch/tui.ts +103 -273
  161. package/extensions/watch/ui.ts +5 -32
  162. package/extensions/web-access/README.md +1 -1
  163. package/extensions/web-access/fetch/page.ts +1 -1
  164. package/extensions/web-access/fetch/tool.ts +6 -2
  165. package/extensions/web-access/search/search.ts +2 -5
  166. package/extensions/wechat/README.md +1 -1
  167. package/extensions/wechat/client.ts +23 -0
  168. package/extensions/wechat/commands.ts +57 -42
  169. package/extensions/wechat/deliver.ts +5 -9
  170. package/extensions/wechat/errors.ts +3 -6
  171. package/extensions/wechat/inbound.ts +2 -5
  172. package/extensions/wechat/index.ts +2 -8
  173. package/extensions/wechat/outbound.ts +2 -4
  174. package/extensions/wechat/peer-worker.ts +3 -8
  175. package/extensions/wechat/poll.ts +10 -9
  176. package/extensions/wechat/presence.ts +35 -62
  177. package/extensions/wechat/reply-pipeline.ts +3 -1
  178. package/extensions/wechat/status-screen.ts +1 -1
  179. package/extensions/wechat/timer.ts +57 -0
  180. package/extensions/worktree/config.ts +8 -20
  181. package/extensions/worktree/enter.ts +7 -12
  182. package/extensions/worktree/git.ts +38 -69
  183. package/extensions/worktree/index.ts +5 -4
  184. package/extensions/worktree/registry.ts +7 -7
  185. package/extensions/worktree/root-tools.ts +2 -2
  186. package/extensions/worktree/status.ts +15 -9
  187. package/lib/README.md +23 -8
  188. package/lib/config.ts +50 -3
  189. package/lib/errors.ts +12 -0
  190. package/lib/format.ts +50 -0
  191. package/lib/git/read.ts +28 -0
  192. package/lib/goal-state.ts +23 -8
  193. package/lib/http/headers.ts +74 -0
  194. package/lib/http/index.ts +13 -0
  195. package/lib/http/response.ts +46 -0
  196. package/lib/http/router.ts +52 -0
  197. package/lib/http/server.ts +109 -0
  198. package/lib/lines.ts +77 -0
  199. package/lib/pdf.ts +82 -0
  200. package/lib/plan-entry.ts +71 -0
  201. package/lib/plan-state.ts +34 -0
  202. package/lib/pool.ts +62 -0
  203. package/lib/report.ts +99 -0
  204. package/lib/requirements.ts +25 -19
  205. package/lib/spawn.ts +170 -0
  206. package/lib/todo-state.ts +110 -23
  207. package/lib/tool-names.ts +7 -0
  208. package/lib/tool-result.ts +33 -0
  209. package/lib/tui/README.md +68 -3
  210. package/lib/tui/chrome.ts +28 -4
  211. package/lib/tui/columns.ts +6 -1
  212. package/lib/tui/confirm.ts +65 -0
  213. package/lib/tui/dock-screen.ts +418 -0
  214. package/lib/tui/glyphs.ts +2 -1
  215. package/lib/tui/indent.ts +1 -1
  216. package/lib/tui/index.ts +2 -0
  217. package/lib/tui/rows.ts +21 -0
  218. package/lib/tui/status-controller.ts +35 -1
  219. package/lib/tui/tool.ts +59 -1
  220. package/lib/web/components.ts +20 -1
  221. package/lib/web/markup.ts +32 -0
  222. package/lib/web/runtime.ts +50 -0
  223. package/package.json +22 -5
  224. package/skills/init/SKILL.md +165 -0
  225. package/skills/init/references/agents-md-quality.md +103 -0
  226. package/skills/init/references/interview-questions.md +81 -0
  227. package/skills/refactor/SKILL.md +141 -0
  228. package/extensions/watch/process.ts +0 -63
  229. package/extensions/web-access/fetch/pdf.ts +0 -45
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Pi suppresses its own package-update block while bytefly owns the surface.
3
+ *
4
+ * Pi draws `Package Updates Available` from `InteractiveMode.run()` after the
5
+ * session starts, with no setting or extension event to turn it off. The one
6
+ * public seam is the package manager it calls: replacing
7
+ * `checkForAvailableUpdates` with an empty result suppresses that block, while
8
+ * the captured original still serves bytefly's own query. The patch is
9
+ * process-wide, so it is installed at `session_start` and restored at
10
+ * `session_shutdown`.
11
+ *
12
+ * A future host that changes the method shape makes the patch a no-op rather
13
+ * than an error; the update surface then falls back to Pi's block.
14
+ */
15
+
16
+ import { DefaultPackageManager } from "@earendil-works/pi-coding-agent";
17
+ import type { PackageUpdate, PackageUpdateCheck } from "./types.ts";
18
+
19
+ /** A patchable class prototype, kept narrow so a test needs no host object. */
20
+ export interface PackageManagerPrototype {
21
+ checkForAvailableUpdates: PackageUpdateCheck;
22
+ }
23
+
24
+ /** The installed suppression: the real check, and how to restore Pi's method. */
25
+ export interface UpdateSuppression {
26
+ /** The original method, bound at call time so it still sees its manager. */
27
+ readonly check: PackageUpdateCheck;
28
+ /** Put Pi's method back; safe to call more than once. */
29
+ restore(): void;
30
+ }
31
+
32
+ /** Marker for the installed patch, so a second registration reuses it. */
33
+ const SUPPRESSION = Symbol.for("@gavin-hu/my-pi-agent/pi-update-suppression");
34
+
35
+ interface MarkedPrototype extends PackageManagerPrototype {
36
+ [SUPPRESSION]?: UpdateSuppression;
37
+ }
38
+
39
+ /**
40
+ * Replace Pi's package-update check with one that reports nothing.
41
+ *
42
+ * `prototype` is a seam for tests; production passes Pi's
43
+ * `DefaultPackageManager.prototype`. When the method is missing, the returned
44
+ * suppression is inert and the original read stays `[]`.
45
+ */
46
+ export function suppressPiPackageUpdates(
47
+ prototype: PackageManagerPrototype = DefaultPackageManager.prototype as PackageManagerPrototype,
48
+ ): UpdateSuppression {
49
+ const marked = prototype as MarkedPrototype;
50
+ const existing = marked[SUPPRESSION];
51
+ if (existing) return existing;
52
+
53
+ const original = prototype.checkForAvailableUpdates;
54
+ if (typeof original !== "function") return { check: async () => [], restore: () => {} };
55
+ const originalCheck = original as unknown as (this: DefaultPackageManager) => Promise<PackageUpdate[]>;
56
+ const check: PackageUpdateCheck = (manager) => originalCheck.call(manager);
57
+ const patched: PackageUpdateCheck = async () => [];
58
+ const restore = (): void => {
59
+ if (prototype.checkForAvailableUpdates === patched) prototype.checkForAvailableUpdates = original;
60
+ delete marked[SUPPRESSION];
61
+ };
62
+ const suppression: UpdateSuppression = { check, restore };
63
+ prototype.checkForAvailableUpdates = patched;
64
+ marked[SUPPRESSION] = suppression;
65
+ return suppression;
66
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The package-update shape Pi's manager returns.
3
+ *
4
+ * Pi exports the manager but not its `PackageUpdate` interface, so the type is
5
+ * derived from the public method. That keeps the extension on the same contract
6
+ * without reaching into a dist path or restating the fields.
7
+ */
8
+
9
+ import type { DefaultPackageManager } from "@earendil-works/pi-coding-agent";
10
+
11
+ /** One package Pi reports as updateable. */
12
+ export type PackageUpdate = Awaited<ReturnType<DefaultPackageManager["checkForAvailableUpdates"]>>[number];
13
+
14
+ /** Pi's check, captured before suppression and called with a manager. */
15
+ export type PackageUpdateCheck = (manager: DefaultPackageManager) => Promise<PackageUpdate[]>;
@@ -0,0 +1,253 @@
1
+ # check — cross-stack verification runs
2
+
3
+ Run the project's test, lint, or typecheck command from Pi, watch it live, and
4
+ turn its output into a navigable `file:line` list. The same run is available to
5
+ the model as `run_check`, with structured failures instead of a wall of log
6
+ text.
7
+
8
+ `check` is deliberately generic: it detects the project's command from its
9
+ manifest or takes `check.command`, and it does not assume Node, Bun, a test
10
+ framework, or a particular CI system.
11
+
12
+ ```
13
+ pi --extension ./extensions/check # load just this extension
14
+ pi -e . # load the whole @gavin-hu/my-pi-agent package
15
+ pi install ./ # install the package
16
+ ```
17
+
18
+ ## Screens
19
+
20
+ <img src="../../assets/check.png" alt="The /check screen: a failed run with failure rows and the focused failure's message" width="100%">
21
+
22
+ <img src="../../assets/check-detail.png" alt="The /check failure detail: the location and message over a source excerpt with the failing line marked by >" width="100%">
23
+
24
+ ## Tool rendering
25
+
26
+ The call line is `run_check <target>`. While the command runs, the result is one
27
+ line — `Running bun run check… · 12s` — and it repaints once a second, so a
28
+ silent compiler still shows progress. The settled block keeps the transcript to
29
+ a capped view of the run: a status summary, at most five failure rows, and the
30
+ bounded tail when nothing parsed. `ctrl+o` expands it to the model's 20-failure
31
+ cap; `/check` remains the full navigable list.
32
+
33
+ <img src="../../assets/tools/run_check.png" alt="The run_check transcript block: a status summary and two failure rows under a rail" width="100%">
34
+
35
+ A failed command whose output parsed no location names the exit status in the
36
+ summary, prints `no parsed failure; last output:`, and shows the last five tail
37
+ lines instead of an exit-status row. The transcript never prints the model
38
+ report; the full failure list and the tail stay in `details`.
39
+
40
+ ## What it does
41
+
42
+ - **Detects a command** from `package.json`, `Cargo.toml`, `go.mod`, a pytest
43
+ configuration, a `justfile`, or a `Makefile`, or uses `check.command`.
44
+ - **Runs it in a supervised process** with a wall-clock timeout and a bounded
45
+ log; `Esc` cancels the process tree.
46
+ - **Shows a live tail** while the command runs, then a failure list with a
47
+ detail view for the focused failure: a source excerpt around `path:line` when
48
+ the file is readable, otherwise the bounded tail.
49
+ - **Returns structured failures** to the model with `path`, `line`, `column`,
50
+ `test`, `message`, and `command`.
51
+ - **Falls back honestly**: a failed command with no recognizable failure becomes
52
+ one “Command exited with status N” entry, with the bounded tail in its detail.
53
+ - **One line on success**: `Check passed: bun run check · 12.4s`.
54
+
55
+ ## Tool
56
+
57
+ `run_check` takes optional `target`:
58
+
59
+ | Field | Value |
60
+ |---|---|
61
+ | `target` | `test`, `lint`, `typecheck`, or `all`; defaults to `all`. |
62
+ | `exposure` | `direct`, active by default. |
63
+ | `executionMode` | `sequential` — one run owns the process and the live buffer. |
64
+ | `annotations` | `readOnlyHint: false`, `destructiveHint: false`, `openWorldHint: false`. |
65
+ | `outputSchema` | `{ target, status, commands, exitCode, durationMs, truncated, failures[] }`, mirrored by `structuredContent`. |
66
+
67
+ `content` is a short summary plus up to 20 failures. `details` carries the full
68
+ bounded failure list and the tail, which the transcript block reads. A failing
69
+ check is a normal result (`isError: false`) so the model can fix it; a check
70
+ that cannot run — no detected command, a spawn failure, or another run already
71
+ active — is an error with the corrective action.
72
+
73
+ ## Command
74
+
75
+ | Command | What it does |
76
+ |---|---|
77
+ | `/check` | Run every detected command (or `check.command`). |
78
+ | `/check test` | Run the detected test command. |
79
+ | `/check lint` | Run the detected lint command. |
80
+ | `/check typecheck` | Run the detected typecheck command. |
81
+ | `/check all` | Same as `/check`. |
82
+
83
+ `getArgumentCompletions` offers the four targets. The command refuses while plan
84
+ mode is on: relying on tests is still writing to the project (caches, snapshots,
85
+ generated files), so the plan must be approved first.
86
+
87
+ ## Behaviour by mode
88
+
89
+ | Mode | Behaviour |
90
+ |---|---|
91
+ | Interactive TUI | A live run screen with a scrolling tail and `Esc cancel`; then a failure list with `Enter` detail, `r` rerun, and `Esc` close. A pass posts one notice. |
92
+ | RPC (`hasUI`) | Runs headless and posts the summary plus the failure list; no custom screen. |
93
+ | `print` / `json` | Runs headless and posts the same report. |
94
+ | `run_check` | No UI of its own. Returns the summary in `content`, the structured result, and a capped transcript block drawn from `details`. |
95
+
96
+ ## Configuration
97
+
98
+ The `check` section of the shared `bytefly.json` (global then project, project
99
+ wins). All keys are JSON-only; none has a `/bytefly settings` row.
100
+
101
+ | Key | Default | Bounds | Meaning |
102
+ |---|---|---|---|
103
+ | `command` | `""` | | When non-empty, run this shell command for every target instead of detection. |
104
+ | `timeoutMs` | `600000` | `0`–`3600000` | Wall-clock limit per command; `0` disables it. |
105
+ | `maxLogBytes` | `1048576` | `65536`–`16777216` | Bytes of tail retained per command. |
106
+
107
+ `command` is the escape hatch for any stack without a detector. It is a shell
108
+ command and runs with the project root as its working directory.
109
+
110
+ ## Detection
111
+
112
+ Detection order stops at the first family that declares a command:
113
+
114
+ | Manifest | `test` | `lint` | `typecheck` | `all` |
115
+ |---|---|---|---|---|
116
+ | `package.json` | `scripts.test` | `scripts.lint` | `scripts.typecheck` | `scripts.check`, else the detected targets |
117
+ | `Cargo.toml` | `cargo test` | `cargo clippy -- -D warnings` | `cargo check` | the detected targets |
118
+ | `go.mod` | `go test ./...` | `go vet ./...` | `go build ./...` | the detected targets |
119
+ | pytest config or `tests/` | `pytest` | `ruff check .` with `[tool.ruff]` | `mypy .` with `[tool.mypy]` | the detected targets |
120
+ | `justfile` | `just test` if declared | `just lint` if declared | `just typecheck` if declared | `just check` if declared, else the detected targets |
121
+ | `Makefile` | `make test` if declared | `make lint` if declared | — | `make check` if declared, else the detected targets |
122
+ | any other stack | — | — | — | `check.command` |
123
+
124
+ Node package managers come from the lockfile (`bun`, `pnpm`, `yarn`, `npm`;
125
+ default `npm`). Scripts run as `<pm> run <name>`; Make and Just recipes only by
126
+ their declared names. Nothing is guessed beyond those manifests.
127
+
128
+ A detector is one function returning `DetectedCommands`; adding a family is one
129
+ module entry, not a change to the runtime, screens, or tool.
130
+
131
+ ## Failure parsing
132
+
133
+ Known runner lines are recognized first; the generic compiler-style location is
134
+ the fallback for everything else:
135
+
136
+ | Output shape | Parser |
137
+ |---|---|
138
+ | `path:line(:col): error …` | generic, including a Windows `C:\…` path |
139
+ | `FAIL path` and `● test` | Vitest/Jest |
140
+ | `● test` and `at … (path:line:col)` | Jest/Vitest, Java, and bare stack frames |
141
+ | `File "path", line N, in test` | Python tracebacks |
142
+ | `FAILED path::test - message` | pytest |
143
+ | `--- FAIL: Test` and `path:line:` | Go |
144
+ | `panicked at path:line` / `thread 'test'` | Cargo |
145
+ | anything else on a non-zero exit | one fallback entry with the bounded tail |
146
+
147
+ Failures are deduped and capped at 200; the model report shows 20. A miss is
148
+ never turned into an invented location.
149
+
150
+ ## Security
151
+
152
+ `check` executes project scripts. That is the same trust boundary as running the
153
+ command yourself or letting the agent call `bash`: a project with a hostile
154
+ `test` script can do anything your user can do. The extension does not add a
155
+ sandbox. `/check` is user-invoked and refuses in plan mode; `run_check` declares
156
+ `readOnlyHint: false`, so a permission policy can gate it, and plan mode blocks
157
+ it.
158
+
159
+ The extension adds no network calls of its own. The command's environment is
160
+ made non-interactive (`CI=1`, `NO_COLOR=1`, `FORCE_COLOR=0`, `TERM=dumb`,
161
+ `GIT_PAGER=cat`, `PAGER=cat`).
162
+
163
+ ## Limitations
164
+
165
+ - Detection covers the families above. Other stacks need `check.command`.
166
+ - Parsing is best-effort. A custom reporter that prints neither a location nor a
167
+ known shape falls back to the tail, not to a guessed file.
168
+ - The failure list and detail are TUI-only; other modes print the report.
169
+ - A source excerpt is read only from a regular file inside the project root, at
170
+ most 512 KiB, and only when the reported line exists. A missing, binary,
171
+ oversized, or external file falls back to the bounded tail with the reason.
172
+ - There is no run history, retry policy, or watch mode. Each invocation is one
173
+ run.
174
+ - A test runner that opens a full-screen UI is not supported: stdout is a pipe,
175
+ stdin is closed, and `TERM=dumb`.
176
+ - The tail is bounded; earlier lines are dropped and marked truncated.
177
+
178
+ ## Non-goals
179
+
180
+ - Auto-fixing or editing code in response to a failure.
181
+ - A remote CI client, a scheduler, or a durable run queue.
182
+ - Watching files and rerunning on change.
183
+ - Parsing every reporter in existence; the generic location and the bounded tail
184
+ are the contract.
185
+ - Replacing `job` for long-lived processes.
186
+
187
+ ## Pi integration
188
+
189
+ | Aspect | Contract |
190
+ |---|---|
191
+ | Registration | `check(pi)` guards on `isExtensionEnabled("check")` and calls `registerCheck(pi)`, which registers `run_check`, `/check`, and one `session_shutdown` hook. The factory only registers. |
192
+ | Tool | `run_check`, `exposure: "direct"`, `executionMode: "sequential"`, annotations `{ readOnlyHint: false, destructiveHint: false, openWorldHint: false }`, TypeBox `parameters` and `outputSchema`, `structuredContent` mirror. |
193
+ | Command | `/check [target]` with `getArgumentCompletions` for the four targets; reports through `ctx.ui.notify`. |
194
+ | State | None persisted. The runtime closure owns the single active child, its controller, and the live line buffer; `session_shutdown` aborts it. |
195
+ | Lifecycle | Each run spawns one process per command and kills the tree on timeout, cancel, or shutdown. No `session_start` work. |
196
+ | Config | `check` section of `bytefly.json`, read once per run. |
197
+
198
+ ## Design notes
199
+
200
+ - **The excerpt is guarded like a path-taking tool.** The path comes from command
201
+ output, so `source.ts` resolves it, requires both string and real-path
202
+ containment under the project root, rejects non-files, binaries, and files over
203
+ 512 KiB, and checks the line before reading. A miss shows the tail, never a
204
+ guessed source line.
205
+ - **Why `check`.** It runs the project's *check* command, matching `bun run check`
206
+ and the scripts it detects. `/verify` describes a verdict rather than the
207
+ action, and `run_verify` reads worse than `run_check`; the extension,
208
+ command, tool, and config section all use one word.
209
+ - **Generic by design.** Only `detect.ts` knows manifests, and only `failures.ts`
210
+ knows runner output. The runtime, screens, and tool speak in `CommandSpec` and
211
+ `Failure`.
212
+ - **One-shot, not a job.** `job` owns reattachment, stdin, detached lifetime, and
213
+ a registry; `check` owns a foreground verdict. Reusing `job` would make both
214
+ surfaces worse, so the runner is local and smaller.
215
+ - **A bounded tail is the honest fallback.** A parser miss must not invent a
216
+ location; the exit status and the tail are useful on their own.
217
+ - **The transcript is a capped view.** `render.ts` draws from the same `details`
218
+ the model report reads but caps the rows and never prints the report; `/check`
219
+ is still the full navigable list.
220
+ - **The failure list is a `DockListScreen`.** The frame, the id-anchored cursor,
221
+ the navigation keys, the range row, and the detail pane come from
222
+ `lib/tui/dock-screen.ts`, the base `/jobs`, `/watch`, and `/agents` share, so
223
+ this module holds only the check vocabulary. A failure has no id, so its index
224
+ in the finished run's fixed array is the cursor key.
225
+ - **One run at a time.** The runtime refuses a second start instead of
226
+ interleaving outputs or processes.
227
+
228
+ ## Files
229
+
230
+ | File | Responsibility |
231
+ |---|---|
232
+ | `index.ts` | Registrar: guard, runtime, tool, command, shutdown. |
233
+ | `types.ts` | `CheckTarget`, `CommandSpec`, `Failure`, `CheckResult`, `RunCheckDetails`, `ActiveCheck`. |
234
+ | `config.ts` | `check` config defaults, clamps, and loader. |
235
+ | `detect.ts` | The detector registry and `check.command` planning. |
236
+ | `runner.ts` | One-shot streaming process, bounded log, timeout, cancel, tree kill. |
237
+ | `failures.ts` | Known-runner and generic failure parsers, stack frames, and the tail fallback. |
238
+ | `source.ts` | Guarded source excerpt around a failure's `path:line`. |
239
+ | `runtime.ts` | Planning, sequencing, aggregation, live lines, cancel/shutdown. |
240
+ | `format.ts` | Summary, failure rows, source-excerpt/detail text, model report. |
241
+ | `tui.ts` | `CheckRunComponent` (a live tail) and `CheckResultComponent` (a `DockListScreen` over the failures). |
242
+ | `tools.ts` | `run_check` schema, registration, the running partial result, and the call/result renderers. |
243
+ | `render.ts` | The transcript result block: status summary, capped failure rows, tail fallback. |
244
+ | `commands.ts` | `/check`, target parsing, completions, TUI/headless flow. |
245
+
246
+ ## Testing
247
+
248
+ Co-located tests cover config clamps, every detector family, the runner's
249
+ streaming/timeout/cancel/log cap, the parsers, command planning, aggregation,
250
+ the tool's output contract and transcript block, the command's guards and
251
+ headless path, and both screens' keys and width bounds.
252
+ `extensions/check/check.test.ts` runs a real Node script through the extension,
253
+ so the process path is exercised without depending on Bun or a test framework.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * `/check` — run the project's verification command with a live tail and a
3
+ * navigable failure list.
4
+ *
5
+ * The TUI gets the two screens; RPC, print, and JSON run headless and report
6
+ * the summary plus the failure list. The command refuses while plan mode is on
7
+ * because running tests is a write to the project (caches, logs, generated
8
+ * files), and the tool is separately blocked by its annotation.
9
+ */
10
+
11
+ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
12
+ import { resolveEffectiveCwd } from "../../lib/env.ts";
13
+ import { isPlanModeEnabled } from "../../lib/plan-state.ts";
14
+ import { withRailsSuppressed } from "../../lib/rails.ts";
15
+ import { formatCheckReport, formatCheckSummary } from "./format.ts";
16
+ import { CheckError, type CheckRuntime } from "./runtime.ts";
17
+ import { CheckResultComponent, CheckRunComponent } from "./tui.ts";
18
+ import type { CheckResult, CheckTarget } from "./types.ts";
19
+
20
+ /** The accepted targets and their completion descriptions. */
21
+ const TARGETS: ReadonlyArray<{ value: CheckTarget; description: string }> = [
22
+ { value: "test", description: "Run the test command" },
23
+ { value: "lint", description: "Run the lint command" },
24
+ { value: "typecheck", description: "Run the typecheck command" },
25
+ { value: "all", description: "Run every detected command (default)" },
26
+ ];
27
+
28
+ /** Parse one target, or report usage and return undefined. */
29
+ function parseTarget(raw: string, ctx: ExtensionCommandContext): CheckTarget | undefined {
30
+ const token = raw.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
31
+ if (token === "") return "all";
32
+ if (TARGETS.some((target) => target.value === token)) return token as CheckTarget;
33
+ ctx.ui.notify(`Unknown check target "${token}". Use test, lint, typecheck, or all.`, "warning");
34
+ return undefined;
35
+ }
36
+
37
+ /** The one-line command summary shown while the process runs. */
38
+ function runLabel(commands: readonly string[]): string {
39
+ return commands.length > 0 ? commands.join(" + ") : "check";
40
+ }
41
+
42
+ /** The TUI loop: run, then list/detail, with rerun. */
43
+ async function runTui(runtime: CheckRuntime, ctx: ExtensionCommandContext, target: CheckTarget): Promise<void> {
44
+ while (true) {
45
+ let active: ReturnType<CheckRuntime["start"]>;
46
+ try {
47
+ active = runtime.start(target, ctx);
48
+ } catch (error) {
49
+ ctx.ui.notify(error instanceof CheckError ? error.message : String(error), "warning");
50
+ return;
51
+ }
52
+
53
+ const result = await ctx.ui.custom<CheckResult | undefined>((tui, theme, _keybindings, done) => {
54
+ const component = new CheckRunComponent({
55
+ command: runLabel(active.commands),
56
+ lines: active.lines,
57
+ elapsedMs: active.elapsedMs,
58
+ theme,
59
+ onCancel: () => active.cancel(),
60
+ requestRender: () => tui.requestRender(),
61
+ viewportRows: () => tui.terminal?.rows,
62
+ });
63
+ active.promise.then(done);
64
+ return component;
65
+ });
66
+ if (!result) return;
67
+
68
+ if (result.status === "passed") {
69
+ ctx.ui.notify(formatCheckSummary(result), "info");
70
+ return;
71
+ }
72
+ if (result.status === "cancelled") {
73
+ ctx.ui.notify("Check cancelled.", "info");
74
+ return;
75
+ }
76
+ if (result.status === "error") {
77
+ ctx.ui.notify(formatCheckReport(result), "error");
78
+ return;
79
+ }
80
+
81
+ const choice = await ctx.ui.custom<"rerun" | undefined>((tui, theme, _keybindings, done) => {
82
+ const component = new CheckResultComponent({
83
+ result,
84
+ cwd: resolveEffectiveCwd(ctx.cwd),
85
+ theme,
86
+ onRerun: () => done("rerun"),
87
+ onClose: () => done(undefined),
88
+ requestRender: () => tui.requestRender(),
89
+ viewportRows: () => tui.terminal?.rows,
90
+ });
91
+ return component;
92
+ });
93
+ if (choice !== "rerun") return;
94
+ }
95
+ }
96
+
97
+ export function registerCheckCommand(pi: ExtensionAPI, runtime: CheckRuntime): void {
98
+ pi.registerCommand("check", {
99
+ description: "Run the project's test, lint, or typecheck command and show failures",
100
+ getArgumentCompletions: (prefix) => {
101
+ const trimmed = prefix.trim();
102
+ if (trimmed.includes(" ")) return null;
103
+ const items = TARGETS.filter((target) => target.value.startsWith(trimmed));
104
+ return items.length > 0
105
+ ? items.map((target) => ({ label: target.value, value: `${target.value} `, description: target.description }))
106
+ : null;
107
+ },
108
+ handler: async (args, ctx) => {
109
+ if (isPlanModeEnabled(ctx.sessionManager.getBranch())) {
110
+ ctx.ui.notify("Plan mode is read-only. Approve the plan before running checks.", "warning");
111
+ return;
112
+ }
113
+ const target = parseTarget(args, ctx);
114
+ if (!target) return;
115
+
116
+ if (ctx.mode !== "tui") {
117
+ const result = await runtime.run(target, ctx);
118
+ ctx.ui.notify(
119
+ result.status === "passed" ? formatCheckSummary(result) : formatCheckReport(result),
120
+ result.status === "error" ? "error" : "info",
121
+ );
122
+ return;
123
+ }
124
+
125
+ await withRailsSuppressed(pi, () => runTui(runtime, ctx, target));
126
+ },
127
+ });
128
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Configuration for the check extension.
3
+ *
4
+ * The `check` section of the shared `bytefly.json` (global then project, project
5
+ * wins). Missing or malformed documents are ignored, and every value is clamped
6
+ * to a safe range.
7
+ */
8
+
9
+ import { clampInteger, cleanString, loadConfigSection } from "../../lib/config.ts";
10
+
11
+ export interface CheckConfig {
12
+ /** Shell command used instead of detection for every target. Empty = detect. */
13
+ command: string;
14
+ /** Wall-clock limit for one command; 0 disables it. */
15
+ timeoutMs: number;
16
+ /** Bytes of output retained per command. */
17
+ maxLogBytes: number;
18
+ }
19
+
20
+ export const DEFAULT_CONFIG: CheckConfig = {
21
+ command: "",
22
+ timeoutMs: 600_000,
23
+ maxLogBytes: 1_048_576,
24
+ };
25
+
26
+ /** Normalize one merged config object over the running base. */
27
+ export function normalizeConfig(raw: Record<string, unknown> | undefined, base: CheckConfig): CheckConfig {
28
+ if (!raw) return base;
29
+ return {
30
+ command: cleanString(raw.command, base.command),
31
+ timeoutMs: clampInteger(raw.timeoutMs, base.timeoutMs, 0, 3_600_000),
32
+ maxLogBytes: clampInteger(raw.maxLogBytes, base.maxLogBytes, 65_536, 16_777_216),
33
+ };
34
+ }
35
+
36
+ /** Load the effective config for a working directory. */
37
+ export function loadConfig(cwd: string): CheckConfig {
38
+ return loadConfigSection(cwd, "check", DEFAULT_CONFIG, normalizeConfig);
39
+ }