@codyswann/lisa 3.61.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.
- package/all/copy-overwrite/scripts/lisa-gates.mjs +167 -5
- package/all/copy-overwrite/scripts/lisa-reconcile-policy.mjs +325 -32
- package/dist/cli/gate-report-templates.js +1 -1
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +6 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +14 -9
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/health/ruleset-inspection.d.ts.map +1 -1
- package/dist/health/ruleset-inspection.js +73 -6
- package/dist/health/ruleset-inspection.js.map +1 -1
- package/package.json +2 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-doctor/SKILL.md +46 -7
- package/plugins/lisa/.codex-plugin/skills/lisa-setup-github-repo/SKILL.md +7 -3
- package/plugins/lisa/skills/lisa-doctor/SKILL.md +46 -7
- package/plugins/lisa/skills/lisa-setup-github-repo/SKILL.md +7 -3
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-doctor/SKILL.md +46 -7
- package/plugins/lisa-agy/skills/lisa-setup-github-repo/SKILL.md +7 -3
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-doctor/SKILL.md +46 -7
- package/plugins/lisa-copilot/skills/lisa-setup-github-repo/SKILL.md +7 -3
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-doctor/SKILL.md +46 -7
- package/plugins/lisa-cursor/skills/lisa-setup-github-repo/SKILL.md +7 -3
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-doctor/SKILL.md +46 -7
- package/plugins/src/base/skills/lisa-setup-github-repo/SKILL.md +7 -3
- package/scripts/check-required-check-promotions.mjs +69 -4
- package/scripts/lisa-github-repo-setup.sh +64 -5
- package/scripts/lisa-github-rulesets.sh +201 -21
- package/scripts/lisa-ruleset-payload.mjs +376 -0
- 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
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
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,
|