@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,118 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering.
|
|
3
|
+
/**
|
|
4
|
+
* Build a {@link SteeringConfig} with cross-reference name checking.
|
|
5
|
+
*
|
|
6
|
+
* Observer references in {@link Rule.observer} are typed against the
|
|
7
|
+
* union of observer names gathered from `plugins[*].observers` AND the
|
|
8
|
+
* top-level `observers` array — a typo produces a compile error.
|
|
9
|
+
*
|
|
10
|
+
* The `disabledRules` / `disabledPlugins` arrays are typed against the unions
|
|
11
|
+
* of registered rule / plugin names — typos rejected.
|
|
12
|
+
*
|
|
13
|
+
* `rules[].when.happened.event` and `rules[].when.happened.since` are
|
|
14
|
+
* both typed against the union of all `writes` declarations across
|
|
15
|
+
* plugin rules, plugin observers, user rules, and user observers —
|
|
16
|
+
* typos rejected. (The `since` field on the `Writes` union enforces
|
|
17
|
+
* the same contract as `event`: the sentinel event must be known to
|
|
18
|
+
* the config, not a free-form string.)
|
|
19
|
+
*
|
|
20
|
+
* Runtime behavior: returns a shallow copy of the input with optional
|
|
21
|
+
* fields normalized from `readonly` arrays to mutable arrays (the
|
|
22
|
+
* {@link SteeringConfig} shape doesn't constrain mutability). The
|
|
23
|
+
* return value is safe to pass to the loader / buildConfig.
|
|
24
|
+
*
|
|
25
|
+
* ## Authoring pattern — preserving observer/plugin names for inference
|
|
26
|
+
*
|
|
27
|
+
* For compile-time typo detection on rule `observer` references, declare
|
|
28
|
+
* your observers and plugins with `as const satisfies` so TypeScript
|
|
29
|
+
* preserves the literal `name` values through to `AllObserverNames`:
|
|
30
|
+
*
|
|
31
|
+
* const myObs = {
|
|
32
|
+
* name: "description-read",
|
|
33
|
+
* onResult: (event, ctx) => { ... },
|
|
34
|
+
* } as const satisfies Observer;
|
|
35
|
+
*
|
|
36
|
+
* const myPlugin = {
|
|
37
|
+
* name: "my-plugin",
|
|
38
|
+
* observers: [{ name: "sync-done", onResult: ... }],
|
|
39
|
+
* } as const satisfies Plugin;
|
|
40
|
+
*
|
|
41
|
+
* Authors who prefer type annotations (`const myObs: Observer = ...`)
|
|
42
|
+
* get widened `name: string`, which collapses `AllObserverNames` to
|
|
43
|
+
* `string` and silently disables typo detection. Use `as const satisfies`
|
|
44
|
+
* to keep the inference.
|
|
45
|
+
*
|
|
46
|
+
* ## Behavior with no observers declared
|
|
47
|
+
*
|
|
48
|
+
* When no plugins contribute observers AND no inline `observers[]` is
|
|
49
|
+
* passed, `AllObserverNames` resolves to `never`, which causes ANY
|
|
50
|
+
* string `observer` reference on a Rule to be a compile error. This is
|
|
51
|
+
* deliberate — fail-closed on unknown observer names. For configs that
|
|
52
|
+
* deliberately reference observers by name without registering them
|
|
53
|
+
* inline (e.g., deferred to runtime), use `satisfies SteeringConfig`
|
|
54
|
+
* as a fallback; you lose typo detection but regain flexibility.
|
|
55
|
+
*
|
|
56
|
+
* ## Hover ergonomics for plugin-predicate JSDoc
|
|
57
|
+
*
|
|
58
|
+
* The `const R extends readonly Rule[]` signature narrows the
|
|
59
|
+
* contextual type of inline rule literals to their `const`-inferred
|
|
60
|
+
* shape, bypassing the homomorphic mapped-type linkage that surfaces
|
|
61
|
+
* source-declared JSDoc on hover (e.g. on `when.isClean:`). Factor
|
|
62
|
+
* rules out into `const myRule = { ... } as const satisfies Rule`
|
|
63
|
+
* bindings before passing them to `defineConfig` to keep the
|
|
64
|
+
* hover-rich shape; see the
|
|
65
|
+
* `examples/dynamic-reason-runtime-cwd/steering.ts` example. The
|
|
66
|
+
* `as const` modifier on the binding (and the `const R` modifier on
|
|
67
|
+
* the signature) preserves each rule's literal `name` so
|
|
68
|
+
* `disabledRules` typo detection fires — the alternatives `: Rule`
|
|
69
|
+
* and bare `satisfies Rule` restore hover but widen the inferred
|
|
70
|
+
* type and collapse typo detection (and `when.happened.event`
|
|
71
|
+
* narrowing across declared `writes`).
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* export default defineConfig({
|
|
75
|
+
* plugins: [gitPlugin],
|
|
76
|
+
* observers: [descriptionReadObserver],
|
|
77
|
+
* rules: [
|
|
78
|
+
* { name: "must-read-docs", ..., observer: "description-read" },
|
|
79
|
+
* ],
|
|
80
|
+
* });
|
|
81
|
+
*/
|
|
82
|
+
export function defineConfig(config) {
|
|
83
|
+
// Runtime work is minimal: copy the supplied config, widening the
|
|
84
|
+
// `readonly` tuple slots back to plain arrays for downstream
|
|
85
|
+
// consumers (loader, evaluator) that don't care about the tuple
|
|
86
|
+
// literal types. The generic machinery's job is done at the call
|
|
87
|
+
// site — once we return, we return plain SteeringConfig.
|
|
88
|
+
const out = {};
|
|
89
|
+
if (config.defaultNoOverride !== undefined) {
|
|
90
|
+
out.defaultNoOverride = config.defaultNoOverride;
|
|
91
|
+
}
|
|
92
|
+
if (config.disabledRules !== undefined) {
|
|
93
|
+
out.disabledRules = [...config.disabledRules];
|
|
94
|
+
}
|
|
95
|
+
if (config.disabledPlugins !== undefined) {
|
|
96
|
+
out.disabledPlugins = [...config.disabledPlugins];
|
|
97
|
+
}
|
|
98
|
+
if (config.disableDefaults !== undefined) {
|
|
99
|
+
out.disableDefaults = config.disableDefaults;
|
|
100
|
+
}
|
|
101
|
+
if (config.failOnWarnings !== undefined) {
|
|
102
|
+
out.failOnWarnings = config.failOnWarnings;
|
|
103
|
+
}
|
|
104
|
+
if (config.plugins !== undefined) {
|
|
105
|
+
// Cast: `readonly Plugin[]` → `Plugin[]` (shape is identical;
|
|
106
|
+
// the loader never mutates the array, but SteeringConfig
|
|
107
|
+
// doesn't require readonly).
|
|
108
|
+
out.plugins = [...config.plugins];
|
|
109
|
+
}
|
|
110
|
+
if (config.rules !== undefined) {
|
|
111
|
+
out.rules = [...config.rules];
|
|
112
|
+
}
|
|
113
|
+
if (config.observers !== undefined) {
|
|
114
|
+
out.observers = [...config.observers];
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=define-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-config.js","sourceRoot":"","sources":["../src/define-config.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,uBAAuB;AA4PvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8EG;AACH,MAAM,UAAU,YAAY,CAO1B,MAAuC;IACvC,kEAAkE;IAClE,6DAA6D;IAC7D,gEAAgE;IAChE,iEAAiE;IACjE,yDAAyD;IACzD,MAAM,GAAG,GAAmB,EAAE,CAAC;IAC/B,IAAI,MAAM,CAAC,iBAAiB,KAAK,SAAS,EAAE,CAAC;QAC3C,GAAG,CAAC,iBAAiB,GAAG,MAAM,CAAC,iBAAiB,CAAC;IACnD,CAAC;IACD,IAAI,MAAM,CAAC,aAAa,KAAK,SAAS,EAAE,CAAC;QACvC,GAAG,CAAC,aAAa,GAAG,CAAC,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC;IAChD,CAAC;IACD,IAAI,MAAM,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;QACzC,GAAG,CAAC,eAAe,GAAG,CAAC,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC;IACpD,CAAC;IACD,IAAI,MAAM,CAAC,eAAe,KAAK,SAAS,EAAE,CAAC;QACzC,GAAG,CAAC,eAAe,GAAG,MAAM,CAAC,eAAe,CAAC;IAC/C,CAAC;IACD,IAAI,MAAM,CAAC,cAAc,KAAK,SAAS,EAAE,CAAC;QACxC,GAAG,CAAC,cAAc,GAAG,MAAM,CAAC,cAAc,CAAC;IAC7C,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACjC,8DAA8D;QAC9D,yDAAyD;QACzD,6BAA6B;QAC7B,GAAG,CAAC,OAAO,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC/B,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,MAAM,CAAC,KAAK,CAAW,CAAC;IAC1C,CAAC;IACD,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,EAAE,CAAC;QACnC,GAAG,CAAC,SAAS,GAAG,CAAC,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `definePredicate<T>` — ~5-LOC helper for declaring typed plugin
|
|
3
|
+
* predicate handlers.
|
|
4
|
+
*
|
|
5
|
+
* Parallels {@link defineConfig} — pure pass-through at runtime, all
|
|
6
|
+
* the value is in the type signature. Narrows the handler's first
|
|
7
|
+
* argument to the supplied type parameter `T` so the body reads like
|
|
8
|
+
* a plain function on typed args, without the author needing to
|
|
9
|
+
* write the annotation twice (once on the declaration, once on the
|
|
10
|
+
* handler's `args` parameter). The `PredicateHandler<T>` return
|
|
11
|
+
* preserves that narrowing when the result is assigned to a local
|
|
12
|
+
* variable; the {@link Plugin.predicates} registry slot then accepts
|
|
13
|
+
* it cast-free via {@link AnyPredicateHandler} (= `PredicateHandler<any>`),
|
|
14
|
+
* which uses TS bivariance to admit typed handlers directly.
|
|
15
|
+
*
|
|
16
|
+
* See ADR §10 for the motivating usage.
|
|
17
|
+
*/
|
|
18
|
+
import type { PredicateHandler } from "./schema.ts";
|
|
19
|
+
/**
|
|
20
|
+
* Sugar for declaring a typed {@link PredicateHandler}. The handler
|
|
21
|
+
* is returned unchanged at runtime; the generic parameter `T`
|
|
22
|
+
* narrows the handler's `args` parameter to the author's intended
|
|
23
|
+
* shape. Return type is `PredicateHandler<T>`, so authors threading
|
|
24
|
+
* the result through local variables keep the narrowed arg type.
|
|
25
|
+
* The registry slot at {@link Plugin.predicates} accepts the result
|
|
26
|
+
* cast-free via {@link AnyPredicateHandler}.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* interface CommitFormatArgs {
|
|
30
|
+
* pattern: RegExp;
|
|
31
|
+
* onUnknown?: "allow" | "block";
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* export const commitFormat = definePredicate<CommitFormatArgs>(
|
|
35
|
+
* (args, ctx) => {
|
|
36
|
+
* // `args` is narrowed to CommitFormatArgs here.
|
|
37
|
+
* const msg = extractCommitMessage(ctx.input.args ?? []);
|
|
38
|
+
* return args.pattern.test(msg);
|
|
39
|
+
* },
|
|
40
|
+
* );
|
|
41
|
+
*
|
|
42
|
+
* // Plug into a plugin:
|
|
43
|
+
* export const gitPlugin: Plugin = {
|
|
44
|
+
* name: "git",
|
|
45
|
+
* predicates: { commitFormat },
|
|
46
|
+
* };
|
|
47
|
+
*/
|
|
48
|
+
export declare function definePredicate<T>(handler: PredicateHandler<T>): PredicateHandler<T>;
|
|
49
|
+
//# sourceMappingURL=define-predicate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-predicate.d.ts","sourceRoot":"","sources":["../src/define-predicate.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAC/B,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAC,GAC3B,gBAAgB,CAAC,CAAC,CAAC,CAErB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering.
|
|
3
|
+
/**
|
|
4
|
+
* Sugar for declaring a typed {@link PredicateHandler}. The handler
|
|
5
|
+
* is returned unchanged at runtime; the generic parameter `T`
|
|
6
|
+
* narrows the handler's `args` parameter to the author's intended
|
|
7
|
+
* shape. Return type is `PredicateHandler<T>`, so authors threading
|
|
8
|
+
* the result through local variables keep the narrowed arg type.
|
|
9
|
+
* The registry slot at {@link Plugin.predicates} accepts the result
|
|
10
|
+
* cast-free via {@link AnyPredicateHandler}.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* interface CommitFormatArgs {
|
|
14
|
+
* pattern: RegExp;
|
|
15
|
+
* onUnknown?: "allow" | "block";
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* export const commitFormat = definePredicate<CommitFormatArgs>(
|
|
19
|
+
* (args, ctx) => {
|
|
20
|
+
* // `args` is narrowed to CommitFormatArgs here.
|
|
21
|
+
* const msg = extractCommitMessage(ctx.input.args ?? []);
|
|
22
|
+
* return args.pattern.test(msg);
|
|
23
|
+
* },
|
|
24
|
+
* );
|
|
25
|
+
*
|
|
26
|
+
* // Plug into a plugin:
|
|
27
|
+
* export const gitPlugin: Plugin = {
|
|
28
|
+
* name: "git",
|
|
29
|
+
* predicates: { commitFormat },
|
|
30
|
+
* };
|
|
31
|
+
*/
|
|
32
|
+
export function definePredicate(handler) {
|
|
33
|
+
return handler;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=define-predicate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-predicate.js","sourceRoot":"","sources":["../src/define-predicate.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,uBAAuB;AAsBvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,eAAe,CAC7B,OAA4B;IAE5B,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Predicate context construction for the v2 evaluator.
|
|
3
|
+
*
|
|
4
|
+
* Two concerns live here because they collaborate tightly:
|
|
5
|
+
*
|
|
6
|
+
* 1. `createExecCache` — memoizes `exec(cmd, args, opts)` by
|
|
7
|
+
* `(cmd, args, cwd)` so every rule evaluated for ONE tool_call
|
|
8
|
+
* sees the same result for the same query, without re-running the
|
|
9
|
+
* underlying child process. A fresh cache is created per
|
|
10
|
+
* tool_call; cross-call results are never shared.
|
|
11
|
+
* 2. `createFindEntries` — wraps pi's `sessionManager.getEntries()`
|
|
12
|
+
* into the {@link PredicateContext.findEntries} shape, filtering
|
|
13
|
+
* to `type: "custom"` entries by `customType` and flattening to
|
|
14
|
+
* `{ data, timestamp }` (timestamps normalized from ISO strings to
|
|
15
|
+
* epoch ms, matching what observers producing entries can rely on).
|
|
16
|
+
*
|
|
17
|
+
* The evaluator itself assembles the final {@link PredicateContext}
|
|
18
|
+
* from these closures plus per-candidate fields (cwd / tool / input /
|
|
19
|
+
* agentLoopIndex) as an object literal — no helper needed once the shape
|
|
20
|
+
* is shared across bash and write/edit code paths.
|
|
21
|
+
*
|
|
22
|
+
* Kept internal (under `evaluator-internals/`) so consumers can swap
|
|
23
|
+
* the evaluator without inheriting its helper surface. The only
|
|
24
|
+
* re-export is through `../evaluator.ts`.
|
|
25
|
+
*/
|
|
26
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
import type { ExecOpts, PredicateContext } from "../schema.ts";
|
|
28
|
+
/**
|
|
29
|
+
* Narrow host surface the evaluator needs from the pi runtime. Lets
|
|
30
|
+
* tests pass a stub without building a full fake `ExtensionAPI`, and
|
|
31
|
+
* keeps the evaluator decoupled from the unrelated parts of pi's API
|
|
32
|
+
* (tool registration, slash commands, OAuth, …).
|
|
33
|
+
*/
|
|
34
|
+
export interface EvaluatorHost {
|
|
35
|
+
/** See {@link ExtensionAPI.exec}. */
|
|
36
|
+
exec: ExtensionAPI["exec"];
|
|
37
|
+
/** See {@link ExtensionAPI.appendEntry}. */
|
|
38
|
+
appendEntry: ExtensionAPI["appendEntry"];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a tool_call-scoped exec function that memoizes by
|
|
42
|
+
* `(cmd, args, cwd)`. Caches only the PROMISE so concurrent
|
|
43
|
+
* predicate evaluations that hit the same key await the same
|
|
44
|
+
* in-flight child process — not N redundant ones.
|
|
45
|
+
*
|
|
46
|
+
* `opts.cwd` defaults to `sessionCwd` (mirroring how predicates see
|
|
47
|
+
* their "current cwd" via {@link PredicateContext.cwd}). `timeoutMs`
|
|
48
|
+
* is forwarded as `timeout`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function createExecCache(host: EvaluatorHost, sessionCwd: string): PredicateContext["exec"];
|
|
51
|
+
/**
|
|
52
|
+
* Key under which the engine auto-injects the current `agentLoopIndex`
|
|
53
|
+
* into every entry written via `PredicateContext.appendEntry` or
|
|
54
|
+
* `ObserverContext.appendEntry`. Rules using
|
|
55
|
+
* `when.happened: { in: "agent_loop" }` filter session entries by
|
|
56
|
+
* comparing this key against `ctx.agentLoopIndex`.
|
|
57
|
+
*
|
|
58
|
+
* Part of the on-disk session-JSONL format, exposed as a public
|
|
59
|
+
* module-level constant. Re-exported from the package root
|
|
60
|
+
* so plugin authors who manually inspect entries via
|
|
61
|
+
* `findEntries` can import the constant by name rather than
|
|
62
|
+
* hardcoding the string — a future rename would then break at
|
|
63
|
+
* import time instead of silently producing un-filtered entries.
|
|
64
|
+
*/
|
|
65
|
+
export declare const AGENT_LOOP_INDEX_KEY: "_agentLoopIndex";
|
|
66
|
+
/**
|
|
67
|
+
* Narrow "is plain object" guard used to distinguish payloads that
|
|
68
|
+
* are safe to merge into (spread) from payloads that must be wrapped
|
|
69
|
+
* as `{ value, _agentLoopIndex }`.
|
|
70
|
+
*
|
|
71
|
+
* Anything that is NOT a plain object (arrays, Date, Map, Set, Error,
|
|
72
|
+
* class instances, functions, null, undefined, primitives) falls into
|
|
73
|
+
* the wrap branch. Direct `{...}` or `Object.create(null)` shapes fall
|
|
74
|
+
* into the merge branch.
|
|
75
|
+
*
|
|
76
|
+
* Two-stage detection:
|
|
77
|
+
* 1. `Object.prototype.toString.call(x)` returns `"[object Object]"`
|
|
78
|
+
* only for plain objects and for class instances of user-defined
|
|
79
|
+
* classes. It correctly excludes arrays, Date, Map, Set, Error,
|
|
80
|
+
* etc.
|
|
81
|
+
* 2. A prototype check then rejects user-class instances: plain
|
|
82
|
+
* objects have `Object.prototype` (or `null` for
|
|
83
|
+
* `Object.create(null)`) as their prototype; `new Box(...)` has
|
|
84
|
+
* `Box.prototype`, which is neither.
|
|
85
|
+
*
|
|
86
|
+
* The explicit `Array.isArray` check is belt-and-suspenders — some
|
|
87
|
+
* runtimes have historically misreported arrays via `toString`, and
|
|
88
|
+
* the array case is the one most likely to hit this wrapper (an
|
|
89
|
+
* observer appending a list of watched paths). Cheap to check twice.
|
|
90
|
+
*/
|
|
91
|
+
export declare function isPlainObject(x: unknown): x is Record<string, unknown>;
|
|
92
|
+
/**
|
|
93
|
+
* Wrap a raw `host.appendEntry` so every write auto-injects the
|
|
94
|
+
* current `agentLoopIndex` into the payload. Plain-object payloads
|
|
95
|
+
* get the field merged in; everything else — primitives, arrays,
|
|
96
|
+
* `Date`, `Map`, `Set`, `Error`, class instances, functions, null,
|
|
97
|
+
* undefined — is wrapped as `{ value, _agentLoopIndex }` so
|
|
98
|
+
* downstream consumers always see a consistent object shape.
|
|
99
|
+
*
|
|
100
|
+
* The "everything else" branch exists because the naive spread
|
|
101
|
+
* (`{ ...data, ... }`) silently corrupts non-plain objects: arrays
|
|
102
|
+
* become pseudo-objects with string-indexed keys, Date / Map / Set /
|
|
103
|
+
* Error instances lose their internal state entirely, etc. Wrapping
|
|
104
|
+
* under `value` preserves the original reference unchanged.
|
|
105
|
+
*
|
|
106
|
+
* The returned closure matches both {@link PredicateContext.appendEntry}
|
|
107
|
+
* and {@link ObserverContext.appendEntry} so the evaluator and the
|
|
108
|
+
* observer dispatcher share one wrapper.
|
|
109
|
+
*
|
|
110
|
+
* `findEntriesCache` (optional) is a cache map shared with a sibling
|
|
111
|
+
* {@link createFindEntries} closure. When supplied, every `appendEntry`
|
|
112
|
+
* call invalidates the cache entry for the written `customType` so
|
|
113
|
+
* the next `findEntries(customType)` re-reads the session JSONL and
|
|
114
|
+
* sees the newly-written entry (S2/E1; see ../INVARIANTS.md for the
|
|
115
|
+
* S/E tag glossary). Omit the parameter to keep
|
|
116
|
+
* the pre-S2 behaviour (no invalidation) — handy for tests or callers
|
|
117
|
+
* that don't pair the two closures.
|
|
118
|
+
*/
|
|
119
|
+
export declare function createAppendEntry(host: EvaluatorHost, agentLoopIndex: number, findEntriesCache?: Map<string, Array<{
|
|
120
|
+
data: unknown;
|
|
121
|
+
timestamp: number;
|
|
122
|
+
}>>): PredicateContext["appendEntry"];
|
|
123
|
+
/**
|
|
124
|
+
* Adapt pi's `sessionManager.getEntries()` into the typed-and-filtered
|
|
125
|
+
* view predicates (and observers) expect.
|
|
126
|
+
*
|
|
127
|
+
* Strategy:
|
|
128
|
+
* - pick only `type: "custom"` entries (the shape `pi.appendEntry`
|
|
129
|
+
* produces — see `CustomEntry` in pi's session-manager),
|
|
130
|
+
* - filter by `customType`,
|
|
131
|
+
* - project to `{ data, timestamp }` where `timestamp` is epoch-ms
|
|
132
|
+
* (parsed from the entry's ISO string). Epoch-ms is what turn-state
|
|
133
|
+
* checks want for chronological comparisons without having to
|
|
134
|
+
* re-parse.
|
|
135
|
+
*
|
|
136
|
+
* Results are memoized PER invocation of `createFindEntries` by
|
|
137
|
+
* customType. The evaluator rebuilds the closure on every tool_call;
|
|
138
|
+
* the observer dispatcher rebuilds on every tool_result. So each phase
|
|
139
|
+
* sees a consistent snapshot across reads.
|
|
140
|
+
*
|
|
141
|
+
* Cross-rule write visibility (S2/E1): when the same phase also uses
|
|
142
|
+
* a paired {@link createAppendEntry} with the SAME cache map, a write
|
|
143
|
+
* during rule A's `onFire` invalidates the cached read for that
|
|
144
|
+
* customType so rule B's `when.happened` predicate sees the fresh
|
|
145
|
+
* entry. Callers that want this consistency pass in a shared cache
|
|
146
|
+
* via the optional `cache` parameter; callers that omit it get the
|
|
147
|
+
* old per-closure snapshot behaviour (pre-S2), which is sound only
|
|
148
|
+
* when the closure never interleaves reads with writes.
|
|
149
|
+
*
|
|
150
|
+
* The `ctx` argument is the pi `ExtensionContext` — we re-read
|
|
151
|
+
* `getEntries()` only on a cache miss. Cache keys are per-closure (or
|
|
152
|
+
* per shared cache) so cross-tool_call or cross-tool_result reads
|
|
153
|
+
* always see the freshest state (a new closure = a new cache).
|
|
154
|
+
*/
|
|
155
|
+
export declare function createFindEntries(ctx: ExtensionContext, cache?: Map<string, Array<{
|
|
156
|
+
data: unknown;
|
|
157
|
+
timestamp: number;
|
|
158
|
+
}>>): PredicateContext["findEntries"];
|
|
159
|
+
/**
|
|
160
|
+
* Allocate a fresh session-entry cache shared between a paired
|
|
161
|
+
* {@link createFindEntries} + {@link createAppendEntry} for the same
|
|
162
|
+
* tool_call (evaluator) or tool_result (observer dispatcher) phase.
|
|
163
|
+
*
|
|
164
|
+
* Using a shared cache gives two guarantees the evaluator + dispatcher
|
|
165
|
+
* rely on:
|
|
166
|
+
*
|
|
167
|
+
* 1. Consistent reads: N calls to `findEntries(type)` within one
|
|
168
|
+
* phase materialize the entry list ONCE per type.
|
|
169
|
+
* 2. Write-through-reads (S2/E1): a write via the paired
|
|
170
|
+
* `appendEntry` invalidates that type's cached list, so the next
|
|
171
|
+
* read re-scans the session JSONL and observes the write. Without
|
|
172
|
+
* this, a rule's `onFire` appending X followed by a later rule's
|
|
173
|
+
* `when.happened: { event: X }` would read a stale pre-write
|
|
174
|
+
* snapshot.
|
|
175
|
+
*
|
|
176
|
+
* Consumers who don't need write-through-reads (tests, one-shot
|
|
177
|
+
* `findEntries` calls) can pass a fresh cache or omit the parameter
|
|
178
|
+
* on both constructors — the closures then each get their own cache
|
|
179
|
+
* map and behave like the pre-S2 implementation.
|
|
180
|
+
*/
|
|
181
|
+
export declare function createSessionEntryCache(): Map<string, Array<{
|
|
182
|
+
data: unknown;
|
|
183
|
+
timestamp: number;
|
|
184
|
+
}>>;
|
|
185
|
+
export type { ExecOpts };
|
|
186
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/evaluator-internals/context.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAGjB,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EAAE,QAAQ,EAAc,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAE3E;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,qCAAqC;IACrC,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IAC3B,4CAA4C;IAC5C,WAAW,EAAE,YAAY,CAAC,aAAa,CAAC,CAAC;CAC1C;AAyBD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,aAAa,EACnB,UAAU,EAAE,MAAM,GACjB,gBAAgB,CAAC,MAAM,CAAC,CAa1B;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,EAAG,iBAA0B,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAOtE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,aAAa,EACnB,cAAc,EAAE,MAAM,EACtB,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,GAC1E,gBAAgB,CAAC,aAAa,CAAC,CAajC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,gBAAgB,EACrB,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,GAC/D,gBAAgB,CAAC,aAAa,CAAC,CAwBjC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,IAAI,GAAG,CAC5C,MAAM,EACN,KAAK,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAC5C,CAEA;AAID,YAAY,EAAE,QAAQ,EAAE,CAAC"}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Part of pi-steering.
|
|
3
|
+
/**
|
|
4
|
+
* Key used by the per-tool-call exec cache. Null-byte separator is safe
|
|
5
|
+
* because neither a command path nor POSIX argv can legitimately contain
|
|
6
|
+
* a NUL byte; collisions are impossible in practice.
|
|
7
|
+
*/
|
|
8
|
+
function execCacheKey(cmd, args, cwd) {
|
|
9
|
+
return `${cmd}\x00${args.join("\x00")}\x00${cwd}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Bridge pi's `ExecResult` (uses `code`) to the schema's `ExecResult`
|
|
13
|
+
* (uses `exitCode`). Dropping `killed` is intentional — predicate
|
|
14
|
+
* authors don't need to distinguish "timed out" from "exited
|
|
15
|
+
* non-zero"; both surface as a non-zero exit for guardrail purposes.
|
|
16
|
+
*/
|
|
17
|
+
function toSchemaExecResult(r) {
|
|
18
|
+
return { stdout: r.stdout, stderr: r.stderr, exitCode: r.code };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a tool_call-scoped exec function that memoizes by
|
|
22
|
+
* `(cmd, args, cwd)`. Caches only the PROMISE so concurrent
|
|
23
|
+
* predicate evaluations that hit the same key await the same
|
|
24
|
+
* in-flight child process — not N redundant ones.
|
|
25
|
+
*
|
|
26
|
+
* `opts.cwd` defaults to `sessionCwd` (mirroring how predicates see
|
|
27
|
+
* their "current cwd" via {@link PredicateContext.cwd}). `timeoutMs`
|
|
28
|
+
* is forwarded as `timeout`.
|
|
29
|
+
*/
|
|
30
|
+
export function createExecCache(host, sessionCwd) {
|
|
31
|
+
const cache = new Map();
|
|
32
|
+
return (cmd, args, opts) => {
|
|
33
|
+
const cwd = opts?.cwd ?? sessionCwd;
|
|
34
|
+
const key = execCacheKey(cmd, args, cwd);
|
|
35
|
+
const hit = cache.get(key);
|
|
36
|
+
if (hit !== undefined)
|
|
37
|
+
return hit;
|
|
38
|
+
const piOpts = { cwd };
|
|
39
|
+
if (opts?.timeoutMs !== undefined)
|
|
40
|
+
piOpts.timeout = opts.timeoutMs;
|
|
41
|
+
const p = host.exec(cmd, args, piOpts).then(toSchemaExecResult);
|
|
42
|
+
cache.set(key, p);
|
|
43
|
+
return p;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Key under which the engine auto-injects the current `agentLoopIndex`
|
|
48
|
+
* into every entry written via `PredicateContext.appendEntry` or
|
|
49
|
+
* `ObserverContext.appendEntry`. Rules using
|
|
50
|
+
* `when.happened: { in: "agent_loop" }` filter session entries by
|
|
51
|
+
* comparing this key against `ctx.agentLoopIndex`.
|
|
52
|
+
*
|
|
53
|
+
* Part of the on-disk session-JSONL format, exposed as a public
|
|
54
|
+
* module-level constant. Re-exported from the package root
|
|
55
|
+
* so plugin authors who manually inspect entries via
|
|
56
|
+
* `findEntries` can import the constant by name rather than
|
|
57
|
+
* hardcoding the string — a future rename would then break at
|
|
58
|
+
* import time instead of silently producing un-filtered entries.
|
|
59
|
+
*/
|
|
60
|
+
export const AGENT_LOOP_INDEX_KEY = "_agentLoopIndex";
|
|
61
|
+
/**
|
|
62
|
+
* Narrow "is plain object" guard used to distinguish payloads that
|
|
63
|
+
* are safe to merge into (spread) from payloads that must be wrapped
|
|
64
|
+
* as `{ value, _agentLoopIndex }`.
|
|
65
|
+
*
|
|
66
|
+
* Anything that is NOT a plain object (arrays, Date, Map, Set, Error,
|
|
67
|
+
* class instances, functions, null, undefined, primitives) falls into
|
|
68
|
+
* the wrap branch. Direct `{...}` or `Object.create(null)` shapes fall
|
|
69
|
+
* into the merge branch.
|
|
70
|
+
*
|
|
71
|
+
* Two-stage detection:
|
|
72
|
+
* 1. `Object.prototype.toString.call(x)` returns `"[object Object]"`
|
|
73
|
+
* only for plain objects and for class instances of user-defined
|
|
74
|
+
* classes. It correctly excludes arrays, Date, Map, Set, Error,
|
|
75
|
+
* etc.
|
|
76
|
+
* 2. A prototype check then rejects user-class instances: plain
|
|
77
|
+
* objects have `Object.prototype` (or `null` for
|
|
78
|
+
* `Object.create(null)`) as their prototype; `new Box(...)` has
|
|
79
|
+
* `Box.prototype`, which is neither.
|
|
80
|
+
*
|
|
81
|
+
* The explicit `Array.isArray` check is belt-and-suspenders — some
|
|
82
|
+
* runtimes have historically misreported arrays via `toString`, and
|
|
83
|
+
* the array case is the one most likely to hit this wrapper (an
|
|
84
|
+
* observer appending a list of watched paths). Cheap to check twice.
|
|
85
|
+
*/
|
|
86
|
+
export function isPlainObject(x) {
|
|
87
|
+
if (x === null || typeof x !== "object")
|
|
88
|
+
return false;
|
|
89
|
+
if (Array.isArray(x))
|
|
90
|
+
return false;
|
|
91
|
+
if (Object.prototype.toString.call(x) !== "[object Object]")
|
|
92
|
+
return false;
|
|
93
|
+
const proto = Object.getPrototypeOf(x);
|
|
94
|
+
if (proto !== null && proto !== Object.prototype)
|
|
95
|
+
return false;
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Wrap a raw `host.appendEntry` so every write auto-injects the
|
|
100
|
+
* current `agentLoopIndex` into the payload. Plain-object payloads
|
|
101
|
+
* get the field merged in; everything else — primitives, arrays,
|
|
102
|
+
* `Date`, `Map`, `Set`, `Error`, class instances, functions, null,
|
|
103
|
+
* undefined — is wrapped as `{ value, _agentLoopIndex }` so
|
|
104
|
+
* downstream consumers always see a consistent object shape.
|
|
105
|
+
*
|
|
106
|
+
* The "everything else" branch exists because the naive spread
|
|
107
|
+
* (`{ ...data, ... }`) silently corrupts non-plain objects: arrays
|
|
108
|
+
* become pseudo-objects with string-indexed keys, Date / Map / Set /
|
|
109
|
+
* Error instances lose their internal state entirely, etc. Wrapping
|
|
110
|
+
* under `value` preserves the original reference unchanged.
|
|
111
|
+
*
|
|
112
|
+
* The returned closure matches both {@link PredicateContext.appendEntry}
|
|
113
|
+
* and {@link ObserverContext.appendEntry} so the evaluator and the
|
|
114
|
+
* observer dispatcher share one wrapper.
|
|
115
|
+
*
|
|
116
|
+
* `findEntriesCache` (optional) is a cache map shared with a sibling
|
|
117
|
+
* {@link createFindEntries} closure. When supplied, every `appendEntry`
|
|
118
|
+
* call invalidates the cache entry for the written `customType` so
|
|
119
|
+
* the next `findEntries(customType)` re-reads the session JSONL and
|
|
120
|
+
* sees the newly-written entry (S2/E1; see ../INVARIANTS.md for the
|
|
121
|
+
* S/E tag glossary). Omit the parameter to keep
|
|
122
|
+
* the pre-S2 behaviour (no invalidation) — handy for tests or callers
|
|
123
|
+
* that don't pair the two closures.
|
|
124
|
+
*/
|
|
125
|
+
export function createAppendEntry(host, agentLoopIndex, findEntriesCache) {
|
|
126
|
+
return (customType, data) => {
|
|
127
|
+
const tagged = isPlainObject(data)
|
|
128
|
+
? { ...data, [AGENT_LOOP_INDEX_KEY]: agentLoopIndex }
|
|
129
|
+
: { value: data, [AGENT_LOOP_INDEX_KEY]: agentLoopIndex };
|
|
130
|
+
host.appendEntry(customType, tagged);
|
|
131
|
+
// S2/E1: drop the cached read for this customType so a later
|
|
132
|
+
// `findEntries(customType)` call from the same phase re-materializes
|
|
133
|
+
// the list and sees the write we just made. Without this, a rule's
|
|
134
|
+
// `onFire` that writes + a later rule's `when.happened` that reads
|
|
135
|
+
// see inconsistent snapshots within one tool_call.
|
|
136
|
+
findEntriesCache?.delete(customType);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Adapt pi's `sessionManager.getEntries()` into the typed-and-filtered
|
|
141
|
+
* view predicates (and observers) expect.
|
|
142
|
+
*
|
|
143
|
+
* Strategy:
|
|
144
|
+
* - pick only `type: "custom"` entries (the shape `pi.appendEntry`
|
|
145
|
+
* produces — see `CustomEntry` in pi's session-manager),
|
|
146
|
+
* - filter by `customType`,
|
|
147
|
+
* - project to `{ data, timestamp }` where `timestamp` is epoch-ms
|
|
148
|
+
* (parsed from the entry's ISO string). Epoch-ms is what turn-state
|
|
149
|
+
* checks want for chronological comparisons without having to
|
|
150
|
+
* re-parse.
|
|
151
|
+
*
|
|
152
|
+
* Results are memoized PER invocation of `createFindEntries` by
|
|
153
|
+
* customType. The evaluator rebuilds the closure on every tool_call;
|
|
154
|
+
* the observer dispatcher rebuilds on every tool_result. So each phase
|
|
155
|
+
* sees a consistent snapshot across reads.
|
|
156
|
+
*
|
|
157
|
+
* Cross-rule write visibility (S2/E1): when the same phase also uses
|
|
158
|
+
* a paired {@link createAppendEntry} with the SAME cache map, a write
|
|
159
|
+
* during rule A's `onFire` invalidates the cached read for that
|
|
160
|
+
* customType so rule B's `when.happened` predicate sees the fresh
|
|
161
|
+
* entry. Callers that want this consistency pass in a shared cache
|
|
162
|
+
* via the optional `cache` parameter; callers that omit it get the
|
|
163
|
+
* old per-closure snapshot behaviour (pre-S2), which is sound only
|
|
164
|
+
* when the closure never interleaves reads with writes.
|
|
165
|
+
*
|
|
166
|
+
* The `ctx` argument is the pi `ExtensionContext` — we re-read
|
|
167
|
+
* `getEntries()` only on a cache miss. Cache keys are per-closure (or
|
|
168
|
+
* per shared cache) so cross-tool_call or cross-tool_result reads
|
|
169
|
+
* always see the freshest state (a new closure = a new cache).
|
|
170
|
+
*/
|
|
171
|
+
export function createFindEntries(ctx, cache) {
|
|
172
|
+
const entryCache = cache ?? new Map();
|
|
173
|
+
return (customType) => {
|
|
174
|
+
const hit = entryCache.get(customType);
|
|
175
|
+
if (hit !== undefined) {
|
|
176
|
+
return hit;
|
|
177
|
+
}
|
|
178
|
+
const out = [];
|
|
179
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
180
|
+
if (entry.type !== "custom")
|
|
181
|
+
continue;
|
|
182
|
+
if (entry.customType !== customType)
|
|
183
|
+
continue;
|
|
184
|
+
const ts = Date.parse(entry.timestamp);
|
|
185
|
+
out.push({
|
|
186
|
+
data: entry.data,
|
|
187
|
+
timestamp: Number.isNaN(ts) ? 0 : ts,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
entryCache.set(customType, out);
|
|
191
|
+
return out;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Allocate a fresh session-entry cache shared between a paired
|
|
196
|
+
* {@link createFindEntries} + {@link createAppendEntry} for the same
|
|
197
|
+
* tool_call (evaluator) or tool_result (observer dispatcher) phase.
|
|
198
|
+
*
|
|
199
|
+
* Using a shared cache gives two guarantees the evaluator + dispatcher
|
|
200
|
+
* rely on:
|
|
201
|
+
*
|
|
202
|
+
* 1. Consistent reads: N calls to `findEntries(type)` within one
|
|
203
|
+
* phase materialize the entry list ONCE per type.
|
|
204
|
+
* 2. Write-through-reads (S2/E1): a write via the paired
|
|
205
|
+
* `appendEntry` invalidates that type's cached list, so the next
|
|
206
|
+
* read re-scans the session JSONL and observes the write. Without
|
|
207
|
+
* this, a rule's `onFire` appending X followed by a later rule's
|
|
208
|
+
* `when.happened: { event: X }` would read a stale pre-write
|
|
209
|
+
* snapshot.
|
|
210
|
+
*
|
|
211
|
+
* Consumers who don't need write-through-reads (tests, one-shot
|
|
212
|
+
* `findEntries` calls) can pass a fresh cache or omit the parameter
|
|
213
|
+
* on both constructors — the closures then each get their own cache
|
|
214
|
+
* map and behave like the pre-S2 implementation.
|
|
215
|
+
*/
|
|
216
|
+
export function createSessionEntryCache() {
|
|
217
|
+
return new Map();
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/evaluator-internals/context.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract an inline override-comment's reason that targets a specific
|
|
3
|
+
* rule name. Returns the trimmed reason, or null when no override
|
|
4
|
+
* addressed to `ruleName` is present (empty reasons are treated as
|
|
5
|
+
* "no override" — users must supply an explicit justification).
|
|
6
|
+
*
|
|
7
|
+
* Behaviour parity with v1 `extractOverride`:
|
|
8
|
+
*
|
|
9
|
+
* - case-sensitive rule-name match,
|
|
10
|
+
* - supports stacked overrides on one line (e.g.
|
|
11
|
+
* `cmd # steering-override: a — r1 # steering-override: b — r2`),
|
|
12
|
+
* - first-match-wins across the scanned comments,
|
|
13
|
+
* - all leader styles from v1 accepted.
|
|
14
|
+
*
|
|
15
|
+
* The v1 implementation is the authoritative reference for edge cases
|
|
16
|
+
* — see `evaluator.ts` in this package's v1 tree.
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractOverride(text: string, ruleName: string): string | null;
|
|
19
|
+
//# sourceMappingURL=override.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"override.d.ts","sourceRoot":"","sources":["../../src/evaluator-internals/override.ts"],"names":[],"mappings":"AAoCA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6B7E"}
|