@codyswann/lisa 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +28 -12
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/expo/copy-overwrite/scripts/bdd/baseline.mjs +211 -121
  5. package/expo/copy-overwrite/scripts/bdd/contract.mjs +10 -2
  6. package/expo/copy-overwrite/scripts/bdd/envelope.mjs +3 -2
  7. package/expo/copy-overwrite/scripts/bdd/render.mjs +2 -2
  8. package/expo/copy-overwrite/scripts/check-bdd-coverage.mjs +45 -8
  9. package/expo/copy-overwrite/scripts/classify-maestro-failures.mjs +775 -0
  10. package/expo/create-only/.maestro/flake-classification.json +21 -0
  11. package/expo/create-only/bdd/coverage-map.json +1 -2
  12. package/expo/package-lisa/package.lisa.json +1 -0
  13. package/package.json +1 -1
  14. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa/hooks/threshold-ratchet-families.mjs +24 -0
  17. package/plugins/lisa/rules/eager/credential-substrate-precedence.md +52 -0
  18. package/plugins/lisa/rules/reference/bdd-e2e-coverage.md +19 -8
  19. package/plugins/lisa-agy/plugin.json +1 -1
  20. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  22. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  23. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +24 -0
  27. package/plugins/lisa-copilot/rules/eager/credential-substrate-precedence.md +52 -0
  28. package/plugins/lisa-copilot/rules/reference/bdd-e2e-coverage.md +19 -8
  29. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +24 -0
  31. package/plugins/lisa-cursor/rules/bdd-e2e-coverage-reference.mdc +19 -8
  32. package/plugins/lisa-cursor/rules/credential-substrate-precedence.mdc +57 -0
  33. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo-agy/plugin.json +1 -1
  36. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  51. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  56. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails-agy/plugin.json +1 -1
  61. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  62. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  66. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  71. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  73. package/plugins/src/base/hooks/threshold-ratchet-families.mjs +24 -0
  74. package/plugins/src/base/rules/eager/credential-substrate-precedence.md +52 -0
  75. package/plugins/src/base/rules/reference/bdd-e2e-coverage.md +19 -8
  76. package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +24 -0
  77. package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +24 -0
@@ -0,0 +1,21 @@
1
+ {
2
+ "_comment": "Configuration for `bun run maestro:classify` (scripts/classify-maestro-failures.mjs) — a DIAGNOSTIC that scores a red Maestro run before anybody attributes a regression to it. It gates nothing and cannot change any check's conclusion. This file is create-only: Lisa writes it once and never overwrites your tuned values.",
3
+
4
+ "_signInMarkers": "Selectors that identify a subflow as performing sign-in. Preamble identity is DERIVED from these rather than from a list of filenames, so the classifier cannot drift when a gate is edited. Replace the shipped values with the selectors YOUR sign-in flow uses. Leaving them unmatched is safe: nothing qualifies as a preamble and every failure reads as a product failure, which is the direction this tool is required to err in — a false 'product' costs an investigation, a false 'preamble' hides a real regression.",
5
+ "signInMarkers": ["landing:sign-in", "signin:email-input"],
6
+
7
+ "_knownIntermittent": "Flows that fail SOMETIMES on an unchanged build. Every entry MUST carry a `measured` block — `failures`/`runs` from a real repeated run, the ISO date it was measured, and the methodology behind it. An entry missing any of those is reported as a registry defect and annotates nothing, on purpose: an unmeasured 'known flake' entry is exactly how a real regression gets dismissed. Keep the narrative — what was ruled out, the local repro recipe — in your BDD README and point at it from `notes`; this file holds only the claim a machine can check. Remeasure and update `measuredAt` rather than letting a stale rate justify today's failure.",
8
+ "_exampleEntry": {
9
+ "flow": "saved-insight-save-and-unsave.yaml",
10
+ "platforms": ["android"],
11
+ "measured": {
12
+ "failures": 2,
13
+ "runs": 7,
14
+ "measuredAt": "2026-08-10",
15
+ "method": "seven local runs on one emulator (1080x2400 @ 420dpi, matching the CI AVD pins) against build 0.0.327, arm-alternated against 0.0.319 to rule out the suspected commit"
16
+ },
17
+ "ticket": "TUN-560",
18
+ "notes": "bdd/README.md#known-intermittent-flows"
19
+ },
20
+ "knownIntermittent": []
21
+ }
@@ -29,12 +29,11 @@
29
29
  }
30
30
  },
31
31
  "coverageFloor": {
32
- "_comment": "Committed traceability floor per platform. A ratchet: may rise, may never fall. Seeded at 0 so adopting on a brownfield app never red-gates CI before any scenario exists. Lowering one requires a coverageFloorBaseline record naming the exact change AND the maintainer-applied `bdd-floor-baseline` pull-request label.",
32
+ "_comment": "Committed traceability floor per platform: an ABSOLUTE BAR answering `is this platform below it right now`, evaluated only in enforced mode. It is NOT a ratchet — set it once at adoption to the honest measured number (or leave it at 0) and stop touching it. Coverage already earned is protected separately and deterministically: an obligation covered at the base revision may not stop being covered, and new behavior arrives mapped or waived. Set a floor for EVERY platform above before flipping adoption.state to enforced.",
33
33
  "web": 0,
34
34
  "ios": 0,
35
35
  "android": 0
36
36
  },
37
- "coverageFloorBaseline": [],
38
37
  "retirements": [],
39
38
  "trackers": {
40
39
  "_comment": "Tracker-tag grammar is portfolio-wide; the allowed keys and repos are per-repo. Key style: @TUN-123, @SE-6833. Repo-issue style: @gh-2394 (this repo) or @gh-wiki-124 (a sibling repo). Tags are validated for syntax and membership only — the gate never contacts a tracker, so an unreachable issue can never block a merge.",
@@ -11,6 +11,7 @@
11
11
  "maestro:test:android": "maestro test -p android .maestro/flows",
12
12
  "maestro:test:smoke": "maestro test -p ios --include-tags=smoke .maestro/flows",
13
13
  "maestro:studio": "maestro studio",
14
+ "maestro:classify": "node scripts/classify-maestro-failures.mjs",
14
15
  "bdd:coverage": "node scripts/check-bdd-coverage.mjs",
15
16
  "bdd:coverage:write": "node scripts/check-bdd-coverage.mjs --write",
16
17
  "bdd:matrix": "node scripts/bdd-matrix.mjs --write",
package/package.json CHANGED
@@ -120,7 +120,7 @@
120
120
  }
121
121
  },
122
122
  "name": "@codyswann/lisa",
123
- "version": "3.1.0",
123
+ "version": "3.2.0",
124
124
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
125
125
  "main": "dist/index.js",
126
126
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -10,6 +10,30 @@
10
10
  * File families the ratchet watches. `kind` selects the extractor;
11
11
  * `direction` applies to numeric-leaf kinds ("min" values may only rise,
12
12
  * "max" values may only fall).
13
+ *
14
+ * PER-FAMILY POLICY, reviewed 2026-08-12. A family keeps its ratchet unless a
15
+ * NAMED deterministic non-regression invariant replaces the property the
16
+ * ratchet was providing — deleting a ratchet deletes that property with it, so
17
+ * "this generates churn" is a reason to find a replacement, never a reason to
18
+ * delete on its own:
19
+ *
20
+ * coverage / simplecov / e2e KEPT. Here the ratchet IS the non-regression
21
+ * invariant, and no per-item equivalent exists short of committing a
22
+ * per-file coverage baseline — an artifact that only ever accumulates.
23
+ * Revisit if one appears.
24
+ * stryker KEPT. `thresholds.break` is an absolute floor,
25
+ * kept outright; the mutate-list comparison detects EXEMPTION ADDITIONS
26
+ * rather than creep, and has no number for a pull request to nudge.
27
+ * eslint / rubocop / k6 / lisa-config KEPT, unchanged.
28
+ *
29
+ * Exactly one family was replaced, and it is not watched here and never was:
30
+ * the BDD traceability floor, whose numeric ratchet gave way to per-obligation
31
+ * checks. See `expo/copy-overwrite/scripts/bdd/baseline.mjs`.
32
+ *
33
+ * Retiring a family is SEQUENCED: the replacement invariant lands, then a
34
+ * `thresholdRatchet.allow` entry merges from the base side, then the mechanism
35
+ * changes. Never the reverse — this checker is precisely what stops a change
36
+ * granting itself the exception that permits it.
13
37
  */
14
38
  export const FAMILIES = [
15
39
  {
@@ -0,0 +1,52 @@
1
+ # Credential-Substrate Precedence (load-bearing)
2
+
3
+ **When more than one substrate can reach an external system, the configured credentials
4
+ provider's token/CLI path goes first and the interactive MCP is the fallback — and
5
+ identity-match verification is mandatory on every substrate, at every tier.**
6
+
7
+ **One shared, vendor-neutral contract cited by every `*-access` skill** (the
8
+ `leaf-only-lifecycle` / `repo-scope-split` precedent: one shared slug, never divergent
9
+ per-skill prose). An access skill states its per-vendor mechanics — which token, which
10
+ CLI, which identity anchor — and cites this rule for the ordering. It never restates,
11
+ narrows, or locally overrides that ordering.
12
+
13
+ Settled by decision record `2026-08-12-credential-substrate-precedence` (D6), and
14
+ settled in the `settled-decisions` sense: re-arguing MCP-first inside a skill is out of
15
+ scope for that skill's work.
16
+
17
+ ## The ladder
18
+
19
+ 1. **Tier 1 — configured-provider substrate.** The token or CLI path fed by
20
+ `lisa-secrets-access`, chosen whenever its bootstrap credential is available **and**
21
+ the resolved substrate identity-matches the configured tenant/workspace/site.
22
+ `lisa-secrets-access` is the single chokepoint — never read an OS keychain a second
23
+ time.
24
+ 2. **Tier 2 — interactive MCP**, used only when tier 1 is *genuinely* unavailable:
25
+ no bootstrap, no adapter for the operation (per-operation, not per-session), or a
26
+ provider outage. "The MCP happens to be authenticated" and "tier 1 is slower" are
27
+ **not** qualifying reasons.
28
+ 3. **Tier 3 — loud, actionable failure** naming the exact credential to set and the exact
29
+ remediation. Never silently no-op, never blind-retry a failed or absent substrate,
30
+ never fall through to one that failed identity-match.
31
+
32
+ ## Identity-match is mandatory on every substrate
33
+
34
+ Verified **in both directions** before any operation: the substrate must claim the
35
+ configured tenant, and the configured tenant must be one the substrate can reach. A
36
+ substrate authenticated as a different account is **skipped, never used — including at
37
+ tier 1**. A credential is not an identity claim; the identity claim is what the provider
38
+ says when asked. Skipping the check because "the user obviously meant this workspace" is
39
+ forbidden.
40
+
41
+ ## Mutating operations: fallback is guarded, never routine
42
+
43
+ Falling back to an ambient-bound substrate for a **write** requires: switch profile and
44
+ assert identity immediately before the write → write → re-read the affected objects →
45
+ assert the tenant **from the response** (self URL host, cloudId in the path, response
46
+ metadata), not from the pre-flight check → on mismatch, stop, report a cross-tenant
47
+ hazard, and best-effort roll back. A successful pre-flight switch is not sufficient:
48
+ another process can mutate global state between the check and the write.
49
+
50
+ Full contract (per-vendor identity anchors and probes, the provider-first rationale, MCP's
51
+ first-class fallback role, consequences, and the checklist for adding or editing an access
52
+ skill): [reference/credential-substrate-precedence.md](../reference/credential-substrate-precedence.md).
@@ -48,7 +48,7 @@ platforms it requires and that each named platform has a configured runner.
48
48
  "schemaVersion": 1,
49
49
  "asOf": "<ISO date>",
50
50
  "runnerPlatforms": { "<runner>": ["<platform>", "..."] },
51
- "coverageFloor": { "<platform>": "<0-100, the committed floormay rise, never falls>" },
51
+ "coverageFloor": { "<platform>": "<0-100, an absolute bar checked in enforced mode not a ratchet, set once and left alone>" },
52
52
  "platformWaivers": [
53
53
  {
54
54
  "scenario": "BDD-DOMAIN-NNN",
@@ -123,7 +123,9 @@ Two commands, wired into the project's script surface and into CI:
123
123
  existing mapping);
124
124
  - a discovered test named by no mapping and no exclusion, or an exclusion that no longer excuses
125
125
  anything;
126
- - a regression against the project's committed `coverageFloor` per platform.
126
+ - a platform sitting below its committed `coverageFloor`;
127
+ - coverage given back: an obligation mapped at the base revision that nothing maps here;
128
+ - new behavior nobody mapped or waived.
127
129
 
128
130
  Regeneration is never blocked by the check: `--write` rewrites the report and burndown whenever a
129
131
  report can be built at all, so a stale evidence string can never hold hostage the paperwork that
@@ -134,12 +136,20 @@ that currently fails is a red CI check, a separate signal; the map only asserts
134
136
  exists and still says what it claimed. Both facts are required — a green gate over a red suite is
135
137
  not coverage.
136
138
 
137
- ### Coverage floor, not coverage target
139
+ ### Coverage floor, not coverage target — and not a ratchet
138
140
 
139
- Projects adopting this contract mid-life start below 100% and must not be blocked by that. The gate
140
- enforces a **ratchet**: the committed floor per platform may rise and may never fall. Recorded in
141
- `coverageFloor`, it is bumped by regenerating the matrix after a frontend work item raises it by the
142
- obligations it seals; it is never asked to clear the whole backlog.
141
+ Projects adopting this contract mid-life start below 100% and must not be blocked by that. The
142
+ committed `coverageFloor` per platform is an **absolute bar** answering "is this platform below it
143
+ right now". Set it once at adoption to the honest measured number (or `0`) and leave it: nothing
144
+ forces it upward, and lowering it needs no ceremony.
145
+
146
+ What stops coverage sliding backwards is checked directly, per obligation, against the base
147
+ revision — **an obligation that was mapped may not stop being mapped, and new behavior arrives
148
+ mapped or waived**. Giving coverage back is legitimate but takes two artifacts one author cannot
149
+ produce alone: a recorded route (a `retirements` record or a `platformWaivers` entry) plus the
150
+ maintainer-applied `bdd-floor-baseline` label. Gaps that predate the change are burndown, never a
151
+ gate failure — which is what lets a brownfield project adopt `enforced` without first backfilling
152
+ its whole history.
143
153
 
144
154
  ## Waivers versus `@blocked`
145
155
 
@@ -198,7 +208,8 @@ A repo with no contract yet, taking its first frontend work item:
198
208
  during this step, never left undisclosed.
199
209
  3. **Write only this item's scenarios.** The first item is not a backfill project. Pre-existing
200
210
  uncovered behavior becomes burndown in `docs/e2e-bdd-coverage.md`, and the floor starts where the
201
- repo actually is.
211
+ repo actually is. That is a one-time act, not a recurring one: from here on the floor stays put
212
+ and what protects each new obligation is the per-obligation check, not the number.
202
213
  4. **Seal this item's obligations** and commit the regenerated matrix and burndown with the change.
203
214
 
204
215
  If a required platform has **no** e2e runner at all, that obligation is never left as a bare `N/A` —
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -10,6 +10,30 @@
10
10
  * File families the ratchet watches. `kind` selects the extractor;
11
11
  * `direction` applies to numeric-leaf kinds ("min" values may only rise,
12
12
  * "max" values may only fall).
13
+ *
14
+ * PER-FAMILY POLICY, reviewed 2026-08-12. A family keeps its ratchet unless a
15
+ * NAMED deterministic non-regression invariant replaces the property the
16
+ * ratchet was providing — deleting a ratchet deletes that property with it, so
17
+ * "this generates churn" is a reason to find a replacement, never a reason to
18
+ * delete on its own:
19
+ *
20
+ * coverage / simplecov / e2e KEPT. Here the ratchet IS the non-regression
21
+ * invariant, and no per-item equivalent exists short of committing a
22
+ * per-file coverage baseline — an artifact that only ever accumulates.
23
+ * Revisit if one appears.
24
+ * stryker KEPT. `thresholds.break` is an absolute floor,
25
+ * kept outright; the mutate-list comparison detects EXEMPTION ADDITIONS
26
+ * rather than creep, and has no number for a pull request to nudge.
27
+ * eslint / rubocop / k6 / lisa-config KEPT, unchanged.
28
+ *
29
+ * Exactly one family was replaced, and it is not watched here and never was:
30
+ * the BDD traceability floor, whose numeric ratchet gave way to per-obligation
31
+ * checks. See `expo/copy-overwrite/scripts/bdd/baseline.mjs`.
32
+ *
33
+ * Retiring a family is SEQUENCED: the replacement invariant lands, then a
34
+ * `thresholdRatchet.allow` entry merges from the base side, then the mechanism
35
+ * changes. Never the reverse — this checker is precisely what stops a change
36
+ * granting itself the exception that permits it.
13
37
  */
14
38
  export const FAMILIES = [
15
39
  {
@@ -0,0 +1,52 @@
1
+ # Credential-Substrate Precedence (load-bearing)
2
+
3
+ **When more than one substrate can reach an external system, the configured credentials
4
+ provider's token/CLI path goes first and the interactive MCP is the fallback — and
5
+ identity-match verification is mandatory on every substrate, at every tier.**
6
+
7
+ **One shared, vendor-neutral contract cited by every `*-access` skill** (the
8
+ `leaf-only-lifecycle` / `repo-scope-split` precedent: one shared slug, never divergent
9
+ per-skill prose). An access skill states its per-vendor mechanics — which token, which
10
+ CLI, which identity anchor — and cites this rule for the ordering. It never restates,
11
+ narrows, or locally overrides that ordering.
12
+
13
+ Settled by decision record `2026-08-12-credential-substrate-precedence` (D6), and
14
+ settled in the `settled-decisions` sense: re-arguing MCP-first inside a skill is out of
15
+ scope for that skill's work.
16
+
17
+ ## The ladder
18
+
19
+ 1. **Tier 1 — configured-provider substrate.** The token or CLI path fed by
20
+ `lisa-secrets-access`, chosen whenever its bootstrap credential is available **and**
21
+ the resolved substrate identity-matches the configured tenant/workspace/site.
22
+ `lisa-secrets-access` is the single chokepoint — never read an OS keychain a second
23
+ time.
24
+ 2. **Tier 2 — interactive MCP**, used only when tier 1 is *genuinely* unavailable:
25
+ no bootstrap, no adapter for the operation (per-operation, not per-session), or a
26
+ provider outage. "The MCP happens to be authenticated" and "tier 1 is slower" are
27
+ **not** qualifying reasons.
28
+ 3. **Tier 3 — loud, actionable failure** naming the exact credential to set and the exact
29
+ remediation. Never silently no-op, never blind-retry a failed or absent substrate,
30
+ never fall through to one that failed identity-match.
31
+
32
+ ## Identity-match is mandatory on every substrate
33
+
34
+ Verified **in both directions** before any operation: the substrate must claim the
35
+ configured tenant, and the configured tenant must be one the substrate can reach. A
36
+ substrate authenticated as a different account is **skipped, never used — including at
37
+ tier 1**. A credential is not an identity claim; the identity claim is what the provider
38
+ says when asked. Skipping the check because "the user obviously meant this workspace" is
39
+ forbidden.
40
+
41
+ ## Mutating operations: fallback is guarded, never routine
42
+
43
+ Falling back to an ambient-bound substrate for a **write** requires: switch profile and
44
+ assert identity immediately before the write → write → re-read the affected objects →
45
+ assert the tenant **from the response** (self URL host, cloudId in the path, response
46
+ metadata), not from the pre-flight check → on mismatch, stop, report a cross-tenant
47
+ hazard, and best-effort roll back. A successful pre-flight switch is not sufficient:
48
+ another process can mutate global state between the check and the write.
49
+
50
+ Full contract (per-vendor identity anchors and probes, the provider-first rationale, MCP's
51
+ first-class fallback role, consequences, and the checklist for adding or editing an access
52
+ skill): [reference/credential-substrate-precedence.md](../reference/credential-substrate-precedence.md).
@@ -48,7 +48,7 @@ platforms it requires and that each named platform has a configured runner.
48
48
  "schemaVersion": 1,
49
49
  "asOf": "<ISO date>",
50
50
  "runnerPlatforms": { "<runner>": ["<platform>", "..."] },
51
- "coverageFloor": { "<platform>": "<0-100, the committed floormay rise, never falls>" },
51
+ "coverageFloor": { "<platform>": "<0-100, an absolute bar checked in enforced mode not a ratchet, set once and left alone>" },
52
52
  "platformWaivers": [
53
53
  {
54
54
  "scenario": "BDD-DOMAIN-NNN",
@@ -123,7 +123,9 @@ Two commands, wired into the project's script surface and into CI:
123
123
  existing mapping);
124
124
  - a discovered test named by no mapping and no exclusion, or an exclusion that no longer excuses
125
125
  anything;
126
- - a regression against the project's committed `coverageFloor` per platform.
126
+ - a platform sitting below its committed `coverageFloor`;
127
+ - coverage given back: an obligation mapped at the base revision that nothing maps here;
128
+ - new behavior nobody mapped or waived.
127
129
 
128
130
  Regeneration is never blocked by the check: `--write` rewrites the report and burndown whenever a
129
131
  report can be built at all, so a stale evidence string can never hold hostage the paperwork that
@@ -134,12 +136,20 @@ that currently fails is a red CI check, a separate signal; the map only asserts
134
136
  exists and still says what it claimed. Both facts are required — a green gate over a red suite is
135
137
  not coverage.
136
138
 
137
- ### Coverage floor, not coverage target
139
+ ### Coverage floor, not coverage target — and not a ratchet
138
140
 
139
- Projects adopting this contract mid-life start below 100% and must not be blocked by that. The gate
140
- enforces a **ratchet**: the committed floor per platform may rise and may never fall. Recorded in
141
- `coverageFloor`, it is bumped by regenerating the matrix after a frontend work item raises it by the
142
- obligations it seals; it is never asked to clear the whole backlog.
141
+ Projects adopting this contract mid-life start below 100% and must not be blocked by that. The
142
+ committed `coverageFloor` per platform is an **absolute bar** answering "is this platform below it
143
+ right now". Set it once at adoption to the honest measured number (or `0`) and leave it: nothing
144
+ forces it upward, and lowering it needs no ceremony.
145
+
146
+ What stops coverage sliding backwards is checked directly, per obligation, against the base
147
+ revision — **an obligation that was mapped may not stop being mapped, and new behavior arrives
148
+ mapped or waived**. Giving coverage back is legitimate but takes two artifacts one author cannot
149
+ produce alone: a recorded route (a `retirements` record or a `platformWaivers` entry) plus the
150
+ maintainer-applied `bdd-floor-baseline` label. Gaps that predate the change are burndown, never a
151
+ gate failure — which is what lets a brownfield project adopt `enforced` without first backfilling
152
+ its whole history.
143
153
 
144
154
  ## Waivers versus `@blocked`
145
155
 
@@ -198,7 +208,8 @@ A repo with no contract yet, taking its first frontend work item:
198
208
  during this step, never left undisclosed.
199
209
  3. **Write only this item's scenarios.** The first item is not a backfill project. Pre-existing
200
210
  uncovered behavior becomes burndown in `docs/e2e-bdd-coverage.md`, and the floor starts where the
201
- repo actually is.
211
+ repo actually is. That is a one-time act, not a recurring one: from here on the floor stays put
212
+ and what protects each new obligation is the per-obligation check, not the number.
202
213
  4. **Seal this item's obligations** and commit the regenerated matrix and burndown with the change.
203
214
 
204
215
  If a required platform has **no** e2e runner at all, that obligation is never left as a bare `N/A` —
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -10,6 +10,30 @@
10
10
  * File families the ratchet watches. `kind` selects the extractor;
11
11
  * `direction` applies to numeric-leaf kinds ("min" values may only rise,
12
12
  * "max" values may only fall).
13
+ *
14
+ * PER-FAMILY POLICY, reviewed 2026-08-12. A family keeps its ratchet unless a
15
+ * NAMED deterministic non-regression invariant replaces the property the
16
+ * ratchet was providing — deleting a ratchet deletes that property with it, so
17
+ * "this generates churn" is a reason to find a replacement, never a reason to
18
+ * delete on its own:
19
+ *
20
+ * coverage / simplecov / e2e KEPT. Here the ratchet IS the non-regression
21
+ * invariant, and no per-item equivalent exists short of committing a
22
+ * per-file coverage baseline — an artifact that only ever accumulates.
23
+ * Revisit if one appears.
24
+ * stryker KEPT. `thresholds.break` is an absolute floor,
25
+ * kept outright; the mutate-list comparison detects EXEMPTION ADDITIONS
26
+ * rather than creep, and has no number for a pull request to nudge.
27
+ * eslint / rubocop / k6 / lisa-config KEPT, unchanged.
28
+ *
29
+ * Exactly one family was replaced, and it is not watched here and never was:
30
+ * the BDD traceability floor, whose numeric ratchet gave way to per-obligation
31
+ * checks. See `expo/copy-overwrite/scripts/bdd/baseline.mjs`.
32
+ *
33
+ * Retiring a family is SEQUENCED: the replacement invariant lands, then a
34
+ * `thresholdRatchet.allow` entry merges from the base side, then the mechanism
35
+ * changes. Never the reverse — this checker is precisely what stops a change
36
+ * granting itself the exception that permits it.
13
37
  */
14
38
  export const FAMILIES = [
15
39
  {
@@ -53,7 +53,7 @@ platforms it requires and that each named platform has a configured runner.
53
53
  "schemaVersion": 1,
54
54
  "asOf": "<ISO date>",
55
55
  "runnerPlatforms": { "<runner>": ["<platform>", "..."] },
56
- "coverageFloor": { "<platform>": "<0-100, the committed floormay rise, never falls>" },
56
+ "coverageFloor": { "<platform>": "<0-100, an absolute bar checked in enforced mode not a ratchet, set once and left alone>" },
57
57
  "platformWaivers": [
58
58
  {
59
59
  "scenario": "BDD-DOMAIN-NNN",
@@ -128,7 +128,9 @@ Two commands, wired into the project's script surface and into CI:
128
128
  existing mapping);
129
129
  - a discovered test named by no mapping and no exclusion, or an exclusion that no longer excuses
130
130
  anything;
131
- - a regression against the project's committed `coverageFloor` per platform.
131
+ - a platform sitting below its committed `coverageFloor`;
132
+ - coverage given back: an obligation mapped at the base revision that nothing maps here;
133
+ - new behavior nobody mapped or waived.
132
134
 
133
135
  Regeneration is never blocked by the check: `--write` rewrites the report and burndown whenever a
134
136
  report can be built at all, so a stale evidence string can never hold hostage the paperwork that
@@ -139,12 +141,20 @@ that currently fails is a red CI check, a separate signal; the map only asserts
139
141
  exists and still says what it claimed. Both facts are required — a green gate over a red suite is
140
142
  not coverage.
141
143
 
142
- ### Coverage floor, not coverage target
144
+ ### Coverage floor, not coverage target — and not a ratchet
143
145
 
144
- Projects adopting this contract mid-life start below 100% and must not be blocked by that. The gate
145
- enforces a **ratchet**: the committed floor per platform may rise and may never fall. Recorded in
146
- `coverageFloor`, it is bumped by regenerating the matrix after a frontend work item raises it by the
147
- obligations it seals; it is never asked to clear the whole backlog.
146
+ Projects adopting this contract mid-life start below 100% and must not be blocked by that. The
147
+ committed `coverageFloor` per platform is an **absolute bar** answering "is this platform below it
148
+ right now". Set it once at adoption to the honest measured number (or `0`) and leave it: nothing
149
+ forces it upward, and lowering it needs no ceremony.
150
+
151
+ What stops coverage sliding backwards is checked directly, per obligation, against the base
152
+ revision — **an obligation that was mapped may not stop being mapped, and new behavior arrives
153
+ mapped or waived**. Giving coverage back is legitimate but takes two artifacts one author cannot
154
+ produce alone: a recorded route (a `retirements` record or a `platformWaivers` entry) plus the
155
+ maintainer-applied `bdd-floor-baseline` label. Gaps that predate the change are burndown, never a
156
+ gate failure — which is what lets a brownfield project adopt `enforced` without first backfilling
157
+ its whole history.
148
158
 
149
159
  ## Waivers versus `@blocked`
150
160
 
@@ -203,7 +213,8 @@ A repo with no contract yet, taking its first frontend work item:
203
213
  during this step, never left undisclosed.
204
214
  3. **Write only this item's scenarios.** The first item is not a backfill project. Pre-existing
205
215
  uncovered behavior becomes burndown in `docs/e2e-bdd-coverage.md`, and the floor starts where the
206
- repo actually is.
216
+ repo actually is. That is a one-time act, not a recurring one: from here on the floor stays put
217
+ and what protects each new obligation is the per-obligation check, not the number.
207
218
  4. **Seal this item's obligations** and commit the regenerated matrix and burndown with the change.
208
219
 
209
220
  If a required platform has **no** e2e runner at all, that obligation is never left as a bare `N/A` —
@@ -0,0 +1,57 @@
1
+ ---
2
+ description: "Credential-Substrate Precedence (load-bearing)"
3
+ alwaysApply: true
4
+ ---
5
+
6
+ # Credential-Substrate Precedence (load-bearing)
7
+
8
+ **When more than one substrate can reach an external system, the configured credentials
9
+ provider's token/CLI path goes first and the interactive MCP is the fallback — and
10
+ identity-match verification is mandatory on every substrate, at every tier.**
11
+
12
+ **One shared, vendor-neutral contract cited by every `*-access` skill** (the
13
+ `leaf-only-lifecycle` / `repo-scope-split` precedent: one shared slug, never divergent
14
+ per-skill prose). An access skill states its per-vendor mechanics — which token, which
15
+ CLI, which identity anchor — and cites this rule for the ordering. It never restates,
16
+ narrows, or locally overrides that ordering.
17
+
18
+ Settled by decision record `2026-08-12-credential-substrate-precedence` (D6), and
19
+ settled in the `settled-decisions` sense: re-arguing MCP-first inside a skill is out of
20
+ scope for that skill's work.
21
+
22
+ ## The ladder
23
+
24
+ 1. **Tier 1 — configured-provider substrate.** The token or CLI path fed by
25
+ `lisa-secrets-access`, chosen whenever its bootstrap credential is available **and**
26
+ the resolved substrate identity-matches the configured tenant/workspace/site.
27
+ `lisa-secrets-access` is the single chokepoint — never read an OS keychain a second
28
+ time.
29
+ 2. **Tier 2 — interactive MCP**, used only when tier 1 is *genuinely* unavailable:
30
+ no bootstrap, no adapter for the operation (per-operation, not per-session), or a
31
+ provider outage. "The MCP happens to be authenticated" and "tier 1 is slower" are
32
+ **not** qualifying reasons.
33
+ 3. **Tier 3 — loud, actionable failure** naming the exact credential to set and the exact
34
+ remediation. Never silently no-op, never blind-retry a failed or absent substrate,
35
+ never fall through to one that failed identity-match.
36
+
37
+ ## Identity-match is mandatory on every substrate
38
+
39
+ Verified **in both directions** before any operation: the substrate must claim the
40
+ configured tenant, and the configured tenant must be one the substrate can reach. A
41
+ substrate authenticated as a different account is **skipped, never used — including at
42
+ tier 1**. A credential is not an identity claim; the identity claim is what the provider
43
+ says when asked. Skipping the check because "the user obviously meant this workspace" is
44
+ forbidden.
45
+
46
+ ## Mutating operations: fallback is guarded, never routine
47
+
48
+ Falling back to an ambient-bound substrate for a **write** requires: switch profile and
49
+ assert identity immediately before the write → write → re-read the affected objects →
50
+ assert the tenant **from the response** (self URL host, cloudId in the path, response
51
+ metadata), not from the pre-flight check → on mismatch, stop, report a cross-tenant
52
+ hazard, and best-effort roll back. A successful pre-flight switch is not sufficient:
53
+ another process can mutate global state between the check and the write.
54
+
55
+ Full contract (per-vendor identity anchors and probes, the provider-first rationale, MCP's
56
+ first-class fallback role, consequences, and the checklist for adding or editing an access
57
+ skill): [reference/credential-substrate-precedence.md](credential-substrate-precedence-reference.mdc).
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Expo and React Native-specific skills, agents, rules, and MCP servers.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-expo",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Expo/React Native-specific skills, agents, rules, and MCP servers",
5
5
  "author": {
6
6
  "name": "Cody Swann"