@codyswann/lisa 3.62.0 → 3.64.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 (80) hide show
  1. package/all/copy-overwrite/scripts/lisa-gates.mjs +223 -10
  2. package/all/copy-overwrite/scripts/lisa-reconcile-policy.mjs +325 -32
  3. package/dist/cli/gate-report-templates.js +1 -1
  4. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  5. package/dist/core/lisa-owned-hash-ledger.js +8 -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 +16 -9
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/dist/health/ruleset-inspection.d.ts.map +1 -1
  11. package/dist/health/ruleset-inspection.js +73 -6
  12. package/dist/health/ruleset-inspection.js.map +1 -1
  13. package/package.json +2 -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/.codex-plugin/skills/lisa-doctor/SKILL.md +46 -7
  17. package/plugins/lisa/.codex-plugin/skills/lisa-setup-github-repo/SKILL.md +7 -3
  18. package/plugins/lisa/skills/lisa-doctor/SKILL.md +46 -7
  19. package/plugins/lisa/skills/lisa-setup-github-repo/SKILL.md +7 -3
  20. package/plugins/lisa-agy/plugin.json +1 -1
  21. package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +46 -7
  22. package/plugins/lisa-agy/skills/lisa-setup-github-repo/SKILL.md +7 -3
  23. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  24. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  25. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  26. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  28. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +46 -7
  30. package/plugins/lisa-copilot/skills/lisa-setup-github-repo/SKILL.md +7 -3
  31. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +46 -7
  33. package/plugins/lisa-cursor/skills/lisa-setup-github-repo/SKILL.md +7 -3
  34. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  36. package/plugins/lisa-expo-agy/plugin.json +1 -1
  37. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  38. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  41. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  42. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  46. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  47. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  48. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  51. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  52. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  53. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  56. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  57. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  58. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  61. package/plugins/lisa-rails-agy/plugin.json +1 -1
  62. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  63. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  65. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  66. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  67. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  68. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  69. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  70. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  71. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  72. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  73. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  74. package/plugins/src/base/skills/lisa-doctor/SKILL.md +46 -7
  75. package/plugins/src/base/skills/lisa-setup-github-repo/SKILL.md +7 -3
  76. package/scripts/check-required-check-promotions.mjs +69 -4
  77. package/scripts/lisa-github-repo-setup.sh +64 -5
  78. package/scripts/lisa-github-rulesets.sh +201 -21
  79. package/scripts/lisa-ruleset-payload.mjs +376 -0
  80. package/all/github-rulesets/base.json +0 -64
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.62.0",
3
+ "version": "3.64.0",
4
4
  "description": "Distributable LLM Wiki kernel — ingest, query, lint, and maintain a git-native markdown knowledge base across Claude and Codex.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.62.0",
3
+ "version": "3.64.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.62.0",
3
+ "version": "3.64.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-wiki",
3
- "version": "3.62.0",
3
+ "version": "3.64.0",
4
4
  "description": "LLM Wiki — a distributable, git-native markdown knowledge base for Claude Code and Codex",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -501,7 +501,11 @@ remedy in that vocabulary removes a required context — the type has no such me
501
501
  guarantee holds for every caller rather than for the careful ones. A surface this run could not
502
502
  read is reported as unproven and is never a match.
503
503
 
504
- The interactive reconciler remains the way to *write* the live ruleset:
504
+ Reconciliation is **wired**, not something an agent has to remember to run: it is step 5 of
505
+ `scripts/lisa-github-repo-setup.sh`, alongside the settings baseline and the rulesets themselves,
506
+ and it is exposed as `npm run policy:reconcile`. Until #2917 its only callers were the code blocks
507
+ below — prose in this very file — so the declared configuration converged when somebody happened to
508
+ read a document. Run it by hand for the flags:
505
509
 
506
510
  ```sh
507
511
  node scripts/lisa-reconcile-policy.mjs --dry-run # read-only: what would change
@@ -529,12 +533,41 @@ drift that was *measured*, and here nothing was. Map it to doctor's `WARN`/`FAIL
529
533
  as any other unavailable check surface: never `PASS`.
530
534
 
531
535
  **2. An `EXTRA` context is reported, never removed.** Lisa does not own the whole required list.
532
- `SonarCloud Code Analysis`, `GitGuardian Security Checks`, and `CodeRabbit` are posted by external
533
- apps that no gates block declares, so `contextsFor` cannot derive them and every one of them is
534
- EXTRA by construction. Under `repair` the script therefore ADDS what is missing and leaves what is
535
- extra alone, naming each one. Removing them requires `--prune`, and the right way to clear the list
536
- is one at a time: each EXTRA context is either an app to keep, or a gate that belongs in
537
- `.lisa.config.json` decide which before pruning anything.
536
+ Under `repair` the script ADDS what is missing and leaves what is extra alone, naming each one.
537
+ Removing them requires `--prune`, and the right way to clear the list is one at a time: each EXTRA
538
+ context is either an app to keep, or a check that belongs in `.lisa.config.json` decide which
539
+ before pruning anything.
540
+
541
+ A context posted by an external app no longer HAS to be EXTRA. `SonarCloud Code Analysis`,
542
+ `GitGuardian Security Checks`, and `CodeRabbit` are declarable two ways, and either one makes the
543
+ reconciler treat them as MATCHED rather than as a permanent false alarm:
544
+
545
+ ```json
546
+ "gates": {
547
+ "credential-leakage": {
548
+ "pull-request": {
549
+ "level": "required",
550
+ "await": "GitGuardian Security Checks",
551
+ "posted_by": 46505
552
+ }
553
+ }
554
+ },
555
+ "github": {
556
+ "rulesets": {
557
+ "requiredChecks": {
558
+ "quality checks": [{ "context": "🧩 Plugin artifacts match source" }]
559
+ }
560
+ }
561
+ }
562
+ ```
563
+
564
+ An `await` names the gate the signal PROVES, so declaring nothing requires nothing — which is how a
565
+ project that proves credential leakage with a different scanner says so. `posted_by` is the GitHub
566
+ App id allowed to post that context; omit it and the context is required unpinned, GitHub's "any
567
+ source". `requiredChecks` is for a repository-specific check that no gate models; it replaces the
568
+ additive-only `addRequiredChecks`, which is still read but can never express "stop requiring this".
569
+ Both surfaces name the ruleset a repair writes to, so `--ruleset` is needed only for a
570
+ gate-DERIVED context on a repository whose rulesets are ambiguous.
538
571
 
539
572
  **3. Keep both names during a rename.** `--previous=<old label>` requires the old and the new
540
573
  context simultaneously for one release. Without it the reconciliation reports the still-live old
@@ -549,6 +582,12 @@ names that script rather than reshaping rules it did not build. When more than o
549
582
  status checks it refuses to guess which owns the derived contexts and asks for `--ruleset=<name>`,
550
583
  because writing to the wrong one enforces a context under a different ref-name condition.
551
584
 
585
+ That rule construction is itself config-driven now. `all/github-rulesets/base.json` is deleted; the
586
+ `base` ruleset is generated per project from `policy` — including `policy.ruleset.enforcement`, the
587
+ `policy.ruleset.include_refs` / `exclude_refs` conditions, `policy.ruleset.bypass_actors`, and
588
+ `policy.review.required_approving_review_count`, none of which config could express while the
589
+ template owned them.
590
+
552
591
  ## Secrets configuration
553
592
 
554
593
  Run the secrets health checks through the skill that owns the contract, rather than reimplementing
@@ -23,12 +23,13 @@ Apply the fleet-standard GitHub repository configuration to this project's repo.
23
23
  - Default branch set to the lowest-tier environment branch that exists (dev → staging → main); override with `github.settings.default_branch`
24
24
  - GitHub wiki tab off (Lisa projects use in-repo `wiki/`)
25
25
  - Secret scanning + push protection enabled where the plan supports it
26
- 2. **Rulesets** (`scripts/lisa-github-rulesets.sh`) from Lisa's `<type>/github-rulesets/` templates, matched by project type:
27
- - `base` — deletion/force-push protection on `main`/`dev`/`staging` + default, PRs required (0 approvals, review-thread resolution required, merge method = merge only), required checks: CodeRabbit + GitGuardian
26
+ 2. **Rulesets** (`scripts/lisa-github-rulesets.sh`) — one generated from config, the rest from Lisa's `<type>/github-rulesets/` templates matched by project type:
27
+ - `base` — **generated from `.lisa.config.json`**, not shipped as a template. Deletion/force-push protection from `policy.protect`, the branch list from `policy.ruleset.include_refs`, bypass actors from `policy.ruleset.bypass_actors`, approvals from `policy.review`, merge methods from `policy.merge`, and required checks from whatever gates the project `await`s. A project that declares nothing gets exactly the ruleset the retired `all/github-rulesets/base.json` described, minus its two vendor checks those are now `await` declarations a project can choose not to make
28
28
  - `quality checks` — the stack's CI checks (TypeScript emoji names or Rails names)
29
29
  - `prevent delete`, `protect tags` (`v*`), plus stack overlays (`cdk validation`, staging-only `playwright`)
30
30
  - Repos without `.github/workflows/` get only app-based required checks — an Actions check that can never report would block every PR forever
31
- - **A template change never reaches an already-configured repo on its own** — re-running this step is the migration. Reconciliation is additive (a live-only required context is preserved, never dropped) and legible: each context the run newly makes blocking is printed as `+ now required: <context>`, and a ruleset that already satisfies its template reports `nothing to do` and is not sent at all, so a second run is visibly a no-op
31
+ - **A template change never reaches an already-configured repo on its own** — re-running this step is the migration. Each context the run newly makes blocking is printed as `+ now required: <context>`, and a ruleset that already satisfies its template reports `nothing to do` and is not sent at all, so a second run is visibly a no-op
32
+ - **Declaring a ruleset's required list makes it declarative.** By default a live-only required context is preserved, never dropped — including on the generated `base`, which gets no exemption: removing a protection by default, on a script invoked with `--yes`, is not something an operator ever opted into. Name a ruleset in `github.rulesets.requiredChecks` and the live list stops being unioned in, so removing an entry actually stops requiring it — printed as `- no longer required: <context>`. Dropping an `await` without that opt-in leaves the live context in place; `lisa-reconcile-policy.mjs` then reports it as EXTRA, and `--prune` is where the removal is asked for. The retired `addRequiredChecks` is still read and could only ever add
32
33
  3. **Deploy key** (`scripts/setup-deploy-key.sh --yes`) — write-access deploy key + `DEPLOY_KEY` secret, skipped when already configured. The `base` ruleset's `DeployKey: always` bypass is what lets CI version-bump pushes through protected branches.
33
34
  4. **Deployment environments** (`scripts/lisa-github-environments.sh`) — entirely optional; only runs when `.lisa.config.json` declares environments:
34
35
  ```json
@@ -52,6 +53,9 @@ Apply the fleet-standard GitHub repository configuration to this project's repo.
52
53
  - Every declared environment also gets a **deployment branch policy** pinned to its branch, so only that branch can deploy to it.
53
54
  - Provisioning matters: GitHub silently auto-creates an environment **without** protection rules the first time a workflow references it, so an unprovisioned approval gate gates on nothing.
54
55
  - The stack `deploy.yml` templates read this same config at runtime and pass `require_approval`/`approval_environment` to `release.yml`, whose `release_approval` job is where the run pauses. Requires a public repo or a paid plan for private repos (the script skips gracefully otherwise).
56
+ 5. **Policy reconciliation** (`scripts/lisa-reconcile-policy.mjs`, also `npm run policy:reconcile`) — compares the DECLARED gates and `policy` block against what GitHub actually has, and converges it when `policy.on_drift` is `repair`. It runs here rather than only from a skill file, because a declaration nothing applies is not governance.
57
+ - The installed `scripts/lisa-reconcile-policy.mjs` is preferred; the shipped template is the fallback; finding neither is a hard failure, not a skip.
58
+ - Exit `2` is `UNPROVEN` — `gh` refused, is missing, or answered something unparseable. A private repository on a plan without rulesets answers `403`. Setup **warns and continues**: failing for a plan limitation punishes a repository that has done nothing wrong. The warning names the state, because a blind gate that says nothing is indistinguishable from a clean one.
55
59
 
56
60
  ## Workflow
57
61
 
@@ -139,7 +139,9 @@ import fs from "node:fs";
139
139
  import path from "node:path";
140
140
  import yaml from "js-yaml";
141
141
 
142
+ import { readGates } from "../all/copy-overwrite/scripts/lisa-gates.mjs";
142
143
  import { invokedAsScript } from "./lib/invoked-as-script.mjs";
144
+ import { buildRulesetPayload } from "./lisa-ruleset-payload.mjs";
143
145
 
144
146
  // Literals named once — each was repeated enough times that a typo in one
145
147
  // copy would diverge silently.
@@ -226,13 +228,19 @@ function contextsFromTemplate(absolute, relative) {
226
228
  }
227
229
 
228
230
  /**
229
- * Read the per-repository `addRequiredChecks` opt-in.
231
+ * Read the per-repository required-check opt-in.
230
232
  *
231
233
  * This surface is NOT templated and exists precisely because a Lisa-only
232
234
  * context must never ship in a shared template — host projects would inherit a
233
235
  * context they never report (the #2476 defect). A guard blind to it would clear
234
236
  * every context added this way.
235
237
  *
238
+ * Both key spellings are read: `requiredChecks` is the declarative one that
239
+ * can also STOP requiring a context, and `addRequiredChecks` is the additive
240
+ * one it replaced. A guard that read only the new name would go blind to every
241
+ * repository that has not renamed the key yet — the very blindness described
242
+ * above, reintroduced by the rename that was supposed to improve things.
243
+ *
236
244
  * @param {string} root - absolute repository root.
237
245
  * @returns {{ context: string, integrationId: number, source: string }[]} declarations.
238
246
  */
@@ -245,9 +253,18 @@ function contextsFromConfig(root) {
245
253
  } catch {
246
254
  return [];
247
255
  }
248
- const added = parsed?.github?.rulesets?.addRequiredChecks ?? {};
256
+ const rulesets = parsed?.github?.rulesets ?? {};
257
+ // Both maps are read INDEPENDENTLY, never merged by key. A spread would let
258
+ // `requiredChecks["quality checks"]` overwrite `addRequiredChecks["quality
259
+ // checks"]`, and a repository carrying both during a migration would lose the
260
+ // legacy contexts from promotion checking — a guard going quiet about exactly
261
+ // the checks a half-finished rename leaves behind.
262
+ const declared = [
263
+ ...Object.values(rulesets.addRequiredChecks ?? {}),
264
+ ...Object.values(rulesets.requiredChecks ?? {}),
265
+ ];
249
266
  const found = [];
250
- for (const entries of Object.values(added)) {
267
+ for (const entries of declared) {
251
268
  for (const entry of Array.isArray(entries) ? entries : []) {
252
269
  if (typeof entry?.context !== "string") continue;
253
270
  found.push({
@@ -264,7 +281,54 @@ function contextsFromConfig(root) {
264
281
  }
265
282
 
266
283
  /**
267
- * Collect every required context this repository declares, from both surfaces.
284
+ * Read the required contexts the generated `base` ruleset will carry.
285
+ *
286
+ * `all/github-rulesets/base.json` used to be a template this function found by
287
+ * walking each project type's `github-rulesets` directory, and it named two
288
+ * vendor contexts. It is now
289
+ * generated from `.lisa.config.json`'s `await` gate declarations, so a guard
290
+ * that only walked directories would have gone blind to two contexts that are
291
+ * still required — and worse, gone blind SILENTLY, reporting a clean promotion
292
+ * ledger for a surface it had stopped reading.
293
+ *
294
+ * @param {string} root - absolute repository root.
295
+ * @returns {{ context: string, integrationId: number, source: string }[]} declarations.
296
+ */
297
+ function contextsFromGeneratedBase(root) {
298
+ let payload;
299
+ try {
300
+ const { gates, policy } = readGates(root);
301
+ payload = buildRulesetPayload({ gates, policy });
302
+ } catch {
303
+ return [];
304
+ }
305
+ const found = [];
306
+ for (const rule of payload?.rules ?? []) {
307
+ if (rule?.type !== "required_status_checks") continue;
308
+ for (const check of rule?.parameters?.required_status_checks ?? []) {
309
+ if (typeof check?.context !== "string") continue;
310
+ found.push({
311
+ context: check.context,
312
+ // An absent id here means UNPINNED — GitHub's "any source" — not
313
+ // "GitHub Actions". The generated ruleset emits an awaited vendor check
314
+ // without an id on purpose, and defaulting it to Actions would send
315
+ // this guard looking for a workflow job named after the vendor,
316
+ // reporting a wiring violation against a context Actions never posts.
317
+ // A template check with no id genuinely IS an Actions check, which is
318
+ // why the template reader above still defaults; this one must not.
319
+ integrationId:
320
+ typeof check.integration_id === "number"
321
+ ? check.integration_id
322
+ : null,
323
+ source: ".lisa.config.json (generated base ruleset)",
324
+ });
325
+ }
326
+ }
327
+ return found;
328
+ }
329
+
330
+ /**
331
+ * Collect every required context this repository declares, from every surface.
268
332
  *
269
333
  * @param {string} root - absolute repository root.
270
334
  * @returns {{ context: string, integrationId: number, source: string }[]}
@@ -287,6 +351,7 @@ export function collectDeclaredContexts(root) {
287
351
  }
288
352
  }
289
353
  found.push(...contextsFromConfig(root));
354
+ found.push(...contextsFromGeneratedBase(root));
290
355
  return found.sort(
291
356
  (a, b) =>
292
357
  a.context.localeCompare(b.context) || a.source.localeCompare(b.source)
@@ -11,6 +11,9 @@
11
11
  # 4. Deployment environments with required-reviewer approval gates
12
12
  # (lisa-github-environments.sh), from the optional
13
13
  # github.environments block in .lisa.config.json.
14
+ # 5. Policy reconciliation (lisa-reconcile-policy.mjs): compares the DECLARED
15
+ # gate and policy configuration against what GitHub actually has, and
16
+ # converges it when policy.on_drift is `repair`.
14
17
  #
15
18
  # Usage:
16
19
  # lisa-github-repo-setup.sh [options] [project-path]
@@ -38,7 +41,7 @@ while [[ $# -gt 0 ]]; do
38
41
  case $1 in
39
42
  -n|--dry-run) DRY_RUN=true; PASSTHROUGH+=("--dry-run"); shift ;;
40
43
  -v|--verbose) VERBOSE=true; PASSTHROUGH+=("--verbose"); shift ;;
41
- -h|--help) sed -n '2,27p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
44
+ -h|--help) sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
42
45
  -*) echo "Unknown option: $1" >&2; exit 1 ;;
43
46
  *) PROJECT_PATH="$1"; shift ;;
44
47
  esac
@@ -47,15 +50,34 @@ done
47
50
  PROJECT_PATH="${PROJECT_PATH:-.}"
48
51
  PROJECT_PATH="$(cd "$PROJECT_PATH" && pwd)"
49
52
 
50
- echo "==> Step 1/4: repository settings"
53
+ # The reconciler ships as a template AND is installed into host projects. The
54
+ # installed copy is preferred so a project pinned to an older Lisa reconciles
55
+ # with the script it actually has; the template is the fallback so a repository
56
+ # that has not run `lisa apply` yet still gets reconciled. Failing closed when
57
+ # NEITHER resolves is the point — a missing script that exits 0 is how a control
58
+ # reports success while examining nothing.
59
+ resolve_reconciler() {
60
+ local installed="$PROJECT_PATH/scripts/lisa-reconcile-policy.mjs"
61
+ local shipped="$SCRIPT_DIR/../all/copy-overwrite/scripts/lisa-reconcile-policy.mjs"
62
+
63
+ if [[ -f "$installed" ]]; then
64
+ echo "$installed"
65
+ elif [[ -f "$shipped" ]]; then
66
+ echo "$shipped"
67
+ else
68
+ return 1
69
+ fi
70
+ }
71
+
72
+ echo "==> Step 1/5: repository settings"
51
73
  bash "$SCRIPT_DIR/lisa-github-repo-settings.sh" "${PASSTHROUGH[@]}" "$PROJECT_PATH"
52
74
 
53
75
  echo ""
54
- echo "==> Step 2/4: rulesets"
76
+ echo "==> Step 2/5: rulesets"
55
77
  bash "$SCRIPT_DIR/lisa-github-rulesets.sh" --yes "${PASSTHROUGH[@]}" "$PROJECT_PATH"
56
78
 
57
79
  echo ""
58
- echo "==> Step 3/4: deploy key"
80
+ echo "==> Step 3/5: deploy key"
59
81
  if [[ "$DRY_RUN" == "true" ]]; then
60
82
  echo "[DRY RUN] Would ensure a write-access deploy key + DEPLOY_KEY secret exist"
61
83
  else
@@ -73,8 +95,45 @@ else
73
95
  fi
74
96
 
75
97
  echo ""
76
- echo "==> Step 4/4: deployment environments"
98
+ echo "==> Step 4/5: deployment environments"
77
99
  bash "$SCRIPT_DIR/lisa-github-environments.sh" "${PASSTHROUGH[@]}" "$PROJECT_PATH"
78
100
 
101
+ echo ""
102
+ echo "==> Step 5/5: policy reconciliation"
103
+ if ! RECONCILER="$(resolve_reconciler)"; then
104
+ echo "✗ Could not find lisa-reconcile-policy.mjs in $PROJECT_PATH/scripts/ or in this Lisa install" >&2
105
+ exit 1
106
+ fi
107
+
108
+ # Not `[[ ... ]] && cmd`: under `set -e` a false test at top level is a failing
109
+ # command, so on a non-dry run the script would exit right here.
110
+ RECONCILE_ARGS=()
111
+ if [[ "$DRY_RUN" == "true" ]]; then
112
+ RECONCILE_ARGS+=("--dry-run")
113
+ fi
114
+
115
+ # Exit 2 is UNPROVEN: `gh` refused, is missing, or answered something
116
+ # unparseable. A private repository on a plan without rulesets answers 403, and
117
+ # 13 rulesets across the portfolio do exactly that — failing setup for a plan
118
+ # limitation punishes a repository that has done nothing wrong. But a blind gate
119
+ # has to SAY it is blind, which is why this is a named warning and not silence,
120
+ # and why the reconciler keeps 2 as its own code rather than collapsing it into
121
+ # either 0 or 1.
122
+ set +e
123
+ (cd "$PROJECT_PATH" && node "$RECONCILER" "${RECONCILE_ARGS[@]}")
124
+ RECONCILE_STATUS=$?
125
+ set -e
126
+
127
+ case "$RECONCILE_STATUS" in
128
+ 0) ;;
129
+ 2)
130
+ echo "⚠ Policy was NOT checked (UNPROVEN) — see the reason above. Setup continues; nothing about the declared policy has been verified." >&2
131
+ ;;
132
+ *)
133
+ echo "✗ Policy reconciliation failed (exit $RECONCILE_STATUS)" >&2
134
+ exit "$RECONCILE_STATUS"
135
+ ;;
136
+ esac
137
+
79
138
  echo ""
80
139
  echo "✓ GitHub repository governance setup complete"