@codyswann/lisa 4.74.3 → 4.74.4

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 (62) hide show
  1. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  2. package/dist/core/lisa-owned-hash-ledger.js +2 -0
  3. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  4. package/dist/core/nightly-e2e-guard-behavior-certificate.d.ts +2 -2
  5. package/dist/core/nightly-e2e-guard-behavior-certificate.js +6 -6
  6. package/dist/core/nightly-e2e-guard-behavior-certificate.js.map +1 -1
  7. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  8. package/dist/core/upstream-evidence-manifest.js +4 -2
  9. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  10. package/package.json +4 -4
  11. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  12. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  13. package/plugins/lisa-agy/plugin.json +1 -1
  14. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  17. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-expo-agy/plugin.json +1 -1
  24. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  29. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  34. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  39. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  44. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-rails-agy/plugin.json +1 -1
  49. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  54. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  57. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  58. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  59. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  61. package/scripts/two-channel-couplings.json +1 -1
  62. package/typescript/copy-overwrite/scripts/check-nightly-e2e-health.mjs +227 -41
@@ -1239,6 +1239,7 @@ export function assessSuite(suite, observation, context) {
1239
1239
  label: suite.label,
1240
1240
  workflow: suite.workflow,
1241
1241
  ...(observation.selection ? { selection: observation.selection } : {}),
1242
+ ...(observation.discovery ? { discovery: observation.discovery } : {}),
1242
1243
  };
1243
1244
  const blank = { conclusion: null, url: null, createdAt: null, event: null };
1244
1245
  // An observation with no `scope` at all is the same fact as an artifacts list
@@ -2204,7 +2205,11 @@ export function formatReport(verdict, context) {
2204
2205
  ...verdict.findings.flatMap(finding => {
2205
2206
  const line = `- ${formatFinding(finding)}`;
2206
2207
  const selection = formatSelection(finding.selection);
2207
- return selection === null ? [line] : [line, ` - ${selection}`];
2208
+ return [
2209
+ line,
2210
+ ...(selection === null ? [] : [` - ${selection}`]),
2211
+ ...(finding.discovery ?? []).map(entry => ` - Discovery: ${entry}`),
2212
+ ];
2208
2213
  })
2209
2214
  );
2210
2215
  lines.push("");
@@ -3094,17 +3099,16 @@ export async function apiGet(api, path, wait = sleep) {
3094
3099
  *
3095
3100
  * @param {object} api - API coordinates
3096
3101
  * @param {string} file - Workflow file name
3097
- * @param {string} branch - Branch to read
3098
- * @param {string} event - Run event
3102
+ * @param {string|null} branch - Branch to read; null for client-filtered discovery
3103
+ * @param {string|null} event - Run event; null to omit status/event filters
3099
3104
  * @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
3100
3105
  * @returns {Promise<{runs: ReadonlyArray<object>, missing: boolean}>} The candidates, or a 404 marker
3101
3106
  */
3102
3107
  export async function fetchRunCandidates(api, file, branch, event, wait) {
3103
3108
  const query = new URLSearchParams({
3104
- branch,
3105
- status: "completed",
3106
- event,
3107
- per_page: String(RUN_CANDIDATE_PAGE_SIZE),
3109
+ ...(branch ? { branch } : {}),
3110
+ ...(event ? { status: "completed", event } : {}),
3111
+ per_page: String(event ? RUN_CANDIDATE_PAGE_SIZE : 100),
3108
3112
  });
3109
3113
  const result = await apiGet(
3110
3114
  api,
@@ -3112,12 +3116,191 @@ export async function fetchRunCandidates(api, file, branch, event, wait) {
3112
3116
  wait
3113
3117
  );
3114
3118
  if (result === null) return { runs: Object.freeze([]), missing: true };
3119
+ if (!Array.isArray(result.body.workflow_runs)) {
3120
+ throw staleRunList(file, ["the run list was not an array"]);
3121
+ }
3115
3122
  return {
3116
- runs: Object.freeze(result.body.workflow_runs ?? []),
3123
+ runs: Object.freeze(result.body.workflow_runs),
3117
3124
  missing: false,
3118
3125
  };
3119
3126
  }
3120
3127
 
3128
+ /**
3129
+ * Refuses untrustworthy discovery separately from a suite's actual verdict.
3130
+ * The API-failure path cannot offer a waiver for a suite it never observed.
3131
+ * @param {string} file - Workflow whose discovery could not be established
3132
+ * @param {ReadonlyArray<string>} diagnostics - Raw list and detail observations
3133
+ * @returns {GateApiError} A distinct platform-read refusal
3134
+ */
3135
+ function staleRunList(file, diagnostics) {
3136
+ const error = new GateApiError(
3137
+ `[list_stale] GitHub's run history for ${file} could not establish a current, trustworthy completed run. The platform returned old or conflicting run information; this is not evidence that the nightly suite failed. Re-run this check to retry the platform read.\n\nDiscovery observations:\n${diagnostics.join("\n")}`
3138
+ );
3139
+ error.reason = "list_stale";
3140
+ return error;
3141
+ }
3142
+
3143
+ /**
3144
+ * Checks the fields the server-side filters were supposed to establish.
3145
+ * Branchless discovery has to apply the same scope locally, and direct reads
3146
+ * have to prove that scope independently rather than inherit the list's claim.
3147
+ * @param {object} run - A list entry or single-run payload
3148
+ * @param {string} file - The declared workflow file
3149
+ * @param {string} branch - The declared branch
3150
+ * @returns {boolean} Whether this run is eligible for this workflow and branch
3151
+ */
3152
+ function eligibleDiscoveredRun(run, file, branch) {
3153
+ return (
3154
+ run !== null &&
3155
+ typeof run === "object" &&
3156
+ Number.isSafeInteger(run.id) &&
3157
+ run.id > 0 &&
3158
+ typeof run.created_at === "string" &&
3159
+ Number.isFinite(Date.parse(run.created_at)) &&
3160
+ run.status === "completed" &&
3161
+ run.head_branch === branch &&
3162
+ COUNTED_EVENTS.includes(run.event) &&
3163
+ typeof run.path === "string" &&
3164
+ run.path.split("@")[0] === `.github/workflows/${file}`
3165
+ );
3166
+ }
3167
+
3168
+ /**
3169
+ * Discovers IDs while retaining the newest eligible observation across every
3170
+ * snapshot. Old heads get bounded alternative reads without status/event, then
3171
+ * without branch. A later stale snapshot cannot erase a run already seen.
3172
+ * @param {object} api - API coordinates and bounded retry settings
3173
+ * @param {string} file - Workflow file
3174
+ * @param {string} branch - Branch to inspect
3175
+ * @param {number} freshnessHours - This suite's existing freshness limit
3176
+ * @param {Date} now - Evaluation instant
3177
+ * @param {(ms: number) => Promise<void>} [wait] - Injectable bounded backoff
3178
+ * @returns {Promise<object>} Candidates plus raw discovery diagnostics
3179
+ */
3180
+ async function discoverRunCandidates(
3181
+ api,
3182
+ file,
3183
+ branch,
3184
+ freshnessHours,
3185
+ now,
3186
+ wait = sleep
3187
+ ) {
3188
+ const diagnostics = [];
3189
+ const byId = new Map();
3190
+ const ingest = (result, strategy) => {
3191
+ const raw = result.runs.map(
3192
+ run => `${run?.id ?? "?"}@${run?.created_at ?? "?"}`
3193
+ );
3194
+ const line = `${file} ${strategy}: ${result.missing ? "404" : raw.join(", ") || "empty"}`;
3195
+ diagnostics.push(line);
3196
+ process.stderr.write(`[nightly discovery] ${line}\n`);
3197
+ for (const run of result.runs) {
3198
+ if (!eligibleDiscoveredRun(run, file, branch)) continue;
3199
+ const previous = byId.get(run.id);
3200
+ // An ID's creation time and workflow identity do not change. Disagreement
3201
+ // is an unreadable platform observation, never grounds to overwrite it.
3202
+ if (
3203
+ previous &&
3204
+ (previous.created_at !== run.created_at ||
3205
+ previous.workflow_id !== run.workflow_id ||
3206
+ previous.head_sha !== run.head_sha ||
3207
+ previous.event !== run.event)
3208
+ ) {
3209
+ throw staleRunList(file, [
3210
+ ...diagnostics,
3211
+ `run ${run.id} changed identity between list reads`,
3212
+ ]);
3213
+ }
3214
+ byId.set(run.id, run);
3215
+ }
3216
+ };
3217
+ const sorted = () =>
3218
+ [...byId.values()].sort(
3219
+ (left, right) =>
3220
+ Date.parse(right.created_at) - Date.parse(left.created_at) ||
3221
+ right.id - left.id
3222
+ );
3223
+ const primary = await Promise.all(
3224
+ COUNTED_EVENTS.map(event =>
3225
+ fetchRunCandidates(api, file, branch, event, wait)
3226
+ )
3227
+ );
3228
+ primary.forEach((result, index) =>
3229
+ ingest(result, `filtered ${COUNTED_EVENTS[index]}`)
3230
+ );
3231
+ if (primary.every(result => result.missing))
3232
+ return { missing: true, candidates: [], diagnostics };
3233
+ let candidates = sorted();
3234
+ // One initial filtered snapshot plus at most maxAttempts-1 alternatives.
3235
+ // This shares the existing clamped retry budget instead of adding an
3236
+ // independently configurable source of unbounded merge-time requests.
3237
+ for (
3238
+ let attempt = 1;
3239
+ !isFresh(candidates[0], freshnessHours, now) && attempt < api.maxAttempts;
3240
+ attempt += 1
3241
+ ) {
3242
+ await wait(retryDelayMs({ headers: null }, attempt, api.retryMaxSeconds));
3243
+ const alternateBranch = attempt === 1 ? branch : null;
3244
+ const result = await fetchRunCandidates(
3245
+ api,
3246
+ file,
3247
+ alternateBranch,
3248
+ null,
3249
+ wait
3250
+ );
3251
+ ingest(
3252
+ result,
3253
+ alternateBranch
3254
+ ? "unfiltered events, branch"
3255
+ : "unfiltered events, all branches"
3256
+ );
3257
+ candidates = sorted();
3258
+ }
3259
+ if (
3260
+ !isFresh(candidates[0], freshnessHours, now) &&
3261
+ (candidates.length > 0 || primary.some(result => result.runs.length > 0))
3262
+ ) {
3263
+ throw staleRunList(file, diagnostics);
3264
+ }
3265
+ return { missing: false, candidates, diagnostics };
3266
+ }
3267
+
3268
+ /**
3269
+ * Reads a discovered ID independently before any verdict or cancellation walk.
3270
+ * A single-run 404 or conflict cannot prove that a newer run tested nothing.
3271
+ * @param {object} api - API coordinates
3272
+ * @param {object} candidate - The list's discovery claim
3273
+ * @param {object} discovery - Workflow, branch, diagnostics and detail cache
3274
+ * @param {(ms: number) => Promise<void>} [wait] - Injectable retry sleep
3275
+ * @returns {Promise<object>} Verified single-run payload
3276
+ */
3277
+ async function verifyDiscoveredRun(api, candidate, discovery, wait) {
3278
+ if (discovery.verified.has(candidate.id))
3279
+ return discovery.verified.get(candidate.id);
3280
+ const result = await apiGet(
3281
+ api,
3282
+ `/repos/${api.repo}/actions/runs/${candidate.id}`,
3283
+ wait
3284
+ );
3285
+ const run = result?.body;
3286
+ const matches =
3287
+ eligibleDiscoveredRun(run, discovery.file, discovery.branch) &&
3288
+ run.id === candidate.id &&
3289
+ run.created_at === candidate.created_at &&
3290
+ Number.isSafeInteger(run.workflow_id) &&
3291
+ run.workflow_id === candidate.workflow_id &&
3292
+ run.event === candidate.event &&
3293
+ typeof run.head_sha === "string" &&
3294
+ /^[a-f0-9]{40}$/i.test(run.head_sha) &&
3295
+ run.head_sha === candidate.head_sha;
3296
+ const line = `${discovery.file} detail ${candidate.id}: ${matches ? "verified" : "missing or conflicting"} ${run?.id ?? "?"}@${run?.created_at ?? "?"}`;
3297
+ discovery.diagnostics.push(line);
3298
+ process.stderr.write(`[nightly discovery] ${line}\n`);
3299
+ if (!matches) throw staleRunList(discovery.file, discovery.diagnostics);
3300
+ discovery.verified.set(candidate.id, run);
3301
+ return run;
3302
+ }
3303
+
3121
3304
  /**
3122
3305
  * Every job of a run, paginated to exhaustion.
3123
3306
  *
@@ -3424,8 +3607,8 @@ export async function fetchRunArtifacts(api, runId, wait) {
3424
3607
  * worse one.
3425
3608
  *
3426
3609
  * When nothing conclusive is found inside the window it falls back to the NEWEST
3427
- * candidate — the run the gate scored before this walk existed — so `stale_run`,
3428
- * `indecisive_conclusion` and the rest still fire and the gate still blocks.
3610
+ * verified candidate, so `indecisive_conclusion` and the rest still fire and
3611
+ * the gate still blocks. Old discovery heads are refused before this walk.
3429
3612
  * That fallback is what makes selection unable to invent a pass: it may only
3430
3613
  * ever promote an older fresh, conclusive run over a run that tested nothing.
3431
3614
  *
@@ -3434,14 +3617,23 @@ export async function fetchRunArtifacts(api, runId, wait) {
3434
3617
  * @param {number} freshnessHours - This arm's freshness window
3435
3618
  * @param {Date} now - Evaluation instant
3436
3619
  * @param {(ms: number) => Promise<void>} [wait] - Injectable sleep
3620
+ * @param {object} discovery - Workflow scope, diagnostics and verified detail cache
3437
3621
  * @returns {Promise<{run: object, jobs: ReadonlyArray<object>, jobsComplete: boolean, selection: object}>} The scored run, its jobs, whether that job list was read to its end, and the audit
3438
3622
  */
3439
- async function selectScoredRun(api, candidates, freshnessHours, now, wait) {
3623
+ async function selectScoredRun(
3624
+ api,
3625
+ candidates,
3626
+ freshnessHours,
3627
+ now,
3628
+ wait,
3629
+ discovery
3630
+ ) {
3440
3631
  const jobsById = new Map();
3441
3632
  const walkedPast = [];
3442
3633
  let chosen = null;
3443
- for (const candidate of candidates) {
3444
- if (!isFresh(candidate, freshnessHours, now)) break;
3634
+ for (const listed of candidates) {
3635
+ if (!isFresh(listed, freshnessHours, now)) break;
3636
+ const candidate = await verifyDiscoveredRun(api, listed, discovery, wait);
3445
3637
  const read = await fetchAllJobs(api, candidate.id, wait);
3446
3638
  const { jobs, complete } = read;
3447
3639
  jobsById.set(candidate.id, read);
@@ -3457,7 +3649,8 @@ async function selectScoredRun(api, candidates, freshnessHours, now, wait) {
3457
3649
  totalJobs: jobs.length,
3458
3650
  });
3459
3651
  }
3460
- const run = chosen ?? candidates[0];
3652
+ const run =
3653
+ chosen ?? (await verifyDiscoveredRun(api, candidates[0], discovery, wait));
3461
3654
  const read = jobsById.get(run.id) ?? (await fetchAllJobs(api, run.id, wait));
3462
3655
  const { jobs, complete: jobsComplete } = read;
3463
3656
  // A run cannot be both scored and skipped. When the walk falls back onto a
@@ -3469,8 +3662,8 @@ async function selectScoredRun(api, candidates, freshnessHours, now, wait) {
3469
3662
  // Asked only of the one shape that is ambiguous: a run that reached no
3470
3663
  // verdict of its own yet scored some of its jobs. A decisive run has already
3471
3664
  // said what happened, and a run with nothing decisive tested nothing — both
3472
- // are named without this, so neither pays for it. That keeps the common path
3473
- // at exactly the request count it had before.
3665
+ // are named without this, so neither pays for an annotation read. Independent
3666
+ // run-detail verification still applies on the common path.
3474
3667
  const timedOut =
3475
3668
  jobsComplete &&
3476
3669
  !DECISIVE_CONCLUSIONS.has(run.conclusion ?? null) &&
@@ -3509,31 +3702,18 @@ async function selectScoredRun(api, candidates, freshnessHours, now, wait) {
3509
3702
  export async function observe(api, suites, branch, context, wait) {
3510
3703
  return await Promise.all(
3511
3704
  suites.map(async suite => {
3512
- const perEvent = await Promise.all(
3513
- COUNTED_EVENTS.map(event =>
3514
- fetchRunCandidates(api, suite.workflow, branch, event, wait)
3515
- )
3705
+ const freshnessHours = suite.freshness_hours ?? context.freshnessHours;
3706
+ const { missing, candidates, diagnostics } = await discoverRunCandidates(
3707
+ api,
3708
+ suite.workflow,
3709
+ branch,
3710
+ freshnessHours,
3711
+ context.now,
3712
+ wait
3516
3713
  );
3517
- if (perEvent.every(result => result.missing)) {
3714
+ if (missing) {
3518
3715
  return { workflowMissing: true, run: null, jobs: [] };
3519
3716
  }
3520
- // Newest FIRST by created_at, so a fresh dispatch supersedes an older
3521
- // failed schedule. ISO-8601 UTC compares correctly as strings.
3522
- const candidates = perEvent
3523
- .flatMap(result => result.runs)
3524
- .filter(
3525
- candidate => candidate && typeof candidate.created_at === "string"
3526
- )
3527
- // Equal timestamps compare 0 rather than falling through to -1, which
3528
- // would make the sort unstable and let two runs created in the same
3529
- // second swap places between reads of the same history.
3530
- .sort((left, right) =>
3531
- left.created_at === right.created_at
3532
- ? 0
3533
- : left.created_at < right.created_at
3534
- ? 1
3535
- : -1
3536
- );
3537
3717
  // Jobs are read for EVERY match mode, `run` included. A run-scoped suite
3538
3718
  // needs them to prove the run was complete (row 26) — a `success` run
3539
3719
  // that skipped half its jobs is not evidence about the half it skipped.
@@ -3543,9 +3723,10 @@ export async function observe(api, suites, branch, context, wait) {
3543
3723
  const { run, jobs, jobsComplete, selection } = await selectScoredRun(
3544
3724
  api,
3545
3725
  candidates,
3546
- suite.freshness_hours ?? context.freshnessHours,
3726
+ freshnessHours,
3547
3727
  context.now,
3548
- wait
3728
+ wait,
3729
+ { file: suite.workflow, branch, diagnostics, verified: new Map() }
3549
3730
  );
3550
3731
  // Artifacts are read for every suite, not only the ones declaring
3551
3732
  // `min_flows`: a run that recorded ITSELF as filtered disqualifies with no
@@ -3559,6 +3740,7 @@ export async function observe(api, suites, branch, context, wait) {
3559
3740
  jobs,
3560
3741
  jobsComplete,
3561
3742
  selection,
3743
+ discovery: Object.freeze([...diagnostics]),
3562
3744
  scope: readSuiteScope(artifacts),
3563
3745
  };
3564
3746
  })
@@ -4749,7 +4931,11 @@ async function main(argv) {
4749
4931
  const failure = {
4750
4932
  verdict: "fail",
4751
4933
  blocked: true,
4752
- error: { kind, message },
4934
+ error: {
4935
+ kind,
4936
+ message,
4937
+ ...(error.reason ? { reason: error.reason } : {}),
4938
+ },
4753
4939
  findings: [],
4754
4940
  };
4755
4941
  // A downstream job reading `verdict`/`blocked` must see the gate's OWN