@codyswann/lisa 3.62.0 → 3.63.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 +167 -5
  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 +6 -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 +14 -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
@@ -1974,12 +1974,32 @@ export const EVIDENCE_DEFAULTS = Object.freeze({
1974
1974
  ]),
1975
1975
  });
1976
1976
 
1977
+ /**
1978
+ * How hard a ruleset bites.
1979
+ *
1980
+ * `evaluate` and `disabled` are not milder versions of `active` — a ruleset in
1981
+ * either state asserts NOTHING, which is why `rulesetSignals` refuses to read
1982
+ * policy off one. Declaring the value is what lets a project say it is running
1983
+ * a ruleset in dry-run on purpose instead of having it silently do nothing.
1984
+ */
1985
+ export const ENFORCEMENTS = Object.freeze(["active", "evaluate", "disabled"]);
1986
+
1977
1987
  /**
1978
1988
  * Repository policy Lisa asserts.
1979
1989
  *
1980
1990
  * Policy differs from a gate in what failure means. A gate failing says stop
1981
1991
  * the change; a policy having drifted says put the setting back. That is why
1982
1992
  * `on_drift` defaults to repair and a gate never does.
1993
+ *
1994
+ * `review` and `ruleset` describe the SHAPE of the branch ruleset Lisa builds,
1995
+ * and they exist because a shipped `all/github-rulesets/base.json` used to
1996
+ * carry them instead. Seven of its fields were already declared here, so two
1997
+ * writers set the same settings and whichever ran last won with no drift
1998
+ * report between them; four more — `bypass_actors`, the `ref_name` conditions,
1999
+ * `required_approving_review_count`, `enforcement` — could not be declared at
2000
+ * all, which made a template value a fleet-wide lock no project could override.
2001
+ * The template is gone and the applier generates its payload from these fields,
2002
+ * so there is one writer and one declaration.
1983
2003
  */
1984
2004
  export const POLICY_SCHEMA = Object.freeze({
1985
2005
  merge: Object.freeze({
@@ -2008,6 +2028,16 @@ export const POLICY_SCHEMA = Object.freeze({
2008
2028
  has_wiki: "boolean",
2009
2029
  default_branch: "string",
2010
2030
  }),
2031
+ review: Object.freeze({
2032
+ required_approving_review_count: "number",
2033
+ require_code_owner_review: "boolean",
2034
+ }),
2035
+ ruleset: Object.freeze({
2036
+ enforcement: ENFORCEMENTS,
2037
+ include_refs: "string[]",
2038
+ exclude_refs: "string[]",
2039
+ bypass_actors: "object[]",
2040
+ }),
2011
2041
  });
2012
2042
 
2013
2043
  /** How to respond when reality has drifted from declared policy. */
@@ -2247,6 +2277,54 @@ export function validateGates(gates) {
2247
2277
  for (const [id, gate] of Object.entries(gates ?? {})) {
2248
2278
  problems.push(...validateGate(id, gate));
2249
2279
  }
2280
+ problems.push(...validateAwaitedContexts(gates));
2281
+ return problems;
2282
+ }
2283
+
2284
+ /**
2285
+ * Refuse two required gates that await one context with different pins.
2286
+ *
2287
+ * Two gates may legitimately be proved by the same external signal, and the
2288
+ * ruleset carries one entry per context — so the payload writer has to collapse
2289
+ * them. Collapsing silently keeps whichever it met first and DISCARDS the other
2290
+ * declaration's `posted_by`, which is how a project ends up requiring a context
2291
+ * pinned to an app it never named. An omitted id is not "no opinion" either: it
2292
+ * means unpinned, GitHub's "any source", which is a different requirement from
2293
+ * a pinned one.
2294
+ *
2295
+ * So an exact duplicate — same context, same pin, including both unpinned — is
2296
+ * fine and collapses. Anything else is refused here, before a payload is built
2297
+ * from it.
2298
+ * @param {object} gates The gates block.
2299
+ * @returns {string[]} Problems.
2300
+ */
2301
+ function validateAwaitedContexts(gates) {
2302
+ const problems = [];
2303
+ const seen = new Map();
2304
+ for (const [id, gate] of Object.entries(gates ?? {})) {
2305
+ if (!gate || typeof gate !== "object" || Array.isArray(gate)) continue;
2306
+ for (const [moment, value] of Object.entries(gate)) {
2307
+ const entry =
2308
+ typeof value === "string" ? { level: value } : (value ?? {});
2309
+ if (entry.level !== "required" || !entry.await) continue;
2310
+ const key = `${moment}\u0000${entry.await}`;
2311
+ const pin = entry.posted_by ?? null;
2312
+ const previous = seen.get(key);
2313
+ if (previous === undefined) {
2314
+ seen.set(key, { id, pin });
2315
+ continue;
2316
+ }
2317
+ if (previous.pin === pin) continue;
2318
+ problems.push(
2319
+ `gates."${id}"."${moment}" and gates."${previous.id}"."${moment}" both ` +
2320
+ `await "${entry.await}" but name different apps ` +
2321
+ `(${JSON.stringify(pin)} vs ${JSON.stringify(previous.pin)}). A ruleset ` +
2322
+ `carries ONE entry per context, so one of these pins would be dropped ` +
2323
+ `without a word — and an omitted posted_by means unpinned, which is a ` +
2324
+ `different requirement from a pinned one, not an absent opinion.`
2325
+ );
2326
+ }
2327
+ }
2250
2328
  return problems;
2251
2329
  }
2252
2330
 
@@ -2397,6 +2475,7 @@ function validateMoment(id, moment, value, known, interceptor, gateRun) {
2397
2475
  }
2398
2476
  problems.push(...validateEvidence(id, moment, entry.evidence ?? {}));
2399
2477
  }
2478
+ problems.push(...validatePostedBy(id, moment, entry));
2400
2479
  if (!known && !interceptor && !entry.await && !entry.run && !gateRun) {
2401
2480
  problems.push(
2402
2481
  `gates."${id}"."${moment}" names no prover and Lisa has no default task ` +
@@ -2406,6 +2485,45 @@ function validateMoment(id, moment, value, known, interceptor, gateRun) {
2406
2485
  return problems;
2407
2486
  }
2408
2487
 
2488
+ /**
2489
+ * Validate the app pin on an awaited signal.
2490
+ *
2491
+ * A required status check can name the ONE app allowed to post it, which is
2492
+ * what stops any other writer satisfying it. The pin therefore has to travel
2493
+ * with the declaration of who posts the signal, not with the ruleset payload —
2494
+ * a shipped template that hardcoded two vendor integration ids is precisely
2495
+ * what this replaces, and it was a fleet-wide lock no project could override.
2496
+ *
2497
+ * Refused on a gate Lisa RUNS, because those are posted by GitHub Actions and
2498
+ * the applier pins them to it already; a second, different pin on the same
2499
+ * context would name an app that can never post it and block every pull
2500
+ * request in the repository forever.
2501
+ * @param {string} id Gate id.
2502
+ * @param {string} moment Moment key.
2503
+ * @param {object} entry The moment entry.
2504
+ * @returns {string[]} Problems.
2505
+ */
2506
+ function validatePostedBy(id, moment, entry) {
2507
+ if (entry.posted_by === undefined) return [];
2508
+ const where = `gates."${id}"."${moment}".posted_by`;
2509
+ if (!entry.await) {
2510
+ return [
2511
+ `${where} names the app that posts a signal, but this moment declares ` +
2512
+ `no await. Lisa posts its own gates through GitHub Actions and pins ` +
2513
+ `them itself; a second pin would name an app that never posts the ` +
2514
+ `context, and a required check nothing can post blocks every pull ` +
2515
+ `request.`,
2516
+ ];
2517
+ }
2518
+ if (!Number.isInteger(entry.posted_by) || entry.posted_by <= 0) {
2519
+ return [
2520
+ `${where} is ${JSON.stringify(entry.posted_by)}; expected the positive ` +
2521
+ `integer GitHub App id that posts "${entry.await}".`,
2522
+ ];
2523
+ }
2524
+ return [];
2525
+ }
2526
+
2409
2527
  /**
2410
2528
  * Validate an awaited signal's evidence block.
2411
2529
  * @param {string} id Gate id.
@@ -2480,6 +2598,49 @@ function validateNeeds(id, gate) {
2480
2598
  return problems;
2481
2599
  }
2482
2600
 
2601
+ /**
2602
+ * Describe why a declared policy value does not fit its declared type.
2603
+ *
2604
+ * The vocabulary grew past `typeof` when the ruleset shape moved into config:
2605
+ * `bypass_actors` is an array of objects and `include_refs` an array of
2606
+ * strings, and `typeof` calls both of them "object", so the original check
2607
+ * would have accepted `"include_refs": {}` — a condition list that silently
2608
+ * matches no branch, which is a ruleset that protects nothing while reading as
2609
+ * configured. An expected value given as an ARRAY is a closed set of literals.
2610
+ * @param {string|readonly string[]} expected The schema entry.
2611
+ * @param {*} value The declared value.
2612
+ * @returns {string|null} The problem, or null when the value fits.
2613
+ */
2614
+ function policyTypeProblem(expected, value) {
2615
+ if (Array.isArray(expected)) {
2616
+ return expected.includes(value)
2617
+ ? null
2618
+ : `must be one of ${expected.join(", ")}, got ${JSON.stringify(value)}`;
2619
+ }
2620
+ if (expected === "string[]" || expected === "object[]") {
2621
+ const member = expected === "string[]" ? "string" : "object";
2622
+ if (!Array.isArray(value)) {
2623
+ return `must be an array of ${member}s, got ${typeof value}`;
2624
+ }
2625
+ const bad = value.findIndex(entry =>
2626
+ member === "string"
2627
+ ? typeof entry !== "string"
2628
+ : !entry || typeof entry !== "object" || Array.isArray(entry)
2629
+ );
2630
+ return bad === -1
2631
+ ? null
2632
+ : `must be an array of ${member}s; entry ${bad} is ${JSON.stringify(value[bad])}`;
2633
+ }
2634
+ if (expected === "number") {
2635
+ return Number.isInteger(value) && value >= 0
2636
+ ? null
2637
+ : `must be a non-negative integer, got ${JSON.stringify(value)}`;
2638
+ }
2639
+ return typeof value === expected
2640
+ ? null
2641
+ : `must be a ${expected}, got ${typeof value}`;
2642
+ }
2643
+
2483
2644
  /**
2484
2645
  * Validate the policy block.
2485
2646
  * @param {object} policy The policy block.
@@ -2522,10 +2683,9 @@ export function validatePolicy(policy) {
2522
2683
  );
2523
2684
  continue;
2524
2685
  }
2525
- if (typeof value !== expected) {
2526
- problems.push(
2527
- `policy.${section}.${field} must be a ${expected}, got ${typeof value}`
2528
- );
2686
+ const wrong = policyTypeProblem(expected, value);
2687
+ if (wrong) {
2688
+ problems.push(`policy.${section}.${field} ${wrong}`);
2529
2689
  }
2530
2690
  }
2531
2691
  }
@@ -2586,7 +2746,7 @@ export function auditConfigKeys(config) {
2586
2746
  * UNKNOWN, and an unknown manifest resolves exactly as it did before this
2587
2747
  * option existed — a caller that has not been taught to read the manifest
2588
2748
  * must not have its answers changed by silence.
2589
- * @returns {Array<{id: string, level: string, mode: string, awaits: string|null, task: string|null, command: string|null, label: string, work: string|null, alias: {from: string, to: string}|null, evidence: {proof: string[], no_work: string[], on_hollow: string, wait_minutes: number|null, on_timeout: string}|null}>} Resolved provers, sorted by gate id.
2749
+ * @returns {Array<{id: string, level: string, mode: string, awaits: string|null, postedBy: number|null, task: string|null, command: string|null, label: string, work: string|null, alias: {from: string, to: string}|null, evidence: {proof: string[], no_work: string[], on_hollow: string, wait_minutes: number|null, on_timeout: string}|null}>} Resolved provers, sorted by gate id.
2590
2750
  */
2591
2751
  export function resolveMoment({
2592
2752
  gates,
@@ -2640,6 +2800,7 @@ export function resolveMoment({
2640
2800
  level: "off",
2641
2801
  mode: "off",
2642
2802
  awaits: null,
2803
+ postedBy: null,
2643
2804
  task: null,
2644
2805
  command: null,
2645
2806
  label: REGISTRY[id]?.label ?? id,
@@ -2682,6 +2843,7 @@ export function resolveMoment({
2682
2843
  level: entry.level,
2683
2844
  mode: entry.await ? "await" : intercepts ? "intercept" : "run",
2684
2845
  awaits: entry.await ?? null,
2846
+ postedBy: entry.await ? (entry.posted_by ?? null) : null,
2685
2847
  task: entry.await || intercepts ? null : task,
2686
2848
  command: entry.await || intercepts || !task ? null : `${runner} ${task}`,
2687
2849
  label: definition?.label ?? id,