@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.
- package/CHANGELOG.md +36 -0
- package/LICENSE +29 -0
- package/README.md +920 -0
- package/dist/__test-helpers__.d.ts +95 -0
- package/dist/__test-helpers__.d.ts.map +1 -0
- package/dist/__test-helpers__.js +174 -0
- package/dist/__test-helpers__.js.map +1 -0
- package/dist/bin/pi-steering.d.ts +9 -0
- package/dist/bin/pi-steering.d.ts.map +1 -0
- package/dist/bin/pi-steering.js +506 -0
- package/dist/bin/pi-steering.js.map +1 -0
- package/dist/compat.d.ts +50 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +180 -0
- package/dist/compat.js.map +1 -0
- package/dist/defaults.d.ts +61 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +106 -0
- package/dist/defaults.js.map +1 -0
- package/dist/define-config.d.ts +235 -0
- package/dist/define-config.d.ts.map +1 -0
- package/dist/define-config.js +118 -0
- package/dist/define-config.js.map +1 -0
- package/dist/define-predicate.d.ts +49 -0
- package/dist/define-predicate.d.ts.map +1 -0
- package/dist/define-predicate.js +35 -0
- package/dist/define-predicate.js.map +1 -0
- package/dist/evaluator-internals/context.d.ts +186 -0
- package/dist/evaluator-internals/context.d.ts.map +1 -0
- package/dist/evaluator-internals/context.js +219 -0
- package/dist/evaluator-internals/context.js.map +1 -0
- package/dist/evaluator-internals/override.d.ts +19 -0
- package/dist/evaluator-internals/override.d.ts.map +1 -0
- package/dist/evaluator-internals/override.js +79 -0
- package/dist/evaluator-internals/override.js.map +1 -0
- package/dist/evaluator-internals/predicates.d.ts +152 -0
- package/dist/evaluator-internals/predicates.d.ts.map +1 -0
- package/dist/evaluator-internals/predicates.js +849 -0
- package/dist/evaluator-internals/predicates.js.map +1 -0
- package/dist/evaluator-internals/speculative-synthesis.d.ts +102 -0
- package/dist/evaluator-internals/speculative-synthesis.d.ts.map +1 -0
- package/dist/evaluator-internals/speculative-synthesis.js +101 -0
- package/dist/evaluator-internals/speculative-synthesis.js.map +1 -0
- package/dist/evaluator.d.ts +101 -0
- package/dist/evaluator.d.ts.map +1 -0
- package/dist/evaluator.js +714 -0
- package/dist/evaluator.js.map +1 -0
- package/dist/helpers/walker-unknown-cwd-reason.d.ts +68 -0
- package/dist/helpers/walker-unknown-cwd-reason.d.ts.map +1 -0
- package/dist/helpers/walker-unknown-cwd-reason.js +73 -0
- package/dist/helpers/walker-unknown-cwd-reason.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/drop-unused-observers.d.ts +43 -0
- package/dist/internal/drop-unused-observers.d.ts.map +1 -0
- package/dist/internal/drop-unused-observers.js +50 -0
- package/dist/internal/drop-unused-observers.js.map +1 -0
- package/dist/internal/finalize-plugin-state.d.ts +22 -0
- package/dist/internal/finalize-plugin-state.d.ts.map +1 -0
- package/dist/internal/finalize-plugin-state.js +14 -0
- package/dist/internal/finalize-plugin-state.js.map +1 -0
- package/dist/internal/merge-observers.d.ts +30 -0
- package/dist/internal/merge-observers.d.ts.map +1 -0
- package/dist/internal/merge-observers.js +27 -0
- package/dist/internal/merge-observers.js.map +1 -0
- package/dist/internal/pattern-utils.d.ts +33 -0
- package/dist/internal/pattern-utils.d.ts.map +1 -0
- package/dist/internal/pattern-utils.js +20 -0
- package/dist/internal/pattern-utils.js.map +1 -0
- package/dist/internal/ref-text.d.ts +11 -0
- package/dist/internal/ref-text.d.ts.map +1 -0
- package/dist/internal/ref-text.js +15 -0
- package/dist/internal/ref-text.js.map +1 -0
- package/dist/internal/session-runtime.d.ts +44 -0
- package/dist/internal/session-runtime.d.ts.map +1 -0
- package/dist/internal/session-runtime.js +143 -0
- package/dist/internal/session-runtime.js.map +1 -0
- package/dist/internal/watch-matcher.d.ts +56 -0
- package/dist/internal/watch-matcher.d.ts.map +1 -0
- package/dist/internal/watch-matcher.js +179 -0
- package/dist/internal/watch-matcher.js.map +1 -0
- package/dist/loader.d.ts +97 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +527 -0
- package/dist/loader.js.map +1 -0
- package/dist/observer-dispatcher.d.ts +69 -0
- package/dist/observer-dispatcher.d.ts.map +1 -0
- package/dist/observer-dispatcher.js +184 -0
- package/dist/observer-dispatcher.js.map +1 -0
- package/dist/plugin-merger.d.ts +175 -0
- package/dist/plugin-merger.d.ts.map +1 -0
- package/dist/plugin-merger.js +450 -0
- package/dist/plugin-merger.js.map +1 -0
- package/dist/plugins/git/branch-tracker.d.ts +85 -0
- package/dist/plugins/git/branch-tracker.d.ts.map +1 -0
- package/dist/plugins/git/branch-tracker.js +181 -0
- package/dist/plugins/git/branch-tracker.js.map +1 -0
- package/dist/plugins/git/cwd-extensions.d.ts +10 -0
- package/dist/plugins/git/cwd-extensions.d.ts.map +1 -0
- package/dist/plugins/git/cwd-extensions.js +155 -0
- package/dist/plugins/git/cwd-extensions.js.map +1 -0
- package/dist/plugins/git/git-ops.d.ts +89 -0
- package/dist/plugins/git/git-ops.d.ts.map +1 -0
- package/dist/plugins/git/git-ops.js +107 -0
- package/dist/plugins/git/git-ops.js.map +1 -0
- package/dist/plugins/git/index.d.ts +225 -0
- package/dist/plugins/git/index.d.ts.map +1 -0
- package/dist/plugins/git/index.js +52 -0
- package/dist/plugins/git/index.js.map +1 -0
- package/dist/plugins/git/predicates.d.ts +312 -0
- package/dist/plugins/git/predicates.d.ts.map +1 -0
- package/dist/plugins/git/predicates.js +580 -0
- package/dist/plugins/git/predicates.js.map +1 -0
- package/dist/plugins/git/rules.d.ts +209 -0
- package/dist/plugins/git/rules.d.ts.map +1 -0
- package/dist/plugins/git/rules.js +267 -0
- package/dist/plugins/git/rules.js.map +1 -0
- package/dist/schema.d.ts +1799 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +4 -0
- package/dist/schema.js.map +1 -0
- package/dist/testing/index.d.ts +621 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +747 -0
- package/dist/testing/index.js.map +1 -0
- package/examples/README.md +83 -0
- package/examples/combined-git-discipline/README.md +52 -0
- package/examples/combined-git-discipline/node_modules/.bin/pi-steering +43 -0
- package/examples/combined-git-discipline/package.json +18 -0
- package/examples/combined-git-discipline/steering.json +27 -0
- package/examples/combined-git-discipline/steering.test.ts +42 -0
- package/examples/combined-git-discipline/steering.ts +64 -0
- package/examples/combined-git-discipline/tsconfig.json +7 -0
- package/examples/draft-prs-only/README.md +42 -0
- package/examples/draft-prs-only/node_modules/.bin/pi-steering +43 -0
- package/examples/draft-prs-only/package.json +18 -0
- package/examples/draft-prs-only/steering.json +12 -0
- package/examples/draft-prs-only/steering.test.ts +48 -0
- package/examples/draft-prs-only/steering.ts +36 -0
- package/examples/draft-prs-only/tsconfig.json +7 -0
- package/examples/dynamic-reason-runtime-cwd/README.md +148 -0
- package/examples/dynamic-reason-runtime-cwd/node_modules/.bin/pi-steering +43 -0
- package/examples/dynamic-reason-runtime-cwd/package.json +18 -0
- package/examples/dynamic-reason-runtime-cwd/steering.test.ts +130 -0
- package/examples/dynamic-reason-runtime-cwd/steering.ts +82 -0
- package/examples/dynamic-reason-runtime-cwd/tsconfig.json +7 -0
- package/examples/force-push-strict/README.md +75 -0
- package/examples/force-push-strict/node_modules/.bin/pi-steering +43 -0
- package/examples/force-push-strict/package.json +18 -0
- package/examples/force-push-strict/steering.json +12 -0
- package/examples/force-push-strict/steering.test.ts +51 -0
- package/examples/force-push-strict/steering.ts +54 -0
- package/examples/force-push-strict/tsconfig.json +7 -0
- package/examples/no-amend/README.md +52 -0
- package/examples/no-amend/node_modules/.bin/pi-steering +43 -0
- package/examples/no-amend/package.json +18 -0
- package/examples/no-amend/steering.cwd-scoped.json +12 -0
- package/examples/no-amend/steering.json +11 -0
- package/examples/no-amend/steering.test.ts +49 -0
- package/examples/no-amend/steering.ts +38 -0
- package/examples/no-amend/tsconfig.json +7 -0
- package/examples/work-item-plugin/README.md +103 -0
- package/examples/work-item-plugin/node_modules/.bin/pi-steering +43 -0
- package/examples/work-item-plugin/package.json +19 -0
- package/examples/work-item-plugin/src/index.test.ts +168 -0
- package/examples/work-item-plugin/src/index.ts +122 -0
- package/examples/work-item-plugin/src/observers/npm-test-tracker.test.ts +95 -0
- package/examples/work-item-plugin/src/observers/npm-test-tracker.ts +88 -0
- package/examples/work-item-plugin/src/observers/retest-required-tracker.test.ts +81 -0
- package/examples/work-item-plugin/src/observers/retest-required-tracker.ts +81 -0
- package/examples/work-item-plugin/src/predicates/work-item-format.test.ts +157 -0
- package/examples/work-item-plugin/src/predicates/work-item-format.ts +99 -0
- package/examples/work-item-plugin/src/rules/commit-description-check.test.ts +107 -0
- package/examples/work-item-plugin/src/rules/commit-description-check.ts +83 -0
- package/examples/work-item-plugin/src/rules/commit-requires-work-item.test.ts +79 -0
- package/examples/work-item-plugin/src/rules/commit-requires-work-item.ts +56 -0
- package/examples/work-item-plugin/src/rules/push-requires-tests.test.ts +245 -0
- package/examples/work-item-plugin/src/rules/push-requires-tests.ts +62 -0
- package/examples/work-item-plugin/tsconfig.json +8 -0
- package/package.json +88 -0
- package/skills/steering-authoring/SKILL.md +102 -0
- package/src/INVARIANTS.md +116 -0
- package/src/__test-helpers__.ts +243 -0
- package/src/bin/pi-steering.test.ts +920 -0
- package/src/bin/pi-steering.ts +584 -0
- package/src/compat.test.ts +431 -0
- package/src/compat.ts +282 -0
- package/src/defaults.test.ts +585 -0
- package/src/defaults.ts +148 -0
- package/src/define-config.test.ts +888 -0
- package/src/define-config.ts +375 -0
- package/src/define-predicate.test.ts +70 -0
- package/src/define-predicate.ts +57 -0
- package/src/evaluator-internals/context.ts +290 -0
- package/src/evaluator-internals/override.test.ts +99 -0
- package/src/evaluator-internals/override.ts +83 -0
- package/src/evaluator-internals/predicates.ts +1037 -0
- package/src/evaluator-internals/speculative-synthesis.test.ts +411 -0
- package/src/evaluator-internals/speculative-synthesis.ts +199 -0
- package/src/evaluator.test.ts +6296 -0
- package/src/evaluator.ts +1106 -0
- package/src/factory-time-load.test.ts +630 -0
- package/src/helpers/walker-unknown-cwd-reason.test.ts +73 -0
- package/src/helpers/walker-unknown-cwd-reason.ts +83 -0
- package/src/index.test.ts +836 -0
- package/src/index.ts +229 -0
- package/src/internal/drop-unused-observers.test.ts +141 -0
- package/src/internal/drop-unused-observers.ts +81 -0
- package/src/internal/finalize-plugin-state.ts +43 -0
- package/src/internal/merge-observers.ts +51 -0
- package/src/internal/pattern-utils.test.ts +52 -0
- package/src/internal/pattern-utils.ts +38 -0
- package/src/internal/ref-text.test.ts +54 -0
- package/src/internal/ref-text.ts +17 -0
- package/src/internal/session-runtime.test.ts +629 -0
- package/src/internal/session-runtime.ts +201 -0
- package/src/internal/watch-matcher.test.ts +326 -0
- package/src/internal/watch-matcher.ts +200 -0
- package/src/jsdoc-propagation.test.ts +277 -0
- package/src/loader.test.ts +1098 -0
- package/src/loader.ts +581 -0
- package/src/not-block-onunknown.test.ts +554 -0
- package/src/observer-dispatcher.test.ts +939 -0
- package/src/observer-dispatcher.ts +290 -0
- package/src/per-predicate-typing.test.ts +361 -0
- package/src/plugin-merger.test.ts +790 -0
- package/src/plugin-merger.ts +598 -0
- package/src/plugins/git/README.md +485 -0
- package/src/plugins/git/branch-tracker.test.ts +281 -0
- package/src/plugins/git/branch-tracker.ts +187 -0
- package/src/plugins/git/cwd-extensions.test.ts +230 -0
- package/src/plugins/git/cwd-extensions.ts +160 -0
- package/src/plugins/git/git-ops.test.ts +397 -0
- package/src/plugins/git/git-ops.ts +173 -0
- package/src/plugins/git/index.ts +253 -0
- package/src/plugins/git/integration.test.ts +705 -0
- package/src/plugins/git/predicates.test.ts +1401 -0
- package/src/plugins/git/predicates.ts +693 -0
- package/src/plugins/git/rules.test.ts +1001 -0
- package/src/plugins/git/rules.ts +307 -0
- package/src/schema.test.ts +259 -0
- package/src/schema.ts +1963 -0
- package/src/testing/index.test.ts +1696 -0
- 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
|
+
```
|