@cad0p/pi-steering 0.1.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 (246) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +29 -0
  3. package/README.md +920 -0
  4. package/dist/__test-helpers__.d.ts +95 -0
  5. package/dist/__test-helpers__.d.ts.map +1 -0
  6. package/dist/__test-helpers__.js +174 -0
  7. package/dist/__test-helpers__.js.map +1 -0
  8. package/dist/bin/pi-steering.d.ts +9 -0
  9. package/dist/bin/pi-steering.d.ts.map +1 -0
  10. package/dist/bin/pi-steering.js +506 -0
  11. package/dist/bin/pi-steering.js.map +1 -0
  12. package/dist/compat.d.ts +50 -0
  13. package/dist/compat.d.ts.map +1 -0
  14. package/dist/compat.js +180 -0
  15. package/dist/compat.js.map +1 -0
  16. package/dist/defaults.d.ts +61 -0
  17. package/dist/defaults.d.ts.map +1 -0
  18. package/dist/defaults.js +106 -0
  19. package/dist/defaults.js.map +1 -0
  20. package/dist/define-config.d.ts +235 -0
  21. package/dist/define-config.d.ts.map +1 -0
  22. package/dist/define-config.js +118 -0
  23. package/dist/define-config.js.map +1 -0
  24. package/dist/define-predicate.d.ts +49 -0
  25. package/dist/define-predicate.d.ts.map +1 -0
  26. package/dist/define-predicate.js +35 -0
  27. package/dist/define-predicate.js.map +1 -0
  28. package/dist/evaluator-internals/context.d.ts +186 -0
  29. package/dist/evaluator-internals/context.d.ts.map +1 -0
  30. package/dist/evaluator-internals/context.js +219 -0
  31. package/dist/evaluator-internals/context.js.map +1 -0
  32. package/dist/evaluator-internals/override.d.ts +19 -0
  33. package/dist/evaluator-internals/override.d.ts.map +1 -0
  34. package/dist/evaluator-internals/override.js +79 -0
  35. package/dist/evaluator-internals/override.js.map +1 -0
  36. package/dist/evaluator-internals/predicates.d.ts +152 -0
  37. package/dist/evaluator-internals/predicates.d.ts.map +1 -0
  38. package/dist/evaluator-internals/predicates.js +849 -0
  39. package/dist/evaluator-internals/predicates.js.map +1 -0
  40. package/dist/evaluator-internals/speculative-synthesis.d.ts +102 -0
  41. package/dist/evaluator-internals/speculative-synthesis.d.ts.map +1 -0
  42. package/dist/evaluator-internals/speculative-synthesis.js +101 -0
  43. package/dist/evaluator-internals/speculative-synthesis.js.map +1 -0
  44. package/dist/evaluator.d.ts +101 -0
  45. package/dist/evaluator.d.ts.map +1 -0
  46. package/dist/evaluator.js +714 -0
  47. package/dist/evaluator.js.map +1 -0
  48. package/dist/helpers/walker-unknown-cwd-reason.d.ts +68 -0
  49. package/dist/helpers/walker-unknown-cwd-reason.d.ts.map +1 -0
  50. package/dist/helpers/walker-unknown-cwd-reason.js +73 -0
  51. package/dist/helpers/walker-unknown-cwd-reason.js.map +1 -0
  52. package/dist/index.d.ts +42 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +93 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/internal/drop-unused-observers.d.ts +43 -0
  57. package/dist/internal/drop-unused-observers.d.ts.map +1 -0
  58. package/dist/internal/drop-unused-observers.js +50 -0
  59. package/dist/internal/drop-unused-observers.js.map +1 -0
  60. package/dist/internal/finalize-plugin-state.d.ts +22 -0
  61. package/dist/internal/finalize-plugin-state.d.ts.map +1 -0
  62. package/dist/internal/finalize-plugin-state.js +14 -0
  63. package/dist/internal/finalize-plugin-state.js.map +1 -0
  64. package/dist/internal/merge-observers.d.ts +30 -0
  65. package/dist/internal/merge-observers.d.ts.map +1 -0
  66. package/dist/internal/merge-observers.js +27 -0
  67. package/dist/internal/merge-observers.js.map +1 -0
  68. package/dist/internal/pattern-utils.d.ts +33 -0
  69. package/dist/internal/pattern-utils.d.ts.map +1 -0
  70. package/dist/internal/pattern-utils.js +20 -0
  71. package/dist/internal/pattern-utils.js.map +1 -0
  72. package/dist/internal/ref-text.d.ts +11 -0
  73. package/dist/internal/ref-text.d.ts.map +1 -0
  74. package/dist/internal/ref-text.js +15 -0
  75. package/dist/internal/ref-text.js.map +1 -0
  76. package/dist/internal/session-runtime.d.ts +44 -0
  77. package/dist/internal/session-runtime.d.ts.map +1 -0
  78. package/dist/internal/session-runtime.js +143 -0
  79. package/dist/internal/session-runtime.js.map +1 -0
  80. package/dist/internal/watch-matcher.d.ts +56 -0
  81. package/dist/internal/watch-matcher.d.ts.map +1 -0
  82. package/dist/internal/watch-matcher.js +179 -0
  83. package/dist/internal/watch-matcher.js.map +1 -0
  84. package/dist/loader.d.ts +97 -0
  85. package/dist/loader.d.ts.map +1 -0
  86. package/dist/loader.js +527 -0
  87. package/dist/loader.js.map +1 -0
  88. package/dist/observer-dispatcher.d.ts +69 -0
  89. package/dist/observer-dispatcher.d.ts.map +1 -0
  90. package/dist/observer-dispatcher.js +184 -0
  91. package/dist/observer-dispatcher.js.map +1 -0
  92. package/dist/plugin-merger.d.ts +175 -0
  93. package/dist/plugin-merger.d.ts.map +1 -0
  94. package/dist/plugin-merger.js +450 -0
  95. package/dist/plugin-merger.js.map +1 -0
  96. package/dist/plugins/git/branch-tracker.d.ts +85 -0
  97. package/dist/plugins/git/branch-tracker.d.ts.map +1 -0
  98. package/dist/plugins/git/branch-tracker.js +181 -0
  99. package/dist/plugins/git/branch-tracker.js.map +1 -0
  100. package/dist/plugins/git/cwd-extensions.d.ts +10 -0
  101. package/dist/plugins/git/cwd-extensions.d.ts.map +1 -0
  102. package/dist/plugins/git/cwd-extensions.js +155 -0
  103. package/dist/plugins/git/cwd-extensions.js.map +1 -0
  104. package/dist/plugins/git/git-ops.d.ts +89 -0
  105. package/dist/plugins/git/git-ops.d.ts.map +1 -0
  106. package/dist/plugins/git/git-ops.js +107 -0
  107. package/dist/plugins/git/git-ops.js.map +1 -0
  108. package/dist/plugins/git/index.d.ts +225 -0
  109. package/dist/plugins/git/index.d.ts.map +1 -0
  110. package/dist/plugins/git/index.js +52 -0
  111. package/dist/plugins/git/index.js.map +1 -0
  112. package/dist/plugins/git/predicates.d.ts +312 -0
  113. package/dist/plugins/git/predicates.d.ts.map +1 -0
  114. package/dist/plugins/git/predicates.js +580 -0
  115. package/dist/plugins/git/predicates.js.map +1 -0
  116. package/dist/plugins/git/rules.d.ts +209 -0
  117. package/dist/plugins/git/rules.d.ts.map +1 -0
  118. package/dist/plugins/git/rules.js +267 -0
  119. package/dist/plugins/git/rules.js.map +1 -0
  120. package/dist/schema.d.ts +1799 -0
  121. package/dist/schema.d.ts.map +1 -0
  122. package/dist/schema.js +4 -0
  123. package/dist/schema.js.map +1 -0
  124. package/dist/testing/index.d.ts +621 -0
  125. package/dist/testing/index.d.ts.map +1 -0
  126. package/dist/testing/index.js +747 -0
  127. package/dist/testing/index.js.map +1 -0
  128. package/examples/README.md +83 -0
  129. package/examples/combined-git-discipline/README.md +52 -0
  130. package/examples/combined-git-discipline/node_modules/.bin/pi-steering +43 -0
  131. package/examples/combined-git-discipline/package.json +18 -0
  132. package/examples/combined-git-discipline/steering.json +27 -0
  133. package/examples/combined-git-discipline/steering.test.ts +42 -0
  134. package/examples/combined-git-discipline/steering.ts +64 -0
  135. package/examples/combined-git-discipline/tsconfig.json +7 -0
  136. package/examples/draft-prs-only/README.md +42 -0
  137. package/examples/draft-prs-only/node_modules/.bin/pi-steering +43 -0
  138. package/examples/draft-prs-only/package.json +18 -0
  139. package/examples/draft-prs-only/steering.json +12 -0
  140. package/examples/draft-prs-only/steering.test.ts +48 -0
  141. package/examples/draft-prs-only/steering.ts +36 -0
  142. package/examples/draft-prs-only/tsconfig.json +7 -0
  143. package/examples/dynamic-reason-runtime-cwd/README.md +148 -0
  144. package/examples/dynamic-reason-runtime-cwd/node_modules/.bin/pi-steering +43 -0
  145. package/examples/dynamic-reason-runtime-cwd/package.json +18 -0
  146. package/examples/dynamic-reason-runtime-cwd/steering.test.ts +130 -0
  147. package/examples/dynamic-reason-runtime-cwd/steering.ts +82 -0
  148. package/examples/dynamic-reason-runtime-cwd/tsconfig.json +7 -0
  149. package/examples/force-push-strict/README.md +75 -0
  150. package/examples/force-push-strict/node_modules/.bin/pi-steering +43 -0
  151. package/examples/force-push-strict/package.json +18 -0
  152. package/examples/force-push-strict/steering.json +12 -0
  153. package/examples/force-push-strict/steering.test.ts +51 -0
  154. package/examples/force-push-strict/steering.ts +54 -0
  155. package/examples/force-push-strict/tsconfig.json +7 -0
  156. package/examples/no-amend/README.md +52 -0
  157. package/examples/no-amend/node_modules/.bin/pi-steering +43 -0
  158. package/examples/no-amend/package.json +18 -0
  159. package/examples/no-amend/steering.cwd-scoped.json +12 -0
  160. package/examples/no-amend/steering.json +11 -0
  161. package/examples/no-amend/steering.test.ts +49 -0
  162. package/examples/no-amend/steering.ts +38 -0
  163. package/examples/no-amend/tsconfig.json +7 -0
  164. package/examples/work-item-plugin/README.md +103 -0
  165. package/examples/work-item-plugin/node_modules/.bin/pi-steering +43 -0
  166. package/examples/work-item-plugin/package.json +19 -0
  167. package/examples/work-item-plugin/src/index.test.ts +168 -0
  168. package/examples/work-item-plugin/src/index.ts +122 -0
  169. package/examples/work-item-plugin/src/observers/npm-test-tracker.test.ts +95 -0
  170. package/examples/work-item-plugin/src/observers/npm-test-tracker.ts +88 -0
  171. package/examples/work-item-plugin/src/observers/retest-required-tracker.test.ts +81 -0
  172. package/examples/work-item-plugin/src/observers/retest-required-tracker.ts +81 -0
  173. package/examples/work-item-plugin/src/predicates/work-item-format.test.ts +157 -0
  174. package/examples/work-item-plugin/src/predicates/work-item-format.ts +99 -0
  175. package/examples/work-item-plugin/src/rules/commit-description-check.test.ts +107 -0
  176. package/examples/work-item-plugin/src/rules/commit-description-check.ts +83 -0
  177. package/examples/work-item-plugin/src/rules/commit-requires-work-item.test.ts +79 -0
  178. package/examples/work-item-plugin/src/rules/commit-requires-work-item.ts +56 -0
  179. package/examples/work-item-plugin/src/rules/push-requires-tests.test.ts +245 -0
  180. package/examples/work-item-plugin/src/rules/push-requires-tests.ts +62 -0
  181. package/examples/work-item-plugin/tsconfig.json +8 -0
  182. package/package.json +88 -0
  183. package/skills/steering-authoring/SKILL.md +102 -0
  184. package/src/INVARIANTS.md +116 -0
  185. package/src/__test-helpers__.ts +243 -0
  186. package/src/bin/pi-steering.test.ts +920 -0
  187. package/src/bin/pi-steering.ts +584 -0
  188. package/src/compat.test.ts +431 -0
  189. package/src/compat.ts +282 -0
  190. package/src/defaults.test.ts +585 -0
  191. package/src/defaults.ts +148 -0
  192. package/src/define-config.test.ts +888 -0
  193. package/src/define-config.ts +375 -0
  194. package/src/define-predicate.test.ts +70 -0
  195. package/src/define-predicate.ts +57 -0
  196. package/src/evaluator-internals/context.ts +290 -0
  197. package/src/evaluator-internals/override.test.ts +99 -0
  198. package/src/evaluator-internals/override.ts +83 -0
  199. package/src/evaluator-internals/predicates.ts +1037 -0
  200. package/src/evaluator-internals/speculative-synthesis.test.ts +411 -0
  201. package/src/evaluator-internals/speculative-synthesis.ts +199 -0
  202. package/src/evaluator.test.ts +6296 -0
  203. package/src/evaluator.ts +1106 -0
  204. package/src/factory-time-load.test.ts +630 -0
  205. package/src/helpers/walker-unknown-cwd-reason.test.ts +73 -0
  206. package/src/helpers/walker-unknown-cwd-reason.ts +83 -0
  207. package/src/index.test.ts +836 -0
  208. package/src/index.ts +229 -0
  209. package/src/internal/drop-unused-observers.test.ts +141 -0
  210. package/src/internal/drop-unused-observers.ts +81 -0
  211. package/src/internal/finalize-plugin-state.ts +43 -0
  212. package/src/internal/merge-observers.ts +51 -0
  213. package/src/internal/pattern-utils.test.ts +52 -0
  214. package/src/internal/pattern-utils.ts +38 -0
  215. package/src/internal/ref-text.test.ts +54 -0
  216. package/src/internal/ref-text.ts +17 -0
  217. package/src/internal/session-runtime.test.ts +629 -0
  218. package/src/internal/session-runtime.ts +201 -0
  219. package/src/internal/watch-matcher.test.ts +326 -0
  220. package/src/internal/watch-matcher.ts +200 -0
  221. package/src/jsdoc-propagation.test.ts +277 -0
  222. package/src/loader.test.ts +1098 -0
  223. package/src/loader.ts +581 -0
  224. package/src/not-block-onunknown.test.ts +554 -0
  225. package/src/observer-dispatcher.test.ts +939 -0
  226. package/src/observer-dispatcher.ts +290 -0
  227. package/src/per-predicate-typing.test.ts +361 -0
  228. package/src/plugin-merger.test.ts +790 -0
  229. package/src/plugin-merger.ts +598 -0
  230. package/src/plugins/git/README.md +485 -0
  231. package/src/plugins/git/branch-tracker.test.ts +281 -0
  232. package/src/plugins/git/branch-tracker.ts +187 -0
  233. package/src/plugins/git/cwd-extensions.test.ts +230 -0
  234. package/src/plugins/git/cwd-extensions.ts +160 -0
  235. package/src/plugins/git/git-ops.test.ts +397 -0
  236. package/src/plugins/git/git-ops.ts +173 -0
  237. package/src/plugins/git/index.ts +253 -0
  238. package/src/plugins/git/integration.test.ts +705 -0
  239. package/src/plugins/git/predicates.test.ts +1401 -0
  240. package/src/plugins/git/predicates.ts +693 -0
  241. package/src/plugins/git/rules.test.ts +1001 -0
  242. package/src/plugins/git/rules.ts +307 -0
  243. package/src/schema.test.ts +259 -0
  244. package/src/schema.ts +1963 -0
  245. package/src/testing/index.test.ts +1696 -0
  246. package/src/testing/index.ts +1472 -0
@@ -0,0 +1,485 @@
1
+ # pi-steering/plugins/git
2
+
3
+ Git plugin for [pi-steering](../../../README.md) — branch
4
+ awareness, upstream checks, and git-specific cwd tracking on top of
5
+ the core steering engine.
6
+
7
+ > **Opt-in since the v0.1.x monorepo split.** This plugin is NOT
8
+ > registered automatically: `DEFAULT_PLUGINS` is empty. Declare it via
9
+ > `plugins: [gitPlugin]` (import from `pi-steering/plugins/git`) to get
10
+ > the predicates, rules, tracker, and cwd extensions — see
11
+ > [Usage](#usage) below.
12
+
13
+ ## What it ships
14
+
15
+ | Surface | Names | Purpose |
16
+ |---|---|---|
17
+ | Predicates | `branch`, `upstream`, `commitsAhead`, `hasStagedChanges`, `isClean`, `remote` | New `when.<key>` slots for rules |
18
+ | Rules | `no-main-commit`, `no-main-commit-github` | Block direct commits to protected branches; the `-github` variant emits PR-flow guidance on github.com clones |
19
+ | Trackers | `branch` | Walker-threaded branch state (`git checkout X` advances) |
20
+ | Tracker extensions | `cwd.git` | `--git-dir=` / `--work-tree=` flag parsing on top of the built-in cwd tracker |
21
+
22
+ ## Usage
23
+
24
+ ```ts
25
+ // .pi/steering.ts
26
+ import { defineConfig } from "@cad0p/pi-steering";
27
+ import gitPlugin from "@cad0p/pi-steering/plugins/git";
28
+
29
+ export default defineConfig({
30
+ // The plugin is opt-in: declaring it registers it at runtime AND
31
+ // feeds its rule / predicate names into defineConfig's type unions
32
+ // (typo-checking on disabledRules / disabledPlugins).
33
+ plugins: [gitPlugin],
34
+ rules: [
35
+ // Custom rule layered on top of the plugin's predicates:
36
+ {
37
+ name: "no-push-when-dirty",
38
+ tool: "bash",
39
+ field: "command",
40
+ pattern: "^git\\s+push\\b",
41
+ when: { isClean: false },
42
+ reason: "Stash or commit your working changes before pushing.",
43
+ },
44
+ ],
45
+ });
46
+ ```
47
+
48
+ Explicit import still works (e.g. in tests driving `loadHarness`
49
+ with `includeDefaults: false`):
50
+
51
+ ```ts
52
+ import { defineConfig } from "@cad0p/pi-steering";
53
+ import gitPlugin from "@cad0p/pi-steering/plugins/git";
54
+
55
+ export default defineConfig({
56
+ plugins: [gitPlugin],
57
+ rules: [...],
58
+ });
59
+ ```
60
+
61
+ ### Disabling
62
+
63
+ Disabling snippets declare `plugins: [gitPlugin]` first — that's what
64
+ feeds the rule / plugin names into `defineConfig`'s generics for
65
+ typo-checking. (Since the monorepo split, `DEFAULT_PLUGINS` is empty:
66
+ there is no default registration to lean on, and disabling an
67
+ undeclared plugin is a compile error.)
68
+
69
+ Keep the predicates + tracker, drop the shipped rule:
70
+
71
+ ```ts
72
+ import { defineConfig } from "@cad0p/pi-steering";
73
+ import gitPlugin from "@cad0p/pi-steering/plugins/git";
74
+
75
+ export default defineConfig({
76
+ plugins: [gitPlugin],
77
+ disabledRules: ["no-main-commit"],
78
+ });
79
+ ```
80
+
81
+ Drop the whole git plugin (no `branch` / `upstream` / ... predicates,
82
+ no tracker, no cwd extensions, no rule):
83
+
84
+ ```ts
85
+ import { defineConfig } from "@cad0p/pi-steering";
86
+ import gitPlugin from "@cad0p/pi-steering/plugins/git";
87
+
88
+ export default defineConfig({
89
+ plugins: [gitPlugin],
90
+ disabledPlugins: ["git"],
91
+ });
92
+ ```
93
+
94
+ Drop EVERYTHING shipped — `DEFAULT_RULES` (and whatever plugins
95
+ you declared):
96
+
97
+ ```ts
98
+ import { defineConfig } from "@cad0p/pi-steering";
99
+
100
+ export default defineConfig({
101
+ disableDefaults: true,
102
+ });
103
+ ```
104
+
105
+ ## Predicate reference
106
+
107
+ ### `branch`
108
+
109
+ Match the current git branch.
110
+
111
+ ```ts
112
+ when: { branch: /^main$/ }
113
+ when: { branch: "^feat-" } // string = regex source
114
+ when: { branch: { pattern: /^main$/, onUnknown: "allow" } }
115
+ ```
116
+
117
+ Resolution is a three-way discrimination on what the branch tracker
118
+ knows about the current `tool_call` chain:
119
+
120
+ 1. **value** — the tracker observed an in-chain `git checkout <X>` /
121
+ `git switch <X>` with a statically-resolvable target. Match the
122
+ pattern against `X`. This is what makes
123
+ `git checkout main && git commit` evaluate against `main`, not
124
+ the pre-chain branch.
125
+ 2. **unknown** — the tracker observed a checkout but couldn't
126
+ resolve the target (e.g. `git checkout $VAR`). Apply `onUnknown`
127
+ policy WITHOUT shelling out: `git branch --show-current` here
128
+ would return the PRE-checkout branch and silently defeat the
129
+ walker — exactly the case the tracker exists to catch.
130
+ 3. **missing** — no branch-changing command fired in the current
131
+ chain. Shell out via `git branch --show-current` in `ctx.cwd`;
132
+ the shell's current state is the answer the predicate wants.
133
+
134
+ `onUnknown` defaults to `"block"` (fail-closed) — if the branch
135
+ can't be determined (dynamic checkout, exec failure, detached HEAD
136
+ in the missing case), the predicate reports "match" so the rule
137
+ still fires.
138
+
139
+ ### `upstream`
140
+
141
+ Match the current branch's configured upstream (`git rev-parse
142
+ --abbrev-ref @{upstream}`). Same shape as `branch`, no tracker today.
143
+
144
+ ```ts
145
+ when: { upstream: /^origin\/main$/ }
146
+ when: { upstream: { pattern: "^origin/", onUnknown: "allow" } }
147
+ ```
148
+
149
+ ### `commitsAhead`
150
+
151
+ Match the count of commits ahead of a revision (default
152
+ `@{upstream}`).
153
+
154
+ ```ts
155
+ when: { commitsAhead: { eq: 1 } } // exactly one
156
+ when: { commitsAhead: { gt: 0 } } // at least one
157
+ when: { commitsAhead: { gt: 0, lt: 5 } } // 1..4
158
+ when: { commitsAhead: { wrt: "origin/main", eq: 1 } }
159
+ ```
160
+
161
+ At least one of `eq` / `gt` / `lt` must be specified. Returns
162
+ `false` (rule skips) on exec failure or non-numeric output — pair
163
+ with `upstream` for fail-closed behavior.
164
+
165
+ ### `hasStagedChanges` / `isClean`
166
+
167
+ Boolean predicates.
168
+
169
+ ```ts
170
+ when: { hasStagedChanges: true } // staged changes exist
171
+ when: { hasStagedChanges: false } // no staged changes
172
+ when: { isClean: true } // working tree clean
173
+ when: { isClean: false } // working tree dirty
174
+ ```
175
+
176
+ Returns `false` on exec failure. Layer with `upstream` if you need
177
+ fail-closed behavior.
178
+
179
+ ### `remote`
180
+
181
+ Match the `origin` remote URL. Same shape as `branch`.
182
+
183
+ ```ts
184
+ when: { remote: /github\.com:org\// }
185
+ when: { remote: { pattern: /production/, onUnknown: "block" } }
186
+ ```
187
+
188
+ ## Shipped rules
189
+
190
+ ### `no-main-commit`
191
+
192
+ Blocks direct commits to protected branches (`main`, `master`,
193
+ `mainline`, `trunk`).
194
+
195
+ ```ts
196
+ {
197
+ name: "no-main-commit",
198
+ tool: "bash",
199
+ field: "command",
200
+ pattern: GIT_COMMIT_PATTERN,
201
+ when: { branch: PROTECTED_BRANCH_PATTERN },
202
+ reason: "Don't commit directly to a protected branch...",
203
+ noOverride: false,
204
+ }
205
+ ```
206
+
207
+ Overridable via `# steering-override: no-main-commit — <reason>` on
208
+ the bash command. Catches `git -C /path commit`, `sh -c 'git
209
+ commit'`, and — thanks to the branch tracker — `git checkout main
210
+ && git commit`.
211
+
212
+ The pattern is shared with `no-main-commit-github` via the
213
+ exported `GIT_COMMIT_PATTERN` constant in `rules.ts` (re-exported
214
+ from `pi-steering/plugins/git`), so a regex change to one rule is
215
+ physically forced onto the other (a unit test pins each rule's
216
+ `pattern` field against the constant by value).
217
+
218
+ The protected-branch list is likewise shared via the exported
219
+ `PROTECTED_BRANCH_PATTERN` constant (also re-exported from
220
+ `pi-steering/plugins/git`). Because it's a `RegExp` (object)
221
+ constant rather than a string source, the pinning test uses
222
+ shared-reference identity (`when.branch === PROTECTED_BRANCH_PATTERN`),
223
+ which also catches future re-inlining of the same bytes — a stricter
224
+ guarantee than the byte-equality pin on `pattern`. Adding a
225
+ vendor-specific default-branch alias is a one-line edit at the
226
+ constant; both rules pick it up.
227
+
228
+ ### `no-main-commit-github`
229
+
230
+ Specialization of `no-main-commit` for github.com clones. Same
231
+ pattern + protected-branch list, plus a `remote: /github\.com[/:]/`
232
+ clause; the reason text emits PR-flow guidance (`gh pr merge`)
233
+ instead of the generic feature-branch reminder, plus a safety
234
+ reminder against unsolicited PR merges or ready-for-review flips.
235
+
236
+ ```ts
237
+ {
238
+ name: "no-main-commit-github",
239
+ tool: "bash",
240
+ field: "command",
241
+ pattern: GIT_COMMIT_PATTERN, // shared with no-main-commit
242
+ when: {
243
+ branch: PROTECTED_BRANCH_PATTERN, // shared with no-main-commit
244
+ remote: /github\.com[/:]/,
245
+ },
246
+ reason: (ctx) => /* multi-paragraph PR-flow + safety guidance */,
247
+ noOverride: false,
248
+ }
249
+ ```
250
+
251
+ **First-match-wins ordering is load-bearing.**
252
+ `no-main-commit-github` is registered BEFORE `no-main-commit` in
253
+ the plugin's rule array. On a github clone + on main, both rules'
254
+ `when:` clauses match — first-match-wins routes the
255
+ github-flavored guidance to github users. On non-github contexts
256
+ (Brazil packages, vault paths, /tmp scratch repos with non-github
257
+ remotes) the github rule's `remote:` predicate doesn't match → the
258
+ engine falls through to the generic `no-main-commit`. A unit test
259
+ pins this position so reordering for stylistic reasons trips the
260
+ suite.
261
+
262
+ Under walker-unknown cwd (`cd "$VAR" && git commit`), the rule
263
+ still fires fail-closed — but the reason text switches to the
264
+ standard `walkerUnknownCwdReason` message instead of claiming
265
+ github-specific context the engine couldn't verify.
266
+
267
+ ## Customization
268
+
269
+ Three escape valves of increasing scope, ordered most → least
270
+ common. None of these are exemption-by-cwd patterns — see the
271
+ [Cwd-based exemption](#cwd-based-exemption-advanced) advanced
272
+ section below for that case (it has subtle
273
+ walker-unknown-cwd interactions you need to handle explicitly).
274
+
275
+ ```ts
276
+ // 1. Swap the github rule's PR-flow guidance for the generic
277
+ // feature-branch reminder (keep blocking direct commits to
278
+ // main, just drop the github-specific message). The generic
279
+ // `no-main-commit` is still active and fires on a github clone
280
+ // + main; disabling the github specialization makes the engine
281
+ // emit the generic message instead.
282
+ import { defineConfig } from "@cad0p/pi-steering";
283
+ import gitPlugin from "@cad0p/pi-steering/plugins/git";
284
+
285
+ export default defineConfig({
286
+ plugins: [gitPlugin],
287
+ disabledRules: ["no-main-commit-github"],
288
+ });
289
+ ```
290
+
291
+ ```ts
292
+ // 2. Disable + replace with a freshly-named user rule whose
293
+ // `reason` text points your agents at an internal skill /
294
+ // runbook. Spread the original to inherit `pattern`, `when:`,
295
+ // `tool`, `field`, and `noOverride` — only override the field
296
+ // you actually want to change. No `when:` changes → no
297
+ // walker-unknown-cwd interactions to reason about.
298
+ import { defineConfig } from "@cad0p/pi-steering";
299
+ import gitPlugin, { noMainCommitGithub } from "@cad0p/pi-steering/plugins/git";
300
+ import type { Rule } from "@cad0p/pi-steering";
301
+
302
+ const myNoMainCommitGithub = {
303
+ ...noMainCommitGithub,
304
+ // FRESH name — see warning below; reusing the original name has
305
+ // two failure modes, both bad.
306
+ name: "myorg-no-main-commit-github",
307
+ reason:
308
+ "You're on a github clone's protected branch. " +
309
+ "Open a PR for review (`gh pr create`); land via `gh pr merge` " +
310
+ "after approval. See skill `git-discipline@myorg` for our team's " +
311
+ "PR conventions.\n\n" +
312
+ "Safety: NEVER merge a PR or mark it ready-for-review unless " +
313
+ "the user explicitly asks. Wait for explicit user instruction.",
314
+ } as const satisfies Rule;
315
+
316
+ export default defineConfig({
317
+ plugins: [gitPlugin], // typo-checks the disable below
318
+ disabledRules: ["no-main-commit-github"], // drop the default
319
+ rules: [myNoMainCommitGithub], // replacement on
320
+ });
321
+ ```
322
+
323
+ ```ts
324
+ // 3. Disable the entire git plugin (drops all gitPlugin
325
+ // predicates / rules / trackers / extensions):
326
+ import { defineConfig } from "@cad0p/pi-steering";
327
+ import gitPlugin from "@cad0p/pi-steering/plugins/git";
328
+
329
+ export default defineConfig({
330
+ plugins: [gitPlugin],
331
+ disabledPlugins: ["git"],
332
+ });
333
+ ```
334
+
335
+ ### ⚠️ Always use a fresh name when extending or replacing a plugin rule
336
+
337
+ pi-steering composes user rules and plugin rules as
338
+ `[...userRules, ...pluginRules]` with **no name dedup at the
339
+ user/plugin layer.** Reusing the plugin rule's name in your config
340
+ has two failure modes, **both bad**, depending on whether you also
341
+ use `disabledRules`:
342
+
343
+ 1. **Same name + NO `disabledRules`** → BOTH rules are kept. The
344
+ plugin rule fires alongside your customized version, so paths
345
+ you intended to exempt still get the original message. The
346
+ customization silently fails to apply.
347
+
348
+ 2. **Same name + `disabledRules: ["original-name"]`** → the
349
+ `disabledRules` filter applies to ALL rules with that name
350
+ across both the user-config and plugin-rule sources. NEITHER
351
+ rule fires. Silent fail-OPEN — the worst outcome for a safety
352
+ rule, since the agent now has no guardrail at all.
353
+
354
+ Use a fresh name (e.g., `myorg-no-main-commit-github`). Pair it
355
+ with `disabledRules: ["no-main-commit-github"]` so the original is
356
+ dropped and your fresh-named replacement survives the disable
357
+ filter.
358
+
359
+ ### Cwd-based exemption (advanced)
360
+
361
+ A common request: "don't block commits to main inside my vault
362
+ directory" (vault flows like napkin-distill commit to a `main`
363
+ branch by design). Cwd-based exemptions need care because:
364
+
365
+ **The generic `no-main-commit` still fires on vault paths.**
366
+ Disabling only the github specialization isn't enough — the
367
+ generic rule's `when:` is just `{ branch: ... }` (no `remote:`
368
+ gate), so it fires on any github clone or any other repo whose
369
+ branch is one of the protected names. To actually exempt a path
370
+ you need to disable BOTH shipped rules and register a
371
+ user-authored rule.
372
+
373
+ Worked example:
374
+
375
+ ```ts
376
+ import { defineConfig } from "@cad0p/pi-steering";
377
+ import gitPlugin, { noMainCommit } from "@cad0p/pi-steering/plugins/git";
378
+ import type { Pattern, Rule } from "@cad0p/pi-steering";
379
+
380
+ const VAULT_DIRS: Pattern[] = [
381
+ /\/Goldmine\//,
382
+ /\/\.cache\/napkin-distill\//,
383
+ ];
384
+
385
+ const noMainCommitExceptVault = {
386
+ ...noMainCommit,
387
+ name: "myorg-no-main-commit-except-vault",
388
+ when: {
389
+ ...noMainCommit.when,
390
+ // Bare `not: { cwd: VAULT_DIRS }` is fail-CLOSED under
391
+ // walker-unknown cwd via the not-block's default block-level
392
+ // `onUnknown: "block"` modifier (see Walker-unknown cwd
393
+ // below). The inner `cwd:` predicate produces an `"unknown"`
394
+ // verdict, the not-block projects it to `false`, the outer
395
+ // rule fires — a `cd "$VAR" && git commit` is blocked rather
396
+ // than slipping past.
397
+ not: { cwd: VAULT_DIRS },
398
+ },
399
+ } as const satisfies Rule;
400
+
401
+ export default defineConfig({
402
+ plugins: [gitPlugin], // typo-checks the disables below
403
+ // BOTH shipped rules disabled; otherwise the generic
404
+ // `no-main-commit` fires on vault paths and the carve-out
405
+ // doesn't deliver on its name.
406
+ disabledRules: ["no-main-commit-github", "no-main-commit"],
407
+ rules: [noMainCommitExceptVault],
408
+ });
409
+ ```
410
+
411
+ ### Walker-unknown cwd
412
+
413
+ When the walker can't statically resolve cwd (e.g.
414
+ `cd "$VAR" && git commit`), the inner `cwd:` predicate returns
415
+ `"unknown"` and the not-block applies its block-level
416
+ `onUnknown:` modifier (default `"block"`) to project the verdict.
417
+ Default `"block"` means the not-block evaluates to `false`, the
418
+ outer rule's `not:` flips to `true`, and the rule fires —
419
+ fail-CLOSED. Authors who want walker-unknown vault paths to skip
420
+ the rule (fail-OPEN exemption) opt in with the block-level
421
+ modifier:
422
+
423
+ ```ts
424
+ not: { cwd: VAULT_DIRS, onUnknown: "allow" }
425
+ ```
426
+
427
+ The full truth table for `not: { cwd: ... }` under walker-known
428
+ vs walker-unknown cwd lives in the
429
+ [`dynamic-reason-runtime-cwd` example README][cwd-truth-table].
430
+
431
+ [cwd-truth-table]: ../../../examples/dynamic-reason-runtime-cwd/README.md
432
+
433
+ The `Pattern[]` annotation on `VAULT_DIRS` lets you mix string
434
+ patterns and RegExp without TS narrowing the array's element type
435
+ to `RegExp[]`. Annotation is optional for all-RegExp arrays (TS
436
+ infers `RegExp[]`, a subtype of `Pattern[]`); explicit `Pattern[]`
437
+ becomes load-bearing only when mixing strings and RegExp.
438
+
439
+ ## Authoring new plugins
440
+
441
+ This directory is the canonical reference for plugin authors. The
442
+ file layout separates concerns:
443
+
444
+ - `branch-tracker.ts` — walker state modifier (one file per tracker).
445
+ - `cwd-extensions.ts` — modifiers layering onto existing trackers.
446
+ - `predicates.ts` — one handler per `when.<key>` slot.
447
+ - `rules.ts` — rule definitions consuming the above.
448
+ - `index.ts` — default export assembling the plugin.
449
+
450
+ Each file has its own test suite; `integration.test.ts` pins end-to-
451
+ end wiring through `resolvePlugins` and `buildEvaluator`. Copy-adapt
452
+ this layout for your own plugin.
453
+
454
+ ### Composable building blocks
455
+
456
+ Several pieces are re-exported from `pi-steering/plugins/git` so
457
+ downstream plugins can reuse the engine's walker conventions
458
+ without reimplementing them:
459
+
460
+ - `walkerString(ctx, key, initialSentinel)` / `WalkerStringResult` —
461
+ narrows the walker-tracker value read off `ctx.walkerState[key]`
462
+ into one of `{ kind: "value"; value }` | `{ kind: "unknown" }` |
463
+ `{ kind: "missing" }`. The three-way discrimination is what the
464
+ `branch` predicate uses to dispatch on tracker state without
465
+ string-comparison sentinel checks.
466
+ - `NO_CHECKOUT_IN_CHAIN` — the branch tracker's fall-through
467
+ sentinel for chains where no in-chain `git checkout` /
468
+ `git switch` fired. Plugin authors who consume tracker state
469
+ directly can match this sentinel to know they need to fall back
470
+ to a shell-out (vs. `"unknown"`, where the tracker observed an
471
+ unresolvable checkout).
472
+ - `GIT_COMMIT_PATTERN` — the bash-command regex source matching
473
+ `git commit` (with optional pre-subcommand flag slots). Reuse
474
+ in plugin rules that want to share applicability with the
475
+ shipped commit-on-main rules.
476
+
477
+ Example import:
478
+
479
+ ```ts
480
+ import {
481
+ walkerString,
482
+ NO_CHECKOUT_IN_CHAIN,
483
+ GIT_COMMIT_PATTERN,
484
+ } from "@cad0p/pi-steering/plugins/git";
485
+ ```