@codyswann/lisa 3.15.1 → 3.17.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 (120) hide show
  1. package/all/copy-overwrite/scripts/lisa-gates.mjs +1165 -0
  2. package/all/copy-overwrite/scripts/lisa-reconcile-policy.mjs +1188 -0
  3. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +597 -0
  4. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  5. package/dist/core/lisa-owned-hash-ledger.js +24 -0
  6. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  8. package/dist/core/upstream-evidence-manifest.js +76 -9
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/package.json +6 -2
  11. package/plugins/lisa/.claude-plugin/plugin.json +10 -1
  12. package/plugins/lisa/.codex-plugin/hooks.json +9 -0
  13. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  14. package/plugins/lisa/.codex-plugin/skills/lisa-doctor/SKILL.md +108 -2
  15. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  16. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  17. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  18. package/plugins/lisa/.codex-plugin/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  19. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  20. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  21. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  22. package/plugins/lisa/hooks/secrets-preflight.sh +72 -0
  23. package/plugins/lisa/skills/lisa-doctor/SKILL.md +108 -2
  24. package/plugins/lisa/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  25. package/plugins/lisa/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  26. package/plugins/lisa/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  27. package/plugins/lisa/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  28. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  29. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  30. package/plugins/lisa/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  31. package/plugins/lisa-agy/plugin.json +1 -1
  32. package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +108 -2
  33. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  34. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  35. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  36. package/plugins/lisa-agy/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  37. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  38. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  39. package/plugins/lisa-agy/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  40. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  43. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-copilot/.claude-plugin/plugin.json +10 -1
  46. package/plugins/lisa-copilot/hooks/secrets-preflight.sh +72 -0
  47. package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +108 -2
  48. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  49. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  50. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  51. package/plugins/lisa-copilot/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  52. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  53. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  54. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  55. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-cursor/hooks/hooks.json +3 -0
  57. package/plugins/lisa-cursor/hooks/secrets-preflight.sh +72 -0
  58. package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +108 -2
  59. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  60. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  61. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  62. package/plugins/lisa-cursor/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  63. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  64. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  65. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  66. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  67. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  68. package/plugins/lisa-expo-agy/plugin.json +1 -1
  69. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  71. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  72. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  73. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  74. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  75. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  76. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  77. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  78. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  79. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  80. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  81. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  82. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  83. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  84. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  85. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  86. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  87. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  88. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  89. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  90. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  91. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  92. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  93. package/plugins/lisa-rails-agy/plugin.json +1 -1
  94. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  95. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  96. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  97. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  98. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  99. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  100. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  101. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  102. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  103. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  104. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  105. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  106. package/plugins/src/base/.claude-plugin/plugin.json +9 -0
  107. package/plugins/src/base/hooks/secrets-preflight.sh +72 -0
  108. package/plugins/src/base/skills/lisa-doctor/SKILL.md +108 -2
  109. package/plugins/src/base/skills/lisa-secrets-access/scripts/preflight-secrets.mjs +324 -0
  110. package/plugins/src/base/skills/lisa-secrets-access/scripts/routing-floor.mjs +153 -0
  111. package/plugins/src/base/skills/lisa-secrets-access/scripts/surfaces.mjs +55 -7
  112. package/plugins/src/base/skills/lisa-secrets-access/scripts/validate-config.mjs +89 -17
  113. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs +242 -0
  114. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/tool-floor.mjs +94 -0
  115. package/plugins/src/base/skills/lisa-setup-remote-env/scripts/verify-remote-env.mjs +14 -32
  116. package/scripts/generate-lisa-owned-hash-ledger.mjs +10 -1
  117. package/scripts/lib/per-agent-hook-filter.mjs +18 -0
  118. package/typescript/copy-contents/.husky/pre-commit +130 -21
  119. package/typescript/copy-contents/.husky/pre-push +202 -22
  120. package/ui/index.html +271 -12
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Derive the CLIs a project's own configuration already implies.
3
+ *
4
+ * The symmetric half of `routing-floor.mjs`. A credential and the binary that
5
+ * consumes it are the same question asked twice — can this agent do the work —
6
+ * and both were answered only by a hand-maintained list.
7
+ *
8
+ * `detect-tooling` already knows most of these pairings and states them well:
9
+ * "the work-item guardrails shell out to `gh` for every tracker read". But it
10
+ * knows them at *proposal* time, as suggestions a human runs a skill to see and
11
+ * then copies into config. Nothing derives them at runtime, so a project that
12
+ * never ran the skill — or ran it before it adopted Maestro — has a manifest
13
+ * that silently understates what its agents need.
14
+ *
15
+ * This promotes those pairings to a derivation. The floor is unioned into the
16
+ * required set, never written into config, so it cannot drift from the routing
17
+ * that produces it.
18
+ *
19
+ * Deliberately narrow. An entry belongs here only when the configuration that
20
+ * implies it makes the tool unavoidable — not merely likely. A project can have
21
+ * `quality.testCoverage` thresholds and run them through any runner, so no CLI
22
+ * is derived from that; `secrets.provider: "bitwarden"` on the other hand
23
+ * cannot resolve one credential without `bws`.
24
+ * @module tool-floor
25
+ */
26
+
27
+ /**
28
+ * Config predicates that make a CLI unavoidable, with the reason to report.
29
+ *
30
+ * The reason is not decoration. A missing tool has two valid remedies — install
31
+ * it, or change the configuration that demands it — and an operator cannot pick
32
+ * between them without knowing which line of config is responsible.
33
+ */
34
+ const DERIVATIONS = [
35
+ {
36
+ tool: "gh",
37
+ applies: cfg => cfg.tracker === "github" || cfg.source === "github",
38
+ reason: cfg =>
39
+ `${cfg.tracker === "github" ? "tracker" : "source"} is "github", and ` +
40
+ `the work-item guardrails shell out to gh for every tracker read`,
41
+ },
42
+ {
43
+ tool: "bws",
44
+ applies: cfg => cfg.secrets?.provider === "bitwarden",
45
+ reason: () =>
46
+ `secrets.provider is "bitwarden", and the CLI is how every secret is ` +
47
+ `resolved`,
48
+ },
49
+ {
50
+ tool: "doppler",
51
+ applies: cfg => cfg.secrets?.provider === "doppler",
52
+ reason: () => `secrets.provider is "doppler"`,
53
+ },
54
+ {
55
+ tool: "maestro",
56
+ // Keyed on the coverage block rather than a `.maestro` directory: config is
57
+ // the declaration that this project gates on Maestro, where a stray
58
+ // directory could be a leftover. `detect-tooling` uses the directory
59
+ // because it is guessing; here the project has already said so.
60
+ applies: cfg => cfg.quality?.e2eCoverage?.maestro !== undefined,
61
+ reason: () =>
62
+ `quality.e2eCoverage.maestro is configured, and nothing installs the ` +
63
+ `maestro binary`,
64
+ },
65
+ ];
66
+
67
+ /**
68
+ * The CLIs a project's configuration makes mandatory.
69
+ * @param {object} [config] Parsed `.lisa.config.json` root.
70
+ * @returns {Array<{name: string, reason: string}>} Sorted derived tools.
71
+ */
72
+ export function toolFloor(config = {}) {
73
+ return DERIVATIONS.filter(entry => safely(() => entry.applies(config)))
74
+ .map(entry => ({ name: entry.tool, reason: entry.reason(config) }))
75
+ .sort((left, right) => left.name.localeCompare(right.name));
76
+ }
77
+
78
+ /**
79
+ * Evaluate a predicate, treating a malformed config as "does not apply".
80
+ *
81
+ * A config shaped unexpectedly is a problem for the schema validator to report
82
+ * in its own vocabulary. Throwing here would make a readiness check the place a
83
+ * structural defect first surfaces, with a message about tools that names
84
+ * nothing an operator can act on.
85
+ * @param {Function} predicate Test to run.
86
+ * @returns {boolean} The result, or false when it threw.
87
+ */
88
+ function safely(predicate) {
89
+ try {
90
+ return Boolean(predicate());
91
+ } catch {
92
+ return false;
93
+ }
94
+ }
@@ -25,8 +25,8 @@ import { execFileSync } from "node:child_process";
25
25
  import { existsSync, readFileSync, statSync } from "node:fs";
26
26
  import { join } from "node:path";
27
27
 
28
- import { readRemoteEnvConfig } from "./setup-remote-env.mjs";
29
- import { extractVersion } from "./toolchain.mjs";
28
+ import { probe, readRemoteEnvConfig } from "./setup-remote-env.mjs";
29
+ import { planToolchain } from "./toolchain.mjs";
30
30
 
31
31
  /** Collected results, so every check runs before anything reports failure. */
32
32
  const results = [];
@@ -84,39 +84,21 @@ function node(args) {
84
84
 
85
85
  /**
86
86
  * Assert each declared tool is present at its pinned or minimum version.
87
+ *
88
+ * Delegates every decision to `planToolchain` rather than probing here. This
89
+ * function used to run its own loop, and the copy silently forgot `minVersion`:
90
+ * a `require` entry passed as long as the binary answered `--version` at all,
91
+ * so a container verified clean against a node older than the manifest
92
+ * demanded. The plan-side check rejected exactly what this one accepted.
93
+ *
94
+ * That is the same failure the note validators had before one shared module
95
+ * replaced them, and it recurs for the same reason — two implementations of one
96
+ * question cannot be kept in step by intention. There is now one.
87
97
  * @param {object} tools Toolchain manifest.
88
98
  */
89
99
  function verifyToolchain(tools) {
90
- for (const tool of tools.require ?? []) {
91
- try {
92
- const out = execFileSync(tool.name, ["--version"], {
93
- encoding: "utf8",
94
- stdio: ["ignore", "pipe", "ignore"],
95
- });
96
- check(true, `tool ${tool.name}`, extractVersion(out) ?? "present");
97
- } catch {
98
- check(false, `tool ${tool.name}`, "required but not present");
99
- }
100
- }
101
- for (const tool of tools.install ?? []) {
102
- try {
103
- const out = execFileSync(tool.name, ["--version"], {
104
- encoding: "utf8",
105
- stdio: ["ignore", "pipe", "ignore"],
106
- });
107
- const found = extractVersion(out);
108
- check(
109
- found === tool.version,
110
- `tool ${tool.name}`,
111
- `${found} (pin ${tool.version})`
112
- );
113
- } catch {
114
- check(
115
- false,
116
- `tool ${tool.name}`,
117
- `pinned ${tool.version} but not installed`
118
- );
119
- }
100
+ for (const step of planToolchain(tools, probe, "remote")) {
101
+ check(step.action === "present", `tool ${step.name}`, step.reason);
120
102
  }
121
103
  }
122
104
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "3.15.1",
3
+ "version": "3.17.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.15.1",
3
+ "version": "3.17.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.15.1",
3
+ "version": "3.17.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.15.1",
3
+ "version": "3.17.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.15.1",
3
+ "version": "3.17.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.15.1",
3
+ "version": "3.17.0",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -120,6 +120,15 @@
120
120
  "command": "${CLAUDE_PLUGIN_ROOT}/hooks/setup-jira-cli.sh"
121
121
  }
122
122
  ]
123
+ },
124
+ {
125
+ "matcher": "",
126
+ "hooks": [
127
+ {
128
+ "type": "command",
129
+ "command": "${CLAUDE_PLUGIN_ROOT}/hooks/secrets-preflight.sh"
130
+ }
131
+ ]
123
132
  }
124
133
  ],
125
134
  "sessionEnd": [
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env bash
2
+ # Prove at session start that this agent can actually do the work: that the
3
+ # credentials the project needs resolve, and that the CLIs it needs are on PATH.
4
+ #
5
+ # One hook for both because it is one question — can this agent do the work —
6
+ # and an operator reading two separate reports has to assemble the answer
7
+ # themselves. Both halves also share the same failure mode if split: two hooks
8
+ # means two subprocess spawns on every session start, for a check that is silent
9
+ # almost every time.
10
+ #
11
+ # Injects rather than blocks, and that is deliberate. The right response to a
12
+ # missing credential or tool is for the agent to know before it claims work and
13
+ # to route the item to blocked with a reason a human can act on — not for the
14
+ # session to die. A session that cannot reach the vault can still write code,
15
+ # review a PR, or answer a question; killing it would tax every session for a
16
+ # minority need, and a control people route around enforces nothing.
17
+ #
18
+ # The hard gates live where they can afford to be hard: `lisa doctor` and CI
19
+ # run the same two scripts and exit non-zero on them.
20
+ #
21
+ # Silent on success on purpose. A control that announces itself when everything
22
+ # is fine trains its readers to skim past it, and then it is decoration.
23
+ set -uo pipefail
24
+
25
+ INPUT=$(cat 2>/dev/null || true)
26
+ if [ -n "$INPUT" ]; then
27
+ HOOK_EVENT=$(printf '%s' "$INPUT" | jq -r '.hook_event_name // "SessionStart"' 2>/dev/null || echo "SessionStart")
28
+ else
29
+ HOOK_EVENT="SessionStart"
30
+ fi
31
+
32
+ ROOT="${CLAUDE_PLUGIN_ROOT:-${PLUGIN_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}}"
33
+ SECRETS="$ROOT/skills/lisa-secrets-access/scripts/preflight-secrets.mjs"
34
+ TOOLS="$ROOT/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs"
35
+
36
+ command -v node >/dev/null 2>&1 || exit 0
37
+ # The hook's whole output is one `jq -n` document. Without jq there is nothing
38
+ # to write, so exiting is the only honest outcome — the alternative is a hook
39
+ # that prints `jq: command not found` and exits 127, which reports a failure
40
+ # from a check designed never to block. The doctor and CI gates run the same
41
+ # two scripts without jq involved.
42
+ command -v jq >/dev/null 2>&1 || exit 0
43
+
44
+ # stderr carries each report; stdout is reserved for the hook's JSON. A script
45
+ # that is absent (partial or older install) or fails to run at all contributes
46
+ # nothing, because a broken hook must not manufacture a finding.
47
+ REPORT=""
48
+ for script in "$SECRETS" "$TOOLS"; do
49
+ [ -f "$script" ] || continue
50
+ # An escape hatch that is honest about what it costs, and scoped to what it
51
+ # names. Set when a surface has no secrets provider by design and the noise is
52
+ # not worth it; the doctor and CI gates are unaffected, so opting out here
53
+ # does not opt out of the check entirely. It must not also silence the
54
+ # TOOLING half — a variable about secrets that hides a missing `gh` or
55
+ # `maestro` is an opt-out from a check nobody opted out of.
56
+ if [ "$script" = "$SECRETS" ] && [ "${LISA_SKIP_SECRETS_PREFLIGHT:-}" = "1" ]; then
57
+ continue
58
+ fi
59
+ OUT=$(node "$script" 2>&1 >/dev/null) || true
60
+ [ -n "$OUT" ] || continue
61
+ [ -n "$REPORT" ] && REPORT+=$'\n\n'
62
+ REPORT+="$OUT"
63
+ done
64
+
65
+ [ -n "$REPORT" ] || exit 0
66
+
67
+ CONTEXT="## Readiness preflight
68
+
69
+ $REPORT"
70
+
71
+ jq -n --arg event "$HOOK_EVENT" --arg ctx "$CONTEXT" \
72
+ '{"hookSpecificOutput": {"hookEventName": $event, "additionalContext": $ctx}}'
@@ -422,14 +422,93 @@ The verdict ladder is:
422
422
  - `READY_WITH_WARNINGS` — no `FAIL`, but one or more `WARN`.
423
423
  - `NOT_READY` — one or more `FAIL`.
424
424
 
425
+ ## Gate configuration
426
+
427
+ ```sh
428
+ node scripts/lisa-gates.mjs validate # shape + unknown gate ids
429
+ node scripts/lisa-gates.mjs list --moment=pull-request
430
+ node scripts/lisa-gates.mjs contexts # branch-protection contexts
431
+ ```
432
+
433
+ A gate is a **property** — *credential leakage* — not a tool. `gitleaks` is one way to prove
434
+ that property, and which way is the project's choice: each gate names a task in the project's own
435
+ runner, so swapping the tool changes one line of project config and nothing in Lisa.
436
+
437
+ `validate` refuses an unknown gate id rather than ignoring it. A misspelled `credential-leakge`
438
+ would otherwise read as an enabled guarantee and run nothing at all — the same silent-hole shape
439
+ as a skipped required check. Gates a project invents carry an `x-` prefix, which Lisa runs without
440
+ pretending to understand.
441
+
442
+ `contexts` is the value that replaces a hand-transcribed branch-protection list. It is scoped to
443
+ one environment: a gate required before a production deploy is **not** thereby a merge blocker on a
444
+ pull request, and collapsing the two would promote every deploy-time gate into branch protection.
445
+
446
+ When reconciling a ruleset against it, pass `--previous=` with any label retired in the last
447
+ release. Downstream repositories call the shared workflow unpinned, so a renamed job reaches every
448
+ repository before any of them has reconciled — and a required context that never reports leaves
449
+ pull requests waiting indefinitely. The fastest way out of that is deleting the requirement, which
450
+ is how a rename ends up removing a guarantee. Emitting both labels for one release avoids it.
451
+
452
+ ### Reconciling against the live ruleset
453
+
454
+ `contexts` says what the repository *should* require. Reconciliation asks whether GitHub agrees:
455
+
456
+ ```sh
457
+ node scripts/lisa-reconcile-policy.mjs --dry-run # read-only: what would change
458
+ node scripts/lisa-reconcile-policy.mjs --on-drift=report # report, write nothing
459
+ node scripts/lisa-reconcile-policy.mjs --on-drift=block # exit 1 on any drift, for CI
460
+ node scripts/lisa-reconcile-policy.mjs # honors policy.on_drift
461
+ node scripts/lisa-reconcile-policy.mjs --previous="🧽 Lint" # keep a renamed context required
462
+ node scripts/lisa-reconcile-policy.mjs --prune # also remove EXTRA contexts
463
+ ```
464
+
465
+ It reads the live ruleset and repository settings through `gh`, compares them against the derived
466
+ contexts and the `policy` block, and reports three sets: **MISSING** (declared, not live), **EXTRA**
467
+ (live, not declared), **MATCHED**. `--dry-run` never writes, whatever `policy.on_drift` says.
468
+
469
+ Three behaviors need an operator to understand them before reading a report.
470
+
471
+ **1. `UNPROVEN` is not a pass.** If `gh` is missing, unauthenticated, or the API errors — a private
472
+ repository on a plan without rulesets answers `403` — the verdict is `UNPROVEN` and the exit code is
473
+ `2`. It is neither of its neighbours, and the distinction is the same one the secrets preflight
474
+ draws with `unreachable`: reported as clean it is a vacuous green, clean precisely because nothing
475
+ was learned; reported as drift it sends someone to fix a ruleset that may be perfectly correct. The
476
+ drift sets come back `null` rather than empty, because empty is what a clean repository looks like.
477
+ `on_drift` does not soften this — not even `report` — because `on_drift` decides what to do about a
478
+ drift that was *measured*, and here nothing was. Map it to doctor's `WARN`/`FAIL` on the same rule
479
+ as any other unavailable check surface: never `PASS`.
480
+
481
+ **2. An `EXTRA` context is reported, never removed.** Lisa does not own the whole required list.
482
+ `SonarCloud Code Analysis`, `GitGuardian Security Checks`, and `CodeRabbit` are posted by external
483
+ apps that no gates block declares, so `contextsFor` cannot derive them and every one of them is
484
+ EXTRA by construction. Under `repair` the script therefore ADDS what is missing and leaves what is
485
+ extra alone, naming each one. Removing them requires `--prune`, and the right way to clear the list
486
+ is one at a time: each EXTRA context is either an app to keep, or a gate that belongs in
487
+ `.lisa.config.json` — decide which before pruning anything.
488
+
489
+ **3. Keep both names during a rename.** `--previous=<old label>` requires the old and the new
490
+ context simultaneously for one release. Without it the reconciliation reports the still-live old
491
+ context as EXTRA (and `--prune` would delete it) while in-flight pull requests wait on a context
492
+ that will never report again.
493
+
494
+ Repair writes exactly two things: required contexts on a ruleset, and repository settings. Policy
495
+ carried by the *shape* of a rule — linear history, signed commits, force-push and deletion
496
+ protection, conversation resolution — is compared here and repaired by
497
+ `scripts/lisa-github-rulesets.sh`, which owns rule construction; the reconciler reports those and
498
+ names that script rather than reshaping rules it did not build. When more than one ruleset requires
499
+ status checks it refuses to guess which owns the derived contexts and asks for `--ruleset=<name>`,
500
+ because writing to the wrong one enforces a context under a different ref-name condition.
501
+
425
502
  ## Secrets configuration
426
503
 
427
504
  Run the secrets health checks through the skill that owns the contract, rather than reimplementing
428
505
  any part of it here:
429
506
 
430
507
  ```sh
431
- node .claude/skills/lisa-secrets-access/scripts/validate-config.mjs # shape
432
- node .claude/skills/lisa-secrets-access/scripts/doctor-secrets.mjs # health
508
+ node .claude/skills/lisa-secrets-access/scripts/validate-config.mjs # shape
509
+ node .claude/skills/lisa-secrets-access/scripts/preflight-secrets.mjs # credential readiness
510
+ node .claude/skills/lisa-setup-remote-env/scripts/preflight-tools.mjs # tooling readiness
511
+ node .claude/skills/lisa-secrets-access/scripts/doctor-secrets.mjs # health
433
512
  ```
434
513
 
435
514
  Run the validator first. It checks only the *shape* of the `secrets`, `remoteEnv`, and
@@ -438,6 +517,33 @@ would otherwise surface somewhere unhelpful: a container failing mid-setup, a sc
438
517
  never fires, a dispatch naming a surface nobody provisioned. The health check then asks whether the
439
518
  credentials actually resolve.
440
519
 
520
+ The preflight in the middle is the same check the SessionStart hook runs, and it is here for the
521
+ half the hook deliberately cannot do. The hook injects its verdict into an agent's context and
522
+ never blocks — killing a session over a credential it may not need would tax every session for a
523
+ minority need, and a control people route around enforces nothing. Doctor is where the same
524
+ verdict is allowed to be a non-zero exit.
525
+
526
+ It reports three outcomes, and the third carries the weight. `ok` and `missing` are self-evident.
527
+ **`unreachable`** means the provider itself could not be asked — no CLI, no bootstrap token, an API
528
+ that errored — and it is neither of its neighbours. Reported as `ok` it would be a vacuous green,
529
+ clean precisely because nothing was learned. Reported as `missing` it would blame the vault for a
530
+ fault in this machine's access and send someone to grant a credential that was never absent. Both
531
+ fail; they differ in what they tell the reader to fix.
532
+
533
+ The tooling preflight asks the same question about CLIs, and it is a *caller* of
534
+ `planToolchain` rather than a second implementation — that distinction is the point.
535
+ `verify-remote-env.mjs` previously ran its own toolchain loop which never consulted
536
+ `minVersion`, so a container verified clean against a `node` older than the manifest
537
+ demanded while the plan-side check rejected exactly that. One function now answers the
538
+ question everywhere.
539
+
540
+ It needs only two verdicts. A local binary cannot fail to be asked the way a vault can,
541
+ and the analogous trap — a tool present at an unparseable version — already fails closed
542
+ upstream, because an unknown version loses every `minVersion` comparison. What it does add
543
+ is a split credentials have no equivalent for: a missing tool with a pinned, checksummed
544
+ `install` entry is something Lisa can place itself, so it reports an action and exits zero,
545
+ while a tool nothing can provision blocks and exits non-zero.
546
+
441
547
  It reports without ever printing a value, and compares two copies of the same credential by digest.
442
548
  Map its findings into the doctor's own verdicts: `error` → `FAIL`, `warn` → `WARN`.
443
549