@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,148 @@
|
|
|
1
|
+
# Example: dynamic-reason + walker-unknown-cwd
|
|
2
|
+
|
|
3
|
+
Worked example tying together two pi-steering primitives:
|
|
4
|
+
|
|
5
|
+
- Runtime-cwd predicate trinary surfacing (gitPlugin's `isClean` /
|
|
6
|
+
`hasStagedChanges` / `remote` / `upstream` / `commitsAhead` all inline a
|
|
7
|
+
walker-unknown-cwd guard at the top of the handler and surface trinary
|
|
8
|
+
`"unknown"` when the walker can't statically resolve cwd). The engine's
|
|
9
|
+
leaf-level (or block-level inside `not:`) `onUnknown:` policy then
|
|
10
|
+
projects to a definite verdict — default `"block"` (fail-CLOSED, rule
|
|
11
|
+
fires).
|
|
12
|
+
- `walkerUnknownCwdReason` (from `@cad0p/pi-steering`) — the agent-facing reason
|
|
13
|
+
text helper for the walker-unknown-cwd fail-CLOSED branch.
|
|
14
|
+
|
|
15
|
+
Together these give external plugin authors the ergonomics story for
|
|
16
|
+
runtime-cwd predicates: every gitPlugin runtime-cwd predicate gets the
|
|
17
|
+
engine's fail-CLOSED `onUnknown: "block"` semantics for free, and the
|
|
18
|
+
`walkerUnknownCwdReason` helper produces a useful agent-facing message
|
|
19
|
+
that distinguishes "the predicate fired because the state is genuinely
|
|
20
|
+
bad" from "the predicate fired because the walker couldn't tell".
|
|
21
|
+
|
|
22
|
+
## Pattern
|
|
23
|
+
|
|
24
|
+
The rule's `reason` is a `ReasonFn` (not a static string). It branches on
|
|
25
|
+
`ctx.walkerState?.cwd === "unknown"`:
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
reason: (ctx) => {
|
|
29
|
+
if (ctx.walkerState?.cwd === "unknown") {
|
|
30
|
+
// Walker couldn't statically resolve cwd. The predicate's inline
|
|
31
|
+
// guard surfaced trinary "unknown"; the engine's default
|
|
32
|
+
// onUnknown: "block" projected it to true so the rule fired
|
|
33
|
+
// fail-CLOSED. Produce a generic explanation via the helper, then
|
|
34
|
+
// append retry guidance.
|
|
35
|
+
return (
|
|
36
|
+
walkerUnknownCwdReason(ctx, "working tree status") +
|
|
37
|
+
" Run from inside the package directory with a literal path."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
// Walker statically resolved cwd; the predicate genuinely fired
|
|
41
|
+
// (working tree is dirty). Domain-specific reason.
|
|
42
|
+
return "Working tree has uncommitted changes. Commit or stash before deploying.";
|
|
43
|
+
},
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The agent gets a useful next-step in both cases instead of a
|
|
47
|
+
generic "rule blocked" message.
|
|
48
|
+
|
|
49
|
+
The rule is gated by `when: { isClean: false }`. The next subsection
|
|
50
|
+
explains why this canonical positive form is preferred over
|
|
51
|
+
`when: { not: { isClean: true } }` even though both shapes now produce
|
|
52
|
+
the same fail-CLOSED behavior under walker-unknown cwd by default.
|
|
53
|
+
|
|
54
|
+
## Why `isClean: false` over `not: { isClean: true }`
|
|
55
|
+
|
|
56
|
+
Both shapes describe "the working tree is NOT clean". Under the trinary
|
|
57
|
+
engine introduced in v0.1.0, both are fail-CLOSED by default on the
|
|
58
|
+
walker-unknown-cwd branch, so polarity-of-default is no longer the
|
|
59
|
+
deciding factor. The recommendation is now about readability, smaller
|
|
60
|
+
composition surface, and an end-to-end behavior gap on the
|
|
61
|
+
walker-known + git-fails row.
|
|
62
|
+
|
|
63
|
+
| state | `isClean` returns | `when: { isClean: false }` | `when: { not: { isClean: true } }` |
|
|
64
|
+
|-----------------------------|-------------------|----------------------------------|--------------------------------------|
|
|
65
|
+
| walker-unknown cwd | `"unknown"` | fires ✅ (leaf default block) | fires ✅ (block-level default block) |
|
|
66
|
+
| walker-known + clean | `true` | does NOT fire ✅ | does NOT fire ✅ |
|
|
67
|
+
| walker-known + dirty | `false` | fires ✅ | fires ✅ |
|
|
68
|
+
| walker-known + git fails | `false` | does NOT fire ❌ (fail-OPEN) | fires ⚠️ (Kleene-AND-false absorbs) |
|
|
69
|
+
|
|
70
|
+
The two shapes agree on every row except `walker-known + git fails`,
|
|
71
|
+
where they diverge:
|
|
72
|
+
|
|
73
|
+
- `isClean: false`: the handler returns boolean `false` on git failure
|
|
74
|
+
(`getWorkingTreeClean` returns `null` → handler short-circuits to
|
|
75
|
+
`false`, NOT `"unknown"`). Leaf verdict is `false` → rule skips.
|
|
76
|
+
- `not: { isClean: true }`: the handler again returns `false` on git
|
|
77
|
+
failure. Inner verdict is `false`, Kleene AND of a single false leaf
|
|
78
|
+
is `false`, the not-flip yields `true` → rule fires.
|
|
79
|
+
|
|
80
|
+
The asymmetry is real: `isClean: false` reads as "fire when the
|
|
81
|
+
handler positively asserts dirty"; `not: { isClean: true }` reads as
|
|
82
|
+
"fire when the handler does NOT positively assert clean", and a
|
|
83
|
+
failed git call is a non-clean-assertion under that wording. Pair
|
|
84
|
+
`isClean` with an `upstream` check to cover the git-fails branch
|
|
85
|
+
uniformly under either shape (handler returns `false` on git failure;
|
|
86
|
+
the `upstream:` leaf surfaces trinary `"unknown"` and the engine's
|
|
87
|
+
`onUnknown:` policy projects).
|
|
88
|
+
|
|
89
|
+
Why prefer `{ isClean: false }`:
|
|
90
|
+
|
|
91
|
+
- **Reads forward.** "Block when isClean is false" matches how a human
|
|
92
|
+
describes the rule out loud.
|
|
93
|
+
- **Smaller composition surface.** A bare leaf takes per-leaf
|
|
94
|
+
modifiers (`isClean: { value: false, onUnknown: "allow" }`) at the
|
|
95
|
+
leaf level. A `not:` block takes block-level modifiers and forbids
|
|
96
|
+
per-leaf modifiers inside; if you ever need a different `onUnknown:`
|
|
97
|
+
policy on this single leaf, the bare form is the simpler edit.
|
|
98
|
+
- **Idiomatic.** gitPlugin's `predicates.ts` JSDoc documents both
|
|
99
|
+
polarities of every boolean predicate (`isClean: true` /
|
|
100
|
+
`isClean: false`, `hasStagedChanges: true` / `hasStagedChanges: false`)
|
|
101
|
+
exactly so authors can avoid `not:` for negation when a documented
|
|
102
|
+
inverted shape exists.
|
|
103
|
+
|
|
104
|
+
### Pair with `upstream:` to cover the git-fails branch
|
|
105
|
+
|
|
106
|
+
Neither bare-form nor `not:`-form covers the `walker-known + git fails`
|
|
107
|
+
row uniformly. Put a fail-CLOSED `upstream:` check first in the same
|
|
108
|
+
`when:` to convert the gitfailure into a definite verdict (the
|
|
109
|
+
`upstream:` handler surfaces `"unknown"` instead of returning a
|
|
110
|
+
boolean, so the engine's `onUnknown:` policy projects to fail-CLOSED
|
|
111
|
+
and the rule fires regardless of which `isClean:` shape you used):
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
when: { upstream: /./, isClean: false }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`{ not: { isClean: true } }` is correct under the new engine for the
|
|
118
|
+
walker-unknown row — pick whichever reads cleaner at the call site,
|
|
119
|
+
and pair it with `upstream:` either way.
|
|
120
|
+
|
|
121
|
+
Rule of thumb for any future plugin author copying this example:
|
|
122
|
+
prefer the predicate's documented inverted shape (e.g., `isClean: false`,
|
|
123
|
+
`hasStagedChanges: false`) when one exists. Reach for `not:` when
|
|
124
|
+
combining multiple leaves under a single negation
|
|
125
|
+
(`not: { branch: "main", remote: /github/ }`) or when no inverted
|
|
126
|
+
shape is available (regex predicates, etc.).
|
|
127
|
+
|
|
128
|
+
## When to use this pattern
|
|
129
|
+
|
|
130
|
+
When your rule consumes any runtime-cwd predicate from a plugin
|
|
131
|
+
(gitPlugin's `isClean` / `hasStagedChanges` / `remote` / `upstream` /
|
|
132
|
+
`commitsAhead`, or any external plugin's runtime-cwd predicate that
|
|
133
|
+
inlines a `cwdIsWalkerUnknown` guard).
|
|
134
|
+
|
|
135
|
+
Without the two-branch ReasonFn, a rule with a static `reason: "..."`
|
|
136
|
+
would print the same message in both cases — uninformative when the
|
|
137
|
+
trigger was a dynamic-cwd target the walker bailed on.
|
|
138
|
+
|
|
139
|
+
## Cross-references
|
|
140
|
+
|
|
141
|
+
- `walkerUnknownCwdReason` JSDoc — `src/helpers/walker-unknown-cwd-reason.ts`. Read for the helper's signature, the `verifying` arg contract, and a worked rule snippet.
|
|
142
|
+
- gitPlugin runtime-cwd predicates — `src/plugins/git/predicates.ts`. Each has a `@see {@link walkerUnknownCwdReason}` cross-link in its JSDoc, surfacing the helper at the predicate's hover location.
|
|
143
|
+
|
|
144
|
+
## Run the tests
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pnpm --filter @examples/dynamic-reason-runtime-cwd test
|
|
148
|
+
```
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
basedir_win="$basedir"
|
|
4
|
+
exe=""
|
|
5
|
+
msys=""
|
|
6
|
+
|
|
7
|
+
case `uname -a` in
|
|
8
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
9
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
10
|
+
basedir_win=`cygpath -w "$basedir"`
|
|
11
|
+
fi
|
|
12
|
+
exe=".exe"
|
|
13
|
+
msys="true"
|
|
14
|
+
;;
|
|
15
|
+
*WSL2*)
|
|
16
|
+
if command -v wslpath > /dev/null 2>&1; then
|
|
17
|
+
basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
|
|
18
|
+
if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
|
|
19
|
+
basedir_win="$basedir"
|
|
20
|
+
else
|
|
21
|
+
exe=".exe"
|
|
22
|
+
fi
|
|
23
|
+
fi
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
|
|
27
|
+
if [ -z "$NODE_PATH" ]; then
|
|
28
|
+
export NODE_PATH="/home/pier/open-source/github/pi-steering/node_modules/.pnpm/node_modules"
|
|
29
|
+
else
|
|
30
|
+
export NODE_PATH="/home/pier/open-source/github/pi-steering/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
31
|
+
fi
|
|
32
|
+
if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
|
|
33
|
+
exec "$basedir/node.exe" "$basedir_win/../@cad0p/pi-steering/dist/bin/pi-steering.js" "$@"
|
|
34
|
+
elif [ -x "$basedir/node" ]; then
|
|
35
|
+
exec "$basedir/node" "$basedir/../@cad0p/pi-steering/dist/bin/pi-steering.js" "$@"
|
|
36
|
+
elif command -v node >/dev/null 2>&1; then
|
|
37
|
+
exec node "$basedir/../@cad0p/pi-steering/dist/bin/pi-steering.js" "$@"
|
|
38
|
+
elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
|
|
39
|
+
exec node.exe "$basedir_win/../@cad0p/pi-steering/dist/bin/pi-steering.js" "$@"
|
|
40
|
+
else
|
|
41
|
+
exec node "$basedir/../@cad0p/pi-steering/dist/bin/pi-steering.js" "$@"
|
|
42
|
+
fi
|
|
43
|
+
# cmd-shim-target=/home/pier/open-source/github/pi-steering/examples/dynamic-reason-runtime-cwd/node_modules/@cad0p/pi-steering/dist/bin/pi-steering.js
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@examples/dynamic-reason-runtime-cwd",
|
|
3
|
+
"version": "0.0.0-example",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Worked example: dynamic-reason + walker-unknown-cwd pattern (runtime-cwd predicate trinary surfacing + walkerUnknownCwdReason).",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"peerDependencies": {
|
|
8
|
+
"@cad0p/pi-steering": "workspace:*"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@cad0p/pi-steering": "workspace:*"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"pretest": "pnpm --filter @cad0p/pi-steering run clean && pnpm --filter @cad0p/pi-steering run build",
|
|
15
|
+
"test": "node --test --experimental-strip-types 'steering.test.ts'",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering / examples.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for the dynamic-reason + walker-unknown-cwd example.
|
|
6
|
+
*
|
|
7
|
+
* Pin the two ReasonFn branches:
|
|
8
|
+
*
|
|
9
|
+
* 1. Walker-unknown cwd → reason text contains
|
|
10
|
+
* `walkerUnknownCwdReason`'s anchor ("current directory:") AND
|
|
11
|
+
* the example's appended retry guidance ("literal path"
|
|
12
|
+
* reference is in the helper, "package directory" is in the
|
|
13
|
+
* append).
|
|
14
|
+
* 2. Walker-known cwd → reason text is the static "uncommitted
|
|
15
|
+
* changes" message.
|
|
16
|
+
*
|
|
17
|
+
* Also smoke-test the config shape (registered rule, bash/command,
|
|
18
|
+
* non-empty reason callable). The third branch (walker-known + clean
|
|
19
|
+
* tree → no fire) is covered structurally via the rule-shape pin
|
|
20
|
+
* below — inverting the `when:` clause regresses the test — rather
|
|
21
|
+
* than via a full engine roundtrip; full-engine coverage of the
|
|
22
|
+
* three-branch matrix is a v0.1.x candidate.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import assert from "node:assert/strict";
|
|
26
|
+
import { describe, it } from "node:test";
|
|
27
|
+
import type { PredicateContext } from "@cad0p/pi-steering";
|
|
28
|
+
import { mockContext } from "@cad0p/pi-steering/testing";
|
|
29
|
+
import config from "./steering.ts";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pull the only rule out of the config. Throws if the example
|
|
33
|
+
* regresses to a multi-rule shape — the tests below assume one rule.
|
|
34
|
+
*/
|
|
35
|
+
function getDeployRule() {
|
|
36
|
+
assert.ok(config.rules !== undefined, "config.rules should be defined");
|
|
37
|
+
const rule = config.rules.find(
|
|
38
|
+
(r) => r.name === "deploy-requires-clean-tree",
|
|
39
|
+
);
|
|
40
|
+
assert.ok(rule, "deploy-requires-clean-tree rule must be registered");
|
|
41
|
+
return rule;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Invoke the rule's `reason` function. The example uses a function
|
|
46
|
+
* reason (not a string), so this helper unwraps the callable.
|
|
47
|
+
*/
|
|
48
|
+
async function callReason(ctx: PredicateContext): Promise<string> {
|
|
49
|
+
const rule = getDeployRule();
|
|
50
|
+
assert.equal(
|
|
51
|
+
typeof rule.reason,
|
|
52
|
+
"function",
|
|
53
|
+
"this example's reason should be a ReasonFn",
|
|
54
|
+
);
|
|
55
|
+
const out = await (
|
|
56
|
+
rule.reason as (ctx: PredicateContext) => string | Promise<string>
|
|
57
|
+
)(ctx);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("example: dynamic-reason-runtime-cwd", () => {
|
|
62
|
+
it("rule.when uses the canonical positive form (`isClean: false`)", () => {
|
|
63
|
+
// Pins the rule's `when:` to `{ isClean: false }`. The equivalent
|
|
64
|
+
// `{ not: { isClean: true } }` shape produces the same fail-CLOSED
|
|
65
|
+
// behavior on the walker-unknown branch under the trinary engine's
|
|
66
|
+
// default block-level `onUnknown: "block"`, but diverges on the
|
|
67
|
+
// walker-known + git-fails row (see README truth table); the
|
|
68
|
+
// positive form is preferred for readability and per-leaf modifier
|
|
69
|
+
// composition. See README "Why isClean: false over not: { isClean: true }".
|
|
70
|
+
const rule = getDeployRule();
|
|
71
|
+
assert.deepEqual(rule.when, { isClean: false });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("registers the deploy-requires-clean-tree rule (bash/command)", () => {
|
|
75
|
+
const rule = getDeployRule();
|
|
76
|
+
assert.equal(rule.tool, "bash");
|
|
77
|
+
assert.equal(rule.field, "command");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("walker-unknown cwd → reason composes walkerUnknownCwdReason + retry guidance", async () => {
|
|
81
|
+
// Walker couldn't statically resolve cwd. The ReasonFn detects
|
|
82
|
+
// `ctx.walkerState.cwd === "unknown"` and delegates to
|
|
83
|
+
// `walkerUnknownCwdReason`, then appends domain-specific retry
|
|
84
|
+
// guidance.
|
|
85
|
+
const ctx = mockContext({
|
|
86
|
+
cwd: "/repo/root",
|
|
87
|
+
walkerState: { cwd: "unknown", env: new Map() },
|
|
88
|
+
});
|
|
89
|
+
const reason = await callReason(ctx);
|
|
90
|
+
|
|
91
|
+
// Helper anchor — pinned by walker-unknown-cwd-reason.test.ts.
|
|
92
|
+
assert.ok(
|
|
93
|
+
reason.includes("current directory:"),
|
|
94
|
+
`expected helper anchor, got: ${reason}`,
|
|
95
|
+
);
|
|
96
|
+
// Helper surfaces the actual ctx.cwd.
|
|
97
|
+
assert.ok(
|
|
98
|
+
reason.includes("/repo/root"),
|
|
99
|
+
`expected ctx.cwd in reason, got: ${reason}`,
|
|
100
|
+
);
|
|
101
|
+
// Example's appended retry guidance.
|
|
102
|
+
assert.ok(
|
|
103
|
+
reason.includes("package directory"),
|
|
104
|
+
`expected example's retry guidance ('package directory'), got: ${reason}`,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("walker-known cwd → reason is the static `uncommitted changes` message", async () => {
|
|
109
|
+
// Walker statically resolved cwd. The dirty-tree path: gitPlugin's
|
|
110
|
+
// `isClean` returned false, the rule fires, and the ReasonFn
|
|
111
|
+
// produces the domain-specific reason text.
|
|
112
|
+
const ctx = mockContext({
|
|
113
|
+
cwd: "/repo/root",
|
|
114
|
+
walkerState: { cwd: "/repo/root", env: new Map() },
|
|
115
|
+
});
|
|
116
|
+
const reason = await callReason(ctx);
|
|
117
|
+
|
|
118
|
+
// Static text — wording-tweak refactors should not break this
|
|
119
|
+
// assertion; pin substrings, not exact equality.
|
|
120
|
+
assert.ok(
|
|
121
|
+
reason.toLowerCase().includes("uncommitted"),
|
|
122
|
+
`expected static reason to mention 'uncommitted', got: ${reason}`,
|
|
123
|
+
);
|
|
124
|
+
// And the helper anchor MUST be absent on the static branch.
|
|
125
|
+
assert.ok(
|
|
126
|
+
!reason.includes("current directory:"),
|
|
127
|
+
`static branch must not emit the helper anchor, got: ${reason}`,
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering / examples.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Example: dynamic-reason + walker-unknown-cwd pattern.
|
|
6
|
+
*
|
|
7
|
+
* Demonstrates how external plugin authors compose runtime-cwd
|
|
8
|
+
* predicates (gitPlugin's `isClean` / `hasStagedChanges` / `remote` /
|
|
9
|
+
* `upstream` / `commitsAhead` — each inlines a walker-unknown-cwd
|
|
10
|
+
* guard at the top of its handler and surfaces trinary `"unknown"`)
|
|
11
|
+
* with informative agent-facing reasons that distinguish two
|
|
12
|
+
* branches:
|
|
13
|
+
*
|
|
14
|
+
* - Static cwd + predicate fires: domain-specific reason text
|
|
15
|
+
* (the working tree is genuinely dirty).
|
|
16
|
+
* - Walker-unknown cwd: `walkerUnknownCwdReason()` explains the
|
|
17
|
+
* walker couldn't statically resolve cwd, surfaces the actual
|
|
18
|
+
* `ctx.cwd`, and prompts a retry with a literal path. The
|
|
19
|
+
* example appends a small piece of domain-specific retry
|
|
20
|
+
* guidance after the helper's output.
|
|
21
|
+
*
|
|
22
|
+
* Drop this file in at `.pi/steering.ts` (or
|
|
23
|
+
* `.pi/steering/index.ts`) to activate. The example uses gitPlugin's
|
|
24
|
+
* `isClean` predicate to gate `npm run deploy` on a clean working
|
|
25
|
+
* tree.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { defineConfig, type Rule, walkerUnknownCwdReason } from "@cad0p/pi-steering";
|
|
29
|
+
import gitPlugin from "@cad0p/pi-steering/plugins/git";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Rule: block `npm run deploy` when the working tree isn't clean.
|
|
33
|
+
*
|
|
34
|
+
* The `reason` field is a {@link ReasonFn} that branches on
|
|
35
|
+
* `ctx.walkerState?.cwd === "unknown"` to detect the
|
|
36
|
+
* walker-unknown-cwd fail-CLOSED branch. gitPlugin's `isClean` inlines
|
|
37
|
+
* a walker-unknown-cwd guard at the top of its handler and surfaces
|
|
38
|
+
* trinary `"unknown"` when the walker can't statically resolve cwd;
|
|
39
|
+
* the engine's leaf-level `onUnknown:` policy (default `"block"`)
|
|
40
|
+
* projects to a definite `true` and the rule fires fail-CLOSED. On
|
|
41
|
+
* that branch, `walkerUnknownCwdReason` produces the canonical
|
|
42
|
+
* agent-facing explanation; the example appends domain-specific
|
|
43
|
+
* retry guidance after it.
|
|
44
|
+
*/
|
|
45
|
+
const deployRequiresCleanTree = {
|
|
46
|
+
name: "deploy-requires-clean-tree",
|
|
47
|
+
tool: "bash",
|
|
48
|
+
field: "command",
|
|
49
|
+
pattern: /^npm\s+run\s+deploy\b/,
|
|
50
|
+
// Canonical positive form: `isClean: false` ("fires when dirty")
|
|
51
|
+
// reads forward and lets per-leaf modifiers attach at the leaf if
|
|
52
|
+
// ever needed. The equivalent `not: { isClean: true }` form is also
|
|
53
|
+
// safe under the new engine — the trinary `"unknown"` from the
|
|
54
|
+
// inline walker-unknown-cwd guard composes via the not-block's
|
|
55
|
+
// default block-level `onUnknown: "block"` to fire fail-CLOSED on
|
|
56
|
+
// the walker-unknown branch — but `isClean: false` is the simpler
|
|
57
|
+
// shape when no other leaves share the not-block. See README
|
|
58
|
+
// "Why isClean: false over not: { isClean: true }" for the full
|
|
59
|
+
// truth table.
|
|
60
|
+
when: { isClean: false },
|
|
61
|
+
reason: (ctx) => {
|
|
62
|
+
if (ctx.walkerState?.cwd === "unknown") {
|
|
63
|
+
// Walker couldn't statically resolve cwd. The handler's
|
|
64
|
+
// inline guard surfaced trinary `"unknown"`; the engine's
|
|
65
|
+
// default leaf-level `onUnknown: "block"` projected it to true
|
|
66
|
+
// and the rule fired fail-CLOSED. Use the helper for a
|
|
67
|
+
// consistent agent-facing explanation; append domain-specific
|
|
68
|
+
// retry guidance.
|
|
69
|
+
return (
|
|
70
|
+
walkerUnknownCwdReason(ctx, "working tree status") +
|
|
71
|
+
" Run from inside the package directory with a literal path."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
// Predicate fired with a known cwd: working tree is genuinely dirty.
|
|
75
|
+
return "Working tree has uncommitted changes. Commit or stash before deploying.";
|
|
76
|
+
},
|
|
77
|
+
} as const satisfies Rule;
|
|
78
|
+
|
|
79
|
+
export default defineConfig({
|
|
80
|
+
plugins: [gitPlugin],
|
|
81
|
+
rules: [deployRequiresCleanTree],
|
|
82
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# force-push-strict
|
|
2
|
+
|
|
3
|
+
A rule pack that blocks **every** form of `git push --force`, including `--force-with-lease`.
|
|
4
|
+
|
|
5
|
+
## What it enforces
|
|
6
|
+
|
|
7
|
+
- `git push --force` → blocked
|
|
8
|
+
- `git push -f` → blocked
|
|
9
|
+
- `git push --force-with-lease` → **blocked** (this is the difference from the default)
|
|
10
|
+
- `git push origin main` → allowed
|
|
11
|
+
|
|
12
|
+
The pattern also handles git pre-subcommand flags (`git -C /other push --force`, `git -c key=val push -f`) and wrapper bypasses (`sh -c 'git push --force'`, `sudo xargs git push --force`, …) — all transparently, via the AST backend.
|
|
13
|
+
|
|
14
|
+
## How it differs from the default
|
|
15
|
+
|
|
16
|
+
The built-in `no-force-push` rule permits `--force-with-lease` because the lease flag is the documented "safe" way to update a branch after a rebase. That's fine for most teams, but in environments where:
|
|
17
|
+
|
|
18
|
+
- the branch is shared broadly (`main`, `develop`, long-lived release branches),
|
|
19
|
+
- history integrity is a compliance requirement, or
|
|
20
|
+
- you want the agent to never reach for any `--force` variant as a first-line fix,
|
|
21
|
+
|
|
22
|
+
the lease-variant carve-out is an attack surface. This rule pack closes it.
|
|
23
|
+
|
|
24
|
+
## When to use
|
|
25
|
+
|
|
26
|
+
- Strict-history environments (shared release branches, regulated contexts)
|
|
27
|
+
- Teams that want a single "never force push" discipline without agent-side judgement calls
|
|
28
|
+
- As a starting point for more restrictive house rules
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
Two equivalent forms. Pick whichever matches your setup.
|
|
33
|
+
|
|
34
|
+
### TypeScript form (canonical)
|
|
35
|
+
|
|
36
|
+
Copy [`steering.ts`](./steering.ts) to `~/.pi/steering.ts` (or
|
|
37
|
+
`<project-root>/.pi/steering.ts`) for a repo-scoped policy. The
|
|
38
|
+
TypeScript form participates in compile-time checking via
|
|
39
|
+
`defineConfig`. The loader accepts TypeScript only.
|
|
40
|
+
|
|
41
|
+
### JSON form (for migration)
|
|
42
|
+
|
|
43
|
+
The loader does **not** load `.pi/steering.json` files directly. The
|
|
44
|
+
[`steering.json`](./steering.json) below is provided as a reference
|
|
45
|
+
shape for `pi-steering import-json` migration — convert it with
|
|
46
|
+
`pi-steering import-json steering.json -o .pi/steering/index.ts` (or
|
|
47
|
+
programmatically via `fromJSON` from `compat.ts`).
|
|
48
|
+
|
|
49
|
+
JSON is a deliberate subset of the TypeScript shape: pattern-string
|
|
50
|
+
rules, `requires` / `unless`, `when.cwd` (string pattern only), and
|
|
51
|
+
override flags. Plugins, observers, function-valued rule fields,
|
|
52
|
+
plugin-registered predicate keys (`when.<customKey>`), `when.not`,
|
|
53
|
+
and `when.condition` are TypeScript-only — equivalently, any `when`
|
|
54
|
+
clause member other than `when.cwd` is rejected.
|
|
55
|
+
|
|
56
|
+
The `disable` entry below turns off the built-in `no-force-push`; the
|
|
57
|
+
new `no-force-push-strict` rule takes its place:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"disable": ["no-force-push"],
|
|
62
|
+
"rules": [
|
|
63
|
+
{
|
|
64
|
+
"name": "no-force-push-strict",
|
|
65
|
+
"tool": "bash",
|
|
66
|
+
"field": "command",
|
|
67
|
+
"pattern": "^git\\b(?:\\s+-{1,2}[A-Za-z]\\S*(?:\\s+\\S+)?)*\\s+push\\b.*(?:--force\\b|\\s-f(?:\\s|$))",
|
|
68
|
+
"reason": "No force pushes of any kind, including --force-with-lease."
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
See [examples/README.md#json](../README.md#json) for the full migration
|
|
75
|
+
workflow and the v0 fallback option.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
|
3
|
+
basedir_win="$basedir"
|
|
4
|
+
exe=""
|
|
5
|
+
msys=""
|
|
6
|
+
|
|
7
|
+
case `uname -a` in
|
|
8
|
+
*CYGWIN*|*MINGW*|*MSYS*)
|
|
9
|
+
if command -v cygpath > /dev/null 2>&1; then
|
|
10
|
+
basedir_win=`cygpath -w "$basedir"`
|
|
11
|
+
fi
|
|
12
|
+
exe=".exe"
|
|
13
|
+
msys="true"
|
|
14
|
+
;;
|
|
15
|
+
*WSL2*)
|
|
16
|
+
if command -v wslpath > /dev/null 2>&1; then
|
|
17
|
+
basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
|
|
18
|
+
if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
|
|
19
|
+
basedir_win="$basedir"
|
|
20
|
+
else
|
|
21
|
+
exe=".exe"
|
|
22
|
+
fi
|
|
23
|
+
fi
|
|
24
|
+
;;
|
|
25
|
+
esac
|
|
26
|
+
|
|
27
|
+
if [ -z "$NODE_PATH" ]; then
|
|
28
|
+
export NODE_PATH="/home/pier/open-source/github/pi-steering/node_modules/.pnpm/node_modules"
|
|
29
|
+
else
|
|
30
|
+
export NODE_PATH="/home/pier/open-source/github/pi-steering/node_modules/.pnpm/node_modules:$NODE_PATH"
|
|
31
|
+
fi
|
|
32
|
+
if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
|
|
33
|
+
exec "$basedir/node.exe" "$basedir_win/../@cad0p/pi-steering/dist/bin/pi-steering.js" "$@"
|
|
34
|
+
elif [ -x "$basedir/node" ]; then
|
|
35
|
+
exec "$basedir/node" "$basedir/../@cad0p/pi-steering/dist/bin/pi-steering.js" "$@"
|
|
36
|
+
elif command -v node >/dev/null 2>&1; then
|
|
37
|
+
exec node "$basedir/../@cad0p/pi-steering/dist/bin/pi-steering.js" "$@"
|
|
38
|
+
elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
|
|
39
|
+
exec node.exe "$basedir_win/../@cad0p/pi-steering/dist/bin/pi-steering.js" "$@"
|
|
40
|
+
else
|
|
41
|
+
exec node "$basedir/../@cad0p/pi-steering/dist/bin/pi-steering.js" "$@"
|
|
42
|
+
fi
|
|
43
|
+
# cmd-shim-target=/home/pier/open-source/github/pi-steering/examples/force-push-strict/node_modules/@cad0p/pi-steering/dist/bin/pi-steering.js
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@examples/force-push-strict",
|
|
3
|
+
"version": "0.0.0-example",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Rule pack: no force pushes of any kind (strict).",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"peerDependencies": {
|
|
8
|
+
"@cad0p/pi-steering": "workspace:*"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@cad0p/pi-steering": "workspace:*"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"pretest": "pnpm --filter @cad0p/pi-steering run clean && pnpm --filter @cad0p/pi-steering run build",
|
|
15
|
+
"test": "node --test --experimental-strip-types 'steering.test.ts'",
|
|
16
|
+
"typecheck": "tsc --noEmit"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"disable": ["no-force-push"],
|
|
3
|
+
"rules": [
|
|
4
|
+
{
|
|
5
|
+
"name": "no-force-push-strict",
|
|
6
|
+
"tool": "bash",
|
|
7
|
+
"field": "command",
|
|
8
|
+
"pattern": "^git\\b(?:\\s+-{1,2}[A-Za-z]\\S*(?:\\s+\\S+)?)*\\s+push\\b.*(?:--force\\b|\\s-f(?:\\s|$))",
|
|
9
|
+
"reason": "No force pushes of any kind, including --force-with-lease. Create a new commit, or reset + re-commit via a non-force path."
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering / examples.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Smoke test for the force-push-strict rule pack.
|
|
6
|
+
*
|
|
7
|
+
* Scope: README drift guard. Asserts that `steering.ts` compiles
|
|
8
|
+
* (covered by `tsc --noEmit` in the typecheck script) and that the
|
|
9
|
+
* resolved config has the expected shape. Full behavioral coverage
|
|
10
|
+
* (every pattern, every wrapper form) lives in the engine's own test
|
|
11
|
+
* suite, not here.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { describe, it } from "node:test";
|
|
16
|
+
import config from "./steering.ts";
|
|
17
|
+
|
|
18
|
+
describe("example: force-push-strict", () => {
|
|
19
|
+
it("exports a SteeringConfig with at least one rule", () => {
|
|
20
|
+
assert.ok(config.rules !== undefined, "config.rules should be defined");
|
|
21
|
+
assert.ok(
|
|
22
|
+
config.rules.length >= 1,
|
|
23
|
+
"config.rules should have at least one rule",
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("registers the strict rule name", () => {
|
|
28
|
+
const names = config.rules!.map((r) => r.name);
|
|
29
|
+
assert.ok(
|
|
30
|
+
names.includes("no-force-push-strict"),
|
|
31
|
+
`expected no-force-push-strict in rules, got: ${names.join(", ")}`,
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("disables the default `no-force-push` so the stricter variant owns the reason message", () => {
|
|
36
|
+
assert.ok(
|
|
37
|
+
config.disabledRules?.includes("no-force-push"),
|
|
38
|
+
"expected disabledRules to include 'no-force-push'",
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("strict rule has a bash/command shape and a non-empty reason", () => {
|
|
43
|
+
const strict = config.rules!.find((r) => r.name === "no-force-push-strict");
|
|
44
|
+
assert.ok(strict, "no-force-push-strict not found");
|
|
45
|
+
assert.equal(strict!.tool, "bash");
|
|
46
|
+
assert.ok(
|
|
47
|
+
typeof strict!.reason === "string" && strict!.reason.length > 0,
|
|
48
|
+
"reason should be a non-empty string",
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|