@gavin-hu/my-pi-agent 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (324) hide show
  1. package/README.md +66 -52
  2. package/README.zh-CN.md +56 -46
  3. package/docs/extensions/README.md +34 -0
  4. package/docs/extensions/board.md +507 -0
  5. package/{extensions/bytefly/README.md → docs/extensions/bytefly.md} +774 -574
  6. package/{extensions/check/README.md → docs/extensions/check.md} +83 -68
  7. package/{extensions/context/README.md → docs/extensions/context.md} +40 -23
  8. package/docs/extensions/file-browse.md +561 -0
  9. package/{extensions/file-open/README.md → docs/extensions/file-open.md} +31 -23
  10. package/{extensions/file-read/README.md → docs/extensions/file-read.md} +47 -55
  11. package/{extensions/goal/README.md → docs/extensions/goal.md} +115 -131
  12. package/{extensions/job/README.md → docs/extensions/job.md} +62 -52
  13. package/{extensions/memory/README.md → docs/extensions/memory.md} +71 -60
  14. package/{extensions/plan/README.md → docs/extensions/plan.md} +440 -322
  15. package/{extensions/question/README.md → docs/extensions/question.md} +54 -31
  16. package/{extensions/rewind/README.md → docs/extensions/rewind.md} +115 -122
  17. package/docs/extensions/session-bus.md +298 -0
  18. package/{extensions/subagent/README.md → docs/extensions/subagent.md} +165 -118
  19. package/docs/extensions/team.md +254 -0
  20. package/{extensions/todo/README.md → docs/extensions/todo.md} +85 -88
  21. package/{extensions/watch/README.md → docs/extensions/watch.md} +54 -36
  22. package/{extensions/web-access/README.md → docs/extensions/web-access.md} +93 -50
  23. package/{extensions/wechat/README.md → docs/extensions/wechat.md} +260 -163
  24. package/{extensions/worktree/README.md → docs/extensions/worktree.md} +137 -79
  25. package/extensions/board/commands.ts +105 -0
  26. package/extensions/board/config.ts +36 -0
  27. package/extensions/board/format.ts +170 -0
  28. package/extensions/board/index.ts +40 -0
  29. package/extensions/board/input.ts +54 -0
  30. package/extensions/board/join.ts +42 -0
  31. package/extensions/board/page.ts +388 -0
  32. package/extensions/board/result.ts +108 -0
  33. package/extensions/board/rows.ts +48 -0
  34. package/extensions/board/schema.ts +90 -0
  35. package/extensions/board/seat.ts +28 -0
  36. package/extensions/board/snapshot.ts +42 -0
  37. package/extensions/board/tools.ts +444 -0
  38. package/extensions/board/transport/router.ts +91 -0
  39. package/extensions/board/transport/server.ts +138 -0
  40. package/extensions/board/types.ts +121 -0
  41. package/extensions/board/view/client.ts +51 -0
  42. package/extensions/board/view/dialog.ts +43 -0
  43. package/extensions/board/view/keys.ts +125 -0
  44. package/extensions/board/view/live.ts +231 -0
  45. package/extensions/board/view/names.ts +91 -0
  46. package/extensions/board/view/page.ts +118 -0
  47. package/extensions/board/view/render.ts +519 -0
  48. package/extensions/board/view/styles.ts +228 -0
  49. package/extensions/board/view/team.ts +327 -0
  50. package/extensions/bytefly/footer/budget.ts +6 -5
  51. package/extensions/bytefly/footer/collect/entries.ts +12 -4
  52. package/extensions/bytefly/footer/collect/snapshot.ts +8 -17
  53. package/extensions/bytefly/footer/config.ts +19 -8
  54. package/extensions/bytefly/footer/price/price.ts +30 -8
  55. package/extensions/bytefly/footer/register.ts +5 -5
  56. package/extensions/bytefly/footer/render/format.ts +18 -0
  57. package/extensions/bytefly/footer/render/lines.ts +23 -7
  58. package/extensions/bytefly/footer/render/segments.ts +117 -31
  59. package/extensions/bytefly/footer/types.ts +29 -5
  60. package/extensions/bytefly/footer/unpriced.ts +42 -17
  61. package/extensions/bytefly/panel/commands.ts +67 -7
  62. package/extensions/bytefly/panel/filter.ts +68 -0
  63. package/extensions/bytefly/panel/format.ts +16 -15
  64. package/extensions/bytefly/panel/join.ts +56 -13
  65. package/extensions/bytefly/panel/prices.ts +43 -0
  66. package/extensions/bytefly/panel/settings-runtime.ts +0 -6
  67. package/extensions/bytefly/panel/settings.ts +75 -43
  68. package/extensions/bytefly/panel/tui/entries.ts +26 -8
  69. package/extensions/bytefly/panel/tui/frame.ts +130 -93
  70. package/extensions/bytefly/panel/tui/keys.ts +352 -0
  71. package/extensions/bytefly/panel/tui/list-view.ts +25 -46
  72. package/extensions/bytefly/panel/tui/panes.ts +76 -0
  73. package/extensions/bytefly/panel/tui/section-view.ts +126 -0
  74. package/extensions/bytefly/panel/tui/settings-section.ts +54 -125
  75. package/extensions/bytefly/panel/tui/settings-view.ts +30 -131
  76. package/extensions/bytefly/panel/tui/side-pane.ts +224 -0
  77. package/extensions/bytefly/panel/tui.ts +306 -259
  78. package/extensions/bytefly/picker/discovery.ts +0 -2
  79. package/extensions/bytefly/picker/types.ts +0 -2
  80. package/extensions/bytefly/update/check.ts +39 -0
  81. package/extensions/bytefly/update/format.ts +15 -0
  82. package/extensions/bytefly/update/register.ts +73 -34
  83. package/extensions/bytefly/update/state.ts +6 -3
  84. package/extensions/check/commands.ts +2 -1
  85. package/extensions/context/tui.ts +1 -1
  86. package/extensions/file-browse/{config.ts → config/config.ts} +1 -1
  87. package/extensions/file-browse/files/classify.ts +136 -0
  88. package/extensions/file-browse/files/files.ts +12 -0
  89. package/extensions/file-browse/files/index.ts +90 -0
  90. package/extensions/file-browse/files/list.ts +261 -0
  91. package/extensions/file-browse/{paths.ts → files/paths.ts} +1 -1
  92. package/extensions/file-browse/files/tree.ts +101 -0
  93. package/extensions/file-browse/{git.ts → git/git.ts} +10 -3
  94. package/extensions/file-browse/index.ts +4 -4
  95. package/extensions/file-browse/{live.ts → live/live.ts} +20 -69
  96. package/extensions/file-browse/{router.ts → transport/router.ts} +21 -17
  97. package/extensions/file-browse/transport/server.ts +110 -0
  98. package/extensions/file-browse/{chrome.ts → view/chrome.ts} +4 -4
  99. package/extensions/file-browse/{client.ts → view/client.ts} +1 -1
  100. package/extensions/file-browse/{diff-html.ts → view/diff-html.ts} +3 -3
  101. package/extensions/file-browse/view/emit.ts +51 -0
  102. package/extensions/file-browse/{html.ts → view/html.ts} +19 -18
  103. package/extensions/file-browse/{icons.ts → view/icons.ts} +1 -1
  104. package/extensions/file-browse/{markdown.ts → view/markdown.ts} +18 -20
  105. package/extensions/file-browse/{raw-html.ts → view/raw-html.ts} +19 -14
  106. package/extensions/file-browse/{styles.ts → view/styles.ts} +29 -5
  107. package/extensions/file-browse/{tree.ts → view/tree.ts} +1 -1
  108. package/extensions/file-browse/{urls.ts → view/urls.ts} +1 -1
  109. package/extensions/file-read/schema.ts +2 -1
  110. package/extensions/job/runtime.ts +4 -4
  111. package/extensions/memory/store.ts +12 -5
  112. package/extensions/plan/blocks.ts +23 -2
  113. package/extensions/plan/commands.ts +21 -16
  114. package/extensions/plan/feedback.ts +185 -52
  115. package/extensions/plan/format.ts +10 -3
  116. package/extensions/plan/index.ts +23 -5
  117. package/extensions/plan/plan-files.ts +88 -0
  118. package/extensions/plan/plan-index.ts +43 -9
  119. package/extensions/plan/plan-text.ts +33 -23
  120. package/extensions/plan/plans.ts +27 -34
  121. package/extensions/plan/policy.ts +30 -17
  122. package/extensions/plan/review.ts +3 -27
  123. package/extensions/plan/runtime.ts +52 -11
  124. package/extensions/plan/sections.ts +112 -0
  125. package/extensions/plan/steps.ts +3 -22
  126. package/extensions/plan/todos.ts +51 -3
  127. package/extensions/plan/tools/exit.ts +20 -95
  128. package/extensions/plan/tools/todo-seed.ts +4 -2
  129. package/extensions/plan/tools/write.ts +12 -1
  130. package/extensions/plan/tui.ts +5 -34
  131. package/extensions/plan/types.ts +15 -8
  132. package/extensions/plan/web-blocks.ts +89 -0
  133. package/extensions/plan/web-chrome.ts +3 -5
  134. package/extensions/plan/web-client.ts +179 -330
  135. package/extensions/plan/web-collab.ts +3 -3
  136. package/extensions/plan/web-comments.ts +18 -8
  137. package/extensions/plan/web-composer.ts +80 -0
  138. package/extensions/plan/web-draft.ts +40 -0
  139. package/extensions/plan/web-keys.ts +134 -0
  140. package/extensions/plan/web-outline.ts +192 -0
  141. package/extensions/plan/web-page.ts +4 -8
  142. package/extensions/plan/web-plan.ts +150 -0
  143. package/extensions/plan/web-router.ts +3 -58
  144. package/extensions/plan/web-settle.ts +194 -0
  145. package/extensions/plan/web-share-session.ts +24 -30
  146. package/extensions/plan/web-streams.ts +14 -14
  147. package/extensions/plan/web-tokens.ts +2 -2
  148. package/extensions/plan/web.ts +66 -120
  149. package/extensions/rewind/{commands.ts → commands/commands.ts} +5 -5
  150. package/extensions/rewind/{review.ts → commands/review.ts} +15 -14
  151. package/extensions/rewind/{rewind.ts → commands/rewind.ts} +6 -6
  152. package/extensions/rewind/{config.ts → config/config.ts} +1 -1
  153. package/extensions/rewind/index.ts +3 -3
  154. package/extensions/rewind/{policy.ts → state/policy.ts} +4 -4
  155. package/extensions/rewind/{runtime.ts → state/runtime.ts} +11 -11
  156. package/extensions/rewind/{snapshot-run.ts → state/snapshot-run.ts} +1 -1
  157. package/extensions/rewind/{timeline.ts → state/timeline.ts} +2 -2
  158. package/extensions/rewind/{format.ts → text/format.ts} +3 -3
  159. package/extensions/rewind/{git.ts → transport/git.ts} +2 -2
  160. package/extensions/rewind/{restore.ts → transport/restore.ts} +2 -2
  161. package/extensions/rewind/{snapshot.ts → transport/snapshot.ts} +1 -1
  162. package/extensions/rewind/{store.ts → transport/store.ts} +1 -1
  163. package/extensions/rewind/{checklist.ts → view/checklist.ts} +6 -6
  164. package/extensions/rewind/{hunk-picker.ts → view/hunk-picker.ts} +8 -8
  165. package/extensions/rewind/{path-picker.ts → view/path-picker.ts} +7 -7
  166. package/extensions/rewind/{review-tui.ts → view/review-tui.ts} +11 -11
  167. package/extensions/rewind/{rewind-tui.ts → view/rewind-tui.ts} +17 -11
  168. package/extensions/rewind/{status.ts → view/status.ts} +4 -4
  169. package/extensions/session-bus/commands.ts +17 -69
  170. package/extensions/session-bus/format.ts +22 -45
  171. package/extensions/session-bus/index.ts +13 -7
  172. package/extensions/session-bus/join-request.ts +40 -0
  173. package/extensions/session-bus/mail-watch.ts +134 -0
  174. package/extensions/session-bus/notice.ts +32 -4
  175. package/{lib/claims → extensions/session-bus}/overlap.ts +11 -18
  176. package/extensions/session-bus/protocol.ts +19 -49
  177. package/extensions/session-bus/resolve.ts +67 -0
  178. package/extensions/session-bus/result.ts +16 -51
  179. package/extensions/session-bus/rows.ts +11 -5
  180. package/extensions/session-bus/runtime.ts +220 -267
  181. package/extensions/session-bus/store.ts +55 -7
  182. package/extensions/session-bus/tools.ts +31 -66
  183. package/extensions/session-bus/tui.ts +5 -5
  184. package/extensions/session-bus/types.ts +23 -14
  185. package/extensions/subagent/agents.ts +9 -8
  186. package/extensions/subagent/commands.ts +2 -1
  187. package/extensions/subagent/invocation.ts +4 -2
  188. package/extensions/team/commands.ts +145 -0
  189. package/extensions/team/dispatch.ts +133 -0
  190. package/extensions/team/format.ts +119 -0
  191. package/extensions/team/index.ts +34 -0
  192. package/extensions/team/schema.ts +29 -0
  193. package/extensions/team/tools.ts +267 -0
  194. package/extensions/team/types.ts +65 -0
  195. package/extensions/todo/index.ts +4 -1
  196. package/extensions/todo/nudge.ts +8 -2
  197. package/extensions/web-access/fetch/page.ts +4 -1
  198. package/extensions/web-access/fetch/structured.ts +447 -0
  199. package/extensions/web-access/fetch/tool.ts +2 -1
  200. package/extensions/web-access/search/tool.ts +2 -1
  201. package/extensions/wechat/bridge.ts +106 -251
  202. package/extensions/wechat/{commands.ts → commands/commands.ts} +26 -18
  203. package/extensions/wechat/{doctor.ts → commands/doctor.ts} +5 -5
  204. package/extensions/wechat/{history.ts → commands/history.ts} +1 -1
  205. package/extensions/wechat/{login.ts → commands/login.ts} +3 -3
  206. package/extensions/wechat/{slash.ts → commands/slash.ts} +38 -2
  207. package/extensions/wechat/dialog/channels.ts +136 -0
  208. package/extensions/wechat/{interaction.ts → dialog/interaction.ts} +37 -37
  209. package/extensions/wechat/{prompt.ts → dialog/prompt.ts} +2 -2
  210. package/extensions/wechat/{remote-ui.ts → dialog/remote-ui.ts} +1 -1
  211. package/extensions/wechat/index.ts +10 -10
  212. package/extensions/wechat/intake.ts +245 -0
  213. package/extensions/wechat/{admission.ts → peer/admission.ts} +2 -2
  214. package/extensions/wechat/{peer-session.ts → peer/peer-session.ts} +2 -2
  215. package/extensions/wechat/{peer-worker.ts → peer/peer-worker.ts} +100 -22
  216. package/extensions/wechat/{presence.ts → peer/presence.ts} +8 -7
  217. package/extensions/wechat/{reply-pipeline.ts → peer/reply-pipeline.ts} +1 -1
  218. package/extensions/wechat/{credentials.ts → state/credentials.ts} +2 -2
  219. package/extensions/wechat/{state.ts → state/state.ts} +14 -2
  220. package/extensions/wechat/{format.ts → text/format.ts} +3 -3
  221. package/extensions/wechat/{notices.ts → text/notices.ts} +8 -0
  222. package/extensions/wechat/tools.ts +29 -12
  223. package/extensions/wechat/{client.ts → transport/client.ts} +26 -2
  224. package/extensions/wechat/{deliver.ts → transport/deliver.ts} +72 -28
  225. package/extensions/wechat/{inbound.ts → transport/inbound.ts} +5 -5
  226. package/extensions/wechat/{media.ts → transport/media.ts} +11 -5
  227. package/extensions/wechat/transport/outbound.ts +142 -0
  228. package/extensions/wechat/{poll.ts → transport/poll.ts} +3 -3
  229. package/extensions/wechat/types.ts +10 -1
  230. package/extensions/wechat/{login-screen.ts → view/login-screen.ts} +4 -4
  231. package/extensions/wechat/{status-screen.ts → view/status-screen.ts} +31 -26
  232. package/extensions/worktree/commands.ts +24 -17
  233. package/extensions/worktree/exit.ts +12 -6
  234. package/extensions/worktree/git.ts +22 -14
  235. package/extensions/worktree/guard.ts +18 -290
  236. package/extensions/worktree/handoff.ts +10 -2
  237. package/extensions/worktree/index.ts +2 -1
  238. package/extensions/worktree/lifecycle.ts +2 -0
  239. package/extensions/worktree/registry.ts +20 -11
  240. package/extensions/worktree/root-tools.ts +13 -3
  241. package/extensions/worktree/runtime.ts +8 -2
  242. package/extensions/worktree/screen.ts +15 -8
  243. package/extensions/worktree/shell.ts +303 -0
  244. package/extensions/worktree/state.ts +1 -1
  245. package/extensions/worktree/status.ts +42 -9
  246. package/extensions/worktree/tools.ts +19 -13
  247. package/lib/README.md +11 -8
  248. package/lib/board/card.ts +479 -0
  249. package/lib/board/links.ts +141 -0
  250. package/lib/board/page-pointer.ts +310 -0
  251. package/lib/board/round-text.ts +81 -0
  252. package/lib/board/round.ts +94 -0
  253. package/lib/board/rules.ts +437 -0
  254. package/lib/board/session.ts +105 -0
  255. package/lib/board/store.ts +497 -0
  256. package/lib/board/view.ts +234 -0
  257. package/lib/errors.ts +12 -4
  258. package/lib/format.ts +35 -0
  259. package/lib/http/guard.ts +112 -0
  260. package/lib/http/index.ts +16 -5
  261. package/lib/http/serve.ts +237 -0
  262. package/lib/http/server.ts +12 -10
  263. package/lib/http/sink.ts +135 -0
  264. package/lib/http/token.ts +38 -0
  265. package/lib/plan-entry.ts +11 -0
  266. package/lib/policy.ts +25 -2
  267. package/lib/requirements.ts +6 -7
  268. package/lib/session-label.ts +64 -0
  269. package/lib/session-roster.ts +201 -0
  270. package/lib/session-tool.ts +184 -0
  271. package/lib/spawn.ts +37 -3
  272. package/lib/stray-fields.ts +45 -0
  273. package/lib/tool-names.ts +38 -2
  274. package/lib/tui/glyphs.ts +3 -2
  275. package/lib/web/README.md +57 -4
  276. package/lib/web/client-runtime.ts +123 -0
  277. package/lib/web/components.ts +5 -0
  278. package/lib/web/index.ts +4 -2
  279. package/lib/web/markup.ts +88 -1
  280. package/lib/web/names.ts +42 -0
  281. package/lib/web/runtime.ts +7 -1
  282. package/lib/web/tokens.ts +25 -2
  283. package/package.json +26 -14
  284. package/skills/next/SKILL.md +4 -3
  285. package/skills/project-docs/SKILL.md +133 -0
  286. package/skills/project-docs/references/audit.md +131 -0
  287. package/skills/project-docs/references/changelog.md +105 -0
  288. package/skills/project-docs/references/discovery.md +127 -0
  289. package/skills/project-docs/references/readme.md +80 -0
  290. package/skills/project-docs/references/release.md +84 -0
  291. package/skills/project-docs/references/roadmap.md +93 -0
  292. package/skills/project-docs/scripts/state.ts +316 -0
  293. package/skills/refactor/SKILL.md +100 -125
  294. package/skills/refactor/references/handoff.md +35 -0
  295. package/skills/refactor/references/signals.md +54 -0
  296. package/skills/refactor/scripts/inventory.ts +587 -0
  297. package/extensions/bytefly/panel/tui/extensions-view.ts +0 -40
  298. package/extensions/bytefly/panel/tui/skills-view.ts +0 -50
  299. package/extensions/file-browse/README.md +0 -506
  300. package/extensions/file-browse/files.ts +0 -564
  301. package/extensions/file-browse/server.ts +0 -141
  302. package/extensions/job/process.ts +0 -35
  303. package/extensions/plan/split.ts +0 -87
  304. package/extensions/session-bus/README.md +0 -247
  305. package/extensions/session-bus/team.ts +0 -102
  306. package/extensions/wechat/outbound.ts +0 -100
  307. package/lib/plans/sections.ts +0 -172
  308. package/lib/plans/setting.ts +0 -52
  309. package/lib/plans/split.ts +0 -215
  310. package/lib/plans/store.ts +0 -19
  311. package/lib/plans/team.ts +0 -151
  312. package/skills/changelog/SKILL.md +0 -148
  313. package/skills/parallel-dev/SKILL.md +0 -150
  314. /package/extensions/file-browse/{rev.ts → live/rev.ts} +0 -0
  315. /package/extensions/file-browse/{diff.ts → text/diff.ts} +0 -0
  316. /package/extensions/file-browse/{fuzzy.ts → text/fuzzy.ts} +0 -0
  317. /package/extensions/file-browse/{live-client.ts → view/live-client.ts} +0 -0
  318. /package/extensions/file-browse/{palette.ts → view/palette.ts} +0 -0
  319. /package/extensions/rewind/{patch.ts → text/patch.ts} +0 -0
  320. /package/extensions/rewind/{paths.ts → text/paths.ts} +0 -0
  321. /package/extensions/rewind/{queue.ts → transport/queue.ts} +0 -0
  322. /package/extensions/rewind/{temp-index.ts → transport/temp-index.ts} +0 -0
  323. /package/extensions/wechat/{lock.ts → state/lock.ts} +0 -0
  324. /package/extensions/wechat/{channel.ts → text/channel.ts} +0 -0
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Changelog state, for the `project-docs` skill.
3
+ *
4
+ * The skill's judgement is the model's; the counting is not. This prints what a
5
+ * changelog can be asked mechanically — the file, the last tag, the commits since it,
6
+ * the `[Unreleased]` categories, the empty and duplicate headings, the scope prefixes
7
+ * in use, and the link references — so an audit starts from facts instead of a
8
+ * hand-run `git log` whose flags and column are remembered differently each time.
9
+ *
10
+ * It ships inside the skill and resolves against the skill's own directory, so it
11
+ * imports nothing from the package's `lib/`: an installed copy has `skills/` and
12
+ * nothing else.
13
+ *
14
+ * Usage: `bun <skill>/scripts/state.ts [dir]`
15
+ */
16
+
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import { join, relative, resolve } from "node:path";
19
+
20
+ /** One `###` category under `[Unreleased]`, as the file spells it. */
21
+ export interface Category {
22
+ /** The heading text, exactly as written: `Fixed`, not `Fix`. */
23
+ name: string;
24
+ /** 1-based line of the heading. */
25
+ line: number;
26
+ /** Top-level bullets between this heading and the next one. */
27
+ bullets: number;
28
+ }
29
+
30
+ /** What the file's text alone says; `gitFacts` adds the part that needs a repository. */
31
+ export interface State {
32
+ /** Whether the first paragraphs name Keep a Changelog and Semantic Versioning. */
33
+ keepAChangelog: boolean;
34
+ /** The `[Unreleased]` section, when the file keeps one. */
35
+ unreleased?: { line: number; categories: Category[]; bullets: number };
36
+ /** Every dated `## [X.Y.Z]` heading, newest first, which is the order released sections appear in. */
37
+ dated: { version: string; line: number }[];
38
+ /** A heading text used twice inside one section, with every line it occurs on. */
39
+ duplicates: { name: string; lines: number[] }[];
40
+ /** Scope prefixes and how often each appears, most used first. */
41
+ scopes: { name: string; count: number }[];
42
+ /** The link references the file defines, lowercased, and the dated sections that lack one. */
43
+ refs: { unreleased: boolean; dated: number; missing: string[] };
44
+ }
45
+
46
+ /** What the repository adds: the tag the window starts at, and what is in the window. */
47
+ export interface GitFacts {
48
+ /** The newest tag, when the repository has one. */
49
+ tag?: string;
50
+ /** Subject lines of the non-merge commits since the tag, newest first. */
51
+ commits: string[];
52
+ /** Why a fact is absent, in words the report can print: no repository, no tags. */
53
+ note?: string;
54
+ }
55
+
56
+ /** Keep a Changelog's `## [Unreleased]`, and the un-bracketed spelling some files use. */
57
+ const UNRELEASED = /^\[?unreleased\]?$/i;
58
+ /** A dated section heading: `[0.17.0] - 2025-06-01`, `0.17.0`, or a project's own suffix. */
59
+ const DATED = /^\[?(\d+\.\d+\.\d+[^\s\]]*)\]?/;
60
+ const HEADING = /^(#{1,6})\s+(.+?)\s*#*\s*$/;
61
+ const FENCE = /^\s*(?:```|~~~)/;
62
+ const BULLET = /^[-*+]\s+/;
63
+ /** A bullet's scope: backticked, bold, or a bare `area:` prefix. Not a path, and not prose. */
64
+ const SCOPE = /^[-*+]\s+(?:`([^`]+)`|\*\*([^*]+)\*\*|([A-Za-z][A-Za-z0-9./-]{0,24}))\s*:/;
65
+ const LINK_REF = /^\[([^\]]+)\]:\s*\S+/;
66
+
67
+ /** Report caps, so a long history stays one screen. */
68
+ export const MAX_COMMITS = 40;
69
+ export const MAX_SCOPES = 6;
70
+
71
+ interface Heading {
72
+ level: number;
73
+ text: string;
74
+ line: number;
75
+ }
76
+
77
+ /** The changelog a project root holds, in the order projects name one. */
78
+ export const CHANGELOG_NAMES = [
79
+ "CHANGELOG.md",
80
+ "CHANGELOG",
81
+ "CHANGES.md",
82
+ "HISTORY.md",
83
+ "NEWS.md",
84
+ join("docs", "CHANGELOG.md"),
85
+ join("docs", "changelog.md"),
86
+ ];
87
+
88
+ /** The first changelog file in `dir`, as an absolute path, or undefined when it has none. */
89
+ export function findChangelog(dir: string): string | undefined {
90
+ const root = resolve(dir);
91
+ for (const name of CHANGELOG_NAMES) {
92
+ const candidate = join(root, name);
93
+ if (existsSync(candidate)) return candidate;
94
+ }
95
+ return undefined;
96
+ }
97
+
98
+ /** Every heading outside a fence, so a `## example` in a code block is not a section. */
99
+ function headingsOf(markdown: string): Heading[] {
100
+ const headings: Heading[] = [];
101
+ let fenced = false;
102
+ markdown.split("\n").forEach((text, index) => {
103
+ if (FENCE.test(text)) {
104
+ fenced = !fenced;
105
+ return;
106
+ }
107
+ if (fenced) return;
108
+ const match = HEADING.exec(text);
109
+ if (match !== null) headings.push({ level: match[1].length, text: match[2].trim(), line: index + 1 });
110
+ });
111
+ return headings;
112
+ }
113
+
114
+ /** The scope a bullet leads with, if it leads with one. */
115
+ function scopeOf(line: string): string | undefined {
116
+ const match = SCOPE.exec(line);
117
+ if (match === null) return undefined;
118
+ return (match[1] ?? match[2] ?? match[3]).trim();
119
+ }
120
+
121
+ /** Bullet count and scopes over the given 1-based inclusive line window, outside fences. */
122
+ function scanBullets(lines: readonly string[], start: number, end: number): { bullets: number; scopes: string[] } {
123
+ let bullets = 0;
124
+ const scopes: string[] = [];
125
+ let fenced = false;
126
+ for (let line = start; line <= end && line <= lines.length; line++) {
127
+ const text = lines[line - 1];
128
+ if (FENCE.test(text)) {
129
+ fenced = !fenced;
130
+ continue;
131
+ }
132
+ if (fenced || !BULLET.test(text)) continue;
133
+ bullets++;
134
+ const scope = scopeOf(text);
135
+ if (scope !== undefined) scopes.push(scope);
136
+ }
137
+ return { bullets, scopes };
138
+ }
139
+
140
+ /** Read the file's text into the facts the report prints. Pure, so a fixture can drive it. */
141
+ export function readState(markdown: string): State {
142
+ const lines = markdown.split("\n");
143
+ const headings = headingsOf(markdown);
144
+ const preamble = lines.slice(0, 20).join("\n");
145
+
146
+ const unreleasedAt = headings.findIndex((heading) => heading.level === 2 && UNRELEASED.test(heading.text));
147
+ const dated = headings
148
+ .filter((heading) => heading.level === 2)
149
+ .flatMap((heading) => {
150
+ const version = DATED.exec(heading.text)?.[1];
151
+ return version === undefined ? [] : [{ version, line: heading.line }];
152
+ });
153
+
154
+ const scopes = new Map<string, number>();
155
+ for (const scope of scanBullets(lines, 1, lines.length).scopes) {
156
+ scopes.set(scope, (scopes.get(scope) ?? 0) + 1);
157
+ }
158
+
159
+ const duplicates = duplicateHeadings(headings);
160
+
161
+ const refs = new Map<string, number>();
162
+ for (const line of lines) {
163
+ const name = LINK_REF.exec(line)?.[1];
164
+ if (name !== undefined) refs.set(name.toLowerCase(), (refs.get(name.toLowerCase()) ?? 0) + 1);
165
+ }
166
+
167
+ let unreleased: State["unreleased"];
168
+ if (unreleasedAt !== -1) {
169
+ const heading = headings[unreleasedAt];
170
+ const nextSection = headings.slice(unreleasedAt + 1).find((entry) => entry.level <= 2);
171
+ const windowEnd = nextSection === undefined ? lines.length : nextSection.line - 1;
172
+ const inner = headings.filter((entry) => entry.line > heading.line && entry.line <= windowEnd);
173
+ const categories: Category[] = [];
174
+ inner
175
+ .filter((entry) => entry.level === 3)
176
+ .forEach((entry, index) => {
177
+ const next = inner.filter((item) => item.level === 3)[index + 1];
178
+ const categoryEnd = next === undefined ? windowEnd : next.line - 1;
179
+ categories.push({
180
+ name: entry.text,
181
+ line: entry.line,
182
+ bullets: scanBullets(lines, entry.line + 1, categoryEnd).bullets,
183
+ });
184
+ });
185
+ unreleased = {
186
+ line: heading.line,
187
+ categories,
188
+ bullets: categories.reduce((total, category) => total + category.bullets, 0),
189
+ };
190
+ }
191
+
192
+ return {
193
+ keepAChangelog: /keep a changelog/i.test(preamble) && /semantic versioning/i.test(preamble),
194
+ unreleased,
195
+ dated,
196
+ duplicates,
197
+ scopes: [...scopes.entries()]
198
+ .map(([name, count]) => ({ name, count }))
199
+ .sort((a, b) => b.count - a.count || a.name.localeCompare(b.name)),
200
+ refs: {
201
+ unreleased: refs.has("unreleased"),
202
+ dated: dated.length,
203
+ missing: dated.filter((entry) => !refs.has(entry.version.toLowerCase())).map((entry) => entry.version),
204
+ },
205
+ };
206
+ }
207
+
208
+ /** A trim with an ellipsis, so one long line cannot wrap the report. */
209
+ function clip(text: string, width: number): string {
210
+ return text.length > width ? `${text.slice(0, width - 1)}…` : text;
211
+ }
212
+
213
+ /**
214
+ * A heading repeated inside one section, which is what a reader sees as one category
215
+ * split in two. The same `### Fixed` under each release is the format working, not a
216
+ * duplicate, so headings are grouped by the `##` section that contains them.
217
+ */
218
+ function duplicateHeadings(headings: readonly Heading[]): { name: string; lines: number[] }[] {
219
+ const groups = new Map<string, { name: string; lines: number[] }>();
220
+ let section = "top";
221
+ for (const heading of headings) {
222
+ if (heading.level <= 2) section = heading.level === 2 ? heading.text.toLowerCase() : "top";
223
+ const key = `${section}\u0000${heading.text.toLowerCase()}`;
224
+ const group = groups.get(key) ?? { name: heading.text, lines: [] };
225
+ group.lines.push(heading.line);
226
+ groups.set(key, group);
227
+ }
228
+ return [...groups.values()].filter((group) => group.lines.length > 1);
229
+ }
230
+
231
+ /**
232
+ * The report. Every line is a fact with its source, in the order an audit uses them:
233
+ * the file, the window, the unreleased section, then what is wrong with the file, then
234
+ * what the window contains.
235
+ */
236
+ export function formatState(file: string, state: State, git: GitFacts): string {
237
+ const out: string[] = [];
238
+ const preamble = state.keepAChangelog ? "Keep a Changelog preamble" : "no Keep a Changelog preamble";
239
+ out.push(`changelog ${file} · ${preamble}`);
240
+
241
+ const count = `${git.commits.length} commit${git.commits.length === 1 ? "" : "s"} since, merges skipped`;
242
+ out.push(`last tag ${git.tag === undefined ? (git.note ?? "no tags") : `${git.tag} · ${count}`}`);
243
+
244
+ out.push(
245
+ state.unreleased === undefined
246
+ ? "unreleased missing: the file has no `[Unreleased]` section"
247
+ : `unreleased line ${state.unreleased.line} · ${state.unreleased.bullets} bullets`,
248
+ );
249
+ if (state.unreleased !== undefined) {
250
+ const named = state.unreleased.categories.map((category) => `${category.name} ${category.bullets}`);
251
+ out.push(`categories ${named.length === 0 ? "none" : named.join(" · ")} (file order)`);
252
+ const empty = state.unreleased.categories.filter((category) => category.bullets === 0).map((item) => item.name);
253
+ out.push(`empty ${empty.length === 0 ? "none" : empty.join(" · ")}`);
254
+ }
255
+ out.push(
256
+ state.duplicates.length === 0
257
+ ? "duplicate none"
258
+ : `duplicate ${state.duplicates
259
+ .slice(0, 4)
260
+ .map((entry) => `${entry.name} at lines ${entry.lines.join(", ")}`)
261
+ .join(" · ")}`,
262
+ );
263
+ const scopes = state.scopes.slice(0, MAX_SCOPES).map((entry) => `${entry.name} ${entry.count}`);
264
+ const rest = state.scopes.length - MAX_SCOPES;
265
+ out.push(
266
+ `scopes ${scopes.length === 0 ? "none" : `${scopes.join(" · ")}${rest > 0 ? ` · … ${rest} more` : ""}`}`,
267
+ );
268
+ out.push(
269
+ `refs [Unreleased] ${state.refs.unreleased ? "yes" : "no"} · dated refs ${state.refs.dated - state.refs.missing.length}/${state.refs.dated}${
270
+ state.refs.missing.length === 0 ? "" : `, missing ${state.refs.missing.slice(0, 5).join(", ")}`
271
+ }`,
272
+ );
273
+
274
+ if (git.commits.length > 0) {
275
+ out.push(`commits since ${git.tag ?? "the beginning"}`);
276
+ for (const commit of git.commits.slice(0, MAX_COMMITS)) out.push(` ${clip(commit, 110)}`);
277
+ const more = git.commits.length - MAX_COMMITS;
278
+ if (more > 0) out.push(` … ${more} more`);
279
+ }
280
+ return `${out.join("\n")}\n`;
281
+ }
282
+
283
+ /** Run one git command, returning its trimmed stdout or a reason it produced none. */
284
+ function git(args: readonly string[], cwd: string): { text?: string; error?: string } {
285
+ const result = Bun.spawnSync({ cmd: ["git", ...args], cwd, stdout: "pipe", stderr: "pipe" });
286
+ if (result.exitCode !== 0) return { error: result.stderr.toString().trim() || `git ${args[0]} failed` };
287
+ return { text: result.stdout.toString().trim() };
288
+ }
289
+
290
+ /**
291
+ * The tag to audit from, and the non-merge commits after it. A repository with no
292
+ * tags has no window, and a directory outside git has neither; both are reported as a
293
+ * note rather than an error, because the file half of the report is still useful.
294
+ */
295
+ export function gitFacts(cwd: string): GitFacts {
296
+ const inside = git(["rev-parse", "--is-inside-work-tree"], cwd);
297
+ if (inside.text !== "true") return { commits: [], note: "not a git repository" };
298
+
299
+ const described = git(["describe", "--tags", "--abbrev=0"], cwd);
300
+ const tag = described.text === "" ? undefined : described.text;
301
+ const range = tag === undefined ? ["HEAD"] : [`${tag}..HEAD`];
302
+ const log = git(["log", "--no-merges", "--format=%h %s", ...range], cwd);
303
+ const commits = (log.text ?? "").split("\n").filter((line) => line.trim() !== "");
304
+ return { tag, commits, note: tag === undefined ? "no tags" : undefined };
305
+ }
306
+
307
+ if (import.meta.main) {
308
+ const dir = resolve(process.argv[2] ?? ".");
309
+ const changelog = findChangelog(dir);
310
+ if (changelog === undefined) {
311
+ console.error(`no changelog in ${dir}: looked for ${CHANGELOG_NAMES.join(", ")}`);
312
+ process.exit(1);
313
+ }
314
+ const state = readState(readFileSync(changelog, "utf-8"));
315
+ process.stdout.write(formatState(relative(dir, changelog), state, gitFacts(dir)));
316
+ }
@@ -1,141 +1,116 @@
1
1
  ---
2
2
  name: refactor
3
- description: "Find and rank refactoring opportunities in a project or path: duplication, oversized modules, coupling, drift, and high-churn code with weak tests. Use when asked what to refactor, where the code is tangled, or for a refactor audit; discovery is read-only and returns an evidence-based shortlist."
3
+ description: "Audit a project or path for refactoring opportunities — duplication, oversized modules, coupling, drift, high-churn code with weak tests — and return a ranked shortlist with path:line evidence, or carry one chosen candidate through a behaviour-preserving refactor. Use when asked what to refactor, where the code is tangled, for a refactor audit, or to execute a refactor already agreed; discovery is read-only."
4
+ compatibility: "Bun for scripts/inventory.ts; git for the churn window."
4
5
  ---
5
6
 
6
7
  # Find refactoring opportunities
7
8
 
8
- This is an audit, not an edit session. The deliverable is a ranked shortlist with
9
- evidence; do not modify files while discovering. The skill is model-invoked when
10
- a request matches the description; `/skill:refactor` is the explicit fallback.
9
+ Goal: a ranked shortlist a reader can act on — evidence at `path:line`, a verification
10
+ command per candidate, and the reasoning a reviewer can argue with. This is an audit:
11
+ nothing is modified before the user picks a candidate.
11
12
 
12
13
  A refactor changes structure without changing behavior. If the request changes
13
- behavior, call it a behavior change and report it separately. Read the
14
- repository's own instructions first; they win over this skill.
14
+ behavior, say so and treat it as a behavior change, not as this. The project's own
15
+ instructions and thresholds win over anything here.
16
+
17
+ ## Start here
18
+
19
+ ```bash
20
+ bun <skill>/scripts/inventory.ts <path ...> [--churn-days N] [--limit N]
21
+ ```
22
+
23
+ Resolved against this skill's own directory. It prints the scope, the languages it
24
+ measured, the size distribution, fan-in, churn over a stated window, test proximity,
25
+ and the files above the 90th percentile in any of those — facts, never a verdict. The
26
+ numbers are the evidence; the ranking is yours.
27
+
28
+ When it reports `no history`, an `unresolved` count, or a `skipped` language, repeat
29
+ that in the report rather than reading the gap as zero. See
30
+ [`references/signals.md`](./references/signals.md) for what it measures, what it
31
+ cannot, and what each signal needs before it counts.
15
32
 
16
33
  ## 1. Resolve the scope
17
34
 
18
- The text after `/skill:refactor` is the task. It may contain a path (`@src`,
19
- `src`, `packages/api`, or a path inside a sentence), a question, or both.
20
-
21
- - Resolve each path from the working directory. A directory is the audit scope;
22
- a file is the initial target. Audit multiple paths separately and report them
23
- together.
24
- - No path means the current project. If the project is large, propose a scope
25
- from the documented layout instead of scanning everything blindly.
26
- - If a path does not exist, say so and ask for a real one. Do not guess. Do not
27
- leave the working directory unless the user explicitly supplied an outside
28
- path.
35
+ The text after `/skill:refactor` is the task: a path, a question, or both.
36
+
37
+ - Resolve each path from the working directory. A directory is the audit scope; a file
38
+ is the initial target. Audit several paths separately and report them together.
39
+ - No path means the current project. When the project is large, do not scan
40
+ everything: ask which scope to audit, offering two to four concrete directories with
41
+ their file counts.
42
+ - A path that does not exist is a question, not a guess. Ask for a real one, and do not
43
+ leave the working directory unless the user named a path outside it.
29
44
  - State the resolved scope and the number of files examined in the report.
30
45
 
31
46
  ## 2. Learn the project's standards
32
47
 
33
- - Read the repository's instruction files, contributor guide, architecture
34
- notes, shared-helper documentation, and CI configuration.
35
- - Record the languages, layout, test framework, documented check commands, size
36
- or complexity thresholds, and ignore rules.
37
- - Do not import an external style guide, install a tool, or apply a threshold the
38
- project never stated. If there is no stated threshold, compare against the rest
39
- of the scope and say that is the basis.
40
- - Note the available evidence: tests, coverage, type checking, linting, git
41
- history, and any scanner already installed.
42
-
43
- ## 3. Inventory the scope
44
-
45
- - Map source files, tests, exports, imports, and entry points. Mark public
46
- surface versus internal modules.
47
- - Measure cheap facts: file and function sizes, nesting depth, parameter counts,
48
- export counts, fan-in and fan-out, and the distance to the nearest test.
49
- - Read git history for churn (`git log --numstat`) inside the scope. Churn is
50
- context, not a verdict: a frequently changed, tangled file is a stronger
51
- candidate than a stable, isolated oddity.
52
- - Skip version-control metadata, dependencies, build output, generated files,
53
- vendored trees, and secrets unless the user explicitly includes them. Respect
54
- the project's ignore files.
55
- - For a large scope, delegate a read-only survey to a read-only helper if the
56
- host provides one. Keep the filtering and ranking in the main session.
57
-
58
- ## 4. Detect signals
59
-
60
- For every signal, point at a file and line and explain why it matters here.
61
-
62
- | Signal | What to look for | Evidence to cite |
63
- |---|---|---|
64
- | Duplication | The same body or a near-identical block in several places; copy-paste with tiny edits; a helper that already exists in a shared location | Paths and line ranges of each copy; the shared location it should use |
65
- | Size and responsibility | A module or function past the project's limit, or a relative outlier; unrelated exports; deep nesting; long parameter lists | Measured size or responsibility list; the seams where it could split |
66
- | Coupling | Circular or layer-crossing imports, high fan-in or fan-out, feature envy, repeated type switches | The import edge or repeated branch, with both sides named |
67
- | Cohesion and drift | A grab-bag utility module; a name that no longer matches behavior; dead exports; commented-out code; two implementations of one concept | The mismatch or unused export, with its callers |
68
- | Change pressure | High recent churn combined with complexity or weak tests | Churn count over a stated window plus the complexity or test gap |
69
- | Test friction | Behavior with no direct test; tests that mock the unit heavily or assert internals; duplicated setup that hides a missing seam | The test path and the missing or brittle assertion |
70
- | Boundary erosion | Leaking internal types, unchecked casts, mode-selecting boolean parameters, null-heavy returns | The boundary location and the caller that must know the internals |
71
-
72
- ## 5. Verify and filter
73
-
74
- A signal is not a candidate until it is confirmed.
75
-
76
- - Read the callers and tests, not just the definition. Duplication may be
77
- intentional for a protocol version; a large module may be cohesive; an
78
- "unused" export may be public API.
79
- - Require one concrete artifact: a path with line numbers, a counted duplicate, a
80
- measured size, a dependency edge, or a test gap. If you cannot point at it,
81
- drop it.
82
- - Keep pure refactors separate from behavior changes and bugs. Report a bug as a
83
- bug; do not smuggle it into the refactor list.
84
- - Prefer candidates with an existing test that catches a regression. If the
85
- behavior is untested, mark the candidate "tests first".
86
- - Reject weak candidates explicitly. A short "not worth it" list proves the
87
- audit was selective rather than a tour of everything unfamiliar.
88
-
89
- ## 6. Rank
90
-
91
- Use three tiers:
92
-
93
- - **Do next:** safe, contained blast radius, high leverage, small or medium
94
- effort, and a fast verification command.
95
- - **Worth planning:** high leverage but more risk or effort, a public surface, or
48
+ Read the instruction files, contributor guide, architecture notes, and CI
49
+ configuration. Record the languages, layout, test framework, the check command, the
50
+ size or complexity thresholds, and the ignore rules.
51
+
52
+ A threshold counts only when it is quoted with the file that states it. With no stated
53
+ threshold, compare against the rest of the scope — and say that the comparison is what
54
+ the finding rests on. Do not import an external style guide, install a tool, or apply a
55
+ number the project never chose.
56
+
57
+ ## 3. Read, detect, confirm
58
+
59
+ The outlier list from the script is the reading list and the budget; the report says
60
+ what was left outside it. Walk the signals in
61
+ [`references/signals.md`](./references/signals.md) — duplication, size, coupling,
62
+ cohesion and drift, change pressure, shotgun surgery, test friction, boundary erosion,
63
+ doc-versus-code drift — and remember the script measures per file, not per symbol, so
64
+ nesting, parameter counts, and function length come from reading.
65
+
66
+ A signal is not a candidate until it is confirmed. Read the callers and tests, not just
67
+ the definition: duplication may be deliberate, a large module may be cohesive, an
68
+ "unused" export may be public API. Every kept candidate carries one concrete artifact —
69
+ a path with line numbers, a counted duplicate, a measured size, an import edge, or a
70
+ named test gap. A bug is reported as a bug, never smuggled into the list. A candidate
71
+ without a test is marked "tests first".
72
+
73
+ ## 5. Rank
74
+
75
+ Three tiers, and no numeric score — the reasons have to be readable.
76
+
77
+ - **Do next**: contained blast radius, high leverage, small or medium effort, fast
78
+ verification.
79
+ - **Worth planning**: high leverage with more risk or effort, a public surface, or
96
80
  missing tests.
97
- - **Leave it:** low confidence, little leverage, or not worth the churn.
98
-
99
- Within a tier, order by leverage first, then safety, then cost. Do not invent a
100
- numeric score; the reasons must be readable.
101
-
102
- ## 7. Report
103
-
104
- Keep the report scannable. Use this shape:
105
-
106
- - **Scope and method:** path, file count, tools used, and what was not examined.
107
- - **Top three to five opportunities**, each with a title, tier, evidence at
108
- `path:line`, the proposed structural change, expected benefit, risk and blast
109
- radius, effort (S/M/L), and the narrow plus full verification command.
110
- - **Rejected signals:** one line each with the reason it did not qualify.
111
- - **Next step:** ask which candidate to take. Do not start editing.
112
-
113
- ## 8. Stop conditions
114
-
115
- - No worthwhile opportunities: say so with the evidence that was checked instead
116
- of inventing work.
117
- - The request is a specific refactor rather than an audit: skip the shortlist,
118
- but use the handoff below.
119
- - The request changes behavior or asks for a rewrite: treat it as an approved
120
- design change, not this audit.
121
- - The scope cannot be read (missing path, secrets, binary or generated files, or
122
- an inaccessible tree): report the limitation.
123
-
124
- ## After selection: safe handoff
125
-
126
- If the user chooses a candidate, execution follows these rules:
127
-
128
- - State the behavior contract and run the project's documented check to get a
129
- baseline. Record pre-existing failures.
130
- - If the behavior is untested, add characterization tests first.
131
- - Enter plan mode when the host provides it and the change spans files, changes a
132
- public surface, or has an unknown step list. Otherwise track the steps in a
133
- todo list or a short written sequence.
134
- - Make one structural change per step and verify each step. Never mix in
135
- formatting, bug fixes, or unrelated cleanup.
136
- - Finish with the full project check and report the diff, results, and remaining
137
- risk. Do not commit unless the user asks.
138
-
139
- The audit is done when the scope is stated, every candidate has `path:line`
140
- evidence and a verification command, the shortlist is capped, rejected signals
141
- are explained, and no files were modified.
81
+ - **Leave it**: low confidence, little leverage, or not worth the churn.
82
+
83
+ Within a tier, order by leverage, then safety, then cost.
84
+
85
+ ## 6. Report
86
+
87
+ - **Scope and method**: path, file count, what was measured, and what was not examined.
88
+ - **Top three to five**: title, tier, evidence at `path:line`, the structural change,
89
+ expected benefit, risk and blast radius, effort (S/M/L), and the narrow plus full
90
+ verification command.
91
+ - **Rejected signals**: one line each, with the reason it did not qualify.
92
+ - **Next step**: ask which candidate to take, offering the shortlist by name.
93
+
94
+ ## 7. From audit to execution
95
+
96
+ When the user picks one, read [`references/handoff.md`](./references/handoff.md)
97
+ before the first edit: a stated behaviour contract, a recorded baseline,
98
+ characterisation tests first when the behaviour is untested, one structural change per
99
+ step, and no mixed cleanup are what keep the promise.
100
+
101
+ ## Quality gate
102
+
103
+ - The scope, the file count, and what was not examined are stated.
104
+ - Every candidate has `path:line` evidence, a verification command, a tier, and an
105
+ effort; none carries a numeric score.
106
+ - The rejected list is not empty, and it explains each rejection.
107
+ - No file was modified, and no bug or behaviour change is in the refactor list.
108
+
109
+ ## Stop conditions
110
+
111
+ - Nothing worth doing: say so with what was checked instead of inventing work.
112
+ - The request is one specific refactor, not an audit: skip the shortlist and go to
113
+ handoff.
114
+ - The request is a rewrite or a behaviour change: treat it as the design change it is.
115
+ - The scope cannot be read — missing path, secrets, generated or binary files, an
116
+ inaccessible tree: report the limitation and stop.
@@ -0,0 +1,35 @@
1
+ # After a candidate is chosen
2
+
3
+ The audit is over; this is execution, and its only promise is that behavior does not
4
+ change. These are the rules that keep that promise — the rest is ordinary work.
5
+
6
+ ## Before the first edit
7
+
8
+ - **State the behaviour contract.** One paragraph: what must stay true, which callers
9
+ rely on it, and which parts are explicitly free to change. If the contract is
10
+ ambiguous, ask now rather than discovering it in the diff.
11
+ - **Record a baseline.** Run the project's documented check and keep the result. A
12
+ pre-existing failure is not this refactor's to fix, and a refactor that turns one
13
+ test from red to green has changed something nobody asked about.
14
+ - **Add characterisation tests first when the behaviour is untested.** They capture
15
+ what the code does today, including the parts you consider wrong, because that is
16
+ the only net under a structural change.
17
+
18
+ ## While it moves
19
+
20
+ - **One structural change per step**, each with its own verification. A rename, an
21
+ extraction, and a move are three steps, not one; a step that cannot be verified on
22
+ its own is too large.
23
+ - **Never mix in** formatting, a bug fix, a dependency bump, or unrelated cleanup. If
24
+ the new structure makes one of those obvious, finish, report it, and do it as its
25
+ own change.
26
+ - **Ceremony by size**: plan mode for a change that spans files, touches a public
27
+ surface, or has a step list you cannot write down up front; a todo list for the rest.
28
+ - **Stop if it turns into a behaviour change.** Say so, and treat it as the design
29
+ change it now is, rather than smuggling it through under the old name.
30
+
31
+ ## Finishing
32
+
33
+ - Run the full check, not only the narrow command, and compare against the baseline.
34
+ - Report the diff's shape, what the check said, and what is still risky or unverified.
35
+ - Do not commit unless the user asks.