@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,54 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering / examples.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Example: force-push-strict rule pack.
|
|
6
|
+
*
|
|
7
|
+
* Equivalent to `steering.json` in this directory but expressed in the
|
|
8
|
+
* canonical TypeScript form. Drop this file in at
|
|
9
|
+
* `.pi/steering.ts` (or `.pi/steering/index.ts`) to activate.
|
|
10
|
+
*
|
|
11
|
+
* What it enforces: no force pushes of ANY kind, including
|
|
12
|
+
* `--force-with-lease` (which the shipped `DEFAULT_RULES.no-force-push`
|
|
13
|
+
* deliberately allows). Useful on teams where shared-branch history
|
|
14
|
+
* must stay append-only even for "safe" rewrites.
|
|
15
|
+
*
|
|
16
|
+
* Shape:
|
|
17
|
+
*
|
|
18
|
+
* - `disabledRules: ["no-force-push"]` drops the default rule so
|
|
19
|
+
* it doesn't fire alongside our stricter one (otherwise the
|
|
20
|
+
* default's block message would win on `git push --force`).
|
|
21
|
+
* - `no-force-push-strict` fires on `--force` (any suffix, any
|
|
22
|
+
* position) AND on `-f`. Matches the same pre-subcommand flag
|
|
23
|
+
* patterns as the default (`git -C /path push --force`,
|
|
24
|
+
* `git -c key=val push --force`, `git --git-dir=/x push -f`).
|
|
25
|
+
*
|
|
26
|
+
* Scope note: the git plugin's `no-main-commit` fires on top of this
|
|
27
|
+
* rule when the plugin is declared (`plugins: [gitPlugin]`, opt-in).
|
|
28
|
+
* If that's not wanted, omit the plugin or add
|
|
29
|
+
* `disabledRules: ["no-force-push", "no-main-commit"]`.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { defineConfig } from "@cad0p/pi-steering";
|
|
33
|
+
import gitPlugin from "@cad0p/pi-steering/plugins/git";
|
|
34
|
+
|
|
35
|
+
export default defineConfig({
|
|
36
|
+
plugins: [gitPlugin],
|
|
37
|
+
// Disable the shipped default so its less-strict block-reason (
|
|
38
|
+
// "use --force-with-lease if you must") doesn't leak to the LLM
|
|
39
|
+
// alongside our stricter variant.
|
|
40
|
+
disabledRules: ["no-force-push"],
|
|
41
|
+
rules: [
|
|
42
|
+
{
|
|
43
|
+
name: "no-force-push-strict",
|
|
44
|
+
tool: "bash",
|
|
45
|
+
field: "command",
|
|
46
|
+
// Mirrors DEFAULT_RULES.no-force-push's pre-subcommand flag
|
|
47
|
+
// slot but WITHOUT the `--force-with-lease` allowance.
|
|
48
|
+
pattern:
|
|
49
|
+
"^git\\b(?:\\s+-{1,2}[A-Za-z]\\S*(?:\\s+\\S+)?)*\\s+push\\b.*(?:--force\\b|\\s-f(?:\\s|$))",
|
|
50
|
+
reason:
|
|
51
|
+
"No force pushes of any kind, including --force-with-lease. Create a new commit, or reset + re-commit via a non-force path.",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# no-amend
|
|
2
|
+
|
|
3
|
+
A rule pack that blocks `git commit --amend`.
|
|
4
|
+
|
|
5
|
+
> **TypeScript form (canonical):** see [`steering.ts`](./steering.ts) — drop in at `~/.pi/steering.ts` or `<project-root>/.pi/steering.ts`. The loader accepts TypeScript only. The [`steering.json`](./steering.json) form is provided for reference + `pi-steering import-json` migration; the loader does NOT pick up JSON files. See [examples/README.md#json](../README.md#json) for migration steps.
|
|
6
|
+
|
|
7
|
+
## What it enforces
|
|
8
|
+
|
|
9
|
+
- `git commit --amend` → blocked
|
|
10
|
+
- `git commit -m "msg"` → allowed
|
|
11
|
+
- `git -C /path commit --amend` → blocked (handles the git pre-subcommand `-C` flag)
|
|
12
|
+
|
|
13
|
+
The AST backend also sees through wrappers — `sh -c 'git commit --amend'` and `sudo git commit --amend` are blocked.
|
|
14
|
+
|
|
15
|
+
## Rationale
|
|
16
|
+
|
|
17
|
+
`--amend` rewrites the last commit's SHA. For code-review workflows that track diffs across pushes, that's a regression:
|
|
18
|
+
|
|
19
|
+
- Reviewers compare revisions by SHA. An amended commit presents as a brand-new revision with no relation to the previous one, and per-line comments lose their anchor.
|
|
20
|
+
- History lineage matters for bisect and blame. Amending mid-review tangles both.
|
|
21
|
+
- If you genuinely need to fix the last commit's message or contents, a **follow-up commit** (`git commit -m "fix …"`) preserves continuity. Squash-merge at the end of review collapses the chain.
|
|
22
|
+
|
|
23
|
+
## Variant: cwd-scoped (`steering.cwd-scoped.json`)
|
|
24
|
+
|
|
25
|
+
If you want to apply the rule only to specific directory trees (e.g. a monorepo where some sub-projects enforce linear history and others don't), use `when.cwd`:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"rules": [
|
|
30
|
+
{
|
|
31
|
+
"name": "no-amend-in-personal",
|
|
32
|
+
"tool": "bash",
|
|
33
|
+
"field": "command",
|
|
34
|
+
"pattern": "^git\\b(?:\\s+-{1,2}[A-Za-z]\\S*(?:\\s+\\S+)?)*\\s+commit\\b.*--amend\\b",
|
|
35
|
+
"when": { "cwd": "^/home/[^/]+/projects/personal/" },
|
|
36
|
+
"reason": "In personal/shared repos, don't rewrite history. Create a new commit instead."
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`when.cwd` is the feature that distinguishes this engine from a regex-on-raw approach: it's tested against the **effective** cwd of each extracted command, so even `cd /home/me/projects/personal/site && git commit --amend` — run from a different session cwd — is still caught. The walker handles `&&`, `;`, subshells, and wrapper arguments.
|
|
43
|
+
|
|
44
|
+
## When to use
|
|
45
|
+
|
|
46
|
+
- Teams with post-push review (Gerrit, Phabricator, CRUX-style CR tools) where commit-SHA stability matters
|
|
47
|
+
- Projects that rely on bisect / blame history continuity
|
|
48
|
+
- Any workflow where the agent reaching for `--amend` as a "clean up the last commit" reflex hides work from review
|
|
49
|
+
|
|
50
|
+
## Install
|
|
51
|
+
|
|
52
|
+
Copy [`steering.ts`](./steering.ts) to `~/.pi/steering.ts` (global) or to `<project-root>/.pi/steering.ts` (scoped via walk-up loader). For the cwd-scoped variant, adapt the `when.cwd` regex inline. The loader accepts TypeScript only; the `steering.cwd-scoped.json` reference can be migrated via `pi-steering import-json` — see [examples/README.md#json](../README.md#json).
|
|
@@ -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/no-amend/node_modules/@cad0p/pi-steering/dist/bin/pi-steering.js
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@examples/no-amend",
|
|
3
|
+
"version": "0.0.0-example",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Rule pack: block git commit --amend.",
|
|
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
|
+
"rules": [
|
|
3
|
+
{
|
|
4
|
+
"name": "no-amend-in-personal",
|
|
5
|
+
"tool": "bash",
|
|
6
|
+
"field": "command",
|
|
7
|
+
"pattern": "^git\\b(?:\\s+-{1,2}[A-Za-z]\\S*(?:\\s+\\S+)?)*\\s+commit\\b.*--amend\\b",
|
|
8
|
+
"when": { "cwd": "^/home/[^/]+/projects/personal/" },
|
|
9
|
+
"reason": "In personal/shared repos, don't rewrite history. Create a new commit instead."
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rules": [
|
|
3
|
+
{
|
|
4
|
+
"name": "no-amend",
|
|
5
|
+
"tool": "bash",
|
|
6
|
+
"field": "command",
|
|
7
|
+
"pattern": "^git\\b(?:\\s+-{1,2}[A-Za-z]\\S*(?:\\s+\\S+)?)*\\s+commit\\b.*--amend\\b",
|
|
8
|
+
"reason": "Don't rewrite history with --amend. Create a new commit instead. If you need to fix the last commit's message, do it in a follow-up commit — PR reviewers track diffs across pushes and amend confuses that."
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering / examples.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Smoke test for the no-amend rule pack.
|
|
6
|
+
*
|
|
7
|
+
* Scope: README drift guard. See `force-push-strict/steering.test.ts`
|
|
8
|
+
* for the rationale (we're catching compilation drift, not regressing
|
|
9
|
+
* every rule behavior).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { describe, it } from "node:test";
|
|
14
|
+
import config from "./steering.ts";
|
|
15
|
+
|
|
16
|
+
describe("example: no-amend", () => {
|
|
17
|
+
it("exports a SteeringConfig with at least one rule", () => {
|
|
18
|
+
assert.ok(config.rules !== undefined, "config.rules should be defined");
|
|
19
|
+
assert.ok(
|
|
20
|
+
config.rules.length >= 1,
|
|
21
|
+
"config.rules should have at least one rule",
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("registers the no-amend rule", () => {
|
|
26
|
+
const names = config.rules!.map((r) => r.name);
|
|
27
|
+
assert.ok(
|
|
28
|
+
names.includes("no-amend"),
|
|
29
|
+
`expected no-amend in rules, got: ${names.join(", ")}`,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("no-amend rule has a bash/command shape and a non-empty reason", () => {
|
|
34
|
+
const rule = config.rules!.find((r) => r.name === "no-amend");
|
|
35
|
+
assert.ok(rule);
|
|
36
|
+
assert.equal(rule!.tool, "bash");
|
|
37
|
+
assert.ok(
|
|
38
|
+
typeof rule!.reason === "string" && rule!.reason.length > 0,
|
|
39
|
+
"reason should be a non-empty string",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("does not disable any default rules (additive)", () => {
|
|
44
|
+
assert.ok(
|
|
45
|
+
config.disabledRules === undefined || config.disabledRules.length === 0,
|
|
46
|
+
"no-amend is additive; disabledRules should be empty or absent",
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering / examples.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Example: no-amend rule pack.
|
|
6
|
+
*
|
|
7
|
+
* Equivalent to `steering.json` in this directory but expressed in the
|
|
8
|
+
* v0.1.0 canonical TypeScript form. Drop this file in at
|
|
9
|
+
* `.pi/steering.ts` (or `.pi/steering/index.ts`) to activate.
|
|
10
|
+
*
|
|
11
|
+
* What it enforces: blocks `git commit --amend` in any form. Useful
|
|
12
|
+
* on review-driven workflows where commit-SHA stability matters (an
|
|
13
|
+
* amend rewrites the SHA, which breaks reviewers' cross-push diff
|
|
14
|
+
* tracking).
|
|
15
|
+
*
|
|
16
|
+
* Scope note: does NOT conflict with any shipped `DEFAULT_RULES`.
|
|
17
|
+
* The rule is additive.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { defineConfig } from "@cad0p/pi-steering";
|
|
21
|
+
|
|
22
|
+
export default defineConfig({
|
|
23
|
+
rules: [
|
|
24
|
+
{
|
|
25
|
+
name: "no-amend",
|
|
26
|
+
tool: "bash",
|
|
27
|
+
field: "command",
|
|
28
|
+
// Mirrors the pre-subcommand flag slot used by
|
|
29
|
+
// DEFAULT_RULES.no-force-push so `git -C /path commit --amend`,
|
|
30
|
+
// `git -c key=val commit --amend`, and
|
|
31
|
+
// `git --git-dir=/x commit --amend` are all caught.
|
|
32
|
+
pattern:
|
|
33
|
+
"^git\\b(?:\\s+-{1,2}[A-Za-z]\\S*(?:\\s+\\S+)?)*\\s+commit\\b.*--amend\\b",
|
|
34
|
+
reason:
|
|
35
|
+
"Don't rewrite history with --amend. Create a new commit instead. If you need to fix the last commit's message, do it in a follow-up commit \u2014 PR reviewers track diffs across pushes and amend confuses that.",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# work-item-plugin — example plugin for pi-steering
|
|
2
|
+
|
|
3
|
+
> **Note**: this is a READABLE REFERENCE, not a plugin for production use. The rules and patterns are domain-generic on purpose — copy the structure, swap the specifics.
|
|
4
|
+
|
|
5
|
+
A compact plugin that demonstrates every authoring pattern the v0.1.0 release ships. Reading the source top-to-bottom is the fastest way to understand how to build one of these.
|
|
6
|
+
|
|
7
|
+
## What's in the box
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
examples/work-item-plugin/
|
|
11
|
+
├── package.json # peerDep on pi-steering
|
|
12
|
+
├── tsconfig.json # extends repo base
|
|
13
|
+
├── README.md # (this file)
|
|
14
|
+
└── src/
|
|
15
|
+
├── index.ts # default export: Plugin
|
|
16
|
+
├── index.test.ts # end-to-end plugin tests
|
|
17
|
+
├── predicates/
|
|
18
|
+
│ ├── work-item-format.ts # workItemFormat + [PROJ-N] regex
|
|
19
|
+
│ └── work-item-format.test.ts
|
|
20
|
+
├── observers/
|
|
21
|
+
│ ├── npm-test-tracker.ts # TEST_PASSED_EVENT + observer
|
|
22
|
+
│ ├── npm-test-tracker.test.ts
|
|
23
|
+
│ ├── retest-required-tracker.ts # RETEST_REQUIRED_EVENT + observer
|
|
24
|
+
│ └── retest-required-tracker.test.ts
|
|
25
|
+
└── rules/
|
|
26
|
+
├── commit-requires-work-item.ts
|
|
27
|
+
├── commit-requires-work-item.test.ts
|
|
28
|
+
├── push-requires-tests.ts
|
|
29
|
+
├── push-requires-tests.test.ts
|
|
30
|
+
├── commit-description-check.ts
|
|
31
|
+
└── commit-description-check.test.ts
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What each piece demonstrates
|
|
35
|
+
|
|
36
|
+
| File | Concept |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `predicates/work-item-format.ts` | `definePredicate<T>` for typed arg shapes. Quote-aware `input.args` access (ADR §9). |
|
|
39
|
+
| `observers/npm-test-tracker.ts` | Observer encapsulation convention (ADR §14): export `<EVENT>_EVENT` + `mark<Event>(ctx)` helper + the observer. |
|
|
40
|
+
| `observers/retest-required-tracker.ts` | Invalidation-sentinel observer for the `happened.since` pattern. Writes `RETEST_REQUIRED_EVENT` on every successful `git pull`. |
|
|
41
|
+
| `rules/commit-requires-work-item.ts` | Plugin predicate consumed from `when.<key>`. `not:` inversion. |
|
|
42
|
+
| `rules/push-requires-tests.ts` | Observer → rule coupling via shared event constants. `when.happened: { in: "agent_loop", since: ... }` with temporal invalidation. Also demonstrates `&&`-chain speculative allow for `npm test && git push`. |
|
|
43
|
+
| `rules/commit-description-check.ts` | Self-marking rule with `Rule.onFire` (ADR §6). Constant + helper co-located with the rule when no observer corresponds (ADR §14). |
|
|
44
|
+
| `index.ts` | `as const satisfies Plugin` to preserve literal types for `defineConfig`'s compile-time cross-reference checking (ADR §7). |
|
|
45
|
+
|
|
46
|
+
## ADR references
|
|
47
|
+
|
|
48
|
+
The canonical references for every pattern this plugin demonstrates live in the repo's ADR log (napkin vault):
|
|
49
|
+
|
|
50
|
+
- §6 — `Rule.onFire` side-effect hook (self-marking pattern)
|
|
51
|
+
- §7 — `Observer.writes` and `Rule.writes` declarations (compile-time type constraints)
|
|
52
|
+
- §9 — `PredicateToolInput.args` + `basename` (quote-aware structured access)
|
|
53
|
+
- §10 — `definePredicate<T>` helper (typed variance cast)
|
|
54
|
+
- §14 — Observer encapsulation convention (type + helper + consumer pattern)
|
|
55
|
+
- §15 — This example plugin's goals
|
|
56
|
+
|
|
57
|
+
## Running the tests
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
# From the repo root:
|
|
61
|
+
pnpm install
|
|
62
|
+
pnpm --filter @examples/work-item-plugin test
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
The `pretest` script builds `@cad0p/pi-steering` first, so the example resolves
|
|
66
|
+
the package through its emitted `dist/` exports. On a fresh clone this
|
|
67
|
+
adds a few seconds on the first run; subsequent runs skip the rebuild if
|
|
68
|
+
nothing changed.
|
|
69
|
+
|
|
70
|
+
Or `pnpm -r test` runs all packages' suites including this one.
|
|
71
|
+
|
|
72
|
+
## Consuming this plugin from your own config
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// .pi/steering/index.ts
|
|
76
|
+
import { defineConfig } from "@cad0p/pi-steering";
|
|
77
|
+
import workItemPlugin from "@examples/work-item-plugin";
|
|
78
|
+
|
|
79
|
+
export default defineConfig({
|
|
80
|
+
plugins: [workItemPlugin],
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
In a real project, you'd replace `@examples/work-item-plugin` with your own published plugin that follows this same layout.
|
|
85
|
+
|
|
86
|
+
## The rules, in one paragraph each
|
|
87
|
+
|
|
88
|
+
**`commit-requires-work-item`** — intercepts `git commit -m "..."` and blocks unless the message carries a `[PROJ-N]` token. Uses the `workItemFormat` predicate under `when.not` (the predicate returns "work-item is present" as TRUE; we invert it to mean "fire when missing").
|
|
89
|
+
|
|
90
|
+
**`push-requires-tests`** — intercepts `git push`. Blocks unless `npm test` has succeeded in the current agent loop AND no subsequent `git pull` has stale-d that success. Combines three features in one rule:
|
|
91
|
+
|
|
92
|
+
- `npm-test-tracker` writes `TEST_PASSED_EVENT` on every successful `npm test`.
|
|
93
|
+
- `retest-required-tracker` writes `RETEST_REQUIRED_EVENT` on every successful `git pull`.
|
|
94
|
+
- The rule gates via `when.happened: { event: TEST_PASSED_EVENT, in: "agent_loop", since: RETEST_REQUIRED_EVENT }` — fires when tests haven't passed at all in this loop, OR the latest test entry is older than the latest pull entry.
|
|
95
|
+
- Chain-aware: `npm test && git push` is speculatively allowed pre-execution. `&&` short-circuits on test failure, so the push is guarded without forcing a round-trip through the observer.
|
|
96
|
+
|
|
97
|
+
**`commit-description-check`** — self-marking reminder. First `git commit` per agent loop blocks with a "re-read your description" message and self-marks via `onFire`. Second `git commit` in the same loop passes (the self-mark satisfies `when.happened`). A new agent loop resets the reminder.
|
|
98
|
+
|
|
99
|
+
## What's intentionally missing
|
|
100
|
+
|
|
101
|
+
- **A `Tracker`** — the git plugin (`src/plugins/git/`) is the canonical reference for tracker + trackerExtension authoring. This plugin keeps the surface minimal.
|
|
102
|
+
- **Production-grade regexes** — the `[PROJ-N]` pattern is a placeholder. Real adopters replace with their project's actual ticket format (e.g. `JIRA-\d+`, `AWF-\d+`).
|
|
103
|
+
- **Override customization** — each rule uses the engine defaults around `noOverride`. See the main README's "Authoring rules" section for how overrides work.
|
|
@@ -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/work-item-plugin/node_modules/@cad0p/pi-steering/dist/bin/pi-steering.js
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@examples/work-item-plugin",
|
|
3
|
+
"version": "0.0.0-example",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Canonical example plugin for pi-steering (ADR \u00a715).",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"peerDependencies": {
|
|
8
|
+
"@cad0p/pi-steering": "workspace:*"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@cad0p/pi-steering": "workspace:*",
|
|
12
|
+
"@cad0p/unbash-walker": "^0.1.0"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"pretest": "pnpm --filter @cad0p/pi-steering run clean && pnpm --filter @cad0p/pi-steering run build",
|
|
16
|
+
"test": "node --test --experimental-strip-types 'src/**/*.test.ts'",
|
|
17
|
+
"typecheck": "tsc --noEmit"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of the pi-steering work-item-plugin example.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Plugin-level integration test.
|
|
6
|
+
*
|
|
7
|
+
* Wires the WHOLE plugin (not just one rule at a time) and exercises
|
|
8
|
+
* a few end-to-end scenarios. Each per-file test already covers unit
|
|
9
|
+
* behavior; this suite's job is to catch wiring-level regressions —
|
|
10
|
+
* e.g. the predicate not being registered, a rule not being listed,
|
|
11
|
+
* the observer's `writes` declaration not threading through.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { describe, it } from "node:test";
|
|
16
|
+
import {
|
|
17
|
+
createRecordingHost,
|
|
18
|
+
expectAllows,
|
|
19
|
+
expectBlocks,
|
|
20
|
+
loadHarness,
|
|
21
|
+
mockExtensionContext,
|
|
22
|
+
} from "@cad0p/pi-steering/testing";
|
|
23
|
+
import workItemPlugin, {
|
|
24
|
+
DESCRIPTION_REVIEWED_EVENT,
|
|
25
|
+
TEST_PASSED_EVENT,
|
|
26
|
+
} from "./index.ts";
|
|
27
|
+
|
|
28
|
+
describe("work-item-plugin (end-to-end)", () => {
|
|
29
|
+
it("registers the expected predicates, rules, observers", () => {
|
|
30
|
+
assert.equal(workItemPlugin.name, "work-item");
|
|
31
|
+
assert.ok(workItemPlugin.predicates?.workItemFormat);
|
|
32
|
+
assert.equal(workItemPlugin.rules?.length, 3);
|
|
33
|
+
assert.equal(workItemPlugin.observers?.length, 2);
|
|
34
|
+
|
|
35
|
+
const ruleNames = workItemPlugin.rules?.map((r) => r.name);
|
|
36
|
+
assert.deepEqual(ruleNames, [
|
|
37
|
+
"commit-requires-work-item",
|
|
38
|
+
"push-requires-tests",
|
|
39
|
+
"commit-description-check",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const observerNames = workItemPlugin.observers?.map((o) => o.name);
|
|
43
|
+
assert.deepEqual(observerNames, [
|
|
44
|
+
"npm-test-tracker",
|
|
45
|
+
"retest-required-tracker",
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("blocks a commit without a work-item tag", async () => {
|
|
50
|
+
const harness = loadHarness({
|
|
51
|
+
config: { plugins: [workItemPlugin] },
|
|
52
|
+
});
|
|
53
|
+
await expectBlocks(
|
|
54
|
+
harness,
|
|
55
|
+
{ command: 'git commit -m "feat: add thing"' },
|
|
56
|
+
{ rule: "commit-requires-work-item" },
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("blocks git push when tests haven't passed this loop", async () => {
|
|
61
|
+
const harness = loadHarness({
|
|
62
|
+
config: { plugins: [workItemPlugin] },
|
|
63
|
+
});
|
|
64
|
+
await expectBlocks(
|
|
65
|
+
harness,
|
|
66
|
+
{ command: "git push" },
|
|
67
|
+
{ rule: "push-requires-tests" },
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("allows a well-formed commit after description-check self-marks", async () => {
|
|
72
|
+
// This test is intentionally the FULL happy-path: block on
|
|
73
|
+
// first commit (description-check fires), self-mark, then
|
|
74
|
+
// allow the second commit that carries a [PROJ-N] tag.
|
|
75
|
+
const host = createRecordingHost();
|
|
76
|
+
const ctx = mockExtensionContext("/tmp/test", host.entries);
|
|
77
|
+
const harness = loadHarness({
|
|
78
|
+
config: { plugins: [workItemPlugin] },
|
|
79
|
+
host,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// First: commit-description-check fires and self-marks.
|
|
83
|
+
const first = await harness.evaluate(
|
|
84
|
+
{
|
|
85
|
+
type: "tool_call",
|
|
86
|
+
toolCallId: "tc1",
|
|
87
|
+
toolName: "bash",
|
|
88
|
+
input: { command: 'git commit -m "feat: [PROJ-1] work"' },
|
|
89
|
+
} as unknown as Parameters<typeof harness.evaluate>[0],
|
|
90
|
+
ctx,
|
|
91
|
+
1,
|
|
92
|
+
);
|
|
93
|
+
assert.ok(
|
|
94
|
+
first !== undefined && first !== null,
|
|
95
|
+
"first commit should block on the reminder",
|
|
96
|
+
);
|
|
97
|
+
assert.ok(
|
|
98
|
+
host.entries.some((e) => e.customType === DESCRIPTION_REVIEWED_EVENT),
|
|
99
|
+
"DESCRIPTION_REVIEWED_EVENT should be present after onFire",
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Second: well-formed commit passes. `commit-requires-work-item`
|
|
103
|
+
// also runs; message has the tag so it's fine.
|
|
104
|
+
const second = await harness.evaluate(
|
|
105
|
+
{
|
|
106
|
+
type: "tool_call",
|
|
107
|
+
toolCallId: "tc2",
|
|
108
|
+
toolName: "bash",
|
|
109
|
+
input: { command: 'git commit -m "feat: [PROJ-1] work"' },
|
|
110
|
+
} as unknown as Parameters<typeof harness.evaluate>[0],
|
|
111
|
+
ctx,
|
|
112
|
+
1,
|
|
113
|
+
);
|
|
114
|
+
assert.equal(second, undefined, "second well-formed commit should pass");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("dispatch → evaluate: npm test success unblocks git push in the same loop", async () => {
|
|
118
|
+
const host = createRecordingHost();
|
|
119
|
+
const ctx = mockExtensionContext("/tmp/test", host.entries);
|
|
120
|
+
const harness = loadHarness({
|
|
121
|
+
config: { plugins: [workItemPlugin] },
|
|
122
|
+
host,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Fire npm test success through the dispatcher — observer
|
|
126
|
+
// writes TEST_PASSED_EVENT into the shared entries store.
|
|
127
|
+
await harness.dispatch(
|
|
128
|
+
{
|
|
129
|
+
type: "tool_result",
|
|
130
|
+
toolCallId: "tc1",
|
|
131
|
+
toolName: "bash",
|
|
132
|
+
input: { command: "npm test" },
|
|
133
|
+
content: [],
|
|
134
|
+
details: { exitCode: 0 },
|
|
135
|
+
} as unknown as Parameters<typeof harness.dispatch>[0],
|
|
136
|
+
ctx,
|
|
137
|
+
7,
|
|
138
|
+
);
|
|
139
|
+
assert.ok(
|
|
140
|
+
host.entries.some((e) => e.customType === TEST_PASSED_EVENT),
|
|
141
|
+
"observer should have written TEST_PASSED_EVENT",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Push now allowed — tests passed this loop.
|
|
145
|
+
const pushResult = await harness.evaluate(
|
|
146
|
+
{
|
|
147
|
+
type: "tool_call",
|
|
148
|
+
toolCallId: "tc2",
|
|
149
|
+
toolName: "bash",
|
|
150
|
+
input: { command: "git push" },
|
|
151
|
+
} as unknown as Parameters<typeof harness.evaluate>[0],
|
|
152
|
+
ctx,
|
|
153
|
+
7,
|
|
154
|
+
);
|
|
155
|
+
assert.equal(
|
|
156
|
+
pushResult,
|
|
157
|
+
undefined,
|
|
158
|
+
"push-requires-tests should not fire after npm test succeeded",
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("does not fire on unrelated commands", async () => {
|
|
163
|
+
const harness = loadHarness({
|
|
164
|
+
config: { plugins: [workItemPlugin] },
|
|
165
|
+
});
|
|
166
|
+
await expectAllows(harness, { command: "ls -la" });
|
|
167
|
+
});
|
|
168
|
+
});
|